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,161 @@
|
||||
import { z } from "zod";
|
||||
import { EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common";
|
||||
|
||||
// 实例域:媒体库根、下载器实例、媒体服务器实例、路径映射(REQ §7.1、§8.4、§12.5)
|
||||
// 凭证只以 secretRef 暴露;创建/更新时的 credential 是只写明文,服务端加密为 secret 后丢弃。
|
||||
|
||||
export const InstanceHealth = z.enum(["unknown", "healthy", "degraded", "offline"]);
|
||||
export type InstanceHealth = z.infer<typeof InstanceHealth>;
|
||||
|
||||
export const LibraryMediaKind = z.enum(["movie", "series", "variety", "mixed", "other"]);
|
||||
export type LibraryMediaKind = z.infer<typeof LibraryMediaKind>;
|
||||
|
||||
/** register_only:只登记不自动维护;managed:允许按策略补齐/升级(REQ §6.6) */
|
||||
export const ManagementMode = z.enum(["register_only", "managed"]);
|
||||
export type ManagementMode = z.infer<typeof ManagementMode>;
|
||||
|
||||
export const LibraryRoot = EntityBase.extend({
|
||||
name: z.string().min(1),
|
||||
mediaKind: LibraryMediaKind,
|
||||
/** 执行进程可访问的绝对路径 */
|
||||
localPath: z.string().startsWith("/"),
|
||||
managementMode: ManagementMode,
|
||||
enabled: z.boolean(),
|
||||
sentinelFile: z.string().nullable(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastHealthAt: Timestamp.nullable(),
|
||||
healthDetail: JsonObject,
|
||||
notes: z.string().nullable(),
|
||||
});
|
||||
export type LibraryRoot = z.infer<typeof LibraryRoot>;
|
||||
export const LibraryRootCreate = LibraryRoot.pick({
|
||||
name: true,
|
||||
mediaKind: true,
|
||||
localPath: true,
|
||||
managementMode: true,
|
||||
enabled: true,
|
||||
sentinelFile: true,
|
||||
notes: true,
|
||||
}).partial({ mediaKind: true, managementMode: true, enabled: true, sentinelFile: true, notes: true });
|
||||
export type LibraryRootCreate = z.infer<typeof LibraryRootCreate>;
|
||||
export const LibraryRootUpdate = LibraryRootCreate.partial();
|
||||
export type LibraryRootUpdate = z.infer<typeof LibraryRootUpdate>;
|
||||
|
||||
/** 只写凭证:明文仅在请求中出现,绝不回显 */
|
||||
const Credential = z.string().min(1).max(4096);
|
||||
|
||||
export const DownloaderKind = z.enum(["qbittorrent"]);
|
||||
export type DownloaderKind = z.infer<typeof DownloaderKind>;
|
||||
|
||||
export const DownloaderInstance = EntityBase.extend({
|
||||
name: z.string().min(1),
|
||||
kind: DownloaderKind,
|
||||
baseUrl: z.url(),
|
||||
username: z.string().nullable(),
|
||||
secretRef: Id.nullable(),
|
||||
/** 管理标签名(REQ §8.4) */
|
||||
managedTag: z.string().min(1),
|
||||
/** 共享缓存卷标识:多 qB 共用同一卷时按此汇总水位 */
|
||||
cacheVolumeKey: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastSeenAt: Timestamp.nullable(),
|
||||
versionInfo: JsonObject,
|
||||
settings: JsonObject,
|
||||
});
|
||||
export type DownloaderInstance = z.infer<typeof DownloaderInstance>;
|
||||
export const DownloaderInstanceCreate = DownloaderInstance.pick({
|
||||
name: true,
|
||||
kind: true,
|
||||
baseUrl: true,
|
||||
username: true,
|
||||
managedTag: true,
|
||||
cacheVolumeKey: true,
|
||||
enabled: true,
|
||||
settings: true,
|
||||
})
|
||||
.partial({ kind: true, username: true, managedTag: true, cacheVolumeKey: true, enabled: true, settings: true })
|
||||
.extend({ credential: Credential.optional() });
|
||||
export type DownloaderInstanceCreate = z.infer<typeof DownloaderInstanceCreate>;
|
||||
export const DownloaderInstanceUpdate = DownloaderInstanceCreate.partial();
|
||||
export type DownloaderInstanceUpdate = z.infer<typeof DownloaderInstanceUpdate>;
|
||||
|
||||
export const MediaServerKind = z.enum(["emby", "jellyfin"]);
|
||||
export type MediaServerKind = z.infer<typeof MediaServerKind>;
|
||||
|
||||
export const MediaServerInstance = EntityBase.extend({
|
||||
name: z.string().min(1),
|
||||
kind: MediaServerKind,
|
||||
baseUrl: z.url(),
|
||||
userId: z.string().nullable(),
|
||||
secretRef: Id.nullable(),
|
||||
enabled: z.boolean(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastSeenAt: Timestamp.nullable(),
|
||||
versionInfo: JsonObject,
|
||||
settings: JsonObject,
|
||||
});
|
||||
export type MediaServerInstance = z.infer<typeof MediaServerInstance>;
|
||||
export const MediaServerInstanceCreate = MediaServerInstance.pick({
|
||||
name: true,
|
||||
kind: true,
|
||||
baseUrl: true,
|
||||
userId: true,
|
||||
enabled: true,
|
||||
settings: true,
|
||||
})
|
||||
.partial({ kind: true, userId: true, enabled: true, settings: true })
|
||||
.extend({ credential: Credential.optional() });
|
||||
export type MediaServerInstanceCreate = z.infer<typeof MediaServerInstanceCreate>;
|
||||
export const MediaServerInstanceUpdate = MediaServerInstanceCreate.partial();
|
||||
export type MediaServerInstanceUpdate = z.infer<typeof MediaServerInstanceUpdate>;
|
||||
|
||||
export const PathViewKind = z.enum(["downloader", "media_server"]);
|
||||
export type PathViewKind = z.infer<typeof PathViewKind>;
|
||||
export const PathMappingVerifyStatus = z.enum(["unverified", "ok", "failed"]);
|
||||
export type PathMappingVerifyStatus = z.infer<typeof PathMappingVerifyStatus>;
|
||||
|
||||
/** 下载器/媒体服务器路径视图 → 执行进程可访问路径的前缀映射(REQ §12.5) */
|
||||
export const PathMapping = EntityBase.extend({
|
||||
viewKind: PathViewKind,
|
||||
downloaderInstanceId: Id.nullable(),
|
||||
mediaServerInstanceId: Id.nullable(),
|
||||
libraryRootId: Id.nullable(),
|
||||
remotePrefix: z.string().min(1),
|
||||
localPrefix: z.string().startsWith("/"),
|
||||
priority: z.number().int(),
|
||||
enabled: z.boolean(),
|
||||
verifyStatus: PathMappingVerifyStatus,
|
||||
verifiedAt: Timestamp.nullable(),
|
||||
verifyDetail: JsonObject,
|
||||
});
|
||||
export type PathMapping = z.infer<typeof PathMapping>;
|
||||
|
||||
const pathMappingOwnerOk = (v: {
|
||||
viewKind?: PathViewKind;
|
||||
downloaderInstanceId?: string | null;
|
||||
mediaServerInstanceId?: string | null;
|
||||
}) =>
|
||||
v.viewKind === undefined ||
|
||||
(v.viewKind === "downloader"
|
||||
? !!v.downloaderInstanceId && !v.mediaServerInstanceId
|
||||
: !!v.mediaServerInstanceId && !v.downloaderInstanceId);
|
||||
|
||||
const PathMappingInput = PathMapping.omit({
|
||||
...SERVER_MANAGED,
|
||||
verifyStatus: true,
|
||||
verifiedAt: true,
|
||||
verifyDetail: true,
|
||||
});
|
||||
export const PathMappingCreate = PathMappingInput.partial({
|
||||
downloaderInstanceId: true,
|
||||
mediaServerInstanceId: true,
|
||||
libraryRootId: true,
|
||||
priority: true,
|
||||
enabled: true,
|
||||
}).refine(pathMappingOwnerOk, { message: "viewKind 与实例 ID 不匹配" });
|
||||
export type PathMappingCreate = z.infer<typeof PathMappingCreate>;
|
||||
export const PathMappingUpdate = PathMappingInput.partial().refine(pathMappingOwnerOk, {
|
||||
message: "viewKind 与实例 ID 不匹配",
|
||||
});
|
||||
export type PathMappingUpdate = z.infer<typeof PathMappingUpdate>;
|
||||
Reference in new issue
Block a user