Spaces:
Sleeping
Sleeping
File size: 13,116 Bytes
3493993 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | -- 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;
|