浏览代码

refactor: Start migrating to sequelize

Owen Diffey 9 月之前
父节点
当前提交
ac11d3cb24
共有 48 个文件被更改,包括 1267 次插入883 次删除
  1. 3 0
      .env.example
  2. 4 0
      backend/config/custom-environment-variables.json
  3. 7 0
      backend/config/default.json
  4. 584 33
      backend/package-lock.json
  5. 5 0
      backend/package.json
  6. 0 1
      backend/src/@types/mongoose-update-versioning.d.ts
  7. 0 57
      backend/src/@types/mongoose.d.ts
  8. 3 5
      backend/src/Job.ts
  9. 54 30
      backend/src/main.ts
  10. 146 309
      backend/src/modules/DataModule.ts
  11. 2 3
      backend/src/modules/DataModule/CreateJob.ts
  12. 8 9
      backend/src/modules/DataModule/DataModuleEvent.ts
  13. 11 17
      backend/src/modules/DataModule/DataModuleJob.ts
  14. 3 3
      backend/src/modules/DataModule/DeleteByIdJob.ts
  15. 3 6
      backend/src/modules/DataModule/DeleteManyByIdJob.ts
  16. 4 4
      backend/src/modules/DataModule/FindByIdJob.ts
  17. 3 6
      backend/src/modules/DataModule/FindManyByIdJob.ts
  18. 165 8
      backend/src/modules/DataModule/GetDataJob.ts
  19. 0 17
      backend/src/modules/DataModule/Migration.ts
  20. 3 3
      backend/src/modules/DataModule/UpdateByIdJob.ts
  21. 103 0
      backend/src/modules/DataModule/models/News.ts
  22. 0 0
      backend/src/modules/DataModule/models/News/NewsStatus.ts
  23. 3 2
      backend/src/modules/DataModule/models/News/events/NewsCreatedEvent.ts
  24. 3 2
      backend/src/modules/DataModule/models/News/events/NewsDeletedEvent.ts
  25. 3 2
      backend/src/modules/DataModule/models/News/events/NewsPublishedEvent.ts
  26. 3 2
      backend/src/modules/DataModule/models/News/events/NewsUnpublishedEvent.ts
  27. 3 2
      backend/src/modules/DataModule/models/News/events/NewsUpdatedEvent.ts
  28. 2 1
      backend/src/modules/DataModule/models/News/jobs/Create.ts
  29. 2 1
      backend/src/modules/DataModule/models/News/jobs/DeleteById.ts
  30. 2 1
      backend/src/modules/DataModule/models/News/jobs/DeleteManyById.ts
  31. 2 1
      backend/src/modules/DataModule/models/News/jobs/FindById.ts
  32. 2 1
      backend/src/modules/DataModule/models/News/jobs/FindManyById.ts
  33. 25 0
      backend/src/modules/DataModule/models/News/jobs/GetData.ts
  34. 25 0
      backend/src/modules/DataModule/models/News/jobs/Newest.ts
  35. 17 0
      backend/src/modules/DataModule/models/News/jobs/Published.ts
  36. 4 1
      backend/src/modules/DataModule/models/News/jobs/UpdateById.ts
  37. 0 70
      backend/src/modules/DataModule/models/news/config.ts
  38. 0 53
      backend/src/modules/DataModule/models/news/getData.ts
  39. 0 8
      backend/src/modules/DataModule/models/news/jobs/GetData.ts
  40. 0 25
      backend/src/modules/DataModule/models/news/jobs/Newest.ts
  41. 0 15
      backend/src/modules/DataModule/models/news/jobs/Published.ts
  42. 0 58
      backend/src/modules/DataModule/models/news/migrations/1620330161000-news-markdown.ts
  43. 0 95
      backend/src/modules/DataModule/models/news/schema.ts
  44. 3 4
      backend/src/modules/DataModule/permissions/modelPermissions/isNewsPublished.ts
  45. 0 15
      backend/src/modules/DataModule/plugins/documentVersion.ts
  46. 7 7
      backend/src/modules/EventsModule/jobs/Subscribe.spec.ts
  47. 16 0
      compose.yml
  48. 34 6
      musare.sh

+ 3 - 0
.env.example

@@ -16,6 +16,9 @@ MONGO_USER_USERNAME=musare
 MONGO_USER_PASSWORD=OTHER_PASSWORD_HERE
 MONGO_VERSION=6
 
+POSTGRES_USERNAME=musare
+POSTGRES_PASSWORD=PASSWORD
+
 REDIS_PASSWORD=PASSWORD
 
 BACKUP_LOCATION=

+ 4 - 0
backend/config/custom-environment-variables.json

@@ -8,6 +8,10 @@
 		"user": "MONGO_USER_USERNAME",
 		"password": "MONGO_USER_PASSWORD"
 	},
