2026-10-03 13:19:20 +00:00
|
|
|
import { Name, UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
import { Maybe, Result } from "@repo/rdx-utils";
|
|
|
|
|
|
2026-10-09 08:38:19 +00:00
|
|
|
import { CalendarDate } from "../value-objects/calendar-date.vo";
|
2026-10-03 13:19:20 +00:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|