Files
UHD_Media_Tool/packages/db/migrations/0003_instances.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

95 lines
3.8 KiB
TypeScript

import type { Kysely } from "kysely";
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
// 实例域:媒体库根、下载器实例、媒体服务器实例、多实例路径映射(REQ §7.1、§8.4、§12.5)
const HEALTH = ["unknown", "healthy", "degraded", "offline"] as const;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
${entityTable(
"library_root",
`
name text not null,
media_kind text not null default 'mixed' ${oneOf("media_kind", ["movie", "series", "variety", "mixed", "other"])},
local_path text not null,
management_mode text not null default 'register_only' ${oneOf("management_mode", ["register_only", "managed"])},
enabled boolean not null default true,
sentinel_file text,
health_status text not null default 'unknown' ${oneOf("health_status", HEALTH)},
last_health_at timestamptz,
health_detail jsonb not null default '{}'::jsonb,
notes text,
constraint library_root_name_key unique (name),
constraint library_root_local_path_key unique (local_path)`,
)}
${entityTable(
"downloader_instance",
`
name text not null,
kind text not null default 'qbittorrent' ${oneOf("kind", ["qbittorrent"])},
base_url text not null,
username text,
secret_ref uuid references secret (id) on delete restrict,
managed_tag text not null default 'umt',
cache_volume_key text,
enabled boolean not null default true,
health_status text not null default 'unknown' ${oneOf("health_status", HEALTH)},
last_seen_at timestamptz,
version_info jsonb not null default '{}'::jsonb,
settings jsonb not null default '{}'::jsonb,
constraint downloader_instance_name_key unique (name)`,
)}
${entityTable(
"media_server_instance",
`
name text not null,
kind text not null default 'emby' ${oneOf("kind", ["emby", "jellyfin"])},
base_url text not null,
user_id text,
secret_ref uuid references secret (id) on delete restrict,
enabled boolean not null default true,
health_status text not null default 'unknown' ${oneOf("health_status", HEALTH)},
last_seen_at timestamptz,
version_info jsonb not null default '{}'::jsonb,
settings jsonb not null default '{}'::jsonb,
constraint media_server_instance_name_key unique (name)`,
)}
${entityTable(
"path_mapping",
`
view_kind text not null ${oneOf("view_kind", ["downloader", "media_server"])},
downloader_instance_id uuid references downloader_instance (id) on delete cascade,
media_server_instance_id uuid references media_server_instance (id) on delete cascade,
library_root_id uuid references library_root (id) on delete set null,
remote_prefix text not null,
local_prefix text not null,
priority integer not null default 0,
enabled boolean not null default true,
verify_status text not null default 'unverified' ${oneOf("verify_status", ["unverified", "ok", "failed"])},
verified_at timestamptz,
verify_detail jsonb not null default '{}'::jsonb,
constraint path_mapping_view_owner_check check (
(view_kind = 'downloader' and downloader_instance_id is not null and media_server_instance_id is null)
or (view_kind = 'media_server' and media_server_instance_id is not null and downloader_instance_id is null)
)`,
)}
create unique index path_mapping_downloader_prefix_idx on path_mapping (downloader_instance_id, remote_prefix)
where downloader_instance_id is not null;
create unique index path_mapping_media_server_prefix_idx on path_mapping (media_server_instance_id, remote_prefix)
where media_server_instance_id is not null;
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, dropTables(["path_mapping", "media_server_instance", "downloader_instance", "library_root"]));
}