Files
UHD_Media_Tool/packages/db/migrations/0012_organization.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

117 lines
5.5 KiB
TypeScript

import type { Kysely } from "kysely";
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
// 整理域:organization_record(批次)+ organization_file_result(逐文件)(REQ §12.4)
// change_seq:任何 update(含其文件明细变化)都会刷新为新的序列值,用于稳定游标增量读取;
// change_xid:最后一次变更的事务号。读游标时只返回 change_xid < pg_snapshot_xmin(pg_current_snapshot()) 的行,
// 避免"序号小但提交晚"的事务被游标跳过。
const RECORD_STATUS = ["queued", "running", "succeeded", "partial", "failed", "cancelled"] as const;
const ACTIONS = ["copy", "move", "rename", "relocate", "hardlink"] as const;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
create sequence organization_record_change_seq as bigint;
${entityTable(
"organization_record",
`
task_id uuid references task (id) on delete set null,
trigger text not null ${oneOf("trigger", ["auto", "manual", "agent"])},
actor_type text not null ${oneOf("actor_type", ["user", "agent", "system", "api_key"])},
actor_id text,
media_entity_id uuid references media_entity (id) on delete set null,
content_unit_id uuid references content_unit (id) on delete set null,
download_task_id uuid references download_task (id) on delete set null,
source_downloader_instance_id uuid references downloader_instance (id) on delete set null,
source_path_view text not null default 'local' ${oneOf("source_path_view", ["downloader", "local", "library"])},
source_path text,
target_library_root_id uuid references library_root (id) on delete set null,
target_path text,
action text not null ${oneOf("action", ACTIONS)},
status text not null default 'queued' ${oneOf("status", RECORD_STATUS)},
reason text,
error jsonb,
plan jsonb not null default '{}'::jsonb,
plan_fingerprint text,
file_total integer not null default 0,
file_succeeded integer not null default 0,
file_failed integer not null default 0,
file_skipped integer not null default 0,
started_at timestamptz,
finished_at timestamptz,
change_seq bigint not null default nextval('organization_record_change_seq'),
change_xid xid8 not null default pg_current_xact_id()`,
)}
alter sequence organization_record_change_seq owned by organization_record.change_seq;
create unique index organization_record_change_seq_key on organization_record (change_seq);
create index organization_record_entity_idx on organization_record (media_entity_id) where media_entity_id is not null;
create index organization_record_task_idx on organization_record (task_id) where task_id is not null;
create index organization_record_status_idx on organization_record (status, created_at desc);
create function organization_record_bump_change_seq() returns trigger language plpgsql as $$
begin
new.change_seq := nextval('organization_record_change_seq');
new.change_xid := pg_current_xact_id();
return new;
end $$;
create trigger organization_record_bump_change_seq before update on organization_record
for each row execute function organization_record_bump_change_seq();
${entityTable(
"organization_file_result",
`
organization_record_id uuid not null references organization_record (id) on delete cascade,
ordinal integer not null default 0,
asset_operation_id uuid references asset_operation (id) on delete set null,
media_asset_id uuid references media_asset (id) on delete set null,
content_unit_id uuid references content_unit (id) on delete set null,
role text not null default 'main' ${oneOf("role", ["main", "subtitle", "audio", "bluray_dir", "nfo", "image", "other"])},
mode text not null ${oneOf("mode", ACTIONS)},
source_path text not null,
source_size_bytes bigint,
target_library_root_id uuid references library_root (id) on delete set null,
target_path text,
status text not null default 'queued' ${oneOf("status", ["queued", "running", "succeeded", "failed", "skipped", "cancelled"])},
skip_reason text,
error jsonb,
checksum text,
started_at timestamptz,
finished_at timestamptz`,
)}
create index organization_file_result_record_idx on organization_file_result (organization_record_id, ordinal);
-- 文件明细变化时刷新父记录 change_seq,使增量读取能拿到"既有记录状态更新"
create function organization_file_result_touch_record() returns trigger language plpgsql as $$
begin
update organization_record set updated_at = now()
where id = coalesce(new.organization_record_id, old.organization_record_id);
return null;
end $$;
create trigger organization_file_result_touch_record after insert or update or delete on organization_file_result
for each row execute function organization_file_result_touch_record();
alter table asset_operation add column organization_record_id uuid references organization_record (id) on delete set null;
create index asset_operation_record_idx on asset_operation (organization_record_id) where organization_record_id is not null;
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
drop index if exists asset_operation_record_idx;
alter table asset_operation drop column if exists organization_record_id;
${dropTables(["organization_file_result", "organization_record"])}
drop function if exists organization_file_result_touch_record();
drop function if exists organization_record_bump_change_seq();
-- organization_record_change_seq 由 change_seq 列拥有,随表一起删除
`,
);
}