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

183 lines
6.0 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";
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 ||
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 });
});
Object.freeze(frozenFields);
return Result.ok(
new ResourceSpecification(
Object.freeze({
templateId: template.data.toPrimitive(),
templateName: name.data.toPrimitive(),
fields: frozenFields,
}),
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 &&
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.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;
return Result.ok(
new ResourceValues(
Object.freeze({
...data,
name: name.data.toPrimitive(),
templateId: data.templateId.toLowerCase(),
notes: data.notes.trim(),
characteristics: Object.freeze(characteristics),
}),
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 &&
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);
}