metis-desk/modules/resources/src/api/domain/value-objects/resource-values.vo.ts

282 lines
9.3 KiB
TypeScript
Raw Normal View History

2026-10-05 11:13:51 +00:00
import { Name, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { ResourceCatalogError } from "../resource-definition";
import type {
ResourceCharacteristic,
ResourceInput,
ResourceSpecificationData,
} from "../resource-values";
import { ResourceInputSchema, ResourceSpecificationSchema } from "../resource-values";
import {
freezeTemplateSections,
sameTemplateSections,
validTemplateSections,
} from "../template-sections";
2026-10-05 11:13:51 +00:00
export class ResourceSpecification {
private constructor(
private readonly data: ResourceSpecificationData,
public readonly templateId: UniqueID,
public readonly name: Name
) {
Object.freeze(this);
}
toPrimitive() {
return this.data;
}
static create(
input: ResourceSpecificationData
): Result<ResourceSpecification, ResourceCatalogError> {
const parsed = ResourceSpecificationSchema.safeParse(input);
if (
!(parsed.success && validTemplateSections(input.sections ?? [], input.fields)) ||
2026-10-05 11:13:51 +00:00
new Set(input.fields.map((field) => field.fieldId.toLowerCase())).size !== input.fields.length
) {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
const template = UniqueID.create(input.templateId);
const name = Name.create(input.templateName, 100);
if (!(template.isSuccess && name.isSuccess)) {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
const frozenFields = input.fields.map((field) => {
const choices = [...field.choices];
Object.freeze(choices);
return Object.freeze({
...field,
fieldId: field.fieldId.toLowerCase(),
choices,
...(field.sectionId ? { sectionId: field.sectionId.toLowerCase() } : {}),
});
2026-10-05 11:13:51 +00:00
});
Object.freeze(frozenFields);
return Result.ok(
new ResourceSpecification(
Object.freeze({
templateId: template.data.toPrimitive(),
templateName: name.data.toPrimitive(),
fields: frozenFields,
...(input.sections ? { sections: freezeTemplateSections(input.sections) } : {}),
2026-10-05 11:13:51 +00:00
}),
template.data,
name.data
)
);
}
equals(other: ResourceSpecification | undefined) {
if (!(other instanceof ResourceSpecification)) {
return false;
}
const first = this.toPrimitive();
const second = other.toPrimitive();
return (
first.templateId === second.templateId &&
first.templateName === second.templateName &&
sameTemplateSections(first.sections ?? [], second.sections ?? []) &&
2026-10-05 11:13:51 +00:00
first.fields.length === second.fields.length &&
first.fields.every((field, index) => {
const candidate = second.fields[index];
return (
candidate !== undefined &&
field.fieldId === candidate.fieldId &&
field.name === candidate.name &&
field.type === candidate.type &&
field.unit === candidate.unit &&
field.required === candidate.required &&
field.sectionId === candidate.sectionId &&
2026-10-05 11:13:51 +00:00
field.choices.length === candidate.choices.length &&
field.choices.every((choice, position) => choice === candidate.choices[position])
);
})
);
}
}
function validCharacteristic(field: ResourceCharacteristic, value: unknown) {
switch (field.type) {
case "text":
return typeof value === "string";
case "number":
return typeof value === "number" && Number.isFinite(value);
case "boolean":
return typeof value === "boolean";
case "choice":
return typeof value === "string" && field.choices.includes(value);
case "date":
return z.iso.date().safeParse(value).success;
}
}
export class ResourceValues {
private constructor(
private readonly data: ResourceInput,
public readonly name: Name
) {
Object.freeze(this);
}
toPrimitive() {
return this.data;
}
static create(
input: ResourceInput,
specification: ResourceSpecification
): Result<ResourceValues, ResourceCatalogError> {
const parsed = ResourceInputSchema.safeParse(input);
const name = Name.create(input.name, 100);
if (
!(parsed.success && name.isSuccess) ||
input.templateId.toLowerCase() !== specification.toPrimitive().templateId
) {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
const data = parsed.data;
const parsedCharacteristics = normalizeCharacteristics(data.characteristics, specification);
if (!parsedCharacteristics.isSuccess) {
return parsedCharacteristics;
}
const characteristics = parsedCharacteristics.data;
2026-10-05 15:01:32 +00:00
const integration = data.integration;
const integrationError = validateIntegration(data, characteristics, specification);
if (integrationError) {
return Result.fail(new ResourceCatalogError(integrationError));
}
2026-10-05 11:13:51 +00:00
return Result.ok(
new ResourceValues(
Object.freeze({
...data,
name: name.data.toPrimitive(),
templateId: data.templateId.toLowerCase(),
notes: data.notes.trim(),
characteristics: Object.freeze(characteristics),
2026-10-05 15:01:32 +00:00
...(integration ? { integration: Object.freeze({ ...integration }) } : {}),
2026-10-05 11:13:51 +00:00
}),
name.data
)
);
}
equals(other: ResourceValues | undefined) {
if (!(other instanceof ResourceValues)) {
return false;
}
const first = this.toPrimitive();
const second = other.toPrimitive();
return (
first.name === second.name &&
first.templateId === second.templateId &&
first.active === second.active &&
first.notes === second.notes &&
2026-10-05 15:01:32 +00:00
sameIntegration(first.integration, second.integration) &&
2026-10-05 11:13:51 +00:00
Object.keys(first.characteristics).length === Object.keys(second.characteristics).length &&
Object.entries(first.characteristics).every(
([key, value]) => value === second.characteristics[key]
)
);
}
}
function normalizeCharacteristics(
input: ResourceInput["characteristics"],
specification: ResourceSpecification
): Result<ResourceInput["characteristics"], ResourceCatalogError> {
const characteristics: ResourceInput["characteristics"] = {};
for (const [key, raw] of Object.entries(input)) {
const field = specification
.toPrimitive()
.fields.find((item) => item.fieldId === key.toLowerCase());
const value = typeof raw === "string" ? raw.trim() : raw;
if (!field || key.toLowerCase() in characteristics || !validCharacteristic(field, value)) {
return Result.fail(new ResourceCatalogError("INVALID_CHARACTERISTICS"));
}
if (value !== "") {
characteristics[key.toLowerCase()] = value;
}
}
if (
specification
.toPrimitive()
.fields.some((field) => field.required && !(field.fieldId in characteristics))
) {
return Result.fail(new ResourceCatalogError("REQUIRED_CHARACTERISTICS"));
}
return Result.ok(characteristics);
}
2026-10-05 15:01:32 +00:00
function sameIntegration(
first: ResourceInput["integration"],
second: ResourceInput["integration"]
) {
if (!(first && second)) {
return first === second;
}
if (first.kind === "mailbox" && second.kind === "mailbox") {
return (
first.addressFieldId === second.addressFieldId &&
first.contextFieldId === second.contextFieldId &&
first.mailboxKind === second.mailboxKind &&
first.accountId === second.accountId
);
}
return (
first.kind === "access" &&
second.kind === "access" &&
first.referenceFieldId === second.referenceFieldId &&
first.resourceTypeId === second.resourceTypeId &&
first.systemId === second.systemId &&
first.environmentId === second.environmentId
);
}
function validateIntegration(
data: ResourceInput,
characteristics: ResourceInput["characteristics"],
specification: ResourceSpecification
) {
const integration = data.integration;
if (!validIntegrationFields(integration, specification)) {
return "INVALID_CHARACTERISTICS" as const;
}
if (integration && data.notes.trim().length > 4000) {
return "INVALID_REQUEST" as const;
}
if (integration?.kind === "mailbox") {
const address = characteristics[integration.addressFieldId];
if (typeof address !== "string" || !z.email().max(254).safeParse(address).success) {
return "INVALID_EMAIL" as const;
}
characteristics[integration.addressFieldId] = address.toLowerCase();
const context = characteristics[integration.contextFieldId];
if (typeof context === "string" && context.length > 255) {
return "INVALID_CHARACTERISTICS" as const;
}
}
if (integration?.kind === "access") {
const reference = characteristics[integration.referenceFieldId];
if (typeof reference === "string" && reference.length > 255) {
return "INVALID_CHARACTERISTICS" as const;
}
}
return null;
}
function validIntegrationFields(
integration: ResourceInput["integration"],
specification: ResourceSpecification
) {
const fields =
integration?.kind === "mailbox"
? [integration.addressFieldId, integration.contextFieldId]
: integration?.kind === "access"
? [integration.referenceFieldId]
: [];
return !(
new Set(fields).size !== fields.length ||
fields.some(
(id) =>
!specification
.toPrimitive()
.fields.some((field) => field.fieldId === id && field.type === "text")
)
);
}