Files
UHD_Media_Tool/spikes/deno/packages/contracts/src/identity.ts
T
2026-09-27 18:24:08 +08:00

122 lines
4.3 KiB
TypeScript

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>;