- 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]>
67 lines
3.3 KiB
TypeScript
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"]));
|
|
}
|