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,140 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// 来源域:site、site_account、feed_source、source_release、observation(REQ §4.1–§4.6)
|
||||
// 站点级限流共享;多 Feed 观察关联同一 source_release;含 passkey 的 URL 只以 secret_ref 保存。
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
${entityTable(
|
||||
"site",
|
||||
`
|
||||
key text not null check (key ~ '^[a-z][a-z0-9_-]*$'),
|
||||
name text not null,
|
||||
adapter text not null ${oneOf("adapter", ["nexusphp", "mteam_api", "generic_rss"])},
|
||||
base_url text not null,
|
||||
enabled boolean not null default true,
|
||||
rate_limit jsonb not null default '{}'::jsonb,
|
||||
config jsonb not null default '{}'::jsonb,
|
||||
health_status text not null default 'unknown' ${oneOf("health_status", ["unknown", "healthy", "degraded", "failing", "disabled"])},
|
||||
circuit_state text not null default 'closed' ${oneOf("circuit_state", ["closed", "open", "half_open"])},
|
||||
circuit_open_until timestamptz,
|
||||
snapshot_enabled boolean not null default false,
|
||||
snapshot_interval_sec integer,
|
||||
last_snapshot_at timestamptz,
|
||||
snapshot_cursor jsonb not null default '{}'::jsonb,
|
||||
constraint site_key_key unique (key)`,
|
||||
)}
|
||||
|
||||
${entityTable(
|
||||
"site_account",
|
||||
`
|
||||
site_id uuid not null references site (id) on delete cascade,
|
||||
label text not null,
|
||||
auth_kind text not null ${oneOf("auth_kind", ["cookie", "passkey", "api_key", "password"])},
|
||||
secret_ref uuid references secret (id) on delete restrict,
|
||||
enabled boolean not null default true,
|
||||
status text not null default 'unknown' ${oneOf("status", ["unknown", "active", "invalid", "expired"])},
|
||||
last_verified_at timestamptz,
|
||||
user_info jsonb not null default '{}'::jsonb,
|
||||
constraint site_account_label_key unique (site_id, label)`,
|
||||
)}
|
||||
|
||||
${entityTable(
|
||||
"feed_source",
|
||||
`
|
||||
site_id uuid references site (id) on delete cascade,
|
||||
site_account_id uuid references site_account (id) on delete set null,
|
||||
name text not null,
|
||||
purpose text not null default 'generic' ${oneOf("purpose", ["movie", "series", "variety", "promotion", "search", "generic"])},
|
||||
kind text not null default 'rss' ${oneOf("kind", ["rss", "atom", "list", "query"])},
|
||||
url_secret_ref uuid references secret (id) on delete restrict,
|
||||
url_display text not null,
|
||||
parser_version text not null default '1',
|
||||
enabled boolean not null default true,
|
||||
poll_interval_sec integer not null default 900 check (poll_interval_sec >= 60),
|
||||
filter jsonb not null default '{}'::jsonb,
|
||||
cursor jsonb not null default '{}'::jsonb,
|
||||
etag text,
|
||||
last_modified text,
|
||||
last_fetch_at timestamptz,
|
||||
last_success_at timestamptz,
|
||||
next_fetch_at timestamptz,
|
||||
consecutive_failures integer not null default 0,
|
||||
backoff_until timestamptz,
|
||||
health_status text not null default 'unknown' ${oneOf("health_status", ["unknown", "healthy", "degraded", "failing", "circuit_open", "disabled"])},
|
||||
last_error jsonb`,
|
||||
)}
|
||||
create index feed_source_due_idx on feed_source (next_fetch_at) where enabled;
|
||||
create index feed_source_site_idx on feed_source (site_id);
|
||||
|
||||
${entityTable(
|
||||
"source_release",
|
||||
`
|
||||
site_id uuid not null references site (id) on delete restrict,
|
||||
site_release_id text not null,
|
||||
title text not null,
|
||||
subtitle text,
|
||||
category_raw text,
|
||||
tags_raw text[] not null default '{}'::text[],
|
||||
size_bytes bigint,
|
||||
infohash_v1 text,
|
||||
infohash_v2 text,
|
||||
file_list jsonb,
|
||||
promotion jsonb not null default '{}'::jsonb,
|
||||
promotion_expires_at timestamptz,
|
||||
seeders integer,
|
||||
leechers integer,
|
||||
source_published_at timestamptz,
|
||||
first_seen_at timestamptz not null default now(),
|
||||
last_seen_at timestamptz not null default now(),
|
||||
detail_fetched_at timestamptz,
|
||||
parsed jsonb not null default '{}'::jsonb,
|
||||
parser_version 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,
|
||||
resolution_status text not null default 'unresolved' ${oneOf("resolution_status", ["unresolved", "resolved", "ambiguous", "unidentifiable", "rejected"])},
|
||||
resolution_evidence jsonb not null default '{}'::jsonb,
|
||||
availability text not null default 'active' ${oneOf("availability", ["active", "site_disabled", "expired", "unknown"])},
|
||||
raw jsonb not null default '{}'::jsonb,
|
||||
constraint source_release_site_release_key unique (site_id, site_release_id)`,
|
||||
)}
|
||||
create index source_release_infohash_v1_idx on source_release (infohash_v1) where infohash_v1 is not null;
|
||||
create index source_release_infohash_v2_idx on source_release (infohash_v2) where infohash_v2 is not null;
|
||||
create index source_release_entity_idx on source_release (media_entity_id) where media_entity_id is not null;
|
||||
create index source_release_unresolved_idx on source_release (first_seen_at) where resolution_status in ('unresolved', 'ambiguous');
|
||||
create index source_release_title_trgm_idx on source_release using gin (title gin_trgm_ops);
|
||||
|
||||
${entityTable(
|
||||
"observation",
|
||||
`
|
||||
feed_id uuid not null references feed_source (id) on delete cascade,
|
||||
guid text not null,
|
||||
content_digest text not null,
|
||||
source_release_id uuid references source_release (id) on delete set null,
|
||||
title text not null,
|
||||
link_redacted text,
|
||||
raw jsonb not null default '{}'::jsonb,
|
||||
source_published_at timestamptz,
|
||||
fetched_at timestamptz not null default now(),
|
||||
first_seen_at timestamptz not null default now(),
|
||||
last_seen_at timestamptz not null default now(),
|
||||
status text not null default 'new' ${oneOf("status", ["new", "linked", "ignored", "error"])},
|
||||
resolve_attempts integer not null default 0,
|
||||
next_resolve_at timestamptz,
|
||||
last_error jsonb,
|
||||
constraint observation_feed_guid_key unique (feed_id, guid)`,
|
||||
)}
|
||||
create index observation_release_idx on observation (source_release_id) where source_release_id is not null;
|
||||
create index observation_next_resolve_idx on observation (next_resolve_at) where status in ('new', 'error');
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, dropTables(["observation", "source_release", "feed_source", "site_account", "site"]));
|
||||
}
|
||||
Reference in new issue
Block a user