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).
28 lines
1.3 KiB
SQL
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);
|