import type { UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import type { Sequelize, Transaction } from "sequelize"; import { QueryTypes } from "sequelize"; import { z } from "zod/v4"; import type { ListGlobalApplicationHistoryRequestDTO } from "../../../common"; import { GlobalResourceAdminSchema, ListGlobalApplicationHistoryResponseSchema, ResourceOwnershipSchema, } from "../../../common"; import type { IGlobalResourceRepository } from "../../application/global-resource.repository"; import { captureGlobalResourceSpecification } from "../../application/global-resource-specification"; import { ResourceCatalogError } from "../../domain/resource-definition"; import { ResourceBindingModel, ResourceCatalogModel } from "./resource-catalog.model"; import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper"; import { readDefinitions } from "./resource-catalog-relational"; import { readInventoryByIds, writeGlobalInventory } from "./resource-inventory-relational"; import { ResourcePersistenceMapper } from "./resource-persistence.mapper"; import { readResourceReferenceMetadata } from "./resource-reference-metadata"; import { ResourceScopeSchemaReader } from "./resource-scope-schema"; const HistoryCursorSchema = z.strictObject({ version: z.literal(1), resourceId: z.uuid(), position: z.number().int().nonnegative(), }); const HistoryIdentitySchema = z.object({ id: z.uuid(), position: z.number().int().nonnegative() }); export class SequelizeGlobalResourceRepository implements IGlobalResourceRepository { private readonly schema: ResourceScopeSchemaReader; private readonly mapper = new ResourcePersistenceMapper(); constructor(private readonly database: Sequelize) { this.schema = new ResourceScopeSchemaReader(database); } private async catalog(transaction: Transaction, write: boolean) { const root = await ResourceCatalogModel.findOne({ where: { catalogKey: "global" }, transaction, ...(write ? { lock: transaction.LOCK.UPDATE } : {}), }); if (!root) { throw new Error("Catálogo de recursos no inicializado."); } const definitions = await readDefinitions(this.database, root.id, transaction); const bindings = await ResourceBindingModel.findAll({ where: { templateId: definitions.map((item) => item.id) }, transaction, }); return new ResourceCatalogPersistenceMapper().toDomain(root, definitions, bindings, []); } template(options = { existing: true }) { return this.database.transaction(async (transaction) => { const catalog = await this.catalog(transaction, false); const template = catalog.definitions.find( (item) => item.stockKey.getOrUndefined() === "template:application" ); return template ? this.specificationResponse( captureGlobalResourceSpecification(catalog, template.id.toPrimitive(), options) ) : Result.fail(new ResourceCatalogError("TEMPLATE_UNAVAILABLE")); }); } private specificationResponse(result: ReturnType) { return result.isSuccess ? Result.ok(result.data.toPrimitive()) : result; } private async isGlobal(id: UniqueID, transaction: Transaction) { const rows = await this.database.query<{ companyId: unknown; scope?: unknown; templateId: string; }>("SELECT * FROM resources WHERE id=:id", { replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT, }); const root = rows[0]; if (!root) { return false; } const mode = await this.schema.read(); const ownership = ResourceOwnershipSchema.parse({ scope: mode === "legacy" ? "company" : root.scope, companyId: root.companyId, }); if (ownership.scope === "global") { await readResourceReferenceMetadata(this.database, transaction, mode, { globalOnly: true, ids: [id.toPrimitive()], }); } return ownership.scope === "global"; } admin(id: UniqueID) { return this.database.transaction(async (transaction) => { if (!(await this.isGlobal(id, transaction))) { return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND")); } const catalog = await this.catalog(transaction, false); const latest = await this.database.query<{ id: string }>( "SELECT id FROM resource_revisions WHERE resourceId=:id ORDER BY position DESC LIMIT 1", { replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT } ); const loaded = await readInventoryByIds( this.database, [id.toPrimitive()], transaction, latest.map((item) => item.id) ); const row = loaded.rows[0]; if (!(row && latest[0])) { throw new Error("Raíz global sin revisión actual."); } const eligible = captureGlobalResourceSpecification(catalog, row.templateId, { existing: true, }); if (!eligible.isSuccess) { throw new Error("Plantilla global persistida incompatible."); } return Result.ok( GlobalResourceAdminSchema.parse({ id: row.id, scope: "global", companyId: null, revisionId: latest[0].id, values: row.values, specification: row.specification, }) ); }); } private async writeContext(id: UniqueID, transaction: Transaction) { const mode = await this.schema.read(); const catalog = await this.catalog(transaction, true); const roots = await this.database.query<{ companyId: unknown; scope?: unknown }>( "SELECT * FROM resources WHERE id=:id FOR UPDATE", { replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT } ); const root = roots[0]; const ownership = root ? ResourceOwnershipSchema.parse({ scope: mode === "legacy" ? "company" : root.scope, companyId: root.companyId, }) : undefined; const loaded = ownership?.scope === "global" ? await readInventoryByIds(this.database, [id.toPrimitive()], transaction) : undefined; const row = loaded?.rows[0]; const previous = row && loaded ? this.mapper.toScopedDomain(row, loaded.revisions) : undefined; if (previous) { await readResourceReferenceMetadata(this.database, transaction, mode, { globalOnly: true, ids: [id.toPrimitive()], }); } if ( previous && !captureGlobalResourceSpecification(catalog, previous.values.toPrimitive().templateId, { existing: true, }).isSuccess ) { throw new Error("Plantilla global persistida incompatible."); } return { mode, catalog, root, previous, persistedRevisionIds: new Set(previous?.history.map((item) => item.id.toPrimitive()) ?? []), }; } async write(id: UniqueID, action: Parameters[1]) { try { return await this.database.transaction(async (transaction) => { const { mode, catalog, root, previous, persistedRevisionIds } = await this.writeContext( id, transaction ); const changed = action({ catalog, resource: previous ? Maybe.some(previous) : Maybe.none(), occupied: Boolean(root), }); if (!changed.isSuccess) { return changed; } if (mode !== "scoped") { return Result.fail(new ResourceCatalogError("RESOURCE_GLOBAL_FEATURE_DISABLED")); } if (!changed.data.id.equals(id) || changed.data.scope !== "global") { throw new ResourceCatalogError("RESOURCE_OWNERSHIP_FIXED"); } const eligible = captureGlobalResourceSpecification( catalog, changed.data.values.toPrimitive().templateId, { existing: true } ); if (!eligible.isSuccess) { return eligible; } if (changed.data !== previous) { await writeGlobalInventory( this.database, this.mapper.toGlobalPersistence(changed.data), persistedRevisionIds, transaction ); } const revision = changed.data.history.at(-1); if (!revision) { throw new Error("Recurso global sin revisión."); } return Result.ok({ created: !root, resource: GlobalResourceAdminSchema.parse({ id: changed.data.id.toPrimitive(), scope: "global", companyId: null, revisionId: revision.id.toPrimitive(), values: changed.data.values.toPrimitive(), specification: changed.data.specification.toPrimitive(), }), }); }); } catch (error) { if (error instanceof ResourceCatalogError) { return Result.fail(error); } throw error; } } history(id: UniqueID, query: ListGlobalApplicationHistoryRequestDTO) { return this.database.transaction(async (transaction) => { if (!(await this.isGlobal(id, transaction))) { return Result.fail(new ResourceCatalogError("RESOURCE_NOT_FOUND")); } let position: number | null = null; if (query.cursor) { try { const cursor = HistoryCursorSchema.parse( JSON.parse(Buffer.from(query.cursor, "base64url").toString("utf8")) ); if (cursor.resourceId !== id.toPrimitive()) { return Result.fail(new ResourceCatalogError("INVALID_REQUEST")); } position = cursor.position; } catch { return Result.fail(new ResourceCatalogError("INVALID_REQUEST")); } } const identities = z.array(HistoryIdentitySchema).parse( await this.database.query( "SELECT id,position FROM resource_revisions WHERE resourceId=:id AND (:position IS NULL OR position < :position) ORDER BY position DESC LIMIT :limit", { replacements: { id: id.toPrimitive(), position, limit: query.pageSize + 1 }, transaction, type: QueryTypes.SELECT, } ) ); const page = identities.slice(0, query.pageSize); const loaded = await readInventoryByIds( this.database, [id.toPrimitive()], transaction, page.map((item) => item.id) ); const actors = page.length ? await this.database.query<{ id: string; name: string }>( "SELECT u.id,u.name FROM users u JOIN resource_revisions r ON r.actorId=u.id WHERE r.id IN (:ids)", { replacements: { ids: page.map((item) => item.id) }, transaction, type: QueryTypes.SELECT, } ) : []; const last = page.at(-1); return Result.ok( ListGlobalApplicationHistoryResponseSchema.parse({ items: page.map((item) => { const revision = loaded.revisions.find((entry) => entry.id === item.id); if (!revision) { throw new Error("Revisión global ausente."); } return { id: revision.id, values: revision.values, specification: revision.specification, actorId: revision.actorId, actorName: actors.find((actor) => actor.id === revision.actorId)?.name ?? revision.actorId, recordedAt: revision.recordedAt, }; }), nextCursor: last && identities.length > page.length ? Buffer.from( JSON.stringify({ version: 1, resourceId: id.toPrimitive(), position: last.position, }) ).toString("base64url") : null, }) ); }); } }