2026-10-10 14:40:51 +00:00
import type { ModelAttributeColumnOptions , Sequelize , Transaction } from "sequelize" ;
import { DataTypes , QueryTypes } from "sequelize" ;
import { z } from "zod/v4" ;
import {
AccessDefinitionSchema ,
type AccessKind ,
AccessKindSchema ,
} from "../../domain/access/access-definition" ;
import { supportIdentifier , writeSupportScalars } from "./support-record-relational" ;
const ref = ( model = "access_items" , allowNull = false ) : ModelAttributeColumnOptions = > ( {
type : DataTypes . UUID ,
allowNull ,
references : { model , key : "id" } ,
onDelete : "RESTRICT" ,
} ) ;
const named = {
name : { type : DataTypes . STRING ( 100 ) , allowNull : false } ,
active : { type : DataTypes . BOOLEAN , allowNull : false } ,
} ;
const note = { type : DataTypes . TEXT , allowNull : true } ;
const scope = { systemId : ref ( ) , environmentId : ref ( "access_items" , true ) } ;
const period = {
startDate : { type : DataTypes . STRING ( 10 ) , allowNull : false } ,
endDate : { type : DataTypes . STRING ( 10 ) , allowNull : true } ,
} ;
const configuration = { type : DataTypes . STRING ( 20 ) , allowNull : false } ;
export const accessColumns : Record < AccessKind , Record < string , ModelAttributeColumnOptions > > = {
system : named ,
resource_type : named ,
permission : { . . . named , resourceTypeId : ref ( ) , description : note } ,
template : { . . . named , description : note , systemId : ref ( "access_items" , true ) } ,
environment : { . . . named , systemId : ref ( ) } ,
account : {
. . . scope ,
login : { type : DataTypes . STRING ( 255 ) , allowNull : false } ,
usage : { type : DataTypes . STRING ( 16 ) , allowNull : false } ,
active : named.active ,
expiresOn : { type : DataTypes . STRING ( 10 ) , allowNull : true } ,
notes : note ,
} ,
account_person : {
accountId : ref ( ) ,
personId : ref ( "company_contacts" ) ,
. . . period ,
configuration : { . . . configuration , allowNull : true } ,
} ,
group : {
. . . scope ,
. . . named ,
identifier : { type : DataTypes . STRING ( 255 ) , allowNull : false } ,
description : note ,
templateId : ref ( "access_items" , true ) ,
} ,
membership : { accountId : ref ( ) , groupId : ref ( ) , . . . period , configuration } ,
resource : {
. . . named ,
resourceTypeId : ref ( ) ,
systemId : ref ( "access_items" , true ) ,
environmentId : ref ( "access_items" , true ) ,
reference : { type : DataTypes . STRING ( 255 ) , allowNull : true } ,
notes : note ,
} ,
grant : {
resourceId : ref ( ) ,
permissionId : ref ( ) ,
accountId : ref ( "access_items" , true ) ,
groupId : ref ( "access_items" , true ) ,
. . . period ,
configuration ,
} ,
need : { personId : ref ( "company_contacts" ) , resourceId : ref ( ) , . . . period , notes : note } ,
coverage : {
needId : ref ( ) ,
grantId : ref ( ) ,
accountPersonId : ref ( ) ,
membershipId : ref ( "access_items" , true ) ,
. . . period ,
} ,
} ;
export function accessValueTable ( kind : AccessKind , history = false ) {
return ` access_ ${ kind } _ ${ history ? "revision_" : "" } values ` ;
}
const itemSchema = z . object ( {
id : z.uuid ( ) ,
workspaceId : z.uuid ( ) ,
kind : AccessKindSchema ,
values : AccessDefinitionSchema ,
} ) ;
const revisionSchema = z . object ( {
id : z.uuid ( ) ,
itemId : z.uuid ( ) ,
sequence : z.number ( ) . int ( ) . nonnegative ( ) ,
actorId : z.uuid ( ) ,
recordedAt : z.iso.datetime ( ) ,
values : AccessDefinitionSchema ,
} ) ;
export type AccessItemRow = z . infer < typeof itemSchema > ;
export type AccessRevisionRow = z . infer < typeof revisionSchema > ;
function decoded ( row : Record < string , unknown > , permissionIds : string [ ] ) {
const kind = AccessKindSchema . parse ( row . kind ) ;
const values = Object . fromEntries (
Object . keys ( accessColumns [ kind ] ) . map ( ( key ) = > [
key ,
key === "active"
? z
. union ( [ z . boolean ( ) , z . literal ( 0 ) , z . literal ( 1 ) ] )
. transform ( Boolean )
. parse ( row [ key ] )
: row [ key ] ,
] )
) ;
return AccessDefinitionSchema . parse ( {
kind ,
. . . values ,
. . . ( kind === "need" ? { permissionIds } : { } ) ,
} ) ;
}
export async function readAccessItems (
database : Sequelize ,
transaction : Transaction ,
companyId? : string
) {
const rows = await database . query < Record < string , unknown > > (
` SELECT i.id,i.workspaceId,v.* FROM access_items i JOIN access_workspaces w ON w.id=i.workspaceId LEFT JOIN access_current_values v ON v.ownerId=i.id AND v.kind=i.kind ${ companyId ? " WHERE w.companyId=:companyId OR w.companyId IS NULL" : "" } ` ,
{ replacements : { companyId : companyId ? ? null } , transaction , type : QueryTypes . SELECT }
) ;
const permissions = await database . query < {
ownerId : string ;
permissionId : string ;
position : number ;
} > ( "SELECT * FROM access_need_permissions ORDER BY ownerId,position" , {
transaction ,
type : QueryTypes . SELECT ,
} ) ;
return rows . map ( ( row ) = >
itemSchema . parse ( { . . . row , values : decoded ( row , permissionIds ( permissions , row . id ) ) } )
) ;
}
export async function readAccessRevisions ( database : Sequelize , transaction : Transaction ) {
const rows = await database . query < Record < string , unknown > > (
"SELECT r.id,r.itemId,r.sequence,r.actorId,r.recordedAt,v.* FROM access_revisions r JOIN access_items i ON i.id=r.itemId LEFT JOIN access_historical_values v ON v.ownerId=r.id AND v.kind=i.kind" ,
{ transaction , type : QueryTypes . SELECT }
) ;
const permissions = await database . query < {
ownerId : string ;
permissionId : string ;
position : number ;
} > ( "SELECT * FROM access_need_revision_permissions ORDER BY ownerId,position" , {
transaction ,
type : QueryTypes . SELECT ,
} ) ;
return rows . map ( ( row ) = >
revisionSchema . parse ( { . . . row , values : decoded ( row , permissionIds ( permissions , row . id ) ) } )
) ;
}
function permissionIds (
rows : readonly { ownerId : string ; permissionId : string ; position : number } [ ] ,
id : unknown
) {
const selected = rows . filter ( ( row ) = > row . ownerId === id ) ;
if ( selected . some ( ( row , position ) = > row . position !== position ) ) {
throw new Error ( "Orden de permisos de necesidad inconsistente." ) ;
}
return selected . map ( ( row ) = > row . permissionId ) ;
}
export async function writeAccessRow (
database : Sequelize ,
row : AccessItemRow | AccessRevisionRow ,
transaction : Transaction ,
operation : "insert" | "update"
) {
const { values , . . . metadata } = row ;
const input = AccessDefinitionSchema . parse ( values ) ;
const history = "itemId" in row ;
await writeSupportScalars (
database ,
history ? "access_revisions" : "access_items" ,
metadata ,
transaction ,
operation
) ;
await writeAccessValues ( database , row . id , input , history , transaction ) ;
}
async function writeAccessValues (
database : Sequelize ,
id : string ,
input : z.infer < typeof AccessDefinitionSchema > ,
history : boolean ,
transaction : Transaction
) {
const { kind , . . . values } = input ;
const { permissionIds : _permissions , . . . columns } = z
. record ( z . string ( ) , z . unknown ( ) )
. parse ( values ) ;
const table = accessValueTable ( kind , history ) ;
await database . query ( ` DELETE FROM ${ supportIdentifier ( table ) } WHERE ownerId=:id ` , {
replacements : { id } ,
transaction ,
} ) ;
await writeSupportScalars ( database , table , { ownerId : id , . . . columns } , transaction , "insert" ) ;
if ( kind === "need" ) {
const links = history ? "access_need_revision_permissions" : "access_need_permissions" ;
await database . query ( ` DELETE FROM ${ supportIdentifier ( links ) } WHERE ownerId=:id ` , {
replacements : { id } ,
transaction ,
} ) ;
for ( const [ position , permissionId ] of input . permissionIds . entries ( ) ) {
await writeSupportScalars (
database ,
links ,
{ ownerId : id , permissionId , position } ,
transaction ,
"insert"
) ;
}
}
}
async function prepareAccessValues ( database : Sequelize ) {
const query = database . getQueryInterface ( ) ;
const existing = await database . query < { name : string } > (
"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE()" ,
{ type : QueryTypes . SELECT }
) ;
for ( const kind of AccessKindSchema . options ) {
for ( const history of [ false , true ] ) {
const table = accessValueTable ( kind , history ) ;
if ( ! existing . some ( ( row ) = > row . name === table ) ) {
await query . createTable ( table , {
ownerId : {
. . . ref ( history ? "access_revisions" : "access_items" ) ,
primaryKey : true ,
onDelete : "CASCADE" ,
} ,
. . . accessColumns [ kind ] ,
} ) ;
}
}
}
await prepareNeedPermissions ( database , existing ) ;
await createAccessViews ( database ) ;
}
async function prepareNeedPermissions ( database : Sequelize , existing : readonly { name : string } [ ] ) {
const query = database . getQueryInterface ( ) ;
for ( const history of [ false , true ] ) {
const table = history ? "access_need_revision_permissions" : "access_need_permissions" ;
if ( ! existing . some ( ( row ) = > row . name === table ) ) {
await query . createTable ( table , {
ownerId : {
. . . ref ( accessValueTable ( "need" , history ) ) ,
references : { model : accessValueTable ( "need" , history ) , key : "ownerId" } ,
primaryKey : true ,
onDelete : "CASCADE" ,
} ,
position : { type : DataTypes . INTEGER , allowNull : false , primaryKey : true } ,
permissionId : ref ( ) ,
} ) ;
}
const indexes = await database . query < { name : string } > (
"SELECT DISTINCT INDEX_NAME AS name FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:table" ,
{ replacements : { table } , type : QueryTypes . SELECT }
) ;
if ( ! indexes . some ( ( index ) = > index . name === ` ${ table } _unique ` ) ) {
await query . addIndex ( table , [ "ownerId" , "permissionId" ] , {
unique : true ,
name : ` ${ table } _unique ` ,
} ) ;
}
}
}
async function createAccessViews ( database : Sequelize ) {
// Vistas de lectura: columnas SQL, sin reconstruir documentos JSON en la base.
const allColumns = [ . . . new Set ( Object . values ( accessColumns ) . flatMap ( Object . keys ) ) ] ;
for ( const history of [ false , true ] ) {
const selections = AccessKindSchema . options . map (
( kind ) = >
` SELECT ownerId,' ${ kind } ' AS kind, ${ allColumns . map ( ( name ) = > ( name in accessColumns [ kind ] ? supportIdentifier ( name ) : ` NULL AS ${ supportIdentifier ( name ) } ` ) ) . join ( "," ) } FROM ${ supportIdentifier ( accessValueTable ( kind , history ) ) } `
) ;
await database . query (
` CREATE OR REPLACE VIEW ${ history ? "access_historical_values" : "access_current_values" } AS ${ selections . join ( " UNION ALL " ) } `
) ;
}
}
2026-10-10 15:57:05 +00:00
export async function initializeAccessValues ( database : Sequelize ) {
2026-10-10 14:40:51 +00:00
await prepareAccessValues ( database ) ;
}
export function readCompanyAccessItems (
database : Sequelize ,
companyId : string ,
transaction : Transaction
) {
return readAccessItems ( database , transaction , companyId ) ;
}