import { AggregateRoot, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; import { ResourceCatalogError } from "./resource-definition"; import { ResourceRevision } from "./resource-revision.entity"; import { ResourceOwnershipSchema } from "./resource-scope"; import type { ResourceSpecification } from "./value-objects/resource-values.vo"; import { ResourceValues } from "./value-objects/resource-values.vo"; type ResourceProps = Readonly< ({ scope: "company"; companyId: UniqueID } | { scope: "global"; companyId: null }) & { specification: ResourceSpecification; values: ResourceValues; history: readonly ResourceRevision[]; } >; export class Resource extends AggregateRoot { private constructor(props: ResourceProps, id?: UniqueID) { super({ ...props, history: Object.freeze([...props.history]) }, id); const ownership = ResourceOwnershipSchema.safeParse({ scope: props.scope, companyId: props.companyId instanceof UniqueID ? props.companyId.toPrimitive() : props.companyId === null ? null : undefined, }); if (!ownership.success) { throw new ResourceCatalogError("INVALID_RESOURCE_SCOPE"); } } static create( companyId: UniqueID, specification: ResourceSpecification, values: ResourceValues, actorId: UniqueID, recordedAt: string ): Result { if (!(companyId instanceof UniqueID)) { return Result.fail(new ResourceCatalogError("INVALID_RESOURCE_SCOPE")); } return Result.ok( new Resource({ scope: "company", companyId, specification, values, history: [ResourceRevision.create({ values, specification, actorId, recordedAt })], }) ); } static rehydrate(props: ResourceProps, id: UniqueID) { return new Resource(props, id); } static createGlobal( id: UniqueID, specification: ResourceSpecification, values: ResourceValues, actorId: UniqueID, recordedAt: string ): Result { if ( !z.uuidv7().safeParse(id.toPrimitive()).success || values.toPrimitive().integration || values.toPrimitive().templateId !== specification.toPrimitive().templateId ) { return Result.fail(new ResourceCatalogError("RESOURCE_GLOBAL_TEMPLATE_NOT_ALLOWED")); } return Result.ok( new Resource( { scope: "global", companyId: null, specification, values, history: [ResourceRevision.create({ values, specification, actorId, recordedAt })], }, id ) ); } get scope() { return this.props.scope; } belongsToCompany(companyId: UniqueID) { return this.props.scope === "company" && this.props.companyId.equals(companyId); } requireCompanyId(): UniqueID { if (this.props.scope !== "company") { throw new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"); } return this.props.companyId; } get companyId() { return this.props.companyId; } get specification() { return this.props.specification; } get values() { return this.props.values; } get history() { return this.props.history; } synchronizeSpecification( specification: ResourceSpecification, actorId: UniqueID, recordedAt: string ) { if (this.scope !== "company") { return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED")); } return this.synchronizeValues(specification, actorId, recordedAt); } synchronizeGlobalSpecification( specification: ResourceSpecification, actorId: UniqueID, recordedAt: string ) { if ( this.scope !== "global" || this.values.toPrimitive().integration || specification.templateId.toPrimitive() !== this.values.toPrimitive().templateId ) { return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED")); } return this.synchronizeValues(specification, actorId, recordedAt); } private synchronizeValues( specification: ResourceSpecification, actorId: UniqueID, recordedAt: string ) { const allowed = new Set(specification.toPrimitive().fields.map((field) => field.fieldId)); const values = ResourceValues.reconstruct( { ...this.values.toPrimitive(), characteristics: Object.fromEntries( Object.entries(this.values.toPrimitive().characteristics).filter(([id]) => allowed.has(id) ) ), }, specification ); if (!values.isSuccess) { return values; } if (this.specification.equals(specification) && this.values.equals(values.data)) { return Result.ok(this); } return Result.ok( new Resource( { ...this.props, specification, values: values.data, history: [ ...this.history, ResourceRevision.create({ specification, values: values.data, actorId, recordedAt }), ], }, this.id ) ); } update( values: ResourceValues, actorId: UniqueID, recordedAt: string ): Result { if (this.scope !== "company") { return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED")); } return this.updateValues(values, actorId, recordedAt); } updateGlobal(values: ResourceValues, actorId: UniqueID, recordedAt: string) { if (this.scope !== "global" || values.toPrimitive().integration) { return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED")); } return this.updateValues(values, actorId, recordedAt); } private updateValues( values: ResourceValues, actorId: UniqueID, recordedAt: string ): Result { if (values.toPrimitive().templateId !== this.values.toPrimitive().templateId) { return Result.fail(new ResourceCatalogError("TEMPLATE_FIXED")); } const previous = this.values.toPrimitive().integration; const next = values.toPrimitive().integration; if ( previous?.kind !== next?.kind || (previous?.kind === "mailbox" && next?.kind === "mailbox" && (previous.mailboxKind !== next.mailboxKind || previous.addressFieldId !== next.addressFieldId || previous.contextFieldId !== next.contextFieldId)) || (previous?.kind === "access" && next?.kind === "access" && (previous.referenceFieldId !== next.referenceFieldId || previous.resourceTypeId !== next.resourceTypeId || previous.systemId !== next.systemId || previous.environmentId !== next.environmentId)) ) { return Result.fail(new ResourceCatalogError("CONTEXT_FIXED")); } return Result.ok( this.values.equals(values) ? this : new Resource( { ...this.props, values, history: [ ...this.history, ResourceRevision.create({ values, specification: this.specification, actorId, recordedAt, }), ], }, this.id ) ); } }