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" ;
2026-10-21 09:25:23 +00:00
import { QueryTypes , UniqueConstraintError } 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-20 11:20:22 +00:00
import type { IDirectoryContextWriter } from "../../application/directory-context-writer" ;
2026-10-05 11:13:51 +00:00
import type { IResourceRepository , InventoryContext } from "../../application/resource.repository" ;
2026-10-21 09:25:23 +00:00
import type { IResourceAccessWriter } from "../../application/resource-access-writer" ;
2026-10-05 11:13:51 +00:00
import { ResourceCatalogError } from "../../domain/resource-definition" ;
2026-10-20 11:20:22 +00:00
import { projectDirectoryDomain , readDirectoryDomains } from "./directory-domain-projection" ;
2026-10-21 09:25:23 +00:00
import {
networkShareResponse ,
projectNetworkShare ,
readNetworkShares ,
} from "./network-share-projection" ;
import { prepareSmbAccess } from "./prepare-smb-access" ;
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" ;
2026-10-24 23:19:17 +00:00
import { ResourceScopeSchemaReader } from "./resource-scope-schema" ;
2026-10-05 11:13:51 +00:00
export class SequelizeResourceRepository implements IResourceRepository {
private readonly mapper = new ResourcePersistenceMapper ( ) ;
2026-10-24 23:19:17 +00:00
private readonly schema : ResourceScopeSchemaReader ;
2026-10-10 10:01:58 +00:00
2026-10-20 11:20:22 +00:00
constructor (
private readonly database : Sequelize ,
2026-10-21 09:25:23 +00:00
private readonly directoryContexts ? : ( transaction : Transaction ) = > IDirectoryContextWriter ,
2026-10-24 23:19:17 +00:00
private readonly accessWriters ? : ( transaction : Transaction ) = > IResourceAccessWriter ,
private readonly readTransaction? : Transaction
) {
this . schema = new ResourceScopeSchemaReader ( database ) ;
}
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 > > {
2026-10-24 23:19:17 +00:00
const mode = await this . schema . read ( ) ;
2026-10-05 11:13:51 +00:00
// 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." ) ;
}
2026-10-21 09:25:23 +00:00
if ( write && ( this . directoryContexts || this . accessWriters ) ) {
2026-10-20 11:20:22 +00:00
// Orden: catálogo de recursos, catálogo de accesos, compañía y workspace de accesos.
await this . database . query (
"SELECT id FROM access_workspaces WHERE scopeKey='global' FOR UPDATE" ,
{ transaction , type : QueryTypes . SELECT }
) ;
}
2026-10-05 11:13:51 +00:00
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 ) = > {
2026-10-24 23:19:17 +00:00
const history = revisions . filter ( ( revision ) = > revision . resourceId === row . id ) ;
const resource =
mode === "scoped"
? this . mapper . toScopedDomain ( row , history )
: this . mapper . toDomain ( row , history ) ;
if ( ! resource . belongsToCompany ( companyId ) ) {
throw new ResourceCatalogError ( "INVALID_RESOURCE_SCOPE" ) ;
}
2026-10-05 11:13:51 +00:00
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-21 09:25:23 +00:00
const shareRows = await readNetworkShares ( this . database , companyId . toPrimitive ( ) , transaction ) ;
const networkShares = new Map (
shareRows . map ( ( row ) = > {
const resource = resources . find ( ( item ) = > item . id . toPrimitive ( ) === row . resourceId ) ;
if ( ! resource ) {
throw new Error ( "Recurso SMB fuera de su compañía." ) ;
}
return [ row . resourceId , networkShareResponse ( resource , catalog , row . normalizedAddress ) ] ;
} )
) ;
2026-10-05 11:13:51 +00:00
return Result . ok ( {
2026-10-21 09:25:23 +00:00
networkShares ,
2026-10-20 11:20:22 +00:00
directoryDomains : await readDirectoryDomains (
this . database ,
companyId . toPrimitive ( ) ,
transaction
) ,
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-24 23:19:17 +00:00
private persistence (
resource : Parameters < ResourcePersistenceMapper [ " toPersistence " ] > [ 0 ] ,
mode : "legacy" | "scoped"
) {
return mode === "scoped"
? this . mapper . toScopedPersistence ( resource )
: this . mapper . toPersistence ( resource ) ;
}
2026-10-05 11:13:51 +00:00
get ( companyId : UniqueID ) {
2026-10-24 23:19:17 +00:00
return this . readTransaction
? this . load ( companyId , this . readTransaction , false )
: this . database . transaction ( ( transaction ) = > this . load ( companyId , transaction , false ) ) ;
2026-10-05 11:13:51 +00:00
}
2026-10-10 10:01:58 +00:00
2026-10-21 09:25:23 +00:00
async save (
companyId : UniqueID ,
action : Parameters < IResourceRepository [ " save " ] > [ 1 ] ,
creation? : Parameters < IResourceRepository [ " save " ] > [ 2 ]
) {
2026-10-20 11:20:22 +00:00
try {
return await this . database . transaction ( async ( transaction ) = > {
const loaded = await this . load ( companyId , transaction , true ) ;
if ( ! loaded . isSuccess ) {
return loaded ;
}
2026-10-21 09:25:23 +00:00
const accessWriter = this . accessWriters ? . ( transaction ) ;
const context = await prepareSmbAccess ( loaded . data , companyId , creation , accessWriter ) ;
const changed = action ( context ) ;
2026-10-20 11:20:22 +00:00
if ( ! changed . isSuccess ) {
2026-10-21 09:25:23 +00:00
throw changed . error ;
2026-10-20 11:20:22 +00:00
}
2026-10-24 23:19:17 +00:00
if ( ! changed . data . belongsToCompany ( companyId ) ) {
throw new ResourceCatalogError ( "RESOURCE_NOT_FOUND" ) ;
}
const mode = await this . schema . read ( ) ;
const persistence = this . persistence ( changed . data , mode ) ;
2026-10-20 11:20:22 +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
) ;
const directory = await projectDirectoryDomain (
this . database ,
changed . data ,
loaded . data . catalog ,
loaded . data . directoryDomains ? . get ( changed . data . id . toPrimitive ( ) ) ,
this . directoryContexts ? . ( transaction ) ,
transaction
) ;
if ( ! directory . isSuccess ) {
// Lanzar dentro del callback gestionado garantiza rollback de Resource y Companies.
throw directory . error ;
}
2026-10-21 09:25:23 +00:00
const networkShare = await projectNetworkShare (
this . database ,
changed . data ,
loaded . data . catalog ,
loaded . data . resources . find ( ( item ) = > item . id . equals ( changed . data . id ) ) ,
loaded . data . networkShares ? . has ( changed . data . id . toPrimitive ( ) ) ? ? false ,
transaction
) ;
await projectResource ( this . database , changed . data , transaction , accessWriter ) ;
2026-10-20 11:20:22 +00:00
return Result . ok ( {
2026-10-21 09:25:23 +00:00
. . . ( networkShare ? { networkShare } : { } ) ,
2026-10-20 11:20:22 +00:00
resource : changed.data ,
actorNames : loaded.data.actorNames ,
. . . ( directory . data ? { directory : directory.data } : { } ) ,
} ) ;
} ) ;
} catch ( error ) {
2026-10-21 09:25:23 +00:00
if (
error instanceof UniqueConstraintError &&
error . errors . some (
( item ) = >
item . path === "network_share_active_address" ||
item . message . includes ( "network_share_active_address" )
)
) {
return Result . fail ( new ResourceCatalogError ( "SMB_ADDRESS_EXISTS" ) ) ;
}
2026-10-20 11:20:22 +00:00
if ( error instanceof ResourceCatalogError ) {
return Result . fail ( error ) ;
2026-10-05 11:13:51 +00:00
}
2026-10-20 11:20:22 +00:00
throw error ;
}
2026-10-05 11:13:51 +00:00
}
}