2026-10-19 09:45:09 +00:00
import type { Sequelize , Transaction } from "sequelize" ;
import { DataTypes , QueryTypes } from "sequelize" ;
import { z } from "zod/v4" ;
import type { ResourceSpecificationData } from "../../domain/resource-values" ;
import { ResourceSpecificationSchema } from "../../domain/resource-values" ;
import { readResourceRows , writeResourceRow } from "./resource-relational-sql" ;
const bool = z . union ( [ z . boolean ( ) , z . literal ( 0 ) , z . literal ( 1 ) ] ) . transform ( Boolean ) ;
const identity = z . object ( { revisionId : z.uuid ( ) , resourceId : z.uuid ( ) } ) ;
const headerSchema = identity . extend ( {
templateId : z.uuid ( ) ,
templateName : z.string ( ) ,
sectionsDefined : bool ,
} ) ;
const fieldSchema = identity . extend ( {
fieldId : z.uuid ( ) ,
name : z.string ( ) ,
fieldType : z.enum ( [ "text" , "multiline" , "number" , "date" , "boolean" , "choice" ] ) ,
unit : z.string ( ) ,
required : bool ,
sectionId : z.uuid ( ) . nullable ( ) ,
position : z.number ( ) . int ( ) ,
} ) ;
const sectionSchema = identity . extend ( {
id : z.uuid ( ) ,
name : z.string ( ) ,
description : z.string ( ) ,
position : z.number ( ) . int ( ) ,
} ) ;
const choiceSchema = identity . extend ( {
fieldId : z.uuid ( ) ,
position : z.number ( ) . int ( ) ,
value : z.string ( ) ,
} ) ;
/** Se ejecuta antes de que una estructura viva pueda cambiar; las revisiones anteriores reciben su instantánea original. */
export async function migrateResourceRevisionSpecifications ( database : Sequelize ) {
const query = database . getQueryInterface ( ) ;
const tables = await database . query < { name : string } > (
"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE()" ,
{ type : QueryTypes . SELECT }
) ;
const revision = {
type : DataTypes . UUID ,
primaryKey : true ,
references : { model : "resource_revisions" , key : "id" } ,
onDelete : "CASCADE" ,
} ;
const parent = {
type : DataTypes . UUID ,
allowNull : false ,
references : { model : "resources" , key : "id" } ,
onDelete : "RESTRICT" ,
} ;
const definitions = [
[
"resource_revision_specifications" ,
{
revisionId : revision ,
resourceId : parent ,
templateId : { type : DataTypes . UUID , allowNull : false } ,
templateName : { type : DataTypes . STRING ( 100 ) , allowNull : false } ,
sectionsDefined : { type : DataTypes . BOOLEAN , allowNull : false } ,
} ,
] ,
[
"resource_revision_specification_sections" ,
{
revisionId : revision ,
resourceId : parent ,
id : { type : DataTypes . UUID , primaryKey : true } ,
name : { type : DataTypes . STRING ( 100 ) , allowNull : false } ,
description : { type : DataTypes . TEXT , allowNull : false } ,
position : { type : DataTypes . INTEGER , allowNull : false } ,
} ,
] ,
[
"resource_revision_specification_fields" ,
{
revisionId : revision ,
resourceId : parent ,
fieldId : {
type : DataTypes . UUID ,
primaryKey : true ,
references : { model : "resource_definitions" , key : "id" } ,
onDelete : "RESTRICT" ,
} ,
name : { type : DataTypes . STRING ( 100 ) , allowNull : false } ,
fieldType : { type : DataTypes . STRING ( 12 ) , allowNull : false } ,
unit : { type : DataTypes . STRING ( 30 ) , allowNull : false } ,
required : { type : DataTypes . BOOLEAN , allowNull : false } ,
sectionId : { type : DataTypes . UUID , allowNull : true } ,
position : { type : DataTypes . INTEGER , allowNull : false } ,
} ,
] ,
[
"resource_revision_specification_choices" ,
{
revisionId : revision ,
resourceId : parent ,
fieldId : { type : DataTypes . UUID , primaryKey : true } ,
position : { type : DataTypes . INTEGER , primaryKey : true } ,
value : { type : DataTypes . STRING ( 100 ) , allowNull : false } ,
} ,
] ,
] as const ;
for ( const [ table , columns ] of definitions ) {
if ( ! tables . some ( ( item ) = > item . name === table ) ) {
await query . createTable ( table , columns ) ;
}
}
await database . transaction ( async ( transaction ) = > {
const missing = await database . query < { id : string } > (
"SELECT r.id FROM resource_revisions r LEFT JOIN resource_revision_specifications s ON s.revisionId=r.id WHERE s.revisionId IS NULL" ,
{ type : QueryTypes . SELECT , transaction }
) ;
for ( let offset = 0 ; offset < missing . length ; offset += 500 ) {
const ids = missing . slice ( offset , offset + 500 ) . map ( ( item ) = > item . id ) ;
const options = { replacements : { ids } , transaction } ;
await database . query (
"INSERT INTO resource_revision_specifications (revisionId,resourceId,templateId,templateName,sectionsDefined) SELECT r.id,a.id,a.templateId,a.templateName,a.sectionsDefined FROM resource_revisions r JOIN resources a ON a.id=r.resourceId WHERE r.id IN (:ids)" ,
options
) ;
for ( const [ suffix , columns ] of [
[ "sections" , "id,name,description,position" ] ,
[ "fields" , "fieldId,name,fieldType,unit,required,sectionId,position" ] ,
[ "choices" , "fieldId,position,value" ] ,
] as const ) {
const source =
suffix === "choices"
? "resource_specification_field_choices"
: ` resource_specification_ ${ suffix } ` ;
const selected = columns
. split ( "," )
. map ( ( column ) = > ` s. \` ${ column } \` ` )
. join ( "," ) ;
await database . query (
` INSERT INTO resource_revision_specification_ ${ suffix } (revisionId,resourceId, ${ columns
. split ( "," )
. map ( ( column ) = > ` \` ${ column } \` ` )
. join (
","
) } ) SELECT r . id , r . resourceId , $ { selected } FROM resource_revisions r JOIN $ { source } s ON s . resourceId = r . resourceId WHERE r . id IN ( : ids ) ` ,
options
) ;
}
}
} ) ;
}
export async function readResourceRevisionSpecifications (
database : Sequelize ,
ids : readonly string [ ] ,
2026-10-24 23:19:17 +00:00
transaction : Transaction ,
revisionIds? : readonly string [ ]
2026-10-19 09:45:09 +00:00
) {
const headers = z
. array ( headerSchema )
. parse (
await readResourceRows (
database ,
"resource_revision_specifications" ,
2026-10-24 23:19:17 +00:00
revisionIds ? "revisionId" : "resourceId" ,
revisionIds ? ? ids ,
2026-10-19 09:45:09 +00:00
transaction
)
) ;
const fields = z
. array ( fieldSchema )
. parse (
await readResourceRows (
database ,
"resource_revision_specification_fields" ,
2026-10-24 23:19:17 +00:00
revisionIds ? "revisionId" : "resourceId" ,
revisionIds ? ? ids ,
2026-10-19 09:45:09 +00:00
transaction
)
) ;
const sections = z
. array ( sectionSchema )
. parse (
await readResourceRows (
database ,
"resource_revision_specification_sections" ,
2026-10-24 23:19:17 +00:00
revisionIds ? "revisionId" : "resourceId" ,
revisionIds ? ? ids ,
2026-10-19 09:45:09 +00:00
transaction
)
) ;
const choices = z
. array ( choiceSchema )
. parse (
await readResourceRows (
database ,
"resource_revision_specification_choices" ,
2026-10-24 23:19:17 +00:00
revisionIds ? "revisionId" : "resourceId" ,
revisionIds ? ? ids ,
2026-10-19 09:45:09 +00:00
transaction
)
) ;
return new Map (
headers . map ( ( header ) = > [
header . revisionId ,
ResourceSpecificationSchema . parse ( {
templateId : header.templateId ,
templateName : header.templateName ,
. . . ( header . sectionsDefined
? {
sections : sections
. filter ( ( item ) = > item . revisionId === header . revisionId )
. sort ( ( a , b ) = > a . position - b . position )
. map ( ( { id , name , description } ) = > ( { id , name , description } ) ) ,
}
: { } ) ,
fields : fields
. filter ( ( item ) = > item . revisionId === header . revisionId )
. sort ( ( a , b ) = > a . position - b . position )
. map ( ( field ) = > ( {
fieldId : field.fieldId ,
name : field.name ,
type : field . fieldType ,
unit : field.unit ,
required : field.required ,
. . . ( field . sectionId ? { sectionId : field.sectionId } : { } ) ,
choices : choices
. filter (
( item ) = > item . revisionId === header . revisionId && item . fieldId === field . fieldId
)
. sort ( ( a , b ) = > a . position - b . position )
. map ( ( item ) = > item . value ) ,
} ) ) ,
} ) ,
] )
) ;
}
export async function writeResourceRevisionSpecification (
database : Sequelize ,
resourceId : string ,
revisionId : string ,
specification : ResourceSpecificationData ,
transaction : Transaction
) {
const identity = { resourceId , revisionId } ;
await writeResourceRow (
database ,
"resource_revision_specifications" ,
{
. . . identity ,
templateId : specification.templateId ,
templateName : specification.templateName ,
sectionsDefined : specification.sections !== undefined ,
} ,
transaction
) ;
for ( const [ position , section ] of ( specification . sections ? ? [ ] ) . entries ( ) ) {
await writeResourceRow (
database ,
"resource_revision_specification_sections" ,
{ . . . identity , . . . section , position } ,
transaction
) ;
}
for ( const [ position , field ] of specification . fields . entries ( ) ) {
await writeResourceRow (
database ,
"resource_revision_specification_fields" ,
{
. . . identity ,
fieldId : field.fieldId ,
name : field.name ,
fieldType : field.type ,
unit : field.unit ,
required : field.required ,
sectionId : field.sectionId ? ? null ,
position ,
} ,
transaction
) ;
for ( const [ choicePosition , value ] of field . choices . entries ( ) ) {
await writeResourceRow (
database ,
"resource_revision_specification_choices" ,
{ . . . identity , fieldId : field.fieldId , position : choicePosition , value } ,
transaction
) ;
}
}
}