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; const integration = data.integration; const integrationError = validateIntegration(data, characteristics, specification); if (integrationError) { return Result.fail(new ResourceCatalogError(integrationError)); } return Result.ok( new ResourceValues( Object.freeze({ ...data, name: name.data.toPrimitive(), templateId: data.templateId.toLowerCase(), notes: data.notes.trim(), characteristics: Object.freeze(characteristics), ...(integration ? { integration: Object.freeze({ ...integration }) } : {}), }), 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 && sameIntegration(first.integration, second.integration) && 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); } 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") ) ); }