122 lines
4.3 KiB
TypeScript
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>;
|