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,121 @@
|
||||
import { z } from "zod";
|
||||
import { ActorType, EntityBase, Id, JsonObject, Timestamp } from "./common";
|
||||
|
||||
// 身份域:外部身份映射与修订(REQ §7.2)
|
||||
// provider + entityType + externalId 对 accepted 唯一;TMDB movie:123 与 tv:123 是两个身份。
|
||||
|
||||
export const IdentityStatus = z.enum(["accepted", "proposed", "conflicted", "retired"]);
|
||||
export type IdentityStatus = z.infer<typeof IdentityStatus>;
|
||||
export const ExternalEntityType = z.enum(["movie", "tv", "season", "episode", "collection", "person"]);
|
||||
export type ExternalEntityType = z.infer<typeof ExternalEntityType>;
|
||||
/** 证据等级:未经标注集校准前不展示概率(REQ §5.3) */
|
||||
export const EvidenceLevel = z.enum(["manual", "rule_confirmed", "multi_evidence", "needs_review"]);
|
||||
export type EvidenceLevel = z.infer<typeof EvidenceLevel>;
|
||||
export const IdentityVerifyStatus = z.enum(["unverified", "ok", "not_found", "type_mismatch", "error"]);
|
||||
export type IdentityVerifyStatus = z.infer<typeof IdentityVerifyStatus>;
|
||||
export const IdentityProvider = z.string().regex(/^[a-z][a-z0-9_]*$/);
|
||||
|
||||
export const ExternalIdentity = EntityBase.extend({
|
||||
mediaEntityId: Id.nullable(),
|
||||
contentUnitId: Id.nullable(),
|
||||
provider: IdentityProvider,
|
||||
entityType: ExternalEntityType,
|
||||
externalId: z.string().min(1),
|
||||
status: IdentityStatus,
|
||||
evidenceLevel: EvidenceLevel,
|
||||
evidence: JsonObject,
|
||||
/** 人工锁:自动匹配/刷新不得改动 */
|
||||
locked: z.boolean(),
|
||||
revision: z.number().int().min(1),
|
||||
validFrom: Timestamp.nullable(),
|
||||
validUntil: Timestamp.nullable(),
|
||||
verifiedAt: Timestamp.nullable(),
|
||||
/** 404/超时/鉴权失败只记录到这里,不是删除映射的依据 */
|
||||
verifyStatus: IdentityVerifyStatus,
|
||||
/** 附属字段,如 TMDB episode_group */
|
||||
extra: JsonObject,
|
||||
});
|
||||
export type ExternalIdentity = z.infer<typeof ExternalIdentity>;
|
||||
|
||||
const oneTarget = (v: { mediaEntityId?: string | null; contentUnitId?: string | null }) =>
|
||||
Number(!!v.mediaEntityId) + Number(!!v.contentUnitId) === 1;
|
||||
|
||||
export const ExternalIdentityCreate = ExternalIdentity.pick({
|
||||
mediaEntityId: true,
|
||||
contentUnitId: true,
|
||||
provider: true,
|
||||
entityType: true,
|
||||
externalId: true,
|
||||
status: true,
|
||||
evidenceLevel: true,
|
||||
evidence: true,
|
||||
locked: true,
|
||||
validFrom: true,
|
||||
validUntil: true,
|
||||
extra: true,
|
||||
})
|
||||
.partial({
|
||||
mediaEntityId: true,
|
||||
contentUnitId: true,
|
||||
status: true,
|
||||
evidenceLevel: true,
|
||||
evidence: true,
|
||||
locked: true,
|
||||
validFrom: true,
|
||||
validUntil: true,
|
||||
extra: true,
|
||||
})
|
||||
.refine(oneTarget, { message: "mediaEntityId 与 contentUnitId 必须且只能给一个" });
|
||||
export type ExternalIdentityCreate = z.infer<typeof ExternalIdentityCreate>;
|
||||
|
||||
/** 变更一律生成 identity_revision;expectedRevision 用于乐观并发 */
|
||||
export const ExternalIdentityUpdate = z.object({
|
||||
expectedRevision: z.number().int().min(1),
|
||||
status: IdentityStatus.optional(),
|
||||
evidenceLevel: EvidenceLevel.optional(),
|
||||
evidence: JsonObject.optional(),
|
||||
locked: z.boolean().optional(),
|
||||
validUntil: Timestamp.nullable().optional(),
|
||||
extra: JsonObject.optional(),
|
||||
reason: z.string().max(2000).optional(),
|
||||
});
|
||||
export type ExternalIdentityUpdate = z.infer<typeof ExternalIdentityUpdate>;
|
||||
|
||||
export const IdentityRevisionAction = z.enum([
|
||||
"created",
|
||||
"status_changed",
|
||||
"relinked",
|
||||
"evidence_updated",
|
||||
"locked",
|
||||
"unlocked",
|
||||
]);
|
||||
export type IdentityRevisionAction = z.infer<typeof IdentityRevisionAction>;
|
||||
|
||||
/** 身份修订历史:只追加,不修改 */
|
||||
export const IdentityRevision = EntityBase.extend({
|
||||
externalIdentityId: Id,
|
||||
revision: z.number().int().min(1),
|
||||
action: IdentityRevisionAction,
|
||||
fromStatus: IdentityStatus.nullable(),
|
||||
toStatus: IdentityStatus.nullable(),
|
||||
before: JsonObject.nullable(),
|
||||
after: JsonObject,
|
||||
actorType: ActorType,
|
||||
actorId: z.string().nullable(),
|
||||
reason: z.string().nullable(),
|
||||
});
|
||||
export type IdentityRevision = z.infer<typeof IdentityRevision>;
|
||||
/** 由服务端在身份变更事务内写入;此输入仅供内部仓储使用 */
|
||||
export const IdentityRevisionCreate = IdentityRevision.pick({
|
||||
externalIdentityId: true,
|
||||
revision: true,
|
||||
action: true,
|
||||
fromStatus: true,
|
||||
toStatus: true,
|
||||
before: true,
|
||||
after: true,
|
||||
actorType: true,
|
||||
actorId: true,
|
||||
reason: true,
|
||||
}).partial({ fromStatus: true, toStatus: true, before: true, actorId: true, reason: true });
|
||||
export type IdentityRevisionCreate = z.infer<typeof IdentityRevisionCreate>;
|
||||
Reference in new issue
Block a user