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