Browse Source

refactor: fix data events, change how association data is returned

Kristian Vos 2 months ago
parent
commit
5e7297823d

+ 9 - 3
backend/src/Job.ts

@@ -233,6 +233,9 @@ export default abstract class Job {
 
 	protected abstract _execute(): Promise<unknown>;
 
+	protected _transformResponse: null | ((response: unknown) => unknown) =
+		null;
+
 	/**
 	 * execute - Execute job
 	 *
@@ -259,7 +262,10 @@ export default abstract class Job {
 
 			await this._authorize();
 
-			const data = await this._execute();
+			let response = await this._execute();
+
+			if (this._transformResponse)
+				response = this._transformResponse(response);
 
 			const socketId = this._context.getSocketId();
 			const callbackRef = this._context.getCallbackRef();
@@ -271,7 +277,7 @@ export default abstract class Job {
 							socketId,
 							callbackRef,
 							status: "success",
-							data
+							data: response
 						},
 						this.getUuid()
 					)
@@ -288,7 +294,7 @@ export default abstract class Job {
 				JobStatisticsType.SUCCESSFUL
 			);
 
-			return data;
+			return response;
 		} catch (error: unknown) {
 			const message = getErrorMessage(error);
 

+ 80 - 79
backend/src/modules/DataModule.ts

@@ -18,6 +18,7 @@ import BaseModule, { ModuleStatus } from "@/BaseModule";
 import DataModuleJob from "./DataModule/DataModuleJob";
 import Job from "@/Job";
 import EventsModule from "./EventsModule";
+import { EventClass } from "./EventsModule/Event";
 
 export type ObjectIdType = string;
 
@@ -134,16 +135,92 @@ export class DataModule extends BaseModule {
 		return sequelize;
 	}
 
+	private _setupSequelizeHooks() {
+		const getEventFromModel = (
+			model: SequelizeModel<any, any>,
+			suffix: string
+		): EventClass | null => {
+			const modelName = (
+				model.constructor as ModelStatic<any>
+			).getTableName();
+			let EventClass;
+
+			try {
+				EventClass = this.getEvent(`${modelName}.${suffix}`);
+			} catch (error) {
+				// TODO: Catch and ignore only event not found
+				return null;
+			}
+
+			return EventClass;
+		};
+
+		this._sequelize!.addHook("afterCreate", async model => {
+			const EventClass = getEventFromModel(model, "created");
+			if (!EventClass) return;
+
+			await EventsModule.publish(
+				new EventClass({
+					doc: model
+				})
+			);
+		});
+
+		this._sequelize!.addHook("afterUpdate", async model => {
+			const EventClass = getEventFromModel(model, "updated");
+			if (!EventClass) return;
+
+			await EventsModule.publish(
+				new EventClass(
+					{
+						doc: model,
+						oldDoc: {
+							_id: model.get("_id")
+						}
+					},
+					model.get("_id")!.toString()
+				)
+			);
+		});
+
+		this._sequelize!.addHook("afterDestroy", async model => {
+			const EventClass = getEventFromModel(model, "deleted");
+			if (!EventClass) return;
+
+			await EventsModule.publish(
+				new EventClass(
+					{
+						oldDoc: {
+							_id: model.get("_id")
+						}
+					},
+					model.get("_id")!.toString()
+				)
+			);
+		});
+
+		// Make sure every update/destroy has individualhooks
+
+		this._sequelize!.addHook("beforeValidate", async model => {
+			// TODO review
+			if (model.isNewRecord) {
+				const key = (model.constructor as ModelStatic<any>)
+					.primaryKeyAttribute;
+				model.dataValues[key] ??= ObjectID();
+			}
+		});
+	}
+
 	/**
 	 * setupSequelize - Setup sequelize instance
 	 */
 	private async _setupSequelize() {
-		this._sequelize = await this._createSequelizeInstance({
-			hooks: this._getSequelizeHooks()
-		});
+		this._sequelize = await this._createSequelizeInstance({});
 
 		await this._sequelize.authenticate();
 
+		this._setupSequelizeHooks();
+
 		const setupFunctions: (() => Promise<void>)[] = [];
 
 		await forEachIn(
@@ -205,82 +282,6 @@ export class DataModule extends BaseModule {
 		return this._sequelize.model(camelizedName) as ModelStatic<ModelType>; // This fails - news has not been defined
 	}
 
-	private _getSequelizeHooks(): Options["hooks"] {
-		return {
-			afterCreate: async model => {
-				const modelName = (
-					model.constructor as ModelStatic<any>
-				).getTableName();
-				let EventClass;
-
-				try {
-					EventClass = this.getEvent(`${modelName}.created`);
-				} catch (error) {
-					// TODO: Catch and ignore only event not found
-					return;
-				}
-
-				await EventsModule.publish(
-					new EventClass({
-						doc: model.get()
-					})
-				);
-			},
-			afterUpdate: async model => {
-				const modelName = (
-					model.constructor as ModelStatic<any>
-				).getTableName();
-				let EventClass;
-
-				try {
-					EventClass = this.getEvent(`${modelName}.updated`);
-				} catch (error) {
-					// TODO: Catch and ignore only event not found
-					return;
-				}
-
-				await EventsModule.publish(
-					new EventClass(
-						{
-							doc: model.get(),
-							oldDoc: model.previous()
-						},
-						model.get("_id") ?? model.previous("_id")
-					)
-				);
-			},
-			afterDestroy: async model => {
-				const modelName = (
-					model.constructor as ModelStatic<any>
-				).getTableName();
-				let EventClass;
-
-				try {
-					EventClass = this.getEvent(`${modelName}.deleted`);
-				} catch (error) {
-					// TODO: Catch and ignore only event not found
-					return;
-				}
-
-				await EventsModule.publish(
-					new EventClass(
-						{
-							oldDoc: model.previous()
-						},
-						model.previous("_id")
-					)
-				);
-			},
-			beforeValidate: async model => {
-				if (model.isNewRecord) {
-					const key = (model.constructor as ModelStatic<any>)
-						.primaryKeyAttribute;
-					model.dataValues[key] ??= ObjectID();
-				}
-			}
-		};
-	}
-
 	private async _loadModelJobs(modelClassName: string) {
 		let jobs: Dirent[];
 

+ 4 - 0
backend/src/modules/DataModule/DataModuleJob.ts

@@ -2,6 +2,7 @@ import { isValidObjectId } from "mongoose";
 import { Model, ModelStatic } from "sequelize";
 import Job, { JobOptions } from "@/Job";
 import DataModule from "../DataModule";
+import transformModels from "@/utils/transformModels";
 
 export default abstract class DataModuleJob extends Job {
 	protected static _model: ModelStatic<any>;
@@ -91,4 +92,7 @@ export default abstract class DataModuleJob extends Job {
 
 		await this._context.assertPermission(this.getPath());
 	}
+
+	protected _transformResponse = (response: unknown) =>
+		transformModels(response);
 }

+ 2 - 1
backend/src/modules/DataModule/DeleteByIdJob.ts

@@ -12,7 +12,8 @@ export default abstract class DeleteByIdJob extends DataModuleJob {
 		const { _id } = this._payload;
 
 		return this.getModel().destroy({
-			where: { _id }
+			where: { _id },
+			individualHooks: true
 		});
 	}
 }

+ 4 - 1
backend/src/modules/DataModule/DeleteManyByIdJob.ts

@@ -19,7 +19,10 @@ export default abstract class DeleteManyByIdJob extends DataModuleJob {
 		const { _ids } = this._payload;
 
 		return this.getModel().destroy({
-			where: { _id: _ids }
+			where: {
+				_id: _ids,
+				individualHooks: true
+			}
 		});
 	}
 }

+ 1 - 3
backend/src/modules/DataModule/GetDataJob.ts

@@ -280,10 +280,8 @@ export default abstract class GetDataJob extends DataModuleJob {
 			findQuery
 		);
 
-		const data = rows.map(model => model.toJSON()); // TODO: Review generally
-
 		// TODO make absolutely sure createdByModel and similar here have been removed or aren't included, if they've been included at all
 
-		return { data, count };
+		return { data: rows, count };
 	}
 }

