Pārlūkot izejas kodu

feat: worked on APIModule<->DataModule middleware and jobs (WIP)

Kristian Vos 1 gadu atpakaļ
vecāks
revīzija
12d2fb5e12

+ 41 - 0
backend/src/modules/APIModule.ts

@@ -88,6 +88,47 @@ export default class APIModule extends BaseModule {
 		});
 	}
 
+	/**
+	 * Experimental: for APIModule<->DataModule
+	 */
+	public async runDataJob(
+		context: JobContext,
+		{
+			jobName,
+			payload,
+			sessionId,
+			socketId
+		}: {
+			jobName: string;
+			payload: any;
+			sessionId?: string;
+			socketId?: string;
+		}
+	) {
+		let session;
+		if (sessionId) {
+			const Session = await context.getModel("session");
+
+			session = await Session.findByIdAndUpdate(sessionId, {
+				updatedAt: Date.now()
+			});
+		}
+
+		const Model = await context.getModel(payload.model);
+
+		if (jobName === "find") {
+			const response = await Model.find(payload.query).setOptions({
+				userContext: {
+					session
+				}
+			});
+
+			return response;
+		}
+
+		return null;
+	}
+
 	/**
 	 * getCookieValueFromHeader - Get value of a cookie from cookie header string
 	 */

+ 176 - 30
backend/src/modules/DataModule.ts

@@ -18,6 +18,94 @@ import documentVersionPlugin from "../schemas/plugins/documentVersion";
 import getDataPlugin from "../schemas/plugins/getData";
 import Migration from "../Migration";
 
