import { Name, UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { CalendarDate } from "../value-objects/calendar-date.vo"; import type { AccessDefinition } from "./access-definition"; import { AccessDefinitionSchema } from "./access-definition"; import { AccessError } from "./access-error"; function referenceValues(data: AccessDefinition): readonly [string, string][] { return Object.entries(data).flatMap<[string, string]>(([key, value]) => { if (key.endsWith("Id") && typeof value === "string") { return [[key, value]]; } if (key === "permissionIds" && Array.isArray(value)) { return value.map((id: string): [string, string] => [`permission:${id}`, id]); } return []; }); } function identitiesOf(data: AccessDefinition) { const identities = new Map(); for (const [key, value] of referenceValues(data)) { const id = UniqueID.create(value); if (!id.isSuccess) { return Result.fail(new AccessError("INVALID_REQUEST")); } identities.set(key, id.data); } return Result.ok(identities); } function datesOf(data: AccessDefinition) { const dates = new Map(); for (const [key, value] of Object.entries(data)) { if (!["startDate", "endDate", "expiresOn"].includes(key) || typeof value !== "string") { continue; } const date = CalendarDate.create(value); if (!date.isSuccess) { return Result.fail(new AccessError("INVALID_REQUEST")); } dates.set(key, date.data); } if ("startDate" in data && data.endDate !== null && data.endDate < data.startDate) { return Result.fail(new AccessError("INVALID_REQUEST")); } return Result.ok(dates); } function titleOf(data: AccessDefinition) { const title = "name" in data ? data.name : "login" in data ? data.login : undefined; if (title === undefined) { return Result.ok(Maybe.none()); } const name = Name.create(title, "name" in data ? 100 : 255); return name.isSuccess ? Result.ok(Maybe.some(name.data)) : Result.fail(new AccessError("INVALID_REQUEST")); } /** Valor compuesto inmutable: igualdad semántica, referencias UUID y fechas reconstruidas como Value Objects. */ export class AccessValues { private constructor( public readonly data: Readonly, private readonly identities: ReadonlyMap, private readonly dates: ReadonlyMap, public readonly displayName: Maybe ) { Object.freeze(this); } static create(input: AccessDefinition): Result { const parsed = AccessDefinitionSchema.safeParse(input); if (!parsed.success) { return Result.fail(new AccessError("INVALID_REQUEST")); } // Un UUID escrito en mayúsculas representa la misma identidad; no debe eludir unicidad ni pertenencia. const data = AccessDefinitionSchema.parse( Object.fromEntries( Object.entries(parsed.data).map(([key, value]) => { if (key.endsWith("Id") && typeof value === "string") { return [key, value.toLowerCase()]; } if (key === "permissionIds" && Array.isArray(value)) { return [key, value.map((id: string) => id.toLowerCase())]; } return [key, value]; }) ) ); const ids = identitiesOf(data); const dates = datesOf(data); const name = titleOf(data); if (!ids.isSuccess) { return ids; } if (!dates.isSuccess) { return dates; } if (!name.isSuccess) { return name; } const frozen = "permissionIds" in data ? { ...data, permissionIds: [...new Set(data.permissionIds)] } : { ...data }; if ("permissionIds" in frozen) { Object.freeze(frozen.permissionIds); } return Result.ok(new AccessValues(Object.freeze(frozen), ids.data, dates.data, name.data)); } reference(key: string): Maybe { const id = this.identities.get(key); return id ? Maybe.some(id) : Maybe.none(); } date(key: string): Maybe { const date = this.dates.get(key); return date ? Maybe.some(date) : Maybe.none(); } references(): readonly UniqueID[] { return [...this.identities.values()]; } equals(other: AccessValues) { if (this.data.kind !== other.data.kind) { return false; } return Object.entries(this.data).every(([key, value]) => { const candidate = Object.entries(other.data).find(([name]) => name === key)?.[1]; return Array.isArray(value) && Array.isArray(candidate) ? value.length === candidate.length && value.every((item) => candidate.includes(item)) : value === candidate; }); } }