FactuGES_ERP/apps/server/src/lib/modules/model-loader.ts
2026-02-13 10:09:19 +01:00

165 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ModuleParams } from "@erp/core/api";
import { logger } from "../logger";
/**
Tipos mínimos para evitar acoplarse a una versión concreta de Sequelize
*/
type SequelizeLike = {
sync: (options?: any) => Promise<void>;
models: Record<string, unknown>;
};
type ModelStatic = {
name: string;
// Algunas implementaciones añaden associate(sequelize) para definir relaciones
associate?: (sequelize: SequelizeLike) => void;
};
export type ModelInitializer = (sequelize: SequelizeLike) => ModelStatic;
/**
Estructuras internas (no exportadas) con trazabilidad de módulo.
*/
type InitializerEntry = { init: ModelInitializer; moduleName: string };
const allModelInitializers: InitializerEntry[] = [];
const registeredModels: Map<string, { model: ModelStatic; moduleName: string }> = new Map();
let modelsInitialized = false;
/**
🔹 Registra inicializadores de modelos de un módulo
models: lista de funciones que definen modelos (sequelize) => ModelStatic
ctx.moduleName: para trazabilidad y detección de colisiones
*/
export const registerModels = (
models: ModelInitializer[],
_params?: ModuleParams,
ctx?: { moduleName: string }
) => {
const moduleName = ctx?.moduleName ?? "unknown";
if (!Array.isArray(models) || models.length === 0) {
logger.warn(`No models provided by module "${moduleName}"`, { label: "registerModels" });
return;
}
for (const init of models) {
// Guardamos con el módulo que los aporta (para logs/errores)
allModelInitializers.push({ init, moduleName });
}
logger.info(`📦 ${models.length} model initializer(s) enqueued from "${moduleName}"`, {
label: "registerModels",
});
};
/**
🔹 Inicializa todos los modelos registrados y configura asociaciones.
Detecta colisiones por nombre de modelo entre módulos.
Controla el modo de sincronización por ENV.
*/
export const initModels = async (params: ModuleParams) => {
if (modelsInitialized) {
logger.warn("Models already initialized. Skipping initModels()", { label: "initModels" });
return;
}
logger.info("Init models...", { label: "initModels" });
const { database } = params as { database: SequelizeLike };
if (!database) {
const error = new Error("❌ Database not found.");
logger.error(error.message, { label: "initModels" });
throw error;
}
// 1) Definir modelos (y detectar colisiones)
for (const { init, moduleName } of allModelInitializers) {
try {
const model = init(database);
if (!model || typeof model.name !== "string" || !model.name) {
throw new Error(`Invalid model initializer: missing or empty "name"`);
}
if (registeredModels.has(model.name)) {
const existing = registeredModels.get(model.name)!;
throw new Error(
`Model name collision: "${model.name}" from module "${moduleName}" conflicts with existing model from "${existing.moduleName}"`
);
}
registeredModels.set(model.name, { model, moduleName });
logger.info(`🔸 Model "${model.name}" registered (sequelize)`, {
label: "registerModel",
module: moduleName,
});
} catch (err: any) {
// Agregamos contexto del módulo que aportó el initializer
logger.error(`⛔️ Failed to define model (module="${moduleName}") : ${err?.message ?? err}`, {
label: "initModels",
stack: err?.stack,
});
throw err;
}
}
// 2) Configurar asociaciones
for (const { model, moduleName } of registeredModels.values()) {
if (typeof model.associate === "function") {
try {
model.associate(database);
} catch (err: any) {
logger.error(
`⛔️ Failed to associate model "${model.name}" (module="${moduleName}") : ${err?.message ?? err}`,
{ label: "initModels", stack: err?.stack }
);
throw err;
}
}
}
// 3) Sincronizar base de datos (según modo)
const syncModeEnv = params.config.database.syncMode; // "none" | "alter" | "force"
logger.info(`ℹ️ Database sync mode ${syncModeEnv}`, {
label: "initModels",
});
try {
if (syncModeEnv === "none") {
logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" });
} else if (syncModeEnv === "alter") {
await database.sync({ force: false, alter: true });
logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" });
} else if (syncModeEnv === "force") {
await database.sync({ force: true, alter: false });
logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" });
} else {
logger.warn(`⚠️ Unknown DB sync mode "${String(syncModeEnv)}" → skipping`, {
label: "initModels",
});
}
} catch (err) {
const error = err as Error;
logger.error("❌ Error synchronizing database:", { error, label: "initModels" });
throw error;
} finally {
modelsInitialized = true;
}
};
/**
🔹 Utilidades opcionales (DX/tests)
*/
export const getRegisteredModels = () =>
Array.from(registeredModels.entries()).map(([name, { moduleName }]) => ({
name,
moduleName,
}));
export const resetModelsRegistry = () => {
allModelInitializers.length = 0;
registeredModels.clear();
modelsInitialized = false;
logger.info("Model registry has been reset.", { label: "initModels" });
};