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 { 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 { 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 { 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); }