T-HUB/apps/api/migrations/043_edl_projects.sql
glass beadian d8de9f5d82 feat: editor-v2 S1 — EDL schema, strict validation, /video/edl API, edl_projects store
apps/api/src/media-control/edl.ts: the EdlProject schema (closed enums,
integer ms, 3dB steps, <=50 items, <=15 min, filtergraph-metacharacter
blocklist on card text) + validateEdlProject — the mirror-assert owner for
the worker's edl-plan port. 043_edl_projects: RLS tenant table. EdlController:
POST /video/edl, PATCH autosave, GET (project + latest assemble job),
POST :id/render (queues kind 'assemble' with the validated project).
Tests 130 passing across media-control.
2026-10-16 21:22:00 +00:00

56 lines
2.6 KiB
PL/PgSQL

-- 043: EDL projects — the intake half of the editor-v2 assemble feature.
--
-- edl_projects stores a validated editor timeline (the EdlProject shape from
-- apps/api/src/media-control/edl.ts) as jsonb. The api validates strictly
-- (closed enums, integer ms, unknown keys rejected, filtergraph metacharacters
-- blocked in card text) before anything is written; the media-worker
-- re-validates the job's input_json on the assemble lane (044).
--
-- video_processing_jobs gains kind 'assemble' (044's claim already admits it)
-- and video_asset_id becomes nullable: an assemble job is queued from an EDL
-- project, not a single source asset, so there is no asset to point at yet —
-- the produced output is a derived video_assets row like compose/mix.
--
-- RLS / NULLIF / reject_workspace_change / sp_app grants follow the
-- 001/008/021/026/029 conventions: no app.workspace_id => NULL => no rows;
-- FORCE binds sp_app too. The jsonb column is content, not a schema facet:
-- the authoritative typed validation is app-side (edl.ts).
BEGIN;
CREATE TABLE edl_projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE ON UPDATE RESTRICT,
group_id uuid NULL,
project jsonb NOT NULL,
created_by uuid NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (workspace_id, id)
);
-- Tenant-pinned reference target (029 style): the composite UNIQUE lets child
-- rows pin (id, workspace_id) so an EDL project can never point across tenants.
CREATE INDEX edl_projects_ws_updated_idx ON edl_projects (workspace_id, updated_at DESC);
CREATE TRIGGER trg_edl_projects_ws_immutable BEFORE UPDATE ON edl_projects
FOR EACH ROW EXECUTE FUNCTION reject_workspace_change();
ALTER TABLE edl_projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE edl_projects FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON edl_projects
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid)
WITH CHECK (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON edl_projects TO sp_app;
-- The assemble lane: widen the 039 kind CHECK so an undecodable row cannot
-- exist, and let an assemble job carry no source asset yet.
ALTER TABLE video_processing_jobs DROP CONSTRAINT video_processing_jobs_kind_check;
ALTER TABLE video_processing_jobs
ADD CONSTRAINT video_processing_jobs_kind_check
CHECK (kind IN ('probe', 'compose', 'mix', 'assemble'));
ALTER TABLE video_processing_jobs ALTER COLUMN video_asset_id DROP NOT NULL;
COMMIT;