+/**
+ * Experimental: function to get all nested keys from a MongoDB query object
+ */
+function getAllKeys(obj: object) {
+	const keys: string[] = [];
+
+	function processObject(obj: object, parentKey = "") {
+		let returnChanged = false;
+
+		// eslint-disable-next-line
+		for (let key in obj) {
+			// eslint-disable-next-line
+			if (obj.hasOwnProperty(key)) {
+				if (key.startsWith("$")) {
+					// eslint-disable-next-line
+					// @ts-ignore
+					// eslint-disable-next-line
+					processNestedObject(obj[key], parentKey); // Process nested keys without including the current key
+					// eslint-disable-next-line
+					continue; // Skip the current key
+				}
+
+				const currentKey = parentKey ? `${parentKey}.${key}` : key;
+
+				// eslint-disable-next-line
+				// @ts-ignore
+				if (typeof obj[key] === "object" && obj[key] !== null) {
+					// eslint-disable-next-line
+					// @ts-ignore
+					if (Array.isArray(obj[key])) {
+						// eslint-disable-next-line
+						// @ts-ignore
+						// eslint-disable-next-line
+						if (processArray(obj[key], currentKey)) {
+							returnChanged = true;
+							// eslint-disable-next-line
+							continue;
+						}
+					}
+					// eslint-disable-next-line
+					// @ts-ignore
+					else if (processObject(obj[key], currentKey)) {
+						returnChanged = true;
+						// eslint-disable-next-line
+						continue;
+					}
+				}
+
+				keys.push(currentKey);
+
+				returnChanged = true;
+			}
+		}
+
+		return returnChanged;
+	}
+
+	function processArray(arr: Array<any>, parentKey: string) {
+		let returnChanged = false;
+
+		for (let i = 0; i < arr.length; i += 1) {
+			const currentKey = parentKey;
+
+			if (typeof arr[i] === "object" && arr[i] !== null) {
+				if (Array.isArray(arr[i])) {
+					if (processArray(arr[i], currentKey)) returnChanged = true;
+				} else if (processObject(arr[i], currentKey))
+					returnChanged = true;
+			}
+		}
+
+		return returnChanged;
+	}
+
+	function processNestedObject(obj: object, parentKey: string) {
+		if (typeof obj === "object" && obj !== null) {
+			if (Array.isArray(obj)) {
+				processArray(obj, parentKey);
+			} else {
+				processObject(obj, parentKey);
+			}
+		}
+	}
+
+	processObject(obj);
+	return keys;
+}
+
 export default class DataModule extends BaseModule {
 	private models?: Models;
 
@@ -117,36 +205,94 @@ export default class DataModule extends BaseModule {
 		ModelName extends keyof Models,
 		SchemaType extends Schemas[keyof ModelName]
 	>(modelName: ModelName, schema: SchemaType) {
-		// const preMethods: string[] = [
-		// 	"aggregate",
-		// 	"count",
-		// 	"countDocuments",
-		// 	"deleteOne",
-		// 	"deleteMany",
-		// 	"estimatedDocumentCount",
-		// 	"find",
-		// 	"findOne",
-		// 	"findOneAndDelete",
-		// 	"findOneAndRemove",
-		// 	"findOneAndReplace",
-		// 	"findOneAndUpdate",
-		// 	"init",
-		// 	"insertMany",
-		// 	"remove",
-		// 	"replaceOne",
-		// 	"save",
-		// 	"update",
-		// 	"updateOne",
-		// 	"updateMany",
-		// 	"validate"
-		// ];
-
-		// preMethods.forEach(preMethod => {
-		// 	// @ts-ignore
-		// 	schema.pre(preMethods, () => {
-		// 		console.log(`Pre-${preMethod}!`);
-		// 	});
-		// });
+		const methods: string[] = [
+			"aggregate",
+			"count",
+			"countDocuments",
+			"deleteOne",
+			"deleteMany",
+			"estimatedDocumentCount",
+			"find",
+			"findOne",
+			"findOneAndDelete",
+			"findOneAndRemove",
+			"findOneAndReplace",
+			"findOneAndUpdate",
+			// "init",
+			"insertMany",
+			"remove",
+			"replaceOne",
+			"save",
+			"update",
+			"updateOne",
+			"updateMany"
+			// "validate"
+		];
+
+		methods.forEach(method => {
+			// NOTE: some Mongo selectors may also search through linked documents. Prevent that
+			schema.pre(method, async function () {
+				console.log(`Pre-${method}! START`);
+
+				if (
+					this.options?.userContext &&
+					["find", "update", "deleteOne", "save"].indexOf(method) ===
+						-1
+				)
+					throw new Error("Method not allowed");
+
+				console.log(`Pre-${method}!`, this.options?.userContext);
+
+				if (["find", "update", "deleteOne"].indexOf(method) !== -1) {
+					const filter = this.getFilter();
+					const filterKeys = getAllKeys(filter);
+
+					filterKeys.forEach(filterKey => {
+						const splitFilterKeys = filterKey
+							.split(".")
+							.reduce(
+								(keys: string[], key: string) =>
+									keys.length > 0
+										? [
+												...keys,
+												`${
+													keys[keys.length - 1]
+												}.${key}`
+										  ]
+										: [key],
+								[]
+							);
+						splitFilterKeys.forEach(splitFilterKey => {
+							const path = this.schema.path(splitFilterKey);
+							if (!path) {
+								throw new Error(
+									"Attempted to query with non-existant property"
+								);
+							}
+							if (path.options.restricted) {
+								throw new Error(
+									"Attempted to query with restricted property"
+								);
+							}
+						});
+					});
+
+					console.log(`Pre-${method}!`, filterKeys);
+
+					// Here we want to always exclude some properties depending on the model, like passwords/tokens
+					this.projection({ restrictedName: 0 });
+				}
+
+				console.log(`Pre-${method}! END`);
+			});
+
+			schema.post(method, async function (docOrDocs) {
+				console.log(`Post-${method} START!`);
+				console.log(`Post-${method}!`, docOrDocs);
+				console.log(`Post-${method}!`, this);
+				console.log(`Post-${method} END!`);
+			});
+		});
 
 		const { enabled, eventCreated, eventUpdated, eventDeleted } =
 			schema.get("patchHistory") ?? {};

+ 25 - 11
backend/src/modules/WebSocketModule.ts

@@ -147,18 +147,32 @@ export default class WebSocketModule extends BaseModule {
 					`No callback reference provided for job ${moduleJob}`
 				);
 
-			const res = await this.jobQueue.runJob("api", "runJob", {
-				moduleName,
-				jobName,
-				payload,
-				sessionId: socket.getSessionId(),
-				socketId: socket.getSocketId()
-			});
+			if (moduleName === "data") {
+				const res = await this.jobQueue.runJob("api", "runDataJob", {
+					jobName,
+					payload,
+					sessionId: socket.getSessionId(),
+					socketId: socket.getSocketId()
+				});
 
-			socket.dispatch("CB_REF", callbackRef, {
-				status: "success",
-				data: res
-			});
+				socket.dispatch("CB_REF", callbackRef, {
+					status: "success",
+					data: res
+				});
+			} else {
+				const res = await this.jobQueue.runJob("api", "runJob", {
+					moduleName,
+					jobName,
+					payload,
+					sessionId: socket.getSessionId(),
+					socketId: socket.getSocketId()
+				});
+
+				socket.dispatch("CB_REF", callbackRef, {
+					status: "success",
+					data: res
+				});
+			}
 		} catch (error) {
 			const message = error?.message ?? error;
 

+ 8 - 4
backend/src/schemas/abc.ts

@@ -21,10 +21,14 @@ export const schema = new Schema<AbcSchema, AbcModel>(
 			required: true
 		},
 		autofill: {
-			enabled: {
-				type: SchemaTypes.Boolean,
-				required: false
-			}
+			type: {
+				_id: false,
+				enabled: {
+					type: SchemaTypes.Boolean,
+					required: false
+				}
+			},
+			restricted: true
 		},
 		someNumbers: [{ type: SchemaTypes.Number }],
 		songs: [