+ 2 - 1
backend/src/modules/DataModule/UpdateByIdJob.ts

@@ -13,7 +13,8 @@ export default abstract class UpdateByIdJob extends DataModuleJob {
 		const { _id, query } = this._payload;
 
 		return this.getModel().update(query, {
-			where: { _id }
+			where: { _id },
+			individualHooks: true
 		});
 	}
 }

+ 27 - 44
backend/src/modules/DataModule/models/News.ts

@@ -87,7 +87,16 @@ export const schema = {
 	_name: {
 		type: DataTypes.VIRTUAL,
 		get() {
-			return `news`;
+			return "news";
+		}
+	},
+
+	_associations: {
+		type: DataTypes.VIRTUAL,
+		get() {
+			return {
+				createdBy: "minifiedUsers"
+			};
 		}
 	}
 };
@@ -107,78 +116,52 @@ export const setup = async () => {
 	});
 
 	News.afterSave(async record => {
-		const oldDoc = record.previous();
 		const doc = record.get();
 
-		if (oldDoc.status === doc.status) return;
+		const oldStatus = record.previous("status");
+		const newStatus = record.get("status");
 
-		if (doc.status === NewsStatus.PUBLISHED) {
+		if (oldStatus === newStatus) return;
+
+		if (newStatus === NewsStatus.PUBLISHED) {
 			const EventClass = EventsModule.getEvent(`data.news.published`);
 			await EventsModule.publish(
 				new EventClass({
-					doc
+					doc: record
 				})
 			);
-		} else if (oldDoc.status === NewsStatus.PUBLISHED) {
+		} else if (oldStatus === NewsStatus.PUBLISHED) {
 			const EventClass = EventsModule.getEvent(`data.news.unpublished`);
 			await EventsModule.publish(
 				new EventClass(
 					{
-						oldDoc
+						oldDoc: {
+							_id: doc._id.toString()
+						}
 					},
-					oldDoc._id!.toString()
+					doc._id.toString()
 				)
 			);
 		}
 	});
 
 	News.afterDestroy(async record => {
-		const oldDoc = record.previous();
+		const doc = record.get();
 
-		if (oldDoc.status === NewsStatus.PUBLISHED) {
+		if (doc.status === NewsStatus.PUBLISHED) {
 			const EventClass = EventsModule.getEvent(`data.news.unpublished`);
 			await EventsModule.publish(
 				new EventClass(
 					{
-						oldDoc
+						oldDoc: {
+							_id: doc._id.toString()
+						}
 					},
-					oldDoc._id!.toString()
+					doc._id.toString()
 				)
 			);
 		}
 	});
-
-	News.addHook("afterFind", _news => {
-		if (!_news) return;
-
-		// TODO improve TS
-		let news: Model<
-			InferAttributes<
-				News,
-				{
-					omit: never;
-				}
-			>,
-			InferCreationAttributes<
-				News,
-				{
-					omit: never;
-				}
-			>
-		>[] = [];
-
-		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.toString(),
-				_name: "minifiedUsers"
-			};
-		});
-	});
 };
 
 export default News;

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