+	"postgres": {
+		"username": "POSTGRES_USERNAME",
+		"password": "POSTGRES_PASSWORD"
+	},
 	"debug": {
 		"git": {
 			"remote": "MUSARE_DEBUG_GIT_REMOTE",

+ 7 - 0
backend/config/default.json

@@ -91,6 +91,13 @@
 		"port": 27017,
 		"database": "musare"
 	},
+	"postgres": {
+		"username": "musare",
+		"password": "PASSWORD",
+		"host": "postgres",
+		"port": 5432,
+		"database": "musare"
+	},
 	"blacklistedCommunityStationNames": [
 		"musare"
 	],

文件差异内容过多而无法显示
+ 584 - 33
backend/package-lock.json


+ 5 - 0
backend/package.json

@@ -24,16 +24,21 @@
 		"cookie-parser": "^1.4.6",
 		"cors": "^2.8.5",
 		"express": "^4.18.2",
+		"inflection": "^3.0.0",
 		"joi": "^17.13.3",
 		"moment": "^2.29.4",
 		"mongoose": "^7.2.0",
 		"mongoose-update-versioning": "^0.3.0",
 		"nodemailer": "^6.9.2",
 		"oauth": "^0.10.0",
+		"pg": "^8.12.0",
+		"pg-hstore": "^2.3.4",
 		"redis": "^4.6.6",
 		"retry-axios": "^3.0.0",
+		"sequelize": "^6.37.3",
 		"sha256": "^0.2.0",
 		"ts-patch-mongoose": "^2.0.5",
+		"umzug": "^3.8.1",
 		"ws": "^8.13.0"
 	},
 	"devDependencies": {

+ 0 - 1
backend/src/@types/mongoose-update-versioning.d.ts

@@ -1 +0,0 @@
-declare module "mongoose-update-versioning";

+ 0 - 57
backend/src/@types/mongoose.d.ts

@@ -1,57 +0,0 @@
-import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
-import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
-import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
-
-declare module "mongoose" {
-	// Add some additional possible config options to Mongoose's schema options
-	interface SchemaOptions<
-		DocType = unknown,
-		/* eslint-disable */
-		TInstanceMethods = {},
-		QueryHelpers = {},
-		TStaticMethods = {},
-		TVirtuals = {},
-		THydratedDocumentType = HydratedDocument<
-			DocType,
-			TInstanceMethods,
-			QueryHelpers
-		>
-		/* eslint-enable */
-	> {
-		patchHistory?: {
-			enabled: boolean;
-			patchHistoryDisabled?: boolean;
-			eventCreated?: string;
-			eventUpdated?: string;
-			eventDeleted?: string;
-		};
-
-		getData?: {
-			enabled: boolean;
-			blacklistedProperties?: string[];
-			specialProperties?: Record<string, PipelineStage[]>;
-			specialQueries?: Record<
-				string,
-				(query: Record<string, any>) => Record<string, any>
-			>;
-			specialFilters?: Record<
-				string,
-				(...args: any[]) => PipelineStage[]
-			>;
-		};
-
-		documentVersion?: number;
-
-		eventListeners?: {
-			[key: `${string}.created`]: (
-				event: ModelCreatedEvent
-			) => Promise<void>;
-			[key: `${string}.updated`]: (
-				event: ModelUpdatedEvent
-			) => Promise<void>;
-			[key: `${string}.deleted`]: (
-				event: ModelDeletedEvent
-			) => Promise<void>;
-		};
-	}
-}

+ 3 - 5
backend/src/Job.ts

@@ -3,6 +3,7 @@ import { getErrorMessage } from "@common/utils/getErrorMessage";
 import { generateUuid } from "@common/utils/generateUuid";
 import { HydratedDocument } from "mongoose";
 import Joi from "joi";
+import * as inflection from "inflection";
 import JobContext from "@/JobContext";
 import JobStatistics, { JobStatisticsType } from "@/JobStatistics";
 import LogBook, { Log } from "@/LogBook";
@@ -106,17 +107,14 @@ export default abstract class Job {
 	 * getName - Get job name
 	 */
 	public static getName() {
-		return this.name.substring(0, 1).toLowerCase() + this.name.substring(1);
+		return inflection.camelize(this.name, true);
 	}
 
 	/**
 	 * getName - Get job name
 	 */
 	public getName() {
-		return (
-			this.constructor.name.substring(0, 1).toLowerCase() +
-			this.constructor.name.substring(1)
-		);
+		return (this.constructor as typeof Job).getName();
 	}
 
 	/**

+ 54 - 30
backend/src/main.ts

@@ -3,10 +3,13 @@ import ModuleManager from "@/ModuleManager";
 import LogBook from "@/LogBook";
 import JobQueue from "@/JobQueue";
 import JobStatistics from "@/JobStatistics";
-import DataModule from "@/modules/DataModule";
+// import DataModule from "@/modules/DataModule";
 import EventsModule from "./modules/EventsModule";
-import { NewsModel } from "./modules/DataModule/models/news/schema";
-import { FilterType } from "./modules/DataModule/plugins/getData";
+// import { NewsModel } from "./modules/DataModule/models/news/schema";
+// import { FilterType } from "./modules/DataModule/plugins/getData";
+import News from "./modules/DataModule/models/News";
+import { NewsStatus } from "@models/News/NewsStatus";
+import GetData from "./modules/DataModule/models/News/jobs/GetData";
 
 process.removeAllListeners("uncaughtException");
 process.on("uncaughtException", err => {
@@ -21,33 +24,33 @@ process.on("uncaughtException", err => {
 });
 
 ModuleManager.startup().then(async () => {
-	const Model = await DataModule.getModel<NewsModel>("news");
-	// console.log("Model", Model);
-	const abcs = await Model.findOne({}).newest();
-	console.log("Abcs", abcs);
-	console.log(
-		"getData",
-		await Model.getData({
-			page: 1,
-			pageSize: 3,
-			properties: [
-				"title",
-				"markdown",
-				"status",
-				"showToNewUsers",
-				"createdBy"
-			],
-			sort: {},
-			queries: [
-				{
-					data: "v7",
-					filter: { property: "title" },
-					filterType: FilterType.CONTAINS
-				}
-			],
-			operator: "and"
-		})
-	);
+	// const Model = await DataModule.getModel<NewsModel>("news");
+	// // console.log("Model", Model);
+	// const abcs = await Model.findOne({}).newest();
+	// console.log("Abcs", abcs);
+	// console.log(
+	// 	"getData",
+	// 	await Model.getData({
+	// 		page: 1,
+	// 		pageSize: 3,
+	// 		properties: [
+	// 			"title",
+	// 			"markdown",
+	// 			"status",
+	// 			"showToNewUsers",
+	// 			"createdBy"
+	// 		],
+	// 		sort: {},
+	// 		queries: [
+	// 			{
+	// 				data: "v7",
+	// 				filter: { property: "title" },
+	// 				filterType: FilterType.CONTAINS
+	// 			}
+	// 		],
+	// 		operator: "and"
+	// 	})
+	// );
 
 	// Model.create({
 	// 	name: "Test name",
@@ -69,6 +72,27 @@ ModuleManager.startup().then(async () => {
 	// 	console.log(`PUBLISHED: ${value}`);
 	// });
 	// await EventsModule.publish("test", "a value!");
+
+	console.log(
+		await News.findAll({
+			where: {
+				status: NewsStatus.PUBLISHED
+			}
+		})
+	);
+
+	console.log(
+		await (new GetData({
+			page: 1,
+			pageSize: 10,
+			properties: ['id'],
+			sort: {
+				id: 'ascending'
+			},
+			queries: [],
+			operator: 'and'
+		}).execute())
+	);
 });
 
 // TOOD remove, or put behind debug option

+ 146 - 309
backend/src/modules/DataModule.ts

@@ -1,22 +1,17 @@
 import config from "config";
-import mongoose, { Connection, Model, Schema, SchemaTypes } from "mongoose";
-import { patchHistoryPlugin, patchEventEmitter } from "ts-patch-mongoose";
 import { readdir } from "fs/promises";
 import path from "path";
-import updateVersioningPlugin from "mongoose-update-versioning";
 import { forEachIn } from "@common/utils/forEachIn";
-import Migration from "@/modules/DataModule/Migration";
-import documentVersionPlugin from "@/modules/DataModule/plugins/documentVersion";
-import getDataPlugin from "@/modules/DataModule/plugins/getData";
+import { Sequelize, Model as SequelizeModel, ModelStatic } from "sequelize";
+import { Dirent } from "fs";
+import * as inflection from "inflection";
 import BaseModule, { ModuleStatus } from "@/BaseModule";
 import EventsModule from "./EventsModule";
 import DataModuleJob from "./DataModule/DataModuleJob";
 import Job from "@/Job";
 
 export class DataModule extends BaseModule {
-	private _models?: Record<string, Model<any>>;
-
-	private _mongoConnection?: Connection;
+	private _sequelize?: Sequelize;
 
 	declare _jobs: Record<string, typeof Job | typeof DataModuleJob>;
 
@@ -35,17 +30,9 @@ export class DataModule extends BaseModule {
 	public override async startup() {
 		await super.startup();
 
-		await this._createMongoConnection();
-
-		await this._runMigrations();
-
-		await this._loadModels();
-
-		await this._syncModelIndexes();
+		await this._setupSequelize();
 
-		await this._loadModelJobs();
-
-		await this._loadModelEvents();
+		// await this._runMigrations();
 
 		await super._started();
 	}
@@ -55,246 +42,148 @@ export class DataModule extends BaseModule {
 	 */
 	public override async shutdown() {
 		await super.shutdown();
-		patchEventEmitter.removeAllListeners();
-		if (this._mongoConnection) await this._mongoConnection.close();
+		await this._sequelize?.close();
 		await this._stopped();
 	}
 
 	/**
-	 * createMongoConnection - Create mongo connection
+	 * setupSequelize - Setup sequelize instance
 	 */
-	private async _createMongoConnection() {
-		mongoose.set({
-			runValidators: true,
-			sanitizeFilter: true,
-			strict: "throw",
-			strictQuery: "throw"
+	private async _setupSequelize() {
+		const { username, password, host, port, database } = config.get<any>("postgres");
+		this._sequelize = new Sequelize(database, username, password, {
+			host,
+			port,
+			dialect: "postgres",
+			logging: message => this.log(message)
 		});
 
-		const { user, password, host, port, database } = config.get<{
-			user: string;
-			password: string;
-			host: string;
-			port: number;
-			database: string;
-		}>("mongo");
-		const mongoUrl = `mongodb://${user}:${password}@${host}:${port}/${database}`;
-
-		this._mongoConnection = await mongoose
-			.createConnection(mongoUrl)
-			.asPromise();
-	}
-
-	/**
-	 * registerEvents - Register events for schema with event module
-	 */
-	private async _registerEvents(modelName: string, schema: Schema<any>) {
-		const { enabled, eventCreated, eventUpdated, eventDeleted } =
-			schema.get("patchHistory") ?? {};
-
-		if (!enabled) return;
-
-		Object.entries({
-			created: eventCreated,
-			updated: eventUpdated,
-			deleted: eventDeleted
-		})
-			.filter(([, event]) => !!event)
-			.forEach(([action, event]) => {
-				patchEventEmitter.on(event!, async ({ doc, oldDoc }) => {
-					const modelId = doc?._id ?? oldDoc?._id;
-
-					const Model = await this.getModel(modelName);
-
-					if (doc) doc = Model.hydrate(doc);
-
-					if (oldDoc) oldDoc = Model.hydrate(oldDoc);
-
-					if (!modelId && action !== "created")
-						throw new Error(`Model Id not found for "${event}"`);
-
-					const EventClass = this.getEvent(`${modelName}.${action}`);
-
-					await EventsModule.publish(
-						new EventClass({ doc, oldDoc }, modelId)
-					);
-				});
-			});
-	}
-
-	/**
-	 * registerEvents - Register events for schema with event module
-	 */
-	private async _registerEventListeners(schema: Schema<any>) {
-		const eventListeners = schema.get("eventListeners");
-
-		if (
-			typeof eventListeners !== "object" ||
-			Object.keys(eventListeners).length === 0
-		)
-			return;
+		await this._sequelize.authenticate();
 
 		await forEachIn(
-			Object.entries(eventListeners),
-			async ([event, callback]) =>
-				EventsModule.pSubscribe(event, callback)
-		);
-	}
-
-	/**
-	 * loadModel - Import and load model schema
-	 *
-	 * @param modelName - Name of the model
-	 * @returns Model
-	 */
-	private async _loadModel(modelName: string): Promise<Model<any>> {
-		if (!this._mongoConnection) throw new Error("Mongo is not available");
-
-		const { schema }: { schema: Schema<any> } = await import(
-			`./DataModule/models/${modelName.toString()}/schema`
-		);
-
-		schema.plugin(documentVersionPlugin);
-
-		schema.set("timestamps", schema.get("timestamps") ?? true);
-
-		const patchHistoryConfig = {
-			enabled: true,
-			patchHistoryDisabled: true,
-			eventCreated: `${modelName}.created`,
-			eventUpdated: `${modelName}.updated`,
-			eventDeleted: `${modelName}.deleted`,
-			...(schema.get("patchHistory") ?? {})
-		};
-		schema.set("patchHistory", patchHistoryConfig);
-
-		if (patchHistoryConfig.enabled) {
-			schema.plugin(patchHistoryPlugin, patchHistoryConfig);
-		}
-
-		const { enabled: getDataEnabled = false } = schema.get("getData") ?? {};
-
-		if (getDataEnabled) schema.plugin(getDataPlugin);
-
-		schema.static("getModelName", () => modelName);
+			await readdir(
+				path.resolve(__dirname, `./${this.constructor.name}/models`),
+				{
+					withFileTypes: true
+				}
+			),
+			async modelFile => {
+				if (!modelFile.isFile() || modelFile.name.includes(".spec."))
+					return;
 
-		await this._registerEvents(modelName, schema);
+				const {
+					default: ModelClass,
+					schema,
+					options = {},
+					setup
+				} = await import(`${modelFile.path}/${modelFile.name}`);
 
-		await this._registerEventListeners(schema);
+				const tableName = inflection.camelize(
+					inflection.pluralize(ModelClass.name),
+					true
+				);
 
-		schema.set("toObject", { getters: true, virtuals: true });
-		schema.set("toJSON", { getters: true, virtuals: true });
+				ModelClass.init(schema, {
+					tableName,
+					...options,
+					sequelize: this._sequelize
+				});
 
-		schema.virtual("_name").get(() => modelName);
+				if (typeof setup === "function") await setup();
 
-		schema.plugin(updateVersioningPlugin);
+				await this._loadModelEvents(ModelClass.name);
 
-		await forEachIn(
-			Object.entries(schema.paths).filter(
-				([, type]) =>
-					type instanceof SchemaTypes.ObjectId ||
-					(type instanceof SchemaTypes.Array &&
-						type.caster instanceof SchemaTypes.ObjectId)
-			),
-			async ([key, type]) => {
-				const { ref } =
-					(type instanceof SchemaTypes.Array
-						? type.caster?.options
-						: type?.options) ?? {};
-
-				if (ref)
-					schema.path(key).get((value: any) => {
-						if (
-							typeof value === "object" &&
-							type instanceof SchemaTypes.ObjectId
-						)
-							return {
-								_id: value,
-								_name: ref
-							};
-
-						if (
-							Array.isArray(value) &&
-							type instanceof SchemaTypes.Array
-						)
-							return value.map(item =>
-								item === null
-									? null
-									: {
-											_id: item,
-											_name: ref
-									  }
-							);
-
-						return value;
-					});
+				await this._loadModelJobs(ModelClass.name);
 			}
 		);
 
-		return this._mongoConnection.model(modelName.toString(), schema);
+		this._sequelize.sync();
 	}
 
-	/**
-	 * loadModels - Load and initialize all models
-	 *
-	 * @returns Promise
-	 */
-	private async _loadModels() {
-		mongoose.SchemaTypes.String.set("trim", true);
-
-		this._models = {
-			abc: await this._loadModel("abc"),
-			minifiedUsers: await this._loadModel("minifiedUsers"),
-			news: await this._loadModel("news"),
-			sessions: await this._loadModel("sessions"),
-			stations: await this._loadModel("stations"),
-			users: await this._loadModel("users")
-		};
-	}
-
-	/**
-	 * syncModelIndexes - Sync indexes for all models
-	 */
-	private async _syncModelIndexes() {
-		if (!this._models) throw new Error("Models not loaded");
-
-		await forEachIn(
-			Object.values(this._models).filter(
-				model => model.schema.get("autoIndex") !== false
-			),
-			model => model.syncIndexes()
-		);
-	}
+	// /**
+	//  * registerEvents - Register events for schema with event module
+	//  */
+	// private async _registerEvents(modelName: string, schema: Schema<any>) {
+	// 	const { enabled, eventCreated, eventUpdated, eventDeleted } =
+	// 		schema.get("patchHistory") ?? {};
+
+	// 	if (!enabled) return;
+
+	// 	Object.entries({
+	// 		created: eventCreated,
+	// 		updated: eventUpdated,
+	// 		deleted: eventDeleted
+	// 	})
+	// 		.filter(([, event]) => !!event)
+	// 		.forEach(([action, event]) => {
+	// 			patchEventEmitter.on(event!, async ({ doc, oldDoc }) => {
+	// 				const modelId = doc?._id ?? oldDoc?._id;
+
+	// 				const Model = await this.getModel(modelName);
+
+	// 				if (doc) doc = Model.hydrate(doc);
+
+	// 				if (oldDoc) oldDoc = Model.hydrate(oldDoc);
+
+	// 				if (!modelId && action !== "created")
+	// 					throw new Error(`Model Id not found for "${event}"`);
+
+	// 				const EventClass = this.getEvent(`${modelName}.${action}`);
+
+	// 				await EventsModule.publish(
+	// 					new EventClass({ doc, oldDoc }, modelId)
+	// 				);
+	// 			});
+	// 		});
+	// }
+
+	// /**
+	//  * registerEvents - Register events for schema with event module
+	//  */
+	// private async _registerEventListeners(schema: Schema<any>) {
+	// 	const eventListeners = schema.get("eventListeners");
+
+	// 	if (
+	// 		typeof eventListeners !== "object" ||
+	// 		Object.keys(eventListeners).length === 0
+	// 	)
+	// 		return;
+
+	// 	await forEachIn(
+	// 		Object.entries(eventListeners),
+	// 		async ([event, callback]) =>
+	// 			EventsModule.pSubscribe(event, callback)
+	// 	);
+	// }
 
 	/**
 	 * getModel - Get model
 	 *
 	 * @returns Model
 	 */
-	public async getModel<ModelType extends Model<any>>(
+	public async getModel<ModelType extends SequelizeModel<any>>(
 		name: string
-	): Promise<ModelType> {
-		if (!this._models) throw new Error("Models not loaded");
+	): Promise<ModelStatic<ModelType>> {
+		if (!this._sequelize?.models) throw new Error("Models not loaded");
 
 		if (this.getStatus() !== ModuleStatus.STARTED)
 			throw new Error("Module not started");
 
-		if (!this._models[name]) throw new Error("Model not found");
-
-		return this._models[name] as ModelType;
+		return this._sequelize.model(name) as ModelStatic<ModelType>;
 	}
 
-	private async _loadModelMigrations(modelName: string) {
-		if (!this._mongoConnection) throw new Error("Mongo is not available");
-
-		let migrations;
+	private async _loadModelJobs(modelClassName: string) {
+		let jobs: Dirent[];
 
 		try {
-			migrations = await readdir(
+			jobs = await readdir(
 				path.resolve(
 					__dirname,
-					`./DataModule/models/${modelName}/migrations/`
-				)
+					`./${this.constructor.name}/models/${modelClassName}/jobs/`
+				),
+				{
+					withFileTypes: true
+				}
 			);
 		} catch (error) {
 			if (
@@ -302,107 +191,55 @@ export class DataModule extends BaseModule {
 				"code" in error &&
 				error.code === "ENOENT"
 			)
-				return [];
+				return;
 
 			throw error;
 		}
 
-		return forEachIn(migrations, async migrationFile => {
-			const { default: Migrate }: { default: typeof Migration } =
-				await import(
-					`./DataModule/models/${modelName}/migrations/${migrationFile}`
-				);
-			return new Migrate(this._mongoConnection as Connection);
-		});
-	}
+		await forEachIn(jobs, async jobFile => {
+			if (!jobFile.isFile() || jobFile.name.includes(".spec.")) return;
 
-	private async _loadMigrations() {
-		const models = await readdir(
-			path.resolve(__dirname, "./DataModule/models/")
-		);
-
-		return forEachIn(models, async modelName =>
-			this._loadModelMigrations(modelName)
-		);
-	}
-
-	private async _runMigrations() {
-		const migrations = (await this._loadMigrations()).flat();
-
-		for (let i = 0; i < migrations.length; i += 1) {
-			const migration = migrations[i];
-			// eslint-disable-next-line no-await-in-loop
-			await migration.up();
-		}
-	}
-
-	private async _loadModelJobs() {
-		if (!this._models) throw new Error("Models not loaded");
-
-		await forEachIn(Object.keys(this._models), async modelName => {
-			let jobs;
-
-			try {
-				jobs = await readdir(
-					path.resolve(
-						__dirname,
-						`./${this.constructor.name}/models/${modelName}/jobs/`
-					)
-				);
-			} catch (error) {
-				if (
-					error instanceof Error &&
-					"code" in error &&
-					error.code === "ENOENT"
-				)
-					return;
-
-				throw error;
-			}
-
-			await forEachIn(jobs, async jobFile => {
-				if (jobFile.includes(".spec.")) return;
-
-				const { default: Job } = await import(
-					`./${this.constructor.name}/models/${modelName}/jobs/${jobFile}`
-				);
+			const { default: JobClass } = await import(
+				`${jobFile.path}/${jobFile.name}`
+			);
 
-				this._jobs[Job.getName()] = Job;
-			});
+			this._jobs[JobClass.getName()] = JobClass;
 		});
 	}
 
-	private async _loadModelEvents() {
-		if (!this._models) throw new Error("Models not loaded");
+	private async _loadModelEvents(modelClassName: string) {
+		let events: Dirent[];
 
-		await forEachIn(Object.keys(this._models), async modelName => {
-			let events;
+		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;
 
-			try {
-				events = await readdir(
-					path.resolve(
-						__dirname,
-						`./${this.constructor.name}/models/${modelName}/events/`
-					)
-				);
-			} catch (error) {
-				if (
-					error instanceof Error &&
-					"code" in error &&
-					error.code === "ENOENT"
-				)
-					return;
+			throw error;
+		}
 
-				throw error;
-			}
+		await forEachIn(events, async eventFile => {
+			if (!eventFile.isFile() || eventFile.name.includes(".spec."))
+				return;
 
-			await forEachIn(events, async eventFile => {
-				const { default: EventClass } = await import(
-					`./${this.constructor.name}/models/${modelName}/events/${eventFile}`
-				);
+			const { default: EventClass } = await import(
+				`${eventFile.path}/${eventFile.name}`
+			);
 
-				this._events[EventClass.getName()] = EventClass;
-			});
+			this._events[EventClass.getName()] = EventClass;
 		});
 	}
 }

+ 2 - 3
backend/src/modules/DataModule/CreateJob.ts

@@ -1,5 +1,4 @@
 import Joi from "joi";
-import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
 
 export default abstract class CreateJob extends DataModuleJob {
@@ -10,9 +9,9 @@ export default abstract class CreateJob extends DataModuleJob {
 	protected async _execute() {
 		const { query } = this._payload;
 
-		const model = await DataModule.getModel(this.getModelName());
+		const model = this.getModel();
 
-		if (model.schema.path("createdBy"))
+		if (Object.hasOwn(model.getAttributes(), "createdBy"))
 			query.createdBy = (await this._context.getUser())._id;
 
 		return model.create(query);

+ 8 - 9
backend/src/modules/DataModule/DataModuleEvent.ts

@@ -1,29 +1,28 @@
-import { HydratedDocument, Model } from "mongoose";
+import { Model, ModelStatic } from "sequelize";
 import DataModule from "../DataModule";
 import ModuleEvent from "../EventsModule/ModuleEvent";
-import { UserSchema } from "./models/users/schema";
 
 export default abstract class DataModuleEvent extends ModuleEvent {
 	protected static _module = DataModule;
 
-	protected static _modelName: string;
+	protected static _model: ModelStatic<any>;
 
 	protected static _hasModelPermission:
 		| boolean
 		| CallableFunction
 		| (boolean | CallableFunction)[] = false;
 
-	public static override getName() {
-		return `${this._modelName}.${super.getName()}`;
+	public static getModel() {
+		return this._model;
 	}
 
-	public static getModelName() {
-		return this._modelName;
+	public static override getName() {
+		return `${this.getModel().getTableName()}.${super.getName()}`;
 	}
 
 	public static async hasModelPermission(
-		model: HydratedDocument<Model<any>>, // TODO model can be null too, as GetModelPermissions is currently written
-		user: HydratedDocument<UserSchema> | null
+		model: Model | null,
+		user: Model | null
 	) {
 		const options = Array.isArray(this._hasModelPermission)
 			? this._hasModelPermission

+ 11 - 17
backend/src/modules/DataModule/DataModuleJob.ts

@@ -1,10 +1,10 @@
-import { HydratedDocument, Model, isValidObjectId } from "mongoose";
+import { isValidObjectId } from "mongoose";
+import { Model, ModelStatic } from "sequelize";
 import Job, { JobOptions } from "@/Job";
 import DataModule from "../DataModule";
-import { UserSchema } from "./models/users/schema";
 
 export default abstract class DataModuleJob extends Job {
-	protected static _modelName: string;
+	protected static _model: ModelStatic<any>;
 
 	protected static _isBulk = false;
 
@@ -18,21 +18,15 @@ export default abstract class DataModuleJob extends Job {
 	}
 
 	public static override getName() {
-		return `${this._modelName}.${super.getName()}`;
+		return `${this.getModel().getTableName()}.${super.getName()}`;
 	}
 
-	public override getName() {
-		return `${
-			(this.constructor as typeof DataModuleJob)._modelName
-		}.${super.getName()}`;
+	public static getModel() {
+		return this._model;
 	}
 
-	public static getModelName() {
-		return this._modelName;
-	}
-
-	public getModelName() {
-		return (this.constructor as typeof DataModuleJob)._modelName;
+	public getModel() {
+		return (this.constructor as typeof DataModuleJob).getModel();
 	}
 
 	public static isBulk() {
@@ -40,12 +34,12 @@ export default abstract class DataModuleJob extends Job {
 	}
 
 	public isBulk() {
-		return (this.constructor as typeof DataModuleJob)._isBulk;
+		return (this.constructor as typeof DataModuleJob).isBulk();
 	}
 
 	public static async hasModelPermission(
-		model: HydratedDocument<Model<any>>, // TODO model can be null too, as GetModelPermissions is currently written
-		user: HydratedDocument<UserSchema> | null
+		model: Model | null,
+		user: Model | null
 	) {
 		const options = Array.isArray(this._hasModelPermission)
 			? this._hasModelPermission

+ 3 - 3
backend/src/modules/DataModule/DeleteByIdJob.ts

@@ -12,8 +12,8 @@ export default abstract class DeleteByIdJob extends DataModuleJob {
 	protected async _execute() {
 		const { _id } = this._payload;
 
-		const model = await DataModule.getModel(this.getModelName());
-
-		return model.deleteOne({ _id });
+		return this.getModel().destroy({
+			where: { _id }
+		});
 	}
 }

+ 3 - 6
backend/src/modules/DataModule/DeleteManyByIdJob.ts

@@ -17,13 +17,10 @@ export default abstract class DeleteManyByIdJob extends DataModuleJob {
 	});
 
 	protected async _execute() {
-		const model = await DataModule.getModel(this.getModelName());
-
 		const { _ids } = this._payload;
-		const query = model.deleteMany({
-			_id: _ids
-		});
 
-		return query.exec();
+		return this.getModel().destroy({
+			where: { _id: _ids }
+		});
 	}
 }

+ 4 - 4
backend/src/modules/DataModule/FindByIdJob.ts

@@ -10,10 +10,10 @@ export default abstract class FindByIdJob extends DataModuleJob {
 	});
 
 	protected async _execute() {
-		const model = await DataModule.getModel(this.getModelName());
+		const { _id } = this._payload;
 
-		const query = model.findById(this._payload._id);
-
-		return query.exec();
+		return this.getModel().findOne({
+			where: { _id }
+		});
 	}
 }

+ 3 - 6
backend/src/modules/DataModule/FindManyByIdJob.ts

@@ -17,13 +17,10 @@ export default abstract class FindManyByIdJob extends DataModuleJob {
 	});
 
 	protected async _execute() {
-		const model = await DataModule.getModel(this.getModelName());
-
 		const { _ids } = this._payload;
-		const query = model.find({
-			_id: _ids
-		});
 
-		return query.exec();
+		return this.getModel().findAll({
+			where: { _id: _ids }
+		});
 	}
 }

+ 165 - 8
backend/src/modules/DataModule/GetDataJob.ts

@@ -1,8 +1,22 @@
-import { Model } from "mongoose";
 import Joi from "joi";
 import DataModule from "../DataModule";
 import DataModuleJob from "./DataModuleJob";
-import { FilterType, GetData } from "./plugins/getData";
+import { FindOptions, WhereOptions, Op } from "sequelize";
+
+export enum FilterType {
+	REGEX = "regex",
+	CONTAINS = "contains",
+	EXACT = "exact",
+	DATETIME_BEFORE = "datetimeBefore",
+	DATETIME_AFTER = "datetimeAfter",
+	NUMBER_LESSER_EQUAL = "numberLesserEqual",
+	NUMBER_LESSER = "numberLesser",
+	NUMBER_GREATER = "numberGreater",
+	NUMBER_GREATER_EQUAL = "numberGreaterEqual",
+	NUMBER_EQUAL = "numberEquals",
+	BOOLEAN = "boolean",
+	SPECIAL = "special"
+}
 
 export default abstract class GetDataJob extends DataModuleJob {
 	protected static _payloadSchema = Joi.object({
@@ -33,14 +47,157 @@ export default abstract class GetDataJob extends DataModuleJob {
 		operator: Joi.string().valid("and", "or", "nor").required()
 	});
 
+	protected _blacklistedProperties?: string[];
+
+	protected _specialFilters?: Record<string, (query: FindOptions, data: any) => FindOptions>;
+
+	protected _specialProperties?: Record<string, (query: FindOptions) => FindOptions>;
+
+	protected _specialQueries?: Record<string, (query: WhereOptions) => WhereOptions>;
+
 	protected async _execute() {
-		const model = await DataModule.getModel<Model<any> & Partial<GetData>>(
-			this.getModelName()
-		);
+		const { page, pageSize, properties, sort, queries, operator } = this._payload;
+
+		let findQuery: FindOptions = {};
+
+		// If a query filter property or sort property is blacklisted, throw error
+		if (this._blacklistedProperties?.length ?? 0 > 0) {
+			if (
+				queries.some(query =>
+					this._blacklistedProperties!.some(blacklistedProperty =>
+						blacklistedProperty.startsWith(
+							query.filter.property
+						)
+					)
+				)
+			)
+				throw new Error(
+					"Unable to filter by blacklisted property."
+				);
+			if (
+				Object.keys(sort).some(property =>
+					this._blacklistedProperties!.some(blacklistedProperty =>
+						blacklistedProperty.startsWith(property)
+					)
+				)
+			)
+				throw new Error("Unable to sort by blacklisted property.");
+		}
+
+		// If a filter or property exists for a special property, add some custom pipeline steps
+		if (this._specialProperties)
+			Object.entries(this._specialProperties).forEach(
+				([specialProperty, modifyQuery]) => {
+					// Check if a filter with the special property exists
+					const filterExists =
+						queries
+							.map(query => query.filter.property)
+							.indexOf(specialProperty) !== -1;
+
+					// Check if a property with the special property exists
+					const propertyExists =
+						properties.indexOf(specialProperty) !== -1;
+
+					// If no such filter or property exists, skip this function
+					if (!filterExists && !propertyExists) return;
+
+					// Add the specified pipeline steps into the pipeline
+					findQuery = modifyQuery(findQuery);
+				}
+			);
+
+		// Adds where stage to query, which is responsible for filtering
+		const filterQueries = queries.flatMap(query => {
+			const { data, filter, filterType } = query;
+			const { property } = filter;
+
+			const newQuery: any = {};
+			switch (filterType) {
+				case FilterType.REGEX:
+					newQuery[property] = new RegExp(
+						`${data.slice(1, data.length - 1)}`,
+						"i"
+					);
+					break;
+				case FilterType.CONTAINS:
+					newQuery[property] = new RegExp(
+						`${data.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
+						"i"
+					);
+					break;
+				case FilterType.EXACT:
+					newQuery[property] = data.toString();
+					break;
+				case FilterType.DATETIME_BEFORE:
+					newQuery[property] = { [Op.lte]: new Date(data) };
+					break;
+				case FilterType.DATETIME_AFTER:
+					newQuery[property] = { [Op.gte]: new Date(data) };
+					break;
+				case FilterType.NUMBER_LESSER_EQUAL:
+					newQuery[property] = { [Op.lte]: Number(data) };
+					break;
+				case FilterType.NUMBER_LESSER:
+					newQuery[property] = { [Op.lt]: Number(data) };
+					break;
+				case FilterType.NUMBER_GREATER:
+					newQuery[property] = { [Op.gt]: Number(data) };
+					break;
+				case FilterType.NUMBER_GREATER_EQUAL:
+					newQuery[property] = { [Op.gte]: Number(data) };
+					break;
+				case FilterType.NUMBER_EQUAL:
+					newQuery[property] = { [Op.eq]: Number(data) };
+					break;
+				case FilterType.BOOLEAN:
+					newQuery[property] = { [Op.eq]: !!data };
+					break;
+				case FilterType.SPECIAL:
+					if (
+						typeof this._specialFilters?.[filter.property] ===
+							"function"
+					) {
+						findQuery = this._specialFilters[filter.property](findQuery, data);
+						newQuery[property] = { [Op.eq]: true };
+					}
+					break;
+				default:
+					throw new Error(`Invalid filter type for "${filter}"`);
+			}
+
+			if (
+				typeof this._specialQueries?.[filter.property] === "function"
+			) {
+				return this._specialQueries[filter.property](newQuery);
+			}
+
+			return newQuery;
+		});
+
+		if (filterQueries.length > 0)
+			findQuery.where = {
+				[Op[operator]]: filterQueries
+			};
+
+		// Adds order stage to query if there is at least one column being sorted, responsible for sorting data
+		if (Object.keys(sort).length > 0)
+			findQuery.order = Object.entries(sort).map(([property, direction]) => [
+				property,
+				direction === "ascending" ? "ASC" : "DESC"
+			]);
+
+		findQuery.attributes = {
+			include: properties,
+			exclude: this._blacklistedProperties
+		};
+		findQuery.offset = pageSize * (page - 1);
+		findQuery.limit = pageSize;
+
+		// Executes the query
+		const { rows, count } = (await this.getModel().findAndCountAll(findQuery));
 
-		if (typeof model.getData !== "function")
-			throw new Error("Get data not available for model");
+		const data = rows.map(model => model.toJSON()); // TODO: Review generally
 
-		return model.getData(this._payload);
+		return { data, count };
 	}
 }

+ 0 - 17
backend/src/modules/DataModule/Migration.ts

@@ -1,17 +0,0 @@
-import { Connection } from "mongoose";
-
-export default class Migration {
-	private _mongoConnection: Connection;
-
-	constructor(mongoConnection: Connection) {
-		this._mongoConnection = mongoConnection;
-	}
-
-	protected _getDb() {
-		return this._mongoConnection.db;
-	}
-
-	public async up() {}
-
-	public async down() {}
-}

+ 3 - 3
backend/src/modules/DataModule/UpdateByIdJob.ts

@@ -13,8 +13,8 @@ export default abstract class UpdateByIdJob extends DataModuleJob {
 	protected async _execute() {
 		const { _id, query } = this._payload;
 
-		const model = await DataModule.getModel(this.getModelName());
-
-		return model.updateOne({ _id }, { $set: query });
+		return this.getModel().update(query, {
+			where: { _id }
+		});
 	}
 }

+ 103 - 0
backend/src/modules/DataModule/models/News.ts

@@ -0,0 +1,103 @@
+import {
+	Sequelize,
+	DataTypes,
+	Model,
+	InferAttributes,
+	InferCreationAttributes,
+	CreationOptional,
+} from "sequelize";
+import { NewsStatus } from "@models/News/NewsStatus";
+import EventsModule from "@/modules/EventsModule";
+
+export class News extends Model<
+	InferAttributes<News>,
+	InferCreationAttributes<News>
+> {
+	declare id: CreationOptional<number>;
+
+	declare title: string;
+
+	declare markdown: string;
+
+	declare status: CreationOptional<NewsStatus>;
+
+	declare showToNewUsers: CreationOptional<boolean>;
+
+	declare createdAt: CreationOptional<Date>;
+
+	declare updatedAt: CreationOptional<Date>;
+}
+
+export const schema = {
+	id: {
+		type: DataTypes.BIGINT,
+		autoIncrement: true,
+		primaryKey: true
+	},
+	title: {
+		type: DataTypes.STRING,
+		allowNull: false
+	},
+	markdown: {
+		type: DataTypes.TEXT,
+		allowNull: false
+	},
+	status: {
+		type: DataTypes.ENUM(...Object.values(NewsStatus)),
+		defaultValue: NewsStatus.DRAFT,
+		allowNull: false
+	},
+	showToNewUsers: {
+		type: DataTypes.BOOLEAN,
+		defaultValue: false,
+		allowNull: false
+	},
+	createdBy: {
+		type: DataTypes.BIGINT,
+		allowNull: false
+	},
+	createdAt: DataTypes.DATE,
+	updatedAt: DataTypes.DATE
+};
+
+export const options = {};
+
+export const setup = async () => {
+	News.afterSave(async record => {
+		const oldDoc = record.previous();
+		const doc = record.get();
+	
+		if (oldDoc.status === doc.status) return;
+	
+		if (doc.status === NewsStatus.PUBLISHED) {
+			const EventClass = EventsModule.getEvent(
+				`data.news.published`
+			);
+			await EventsModule.publish(new EventClass({
+				doc
+			}));
+		} else if (oldDoc.status === NewsStatus.PUBLISHED) {
+			const EventClass = EventsModule.getEvent(
+				`data.news.unpublished`
+			);
+			await EventsModule.publish(new EventClass({
+				oldDoc
+			}, oldDoc.id!.toString()));
+		}
+	});
+	
+	News.afterDestroy(async record => {
+		const oldDoc = record.previous();
+	
+		if (oldDoc.status === NewsStatus.PUBLISHED) {
+			const EventClass = EventsModule.getEvent(
+				`data.news.unpublished`
+			);
+			await EventsModule.publish(new EventClass({
+				oldDoc
+			}, oldDoc.id!.toString()));
+		}
+	});
+};
+
+export default News;

+ 0 - 0
backend/src/modules/DataModule/models/news/NewsStatus.ts → backend/src/modules/DataModule/models/News/NewsStatus.ts


+ 3 - 2
backend/src/modules/DataModule/models/news/events/NewsCreatedEvent.ts → backend/src/modules/DataModule/models/News/events/NewsCreatedEvent.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import ModelCreatedEvent from "@/modules/DataModule/ModelCreatedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
-export default abstract class NewsCreatedEvent extends ModelCreatedEvent {
-	protected static _modelName = "news";
+export default class NewsCreatedEvent extends ModelCreatedEvent {
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 3 - 2
backend/src/modules/DataModule/models/news/events/NewsDeletedEvent.ts → backend/src/modules/DataModule/models/News/events/NewsDeletedEvent.ts

@@ -1,9 +1,10 @@
+import News from "@models/News";
 import ModelDeletedEvent from "@/modules/DataModule/ModelDeletedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 import isNewsPublished from "@/modules/DataModule/permissions/modelPermissions/isNewsPublished";
 
-export default abstract class NewsDeletedEvent extends ModelDeletedEvent {
-	protected static _modelName = "news";
+export default class NewsDeletedEvent extends ModelDeletedEvent {
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 

+ 3 - 2
backend/src/modules/DataModule/models/news/events/NewsPublishedEvent.ts → backend/src/modules/DataModule/models/News/events/NewsPublishedEvent.ts

@@ -1,7 +1,8 @@
+import News from "@models/News";
 import DataModuleEvent from "@/modules/DataModule/DataModuleEvent";
 
-export default abstract class NewsPublishedEvent extends DataModuleEvent {
-	protected static _modelName = "news";
+export default class NewsPublishedEvent extends DataModuleEvent {
+	protected static _model = News;
 
 	protected static _name = "published";
 

+ 3 - 2
backend/src/modules/DataModule/models/news/events/NewsUnpublishedEvent.ts → backend/src/modules/DataModule/models/News/events/NewsUnpublishedEvent.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import DataModuleEvent from "@/modules/DataModule/DataModuleEvent";
 import isNewsPublished from "@/modules/DataModule/permissions/modelPermissions/isNewsPublished";
 
-export default abstract class NewsUnpublishedEvent extends DataModuleEvent {
-	protected static _modelName = "news";
+export default class NewsUnpublishedEvent extends DataModuleEvent {
+	protected static _model = News;
 
 	protected static _name = "unpublished";
 

+ 3 - 2
backend/src/modules/DataModule/models/news/events/NewsUpdatedEvent.ts → backend/src/modules/DataModule/models/News/events/NewsUpdatedEvent.ts

@@ -1,9 +1,10 @@
+import News from "@models/News";
 import ModelUpdatedEvent from "@/modules/DataModule/ModelUpdatedEvent";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 import isNewsPublished from "@/modules/DataModule/permissions/modelPermissions/isNewsPublished";
 
-export default abstract class NewsUpdatedEvent extends ModelUpdatedEvent {
-	protected static _modelName = "news";
+export default class NewsUpdatedEvent extends ModelUpdatedEvent {
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/Create.ts → backend/src/modules/DataModule/models/News/jobs/Create.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import CreateJob from "@/modules/DataModule/CreateJob";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class Create extends CreateJob {
-	protected static _modelName = "news";
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/DeleteById.ts → backend/src/modules/DataModule/models/News/jobs/DeleteById.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import DeleteByIdJob from "@/modules/DataModule/DeleteByIdJob";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class DeleteById extends DeleteByIdJob {
-	protected static _modelName = "news";
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/DeleteManyById.ts → backend/src/modules/DataModule/models/News/jobs/DeleteManyById.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import DeleteManyByIdJob from "@/modules/DataModule/DeleteManyByIdJob";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class DeleteManyById extends DeleteManyByIdJob {
-	protected static _modelName = "news";
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/FindById.ts → backend/src/modules/DataModule/models/News/jobs/FindById.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import FindByIdJob from "@/modules/DataModule/FindByIdJob";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class FindById extends FindByIdJob {
-	protected static _modelName = "news";
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 2 - 1
backend/src/modules/DataModule/models/news/jobs/FindManyById.ts → backend/src/modules/DataModule/models/News/jobs/FindManyById.ts

@@ -1,8 +1,9 @@
+import News from "@models/News";
 import FindManyByIdJob from "@/modules/DataModule/FindManyByIdJob";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 
 export default class FindManyById extends FindManyByIdJob {
-	protected static _modelName = "news";
+	protected static _model = News;
 
 	protected static _hasPermission = isAdmin;
 }

+ 25 - 0
backend/src/modules/DataModule/models/News/jobs/GetData.ts

@@ -0,0 +1,25 @@
+import News from "@models/News";
+import GetDataJob from "@/modules/DataModule/GetDataJob";
+import isAdmin from "@/modules/DataModule/permissions/isAdmin";
+import { FindOptions, WhereOptions, Op } from "sequelize";
+
+export default class GetData extends GetDataJob {
+	protected static _model = News;
+
+	protected static _hasPermission = isAdmin;
+
+	protected _specialProperties?: Record<string, (query: FindOptions<News>) => FindOptions<News>> = {
+		createdBy: query => query
+	};
+
+	protected _specialQueries?: Record<string, (where: WhereOptions<News>) => WhereOptions<News>> = {
+		createdBy: where => ({
+			...where,
+			[Op.or]: [
+				{ createdBy: where.createdBy },
+				{ createdByUsername: where.createdBy }
+			],
+			createdBy: undefined
+		})
+	};
+}

+ 25 - 0
backend/src/modules/DataModule/models/News/jobs/Newest.ts

@@ -0,0 +1,25 @@
+import Joi from "joi";
+import News from "@models/News";
+import { NewsStatus } from "@models/News/NewsStatus";
+import DataModuleJob from "@/modules/DataModule/DataModuleJob";
+
+export default class Newest extends DataModuleJob {
+	protected static _model = News;
+
+	protected static _hasPermission = true;
+
+	protected static _payloadSchema = Joi.object({
+		showToNewUsers: Joi.boolean().optional(),
+		limit: Joi.number().min(1).optional()
+	});
+
+	protected async _execute() {
+		return this.getModel().findAll({
+			order: [["created_at", "DESC"]],
+			limit: this._payload?.limit,
+			where: {
+				status: NewsStatus.PUBLISHED
+			}
+		});
+	}
+}

+ 17 - 0
backend/src/modules/DataModule/models/News/jobs/Published.ts

@@ -0,0 +1,17 @@
+import News from "@models/News";
+import { NewsStatus } from "@models/News/NewsStatus";
+import DataModuleJob from "@/modules/DataModule/DataModuleJob";
+
+export default class Published extends DataModuleJob {
+	protected static _model = News;
+
+	protected static _hasPermission = true;
+
+	protected async _execute() {
+		return this.getModel().findAll({
+			where: {
+				status: NewsStatus.PUBLISHED
+			}
+		});
+	}
+}

+ 4 - 1
backend/src/modules/DataModule/models/news/jobs/UpdateById.ts → backend/src/modules/DataModule/models/News/jobs/UpdateById.ts

@@ -1,8 +1,11 @@
+import News from "@models/News";
 import isAdmin from "@/modules/DataModule/permissions/isAdmin";
 import UpdateByIdJob from "@/modules/DataModule/UpdateByIdJob";
 
 export default class UpdateById extends UpdateByIdJob {
-	protected static _modelName = "news";
+	protected static _model = News;
+
+	protected static _modelName = "News";
 
 	protected static _hasPermission = isAdmin;
 }

+ 0 - 70
backend/src/modules/DataModule/models/news/config.ts

@@ -1,70 +0,0 @@
-import { HydratedDocument } from "mongoose";
-import ModelCreatedEvent from "../../ModelCreatedEvent";
-import ModelDeletedEvent from "../../ModelDeletedEvent";
-import ModelUpdatedEvent from "../../ModelUpdatedEvent";
-import { NewsStatus } from "./NewsStatus";
-import getData from "./getData";
-import { NewsSchema, NewsSchemaOptions } from "./schema";
-import EventsModule from "@/modules/EventsModule";
-
-export default {
-	documentVersion: 3,
-	query: {
-		published() {
-			return this.where({ status: NewsStatus.PUBLISHED });
-		},
-		newest(showToNewUsers = false) {
-			const query = this.published().sort({ createdAt: "desc" });
-			if (showToNewUsers)
-				return query.where({ showToNewUsers: !!showToNewUsers });
-			return query;
-		}
-	},
-	eventListeners: {
-		"data.news.created.*": async (event: ModelCreatedEvent) => {
-			const { doc }: { doc: HydratedDocument<NewsSchema> } =
-				event.getData();
-
-			if (doc.status === NewsStatus.PUBLISHED) {
-				const EventClass = EventsModule.getEvent(`data.news.published`);
-				await EventsModule.publish(new EventClass({ doc }));
-			}
-		},
-		"data.news.updated.*": async (event: ModelUpdatedEvent) => {
-			const {
-				doc,
-				oldDoc
-			}: {
-				doc: HydratedDocument<NewsSchema>;
-				oldDoc: HydratedDocument<NewsSchema>;
-			} = event.getData();
-
-			if (doc.status === oldDoc.status) return;
-			if (doc.status === NewsStatus.PUBLISHED) {
-				const EventClass = EventsModule.getEvent(`data.news.published`);
-				await EventsModule.publish(new EventClass({ doc }));
-			} else if (oldDoc.status === NewsStatus.PUBLISHED) {
-				const EventClass = EventsModule.getEvent(
-					`data.news.unpublished`
-				);
-				await EventsModule.publish(
-					new EventClass({ oldDoc }, doc._id.toString()) // TODO maybe only pass modelId to unpublished event here, or when sending to clients
-				);
-			}
-		},
-		"data.news.deleted.*": async (event: ModelDeletedEvent) => {
-			const { oldDoc }: { oldDoc: HydratedDocument<NewsSchema> } =
-				event.getData();
-
-			if (oldDoc.status === NewsStatus.PUBLISHED) {
-				const EventClass = EventsModule.getEvent(
-					`data.news.unpublished`
-				);
-				await EventsModule.publish(
-					new EventClass({ oldDoc }, oldDoc._id.toString()) // TODO maybe only pass modelId to unpublished event here, or when sending to clients
-				);
-			}
-		}
-	},
-	getData
-} as NewsSchemaOptions;

+ 0 - 53
backend/src/modules/DataModule/models/news/getData.ts

@@ -1,53 +0,0 @@
-import { NewsSchemaOptions } from "./schema";
-
-export default {
-	enabled: true,
-	specialProperties: {
-		createdBy: [
-			{
-				$addFields: {
-					createdByOID: {
-						$convert: {
-							input: "$createdBy",
-							to: "objectId",
-							onError: "unknown",
-							onNull: "unknown"
-						}
-					}
-				}
-			},
-			{
-				$lookup: {
-					from: "users",
-					localField: "createdByOID",
-					foreignField: "_id",
-					as: "createdByUser"
-				}
-			},
-			{
-				$unwind: {
-					path: "$createdByUser",
-					preserveNullAndEmptyArrays: true
-				}
-			},
-			{
-				$addFields: {
-					createdByUsername: {
-						$ifNull: ["$createdByUser.username", "unknown"]
-					}
-				}
-			},
-			{
-				$project: {
-					createdByOID: 0,
-					createdByUser: 0
-				}
-			}
-		]
-	},
-	specialQueries: {
-		createdBy: newQuery => ({
-			$or: [newQuery, { createdByUsername: newQuery.createdBy }]
-		})
-	}
-} as NewsSchemaOptions["getData"];

+ 0 - 8
backend/src/modules/DataModule/models/news/jobs/GetData.ts

@@ -1,8 +0,0 @@
-import GetDataJob from "@/modules/DataModule/GetDataJob";
-import isAdmin from "@/modules/DataModule/permissions/isAdmin";
-
-export default class GetData extends GetDataJob {
-	protected static _modelName = "news";
-
-	protected static _hasPermission = isAdmin;
-}

+ 0 - 25
backend/src/modules/DataModule/models/news/jobs/Newest.ts

@@ -1,25 +0,0 @@
-import Joi from "joi";
-import DataModule from "@/modules/DataModule";
-import DataModuleJob from "@/modules/DataModule/DataModuleJob";
-import { NewsModel } from "../schema";
-
-export default class Newest extends DataModuleJob {
-	protected static _modelName = "news";
-
-	protected static _hasPermission = true;
-
-	protected static _payloadSchema = Joi.object({
-		showToNewUsers: Joi.boolean().optional(),
-		limit: Joi.number().min(1).optional()
-	});
-
-	protected async _execute() {
-		const model = await DataModule.getModel<NewsModel>(this.getModelName());
-
-		const query = model.find().newest(this._payload?.showToNewUsers);
-
-		if (this._payload?.limit) return query.limit(this._payload?.limit);
-
-		return query;
-	}
-}

+ 0 - 15
backend/src/modules/DataModule/models/news/jobs/Published.ts

@@ -1,15 +0,0 @@
-import DataModule from "@/modules/DataModule";
-import DataModuleJob from "@/modules/DataModule/DataModuleJob";
-import { NewsModel } from "../schema";
-
-export default class Published extends DataModuleJob {
-	protected static _modelName = "news";
-
-	protected static _hasPermission = true;
-
-	protected async _execute() {
-		const model = await DataModule.getModel<NewsModel>(this.getModelName());
-
-		return model.find().published();
-	}
-}

+ 0 - 58
backend/src/modules/DataModule/models/news/migrations/1620330161000-news-markdown.ts

@@ -1,58 +0,0 @@
-import Migration from "@/modules/DataModule/Migration";
-
-export default class Migration1620330161000 extends Migration {
-	async up() {
-		const News = this._getDb().collection("news");
-
-		const newsItems = News.find({ documentVersion: 1 }).stream();
-
-		for await (const newsItem of newsItems) {
-			newsItem.markdown = `# ${newsItem.title}\n\n`;
-			newsItem.markdown += `## ${newsItem.description}\n\n`;
-
-			if (newsItem.bugs) {
-				newsItem.markdown += `**Bugs:**\n\n${newsItem.bugs.join(
-					", "
-				)}\n\n`;
-			}
-
-			if (newsItem.features) {
-				newsItem.markdown += `**Features:**\n\n${newsItem.features.join(
-					", "
-				)}\n\n`;
-			}
-
-			if (newsItem.improvements) {
-				newsItem.markdown += `**Improvements:**\n\n${newsItem.improvements.join(
-					", "
-				)}\n\n`;
-			}
-
-			if (newsItem.upcoming) {
-				newsItem.markdown += `**Upcoming:**\n\n${newsItem.upcoming.join(
-					", "
-				)}\n`;
-			}
-
-			await News.updateOne(
-				{ _id: newsItem._id },
-				{
-					$set: {
-						markdown: newsItem.markdown,
-						status: "published",
-						documentVersion: 2
-					},
-					$unset: {
-						description: "",
-						bugs: "",
-						features: "",
-						improvements: "",
-						upcoming: ""
-					}
-				}
-			);
-		}
-	}
-
-	async down() {}
-}

+ 0 - 95
backend/src/modules/DataModule/models/news/schema.ts

@@ -1,95 +0,0 @@
-import {
-	HydratedDocument,
-	Model,
-	QueryWithHelpers,
-	Schema,
-	SchemaOptions,
-	SchemaTypes,
-	Types
-} from "mongoose";
-import { GetData } from "@/modules/DataModule/plugins/getData";
-import { BaseSchema } from "@/modules/DataModule/types/Schemas";
-import JobContext from "@/JobContext";
-import { NewsStatus } from "./NewsStatus";
-import config from "./config";
-
-export interface NewsSchema extends BaseSchema {
-	title: string;
-	markdown: string;
-	status: NewsStatus;
-	showToNewUsers: boolean;
-	createdBy: Types.ObjectId;
-}
-
-export interface NewsQueryHelpers {
-	published(
-		this: QueryWithHelpers<
-			any,
-			HydratedDocument<NewsSchema>,
-			NewsQueryHelpers
-		>,
-		published?: boolean
-	): QueryWithHelpers<
-		HydratedDocument<NewsSchema>[],
-		HydratedDocument<NewsSchema>,
-		NewsQueryHelpers
-	>;
-	newest(
-		this: QueryWithHelpers<
-			any,
-			HydratedDocument<NewsSchema>,
-			NewsQueryHelpers
-		>,
-		showToNewUsers?: boolean
-	): QueryWithHelpers<
-		HydratedDocument<NewsSchema>[],
-		HydratedDocument<NewsSchema>,
-		NewsQueryHelpers
-	>;
-}
-
-export interface NewsModel
-	extends Model<NewsSchema, NewsQueryHelpers>,
-		GetData {
-	published: (context: JobContext) => Promise<NewsSchema[]>;
-	newest: (
-		context: JobContext,
-		payload: { showToNewUsers?: boolean }
-	) => Promise<NewsSchema[]>;
-}
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-export const schema = new Schema<NewsSchema, NewsModel, {}, NewsQueryHelpers>(
-	{
-		title: {
-			type: SchemaTypes.String,
-			required: true
-		},
-		markdown: {
-			type: SchemaTypes.String,
-			required: true
-		},
-		status: {
-			type: SchemaTypes.String,
-			enum: Object.values(NewsStatus),
-			default: NewsStatus.DRAFT,
-			required: true
-		},
-		showToNewUsers: {
-			type: SchemaTypes.Boolean,
-			default: false,
-			required: true
-		},
-		createdBy: {
-			type: SchemaTypes.ObjectId,
-			ref: "minifiedUsers",
-			required: true
-		}
-	},
-	config
-);
-
-export type NewsSchemaType = typeof schema;
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-export type NewsSchemaOptions = SchemaOptions<NewsSchema, {}, NewsQueryHelpers>;

+ 3 - 4
backend/src/modules/DataModule/permissions/modelPermissions/isNewsPublished.ts

@@ -1,9 +1,8 @@
-import { HydratedDocument } from "mongoose";
-import { NewsStatus } from "../../models/news/NewsStatus";
-import { NewsSchema } from "../../models/news/schema";
+import News from "@models/News";
+import { NewsStatus } from "@models/News/NewsStatus";
 
 /**
  * Simply used to check if a news model exists and is published
  */
-export default (model: HydratedDocument<NewsSchema>) =>
+export default (model?: News) =>
 	model && model.status === NewsStatus.PUBLISHED;

+ 0 - 15
backend/src/modules/DataModule/plugins/documentVersion.ts

@@ -1,15 +0,0 @@
-import { Schema, SchemaTypes } from "mongoose";
-
-export interface DocumentVersion {
-	documentVersion: number;
-}
-
-export default function documentVersionPlugin(schema: Schema) {
-	schema.add({
-		documentVersion: {
-			type: SchemaTypes.Number,
-			default: schema.get("documentVersion") ?? 1,
-			required: true
-		}
-	});
-}

+ 7 - 7
backend/src/modules/EventsModule/jobs/Subscribe.spec.ts

@@ -6,15 +6,15 @@ import { TestModule } from "@/tests/support/TestModule";
 import Subscribe from "@/modules/EventsModule/jobs/Subscribe";
 import DataModule from "@/modules/DataModule";
 import EventsModule from "@/modules/EventsModule";
-import NewsCreatedEvent from "@/modules/DataModule/models/news/events/NewsCreatedEvent";
-import GetModelPermissions from "@/modules/DataModule/models/users/jobs/GetModelPermissions";
+import NewsCreatedEvent from "@models/News/events/NewsCreatedEvent";
+import GetModelPermissions from "@models/users/jobs/GetModelPermissions";
 import JobContext from "@/JobContext";
-import { UserRole } from "@/modules/DataModule/models/users/UserRole";
-import GetPermissions from "@/modules/DataModule/models/users/jobs/GetPermissions";
+import { UserRole } from "@models/users/UserRole";
+import GetPermissions from "@models/users/jobs/GetPermissions";
 import CacheModule from "@/modules/CacheModule";
-import NewsUpdatedEvent from "@/modules/DataModule/models/news/events/NewsUpdatedEvent";
-import NewsDeletedEvent from "@/modules/DataModule/models/news/events/NewsDeletedEvent";
-import { NewsStatus } from "@/modules/DataModule/models/news/NewsStatus";
+import NewsUpdatedEvent from "@models/News/events/NewsUpdatedEvent";
+import NewsDeletedEvent from "@models/News/events/NewsDeletedEvent";
+import { NewsStatus } from "@models/News/NewsStatus";
 
 describe("Subscribe job", async function () {
 	describe("execute", function () {

+ 16 - 0
compose.yml

@@ -18,6 +18,8 @@ services:
       - MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT=${MUSARE_DEBUG_GIT_LATEST_COMMIT_SHORT:-true}
       - MONGO_USER_USERNAME=${MONGO_USER_USERNAME}
       - MONGO_USER_PASSWORD=${MONGO_USER_PASSWORD}
+      - POSTGRES_USERNAME=${POSTGRES_USERNAME}
+      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
       - REDIS_PASSWORD=${REDIS_PASSWORD}
     links:
       - mongo
@@ -74,6 +76,18 @@ services:
     volumes:
       - cache:/data
 
+  postgres:
+    image: postgres
+    restart: unless-stopped
+    networks:
+      - backend
+    environment:
+      - POSTGRES_DB=musare
+      - POSTGRES_USER=${POSTGRES_USERNAME}
+      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+    volumes:
+      - pgdata:/var/lib/postgresql/data 
+
 networks:
   proxy:
 
@@ -82,4 +96,6 @@ networks:
 volumes:
   database:
 
+  pgdata:
+
   cache:

+ 34 - 6
musare.sh

@@ -122,14 +122,14 @@ runDockerCommand()
         throw "Error: Invalid runDockerCommand input"
     fi
 
-    servicesString=$(handleServices "backend frontend mongo redis" "${@:3}")
+    servicesString=$(handleServices "backend frontend mongo postgres redis" "${@:3}")
     if [[ ${servicesString:0:1} != 1 ]]; then
-        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, redis]"
+        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, postgres, redis]"
     fi
 
     if [[ ${servicesString:2:4} == "all" ]]; then
         servicesString=""
-        pullServices="mongo redis"
+        pullServices="mongo postgres redis"
         buildServices="backend frontend"
     else
         servicesString=${servicesString:2}
@@ -140,6 +140,10 @@ runDockerCommand()
             pullArray+=("mongo")
         fi
 
+        if [[ "${servicesString}" == *postgres* ]]; then
+            pullArray+=("postgres")
+        fi
+
         if [[ "${servicesString}" == *redis* ]]; then
             pullArray+=("redis")
         fi
@@ -198,9 +202,9 @@ getContainerId()
 # Reset services
 handleReset()
 {
-    servicesString=$(handleServices "backend frontend mongo redis" "${@:2}")
+    servicesString=$(handleServices "backend frontend mongo postgres redis" "${@:2}")
     if [[ ${servicesString:0:1} != 1 ]]; then
-        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, redis]"
+        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, postgres, redis]"
     fi
 
     confirmMessage="${GREEN}Are you sure you want to reset all data"
@@ -249,6 +253,11 @@ attachContainer()
             fi
             ;;
 
+        postgres)
+            echo -e "${YELLOW}Detach with CTRL+D${NC}"
+            PGPASSWORD="${POSTGRES_PASSWORD}" ${dockerCompose} exec postgres psql "${POSTGRES_USERNAME}" musare
+            ;;
+
         redis)
             echo -e "${YELLOW}Detach with CTRL+C${NC}"
             ${dockerCompose} exec redis redis-cli -a "${REDIS_PASSWORD}"
@@ -536,6 +545,18 @@ handleAdmin()
     esac
 }
 
+# Execute a command in a container
+handleExecute()
+{
+    servicesString=$(handleServices "backend frontend mongo postgres redis" "${2}")
+    if [[ ${servicesString:0:1} != 1 ]]; then
+        throw "${servicesString:2}\n${YELLOW}Usage: ${1} [backend, frontend, mongo, postgres, redis]"
+    fi
+
+    # shellcheck disable=SC2068
+    ${dockerCompose} exec "${2}" ${@:3}
+}
+
 availableCommands=$(cat << COMMANDS
 start - Start services
 stop - Stop services
@@ -543,7 +564,7 @@ restart - Restart services
 status - Service status
 logs - View logs for services
 update - Update Musare
-attach [backend,mongo,redis] - Attach to backend service, mongo or redis shell
+attach [backend,mongo,postgres,redis] - Attach to backend service, mongo, postgres or redis shell
 build - Build services
 lint - Run lint on frontend, backend, docs and/or shell
 backup - Backup database data to file
@@ -551,6 +572,7 @@ restore - Restore database data from backup file
 reset - Reset service data
 admin [add,remove] - Assign/unassign admin role to/from a user
 typescript - Run typescript checks on frontend and/or backend
+execute - Execute command in service docker container
 COMMANDS
 )
 
@@ -653,6 +675,12 @@ case $1 in
         handleAdmin "$(basename "$0") $1" ${@:2}
         ;;
 
+    execute|exec)
+        echo -e "${CYAN}Musare | Execute${NC}"
+        # shellcheck disable=SC2068
+        handleExecute "$(basename "$0") $1" ${@:2}
+        ;;
+
     "")
         echo -e "${CYAN}Musare | Available Commands${NC}"
         echo -e "${YELLOW}${availableCommands}${NC}"

部分文件因为文件数量过多而无法显示