Files
UHD_Media_Tool/packages/db/migrations/0005_identity.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

67 lines
3.3 KiB
TypeScript

import type { Kysely } from "kysely";
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
// 身份域:external_identity + identity_revision(REQ §7.2)
// provider + entity_type + external_id 对 accepted 唯一;候选可并存但不能同时被当作已确认身份。
const IDENTITY_STATUS = ["accepted", "proposed", "conflicted", "retired"] as const;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
${entityTable(
"external_identity",
`
media_entity_id uuid references media_entity (id) on delete restrict,
content_unit_id uuid references content_unit (id) on delete restrict,
provider text not null check (provider ~ '^[a-z][a-z0-9_]*$'),
entity_type text not null ${oneOf("entity_type", ["movie", "tv", "season", "episode", "collection", "person"])},
external_id text not null,
status text not null default 'proposed' ${oneOf("status", IDENTITY_STATUS)},
evidence_level text not null default 'needs_review' ${oneOf("evidence_level", ["manual", "rule_confirmed", "multi_evidence", "needs_review"])},
evidence jsonb not null default '{}'::jsonb,
locked boolean not null default false,
revision integer not null default 1,
valid_from timestamptz,
valid_until timestamptz,
verified_at timestamptz,
verify_status text not null default 'unverified' ${oneOf("verify_status", ["unverified", "ok", "not_found", "type_mismatch", "error"])},
extra jsonb not null default '{}'::jsonb,
constraint external_identity_target_check check (num_nonnulls(media_entity_id, content_unit_id) = 1)`,
)}
create unique index external_identity_accepted_key on external_identity (provider, entity_type, external_id)
where status = 'accepted';
create unique index external_identity_entity_accepted_key on external_identity (media_entity_id, provider, entity_type)
where status = 'accepted' and media_entity_id is not null;
create unique index external_identity_unit_accepted_key on external_identity (content_unit_id, provider, entity_type)
where status = 'accepted' and content_unit_id is not null;
create index external_identity_lookup_idx on external_identity (provider, entity_type, external_id);
create index external_identity_entity_idx on external_identity (media_entity_id) where media_entity_id is not null;
create index external_identity_unit_idx on external_identity (content_unit_id) where content_unit_id is not null;
${entityTable(
"identity_revision",
`
external_identity_id uuid not null references external_identity (id) on delete cascade,
revision integer not null,
action text not null ${oneOf("action", ["created", "status_changed", "relinked", "evidence_updated", "locked", "unlocked"])},
from_status text ${oneOf("from_status", IDENTITY_STATUS)},
to_status text ${oneOf("to_status", IDENTITY_STATUS)},
before jsonb,
after jsonb not null default '{}'::jsonb,
actor_type text not null ${oneOf("actor_type", ["user", "agent", "system", "api_key"])},
actor_id text,
reason text,
constraint identity_revision_seq_key unique (external_identity_id, revision)`,
)}
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, dropTables(["identity_revision", "external_identity"]));
}