import { UniqueID } from "@repo/rdx-ddd"; import { z } from "zod/v4"; import { Resource } from "../../domain/resource.aggregate"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { ResourceRevision } from "../../domain/resource-revision.entity"; import type { ResourceOwnership } from "../../domain/resource-scope"; import { ResourceOwnershipSchema } from "../../domain/resource-scope"; import { ResourceInputSchema, ResourceSpecificationSchema } from "../../domain/resource-values"; import { ResourceSpecification, ResourceValues, } from "../../domain/value-objects/resource-values.vo"; import type { InventoryRow, RevisionRow } from "./resource-inventory-relational"; export function inventoryIdentity(value: string) { const parsed = UniqueID.create(value); if (!parsed.isSuccess) { throw new Error("Identidad de recurso persistida inconsistente."); } return parsed.data; } export type ScopedInventoryRow = Omit & ResourceOwnership; /** Único fallback legacy, reservado al origen de persistencia conocido. */ export function readResourceOwnership(row: { companyId: unknown }, mode: "legacy" | "scoped") { const scope: unknown = mode === "legacy" && !Object.hasOwn(row, "scope") ? "company" : Reflect.get(row, "scope"); const parsed = ResourceOwnershipSchema.safeParse({ scope, companyId: row.companyId }); if (!parsed.success) { throw new ResourceCatalogError("INVALID_RESOURCE_SCOPE"); } return parsed.data; } export class ResourcePersistenceMapper { /** Entrada de la tabla legacy conocida: solo aquí se admite la ausencia de scope. */ toDomain(row: InventoryRow, revisions: readonly RevisionRow[]) { return this.read(row, revisions, readResourceOwnership(row, "legacy")); } toScopedDomain(row: ScopedInventoryRow | InventoryRow, revisions: readonly RevisionRow[]) { return this.read(row, revisions, readResourceOwnership(row, "scoped")); } private read( row: Omit, revisions: readonly RevisionRow[], ownership: ResourceOwnership ) { const owner = ownership.scope === "company" ? { scope: ownership.scope, companyId: inventoryIdentity(ownership.companyId) } : { scope: ownership.scope, companyId: null }; const specification = ResourceSpecification.create( ResourceSpecificationSchema.parse(row.specification) ); if (!specification.isSuccess) { throw new Error("Especificación de recurso persistida inconsistente."); } const values = ResourceValues.reconstruct( ResourceInputSchema.parse(row.values), specification.data ); if (!values.isSuccess) { throw new Error("Valores de recurso persistidos inconsistentes."); } const current = values.data.toPrimitive(); if (row.templateId !== current.templateId) { throw new Error("Referencia de recurso persistida inconsistente."); } const ordered = [...revisions].sort((a, b) => a.position - b.position); const history = ordered.map((revision, position) => { const revisionSpecification = ResourceSpecification.create( ResourceSpecificationSchema.parse(revision.specification ?? row.specification) ); if (!revisionSpecification.isSuccess) { throw new Error("Especificación histórica inconsistente."); } const previous = ResourceValues.reconstruct( ResourceInputSchema.parse(revision.values), revisionSpecification.data ); if ( !previous.isSuccess || revision.position !== position || (position > 0 && revision.recordedAt < (ordered[position - 1]?.recordedAt ?? "")) || !z.iso.datetime().safeParse(revision.recordedAt).success ) { throw new Error("Historial de recurso persistido inconsistente."); } if (revision.resourceId !== row.id) { throw new Error("Revisión fuera del recurso."); } return ResourceRevision.rehydrate( { values: previous.data, specification: revisionSpecification.data, actorId: inventoryIdentity(revision.actorId), recordedAt: revision.recordedAt, }, inventoryIdentity(revision.id) ); }); if (!(history.length && history.at(-1)?.values.equals(values.data))) { throw new Error("Última revisión de recurso inconsistente."); } return Resource.rehydrate( { ...owner, specification: specification.data, values: values.data, history, }, inventoryIdentity(row.id) ); } toScopedPersistence(resource: Resource) { const saved = this.toPersistence(resource); return { ...saved, resource: { ...saved.resource, scope: "company" as const } }; } toPersistence(resource: Resource) { const values = resource.values.toPrimitive(); return { resource: { id: resource.id.toPrimitive(), companyId: resource.requireCompanyId().toPrimitive(), templateId: values.templateId, specification: resource.specification.toPrimitive(), values, }, revisions: this.revisions(resource), }; } toGlobalPersistence(resource: Resource) { if ( resource.scope !== "global" || resource.companyId !== null || resource.values.toPrimitive().integration ) { throw new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"); } return { resource: { id: resource.id.toPrimitive(), scope: "global" as const, companyId: null, templateId: resource.values.toPrimitive().templateId, specification: resource.specification.toPrimitive(), values: resource.values.toPrimitive(), }, revisions: this.revisions(resource), }; } private revisions(resource: Resource) { return resource.history.map((revision, position) => ({ id: revision.id.toPrimitive(), resourceId: resource.id.toPrimitive(), actorId: revision.actorId.toPrimitive(), recordedAt: revision.recordedAt, values: revision.values.toPrimitive(), specification: revision.specification.toPrimitive(), position, })); } }