- 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]>
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
import { z } from "zod";
|
|
import { ActorType, ApiScope, EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common";
|
|
|
|
// 运维域:校验运行、媒体服务器库存映射、API Key、审计、outbox、凭证(REQ §8.1、§7.4、§13.2)
|
|
|
|
export const ReconcileKind = z.enum(["identity", "metadata", "asset", "acquisition", "server"]);
|
|
export type ReconcileKind = z.infer<typeof ReconcileKind>;
|
|
/** read_only:恢复后默认只读对账模式(REQ §8.5) */
|
|
export const ReconcileMode = z.enum(["normal", "read_only"]);
|
|
export type ReconcileMode = z.infer<typeof ReconcileMode>;
|
|
export const ReconcileStatus = z.enum(["queued", "running", "succeeded", "partial", "failed", "cancelled"]);
|
|
export type ReconcileStatus = z.infer<typeof ReconcileStatus>;
|
|
|
|
export const ReconcileRun = EntityBase.extend({
|
|
kind: ReconcileKind,
|
|
mode: ReconcileMode,
|
|
status: ReconcileStatus,
|
|
scope: JsonObject,
|
|
/** 中断续扫位置 */
|
|
resumeCursor: JsonObject,
|
|
stats: JsonObject,
|
|
taskId: Id.nullable(),
|
|
error: JsonObject.nullable(),
|
|
startedAt: Timestamp.nullable(),
|
|
finishedAt: Timestamp.nullable(),
|
|
});
|
|
export type ReconcileRun = z.infer<typeof ReconcileRun>;
|
|
export const ReconcileRunCreate = ReconcileRun.pick({ kind: true, mode: true, scope: true }).partial({
|
|
mode: true,
|
|
scope: true,
|
|
});
|
|
export type ReconcileRunCreate = z.infer<typeof ReconcileRunCreate>;
|
|
export const ReconcileRunUpdate = ReconcileRun.pick({
|
|
status: true,
|
|
resumeCursor: true,
|
|
stats: true,
|
|
taskId: true,
|
|
error: true,
|
|
startedAt: true,
|
|
finishedAt: true,
|
|
}).partial();
|
|
export type ReconcileRunUpdate = z.infer<typeof ReconcileRunUpdate>;
|
|
|
|
export const ServerItemType = z.enum(["movie", "series", "season", "episode", "other"]);
|
|
export type ServerItemType = z.infer<typeof ServerItemType>;
|
|
/** 未知 ≠ 缺失:stale/unmatched 不能推出"没有资源" */
|
|
export const ServerMatchStatus = z.enum(["matched", "ambiguous", "unmatched", "stale"]);
|
|
export type ServerMatchStatus = z.infer<typeof ServerMatchStatus>;
|
|
|
|
/** Emby 兼容库存条目与本地对象的映射(辅助证据,不构成文件纳管授权) */
|
|
export const ServerMapping = EntityBase.extend({
|
|
mediaServerInstanceId: Id,
|
|
serverItemId: z.string().min(1),
|
|
itemType: ServerItemType,
|
|
mediaEntityId: Id.nullable(),
|
|
contentUnitId: Id.nullable(),
|
|
mediaAssetId: Id.nullable(),
|
|
serverPath: z.string().nullable(),
|
|
pathMappingId: Id.nullable(),
|
|
matchStatus: ServerMatchStatus,
|
|
versionInfo: JsonObject,
|
|
evidence: JsonObject,
|
|
observedAt: Timestamp,
|
|
});
|
|
export type ServerMapping = z.infer<typeof ServerMapping>;
|
|
export const ServerMappingCreate = ServerMapping.omit(SERVER_MANAGED).partial({
|
|
mediaEntityId: true,
|
|
contentUnitId: true,
|
|
mediaAssetId: true,
|
|
serverPath: true,
|
|
pathMappingId: true,
|
|
matchStatus: true,
|
|
versionInfo: true,
|
|
evidence: true,
|
|
observedAt: true,
|
|
});
|
|
export type ServerMappingCreate = z.infer<typeof ServerMappingCreate>;
|
|
export const ServerMappingUpdate = ServerMappingCreate.omit({
|
|
mediaServerInstanceId: true,
|
|
serverItemId: true,
|
|
}).partial();
|
|
export type ServerMappingUpdate = z.infer<typeof ServerMappingUpdate>;
|
|
|
|
/** API Key(不含 key_hash) */
|
|
export const ApiKey = EntityBase.extend({
|
|
name: z.string().min(1),
|
|
scopes: z.array(ApiScope),
|
|
revokedAt: Timestamp.nullable(),
|
|
lastUsedAt: Timestamp.nullable(),
|
|
});
|
|
export type ApiKey = z.infer<typeof ApiKey>;
|
|
export const ApiKeyCreate = z.object({
|
|
name: z.string().min(1).max(80),
|
|
scopes: z.array(ApiScope).min(1),
|
|
});
|
|
export type ApiKeyCreate = z.infer<typeof ApiKeyCreate>;
|
|
/** 创建响应:明文 key 只在此返回一次 */
|
|
export const ApiKeyCreated = z.object({ apiKey: ApiKey, plaintextKey: z.string().min(16) });
|
|
export type ApiKeyCreated = z.infer<typeof ApiKeyCreated>;
|
|
export const ApiKeyUpdate = z.object({
|
|
name: z.string().min(1).max(80).optional(),
|
|
scopes: z.array(ApiScope).min(1).optional(),
|
|
revoked: z.literal(true).optional(),
|
|
});
|
|
export type ApiKeyUpdate = z.infer<typeof ApiKeyUpdate>;
|
|
|
|
/** 审计事件(只追加) */
|
|
export const AuditEvent = z.object({
|
|
id: Id,
|
|
at: Timestamp,
|
|
actorType: z.string(),
|
|
actorId: z.string().nullable(),
|
|
action: z.string().min(1),
|
|
targetType: z.string().nullable(),
|
|
targetId: z.string().nullable(),
|
|
payload: JsonObject,
|
|
traceId: z.string().nullable(),
|
|
});
|
|
export type AuditEvent = z.infer<typeof AuditEvent>;
|
|
export const AuditEventCreate = AuditEvent.omit({ id: true, at: true })
|
|
.partial({ actorId: true, targetType: true, targetId: true, payload: true, traceId: true })
|
|
.extend({ actorType: ActorType });
|
|
export type AuditEventCreate = z.infer<typeof AuditEventCreate>;
|
|
|
|
export const OutboxStatus = z.enum(["pending", "dispatched", "failed", "dead"]);
|
|
export type OutboxStatus = z.infer<typeof OutboxStatus>;
|
|
/** outbox:业务事务内写入,分发器轮询 pending 并以 idempotencyKey 入队(ADR-002 §2.1) */
|
|
export const OutboxEvent = z.object({
|
|
id: Id,
|
|
at: Timestamp,
|
|
type: z.string().min(1),
|
|
payload: JsonObject,
|
|
publishedAt: Timestamp.nullable(),
|
|
idempotencyKey: z.string().nullable(),
|
|
status: OutboxStatus,
|
|
attempts: NonNegInt,
|
|
nextAttemptAt: Timestamp,
|
|
lastError: JsonObject.nullable(),
|
|
});
|
|
export type OutboxEvent = z.infer<typeof OutboxEvent>;
|
|
export const OutboxEventCreate = OutboxEvent.pick({
|
|
type: true,
|
|
payload: true,
|
|
idempotencyKey: true,
|
|
nextAttemptAt: true,
|
|
}).partial({
|
|
payload: true,
|
|
idempotencyKey: true,
|
|
nextAttemptAt: true,
|
|
});
|
|
export type OutboxEventCreate = z.infer<typeof OutboxEventCreate>;
|
|
export const OutboxEventUpdate = OutboxEvent.pick({
|
|
status: true,
|
|
attempts: true,
|
|
nextAttemptAt: true,
|
|
lastError: true,
|
|
publishedAt: true,
|
|
}).partial();
|
|
export type OutboxEventUpdate = z.infer<typeof OutboxEventUpdate>;
|
|
|
|
export const SecretPurpose = z.enum([
|
|
"site_cookie",
|
|
"site_passkey",
|
|
"site_api_key",
|
|
"site_password",
|
|
"feed_url",
|
|
"downloader_password",
|
|
"media_server_api_key",
|
|
"metadata_api_key",
|
|
"llm_api_key",
|
|
"other",
|
|
]);
|
|
export type SecretPurpose = z.infer<typeof SecretPurpose>;
|
|
|
|
/** 加密凭证的元数据视图:密文/nonce 永不出 API */
|
|
export const Secret = EntityBase.extend({
|
|
name: z.string().min(1),
|
|
purpose: SecretPurpose,
|
|
algorithm: z.string(),
|
|
kdf: z.string(),
|
|
kdfParams: JsonObject,
|
|
keyVersion: z.number().int().min(1),
|
|
rotatedAt: Timestamp.nullable(),
|
|
});
|
|
export type Secret = z.infer<typeof Secret>;
|
|
/** plaintext 只写;服务端用 SECRET_MASTER_KEY 派生密钥加密后存 ciphertext/nonce */
|
|
export const SecretCreate = z.object({
|
|
name: z.string().min(1).max(120),
|
|
purpose: SecretPurpose,
|
|
plaintext: z.string().min(1).max(16384),
|
|
});
|
|
export type SecretCreate = z.infer<typeof SecretCreate>;
|
|
export const SecretUpdate = z.object({ plaintext: z.string().min(1).max(16384) });
|
|
export type SecretUpdate = z.infer<typeof SecretUpdate>;
|