Browse Source

refactor: fix tests, fix/ignore TS/eslint issues, fix some other issues

Kristian Vos 2 months ago
parent
commit
531e3d063d
25 changed files with 607 additions and 496 deletions
  1. 2 2
      backend/src/Job.ts
  2. 4 4
      backend/src/JobContext.ts
  3. 9 5
      backend/src/modules/DataModule.ts
  4. 54 20
      backend/src/modules/DataModule/GetDataJob.ts
  5. 2 0
      backend/src/modules/DataModule/migrations/1725485641-create-users-table.ts
  6. 4 0
      backend/src/modules/DataModule/migrations/1725660137-create-sessions-table.ts
  7. 4 0
      backend/src/modules/DataModule/migrations/1725660211-create-news-table.ts
  8. 2 0
      backend/src/modules/DataModule/models/MinifiedUser.ts
  9. 26 26
      backend/src/modules/DataModule/models/MinifiedUser/migrations/170526579600-create-minified-users-view.ts
  10. 12 3
      backend/src/modules/DataModule/models/News.ts
  11. 6 13
      backend/src/modules/DataModule/models/News/jobs/GetData.ts
  12. 3 0
      backend/src/modules/DataModule/models/Session.ts
  13. 2 0
      backend/src/modules/DataModule/models/Station.ts
  14. 5 5
      backend/src/modules/DataModule/models/Station/config.ts
  15. 58 58
      backend/src/modules/DataModule/models/Station/getData.ts
  16. 4 0
      backend/src/modules/DataModule/models/User.ts
  17. 26 26
      backend/src/modules/DataModule/models/User/config.ts
  18. 3 1
      backend/src/modules/DataModule/models/User/jobs/GetModelPermissions.ts
  19. 47 47
      backend/src/modules/DataModule/models/abc/schema.ts
  20. 1 0
      backend/src/modules/DataModule/permissions/modelPermissions/isDj.ts
  21. 3 2
      backend/src/modules/DataModule/permissions/modelPermissions/isOwner.ts
  22. 239 239
      backend/src/modules/DataModule/plugins/getData.ts
  23. 10 10
      backend/src/modules/DataModule/types/Schemas.ts
  24. 79 34
      backend/src/modules/EventsModule/jobs/Subscribe.spec.ts
  25. 2 1
      backend/src/types/sequelize.d.ts

+ 2 - 2
backend/src/Job.ts

@@ -1,4 +1,3 @@
-import { SessionSchema } from "@models/sessions/schema";
 import { getErrorMessage } from "@common/utils/getErrorMessage";
 import { generateUuid } from "@common/utils/generateUuid";
 import Joi from "joi";
@@ -10,6 +9,7 @@ import BaseModule from "./BaseModule";
 import EventsModule from "./modules/EventsModule";
 import JobCompletedEvent from "./modules/EventsModule/events/JobCompletedEvent";
 import User from "./modules/DataModule/models/User";
