2026-10-05 11:13:51 +00:00
import type { UniqueID } from "@repo/rdx-ddd" ;
import { Maybe , Result } from "@repo/rdx-utils" ;
import type { Sequelize , Transaction } from "sequelize" ;
import { QueryTypes } from "sequelize" ;
2026-10-05 15:01:32 +00:00
import { z } from "zod/v4" ;
2026-10-05 11:13:51 +00:00
2026-10-05 15:01:32 +00:00
import { ResourceReferenceSchema } from "../../../common/inventory" ;
2026-10-05 11:13:51 +00:00
import type { IResourceRepository , InventoryContext } from "../../application/resource.repository" ;
import { ResourceCatalogError } from "../../domain/resource-definition" ;
2026-10-17 11:58:29 +00:00
import { removeResource } from "./remove-resource" ;
2026-10-05 11:13:51 +00:00
import {
ResourceAvailabilityModel ,
ResourceBindingModel ,
ResourceCatalogModel ,
} from "./resource-catalog.model" ;
import { ResourceCatalogPersistenceMapper } from "./resource-catalog-persistence.mapper" ;
2026-10-10 11:32:40 +00:00
import { readDefinitions } from "./resource-catalog-relational" ;
2026-10-05 15:01:32 +00:00
import { projectResource } from "./resource-compatibility" ;
2026-10-10 11:32:40 +00:00
import { readInventory , writeInventory } from "./resource-inventory-relational" ;
2026-10-05 11:13:51 +00:00
import { ResourcePersistenceMapper } from "./resource-persistence.mapper" ;
export class SequelizeResourceRepository implements IResourceRepository {
private readonly mapper = new ResourcePersistenceMapper ( ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
constructor ( private readonly database : Sequelize ) { }
2026-10-10 10:01:58 +00:00
2026-10-17 11:58:29 +00:00
remove ( companyId : UniqueID , id : UniqueID ) {
return this . database . transaction ( async ( transaction ) = > {
const loaded = await this . load ( companyId , transaction , true ) ;
if ( ! loaded . isSuccess ) {
return Result . fail ( loaded . error ) ;
}
const resource = loaded . data . resources . find ( ( item ) = > item . id . equals ( id ) ) ;
if ( ! resource ) {
return Result . fail ( new ResourceCatalogError ( "RESOURCE_NOT_FOUND" ) ) ;
}
return removeResource ( this . database , resource , transaction ) ;
} ) ;
}
2026-10-05 11:13:51 +00:00
private async load (
companyId : UniqueID ,
transaction : Transaction ,
write : boolean
) : Promise < Result < InventoryContext , ResourceCatalogError > > {
// Orden compartido con las escrituras del catálogo para evitar interbloqueos.
const root = await ResourceCatalogModel . findOne ( {
where : { catalogKey : "global" } ,
transaction ,
. . . ( write ? { lock : transaction.LOCK.UPDATE } : { } ) ,
} ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
if ( ! root ) {
throw new Error ( "Catálogo de recursos no inicializado." ) ;
}
const companies = await this . database . query < { id : string } > (
` SELECT id FROM companies WHERE id=:id ${ write ? " FOR UPDATE" : "" } ` ,
{ replacements : { id : companyId.toPrimitive ( ) } , type : QueryTypes . SELECT , transaction }
) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
if ( ! companies . length ) {
return Result . fail ( new ResourceCatalogError ( "COMPANY_NOT_FOUND" ) ) ;
}
2026-10-10 11:32:40 +00:00
const definitions = await readDefinitions ( this . database , root . id , transaction ) ;
2026-10-05 11:13:51 +00:00
const bindings = definitions . length
? await ResourceBindingModel . findAll ( {
where : { templateId : definitions.map ( ( item ) = > item . id ) } ,
transaction ,
} )
: [ ] ;
const overrides = await ResourceAvailabilityModel . findAll ( {
where : { catalogId : root.id } ,
transaction ,
} ) ;
const catalog = new ResourceCatalogPersistenceMapper ( ) . toDomain (
root ,
definitions ,
bindings ,
overrides
) ;
2026-10-10 11:32:40 +00:00
const { rows , revisions } = await readInventory (
this . database ,
companyId . toPrimitive ( ) ,
transaction
) ;
2026-10-05 11:13:51 +00:00
const resources = rows . map ( ( row ) = > {
const resource = this . mapper . toDomain (
row ,
revisions . filter ( ( revision ) = > revision . resourceId === row . id )
) ;
const spec = resource . specification . toPrimitive ( ) ;
const template = catalog
. visible ( Maybe . some ( companyId ) )
. find ( ( item ) = > item . id . toPrimitive ( ) === spec . templateId ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
if (
! template ||
template . values . data . kind !== "template" ||
spec . fields . some (
( field ) = >
! catalog
. visible ( Maybe . some ( companyId ) )
. some (
( item ) = >
item . id . toPrimitive ( ) === field . fieldId && item . values . data . kind === "field"
)
)
) {
throw new Error ( "Especificación de recurso fuera de su catálogo o compañía." ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
return resource ;
} ) ;
const actors = await this . database . query < { id : string ; name : string } > (
"SELECT id, name FROM users" ,
{ type : QueryTypes . SELECT , transaction }
) ;
2026-10-10 14:40:51 +00:00
const rawReferences = await this . database . query < Record < string , unknown > > (
"SELECT i.id,v.kind,v.name,v.identifier,v.active,v.systemId FROM access_items i JOIN access_workspaces w ON w.id=i.workspaceId JOIN access_current_values v ON v.ownerId=i.id AND v.kind=i.kind WHERE (w.companyId=:companyId OR w.companyId IS NULL) AND i.kind IN ('account','system','environment','resource_type')" ,
2026-10-05 15:01:32 +00:00
{ replacements : { companyId : companyId.toPrimitive ( ) } , transaction , type : QueryTypes . SELECT }
) ;
const references = rawReferences . map ( ( row ) = > {
const data = z
. object ( {
kind : z.enum ( [ "account" , "system" , "environment" , "resource_type" ] ) ,
2026-10-10 14:40:51 +00:00
name : z.string ( ) . nullish ( ) ,
identifier : z.string ( ) . nullish ( ) ,
active : z.union ( [ z . boolean ( ) , z . literal ( 0 ) , z . literal ( 1 ) ] ) . transform ( Boolean ) ,
systemId : z.uuid ( ) . nullish ( ) ,
2026-10-05 15:01:32 +00:00
} )
2026-10-10 14:40:51 +00:00
. parse ( row ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 15:01:32 +00:00
return ResourceReferenceSchema . parse ( {
id : row.id ,
kind : data.kind ,
name : data.name ? ? data . identifier ? ? "" ,
active : data.active ,
systemId : data.systemId ? ? null ,
} ) ;
} ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
return Result . ok ( {
2026-10-05 15:01:32 +00:00
references ,
2026-10-05 11:13:51 +00:00
catalog ,
resources ,
actorNames : new Map ( actors . map ( ( actor ) = > [ actor . id , actor . name ] ) ) ,
} ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
get ( companyId : UniqueID ) {
return this . database . transaction ( ( transaction ) = > this . load ( companyId , transaction , false ) ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
save ( companyId : UniqueID , action : Parameters < IResourceRepository [ " save " ] > [ 1 ] ) {
return this . database . transaction ( async ( transaction ) = > {
const loaded = await this . load ( companyId , transaction , true ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
if ( ! loaded . isSuccess ) {
return loaded ;
}
const changed = action ( loaded . data ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
if ( ! changed . isSuccess ) {
return changed ;
}
const persistence = this . mapper . toPersistence ( changed . data ) ;
2026-10-10 10:01:58 +00:00
2026-10-05 15:01:32 +00:00
await projectResource ( this . database , changed . data , transaction ) ;
2026-10-10 11:32:40 +00:00
await writeInventory (
this . database ,
persistence ,
new Set (
loaded . data . resources
. find ( ( item ) = > item . id . equals ( changed . data . id ) )
? . history . map ( ( revision ) = > revision . id . toPrimitive ( ) ) ? ? [ ]
) ,
transaction
) ;
2026-10-10 10:01:58 +00:00
2026-10-05 11:13:51 +00:00
return Result . ok ( { resource : changed.data , actorNames : loaded.data.actorNames } ) ;
} ) ;
}
}