Просмотр исходного кода

feat: Added collection document validation logic and job average time stats

Owen Diffey 2 лет назад
Родитель
Сommit
6fbea63faa

+ 20 - 5
backend/src/JobQueue.ts

@@ -17,6 +17,7 @@ export default class JobQueue {
 			failed: number;
 			total: number;
 			added: number;
+			averageTime: number;
 		}
 	>;
 
@@ -91,6 +92,7 @@ export default class JobQueue {
 
 		this.queue.splice(this.queue.indexOf(job), 1);
 		this.active.push(job);
+		const startTime = Date.now();
 
 		job.execute()
 			.then(() => {
@@ -101,6 +103,11 @@ export default class JobQueue {
 			})
 			.finally(() => {
 				this.updateStats(job.getName(), "total");
+				this.updateStats(
+					job.getName(),
+					"averageTime",
+					Date.now() - startTime
+				);
 				this.active.splice(this.active.indexOf(job), 1);
 				setTimeout(() => {
 					this.process();
@@ -135,13 +142,15 @@ export default class JobQueue {
 					successful: a.successful + b.successful,
 					failed: a.failed + b.failed,
 					total: a.total + b.total,
-					added: a.added + b.added
+					added: a.added + b.added,
+					averageTime: -1
 				}),
 				{
 					successful: 0,
 					failed: 0,
 					total: 0,
-					added: 0
+					added: 0,
+					averageTime: -1
 				}
 			)
 		};
@@ -182,15 +191,21 @@ export default class JobQueue {
 	 */
 	private updateStats(
 		jobName: string,
-		type: "successful" | "failed" | "total" | "added"
+		type: "successful" | "failed" | "total" | "added" | "averageTime",
+		duration?: number
 	) {
 		if (!this.stats[jobName])
 			this.stats[jobName] = {
 				successful: 0,
 				failed: 0,
 				total: 0,
-				added: 0
+				added: 0,
+				averageTime: 0
 			};
-		this.stats[jobName][type] += 1;
+		if (type === "averageTime" && duration)
+			this.stats[jobName].averageTime +=
+				(duration - this.stats[jobName].averageTime) /
+				this.stats[jobName].total;
+		else this.stats[jobName][type] += 1;
 	}
 }

+ 24 - 47
backend/src/ModuleManager.ts

@@ -112,63 +112,40 @@ export default class ModuleManager {
 	/**
 	 * startup - Handle startup
 	 */
-	public startup(): void {
-		this.loadModules()
-			.then(() => {
-				if (!this.modules) throw new Error("No modules were loaded");
-				async.each(
-					Object.values(this.modules),
-					(module, next) => {
-						module
-							.startup()
-							.then(() => next())
-							.catch(err => {
-								module.setStatus("ERROR");
-								next(err);
-							});
-					},
-					async err => {
-						if (err) {
-							await this.shutdown();
-							throw err;
-						}
-						this.jobQueue.resume();
-					}
-				);
+	public async startup(): Promise<void> {
+		await this.loadModules().catch(async err => {
+			await this.shutdown();
+			throw err;
+		});
+		if (!this.modules) throw new Error("No modules were loaded");
+		await async
+			.each(Object.values(this.modules), async module => {
+				await module.startup().catch(async err => {
+					module.setStatus("ERROR");
+					throw err;
+				});
 			})
 			.catch(async err => {
 				await this.shutdown();
 				throw err;
 			});
+		this.jobQueue.resume();
 	}
 
 	/**
 	 * shutdown - Handle shutdown
 	 */
-	public shutdown(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			// TODO: await jobQueue completion/handle shutdown
-			if (this.modules)
-				async.each(
-					Object.values(this.modules),
-					(module, next) => {
-						if (
-							module.getStatus() === "STARTED" ||
-							module.getStatus() === "STARTING" || // TODO: Handle better
-							module.getStatus() === "ERROR"
-						)
-							module
-								.shutdown()
-								.then(() => next())
-								.catch(next);
-					},
-					err => {
-						if (err) reject(err);
-						else resolve();
-					}
-				);
-			else resolve();
-		});
+	public async shutdown(): Promise<void> {
+		// TODO: await jobQueue completion/handle shutdown
+		if (this.modules)
+			await async.each(Object.values(this.modules), async module => {
+				if (
+					module.getStatus() === "STARTED" ||
+					module.getStatus() === "STARTING" || // TODO: Handle better
+					module.getStatus() === "ERROR"
+				)
+					await module.shutdown();
+			});
 	}
 
 	/**

+ 16 - 2
backend/src/collections/abc.ts

@@ -20,7 +20,11 @@ export const schema: AbcCollection = {
 			type: mongoose.Types.ObjectId,
 			required: true,
 			cacheKey: true,
-			restricted: false
+			restricted: false,
+			validate: async (value: any) => {
+				if (!mongoose.Types.ObjectId.isValid(value))
+					throw new Error("Value is not a valid ObjectId");
+			}
 		},
 		createdAt: {
 			type: Date,
@@ -35,7 +39,17 @@ export const schema: AbcCollection = {
 		name: {
 			type: String,
 			required: true,
-			restricted: false
+			restricted: false,
+			validate: async (value: any) => {
+				if (value.length < 1 || value.length > 64)
+					throw new Error("Name must be 1-64 characters");
+				if (!/^[\p{Letter}0-9 .'_-]+$/gu.test(value))
+					throw new Error("Invalid name provided");
+				if (value.replaceAll(/[ .'_-]/g, "").length === 0)
+					throw new Error(
+						"Name must contain at least 1 letter or number"
+					);
+			}
 		},
 		autofill: {
 			enabled: {

+ 26 - 18
backend/src/modules/DataModule.ts

@@ -108,10 +108,10 @@ export default class DataModule extends BaseModule {
 		return new Promise(resolve => {
 			super
 				.shutdown()
-				.then(() => {
+				.then(async () => {
 					// TODO: Ensure the following shutdown correctly
-					if (this.redis) this.redis.disconnect();
-					mongoose.connection.close(false);
+					if (this.redis) await this.redis.quit();
+					await mongoose.connection.close(false);
 				})
 				.finally(() => resolve());
 		});
@@ -231,14 +231,20 @@ export default class DataModule extends BaseModule {
 								value.$in,
 								async (_value: any) => {
 									if (
-										key === "_id" &&
-										!schema[key].type.isValid(_value)
-									)
-										throw new Error(
-											"Invalid value for _id"
-										);
-									if (typeof schema[key].type === "function")
-										return new schema[key].type(_value);
+										typeof schema[key].type === "function"
+									) {
+										const Type = schema[key].type;
+										const castValue = new Type(_value);
+										if (schema[key].validate)
+											await schema[key]
+												.validate(castValue)
+												.catch(err => {
+													throw new Error(
+														`Invalid value for ${key}, ${err}`
+													);
+												});
+										return castValue;
+									}
 									throw new Error(
 										`Invalid schema type for ${key}`
 									);
@@ -246,13 +252,15 @@ export default class DataModule extends BaseModule {
 							)
 						};
 					else throw new Error(`Invalid value for ${key}`);
-				} else {
-					if (key === "_id" && !schema[key].type.isValid(value))
-						throw new Error("Invalid value for _id");
-					if (typeof schema[key].type === "function")
-						castQuery[key] = new schema[key].type(value);
-					else throw new Error(`Invalid schema type for ${key}`);
-				}
+				} else if (typeof schema[key].type === "function") {
+					const Type = schema[key].type;
+					const castValue = new Type(value);
+					if (schema[key].validate)
+						await schema[key].validate(castValue).catch(err => {
+							throw new Error(`Invalid value for ${key}, ${err}`);
+						});
+					castQuery[key] = castValue;
+				} else throw new Error(`Invalid schema type for ${key}`);
 			} else {
 				throw new Error(
 					`Invalid query provided. Key "${key}" not found`

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

@@ -7,12 +7,14 @@ export type DocumentAttribute<
 		required?: boolean;
 		cacheKey?: boolean;
 		restricted?: boolean;
+		validate?: (value: any) => Promise<void>;
 	}
 > = {
 	type: T["type"];
 	required: T["required"]; // TODO fix default unknown
 	cacheKey?: T["cacheKey"]; // TODO fix default unknown
 	restricted: T["restricted"]; // TODO fix default unknown
+	validate?: T["validate"]; // TODO fix default unknown
 };
 
 export type DefaultSchema = {