@@ -150,6 +150,14 @@ export const schema = {
 			return `stations`;
 		}
 	},
+	_associations: {
+		type: DataTypes.VIRTUAL,
+		get() {
+			return {
+				owner: "minifiedUsers"
+			};
+		}
+	},
 	// Temporary
 	djs: {
 		type: DataTypes.VIRTUAL,
@@ -163,12 +171,7 @@ export const options = {};
 
 export const setup = async () => {
 	// Station.afterSave(async record => {});
-
 	// Station.afterDestroy(async record => {});
-
-	Station.addHook("afterFind", (station, options) => {
-		console.log("AFTER FIND STATION", station, options);
-	});
 };
 
 export default Station;

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

@@ -317,10 +317,6 @@ export const setup = async () => {
 	// User.afterSave(async record => {});
 
 	// User.afterDestroy(async record => {});
-
-	User.addHook("afterFind", (user, options) => {
-		console.log("AFTER FIND USER", user, options);
-	});
 };
 
 export default User;

+ 2 - 1
backend/src/modules/EventsModule/Event.ts

@@ -1,3 +1,4 @@
+import transformModels from "@/utils/transformModels";
 import User from "../DataModule/models/User";
 
 export default abstract class Event {
@@ -12,7 +13,7 @@ export default abstract class Event {
 	protected _scope?: string;
 
 	public constructor(data: any, scope?: string) {
-		this._data = data;
+		this._data = transformModels(data);
 		this._scope = scope;
 	}
 

+ 37 - 0
backend/src/utils/transformModels.ts

@@ -0,0 +1,37 @@
+/* eslint no-use-before-define: 0 */
+
+import { Model } from "sequelize";
+
+const handleModel = (model: Model) => {
+	const result = model.toJSON();
+
+	Object.entries(result._associations ?? {}).forEach(([property, name]) => {
+		result[property] = {
+			_id: result[property],
+			_name: name
+		};
+	});
+
+	delete result._associations;
+
+	return result;
+};
+
+const handleItem = (item: any) => {
+	if (!item) return item;
+	if (Array.isArray(item)) return handleArray(item);
+	if (item instanceof Model) return handleModel(item);
+	if (typeof item === "object" && item.constructor.name === "Object")
+		return handleObject(item);
+	return item;
+};
+
+const handleArray = (array: any[]): any[] => array.map(handleItem);
+
+const handleObject = (object: object): object =>
+	Object.fromEntries(
+		Object.entries(object).map(([key, value]) => [key, handleItem(value)])
+	);
+
+// Replaces association properties to include info about the association
+export default (response: unknown) => handleItem(response);