+import Session from "./modules/DataModule/models/Session";
 
 export enum JobStatus {
 	QUEUED = "QUEUED",
@@ -21,7 +21,7 @@ export enum JobStatus {
 export type JobOptions = {
 	priority?: number;
 	longJob?: string;
-	session?: SessionSchema;
+	session?: Session;
 	socketId?: string;
 	callbackRef?: string;
 };

+ 4 - 4
backend/src/JobContext.ts

@@ -1,5 +1,4 @@
 import { forEachIn } from "@common/utils/forEachIn";
-import { SessionSchema } from "@/modules/DataModule/models/sessions/schema";
 import Job, { JobOptions } from "@/Job";
 import { Log } from "@/LogBook";
 import DataModule from "@/modules/DataModule";
@@ -11,6 +10,7 @@ import {
 } from "./modules/DataModule/models/User/jobs/GetModelPermissions";
 import { GetPermissionsResult } from "./modules/DataModule/models/User/jobs/GetPermissions";
 import User from "./modules/DataModule/models/User";
+import Session from "./modules/DataModule/models/Session";
 
 const permissionRegex =
 	// eslint-disable-next-line max-len
@@ -19,7 +19,7 @@ const permissionRegex =
 export default class JobContext {
 	public readonly job: Job;
 
-	private _session?: SessionSchema;
+	private _session?: Session;
 
 	private readonly _socketId?: string;
 
@@ -28,7 +28,7 @@ export default class JobContext {
 	public constructor(
 		job: Job,
 		options?: {
-			session?: SessionSchema;
+			session?: Session;
 			socketId?: string;
 			callbackRef?: string;
 		}
@@ -52,7 +52,7 @@ export default class JobContext {
 		return this._session;
 	}
 
-	public setSession(session?: SessionSchema) {
+	public setSession(session?: Session) {
 		this._session = session;
 	}
 

+ 9 - 5
backend/src/modules/DataModule.ts

@@ -8,7 +8,6 @@ import {
 	ModelStatic,
 	DataTypes,
 	Utils,
-	ModelOptions,
 	Options
 } from "sequelize";
 import { Dirent } from "fs";
@@ -27,7 +26,7 @@ export type ObjectIdType = string;
 // 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 {
+export class OBJECTID extends DataTypes.ABSTRACT {
 	// Mandatory: set the type key
 	static key = "OBJECTID";
 
@@ -39,13 +38,16 @@ export class OBJECTID extends DataTypes.ABSTRACT.prototype.constructor {
 	}
 
 	// Optional: validator function
+	// eslint-disable-next-line
 	// @ts-ignore
-	validate(value, options) {
+	validate() {
+		// value, options
 		return true;
 		// return (typeof value === 'number') && (!Number.isNaN(value));
 	}
 
 	// Optional: sanitizer
+	// eslint-disable-next-line
 	// @ts-ignore
 	_sanitize(value) {
 		return value;
@@ -54,6 +56,7 @@ export class OBJECTID extends DataTypes.ABSTRACT.prototype.constructor {
 	}
 
 	// Optional: value stringifier before sending to database
+	// eslint-disable-next-line
 	// @ts-ignore
 	_stringify(value) {
 		if (value instanceof ObjectID) return value.toHexString();
@@ -61,6 +64,7 @@ export class OBJECTID extends DataTypes.ABSTRACT.prototype.constructor {
 	}
 
 	// Optional: parser for values received from the database
+	// eslint-disable-next-line
 	// @ts-ignore
 	static parse(value) {
 		return value;
@@ -140,7 +144,7 @@ export class DataModule extends BaseModule {
 
 		await this._sequelize.authenticate();
 
-		const setupFunctions: Function[] = [];
+		const setupFunctions: (() => Promise<void>)[] = [];
 
 		await forEachIn(
 			await readdir(
@@ -201,7 +205,7 @@ export class DataModule extends BaseModule {
 		return this._sequelize.model(camelizedName) as ModelStatic<ModelType>; // This fails - news has not been defined
 	}
 
-	private _getSequelizeHooks():  Options["hooks"] {
+	private _getSequelizeHooks(): Options["hooks"] {
 		return {
 			afterCreate: async model => {
 				const modelName = (

+ 54 - 20
backend/src/modules/DataModule/GetDataJob.ts

@@ -1,5 +1,5 @@
 import Joi from "joi";
-import { FindOptions, WhereOptions, Op } from "sequelize";
+import { FindOptions, WhereOptions, Op, IncludeOptions } from "sequelize";
 import DataModuleJob from "./DataModuleJob";
 
 export enum FilterType {
@@ -17,6 +17,27 @@ export enum FilterType {
 	SPECIAL = "special"
 }
 
+interface Sort {
+	[property: string]: "ascending" | "descending";
+}
+
+interface Query {
+	filter: {
+		property: string;
+	};
+	filterType: FilterType;
+	data: string;
+}
+
+type Payload = {
+	page: number;
+	pageSize: number;
+	properties: string[];
+	sort: Sort;
+	queries: Query[];
+	operator: "and" | "or" | "nor";
+};
+
 export default abstract class GetDataJob extends DataModuleJob {
 	protected static _payloadSchema = Joi.object({
 		page: Joi.number().required(),
@@ -52,7 +73,13 @@ export default abstract class GetDataJob extends DataModuleJob {
 					filterType: Joi.string()
 						.valid(...Object.values(FilterType))
 						.required(),
-					data: Joi.string().required()
+					data: Joi.alternatives()
+						.try(
+							Joi.boolean(),
+							Joi.string()
+							// Joi.number(),
+						)
+						.required()
 				})
 			)
 			.required(),
@@ -73,20 +100,20 @@ export default abstract class GetDataJob extends DataModuleJob {
 
 	protected _specialQueries?: Record<
 		string,
-		(query: WhereOptions) => {
+		(query: Record<string, WhereOptions>) => {
 			query: WhereOptions;
 			includeProperties: string[];
 		}
 	>;
 
 	protected async _execute() {
-		const { page, pageSize, properties, sort, queries, operator } =
-			this._payload;
+		const { page, pageSize, properties, sort, queries, operator } = this
+			._payload as Payload;
 
 		let findQuery: FindOptions = {};
 
 		// If a query filter property or sort property is blacklisted, throw error
-		if (this._blacklistedProperties?.length ?? 0 > 0) {
+		if (this._blacklistedProperties?.length) {
 			if (
 				queries.some(query =>
 					this._blacklistedProperties!.some(blacklistedProperty =>
@@ -128,7 +155,7 @@ export default abstract class GetDataJob extends DataModuleJob {
 			);
 
 		// Properties that we need to include (join) with Sequelize, e.g. createdByModel
-		const includePropertiesSet = new Set();
+		const includePropertiesSet = new Set<string>();
 
 		// Adds where stage to query, which is responsible for filtering
 		const filterQueries = queries.flatMap(query => {
@@ -138,16 +165,14 @@ export default abstract class GetDataJob extends DataModuleJob {
 			const newQuery: any = {};
 			switch (filterType) {
 				case FilterType.REGEX:
-					newQuery[property] = new RegExp(
-						`${data.slice(1, data.length - 1)}`,
-						"i"
-					);
+					newQuery[property] = {
+						[Op.iRegexp]: data
+					};
 					break;
 				case FilterType.CONTAINS:
-					newQuery[property] = new RegExp(
-						`${data.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
-						"i"
-					);
+					newQuery[property] = {
+						[Op.like]: `%${data}%`
+					};
 					break;
 				case FilterType.EXACT:
 					newQuery[property] = data.toString();
@@ -207,10 +232,19 @@ export default abstract class GetDataJob extends DataModuleJob {
 			return newQuery;
 		});
 
-		if (filterQueries.length > 0)
-			findQuery.where = {
-				[Op[operator]]: filterQueries
-			};
+		if (filterQueries.length > 0) {
+			if (operator === "nor") {
+				findQuery.where = {
+					[Op.not]: {
+						[Op.or]: filterQueries
+					}
+				};
+			} else {
+				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)
@@ -238,7 +272,7 @@ export default abstract class GetDataJob extends DataModuleJob {
 				model: targetModel, // E.g. User
 				as: includeProperty, // e.g. "createdByModel"
 				attributes: [] // We do not want to return any data from anything we include
-			};
+			} as IncludeOptions;
 		});
 
 		// Executes the query

+ 2 - 0
backend/src/modules/DataModule/migrations/1725485641-create-users-table.ts

@@ -6,6 +6,8 @@ export const up = async ({
 }: MigrationParams<Sequelize>) => {
 	await sequelize.getQueryInterface().createTable("users", {
 		_id: {
+			// eslint-disable-next-line
+			// @ts-ignore
 			type: DataTypes.OBJECTID,
 			primaryKey: true,
 			allowNull: false

+ 4 - 0
backend/src/modules/DataModule/migrations/1725660137-create-sessions-table.ts

@@ -6,11 +6,15 @@ export const up = async ({
 }: MigrationParams<Sequelize>) => {
 	await sequelize.getQueryInterface().createTable("sessions", {
 		_id: {
+			// eslint-disable-next-line
+			// @ts-ignore
 			type: DataTypes.OBJECTID,
 			allowNull: false,
 			primaryKey: true
 		},
 		userId: {
+			// eslint-disable-next-line
+			// @ts-ignore
 			type: DataTypes.OBJECTID,
 			allowNull: false
 		},

+ 4 - 0
backend/src/modules/DataModule/migrations/1725660211-create-news-table.ts

@@ -6,6 +6,8 @@ export const up = async ({
 }: MigrationParams<Sequelize>) => {
 	await sequelize.getQueryInterface().createTable("news", {
 		_id: {
+			// eslint-disable-next-line
+			// @ts-ignore
 			type: DataTypes.OBJECTID,
 			allowNull: false,
 			primaryKey: true
@@ -29,6 +31,8 @@ export const up = async ({
 			allowNull: false
 		},
 		createdBy: {
+			// eslint-disable-next-line
+			// @ts-ignore
 			type: DataTypes.OBJECTID,
 			allowNull: false
 		},

+ 2 - 0
backend/src/modules/DataModule/models/MinifiedUser.ts

@@ -10,7 +10,9 @@ import { UserRole } from "./User/UserRole";
 import { schema as userSchema } from "./User";
 
 export class MinifiedUser extends Model<
+	// eslint-disable-next-line no-use-before-define
 	InferAttributes<MinifiedUser>,
+	// eslint-disable-next-line no-use-before-define
 	InferCreationAttributes<MinifiedUser>
 > {
 	declare _id: CreationOptional<ObjectIdType>;

+ 26 - 26
backend/src/modules/DataModule/models/MinifiedUser/migrations/170526579600-create-minified-users-view.ts

@@ -1,28 +1,28 @@
-import Migration from "@/modules/DataModule/Migration";
+// import Migration from "@/modules/DataModule/Migration";
 
-export default class Migration170526579600 extends Migration {
-	async up() {
-		// await this._getDb().createCollection("minifiedUsers", {
-		// 	viewOn: "users",
-		// 	pipeline: [
-		// 		{
-		// 			$project: {
-		// 				_id: 1,
-		// 				name: 1,
-		// 				username: 1,
-		// 				location: 1,
-		// 				bio: 1,
-		// 				role: 1,
-		// 				avatar: 1,
-		// 				createdAt: 1,
-		// 				updatedAt: 1
-		// 			}
-		// 		}
-		// 	]
-		// });
-	}
+// export default class Migration170526579600 extends Migration {
+// 	async up() {
+// 		// await this._getDb().createCollection("minifiedUsers", {
+// 		// 	viewOn: "users",
+// 		// 	pipeline: [
+// 		// 		{
+// 		// 			$project: {
+// 		// 				_id: 1,
+// 		// 				name: 1,
+// 		// 				username: 1,
+// 		// 				location: 1,
+// 		// 				bio: 1,
+// 		// 				role: 1,
+// 		// 				avatar: 1,
+// 		// 				createdAt: 1,
+// 		// 				updatedAt: 1
+// 		// 			}
+// 		// 		}
+// 		// 	]
+// 		// });
+// 	}
 
-	async down() {
-		await this._getDb().dropCollection("minifiedUsers");
-	}
-}
+// 	async down() {
+// 		await this._getDb().dropCollection("minifiedUsers");
+// 	}
+// }

+ 12 - 3
backend/src/modules/DataModule/models/News.ts

@@ -17,7 +17,9 @@ import User from "./User";
 import { ObjectIdType } from "@/modules/DataModule";
 
 export class News extends Model<
+	// eslint-disable-next-line no-use-before-define
 	InferAttributes<News>,
+	// eslint-disable-next-line no-use-before-define
 	InferCreationAttributes<News>
 > {
 	declare _id: CreationOptional<ObjectIdType>;
@@ -30,7 +32,12 @@ export class News extends Model<
 
 	declare showToNewUsers: CreationOptional<boolean>;
 
-	declare createdBy: ForeignKey<User["_id"]>;
+	declare createdBy:
+		| ForeignKey<User["_id"]>
+		| {
+				_id: ForeignKey<User["_id"]>;
+				_name: "minifiedUsers";
+		  };
 
 	declare createdAt: CreationOptional<Date>;
 
@@ -45,6 +52,7 @@ export class News extends Model<
 	declare createdByModel?: NonAttribute<User>;
 
 	declare static associations: {
+		// eslint-disable-next-line no-use-before-define
 		createdByModel: Association<News, User>;
 	};
 }
@@ -140,7 +148,7 @@ export const setup = async () => {
 		}
 	});
 
-	News.addHook("afterFind", (_news, options) => {
+	News.addHook("afterFind", _news => {
 		if (!_news) return;
 
 		// TODO improve TS
@@ -160,12 +168,13 @@ export const setup = async () => {
 		>[] = [];
 
 		if (Array.isArray(_news)) news = _news;
+		// eslint-disable-next-line
 		// @ts-ignore - possibly not needed after TS update
 		else news.push(_news);
 
 		news.forEach(news => {
 			news.dataValues.createdBy = {
-				_id: news.dataValues.createdBy,
+				_id: news.dataValues.createdBy.toString(),
 				_name: "minifiedUsers"
 			};
 		});

+ 6 - 13
backend/src/modules/DataModule/models/News/jobs/GetData.ts

@@ -15,18 +15,11 @@ export default class GetData extends GetDataJob {
 		createdBy: query => query
 	};
 
-	protected _specialQueries?: Record<
-		string,
-		(where: WhereOptions<News>) => {
-			query: WhereOptions<News>;
-			includeProperties: string[];
-		}
-	> = {
-		createdBy: where => {
-			const createdBy =
-				where.createdBy instanceof RegExp
-					? where.createdBy.source
-					: where.createdBy;
+	// eslint-disable-next-line
+	// @ts-ignore
+	protected _specialQueries = {
+		createdBy: (where: { createdBy: WhereOptions<News> }) => {
+			const { createdBy } = where;
 			// See https://sequelize.org/docs/v6/advanced-association-concepts/eager-loading/#complex-where-clauses-at-the-top-level for more info
 			return {
 				query: {
@@ -41,4 +34,4 @@ export default class GetData extends GetDataJob {
 	};
 }
 
-// TODO createdBy should not allow contains/RegExp? Maybe. Or only allow searching for username if it's exact
+// TODO review createdBy regex/contains/case-sensitive/etc. Maybe only allow searching for username if it's exact

+ 3 - 0
backend/src/modules/DataModule/models/Session.ts

@@ -15,7 +15,9 @@ import { ObjectIdType } from "@/modules/DataModule";
 import User from "./User";
 
 export class Session extends Model<
+	// eslint-disable-next-line no-use-before-define
 	InferAttributes<Session>,
+	// eslint-disable-next-line no-use-before-define
 	InferCreationAttributes<Session>
 > {
 	declare _id: ObjectIdType;
@@ -35,6 +37,7 @@ export class Session extends Model<
 	declare userModel?: NonAttribute<User>;
 
 	declare static associations: {
+		// eslint-disable-next-line no-use-before-define
 		userModel: Association<Session, User>;
 	};
 }

+ 2 - 0
backend/src/modules/DataModule/models/Station.ts

@@ -11,7 +11,9 @@ import { StationTheme } from "./Station/StationTheme";
 import { StationPrivacy } from "./Station/StationPrivacy";
 
 export class Station extends Model<
+	// eslint-disable-next-line no-use-before-define
 	InferAttributes<Station>,
+	// eslint-disable-next-line no-use-before-define
 	InferCreationAttributes<Station>
 > {
 	declare _id: CreationOptional<ObjectIdType>;

+ 5 - 5
backend/src/modules/DataModule/models/Station/config.ts

@@ -1,6 +1,6 @@
-import getData from "./getData";
+// import getData from "./getData";
 
-export default {
-	documentVersion: 10,
-	getData
-};
+// export default {
+// 	documentVersion: 10,
+// 	getData
+// };

+ 58 - 58
backend/src/modules/DataModule/models/Station/getData.ts

@@ -1,59 +1,59 @@
-import { StationSchemaOptions } from "./schema";
+// import { StationSchemaOptions } from "./schema";
 
-export default {
-	enabled: true,
-	specialProperties: {
-		owner: [
-			{
-				$addFields: {
-					ownerOID: {
-						$convert: {
-							input: "$owner",
-							to: "objectId",
-							onError: "unknown",
-							onNull: "unknown"
-						}
-					}
-				}
-			},
-			{
-				$lookup: {
-					from: "users",
-					localField: "ownerOID",
-					foreignField: "_id",
-					as: "ownerUser"
-				}
-			},
-			{
-				$unwind: {
-					path: "$ownerUser",
-					preserveNullAndEmptyArrays: true
-				}
-			},
-			{
-				$addFields: {
-					ownerUsername: {
-						$cond: [
-							{ $eq: [{ $type: "$owner" }, "string"] },
-							{
-								$ifNull: ["$ownerUser.username", "unknown"]
-							},
-							"none"
-						]
-					}
-				}
-			},
-			{
-				$project: {
-					ownerOID: 0,
-					ownerUser: 0
-				}
-			}
-		]
-	},
-	specialQueries: {
-		owner: newQuery => ({
-			$or: [newQuery, { ownerUsername: newQuery.owner }]
-		})
-	}
-} as StationSchemaOptions["getData"];
+// export default {
+// 	enabled: true,
+// 	specialProperties: {
+// 		owner: [
+// 			{
+// 				$addFields: {
+// 					ownerOID: {
+// 						$convert: {
+// 							input: "$owner",
+// 							to: "objectId",
+// 							onError: "unknown",
+// 							onNull: "unknown"
+// 						}
+// 					}
+// 				}
+// 			},
+// 			{
+// 				$lookup: {
+// 					from: "users",
+// 					localField: "ownerOID",
+// 					foreignField: "_id",
+// 					as: "ownerUser"
+// 				}
+// 			},
+// 			{
+// 				$unwind: {
+// 					path: "$ownerUser",
+// 					preserveNullAndEmptyArrays: true
+// 				}
+// 			},
+// 			{
+// 				$addFields: {
+// 					ownerUsername: {
+// 						$cond: [
+// 							{ $eq: [{ $type: "$owner" }, "string"] },
+// 							{
+// 								$ifNull: ["$ownerUser.username", "unknown"]
+// 							},
+// 							"none"
+// 						]
+// 					}
+// 				}
+// 			},
+// 			{
+// 				$project: {
+// 					ownerOID: 0,
+// 					ownerUser: 0
+// 				}
+// 			}
+// 		]
+// 	},
+// 	specialQueries: {
+// 		owner: newQuery => ({
+// 			$or: [newQuery, { ownerUsername: newQuery.owner }]
+// 		})
+// 	}
+// } as StationSchemaOptions["getData"];

+ 4 - 0
backend/src/modules/DataModule/models/User.ts

@@ -23,7 +23,9 @@ import Session from "./Session";
 import News from "./News";
 
 export class User extends Model<
+	// eslint-disable-next-line no-use-before-define
 	InferAttributes<User>,
+	// eslint-disable-next-line no-use-before-define
 	InferCreationAttributes<User>
 > {
 	declare _id: CreationOptional<ObjectIdType>;
@@ -95,7 +97,9 @@ export class User extends Model<
 	declare createdNewsModels?: NonAttribute<News[]>;
 
 	declare static associations: {
+		// eslint-disable-next-line no-use-before-define
 		sessionModels: Association<User, Session>;
+		// eslint-disable-next-line no-use-before-define
 		createdNewsModels: Association<User, News>;
 	};
 }

+ 26 - 26
backend/src/modules/DataModule/models/User/config.ts

@@ -1,27 +1,27 @@
-import { SchemaOptions } from "mongoose";
-import CacheModule from "@/modules/CacheModule";
-import getData from "./getData";
-import { UserSchema } from "./schema";
-import ModelUpdatedEvent from "../../ModelUpdatedEvent";
-import ModelDeletedEvent from "../../ModelDeletedEvent";
+// import { SchemaOptions } from "mongoose";
+// import CacheModule from "@/modules/CacheModule";
+// import getData from "./getData";
+// import { UserSchema } from "./schema";
+// import ModelUpdatedEvent from "../../ModelUpdatedEvent";
+// import ModelDeletedEvent from "../../ModelDeletedEvent";
 
-export default {
-	documentVersion: 4,
-	eventListeners: {
-		"data.users.updated.*": async (event: ModelUpdatedEvent) => {
-			const { doc } = event.getData();
-			CacheModule.removeMany([
-				`user-permissions.${doc._id}`,
-				`model-permissions.*.user.${doc._id}`
-			]);
-		},
-		"data.users.deleted.*": async (event: ModelDeletedEvent) => {
-			const { oldDoc } = event.getData();
-			CacheModule.removeMany([
-				`user-permissions.${oldDoc._id}`,
-				`model-permissions.*.user.${oldDoc._id}`
-			]);
-		}
-	},
-	getData
-} as SchemaOptions<UserSchema>;
+// export default {
+// 	documentVersion: 4,
+// 	eventListeners: {
+// 		"data.users.updated.*": async (event: ModelUpdatedEvent) => {
+// 			const { doc } = event.getData();
+// 			CacheModule.removeMany([
+// 				`user-permissions.${doc._id}`,
+// 				`model-permissions.*.user.${doc._id}`
+// 			]);
+// 		},
+// 		"data.users.deleted.*": async (event: ModelDeletedEvent) => {
+// 			const { oldDoc } = event.getData();
+// 			CacheModule.removeMany([
+// 				`user-permissions.${oldDoc._id}`,
+// 				`model-permissions.*.user.${oldDoc._id}`
+// 			]);
+// 		}
+// 	},
+// 	getData
+// } as SchemaOptions<UserSchema>;

+ 3 - 1
backend/src/modules/DataModule/models/User/jobs/GetModelPermissions.ts

@@ -118,7 +118,9 @@ export default class GetModelPermissions extends DataModuleJob {
 		// Loop through the modelIds that were not cached, and get the permissions for each one individually
 		await forEachIn(uncachedModelIds, async modelId => {
 			const model = uncachedModels.find(
-				model => model._id.toString() === modelId.toString()
+				model =>
+					model.dataValues[Model.primaryKeyAttribute] ===
+					modelId.toString()
 			);
 			if (!model) throw new Error(`No model found for ${modelId}.`);
 

+ 47 - 47
backend/src/modules/DataModule/models/abc/schema.ts

@@ -1,51 +1,51 @@
-import { Model, Schema, SchemaTypes, Types } from "mongoose";
-import {
-	BaseSchema,
-	TimestampsSchema
-} from "@/modules/DataModule/types/Schemas";
+// import { Model, Schema, SchemaTypes, Types } from "mongoose";
+// import {
+// 	BaseSchema,
+// 	TimestampsSchema
+// } from "@/modules/DataModule/types/Schemas";
 
-export interface AbcSchema extends Omit<BaseSchema, keyof TimestampsSchema> {
-	name: string;
-	autofill?: {
-		enabled?: boolean;
-	};
-	someNumbers: number[];
-	songs: { _id: Types.ObjectId }[];
-	restrictedName?: string;
-	aNumber: number;
-}
+// export interface AbcSchema extends Omit<BaseSchema, keyof TimestampsSchema> {
+// 	name: string;
+// 	autofill?: {
+// 		enabled?: boolean;
+// 	};
+// 	someNumbers: number[];
+// 	songs: { _id: Types.ObjectId }[];
+// 	restrictedName?: string;
+// 	aNumber: number;
+// }
 
-export type AbcModel = Model<AbcSchema>;
+// export type AbcModel = Model<AbcSchema>;
 
-export const schema = new Schema<AbcSchema, AbcModel>(
-	{
-		name: {
-			type: SchemaTypes.String,
-			required: true
-		},
-		autofill: {
-			type: {
-				_id: false,
-				enabled: {
-					type: SchemaTypes.Boolean,
-					required: false
-				}
-			},
-			restricted: true
-		},
-		someNumbers: [{ type: SchemaTypes.Number }],
-		songs: [
-			{
-				_id: { type: SchemaTypes.ObjectId, required: true }
-			}
-		],
-		restrictedName: {
-			type: SchemaTypes.String,
-			restricted: true
-		},
-		aNumber: { type: SchemaTypes.Number, required: true }
-	},
-	{ timestamps: false }
-);
+// export const schema = new Schema<AbcSchema, AbcModel>(
+// 	{
+// 		name: {
+// 			type: SchemaTypes.String,
+// 			required: true
+// 		},
+// 		autofill: {
+// 			type: {
+// 				_id: false,
+// 				enabled: {
+// 					type: SchemaTypes.Boolean,
+// 					required: false
+// 				}
+// 			},
+// 			restricted: true
+// 		},
+// 		someNumbers: [{ type: SchemaTypes.Number }],
+// 		songs: [
+// 			{
+// 				_id: { type: SchemaTypes.ObjectId, required: true }
+// 			}
+// 		],
+// 		restrictedName: {
+// 			type: SchemaTypes.String,
+// 			restricted: true
+// 		},
+// 		aNumber: { type: SchemaTypes.Number, required: true }
+// 	},
+// 	{ timestamps: false }
+// );
 
-export type AbcSchemaType = typeof schema;
+// export type AbcSchemaType = typeof schema;

+ 1 - 0
backend/src/modules/DataModule/permissions/modelPermissions/isDj.ts

@@ -1,6 +1,7 @@
 import User from "../../models/User";
 import Station from "../../models/Station";
 
+// eslint-disable-next-line
 export default (model: Station, user?: User) => false;
 // TODO
 // model && user && model.djs.includes(user._id);

+ 3 - 2
backend/src/modules/DataModule/permissions/modelPermissions/isOwner.ts

@@ -9,9 +9,10 @@ export default (
 
 	let ownerAttribute;
 
-	if (model.dataValues.hasOwnProperty("createdBy"))
+	if (Object.prototype.hasOwnProperty.call(model.dataValues, "createdBy"))
 		ownerAttribute = "createdBy";
-	else if (model.dataValues.hasOwnProperty("owner")) ownerAttribute = "owner";
+	else if (Object.prototype.hasOwnProperty.call(model.dataValues, "owner"))
+		ownerAttribute = "owner";
 
 	if (ownerAttribute)
 		return (

+ 239 - 239
backend/src/modules/DataModule/plugins/getData.ts

@@ -1,266 +1,266 @@
-import { PipelineStage, Schema, SchemaTypes } from "mongoose";
+// import { PipelineStage, Schema, SchemaTypes } from "mongoose";
 
-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 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 interface GetData {
-	getData(payload: {
-		page: number;
-		pageSize: number;
-		properties: string[];
-		sort: Record<string, "ascending" | "descending">;
-		queries: {
-			data: any;
-			filter: {
-				property: string;
-			};
-			filterType: FilterType;
-		}[];
-		operator: "and" | "or" | "nor";
-	}): Promise<{
-		data: any[];
-		count: number;
-	}>;
-}
+// export interface GetData {
+// 	getData(payload: {
+// 		page: number;
+// 		pageSize: number;
+// 		properties: string[];
+// 		sort: Record<string, "ascending" | "descending">;
+// 		queries: {
+// 			data: any;
+// 			filter: {
+// 				property: string;
+// 			};
+// 			filterType: FilterType;
+// 		}[];
+// 		operator: "and" | "or" | "nor";
+// 	}): Promise<{
+// 		data: any[];
+// 		count: number;
+// 	}>;
+// }
 
-export default function getDataPlugin(schema: Schema) {
-	schema.static(
-		"getData",
-		async function getData(
-			payload: Parameters<GetData["getData"]>[0]
-		): ReturnType<GetData["getData"]> {
-			const { page, pageSize, properties, sort, queries, operator } =
-				payload;
+// export default function getDataPlugin(schema: Schema) {
+// 	schema.static(
+// 		"getData",
+// 		async function getData(
+// 			payload: Parameters<GetData["getData"]>[0]
+// 		): ReturnType<GetData["getData"]> {
+// 			const { page, pageSize, properties, sort, queries, operator } =
+// 				payload;
 
-			const getData = schema.get("getData");
-			if (!getData)
-				throw new Error("Schema doesn't have getData defined.");
-			const {
-				blacklistedProperties,
-				specialFilters,
-				specialProperties,
-				specialQueries
-			} = getData;
+// 			const getData = schema.get("getData");
+// 			if (!getData)
+// 				throw new Error("Schema doesn't have getData defined.");
+// 			const {
+// 				blacklistedProperties,
+// 				specialFilters,
+// 				specialProperties,
+// 				specialQueries
+// 			} = getData;
 
-			const pipeline: PipelineStage[] = [];
+// 			const pipeline: PipelineStage[] = [];
 
-			// If a query filter property or sort property is blacklisted, throw error
-			if (Array.isArray(blacklistedProperties)) {
-				if (
-					queries.some(query =>
-						blacklistedProperties.some(blacklistedProperty =>
-							blacklistedProperty.startsWith(
-								query.filter.property
-							)
-						)
-					)
-				)
-					throw new Error(
-						"Unable to filter by blacklisted property."
-					);
-				if (
-					Object.keys(sort).some(property =>
-						blacklistedProperties.some(blacklistedProperty =>
-							blacklistedProperty.startsWith(property)
-						)
-					)
-				)
-					throw new Error("Unable to sort by blacklisted property.");
-			}
+// 			// If a query filter property or sort property is blacklisted, throw error
+// 			if (Array.isArray(blacklistedProperties)) {
+// 				if (
+// 					queries.some(query =>
+// 						blacklistedProperties.some(blacklistedProperty =>
+// 							blacklistedProperty.startsWith(
+// 								query.filter.property
+// 							)
+// 						)
+// 					)
+// 				)
+// 					throw new Error(
+// 						"Unable to filter by blacklisted property."
+// 					);
+// 				if (
+// 					Object.keys(sort).some(property =>
+// 						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 (typeof specialProperties === "object")
-				Object.entries(specialProperties).forEach(
-					([specialProperty, pipelineSteps]) => {
-						// Check if a filter with the special property exists
-						const filterExists =
-							queries
-								.map(query => query.filter.property)
-								.indexOf(specialProperty) !== -1;
+// 			// If a filter or property exists for a special property, add some custom pipeline steps
+// 			if (typeof specialProperties === "object")
+// 				Object.entries(specialProperties).forEach(
+// 					([specialProperty, pipelineSteps]) => {
+// 						// 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;
+// 						// 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;
+// 						// If no such filter or property exists, skip this function
+// 						if (!filterExists && !propertyExists) return;
 
-						// Add the specified pipeline steps into the pipeline
-						pipeline.push(...pipelineSteps);
-					}
-				);
+// 						// Add the specified pipeline steps into the pipeline
+// 						pipeline.push(...pipelineSteps);
+// 					}
+// 				);
 
-			// Adds the match stage to aggregation pipeline, which is responsible for filtering
-			const filterQueries = queries.flatMap(query => {
-				const { data, filter, filterType } = query;
-				const { property } = filter;
+// 			// Adds the match stage to aggregation pipeline, 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] = { $lte: new Date(data) };
-						break;
-					case FilterType.DATETIME_AFTER:
-						newQuery[property] = { $gte: new Date(data) };
-						break;
-					case FilterType.NUMBER_LESSER_EQUAL:
-						newQuery[property] = { $lte: Number(data) };
-						break;
-					case FilterType.NUMBER_LESSER:
-						newQuery[property] = { $lt: Number(data) };
-						break;
-					case FilterType.NUMBER_GREATER:
-						newQuery[property] = { $gt: Number(data) };
-						break;
-					case FilterType.NUMBER_GREATER_EQUAL:
-						newQuery[property] = { $gte: Number(data) };
-						break;
-					case FilterType.NUMBER_EQUAL:
-						newQuery[property] = { $eq: Number(data) };
-						break;
-					case FilterType.BOOLEAN:
-						newQuery[property] = { $eq: !!data };
-						break;
-					case FilterType.SPECIAL:
-						if (
-							typeof specialFilters === "object" &&
-							typeof specialFilters[filter.property] ===
-								"function"
-						) {
-							pipeline.push(
-								...specialFilters[filter.property](data)
-							);
-							newQuery[property] = { $eq: true };
-						}
-						break;
-					default:
-						throw new Error(`Invalid filter type for "${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] = { $lte: new Date(data) };
+// 						break;
+// 					case FilterType.DATETIME_AFTER:
+// 						newQuery[property] = { $gte: new Date(data) };
+// 						break;
+// 					case FilterType.NUMBER_LESSER_EQUAL:
+// 						newQuery[property] = { $lte: Number(data) };
+// 						break;
+// 					case FilterType.NUMBER_LESSER:
+// 						newQuery[property] = { $lt: Number(data) };
+// 						break;
+// 					case FilterType.NUMBER_GREATER:
+// 						newQuery[property] = { $gt: Number(data) };
+// 						break;
+// 					case FilterType.NUMBER_GREATER_EQUAL:
+// 						newQuery[property] = { $gte: Number(data) };
+// 						break;
+// 					case FilterType.NUMBER_EQUAL:
+// 						newQuery[property] = { $eq: Number(data) };
+// 						break;
+// 					case FilterType.BOOLEAN:
+// 						newQuery[property] = { $eq: !!data };
+// 						break;
+// 					case FilterType.SPECIAL:
+// 						if (
+// 							typeof specialFilters === "object" &&
+// 							typeof specialFilters[filter.property] ===
+// 								"function"
+// 						) {
+// 							pipeline.push(
+// 								...specialFilters[filter.property](data)
+// 							);
+// 							newQuery[property] = { $eq: true };
+// 						}
+// 						break;
+// 					default:
+// 						throw new Error(`Invalid filter type for "${filter}"`);
+// 				}
 
-				if (
-					typeof specialQueries === "object" &&
-					typeof specialQueries[filter.property] === "function"
-				) {
-					return specialQueries[filter.property](newQuery);
-				}
+// 				if (
+// 					typeof specialQueries === "object" &&
+// 					typeof specialQueries[filter.property] === "function"
+// 				) {
+// 					return specialQueries[filter.property](newQuery);
+// 				}
 
-				return newQuery;
-			});
+// 				return newQuery;
+// 			});
 
-			const filterQuery: any = {};
+// 			const filterQuery: any = {};
 
-			if (filterQueries.length > 0)
-				filterQuery[`$${operator}`] = filterQueries;
+// 			if (filterQueries.length > 0)
+// 				filterQuery[`$${operator}`] = filterQueries;
 
-			pipeline.push({ $match: filterQuery });
+// 			pipeline.push({ $match: filterQuery });
 
-			// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
-			if (Object.keys(sort).length > 0)
-				pipeline.push({
-					$sort: Object.fromEntries(
-						Object.entries(sort).map(([property, direction]) => [
-							property,
-							direction === "ascending" ? 1 : -1
-						])
-					)
-				});
+// 			// Adds sort stage to aggregation pipeline if there is at least one column being sorted, responsible for sorting data
+// 			if (Object.keys(sort).length > 0)
+// 				pipeline.push({
+// 					$sort: Object.fromEntries(
+// 						Object.entries(sort).map(([property, direction]) => [
+// 							property,
+// 							direction === "ascending" ? 1 : -1
+// 						])
+// 					)
+// 				});
 
-			// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
-			pipeline.push({
-				$project: Object.fromEntries(
-					properties.map(property => [property, 1])
-				)
-			});
+// 			// Adds first project stage to aggregation pipeline, responsible for including only the requested properties
+// 			pipeline.push({
+// 				$project: Object.fromEntries(
+// 					properties.map(property => [property, 1])
+// 				)
+// 			});
 
-			// Adds second project stage to aggregation pipeline, responsible for excluding some specific properties
-			if (
-				Array.isArray(blacklistedProperties) &&
-				blacklistedProperties.length > 0
-			)
-				pipeline.push({
-					$project: Object.fromEntries(
-						blacklistedProperties.map(property => [property, 0])
-					)
-				});
+// 			// Adds second project stage to aggregation pipeline, responsible for excluding some specific properties
+// 			if (
+// 				Array.isArray(blacklistedProperties) &&
+// 				blacklistedProperties.length > 0
+// 			)
+// 				pipeline.push({
+// 					$project: Object.fromEntries(
+// 						blacklistedProperties.map(property => [property, 0])
+// 					)
+// 				});
 
-			// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
-			pipeline.push({
-				$facet: {
-					count: [{ $count: "count" }],
-					documents: [
-						{ $skip: pageSize * (page - 1) },
-						{ $limit: pageSize }
-					]
-				}
-			});
+// 			// Adds the facet stage to aggregation pipeline, responsible for returning a total document count, skipping and limitting the documents that will be returned
+// 			pipeline.push({
+// 				$facet: {
+// 					count: [{ $count: "count" }],
+// 					documents: [
+// 						{ $skip: pageSize * (page - 1) },
+// 						{ $limit: pageSize }
+// 					]
+// 				}
+// 			});
 
-			// Executes the aggregation pipeline
-			const [result] = await this.aggregate(pipeline).exec();
+// 			// Executes the aggregation pipeline
+// 			const [result] = await this.aggregate(pipeline).exec();
 
-			if (result.count.length === 0) return { data: [], count: 0 };
+// 			if (result.count.length === 0) return { data: [], count: 0 };
 
-			properties.forEach(property => {
-				const type = schema.path(property);
-				if (type instanceof SchemaTypes.ObjectId) {
-					const { ref } = type?.options ?? {};
-					if (ref) {
-						result.documents = result.documents.map(
-							(document: any) => ({
-								...document,
-								[property]: {
-									_name: ref,
-									_id: document[property]
-								}
-							})
-						);
-					}
-				} else if (
-					type instanceof SchemaTypes.Array &&
-					type.caster instanceof SchemaTypes.ObjectId
-				) {
-					console.log("Array relation not implemented", property);
-				}
-			});
+// 			properties.forEach(property => {
+// 				const type = schema.path(property);
+// 				if (type instanceof SchemaTypes.ObjectId) {
+// 					const { ref } = type?.options ?? {};
+// 					if (ref) {
+// 						result.documents = result.documents.map(
+// 							(document: any) => ({
+// 								...document,
+// 								[property]: {
+// 									_name: ref,
+// 									_id: document[property]
+// 								}
+// 							})
+// 						);
+// 					}
+// 				} else if (
+// 					type instanceof SchemaTypes.Array &&
+// 					type.caster instanceof SchemaTypes.ObjectId
+// 				) {
+// 					console.log("Array relation not implemented", property);
+// 				}
+// 			});
 
-			result.documents = result.documents.map((document: any) => ({
-				...document,
-				// TODO properly support getModelName in TypeScript
-				// eslint-disable-next-line
-				// @ts-ignore
-				_name: schema.statics.getModelName()
-			}));
+// 			result.documents = result.documents.map((document: any) => ({
+// 				...document,
+// 				// TODO properly support getModelName in TypeScript
+// 				// eslint-disable-next-line
+// 				// @ts-ignore
+// 				_name: schema.statics.getModelName()
+// 			}));
 
-			const { documents: data } = result;
-			const { count } = result.count[0];
+// 			const { documents: data } = result;
+// 			const { count } = result.count[0];
 
-			return { data, count };
-		}
-	);
-}
+// 			return { data, count };
+// 		}
+// 	);
+// }

+ 10 - 10
backend/src/modules/DataModule/types/Schemas.ts

@@ -1,12 +1,12 @@
-import { Types } from "mongoose";
-import { DocumentVersion } from "@/modules/DataModule/plugins/documentVersion";
+// import { Types } from "mongoose";
+// import { DocumentVersion } from "@/modules/DataModule/plugins/documentVersion";
 
-// eslint-disable-next-line
-export interface BaseSchema extends DocumentVersion, TimestampsSchema {
-	_id: Types.ObjectId;
-}
+// // eslint-disable-next-line
+// export interface BaseSchema extends DocumentVersion, TimestampsSchema {
+// 	_id: Types.ObjectId;
+// }
 
-export interface TimestampsSchema {
-	createdAt: number;
-	updatedAt: number;
-}
+// export interface TimestampsSchema {
+// 	createdAt: number;
+// 	updatedAt: number;
+// }

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

@@ -1,11 +1,11 @@
 import "@/tests/support/setup";
 import sinon from "sinon";
-import mongoose from "mongoose";
-import news from "logic/db/schemas/news";
 import NewsCreatedEvent from "@models/News/events/NewsCreatedEvent";
 import NewsUpdatedEvent from "@models/News/events/NewsUpdatedEvent";
 import NewsDeletedEvent from "@models/News/events/NewsDeletedEvent";
 import { NewsStatus } from "@models/News/NewsStatus";
+import { Sequelize } from "sequelize";
+import ObjectID from "bson-objectid";
 import { TestModule } from "@/tests/support/TestModule";
 import Subscribe from "@/modules/EventsModule/jobs/Subscribe";
 import DataModule from "@/modules/DataModule";
@@ -15,6 +15,8 @@ import JobContext from "@/JobContext";
 import { UserRole } from "@/modules/DataModule/models/User/UserRole";
 import GetPermissions from "@/modules/DataModule/models/User/jobs/GetPermissions";
 import CacheModule from "@/modules/CacheModule";
+import News, { schema as NewsSchema } from "@/modules/DataModule/models/News";
+import User from "@/modules/DataModule/models/User";
 
 describe("Subscribe job", async function () {
 	describe("execute", function () {
@@ -57,10 +59,18 @@ describe("Subscribe job", async function () {
 				"execute"
 			);
 
-			modelFindByIdStub;
+			userGetTableName = sinon.stub(User, "getTableName");
+
+			sequelizeQueryStub;
+
+			modelGetTableNameStub;
+
+			newsFindByPkStub;
 
 			Model;
 
+			sequelize;
+
 			restore() {
 				this.jobContextGetSocketIdStub.restore();
 				this.jobContextGetUserStub.restore();
@@ -73,16 +83,29 @@ describe("Subscribe job", async function () {
 				this.eventsModuleGetEventStub.restore();
 				this.eventsModuleSubscribeSocketStub.restore();
 				this.getPermissionsExecute.restore();
+				this.userGetTableName.restore();
+				this.sequelizeQueryStub.restore();
+				this.modelGetTableNameStub.restore();
+				this.newsFindByPkStub.restore();
 			}
 
-			constructor(Model: mongoose.Model<any>, modelFindByIdStub: any) {
+			constructor(
+				sequelize: Sequelize,
+				Model: typeof News,
+				sequelizeQueryStub: any,
+				modelGetTableNameStub: any,
+				newsFindByPkStub: any
+			) {
+				this.sequelize = sequelize;
 				this.Model = Model;
-				this.modelFindByIdStub = modelFindByIdStub;
+				this.sequelizeQueryStub = sequelizeQueryStub;
+				this.modelGetTableNameStub = modelGetTableNameStub;
+				this.newsFindByPkStub = newsFindByPkStub;
 			}
 		}
 		let th: TypeHelper;
 
-		const fakeUserId = new mongoose.Types.ObjectId();
+		const fakeUserId = ObjectID();
 		// const userGuest = undefined;
 		const userNormal = {
 			_id: fakeUserId,
@@ -97,26 +120,46 @@ describe("Subscribe job", async function () {
 			role: UserRole.ADMIN
 		};
 
-		function createDocument(
-			modelId: mongoose.Types.ObjectId,
-			status: NewsStatus
-		) {
-			const news = new th.Model({
-				_id: modelId,
-				status
+		async function createDocument(modelId: ObjectID, status: NewsStatus) {
+			const news = await th.Model.build({
+				_id: modelId.toHexString(),
+				status,
+				title: "Dummy",
+				markdown: "Dummy",
+				createdBy: "Dummy"
 			});
-			// @ts-ignore
-			th.modelFindByIdStub.withArgs(modelId.toString()).returns(news);
+			th.newsFindByPkStub.withArgs(modelId.toHexString()).returns(news);
 		}
 
-		beforeEach(() => {
-			if (mongoose.modelNames().includes("news"))
-				mongoose.deleteModel("news");
-			const schema = new mongoose.Schema(news);
-			const Model = mongoose.model("news", schema);
-			const modelFindByIdStub = sinon.stub(Model, "findById");
+		beforeEach(async () => {
+			const sequelize = new Sequelize("fake", "fake", "fake", {
+				host: "fake",
+				port: 0,
+				dialect: "postgres"
+			});
+
+			const sequelizeQueryStub = sinon.stub(sequelize, "query");
 
-			th = new TypeHelper(Model, modelFindByIdStub);
+			// @ts-ignore
+			const Model = News.init(NewsSchema, {
+				tableName: "News",
+				...News.options,
+				sequelize
+			});
+
+			const modelGetTableNameStub = sinon
+				.stub(News, "getTableName")
+				.returns("news");
+
+			const newsFindByPkStub = sinon.stub(News, "findByPk");
+
+			th = new TypeHelper(
+				sequelize,
+				Model,
+				sequelizeQueryStub,
+				modelGetTableNameStub,
+				newsFindByPkStub
+			);
 
 			th.cacheModuleGetStub.resolves(null);
 			// th.cacheModuleSetStub.resolves(null);
@@ -150,6 +193,7 @@ describe("Subscribe job", async function () {
 				.withArgs("news.deleted")
 				// @ts-ignore
 				.returns(NewsDeletedEvent);
+			th.userGetTableName.returns("users");
 
 			Reflect.set(
 				DataModule,
@@ -247,7 +291,7 @@ describe("Subscribe job", async function () {
 
 		// eslint-disable-next-line no-template-curly-in-string
 		describe("data.news.updated:${modelId}", function () {
-			const modelId = new mongoose.Types.ObjectId();
+			const modelId = ObjectID();
 			const channel = `data.news.updated:${modelId}`;
 			const permission = `event.data.news.updated:${modelId}`;
 
@@ -261,7 +305,7 @@ describe("Subscribe job", async function () {
 					"event.data.news.updated": true
 				});
 
-				createDocument(modelId, NewsStatus.DRAFT);
+				await createDocument(modelId, NewsStatus.DRAFT);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -278,7 +322,7 @@ describe("Subscribe job", async function () {
 				th.jobContextGetUserStub.resolves(userNormal);
 				th.getPermissionsExecute.resolves({});
 
-				createDocument(modelId, NewsStatus.PUBLISHED);
+				await createDocument(modelId, NewsStatus.PUBLISHED);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -295,7 +339,7 @@ describe("Subscribe job", async function () {
 				th.jobContextGetUserStub.resolves(userNormal);
 				th.getPermissionsExecute.resolves({});
 
-				createDocument(modelId, NewsStatus.DRAFT);
+				await createDocument(modelId, NewsStatus.DRAFT);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -311,7 +355,7 @@ describe("Subscribe job", async function () {
 
 		// eslint-disable-next-line no-template-curly-in-string
 		describe("data.news.deleted:${modelId}", function () {
-			const modelId = new mongoose.Types.ObjectId();
+			const modelId = ObjectID();
 			const channel = `data.news.deleted:${modelId}`;
 			const permission = `event.data.news.deleted:${modelId}`;
 
@@ -325,7 +369,7 @@ describe("Subscribe job", async function () {
 					"event.data.news.deleted": true
 				});
 
-				createDocument(modelId, NewsStatus.DRAFT);
+				await createDocument(modelId, NewsStatus.DRAFT);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -342,7 +386,7 @@ describe("Subscribe job", async function () {
 				th.jobContextGetUserStub.resolves(userNormal);
 				th.getPermissionsExecute.resolves({});
 
-				createDocument(modelId, NewsStatus.PUBLISHED);
+				await createDocument(modelId, NewsStatus.PUBLISHED);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -359,7 +403,7 @@ describe("Subscribe job", async function () {
 				th.jobContextGetUserStub.resolves(userNormal);
 				th.getPermissionsExecute.resolves({});
 
-				createDocument(modelId, NewsStatus.DRAFT);
+				await createDocument(modelId, NewsStatus.DRAFT);
 
 				th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -394,7 +438,7 @@ describe("Subscribe job", async function () {
 
 		// eslint-disable-next-line no-template-curly-in-string
 		describe("data.news.unpublished:${modelId}", function () {
-			// const modelId = new mongoose.Types.ObjectId();
+			// const modelId = ObjectID();
 			// const channel = `data.news.unpublished:${modelId}`;
 			// const permission = `event.data.news.unpublished:${modelId}`;
 
@@ -406,7 +450,7 @@ describe("Subscribe job", async function () {
 			// 	// @ts-ignore
 			// 	th.jobContextGetUserStub.resolves(userAdmin);
 
-			// 	createDocument(modelId, NewsStatus.DRAFT);
+			// 	await createDocument(modelId, NewsStatus.DRAFT);
 
 			// 	th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -423,7 +467,7 @@ describe("Subscribe job", async function () {
 			// 	// @ts-ignore
 			// 	th.jobContextGetUserStub.resolves(userNormal);
 
-			// 	createDocument(modelId, NewsStatus.PUBLISHED);
+			// 	await createDocument(modelId, NewsStatus.PUBLISHED);
 
 			// 	th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -440,7 +484,7 @@ describe("Subscribe job", async function () {
 			// 	// @ts-ignore
 			// 	th.jobContextGetUserStub.resolves(userNormal);
 
-			// 	createDocument(modelId, NewsStatus.DRAFT);
+			// 	await createDocument(modelId, NewsStatus.DRAFT);
 
 			// 	th.jobContextGetSocketIdStub.returns("SomeSocketId");
 
@@ -451,6 +495,7 @@ describe("Subscribe job", async function () {
 		});
 
 		afterEach(() => {
+			th.sequelize.close();
 			th.restore();
 		});
 	});

+ 2 - 1
backend/src/types/sequelize.d.ts

@@ -10,5 +10,6 @@ declare module "sequelize/types/associations/base" {
 }
 
 declare module "sequelize/types/data-types" {
-	export const OBJECTID = ObjectIdClass;
+	// eslint-disable-next-line
+	export let OBJECTID = ObjectIdClass;
 }