165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
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" });
|
||
};
|