// apps/server/src/lib/modules/model-loader.ts import type { ModuleParams } from "@erp/core/api"; import type { Sequelize } from "sequelize"; import { logger } from "../logger"; type SequelizeLike = { sync: (options?: any) => Promise; models: Record; }; type ModelStatic = { name: string; associate?: (sequelize: Sequelize) => void; }; export type ModelInitializer = (sequelize: Sequelize) => ModelStatic; type InitializerEntry = { init: ModelInitializer; moduleName: string }; type RegisteredModelEntry = { model: ModelStatic; moduleName: string; }; const allModelInitializers: InitializerEntry[] = []; const registeredModels: Map = new Map(); let modelsInitialized = false; 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) { allModelInitializers.push({ init, moduleName }); } logger.info(`${models.length} model initializer(s) enqueued from "${moduleName}"`, { label: "registerModels", }); }; 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 = getDatabaseOrThrow(params); try { defineRegisteredModels(database); associateRegisteredModels(database); await syncDatabase(database, getDatabaseSyncMode(params)); modelsInitialized = true; } catch (error) { registeredModels.clear(); throw error; } }; const getDatabaseOrThrow = (params: ModuleParams): SequelizeLike => { const { database } = params as { database?: SequelizeLike }; if (!database) { const error = new Error("Database not found."); logger.error(error.message, { label: "initModels" }); throw error; } return database; }; const defineRegisteredModels = (database: SequelizeLike): void => { for (const entry of allModelInitializers) { defineModel(database, entry); } }; const defineModel = (database: SequelizeLike, entry: InitializerEntry): void => { const { init, moduleName } = entry; try { const model = init(database as unknown as Sequelize); assertValidModel(model); assertModelNameIsAvailable(model.name, moduleName); registeredModels.set(model.name, { model, moduleName }); logger.info(`Model "${model.name}" registered (sequelize)`, { label: "registerModel", module: moduleName, }); } catch (err) { const error = err as Error; logger.error(`Failed to define model (module="${moduleName}") : ${error.message}`, { label: "initModels", stack: error.stack, }); throw error; } }; const assertValidModel = (model: ModelStatic): void => { if (!model || typeof model.name !== "string" || !model.name) { throw new Error('Invalid model initializer: missing or empty "name"'); } }; const assertModelNameIsAvailable = (modelName: string, moduleName: string): void => { const existing = registeredModels.get(modelName); if (!existing) { return; } throw new Error( `Model name collision: "${modelName}" from module "${moduleName}" conflicts with existing model from "${existing.moduleName}"` ); }; const associateRegisteredModels = (database: SequelizeLike): void => { for (const entry of registeredModels.values()) { associateModel(database, entry); } }; const associateModel = (database: SequelizeLike, entry: RegisteredModelEntry): void => { const { model, moduleName } = entry; if (typeof model.associate !== "function") { return; } try { model.associate(database as unknown as Sequelize); } catch (err) { const error = err as Error; logger.error( `Failed to associate model "${model.name}" (module="${moduleName}") : ${error.message}`, { label: "initModels", stack: error.stack } ); throw error; } }; const getDatabaseSyncMode = (params: ModuleParams): string => { return params.config.database.syncMode; }; const syncDatabase = async (database: SequelizeLike, syncMode: string): Promise => { logger.info(`Database sync mode ${syncMode}`, { label: "initModels" }); try { if (syncMode === "none") { logger.info("Database sync skipped (mode=none)", { label: "initModels" }); return; } if (syncMode === "alter") { await database.sync({ force: false, alter: true }); logger.info("Database synchronized successfully (mode=alter).", { label: "initModels" }); return; } if (syncMode === "force") { await database.sync({ force: true, alter: false }); logger.warn("Database synchronized with FORCE (mode=force).", { label: "initModels" }); return; } logger.warn(`Unknown DB sync mode "${String(syncMode)}" → skipping`, { label: "initModels", }); } catch (err) { const error = err as Error; logger.error("Error synchronizing database:", { error, label: "initModels", }); throw error; } }; 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" }); };