217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
// 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<void>;
|
|
models: Record<string, unknown>;
|
|
};
|
|
|
|
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<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 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<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" });
|
|
};
|