FactuGES_ERP/apps/server/src/lib/modules/model-loader.ts
2026-05-05 13:34:36 +02:00

216 lines
5.5 KiB
TypeScript

// apps/server/src/lib/modules/model-loader.ts
import type { ModuleParams } from "@erp/core/api";
import { logger } from "../logger";
type SequelizeLike = {
sync: (options?: any) => Promise<void>;
models: Record<string, unknown>;
};
type ModelStatic = {
name: string;
associate?: (sequelize: SequelizeLike) => void;
};
export type ModelInitializer = (sequelize: SequelizeLike) => ModelStatic;
type InitializerEntry = { init: ModelInitializer; moduleName: string };
type RegisteredModelEntry = {
model: ModelStatic;
moduleName: string;
};
const allModelInitializers: InitializerEntry[] = [];
const registeredModels: Map<string, RegisteredModelEntry> = 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);
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);
} 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<void> => {
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" });
};