Files
gongxue-base/supabase/migrations/202606300007_public_bank_saas_scope.sql
2026-06-30 08:55:22 +08:00

236 lines
8.4 KiB
PL/PgSQL

create or replace function app.uuid_array_from_jsonb(value jsonb)
returns uuid[]
language sql
immutable
as $$
select coalesce(array_agg(item::uuid), '{}'::uuid[])
from jsonb_array_elements_text(
case when jsonb_typeof(value) = 'array' then value else '[]'::jsonb end
) as t(item)
where item ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
$$;
create or replace function app.text_array_from_jsonb(value jsonb)
returns text[]
language sql
immutable
as $$
select coalesce(array_agg(item), '{}'::text[])
from jsonb_array_elements_text(
case when jsonb_typeof(value) = 'array' then value else '[]'::jsonb end
) as t(item)
where length(item) between 1 and 160
$$;
create or replace function app.public_question_bank_grant_allows(
grant_allowed_region_ids uuid[],
grant_allowed_subject_ids uuid[],
source_region_id uuid,
source_subject_ids uuid[]
)
returns boolean
language sql
immutable
as $$
select
(
coalesce(array_length(grant_allowed_region_ids, 1), 0) = 0
or source_region_id = any(grant_allowed_region_ids)
)
and (
coalesce(array_length(grant_allowed_subject_ids, 1), 0) = 0
or coalesce(array_length(source_subject_ids, 1), 0) = 0
or source_subject_ids <@ grant_allowed_subject_ids
)
$$;
create or replace function app.public_question_bank_subscription_allows(
subscription_metadata jsonb,
plan_feature_flags jsonb,
source_question_bank_id uuid,
source_region_id uuid,
source_subject_ids uuid[]
)
returns boolean
language plpgsql
immutable
as $$
declare
plan_cfg jsonb := coalesce(plan_feature_flags #> '{publicQuestionBanks}', '{}'::jsonb);
subscription_cfg jsonb := coalesce(subscription_metadata #> '{publicQuestionBankAccess}', '{}'::jsonb);
plan_region_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedRegionIds');
subscription_region_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedRegionIds');
plan_subject_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedSubjectIds');
subscription_subject_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedSubjectIds');
plan_bank_ids uuid[] := app.uuid_array_from_jsonb(plan_cfg->'allowedQuestionBankIds');
subscription_bank_ids uuid[] := app.uuid_array_from_jsonb(subscription_cfg->'allowedQuestionBankIds');
mode text := coalesce(subscription_cfg->>'mode', plan_cfg->>'mode', 'all');
requires_region_allowlist boolean :=
case coalesce(subscription_cfg->>'requiresRegionAllowlist', plan_cfg->>'requiresRegionAllowlist')
when 'true' then true
when 'false' then false
else false
end;
allow_all_regions boolean := (
case coalesce(subscription_cfg->>'allowAllRegions', plan_cfg->>'allowAllRegions')
when 'true' then true
when 'false' then false
else false
end
) or mode in ('all', 'all_regions', 'national', 'unlimited');
begin
if coalesce(subscription_cfg->>'enabled', plan_cfg->>'enabled', 'true') = 'false' then
return false;
end if;
if coalesce(array_length(plan_bank_ids, 1), 0) > 0 and not source_question_bank_id = any(plan_bank_ids) then
return false;
end if;
if coalesce(array_length(subscription_bank_ids, 1), 0) > 0 and not source_question_bank_id = any(subscription_bank_ids) then
return false;
end if;
if not allow_all_regions and source_region_id is not null then
if requires_region_allowlist
and coalesce(array_length(plan_region_ids, 1), 0) = 0
and coalesce(array_length(subscription_region_ids, 1), 0) = 0 then
return false;
end if;
if coalesce(array_length(plan_region_ids, 1), 0) > 0 and not source_region_id = any(plan_region_ids) then
return false;
end if;
if coalesce(array_length(subscription_region_ids, 1), 0) > 0 and not source_region_id = any(subscription_region_ids) then
return false;
end if;
end if;
if coalesce(array_length(plan_subject_ids, 1), 0) > 0
and coalesce(array_length(source_subject_ids, 1), 0) > 0
and not source_subject_ids <@ plan_subject_ids then
return false;
end if;
if coalesce(array_length(subscription_subject_ids, 1), 0) > 0
and coalesce(array_length(source_subject_ids, 1), 0) > 0
and not source_subject_ids <@ subscription_subject_ids then
return false;
end if;
return true;
end;
$$;
create index if not exists idx_question_bank_grants_allowed_regions
on public.question_bank_grants using gin(allowed_region_ids);
create index if not exists idx_question_bank_grants_allowed_subjects
on public.question_bank_grants using gin(allowed_subject_ids);
update public.platform_saas_plans
set feature_flags = jsonb_set(
feature_flags,
'{publicQuestionBanks}',
'{"enabled":true,"mode":"limited_regions","requiresRegionAllowlist":true,"maxRegionCount":1}'::jsonb,
true
),
updated_at = now()
where code = 'starter_yearly';
update public.platform_saas_plans
set feature_flags = jsonb_set(
feature_flags,
'{publicQuestionBanks}',
'{"enabled":true,"mode":"national","allowAllRegions":true}'::jsonb,
true
),
updated_at = now()
where code = 'pro_yearly';
drop policy if exists question_bank_grants_eligible_tenant_read on public.question_bank_grants;
create policy question_bank_grants_eligible_tenant_read on public.question_bank_grants
for select
using (
status = 'active'
and (starts_at is null or starts_at <= now())
and (expires_at is null or expires_at > now())
and exists (
select 1
from public.question_banks qb
left join lateral (
select array_agg(distinct q.subject_id) filter (where q.subject_id is not null) as subject_ids
from public.questions q
where q.question_bank_id = qb.id
and q.status = 'published'
) ss on true
where qb.id = source_question_bank_id
and qb.source_scope = 'platform'
and qb.status = 'active'
and app.public_question_bank_grant_allows(
allowed_region_ids,
allowed_subject_ids,
qb.region_id,
coalesce(ss.subject_ids, '{}'::uuid[])
)
and (
(
grant_scope = 'all_active_tenants'
and exists (
select 1
from public.tenant_subscriptions ts
left join public.platform_saas_plans p on p.code = ts.plan_code
where ts.tenant_id = app.current_tenant_id()
and ts.status in ('trial', 'active')
and (ts.expires_at is null or ts.expires_at > now())
and app.public_question_bank_subscription_allows(
ts.metadata,
coalesce(p.feature_flags, '{}'::jsonb),
qb.id,
qb.region_id,
coalesce(ss.subject_ids, '{}'::uuid[])
)
)
)
or (
grant_scope in ('plans', 'mixed')
and exists (
select 1
from public.tenant_subscriptions ts
left join public.platform_saas_plans p on p.code = ts.plan_code
where ts.tenant_id = app.current_tenant_id()
and ts.status in ('trial', 'active')
and (ts.expires_at is null or ts.expires_at > now())
and ts.plan_code = any(allowed_plan_codes)
and app.public_question_bank_subscription_allows(
ts.metadata,
coalesce(p.feature_flags, '{}'::jsonb),
qb.id,
qb.region_id,
coalesce(ss.subject_ids, '{}'::uuid[])
)
)
)
or (
grant_scope in ('tenants', 'mixed')
and app.current_tenant_id() = any(allowed_tenant_ids)
and (
metadata->>'requiresActiveSubscription' = 'false'
or exists (
select 1
from public.tenant_subscriptions ts
left join public.platform_saas_plans p on p.code = ts.plan_code
where ts.tenant_id = app.current_tenant_id()
and ts.status in ('trial', 'active')
and (ts.expires_at is null or ts.expires_at > now())
and app.public_question_bank_subscription_allows(
ts.metadata,
coalesce(p.feature_flags, '{}'::jsonb),
qb.id,
qb.region_id,
coalesce(ss.subject_ids, '{}'::uuid[])
)
)
)
)
)
)
);