2026-10-04 16:36:09 +00:00
import type { UniqueID } from "@repo/rdx-ddd" ;
2026-10-04 19:04:14 +00:00
import { Maybe , Result } from "@repo/rdx-utils" ;
2026-10-04 16:36:09 +00:00
import type { Sequelize , Transaction } from "sequelize" ;
import { QueryTypes } from "sequelize" ;
import type { IAssetRepository , InventoryContext } from "../../application/asset.repository" ;
import { AssetCatalogError } from "../../domain/asset-definition" ;
2026-10-04 19:04:14 +00:00
import type { Workstation } from "../../domain/workstation.aggregate" ;
2026-10-04 16:36:09 +00:00
2026-10-10 09:41:42 +00:00
import { AvailabilityModel , CatalogModel } from "./asset-catalog.model" ;
2026-10-04 16:36:09 +00:00
import { AssetCatalogPersistenceMapper } from "./asset-catalog-persistence.mapper" ;
2026-10-10 09:41:42 +00:00
import { readBindings , readDefinitions } from "./asset-catalog-relational" ;
import { readInventory , writeInventory } from "./asset-inventory-relational" ;
2026-10-04 16:36:09 +00:00
import { AssetPersistenceMapper , inventoryIdentity } from "./asset-persistence.mapper" ;
2026-10-10 20:47:23 +00:00
import { sqlIdentifier } from "./asset-relational-sql" ;
2026-10-04 19:04:14 +00:00
import { RelationshipModel } from "./asset-relationship.model" ;
import { AssetRelationshipPersistenceMapper } from "./asset-relationship-persistence.mapper" ;
import { loadWorkstations } from "./load-workstations" ;
2026-10-07 16:08:48 +00:00
import { readBrands } from "./sequelize-brand.repository" ;
2026-10-10 11:32:40 +00:00
import { WorkstationAssetModel } from "./workstation.model" ;
2026-10-04 19:04:14 +00:00
import { WorkstationPersistenceMapper } from "./workstation-persistence.mapper" ;
2026-10-10 11:32:40 +00:00
import { writeWorkstation } from "./workstation-relational" ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
export class SequelizeAssetRepository implements IAssetRepository {
private readonly mapper = new AssetPersistenceMapper ( ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
constructor ( private readonly database : Sequelize ) { }
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
private async load (
companyId : UniqueID ,
transaction : Transaction ,
write : boolean
) : Promise < Result < InventoryContext , AssetCatalogError > > {
// Orden fijo de bloqueos: catálogo antes de compañía, compartido con las escrituras del catálogo.
const root = await CatalogModel . findOne ( {
where : { catalogKey : "global" } ,
transaction ,
. . . ( write ? { lock : transaction.LOCK.UPDATE } : { } ) ,
} ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
if ( ! root ) {
throw new Error ( "Catálogo no inicializado." ) ;
}
const replacements = { id : companyId.toPrimitive ( ) } ;
const companies = await this . database . query < { id : string } > (
` SELECT id FROM companies WHERE id=:id ${ write ? " FOR UPDATE" : "" } ` ,
{ replacements , type : QueryTypes . SELECT , transaction }
) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
if ( ! companies . length ) {
return Result . fail ( new AssetCatalogError ( "COMPANY_NOT_FOUND" ) ) ;
}
2026-10-10 09:41:42 +00:00
const definitions = await readDefinitions ( this . database , root . id , transaction ) ;
const bindings = await readBindings (
this . database ,
definitions . map ( ( item ) = > item . id ) ,
transaction
) ;
2026-10-04 16:36:09 +00:00
const overrides = await AvailabilityModel . findAll ( {
where : { catalogId : root.id } ,
transaction ,
} ) ;
const catalog = new AssetCatalogPersistenceMapper ( ) . toDomain (
root ,
definitions ,
bindings ,
overrides
) ;
2026-10-10 09:41:42 +00:00
const { rows , revisions } = await readInventory (
this . database ,
companyId . toPrimitive ( ) ,
transaction
) ;
2026-10-04 16:36:09 +00:00
const sites = await this . database . query < { id : string } > (
"SELECT id FROM company_sites WHERE companyId=:id" ,
{ replacements , type : QueryTypes . SELECT , transaction }
) ;
const people = await this . database . query < { id : string ; active : boolean } > (
"SELECT id, active FROM company_contacts WHERE companyId=:id" ,
{ replacements , type : QueryTypes . SELECT , transaction }
) ;
const actors = await this . database . query < { id : string ; name : string } > (
"SELECT id, name FROM users" ,
{ type : QueryTypes . SELECT , transaction }
) ;
const assets = rows . map ( ( row ) = > {
const asset = this . mapper . toDomain (
row ,
revisions . filter ( ( revision ) = > revision . assetId === row . id )
) ;
const spec = asset . specification . toPrimitive ( ) ;
const template = definitions . find ( ( item ) = > item . id === spec . templateId ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
if (
! template ||
( template . companyId !== null && template . companyId !== companyId . toPrimitive ( ) )
) {
throw new Error ( "Plantilla de activo fuera de su compañía." ) ;
}
for ( const revision of asset . history ) {
const values = revision . values . toPrimitive ( ) ;
2026-10-10 10:01:58 +00:00
2026-10-09 23:37:10 +00:00
if ( values . siteId && ! sites . some ( ( site ) = > site . id === values . siteId ) ) {
2026-10-04 16:36:09 +00:00
throw new Error ( "Asignación de inventario fuera de su compañía." ) ;
}
}
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
return asset ;
} ) ;
2026-10-04 19:04:14 +00:00
const relationships = (
await RelationshipModel . findAll ( {
where : { companyId : companyId.toPrimitive ( ) } ,
transaction ,
} )
) . map ( ( row ) = > new AssetRelationshipPersistenceMapper ( ) . toDomain ( row ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
for ( const relationship of relationships ) {
const values = relationship . values ;
const definition = catalog
. visible ( Maybe . some ( companyId ) )
. find ( ( item ) = > item . id . equals ( values . typeId ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
if (
! (
definition &&
definition . values . data . kind === "relation" &&
definition . values . data . symmetric === values . symmetric &&
[ values . sourceId , values . targetId ] . every ( ( id ) = >
assets . some ( ( asset ) = > asset . id . equals ( id ) )
)
)
) {
throw new Error ( "Relación de activos fuera de su compañía." ) ;
}
}
const workstations = await loadWorkstations ( companyId , transaction , assets , sites , people ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
return Result . ok ( {
2026-10-04 19:04:14 +00:00
workstations ,
2026-10-07 16:08:48 +00:00
brands : await readBrands ( this . database , transaction , write ) ,
2026-10-04 19:04:14 +00:00
relationships ,
2026-10-04 16:36:09 +00:00
catalog ,
assets ,
sites : sites.map ( ( site ) = > inventoryIdentity ( site . id ) ) ,
people : people.map ( ( person ) = > ( {
id : inventoryIdentity ( person . id ) ,
active : Boolean ( person . active ) ,
} ) ) ,
actorNames : new Map ( actors . map ( ( actor ) = > [ actor . id , actor . name ] ) ) ,
} ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
saveWorkstation ( companyId : UniqueID , action : Parameters < IAssetRepository [ " saveWorkstation " ] > [ 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-04 19:04:14 +00:00
if ( ! loaded . isSuccess ) {
return Result . fail ( loaded . error ) ;
}
const changed = action ( loaded . data ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
if ( ! changed . isSuccess ) {
return Result . fail ( changed . error ) ;
}
const previous = loaded . data . workstations . find ( ( item ) = > item . id . equals ( changed . data . id ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
if ( previous !== changed . data ) {
await this . persistWorkstation ( changed . data , previous , transaction ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
return Result . ok ( { workstation : changed.data , actorNames : loaded.data.actorNames } ) ;
} ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-13 07:30:28 +00:00
removeWorkstation ( companyId : UniqueID , id : UniqueID ) : Promise < Result < void , AssetCatalogError > > {
return this . database . transaction ( async ( transaction ) = > {
const loaded = await this . load ( companyId , transaction , true ) ;
if ( ! loaded . isSuccess ) {
return Result . fail ( loaded . error ) ;
}
if ( ! loaded . data . workstations . some ( ( item ) = > item . id . equals ( id ) ) ) {
return Result . fail ( new AssetCatalogError ( "WORKSTATION_NOT_FOUND" ) ) ;
}
const references = await this . database . query < { tableName : string ; columnName : string } > (
"SELECT TABLE_NAME AS tableName,COLUMN_NAME AS columnName FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA=DATABASE() AND REFERENCED_TABLE_NAME='workstations' AND REFERENCED_COLUMN_NAME='id'" ,
{ type : QueryTypes . SELECT , transaction }
) ;
const owned = new Set ( [ "workstation_assets" , "workstation_members" , "workstation_revisions" ] ) ;
const replacements = { id : id.toPrimitive ( ) , companyId : companyId.toPrimitive ( ) } ;
for ( const reference of references ) {
if ( owned . has ( reference . tableName ) ) {
continue ;
}
const rows = await this . database . query < { value : number } > (
` SELECT 1 AS value FROM ${ sqlIdentifier ( reference . tableName ) } WHERE ${ sqlIdentifier ( reference . columnName ) } =:id LIMIT 1 ` ,
{ replacements , transaction , type : QueryTypes . SELECT }
) ;
if ( rows . length ) {
return Result . fail ( new AssetCatalogError ( "WORKSTATION_IN_USE" ) ) ;
}
}
// Las asociaciones de composición se eliminan; sus activos no se borran.
2026-10-13 08:19:01 +00:00
await this . database . query ( "DELETE FROM workstation_assets WHERE workstationId=:id" , {
replacements ,
transaction ,
} ) ;
await this . database . query ( "DELETE FROM workstation_revisions WHERE workstationId=:id" , {
replacements ,
transaction ,
} ) ;
await this . database . query ( "DELETE FROM workstations WHERE companyId=:companyId AND id=:id" , {
replacements ,
transaction ,
} ) ;
2026-10-13 07:30:28 +00:00
return Result . ok ( undefined ) ;
} ) ;
}
2026-10-04 19:04:14 +00:00
private async persistWorkstation (
item : Workstation ,
previous : Workstation | undefined ,
transaction : Transaction
) {
const rows = new WorkstationPersistenceMapper ( ) . toPersistence ( item ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
const existing = new Set ( previous ? . history . map ( ( item ) = > item . id . toPrimitive ( ) ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-10 11:32:40 +00:00
await writeWorkstation ( this . database , rows , existing , transaction ) ;
2026-10-04 19:04:14 +00:00
for ( const reference of rows . references ) {
await WorkstationAssetModel . upsert ( reference , { transaction } ) ;
}
}
2026-10-10 10:01:58 +00:00
2026-10-10 20:47:23 +00:00
private async hasExternalReferences ( assetId : string , transaction : Transaction ) {
const references = await this . database . query < { tableName : string ; columnName : string } > (
"SELECT TABLE_NAME AS tableName,COLUMN_NAME AS columnName FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA=DATABASE() AND REFERENCED_TABLE_NAME='assets' AND REFERENCED_COLUMN_NAME='id'" ,
{ type : QueryTypes . SELECT , transaction }
) ;
const ownedTables = new Set ( [
"asset_characteristics" ,
"asset_revision_characteristics" ,
"asset_revisions" ,
"asset_specification_field_choices" ,
"asset_specification_fields" ,
"asset_specification_sections" ,
] ) ;
for ( const reference of references ) {
if ( ownedTables . has ( reference . tableName ) ) {
continue ;
}
const rows = await this . database . query < { value : number } > (
` SELECT 1 AS value FROM ${ sqlIdentifier ( reference . tableName ) } WHERE ${ sqlIdentifier ( reference . columnName ) } =:assetId LIMIT 1 ` ,
{ replacements : { assetId } , type : QueryTypes . SELECT , transaction }
) ;
if ( rows . length ) {
return true ;
}
}
return false ;
}
private async removeInventory ( assetId : string , companyId : string , transaction : Transaction ) {
for ( const table of [
"asset_revision_characteristics" ,
"asset_characteristics" ,
"asset_specification_field_choices" ,
"asset_specification_fields" ,
"asset_specification_sections" ,
"asset_revisions" ,
] ) {
await this . database . query ( ` DELETE FROM ${ sqlIdentifier ( table ) } WHERE assetId=:assetId ` , {
replacements : { assetId } ,
transaction ,
} ) ;
}
await this . database . query ( "DELETE FROM assets WHERE companyId=:companyId AND id=:assetId" , {
replacements : { assetId , companyId } ,
transaction ,
} ) ;
}
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 asset = loaded . data . assets . find ( ( item ) = > item . id . equals ( id ) ) ;
if ( ! asset ) {
return Result . fail ( new AssetCatalogError ( "ASSET_NOT_FOUND" ) ) ;
}
if ( await this . hasExternalReferences ( id . toPrimitive ( ) , transaction ) ) {
return Result . fail ( new AssetCatalogError ( "ASSET_IN_USE" ) ) ;
}
await this . removeInventory ( id . toPrimitive ( ) , companyId . toPrimitive ( ) , transaction ) ;
return Result . ok ( { asset , actorNames : loaded.data.actorNames } ) ;
} ) ;
}
2026-10-09 22:19:35 +00:00
removeRelationship ( company : UniqueID , id : UniqueID ) : Promise < Result < void , AssetCatalogError > > {
return this . database . transaction ( async ( transaction ) = > {
await this . database . query ( "SELECT id FROM companies WHERE id=:companyId FOR UPDATE" , {
replacements : { companyId : company.toPrimitive ( ) } ,
transaction ,
type : QueryTypes . SELECT ,
} ) ;
const row = await RelationshipModel . findOne ( {
where : { companyId : company.toPrimitive ( ) , id : id.toPrimitive ( ) } ,
transaction ,
lock : transaction.LOCK.UPDATE ,
} ) ;
2026-10-10 10:01:58 +00:00
2026-10-09 22:19:35 +00:00
if ( ! row ) {
return Result . fail ( new AssetCatalogError ( "RELATIONSHIP_NOT_FOUND" ) ) ;
}
await row . destroy ( { transaction } ) ;
2026-10-10 10:01:58 +00:00
2026-10-09 22:19:35 +00:00
return Result . ok ( undefined ) ;
} ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
saveRelationship (
companyId : UniqueID ,
action : Parameters < IAssetRepository [ " saveRelationship " ] > [ 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-04 19:04:14 +00:00
if ( ! loaded . isSuccess ) {
return Result . fail ( loaded . error ) ;
}
const changed = action ( loaded . data ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
if ( ! changed . isSuccess ) {
return Result . fail ( changed . error ) ;
}
const previous = loaded . data . relationships . find ( ( item ) = > item . id . equals ( changed . data . id ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
if ( previous !== changed . data ) {
await RelationshipModel . upsert (
new AssetRelationshipPersistenceMapper ( ) . toPersistence ( changed . data ) ,
{ transaction }
) ;
}
2026-10-10 10:01:58 +00:00
2026-10-04 19:04:14 +00:00
return Result . ok ( { relationship : changed.data , actorNames : loaded.data.actorNames } ) ;
} ) ;
}
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +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-04 16:36:09 +00:00
save ( companyId : UniqueID , action : Parameters < IAssetRepository [ " 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-04 16:36:09 +00:00
if ( ! loaded . isSuccess ) {
return Result . fail ( loaded . error ) ;
}
const changed = action ( loaded . data ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
if ( ! changed . isSuccess ) {
return Result . fail ( changed . error ) ;
}
const previous = loaded . data . assets . find ( ( asset ) = > asset . id . equals ( changed . data . id ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
if ( changed . data !== previous ) {
const rows = this . mapper . toPersistence ( changed . data ) ;
const existing = new Set ( previous ? . history . map ( ( revision ) = > revision . id . toPrimitive ( ) ) ) ;
2026-10-10 10:01:58 +00:00
2026-10-10 09:41:42 +00:00
await writeInventory ( this . database , rows , existing , transaction ) ;
2026-10-04 16:36:09 +00:00
}
2026-10-10 10:01:58 +00:00
2026-10-04 16:36:09 +00:00
return Result . ok ( { asset : changed.data , actorNames : loaded.data.actorNames } ) ;
} ) ;
}
}