metis-desk/modules/companies/src/api/domain/access/access-values.vo.ts

132 lines
4.6 KiB
TypeScript

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<string, UniqueID>();
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<string, CalendarDate>();
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<Name>());
}
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<AccessDefinition>,
private readonly identities: ReadonlyMap<string, UniqueID>,
private readonly dates: ReadonlyMap<string, CalendarDate>,
public readonly displayName: Maybe<Name>
) {
Object.freeze(this);
}
static create(input: AccessDefinition): Result<AccessValues, AccessError> {
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<UniqueID> {
const id = this.identities.get(key);
return id ? Maybe.some(id) : Maybe.none();
}
date(key: string): Maybe<CalendarDate> {
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;
});
}
}