T-HUB/apps/api/migrations/038_media_assets.sql
glass beadian 71db0a08b2 feat: media attachments backend + producer job intake (t1+t3)
Rich media (t1): media_assets (migration 038) + /media module — multipart
upload with magic-byte sniffing, size caps, auth-scoped serve with
ETag/same-origin; the publishing 'text only' gate lifted to accept
0..4 validated mediaAssetIds with optional positional altTexts.

Producer intake (t3): video_processing_jobs gains kind compose|mix +
input_json + produced_asset_id (migration 039), video_assets.derived_from;
POST /video/:id/produce validates overlays/voiceover strictly and queues
the job for the media-worker runner (landing separately).
2026-10-14 20:00:31 +00:00

28 lines
1.3 KiB
SQL

-- 038: media assets for publishing (media attachments backend).
--
-- Owner need: posts need images/video, not text alone. This is the asset
-- spine the draft's mediaAssetIds point at — one row per uploaded file, the
-- bytes themselves in object storage (envelope-encrypted, content-addressed)
-- keyed by storage_key. Everything is per-workspace; the sha256 is the
-- plaintext content hash the storage service computes, so a re-upload
-- deduplicates the OBJECT but still gets its own row (different alt text,
-- different post usage). created_by is SET NULL on user deletion: the asset
-- outlives the account that uploaded it.
CREATE TABLE media_assets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
storage_key text NOT NULL,
mime text NOT NULL,
size_bytes bigint NOT NULL CHECK (size_bytes > 0),
sha256 text NOT NULL,
alt_text text,
created_by uuid REFERENCES users (id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (id, workspace_id)
);
CREATE POLICY tenant_isolation ON media_assets
USING (workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid);
CREATE INDEX media_assets_workspace_created ON media_assets (workspace_id, created_at);