Bläddra i källkod

refactor: Replace model static method jobs with a jobConfig, and added basic crud jobs for models

Owen Diffey 1 år sedan
förälder
incheckning
61c6989eb1

+ 41 - 10
backend/src/BaseModule.ts

@@ -31,7 +31,9 @@ export default abstract class BaseModule {
 
 	protected _jobConfig: Record<
 		string,
+		| "disabled"
 		| boolean
+		| ((context: JobContext, payload?: any) => Promise<any>)
 		| {
 				api?: boolean;
 				method?: (context: JobContext, payload?: any) => Promise<any>;
@@ -136,18 +138,47 @@ export default abstract class BaseModule {
 
 		await Promise.all(
 			Object.entries(this._jobConfig).map(async ([name, options]) => {
+				if (options === "disabled") {
+					if (this._jobs[name]) delete this._jobs[name];
+
+					return;
+				}
+
+				if (
+					typeof options === "boolean" ||
+					(typeof options === "object" &&
+						typeof options.method !== "function")
+				)
+					return;
+
+				if (this._jobs[name])
+					throw new Error(`Job "${name}" is already defined`);
+
+				let api = this._jobApiDefault;
+
 				if (
 					typeof options === "object" &&
-					typeof options.method === "function"
-				) {
-					if (this._jobs[name])
-						throw new Error(`Job "${name}" is already defined`);
-
-					this._jobs[name] = {
-						api: options.api ?? this._jobApiDefault,
-						method: options.method
-					};
-				}
+					typeof options.api === "boolean"
+				)
+					api = options.api;
+
+				let method = options;
+
+				if (
+					typeof method === "object" &&
+					typeof method.method === "function"
+				)
+					method = method.method;
+
+				if (typeof method !== "function")
+					throw new Error(
+						`Job "${name}" has no function method defined`
+					);
+
+				this._jobs[name] = {
+					api,
+					method
+				};
 			})
 		);
 	}

+ 163 - 31
backend/src/modules/DataModule.ts

@@ -2,6 +2,7 @@ import config from "config";
 // import { createClient, RedisClientType } from "redis";
 import mongoose, {
 	Connection,
+	isObjectIdOrHexString,
 	MongooseDefaultQueryMiddleware,
 	MongooseDistinctQueryMiddleware,
 	MongooseQueryOrDocumentMiddleware
@@ -12,7 +13,7 @@ import path from "path";
 import JobContext from "../JobContext";
 import BaseModule, { ModuleStatus } from "../BaseModule";
 import { UniqueMethods } from "../types/Modules";
-import { Models } from "../types/Models";
+import { AnyModel, Models } from "../types/Models";
 import { Schemas } from "../types/Schemas";
 import documentVersionPlugin from "../schemas/plugins/documentVersion";
 import getDataPlugin from "../schemas/plugins/getData";
@@ -185,6 +186,13 @@ export default class DataModule extends BaseModule {
 	 * createMongoConnection - Create mongo connection
 	 */
 	private async _createMongoConnection() {
+		mongoose.set({
+			runValidators: true,
+			sanitizeFilter: true,
+			strict: "throw",
+			strictQuery: "throw"
+		});
+
 		const { user, password, host, port, database } = config.get<{
 			user: string;
 			password: string;
@@ -197,11 +205,6 @@ export default class DataModule extends BaseModule {
 		this._mongoConnection = await mongoose
 			.createConnection(mongoUrl)
 			.asPromise();
-
-		this._mongoConnection.set("runValidators", true);
-		this._mongoConnection.set("sanitizeFilter", true);
-		this._mongoConnection.set("strict", "throw");
-		this._mongoConnection.set("strictQuery", "throw");
 	}
 
 	/**
@@ -442,45 +445,174 @@ export default class DataModule extends BaseModule {
 		await Promise.all(
 			Object.entries(this._models).map(async ([modelName, model]) => {
 				await Promise.all(
-					["findById"].map(async method => {
-						this._jobConfig[`${modelName}.${method}`] = {
-							method: async (context, payload) =>
-								Object.getPrototypeOf(this)[`_${method}`](
-									context,
-									{
-										...payload,
-										model: modelName
-									}
-								)
-						};
-					})
+					["create", "findById", "updateById", "deleteById"].map(
+						async method => {
+							this._jobConfig[`${modelName}.${method}`] = {
+								method: async (context, payload) =>
+									Object.getPrototypeOf(this)[`_${method}`](
+										context,
+										{
+											...payload,
+											modelName,
+											model
+										}
+									)
+							};
+						}
+					)
 				);
 
-				await Promise.all(
-					Object.keys(model.schema.statics).map(async name => {
-						this._jobConfig[`${modelName}.${name}`] = {
-							method: async (...args) => model[name](...args)
-						};
-					})
-				);
+				const jobConfig = model.schema.get("jobConfig");
+				if (
+					typeof jobConfig === "object" &&
+					Object.keys(jobConfig).length > 0
+				)
+					await Promise.all(
+						Object.entries(jobConfig).map(
+							async ([name, options]) => {
+								if (options === "disabled") {
+									if (this._jobConfig[`${modelName}.${name}`])
+										delete this._jobConfig[
+											`${modelName}.${name}`
+										];
+
+									return;
+								}
+
+								let api = this._jobApiDefault;
+
+								let method;
+
+								const configOptions =
+									this._jobConfig[`${modelName}.${name}`];
+								if (typeof configOptions === "object") {
+									if (typeof configOptions.api === "boolean")
+										api = configOptions.api;
+									if (
+										typeof configOptions.method ===
+										"function"
+									)
+										method = configOptions.method;
+								} else if (typeof configOptions === "function")
+									method = configOptions;
+								else if (typeof configOptions === "boolean")
+									api = configOptions;
+
+								if (
+									typeof options === "object" &&
+									typeof options.api === "boolean"
+								)
+									api = options.api;
+								else if (typeof options === "boolean")
+									api = options;
+
+								if (
+									typeof options === "object" &&
+									typeof options.method === "function"
+								)
+									method = async (...args) =>
+										options.method.apply(model, args);
+								else if (typeof options === "function")
+									method = async (...args) =>
+										options.apply(model, args);
+
+								if (typeof method !== "function")
+									throw new Error(
+										`Job "${name}" has no function method defined`
+									);
+
+								this._jobConfig[`${modelName}.${name}`] = {
+									api,
+									method
+								};
+							}
+						)
+					);
 			})
 		);
 	}
 
 	private async _findById(
 		context: JobContext,
-		payload: { model: keyof Models; _id: Types.ObjectId }
+		payload: {
+			modelName: keyof Models;
+			model: AnyModel;
+			_id: Types.ObjectId;
+		}
 	) {
-		// await context.assertPermission(
-		// 	`data.${payload.model}.findById.${payload._id}`
-		// );
+		const { modelName, model, _id } = payload ?? {};
 
-		const model = await context.getModel(payload.model);
+		await context.assertPermission(`data.${modelName}.findById.${_id}`);
 
-		const query = model.findById(payload._id);
+		const query = model.findById(_id);
 
 		return query.exec();
 	}
+
+	private async _create(
+		context: JobContext,
+		payload: {
+			modelName: keyof Models;
+			model: AnyModel;
+			query: Record<string, any[]>;
+		}
+	) {
+		const { modelName, model, query } = payload ?? {};
+
+		await context.assertPermission(`data.${modelName}.create`);
+
+		if (typeof query !== "object")
+			throw new Error("Query is not an object");
+		if (Object.keys(query).length === 0)
+			throw new Error("Empty query object provided");
+
+		if (model.schema.path("createdBy"))
+			query.createdBy = (await context.getUser())._id;
+
+		return model.create(query);
+	}
+
+	private async _updateById(
+		context: JobContext,
+		payload: {
+			modelName: keyof Models;
+			model: AnyModel;
+			_id: Types.ObjectId;
+			query: Record<string, any[]>;
+		}
+	) {
+		const { modelName, model, _id, query } = payload ?? {};
+
+		await context.assertPermission(`data.${modelName}.updateById.${_id}`);
+
+		if (!isObjectIdOrHexString(_id))
+			throw new Error("_id is not an ObjectId");
+
+		if (typeof query !== "object")
+			throw new Error("Query is not an object");
+		if (Object.keys(query).length === 0)
+			throw new Error("Empty query object provided");
+
+		return model.updateOne({ _id }, { $set: query });
+	}
+
+	private async _deleteById(
+		context: JobContext,
+		payload: {
+			modelName: keyof Models;
+			model: AnyModel;
+			_id: Types.ObjectId;
+		}
+	) {
+		const { modelName, model, _id } = payload ?? {};
+
+		await context.assertPermission(`data.${modelName}.deleteById.${_id}`);
+
+		if (!isObjectIdOrHexString(_id))
+			throw new Error("_id is not an ObjectId");
+
+		return model.deleteOne({ _id });
+	}
 }
 
 export type DataModuleJobs = {

+ 6 - 1
backend/src/permissions.ts

@@ -1,9 +1,14 @@
 import config from "config";
 import { UserRole } from "./schemas/user";
 
-const user = {};
+const temp = {
+	"data.news.getData": true,
+};
+
+const user = { ...temp };
 
 const dj = {
+	...user,
 	"stations.autofill": true,
 	"stations.blacklist": true,
 	"stations.index": true,

+ 7 - 4
backend/src/schemas/news.ts

@@ -100,12 +100,15 @@ export const schema = new Schema<NewsSchema, NewsModel, {}, NewsQueryHelpers>(
 				return query;
 			}
 		},
-		statics: {
-			published() {
+		jobConfig: {
+			async published() {
 				return this.find().published();
 			},
-			newest(context: JobContext, payload: { showToNewUsers?: boolean }) {
-				return this.find().newest(payload.showToNewUsers);
+			async newest(
+				context: JobContext,
+				payload: { showToNewUsers?: boolean }
+			) {
+				return this.find().newest(payload?.showToNewUsers);
 			}
 		},
 		// @ts-ignore need to somehow use GetDataSchemaOptions

+ 28 - 18
backend/src/schemas/plugins/getData.ts

@@ -29,23 +29,20 @@ export interface GetDataSchemaOptions extends SchemaOptions {
 }
 
 export interface GetData {
-	getData(
-		context: JobContext,
-		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<{
+	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;
 	}>;
@@ -55,7 +52,6 @@ export default function getDataPlugin(schema: Schema) {
 	schema.static(
 		"getData",
 		async function getData(
-			context: JobContext,
 			payload: Parameters<GetData["getData"]>[0]
 		): ReturnType<GetData["getData"]> {
 			const { page, pageSize, properties, sort, queries, operator } =
@@ -246,4 +242,18 @@ export default function getDataPlugin(schema: Schema) {
 			return { data, count };
 		}
 	);
+
+	schema.set("jobConfig", {
+		async getData(
+			context: JobContext,
+			payload: Parameters<GetData["getData"]>[0]
+		) {
+			await context.assertPermission(
+				`data.${this.collection.collectionName}.getData`
+			);
+
+			return this.getData(payload);
+		},
+		...(schema.get("jobConfig") ?? {})
+	});
 }

+ 2 - 0
backend/src/types/Models.ts

@@ -11,3 +11,5 @@ export type Models = {
 	station: StationModel;
 	user: UserModel;
 };
+
+export type AnyModel = Models[keyof Models];