Files
UHD_Media_Tool/packages/db/migrations/0004_content.ts
T
rootandClaude Fable 5.1 cc8b3a7df4 feat(db,contracts): full v0.1 data model migrations and shared zod contracts (W1-1)
- migrations 0002-0012 split by domain (foundation/instances/content/identity/
  sources/assets/policy/execution/agent/ops/organization): 41 tables, uuid pk,
  created_at/updated_at + touch trigger, text+CHECK status sets, pgcrypto/pg_trgm
- key constraints: source_release(site_id, site_release_id), observation(feed_id, guid),
  external_identity accepted partial unique, download_task(instance_id, torrent_hash),
  one active acquisition_plan per slot, one active path_binding per entity/season,
  asset_location root+path uniqueness, organization_record change_seq/change_xid cursor
- secret table (ciphertext/nonce/kdf) + secret_ref columns; outbox dispatch fields (ADR-002)
- src/types.ts generated from the migrated schema (scripts/gen-types.ts), drift-checked in tests;
  int8 -> number, date -> string parsers; migrator accepts migrationTableSchema
- contracts: Row/Create/Update schemas per domain, public organization-record API
  (changeSeq cursor), GlobalSettings, CapabilityDescriptor + 14 capability IO schemas,
  typed SSE events, Api endpoint table
- tests: migration up/down round trip on real PG in an isolated schema, constraint checks,
  contracts<->db column/enum sync, positive/negative parse for every exported schema

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:11:37 +08:00

74 lines
3.4 KiB
TypeScript

import type { Kysely } from "kysely";
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
// 内容域:本地作品 media_entity、内容单元 content_unit、别名 title_alias(REQ §3.1、§5.1、§5.4、§7.5)
// 本地 UUID 为主身份;外部 ID 与集号都不是唯一身份,编号可修订。
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
${entityTable(
"media_entity",
`
kind text not null ${oneOf("kind", ["movie", "series", "variety", "collection", "other"])},
title text not null,
original_title text,
year integer,
status text not null default 'active' ${oneOf("status", ["provisional", "active", "merged", "archived"])},
merged_into_id uuid references media_entity (id) on delete restrict,
metadata jsonb not null default '{}'::jsonb,
field_locks text[] not null default '{}'::text[],
last_metadata_refresh_at timestamptz,
constraint media_entity_merged_check check ((status = 'merged') = (merged_into_id is not null))`,
)}
create index media_entity_kind_idx on media_entity (kind);
create index media_entity_merged_into_idx on media_entity (merged_into_id) where merged_into_id is not null;
${entityTable(
"content_unit",
`
media_entity_id uuid not null references media_entity (id) on delete restrict,
parent_unit_id uuid references content_unit (id) on delete restrict,
kind text not null ${oneOf("kind", ["season", "episode", "special", "part"])},
content_type text not null default 'main' ${oneOf("content_type", ["main", "extra", "behind_the_scenes", "performance_only", "preview", "reaction", "unknown"])},
segment text not null default 'full' ${oneOf("segment", ["full", "upper", "middle", "lower", "unknown"])},
season_number integer,
episode_number integer,
source_issue text,
air_date date,
display_order double precision,
title text,
status text not null default 'provisional' ${oneOf("status", ["provisional", "confirmed", "retired"])},
numbering_locked boolean not null default false,
metadata jsonb not null default '{}'::jsonb,
field_locks text[] not null default '{}'::text[]`,
)}
create index content_unit_entity_numbering_idx on content_unit (media_entity_id, season_number, episode_number);
create index content_unit_parent_idx on content_unit (parent_unit_id) where parent_unit_id is not null;
create index content_unit_air_date_idx on content_unit (media_entity_id, air_date) where air_date is not null;
${entityTable(
"title_alias",
`
media_entity_id uuid not null references media_entity (id) on delete cascade,
content_unit_id uuid references content_unit (id) on delete cascade,
alias text not null,
normalized text not null,
language text,
kind text not null default 'other' ${oneOf("kind", ["original", "translated", "short", "site_title", "romanized", "other"])},
source text not null ${oneOf("source", ["tmdb", "user", "site", "agent", "nfo", "import"])},
locked boolean not null default false`,
)}
create unique index title_alias_entity_normalized_idx on title_alias (media_entity_id, content_unit_id, normalized, kind) nulls not distinct;
create index title_alias_normalized_trgm_idx on title_alias using gin (normalized gin_trgm_ops);
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, dropTables(["title_alias", "content_unit", "media_entity"]));
}