-- Reconstructed from the production client and retained SQL, 2026-09-17. -- Empty-project recovery only; original v17 foundation was not in Git history. create table public.sun_v17_orders ( workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, order_id text not null, data jsonb not null, version bigint not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), updated_by uuid references auth.users(id) on delete set null, primary key(workspace_id,order_id) ); create table public.sun_v17_catalog_items ( workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, item_id text not null, data jsonb not null, version bigint not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), updated_by uuid references auth.users(id) on delete set null, primary key(workspace_id,item_id) ); create table public.sun_v17_clients ( workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, client_key text not null,name text,phone text,latest_address text,data jsonb not null default '{}', version bigint not null default 1,created_at timestamptz not null default now(),updated_at timestamptz not null default now(), primary key(workspace_id,client_key) ); create table public.sun_v17_settings ( workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, key text not null,value jsonb,version bigint not null default 1,updated_at timestamptz not null default now(), primary key(workspace_id,key) ); create table public.sun_v17_workspace_meta ( workspace_id uuid primary key references public.sun_workspaces(id) on delete cascade, schema_version integer not null default 17,last_backup_on date,legacy_revision bigint not null default 0, migrated_at timestamptz not null default now(),updated_at timestamptz not null default now() ); create table public.sun_v17_change_events ( id bigint generated by default as identity primary key, workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, entity text not null,entity_key text,operation text not null,version bigint,client_id text, created_by uuid references auth.users(id) on delete set null,created_at timestamptz not null default now() ); create index sun_v17_changes_workspace_id_idx on public.sun_v17_change_events(workspace_id,id); create table public.sun_v17_backups ( id uuid primary key default gen_random_uuid(),workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, kind text not null,label text not null default '',snapshot jsonb not null, created_by uuid references auth.users(id) on delete set null,created_at timestamptz not null default now() ); create index sun_v17_backups_workspace_created_idx on public.sun_v17_backups(workspace_id,created_at desc); create table public.sun_v17_error_events ( id uuid primary key default gen_random_uuid(),workspace_id uuid references public.sun_workspaces(id) on delete cascade, user_id uuid references auth.users(id) on delete set null,client_id text,app_version text,level text,message text, stack text,context jsonb not null default '{}',created_at timestamptz not null default now() ); create index sun_v17_errors_workspace_created_idx on public.sun_v17_error_events(workspace_id,created_at desc); create table public.caterium_company_owner_invites ( token uuid primary key default gen_random_uuid(),workspace_id uuid not null references public.sun_workspaces(id) on delete cascade, email text not null,created_at timestamptz not null default now(),expires_at timestamptz not null default now()+interval '7 days', used_at timestamptz,used_by uuid references auth.users(id) on delete set null ); do $$ declare t text; begin foreach t in array array['sun_v17_orders','sun_v17_catalog_items','sun_v17_clients','sun_v17_settings','sun_v17_workspace_meta','sun_v17_change_events','sun_v17_backups','sun_v17_error_events','caterium_company_owner_invites'] loop execute format('alter table public.%I enable row level security',t); execute format('revoke all on public.%I from anon,authenticated',t); end loop; end $$; -- Browser data access uses the permission-checked RPCs. Realtime only reveals an event. grant select on public.sun_v17_change_events to authenticated; create policy sun_v17_change_read on public.sun_v17_change_events for select to authenticated using(public.sun_member_role(workspace_id) is not null); alter publication supabase_realtime add table public.sun_v17_change_events; create function public.sun_my_workspaces() returns table(id uuid,name text,role text,display_name text,is_active boolean,permissions jsonb) language sql stable security definer set search_path=public as $$ select w.id,w.name,m.role,m.display_name,m.is_active,public.sun_role_default_permissions(m.role)||coalesce(m.permissions,'{}') from public.sun_workspace_members m join public.sun_workspaces w on w.id=m.workspace_id where m.user_id=auth.uid() and m.is_active order by w.created_at,w.id $$; create function public.sun_v17_mirror_legacy(p_workspace uuid,p_payload jsonb,p_client_id text default null) returns void language plpgsql security definer set search_path=public as $$ declare canonical jsonb; r record; v_data jsonb; v_ids text[]; v_key text; v_rev bigint; begin if public.sun_member_role(p_workspace) is null and not public.sun_is_platform_admin() then raise exception 'Access denied'; end if; perform 1 from public.sun_workspaces where id=p_workspace for update; select payload,revision into canonical,v_rev from public.sun_app_state where workspace_id=p_workspace; if canonical is null then return; end if; -- Always mirror the validated server row. The supplied legacy argument is never trusted. for r in select * from (values ('sunOrders','sun_v17_orders','order_id','order'),('sunBoxes','sun_v17_catalog_items','item_id','catalog')) as x(storage_key,table_name,id_column,entity) loop v_data:=coalesce(canonical#>array['storage',r.storage_key,'v'],'[]'::jsonb); if jsonb_typeof(v_data)<>'array' then raise exception 'Invalid entity array: %',r.storage_key; end if; if exists(select 1 from jsonb_array_elements(v_data) e where nullif(e->>'id','') is null) then raise exception 'Entity ID is required'; end if; if (select count(*) from jsonb_array_elements(v_data))<>(select count(distinct e->>'id') from jsonb_array_elements(v_data) e) then raise exception 'Duplicate entity ID'; end if; select coalesce(array_agg(e->>'id'),'{}') into v_ids from jsonb_array_elements(v_data) e; execute format('with gone as (delete from public.%I where workspace_id=$1 and not (%I=any($2)) returning %I,version) insert into public.sun_v17_change_events(workspace_id,entity,entity_key,operation,version,client_id,created_by) select $1,$3,%I,''delete'',version+1,$4,auth.uid() from gone',r.table_name,r.id_column,r.id_column,r.id_column) using p_workspace,v_ids,r.entity,p_client_id; execute format('with saved as (insert into public.%I as dest(workspace_id,%I,data,updated_by) select $1,e->>''id'',e,auth.uid() from jsonb_array_elements($2) e on conflict(workspace_id,%I) do update set data=excluded.data,version=dest.version+1,updated_at=now(),updated_by=auth.uid() where dest.data is distinct from excluded.data returning %I,version) insert into public.sun_v17_change_events(workspace_id,entity,entity_key,operation,version,client_id,created_by) select $1,$3,%I,''upsert'',version,$4,auth.uid() from saved',r.table_name,r.id_column,r.id_column,r.id_column,r.id_column) using p_workspace,v_data,r.entity,p_client_id; end loop; delete from public.sun_v17_settings where workspace_id=p_workspace and not (canonical->'storage' ? key); insert into public.sun_v17_settings as dest(workspace_id,key,value) select p_workspace,key,value from jsonb_each(coalesce(canonical->'storage','{}')) where key not in ('sunOrders','sunBoxes') on conflict(workspace_id,key) do update set value=excluded.value,version=dest.version+1,updated_at=now() where dest.value is distinct from excluded.value; insert into public.sun_v17_workspace_meta(workspace_id,legacy_revision) values(p_workspace,v_rev) on conflict(workspace_id) do update set legacy_revision=excluded.legacy_revision,updated_at=now(); end $$; create function public.sun_save_app_state_v17(p_workspace uuid,p_payload jsonb,p_client_id text,p_expected_revision bigint default null) returns table(workspace_id uuid,payload jsonb,revision bigint,updated_at timestamptz,client_id text) language plpgsql security definer set search_path=public as $$ declare current_revision bigint; begin if public.sun_member_role(p_workspace) is null then raise exception 'Access denied'; end if; perform 1 from public.sun_workspaces w where w.id=p_workspace for update; select s.revision into current_revision from public.sun_app_state s where s.workspace_id=p_workspace; if p_expected_revision is not null and coalesce(current_revision,0)<>p_expected_revision then raise exception using errcode='40001',message=format('SUN_CONFLICT expected=%s actual=%s',p_expected_revision,coalesce(current_revision,0)); end if; perform public.sun_save_app_state(p_workspace,p_payload,p_client_id); perform public.sun_v17_mirror_legacy(p_workspace,p_payload,p_client_id); return query select * from public.sun_fetch_app_state(p_workspace); end $$; create function public.sun_v17_build_snapshot(p_workspace uuid) returns jsonb language sql stable security definer set search_path=public as $$ select jsonb_build_object('version',17,'legacy',payload,'revision',revision,'created_at',now()) from public.sun_app_state where workspace_id=p_workspace $$; create function public.sun_v17_prune_backups(p_workspace uuid,p_keep integer default 30) returns integer language plpgsql security definer set search_path=public as $$ declare n integer; begin if public.sun_member_role(p_workspace) is null and not public.sun_is_platform_admin() then raise exception 'Access denied'; end if; delete from public.sun_v17_backups where workspace_id=p_workspace and kind='daily' and id in (select id from public.sun_v17_backups where workspace_id=p_workspace and kind='daily' order by created_at desc offset greatest(30,coalesce(p_keep,30))); get diagnostics n=row_count; return n; end $$; create function public.sun_workspace_access_mode_internal_v28(p_workspace uuid) returns text language sql stable security definer set search_path=public as $$select public.sun_subscription_access_mode(p_workspace)$$; create function public.sun_workspace_feature_internal_v28(p_workspace uuid,p_feature text) returns boolean language sql stable security definer set search_path=public as $$select public.sun_workspace_has_feature(p_workspace,p_feature)$$; revoke all on function public.sun_v17_build_snapshot(uuid),public.sun_workspace_access_mode_internal_v28(uuid),public.sun_workspace_feature_internal_v28(uuid,text) from public,anon,authenticated; revoke all on function public.sun_my_workspaces(),public.sun_save_app_state_v17(uuid,jsonb,text,bigint),public.sun_v17_mirror_legacy(uuid,jsonb,text),public.sun_v17_prune_backups(uuid,integer) from public,anon; grant execute on function public.sun_my_workspaces(),public.sun_save_app_state_v17(uuid,jsonb,text,bigint),public.sun_v17_mirror_legacy(uuid,jsonb,text),public.sun_v17_prune_backups(uuid,integer) to authenticated;