123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- import config from "config";
- import { readdir } from "fs/promises";
- import path from "path";
- import { forEachIn } from "@common/utils/forEachIn";
- import {
- Sequelize,
- Model as SequelizeModel,
- ModelStatic,
- DataTypes,
- Utils,
- ModelOptions,
- Options
- } from "sequelize";
- import { Dirent } from "fs";
- import * as inflection from "inflection";
- import { SequelizeStorage, Umzug } from "umzug";
- import BaseModule, { ModuleStatus } from "@/BaseModule";
- import DataModuleJob from "./DataModule/DataModuleJob";
- import Job from "@/Job";
- import EventsModule from "./EventsModule";
- export type ObjectIdType = string;
- // TODO fix TS
- // TODO implement actual checking of ObjectId's
- // TODO move to a better spot
- // Strange behavior would result if we extended DataTypes.ABSTRACT because
- // it's a class wrapped in a Proxy by Utils.classToInvokable.
- export class OBJECTID extends DataTypes.ABSTRACT.prototype.constructor {
- // Mandatory: set the type key
- static key = "OBJECTID";
- key = OBJECTID.key;
- // Mandatory: complete definition of the new type in the database
- toSql() {
- return "VARCHAR(24)";
- }
- // Optional: validator function
- // @ts-ignore
- validate(value, options) {
- return true;
- // return (typeof value === 'number') && (!Number.isNaN(value));
- }
- // Optional: sanitizer
- // @ts-ignore
- _sanitize(value) {
- return value;
- // Force all numbers to be positive
- // return value < 0 ? 0 : Math.round(value);
- }
- // Optional: value stringifier before sending to database
- // @ts-ignore
- _stringify(value) {
- return value;
- // return value.toString();
- }
- // Optional: parser for values received from the database
- // @ts-ignore
- static parse(value) {
- return value;
- // return Number.parseInt(value);
- }
- }
- // Optional: add the new type to DataTypes. Optionally wrap it on `Utils.classToInvokable` to
- // be able to use this datatype directly without having to call `new` on it.
- DataTypes.OBJECTID = Utils.classToInvokable(OBJECTID);
- export class DataModule extends BaseModule {
- private _sequelize?: Sequelize;
- declare _jobs: Record<string, typeof Job | typeof DataModuleJob>;
- /**
- * Data Module
- */
- public constructor() {
- super("data");
- this._dependentModules = ["events"];
- }
- /**
- * startup - Startup data module
- */
- public override async startup() {
- await super.startup();
- await this._setupSequelize();
- // await this._runMigrations();
- await super._started();
- }
- /**
- * shutdown - Shutdown data module
- */
- public override async shutdown() {
- await super.shutdown();
- await this._sequelize?.close();
- await this._stopped();
- }
- private async _createSequelizeInstance(options: Options = {}) {
- const { username, password, host, port, database } =
- config.get<any>("postgres");
- const sequelize = new Sequelize(database, username, password, {
- host,
- port,
- dialect: "postgres",
- logging: message =>
- this.log({
- type: "debug",
- category: "sql",
- message
- }),
- ...options
- });
- await sequelize.authenticate();
- return sequelize;
- }
- /**
- * setupSequelize - Setup sequelize instance
- */
- private async _setupSequelize() {
- this._sequelize = await this._createSequelizeInstance({
- define: {
- hooks: this._getSequelizeHooks()
- }
- });
- await this._sequelize.authenticate();
- const setupFunctions: Function[] = [];
- await forEachIn(
- await readdir(
- path.resolve(__dirname, `./${this.constructor.name}/models`),
- {
- withFileTypes: true
- }
- ),
- async modelFile => {
- if (!modelFile.isFile() || modelFile.name.includes(".spec."))
- return;
- const {
- default: ModelClass,
- schema,
- options = {},
- setup
- } = await import(`${modelFile.path}/${modelFile.name}`);
- const tableName = inflection.camelize(
- inflection.pluralize(ModelClass.name),
- true
- );
- ModelClass.init(schema, {
- tableName,
- ...options,
- sequelize: this._sequelize
- });
- if (typeof setup === "function") setupFunctions.push(setup);
- await this._loadModelEvents(ModelClass.name);
- await this._loadModelJobs(ModelClass.name);
- }
- );
- await forEachIn(setupFunctions, setup => setup());
- await this._sequelize.sync();
- await this._runMigrations();
- }
- /**
- * getModel - Get model
- *
- * @returns Model
- */
- public async getModel<ModelType extends SequelizeModel<any>>(
- name: string
- ): Promise<ModelStatic<ModelType>> {
- if (!this._sequelize?.models) throw new Error("Models not loaded");
- if (this.getStatus() !== ModuleStatus.STARTED)
- throw new Error("Module not started");
- // TODO check if we want to do it via singularize&camelize, or another way
- const camelizedName = inflection.singularize(inflection.camelize(name));
- return this._sequelize.model(camelizedName) as ModelStatic<ModelType>; // This fails - news has not been defined
- }
- private _getSequelizeHooks(): ModelOptions<SequelizeModel>["hooks"] {
- return {
- afterSave: console.log,
- afterCreate: async model => {
- const modelName = (
- model.constructor as ModelStatic<any>
- ).getTableName();
- let EventClass;
- try {
- EventClass = this.getEvent(`${modelName}.created`);
- } catch (error) {
- // TODO: Catch and ignore only event not found
- return;
- }
- EventsModule.publish(
- new EventClass({
- doc: model.get()
- })
- );
- },
- afterUpdate: async model => {
- const modelName = (
- model.constructor as ModelStatic<any>
- ).getTableName();
- let EventClass;
- try {
- EventClass = this.getEvent(`${modelName}.updated`);
- } catch (error) {
- // TODO: Catch and ignore only event not found
- return;
- }
- EventsModule.publish(
- new EventClass(
- {
- doc: model.get(),
- oldDoc: model.previous()
- },
- model.get("_id") ?? model.previous("_id")
- )
- );
- },
- afterDestroy: async model => {
- const modelName = (
- model.constructor as ModelStatic<any>
- ).getTableName();
- let EventClass;
- try {
- EventClass = this.getEvent(`${modelName}.deleted`);
- } catch (error) {
- // TODO: Catch and ignore only event not found
- return;
- }
- EventsModule.publish(
- new EventClass(
- {
- oldDoc: model.previous()
- },
- model.previous("_id")
- )
- );
- }
- };
- }
- private async _loadModelJobs(modelClassName: string) {
- let jobs: Dirent[];
- try {
- jobs = await readdir(
- path.resolve(
- __dirname,
- `./${this.constructor.name}/models/${modelClassName}/jobs/`
- ),
- {
- withFileTypes: true
- }
- );
- } catch (error) {
- if (
- error instanceof Error &&
- "code" in error &&
- error.code === "ENOENT"
- ) {
- this.log(
- `Loading ${modelClassName} jobs failed - folder doesn't exist`
- );
- return;
- }
- throw error;
- }
- await forEachIn(jobs, async jobFile => {
- if (!jobFile.isFile() || jobFile.name.includes(".spec.")) return;
- const { default: JobClass } = await import(
- `${jobFile.path}/${jobFile.name}`
- );
- this._jobs[JobClass.getName()] = JobClass;
- });
- }
- private async _loadModelEvents(modelClassName: string) {
- let events: Dirent[];
- try {
- events = await readdir(
- path.resolve(
- __dirname,
- `./${this.constructor.name}/models/${modelClassName}/events/`
- ),
- {
- withFileTypes: true
- }
- );
- } catch (error) {
- if (
- error instanceof Error &&
- "code" in error &&
- error.code === "ENOENT"
- )
- return;
- throw error;
- }
- await forEachIn(events, async eventFile => {
- if (!eventFile.isFile() || eventFile.name.includes(".spec."))
- return;
- const { default: EventClass } = await import(
- `${eventFile.path}/${eventFile.name}`
- );
- this._events[EventClass.getName()] = EventClass;
- });
- }
- private async _runMigrations() {
- const sequelize = await this._createSequelizeInstance({
- logging: message =>
- this.log({
- type: "debug",
- category: "migrations.sql",
- message
- })
- });
- const migrator = new Umzug({
- migrations: {
- glob: [
- `${this.constructor.name}/migrations/*.ts`,
- { cwd: __dirname }
- ]
- },
- context: sequelize,
- storage: new SequelizeStorage({
- sequelize: sequelize!
- }),
- logger: console
- });
- await migrator.up();
- await sequelize.close();
- }
- }
- export default new DataModule();
|