Spaces:
Running
Running
File size: 8,909 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 | -- Provider-neutral generation foundation.
--
-- Apply after 0002_authoritative_tenancy_postgres.sql. This migration adds
-- durable generation intent and execution records only; it does not configure
-- WAN, FLUX, external worker endpoints, or any credentials.
begin;
create table if not exists generation_requests (
id text primary key default gen_random_uuid()::text,
workspace_id text not null references workspaces(id) on delete restrict,
created_by_user_id text not null references users(id) on delete restrict,
provider text not null check (provider ~ '^[a-z][a-z0-9_-]{0,63}$'),
model_id text not null,
modality text not null check (modality in ('image', 'video')),
input_asset_id text references media_assets(id) on delete restrict,
spec jsonb not null default '{}'::jsonb,
request_fingerprint char(64) not null check (request_fingerprint ~ '^[0-9a-f]{64}$'),
idempotency_key text not null,
status text not null default 'queued' check (status in (
'queued', 'submitting', 'running', 'retrying', 'succeeded', 'failed',
'cancel_requested', 'cancelled'
)),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
completed_at timestamptz,
constraint uq_generation_request_workspace_idempotency unique (workspace_id, idempotency_key)
);
create index if not exists ix_generation_requests_workspace_created
on generation_requests(workspace_id, created_at desc);
create index if not exists ix_generation_requests_workspace_status
on generation_requests(workspace_id, status);
create table if not exists generation_jobs (
id text primary key default gen_random_uuid()::text,
generation_request_id text not null unique references generation_requests(id) on delete cascade,
workspace_id text not null references workspaces(id) on delete restrict,
provider text not null check (provider ~ '^[a-z][a-z0-9_-]{0,63}$'),
status text not null default 'queued' check (status in (
'queued', 'submitting', 'running', 'retrying', 'succeeded', 'failed',
'cancel_requested', 'cancelled'
)),
attempt_count integer not null default 0 check (attempt_count >= 0),
max_attempts integer not null default 3 check (max_attempts >= 0),
next_attempt_at timestamptz,
external_job_id text,
provider_metadata jsonb not null default '{}'::jsonb,
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,
updated_at timestamptz not null default now()
);
create index if not exists ix_generation_jobs_workspace_status
on generation_jobs(workspace_id, status);
create index if not exists ix_generation_jobs_next_attempt
on generation_jobs(status, next_attempt_at);
create index if not exists ix_generation_jobs_external
on generation_jobs(provider, external_job_id) where external_job_id is not null;
create table if not exists generation_job_attempts (
id text primary key default gen_random_uuid()::text,
generation_job_id text not null references generation_jobs(id) on delete cascade,
attempt_number integer not null check (attempt_number > 0),
status text not null,
error_code text,
error_message text,
provider_request_id text,
started_at timestamptz not null default now(),
completed_at timestamptz,
constraint uq_generation_job_attempt_number unique (generation_job_id, attempt_number)
);
create index if not exists ix_generation_job_attempts_job
on generation_job_attempts(generation_job_id, attempt_number);
-- Keep denormalised workspace IDs and canonical input/output references
-- internally consistent even when a trusted service role writes the rows.
create or replace function mediarouter_assert_generation_request_asset_workspace()
returns trigger language plpgsql as $$
declare asset_workspace text;
begin
if new.input_asset_id is not null then
select workspace_id into asset_workspace from media_assets where id = new.input_asset_id;
if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
raise exception 'generation input asset must belong to its request workspace' using errcode = '23503';
end if;
end if;
return new;
end;
$$;
drop trigger if exists mediarouter_generation_request_asset_workspace on generation_requests;
create trigger mediarouter_generation_request_asset_workspace
before insert or update of workspace_id, input_asset_id on generation_requests
for each row execute function mediarouter_assert_generation_request_asset_workspace();
create or replace function mediarouter_assert_generation_job_workspace()
returns trigger language plpgsql as $$
declare request_workspace text;
declare asset_workspace text;
begin
select workspace_id into request_workspace from generation_requests where id = new.generation_request_id;
if request_workspace is null or request_workspace is distinct from new.workspace_id then
raise exception 'generation job must belong to its request workspace' using errcode = '23503';
end if;
if new.output_asset_id is not null then
select workspace_id into asset_workspace from media_assets where id = new.output_asset_id;
if asset_workspace is null or asset_workspace is distinct from new.workspace_id then
raise exception 'generation output asset must belong to its job workspace' using errcode = '23503';
end if;
end if;
return new;
end;
$$;
drop trigger if exists mediarouter_generation_job_workspace on generation_jobs;
create trigger mediarouter_generation_job_workspace
before insert or update of workspace_id, generation_request_id, output_asset_id on generation_jobs
for each row execute function mediarouter_assert_generation_job_workspace();
do $$
declare table_name text;
begin
foreach table_name in array array['generation_requests', 'generation_jobs'] loop
execute format('drop trigger if exists mediarouter_tenant_touch_updated_at on %I', table_name);
execute format('create trigger mediarouter_tenant_touch_updated_at before update on %I for each row execute function mediarouter_tenant_touch_updated_at()', table_name);
end loop;
end $$;
-- API paths set app.workspace_id and app.user_id through SecurityDatabase's
-- tenant session. The security/service role is backend-only; these policies
-- remain defence in depth and independently verifiable with a tenant role.
alter table generation_requests enable row level security;
alter table generation_requests force row level security;
drop policy if exists generation_requests_workspace_isolation on generation_requests;
create policy generation_requests_workspace_isolation on generation_requests
using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = generation_requests.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
)
with check (
workspace_id = current_setting('app.workspace_id', true)
and created_by_user_id = current_setting('app.user_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = generation_requests.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
alter table generation_jobs enable row level security;
alter table generation_jobs force row level security;
drop policy if exists generation_jobs_workspace_isolation on generation_jobs;
create policy generation_jobs_workspace_isolation on generation_jobs
using (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = generation_jobs.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
)
with check (
workspace_id = current_setting('app.workspace_id', true)
and exists (
select 1 from workspace_memberships m
where m.workspace_id = generation_jobs.workspace_id
and m.user_id = current_setting('app.user_id', true)
and m.status = 'active'
)
);
alter table generation_job_attempts enable row level security;
alter table generation_job_attempts force row level security;
drop policy if exists generation_job_attempts_workspace_isolation on generation_job_attempts;
create policy generation_job_attempts_workspace_isolation on generation_job_attempts
using (exists (
select 1 from generation_jobs j
where j.id = generation_job_attempts.generation_job_id
and j.workspace_id = current_setting('app.workspace_id', true)
))
with check (exists (
select 1 from generation_jobs j
where j.id = generation_job_attempts.generation_job_id
and j.workspace_id = current_setting('app.workspace_id', true)
));
commit;
|