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]>
This commit is contained in:
1 parent
7ba712ea55
commit
cc8b3a7df4
42 files changed
+7026
-25
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// 文件域:media_asset、asset_unit_coverage、asset_location、path_binding(REQ §7.1、§8.2、§12.2)
|
||||
// 资产与位置分离;同一库根 + 有效相对路径不能被两个位置同时占用;
|
||||
// path_binding 一旦建立不因元数据刷新改变,只有显式整理操作可替换(旧绑定标 superseded)。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
${entityTable(
|
||||
"media_asset",
|
||||
`
|
||||
media_entity_id uuid references media_entity (id) on delete restrict,
|
||||
kind text not null ${oneOf("kind", ["video", "bluray_dir", "subtitle", "audio", "nfo", "image", "other"])},
|
||||
parent_asset_id uuid references media_asset (id) on delete restrict,
|
||||
size_bytes bigint,
|
||||
content_hash text,
|
||||
hash_algorithm text,
|
||||
sample_digest text,
|
||||
media_info jsonb not null default '{}'::jsonb,
|
||||
delivery jsonb not null default '{}'::jsonb,
|
||||
content_edition text,
|
||||
source_release_id uuid references source_release (id) on delete set null,
|
||||
status text not null default 'active' ${oneOf("status", ["active", "replaced", "retired", "missing"])},
|
||||
verified_at timestamptz`,
|
||||
)}
|
||||
create index media_asset_entity_idx on media_asset (media_entity_id) where media_entity_id is not null;
|
||||
create index media_asset_parent_idx on media_asset (parent_asset_id) where parent_asset_id is not null;
|
||||
|
||||
${entityTable(
|
||||
"asset_unit_coverage",
|
||||
`
|
||||
media_asset_id uuid not null references media_asset (id) on delete cascade,
|
||||
content_unit_id uuid not null references content_unit (id) on delete restrict,
|
||||
coverage text not null default 'full' ${oneOf("coverage", ["full", "partial", "segment", "unknown"])},
|
||||
segment text ${oneOf("segment", ["full", "upper", "middle", "lower", "unknown"])},
|
||||
evidence jsonb not null default '{}'::jsonb,
|
||||
constraint asset_unit_coverage_key unique (media_asset_id, content_unit_id)`,
|
||||
)}
|
||||
create index asset_unit_coverage_unit_idx on asset_unit_coverage (content_unit_id);
|
||||
|
||||
${entityTable(
|
||||
"asset_location",
|
||||
`
|
||||
media_asset_id uuid not null references media_asset (id) on delete cascade,
|
||||
location_kind text not null default 'library' ${oneOf("location_kind", ["library", "download_cache", "hardlink", "external"])},
|
||||
library_root_id uuid references library_root (id) on delete restrict,
|
||||
downloader_instance_id uuid references downloader_instance (id) on delete set null,
|
||||
relative_path text not null,
|
||||
status text not null default 'available' ${oneOf("status", ["available", "unavailable", "suspected_missing", "confirmed_missing", "removed"])},
|
||||
is_primary boolean not null default false,
|
||||
size_bytes bigint,
|
||||
inode bigint,
|
||||
device bigint,
|
||||
mtime timestamptz,
|
||||
sidecar_of_location_id uuid references asset_location (id) on delete set null,
|
||||
last_seen_at timestamptz,
|
||||
last_checked_at timestamptz,
|
||||
check_detail jsonb not null default '{}'::jsonb,
|
||||
constraint asset_location_library_root_check check (location_kind <> 'library' or library_root_id is not null)`,
|
||||
)}
|
||||
create unique index asset_location_root_path_key on asset_location (library_root_id, relative_path)
|
||||
where status <> 'removed' and library_root_id is not null;
|
||||
create index asset_location_asset_idx on asset_location (media_asset_id);
|
||||
create index asset_location_check_idx on asset_location (last_checked_at) where status <> 'removed';
|
||||
|
||||
${entityTable(
|
||||
"path_binding",
|
||||
`
|
||||
media_entity_id uuid not null references media_entity (id) on delete restrict,
|
||||
content_unit_id uuid references content_unit (id) on delete restrict,
|
||||
season_number integer,
|
||||
binding_kind text not null ${oneOf("binding_kind", ["entity", "season"])},
|
||||
library_root_id uuid not null references library_root (id) on delete restrict,
|
||||
relative_path text not null,
|
||||
template_version text,
|
||||
status text not null default 'active' ${oneOf("status", ["active", "conflict", "superseded"])},
|
||||
bound_at timestamptz not null default now(),
|
||||
bound_by text,
|
||||
superseded_by_id uuid references path_binding (id) on delete set null,
|
||||
note text,
|
||||
constraint path_binding_kind_check check ((binding_kind = 'season') = (season_number is not null))`,
|
||||
)}
|
||||
create index path_binding_entity_season_idx on path_binding (media_entity_id, season_number);
|
||||
create unique index path_binding_active_key on path_binding (media_entity_id, season_number) nulls not distinct
|
||||
where status = 'active';
|
||||
create unique index path_binding_active_path_key on path_binding (library_root_id, relative_path)
|
||||
where status = 'active';
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, dropTables(["path_binding", "asset_location", "asset_unit_coverage", "media_asset"]));
|
||||
}
|
||||
Reference in new issue
Block a user