Spaces:
Sleeping
Sleeping
| -- Content Studio Phase 2: authoritative editor state and project render jobs. | |
| -- Apply after projects/0001_projects_foundation.sql, projects/0002_project_resources.sql, | |
| -- and security generation migrations. Production migration remains explicit. | |
| begin; | |
| create table if not exists project_editor_states ( | |
| id text primary key default gen_random_uuid()::text, | |
| workspace_id text not null references workspaces(id) on delete restrict, | |
| project_id text not null references projects(id) on delete restrict, | |
| revision integer not null default 1 check (revision > 0), | |
| schema_version integer not null check (schema_version = 1), | |
| state jsonb not null check (jsonb_typeof(state) = 'object'), | |
| created_at timestamptz not null default now(), | |
| updated_at timestamptz not null default now(), | |
| updated_by text not null references users(id) on delete restrict, | |
| constraint uq_project_editor_state_project unique (project_id) | |
| ); | |
| create index if not exists ix_project_editor_states_workspace_project | |
| on project_editor_states(workspace_id, project_id); | |
| create table if not exists project_render_jobs ( | |
| id text primary key default gen_random_uuid()::text, | |
| workspace_id text not null references workspaces(id) on delete restrict, | |
| project_id text not null references projects(id) on delete restrict, | |
| editor_revision integer not null check (editor_revision > 0), | |
| editor_schema_version integer not null check (editor_schema_version = 1), | |
| editor_state jsonb not null check (jsonb_typeof(editor_state) = 'object'), | |
| render_settings jsonb not null check (jsonb_typeof(render_settings) = 'object'), | |
| request_fingerprint text not null check (char_length(request_fingerprint) = 64), | |
| idempotency_key text not null check (char_length(idempotency_key) between 1 and 255), | |
| requested_by text not null references users(id) on delete restrict, | |
| status text not null default 'queued' | |
| check (status in ('queued', 'processing', 'completed', 'failed', 'cancelling', 'cancelled')), | |
| attempt_count integer not null default 0 check (attempt_count >= 0), | |
| max_attempts integer not null default 3 check (max_attempts between 1 and 10), | |
| next_attempt_at timestamptz, | |
| output_asset_id text references media_assets(id) on delete restrict, | |
| error_code text, | |
| error_message text, | |
| created_at timestamptz not null default now(), | |
| started_at timestamptz, | |
| completed_at timestamptz, | |
| cancelled_at timestamptz, | |
| updated_at timestamptz not null default now(), | |
| constraint uq_project_render_idempotency unique (project_id, editor_revision, idempotency_key) | |
| ); | |
| create index if not exists ix_project_render_jobs_workspace_status | |
| on project_render_jobs(workspace_id, status); | |
| create index if not exists ix_project_render_jobs_project_created | |
| on project_render_jobs(project_id, created_at desc); | |
| create index if not exists ix_project_render_jobs_dispatch | |
| on project_render_jobs(status, next_attempt_at); | |
| drop trigger if exists mediarouter_project_editor_touch_updated_at on project_editor_states; | |
| create trigger mediarouter_project_editor_touch_updated_at | |
| before update on project_editor_states | |
| for each row execute function mediarouter_tenant_touch_updated_at(); | |
| drop trigger if exists mediarouter_project_render_touch_updated_at on project_render_jobs; | |
| create trigger mediarouter_project_render_touch_updated_at | |
| before update on project_render_jobs | |
| for each row execute function mediarouter_tenant_touch_updated_at(); | |
| create or replace function mediarouter_assert_project_editor_ownership() | |
| returns trigger language plpgsql as $$ | |
| declare project_workspace text; | |
| declare project_status text; | |
| begin | |
| if tg_op = 'INSERT' and new.revision <> 1 then | |
| raise exception 'initial editor revision must be one' using errcode = '23514'; | |
| end if; | |
| if tg_op = 'UPDATE' and ( | |
| new.workspace_id is distinct from old.workspace_id | |
| or new.project_id is distinct from old.project_id | |
| or new.created_at is distinct from old.created_at | |
| ) then | |
| raise exception 'project editor ownership fields are immutable' using errcode = '23514'; | |
| end if; | |
| if tg_op = 'UPDATE' and new.revision <> old.revision + 1 then | |
| raise exception 'editor revision must increment exactly once' using errcode = '23514'; | |
| end if; | |
| if new.state->>'projectId' is distinct from new.project_id | |
| or new.state->>'schemaVersion' is distinct from new.schema_version::text then | |
| raise exception 'editor state identity does not match its row' using errcode = '23514'; | |
| end if; | |
| select workspace_id, status into project_workspace, project_status from projects where id = new.project_id; | |
| if project_workspace is null or project_workspace is distinct from new.workspace_id then | |
| raise exception 'editor state must belong to its project workspace' using errcode = '23503'; | |
| end if; | |
| if project_status is distinct from 'active' then | |
| raise exception 'archived projects cannot change editor state' using errcode = '23514'; | |
| end if; | |
| if not exists ( | |
| select 1 from workspace_memberships m | |
| where m.workspace_id = new.workspace_id and m.user_id = new.updated_by and m.status = 'active' | |
| ) then | |
| raise exception 'editor state update requires active workspace membership' using errcode = '23503'; | |
| end if; | |
| return new; | |
| end; | |
| $$; | |
| drop trigger if exists mediarouter_project_editor_ownership on project_editor_states; | |
| create trigger mediarouter_project_editor_ownership | |
| before insert or update on project_editor_states | |
| for each row execute function mediarouter_assert_project_editor_ownership(); | |
| create or replace function mediarouter_assert_project_render_ownership() | |
| returns trigger language plpgsql as $$ | |
| declare project_workspace text; | |
| declare project_status text; | |
| declare output_workspace text; | |
| declare output_project text; | |
| begin | |
| if tg_op = 'UPDATE' and ( | |
| new.workspace_id is distinct from old.workspace_id | |
| or new.project_id is distinct from old.project_id | |
| or new.editor_revision is distinct from old.editor_revision | |
| or new.editor_schema_version is distinct from old.editor_schema_version | |
| or new.editor_state is distinct from old.editor_state | |
| or new.render_settings is distinct from old.render_settings | |
| or new.request_fingerprint is distinct from old.request_fingerprint | |
| or new.idempotency_key is distinct from old.idempotency_key | |
| or new.requested_by is distinct from old.requested_by | |
| or new.max_attempts is distinct from old.max_attempts | |
| or new.created_at is distinct from old.created_at | |
| ) then | |
| raise exception 'render identity fields are immutable' using errcode = '23514'; | |
| end if; | |
| if new.editor_state->>'projectId' is distinct from new.project_id | |
| or new.editor_state->>'schemaVersion' is distinct from new.editor_schema_version::text then | |
| raise exception 'render editor snapshot identity does not match its row' using errcode = '23514'; | |
| end if; | |
| if tg_op = 'UPDATE' and new.status is distinct from old.status and not ( | |
| (old.status = 'queued' and new.status in ('processing', 'cancelled', 'failed')) | |
| or (old.status = 'processing' and new.status in ('queued', 'completed', 'failed', 'cancelling', 'cancelled')) | |
| or (old.status = 'cancelling' and new.status in ('completed', 'failed', 'cancelled')) | |
| ) then | |
| raise exception 'invalid render status transition' using errcode = '23514'; | |
| end if; | |
| select workspace_id, status into project_workspace, project_status from projects where id = new.project_id; | |
| if project_workspace is null or project_workspace is distinct from new.workspace_id then | |
| raise exception 'render must belong to its project workspace' using errcode = '23503'; | |
| end if; | |
| if tg_op = 'INSERT' and project_status is distinct from 'active' then | |
| raise exception 'archived projects cannot be rendered' using errcode = '23514'; | |
| end if; | |
| if new.output_asset_id is not null then | |
| select workspace_id, project_id into output_workspace, output_project | |
| from media_assets where id = new.output_asset_id; | |
| if output_workspace is null | |
| or output_workspace is distinct from new.workspace_id | |
| or output_project is distinct from new.project_id then | |
| raise exception 'render output must belong to its project workspace' using errcode = '23503'; | |
| end if; | |
| end if; | |
| if new.status = 'completed' and (new.output_asset_id is null or new.completed_at is null) then | |
| raise exception 'completed render requires an output asset and timestamp' using errcode = '23514'; | |
| end if; | |
| if new.status = 'cancelled' and (new.cancelled_at is null or new.completed_at is null) then | |
| raise exception 'cancelled render requires cancellation timestamps' using errcode = '23514'; | |
| end if; | |
| if new.status = 'failed' and new.completed_at is null then | |
| raise exception 'failed render requires a completion timestamp' using errcode = '23514'; | |
| end if; | |
| if new.status in ('queued', 'processing', 'cancelling') and new.output_asset_id is not null then | |
| raise exception 'active render cannot expose an output asset' using errcode = '23514'; | |
| end if; | |
| if tg_op = 'INSERT' and not exists ( | |
| select 1 from workspace_memberships m | |
| where m.workspace_id = new.workspace_id and m.user_id = new.requested_by and m.status = 'active' | |
| ) then | |
| raise exception 'render requires active workspace membership' using errcode = '23503'; | |
| end if; | |
| return new; | |
| end; | |
| $$; | |
| drop trigger if exists mediarouter_project_render_ownership on project_render_jobs; | |
| create trigger mediarouter_project_render_ownership | |
| before insert or update on project_render_jobs | |
| for each row execute function mediarouter_assert_project_render_ownership(); | |
| alter table project_editor_states enable row level security; | |
| alter table project_editor_states force row level security; | |
| drop policy if exists project_editor_states_select on project_editor_states; | |
| create policy project_editor_states_select on project_editor_states for select using ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active') | |
| ); | |
| drop policy if exists project_editor_states_insert on project_editor_states; | |
| create policy project_editor_states_insert on project_editor_states for insert with check ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and updated_by = current_setting('app.user_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ); | |
| drop policy if exists project_editor_states_update on project_editor_states; | |
| create policy project_editor_states_update on project_editor_states for update using ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ) with check ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and updated_by = current_setting('app.user_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_editor_states.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ); | |
| alter table project_render_jobs enable row level security; | |
| alter table project_render_jobs force row level security; | |
| drop policy if exists project_render_jobs_select on project_render_jobs; | |
| create policy project_render_jobs_select on project_render_jobs for select using ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active') | |
| ); | |
| drop policy if exists project_render_jobs_insert on project_render_jobs; | |
| create policy project_render_jobs_insert on project_render_jobs for insert with check ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and requested_by = current_setting('app.user_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ); | |
| drop policy if exists project_render_jobs_update on project_render_jobs; | |
| create policy project_render_jobs_update on project_render_jobs for update using ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ) with check ( | |
| workspace_id = current_setting('app.workspace_id', true) | |
| and exists (select 1 from workspace_memberships m where m.workspace_id = project_render_jobs.workspace_id | |
| and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer') | |
| ); | |
| commit; | |