FactuGES_ERP/apps/server/src/lib/modules/model-loader.ts

165 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-01-13 18:41:14 +00:00
import type { ModuleParams } from "@erp/core/api";
2025-05-09 10:45:32 +00:00
import { logger } from "../logger";
2025-05-04 20:06:57 +00:00
/**
Tipos mínimos para evitar acoplarse a una versión concreta de Sequelize
*/
type SequelizeLike = {
sync: (options?: any) => Promise<void>;
2025-09-12 16:23:36 +00:00
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;
2025-05-04 20:06:57 +00:00
/**
🔹 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",
});
2025-05-04 20:06:57 +00:00
};
/**
🔹 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.
*/
2025-05-04 20:06:57 +00:00
export const initModels = async (params: ModuleParams) => {
if (modelsInitialized) {
logger.warn("Models already initialized. Skipping initModels()", { label: "initModels" });
return;
}
2025-05-09 10:45:32 +00:00
logger.info("Init models...", { label: "initModels" });
2025-05-04 20:06:57 +00:00
const { database } = params as { database: SequelizeLike };
2025-05-04 20:06:57 +00:00
if (!database) {
const error = new Error("❌ Database not found.");
logger.error(error.message, { label: "initModels" });
2025-05-04 20:06:57 +00:00
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;
}
2025-05-04 20:06:57 +00:00
}
// 2) Configurar asociaciones
for (const { model, moduleName } of registeredModels.values()) {
2025-05-04 20:06:57 +00:00
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;
}
2025-05-04 20:06:57 +00:00
}
}
// 3) Sincronizar base de datos (según modo)
2026-02-13 09:09:19 +00:00
const syncModeEnv = params.config.database.syncMode; // "none" | "alter" | "force"
2026-02-12 14:46:47 +00:00
2026-02-13 09:09:19 +00:00
logger.info(`ℹ️ Database sync mode ${syncModeEnv}`, {
label: "initModels",
});
2025-05-04 20:06:57 +00:00
try {
2026-02-12 14:46:47 +00:00
if (syncModeEnv === "none") {
logger.info("✔️ Database sync skipped (mode=none)", { label: "initModels" });
2026-02-12 14:46:47 +00:00
} else if (syncModeEnv === "alter") {
2025-05-04 20:06:57 +00:00
await database.sync({ force: false, alter: true });
logger.info("✔️ Database synchronized successfully (mode=alter).", { label: "initModels" });
2026-02-12 14:46:47 +00:00
} else if (syncModeEnv === "force") {
await database.sync({ force: true, alter: false });
logger.warn("⚠️ Database synchronized with FORCE (mode=force).", { label: "initModels" });
2025-05-04 20:06:57 +00:00
} else {
2026-02-12 14:46:47 +00:00
logger.warn(`⚠️ Unknown DB sync mode "${String(syncModeEnv)}" → skipping`, {
label: "initModels",
});
2025-05-04 20:06:57 +00:00
}
} catch (err) {
const error = err as Error;
2025-05-09 10:45:32 +00:00
logger.error("❌ Error synchronizing database:", { error, label: "initModels" });
2025-05-04 20:06:57 +00:00
throw error;
} finally {
modelsInitialized = true;
2025-05-04 20:06:57 +00:00
}
};
/**
🔹 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" });
};