1
0
Эх сурвалжийг харах

refactor: Various fixes and tweaks

Owen Diffey 2 жил өмнө
parent
commit
4cfa9e7075

+ 6 - 0
backend/.eslintrc

@@ -55,6 +55,12 @@
 		]
 		]
 	},
 	},
 	"overrides": [
 	"overrides": [
+		{
+			"files": ["src/types/*.ts"],
+			"rules": {
+				"import/prefer-default-export": "off"
+			}
+		},
 		{
 		{
 			"files": ["**/*.test.ts", "**/*.spec.ts"],
 			"files": ["**/*.test.ts", "**/*.spec.ts"],
 			"rules": {
 			"rules": {

+ 0 - 14
backend/package-lock.json

@@ -22,7 +22,6 @@
 				"mongodb": "4.10.0",
 				"mongodb": "4.10.0",
 				"nodemailer": "^6.8.0",
 				"nodemailer": "^6.8.0",
 				"oauth": "^0.10.0",
 				"oauth": "^0.10.0",
-				"object-hash": "^3.0.0",
 				"redis": "^4.3.1",
 				"redis": "^4.3.1",
 				"retry-axios": "^3.0.0",
 				"retry-axios": "^3.0.0",
 				"sha256": "^0.2.0",
 				"sha256": "^0.2.0",
@@ -3545,14 +3544,6 @@
 				"node": ">=0.10.0"
 				"node": ">=0.10.0"
 			}
 			}
 		},
 		},
-		"node_modules/object-hash": {
-			"version": "3.0.0",
-			"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
-			"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
-			"engines": {
-				"node": ">= 6"
-			}
-		},
 		"node_modules/object-inspect": {
 		"node_modules/object-inspect": {
 			"version": "1.12.2",
 			"version": "1.12.2",
 			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
 			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -7635,11 +7626,6 @@
 			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 			"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 			"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
 			"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
 		},
 		},
-		"object-hash": {
-			"version": "3.0.0",
-			"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
-			"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="
-		},
 		"object-inspect": {
 		"object-inspect": {
 			"version": "1.12.2",
 			"version": "1.12.2",
 			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
 			"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",

+ 0 - 1
backend/package.json

@@ -30,7 +30,6 @@
 		"mongodb": "4.10.0",
 		"mongodb": "4.10.0",
 		"nodemailer": "^6.8.0",
 		"nodemailer": "^6.8.0",
 		"oauth": "^0.10.0",
 		"oauth": "^0.10.0",
-		"object-hash": "^3.0.0",
 		"redis": "^4.3.1",
 		"redis": "^4.3.1",
 		"retry-axios": "^3.0.0",
 		"retry-axios": "^3.0.0",
 		"sha256": "^0.2.0",
 		"sha256": "^0.2.0",

+ 25 - 21
backend/src/BaseModule.ts

@@ -1,3 +1,4 @@
+import { Log } from "./LogBook";
 import ModuleManager from "./ModuleManager";
 import ModuleManager from "./ModuleManager";
 import { ModuleStatus } from "./types/Modules";
 import { ModuleStatus } from "./types/Modules";
 
 
@@ -28,7 +29,7 @@ export default abstract class BaseModule {
 	 *
 	 *
 	 * @returns name
 	 * @returns name
 	 */
 	 */
-	public getName(): string {
+	public getName() {
 		return this.name;
 		return this.name;
 	}
 	}
 
 
@@ -37,7 +38,7 @@ export default abstract class BaseModule {
 	 *
 	 *
 	 * @returns status
 	 * @returns status
 	 */
 	 */
-	public getStatus(): ModuleStatus {
+	public getStatus() {
 		return this.status;
 		return this.status;
 	}
 	}
 
 
@@ -46,25 +47,22 @@ export default abstract class BaseModule {
 	 *
 	 *
 	 * @param status - Module status
 	 * @param status - Module status
 	 */
 	 */
-	public setStatus(status: ModuleStatus): void {
+	public setStatus(status: ModuleStatus) {
 		this.status = status;
 		this.status = status;
 	}
 	}
 
 
 	/**
 	/**
 	 * startup - Startup module
 	 * startup - Startup module
 	 */
 	 */
-	public startup(): Promise<void> {
-		return new Promise(resolve => {
-			this.log(`Module (${this.name}) starting`);
-			this.setStatus("STARTING");
-			resolve();
-		});
+	public async startup() {
+		this.log(`Module (${this.name}) starting`);
+		this.setStatus("STARTING");
 	}
 	}
 
 
 	/**
 	/**
 	 * started - called with the module has started
 	 * started - called with the module has started
 	 */
 	 */
-	protected started(): void {
+	protected async started() {
 		this.log(`Module (${this.name}) started`);
 		this.log(`Module (${this.name}) started`);
 		this.setStatus("STARTED");
 		this.setStatus("STARTED");
 	}
 	}
@@ -72,19 +70,16 @@ export default abstract class BaseModule {
 	/**
 	/**
 	 * shutdown - Shutdown module
 	 * shutdown - Shutdown module
 	 */
 	 */
-	public shutdown(): Promise<void> {
-		return new Promise(resolve => {
-			this.log(`Module (${this.name}) stopping`);
-			this.setStatus("STOPPING");
-			this.stopped();
-			resolve();
-		});
+	public async shutdown() {
+		this.log(`Module (${this.name}) stopping`);
+		this.setStatus("STOPPING");
+		await this.stopped();
 	}
 	}
 
 
 	/**
 	/**
 	 * stopped - called when the module has stopped
 	 * stopped - called when the module has stopped
 	 */
 	 */
-	protected stopped(): void {
+	protected async stopped() {
 		this.log(`Module (${this.name}) stopped`);
 		this.log(`Module (${this.name}) stopped`);
 		this.setStatus("STOPPED");
 		this.setStatus("STOPPED");
 	}
 	}
@@ -92,14 +87,23 @@ export default abstract class BaseModule {
 	/**
 	/**
 	 * log - Add log to logbook
 	 * log - Add log to logbook
 	 *
 	 *
-	 * @param message - Log message
+	 * @param log - Log message or object
 	 */
 	 */
-	protected log(message: string) {
+	protected log(log: string | Omit<Log, "timestamp" | "category">) {
+		const {
+			message,
+			type = undefined,
+			data = {}
+		} = {
+			...(typeof log === "string" ? { message: log } : log)
+		};
 		this.moduleManager.logBook.log({
 		this.moduleManager.logBook.log({
 			message,
 			message,
+			type,
 			category: `modules`,
 			category: `modules`,
 			data: {
 			data: {
-				moduleName: this.name
+				moduleName: this.name,
+				...data
 			}
 			}
 		});
 		});
 	}
 	}

+ 6 - 6
backend/src/Job.ts

@@ -75,7 +75,7 @@ export default class Job {
 	 *
 	 *
 	 * @returns module.name
 	 * @returns module.name
 	 */
 	 */
-	public getName(): string {
+	public getName() {
 		return `${this.module.getName()}.${this.name}`;
 		return `${this.module.getName()}.${this.name}`;
 	}
 	}
 
 
@@ -84,7 +84,7 @@ export default class Job {
 	 *
 	 *
 	 * @returns priority
 	 * @returns priority
 	 */
 	 */
-	public getPriority(): number {
+	public getPriority() {
 		return this.priority;
 		return this.priority;
 	}
 	}
 
 
@@ -93,7 +93,7 @@ export default class Job {
 	 *
 	 *
 	 * @returns UUID
 	 * @returns UUID
 	 */
 	 */
-	public getUuid(): string {
+	public getUuid() {
 		return this.uuid;
 		return this.uuid;
 	}
 	}
 
 
@@ -102,7 +102,7 @@ export default class Job {
 	 *
 	 *
 	 * @returns status
 	 * @returns status
 	 */
 	 */
-	public getStatus(): JobStatus {
+	public getStatus() {
 		return this.status;
 		return this.status;
 	}
 	}
 
 
@@ -111,7 +111,7 @@ export default class Job {
 	 *
 	 *
 	 * @param status - Job status
 	 * @param status - Job status
 	 */
 	 */
-	public setStatus(status: JobStatus): void {
+	public setStatus(status: JobStatus) {
 		this.status = status;
 		this.status = status;
 	}
 	}
 
 
@@ -120,7 +120,7 @@ export default class Job {
 	 *
 	 *
 	 * @returns module
 	 * @returns module
 	 */
 	 */
-	public getModule(): Module {
+	public getModule() {
 		return this.module;
 		return this.module;
 	}
 	}
 
 

+ 22 - 20
backend/src/JobContext.ts

@@ -1,7 +1,7 @@
 import { Jobs, Modules } from "./types/Modules";
 import { Jobs, Modules } from "./types/Modules";
 
 
 import Job from "./Job";
 import Job from "./Job";
-import LogBook from "./LogBook";
+import LogBook, { Log } from "./LogBook";
 import ModuleManager from "./ModuleManager";
 import ModuleManager from "./ModuleManager";
 import BaseModule from "./BaseModule";
 import BaseModule from "./BaseModule";
 import { JobOptions } from "./types/JobOptions";
 import { JobOptions } from "./types/JobOptions";
@@ -26,16 +26,24 @@ export default class JobContext {
 	/**
 	/**
 	 * Log a message in the context of the current job, which automatically sets the category and data
 	 * Log a message in the context of the current job, which automatically sets the category and data
 	 *
 	 *
-	 * @param {string} message
-	 * @memberof JobContext
+	 * @param log - Log message or object
 	 */
 	 */
-	public log(message: string) {
+	public log(log: string | Omit<Log, "timestamp" | "category">) {
+		const {
+			message,
+			type = undefined,
+			data = {}
+		} = {
+			...(typeof log === "string" ? { message: log } : log)
+		};
 		this.moduleManager.logBook.log({
 		this.moduleManager.logBook.log({
 			message,
 			message,
+			type,
 			category: `${this.job.getModule().getName()}.${this.job.getName()}`,
 			category: `${this.job.getModule().getName()}.${this.job.getName()}`,
 			data: {
 			data: {
 				moduleName: this.job.getModule().getName(),
 				moduleName: this.job.getModule().getName(),
-				jobName: this.job.getName()
+				jobName: this.job.getName(),
+				...data
 			}
 			}
 		});
 		});
 	}
 	}
@@ -43,16 +51,13 @@ export default class JobContext {
 	/**
 	/**
 	 * Runs a job in the context of an existing job, which by default runs jobs right away
 	 * Runs a job in the context of an existing job, which by default runs jobs right away
 	 *
 	 *
-	 * @template ModuleNameType name of the module, which must exist
-	 * @template JobNameType name of the job, which must exist
-	 * @template PayloadType payload type based on the module and job, which is void if there is no payload
-	 * @template ReturnType return type of the Promise, based on the module and job
-	 * @param {ModuleNameType} moduleName
-	 * @param {JobNameType} jobName
-	 * @param {PayloadType} payload
-	 * @param {JobOptions} [options]
-	 * @return {*}  {Promise<ReturnType>}
-	 * @memberof JobContext
+	 * @typeParam ModuleNameType - name of the module, which must exist
+	 * @typeParam JobNameType - name of the job, which must exist
+	 * @typeParam PayloadType - payload type based on the module and job, which is void if there is no payload
+	 * @param moduleName - Module name
+	 * @param jobName - Job name
+	 * @param payload - Job payload, if none then void
+	 * @param options - Job options
 	 */
 	 */
 	public runJob<
 	public runJob<
 		ModuleNameType extends keyof Jobs & keyof Modules,
 		ModuleNameType extends keyof Jobs & keyof Modules,
@@ -62,16 +67,13 @@ export default class JobContext {
 			? Jobs[ModuleNameType][JobNameType]["payload"] extends undefined
 			? Jobs[ModuleNameType][JobNameType]["payload"] extends undefined
 				? Record<string, never>
 				? Record<string, never>
 				: Jobs[ModuleNameType][JobNameType]["payload"]
 				: Jobs[ModuleNameType][JobNameType]["payload"]
-			: Record<string, never>,
-		ReturnType = "returns" extends keyof Jobs[ModuleNameType][JobNameType]
-			? Jobs[ModuleNameType][JobNameType]["returns"]
-			: never
+			: Record<string, never>
 	>(
 	>(
 		moduleName: ModuleNameType,
 		moduleName: ModuleNameType,
 		jobName: JobNameType,
 		jobName: JobNameType,
 		payload: PayloadType,
 		payload: PayloadType,
 		options?: JobOptions
 		options?: JobOptions
-	): Promise<ReturnType> {
+	) {
 		// If options doesn't exist, create it
 		// If options doesn't exist, create it
 		const newOptions = options ?? {};
 		const newOptions = options ?? {};
 		// If runDirectly is not set, set it to true
 		// If runDirectly is not set, set it to true

+ 7 - 7
backend/src/JobQueue.ts

@@ -40,7 +40,7 @@ export default class JobQueue {
 	 *
 	 *
 	 * @param job - Job
 	 * @param job - Job
 	 */
 	 */
-	public add(job: Job): void {
+	public add(job: Job) {
 		this.queue.push(job);
 		this.queue.push(job);
 		this.updateStats(job.getName(), "added");
 		this.updateStats(job.getName(), "added");
 		setTimeout(() => {
 		setTimeout(() => {
@@ -54,7 +54,7 @@ export default class JobQueue {
 	 * @param jobId - Job UUID
 	 * @param jobId - Job UUID
 	 * @returns Job if found
 	 * @returns Job if found
 	 */
 	 */
-	public getJob(jobId: string): Job | undefined {
+	public getJob(jobId: string) {
 		return (
 		return (
 			this.queue.find(job => job.getUuid() === jobId) ||
 			this.queue.find(job => job.getUuid() === jobId) ||
 			this.active.find(job => job.getUuid() === jobId)
 			this.active.find(job => job.getUuid() === jobId)
@@ -66,14 +66,14 @@ export default class JobQueue {
 	 *
 	 *
 	 * Pause processing of jobs in queue, active jobs will not be paused.
 	 * Pause processing of jobs in queue, active jobs will not be paused.
 	 */
 	 */
-	public pause(): void {
+	public pause() {
 		this.isPaused = true;
 		this.isPaused = true;
 	}
 	}
 
 
 	/**
 	/**
 	 * resume - Resume queue
 	 * resume - Resume queue
 	 */
 	 */
-	public resume(): void {
+	public resume() {
 		this.isPaused = false;
 		this.isPaused = false;
 		this.process();
 		this.process();
 	}
 	}
@@ -81,8 +81,7 @@ export default class JobQueue {
 	/**
 	/**
 	 * Actually run a job function
 	 * Actually run a job function
 	 *
 	 *
-	 * @param {Job} job
-	 * @memberof JobQueue
+	 * @param job - Initiated job
 	 */
 	 */
 	public runJob(job: Job) {
 	public runJob(job: Job) {
 		// Record when we started a job
 		// Record when we started a job
@@ -118,7 +117,7 @@ export default class JobQueue {
 	/**
 	/**
 	 * process - Process queue
 	 * process - Process queue
 	 */
 	 */
-	private process(): void {
+	private process() {
 		// If the process is locked, don't continue. This prevents running process at the same time which could lead to issues
 		// If the process is locked, don't continue. This prevents running process at the same time which could lead to issues
 		if (this.processLock) return;
 		if (this.processLock) return;
 		// If the queue is paused, we've reached the maximum number of active jobs, or there are no jobs in the queue, don't continue
 		// If the queue is paused, we've reached the maximum number of active jobs, or there are no jobs in the queue, don't continue
@@ -233,6 +232,7 @@ export default class JobQueue {
 	 *
 	 *
 	 * @param jobName - Job name
 	 * @param jobName - Job name
 	 * @param type - Stats type
 	 * @param type - Stats type
+	 * @param duration - Duration of job, for average time stats
 	 */
 	 */
 	private updateStats(
 	private updateStats(
 		jobName: string,
 		jobName: string,

+ 3 - 3
backend/src/LogBook.ts

@@ -1,4 +1,3 @@
-// @ts-nocheck
 import config from "config";
 import config from "config";
 
 
 export type Log = {
 export type Log = {
@@ -64,7 +63,7 @@ export default class LogBook {
 				if (config.has(`logging.${output}`))
 				if (config.has(`logging.${output}`))
 					this.default[output] = {
 					this.default[output] = {
 						...this.default[output],
 						...this.default[output],
-						...config.get<any>(`logging.${output}`)
+						...config.get(`logging.${output}`)
 					};
 					};
 			});
 			});
 		this.outputs = this.default;
 		this.outputs = this.default;
@@ -200,7 +199,8 @@ export default class LogBook {
 			case "include":
 			case "include":
 			case "exclude": {
 			case "exclude": {
 				if (action === "set" || action === "add") {
 				if (action === "set" || action === "add") {
-					if (!values) throw new Error("No filters provided");
+					if (!values || typeof values !== "object")
+						throw new Error("No filters provided");
 					const filters = Array.isArray(values) ? values : [values];
 					const filters = Array.isArray(values) ? values : [values];
 					if (action === "set") this.outputs[output][key] = filters;
 					if (action === "set") this.outputs[output][key] = filters;
 					if (action === "add")
 					if (action === "add")

+ 18 - 35
backend/src/ModuleManager.ts

@@ -70,22 +70,15 @@ export default class ModuleManager {
 	 * @param moduleName - Name of the module
 	 * @param moduleName - Name of the module
 	 * @returns Module
 	 * @returns Module
 	 */
 	 */
-	private loadModule<T extends keyof Modules>(
-		moduleName: T
-	): Promise<Modules[T]> {
-		return new Promise(resolve => {
-			const mapper = {
-				stations: "StationModule",
-				others: "OtherModule",
-				data: "DataModule"
-			};
-			import(`./modules/${mapper[moduleName]}`).then(
-				({ default: Module }: { default: ModuleClass<Modules[T]> }) => {
-					const module = new Module(this);
-					resolve(module);
-				}
-			);
-		});
+	private async loadModule<T extends keyof Modules>(moduleName: T) {
+		const mapper = {
+			data: "DataModule",
+			events: "EventsModule",
+			stations: "StationModule"
+		};
+		const { default: Module }: { default: ModuleClass<Modules[T]> } =
+			await import(`./modules/${mapper[moduleName]}`);
+		return new Module(this);
 	}
 	}
 
 
 	/**
 	/**
@@ -93,28 +86,18 @@ export default class ModuleManager {
 	 *
 	 *
 	 * @returns Promise
 	 * @returns Promise
 	 */
 	 */
-	private loadModules(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			const fetchModules = async () => ({
-				data: await this.loadModule("data"),
-				others: await this.loadModule("others"),
-				stations: await this.loadModule("stations")
-			});
-			fetchModules()
-				.then(modules => {
-					this.modules = modules;
-					resolve();
-				})
-				.catch(err => {
-					reject(new Error(err));
-				});
-		});
+	private async loadModules() {
+		this.modules = {
+			data: await this.loadModule("data"),
+			events: await this.loadModule("events"),
+			stations: await this.loadModule("stations")
+		};
 	}
 	}
 
 
 	/**
 	/**
 	 * startup - Handle startup
 	 * startup - Handle startup
 	 */
 	 */
-	public async startup(): Promise<void> {
+	public async startup() {
 		await this.loadModules().catch(async err => {
 		await this.loadModules().catch(async err => {
 			await this.shutdown();
 			await this.shutdown();
 			throw err;
 			throw err;
@@ -137,7 +120,7 @@ export default class ModuleManager {
 	/**
 	/**
 	 * shutdown - Handle shutdown
 	 * shutdown - Handle shutdown
 	 */
 	 */
-	public async shutdown(): Promise<void> {
+	public async shutdown() {
 		// TODO: await jobQueue completion/handle shutdown
 		// TODO: await jobQueue completion/handle shutdown
 		if (this.modules)
 		if (this.modules)
 			await async.each(Object.values(this.modules), async module => {
 			await async.each(Object.values(this.modules), async module => {
@@ -175,7 +158,7 @@ export default class ModuleManager {
 		payload: PayloadType,
 		payload: PayloadType,
 		options?: JobOptions
 		options?: JobOptions
 	): Promise<ReturnType> {
 	): Promise<ReturnType> {
-		return new Promise<ReturnType>((resolve, reject) => {
+		return new Promise((resolve, reject) => {
 			const module = this.modules && this.modules[moduleName];
 			const module = this.modules && this.modules[moduleName];
 			if (!module) reject(new Error("Module not found."));
 			if (!module) reject(new Error("Module not found."));
 			else {
 			else {

+ 12 - 2
backend/src/Schema.ts

@@ -17,13 +17,23 @@ export const createAttribute = ({
 	required,
 	required,
 	restricted,
 	restricted,
 	item,
 	item,
-	schema
+	schema,
+	defaultValue,
+	unique,
+	min,
+	max,
+	enumValues
 }: Partial<Attribute> & { type: Attribute["type"] }) => ({
 }: Partial<Attribute> & { type: Attribute["type"] }) => ({
 	type,
 	type,
 	required: required ?? true,
 	required: required ?? true,
 	restricted: restricted ?? false,
 	restricted: restricted ?? false,
 	item,
 	item,
-	schema
+	schema,
+	defaultValue,
+	unique: unique ?? false,
+	min,
+	max,
+	enumValues
 });
 });
 
 
 export default class Schema {
 export default class Schema {

+ 49 - 18
backend/src/collections/station.ts

@@ -3,22 +3,35 @@ import Schema, { createAttribute, Types } from "../Schema";
 export default new Schema({
 export default new Schema({
 	document: {
 	document: {
 		type: createAttribute({
 		type: createAttribute({
-			type: Types.String
+			type: Types.String,
+			enumValues: ["official", "community"]
 		}),
 		}),
 		name: createAttribute({
 		name: createAttribute({
-			type: Types.String
+			type: Types.String,
+			unique: true,
+			min: 2,
+			max: 16
 		}),
 		}),
 		displayName: createAttribute({
 		displayName: createAttribute({
-			type: Types.String
+			type: Types.String,
+			unique: true,
+			min: 2,
+			max: 32
 		}),
 		}),
 		description: createAttribute({
 		description: createAttribute({
-			type: Types.String
+			type: Types.String,
+			min: 2,
+			max: 128
 		}),
 		}),
 		privacy: createAttribute({
 		privacy: createAttribute({
-			type: Types.String
+			type: Types.String,
+			defaultValue: "private",
+			enumValues: ["public", "unlisted", "private"]
 		}),
 		}),
 		theme: createAttribute({
 		theme: createAttribute({
-			type: Types.String
+			type: Types.String,
+			defaultValue: "blue",
+			enumValues: ["blue", "purple", "teal", "orange", "red"]
 		}),
 		}),
 		owner: createAttribute({
 		owner: createAttribute({
 			type: Types.ObjectId
 			type: Types.ObjectId
@@ -30,22 +43,28 @@ export default new Schema({
 			}
 			}
 		}),
 		}),
 		currentSong: createAttribute({
 		currentSong: createAttribute({
-			type: Types.ObjectId
+			type: Types.ObjectId,
+			required: false
 		}),
 		}),
 		currentSongIndex: createAttribute({
 		currentSongIndex: createAttribute({
-			type: Types.Number
+			type: Types.Number,
+			required: false
 		}),
 		}),
 		startedAt: createAttribute({
 		startedAt: createAttribute({
-			type: Types.Date
+			type: Types.Date,
+			required: false
 		}),
 		}),
 		paused: createAttribute({
 		paused: createAttribute({
-			type: Types.Boolean
+			type: Types.Boolean,
+			defaultValue: false
 		}),
 		}),
 		timePaused: createAttribute({
 		timePaused: createAttribute({
-			type: Types.Number
+			type: Types.Number,
+			defaultValue: 0
 		}),
 		}),
 		pausedAt: createAttribute({
 		pausedAt: createAttribute({
-			type: Types.Date
+			type: Types.Date,
+			required: false
 		}),
 		}),
 		playlist: createAttribute({
 		playlist: createAttribute({
 			type: Types.ObjectId
 			type: Types.ObjectId
@@ -66,13 +85,19 @@ export default new Schema({
 			type: Types.Schema,
 			type: Types.Schema,
 			schema: {
 			schema: {
 				enabled: createAttribute({
 				enabled: createAttribute({
-					type: Types.Boolean
+					type: Types.Boolean,
+					defaultValue: true
 				}),
 				}),
 				access: createAttribute({
 				access: createAttribute({
-					type: Types.String
+					type: Types.String,
+					defaultValue: "owner",
+					enumValues: ["owner", "user"]
 				}),
 				}),
 				limit: createAttribute({
 				limit: createAttribute({
-					type: Types.Number
+					type: Types.Number,
+					defaultValue: 5,
+					min: 1,
+					max: 50
 				})
 				})
 			}
 			}
 		}),
 		}),
@@ -80,7 +105,8 @@ export default new Schema({
 			type: Types.Schema,
 			type: Types.Schema,
 			schema: {
 			schema: {
 				enabled: createAttribute({
 				enabled: createAttribute({
-					type: Types.Boolean
+					type: Types.Boolean,
+					defaultValue: true
 				}),
 				}),
 				playlists: createAttribute({
 				playlists: createAttribute({
 					type: Types.Array,
 					type: Types.Array,
@@ -89,10 +115,15 @@ export default new Schema({
 					}
 					}
 				}),
 				}),
 				limit: createAttribute({
 				limit: createAttribute({
-					type: Types.Number
+					type: Types.Number,
+					defaultValue: 30,
+					min: 1,
+					max: 50
 				}),
 				}),
 				mode: createAttribute({
 				mode: createAttribute({
-					type: Types.String
+					type: Types.String,
+					defaultValue: "random",
+					enumValues: ["random", "sequential"]
 				})
 				})
 			}
 			}
 		})
 		})

+ 223 - 277
backend/src/modules/DataModule.ts

@@ -1,7 +1,7 @@
 import async from "async";
 import async from "async";
 import config from "config";
 import config from "config";
 import { Db, MongoClient, ObjectId } from "mongodb";
 import { Db, MongoClient, ObjectId } from "mongodb";
-import hash from "object-hash";
+import { createHash } from "node:crypto";
 import { createClient, RedisClientType } from "redis";
 import { createClient, RedisClientType } from "redis";
 import JobContext from "../JobContext";
 import JobContext from "../JobContext";
 import BaseModule from "../BaseModule";
 import BaseModule from "../BaseModule";
@@ -10,6 +10,7 @@ import Schema, { Types } from "../Schema";
 import { Collections } from "../types/Collections";
 import { Collections } from "../types/Collections";
 import { Document as SchemaDocument } from "../types/Document";
 import { Document as SchemaDocument } from "../types/Document";
 import { UniqueMethods } from "../types/Modules";
 import { UniqueMethods } from "../types/Modules";
+import { AttributeValue } from "../types/AttributeValue";
 
 
 interface ProjectionObject {
 interface ProjectionObject {
 	[property: string]: boolean | string[] | ProjectionObject;
 	[property: string]: boolean | string[] | ProjectionObject;
@@ -22,13 +23,8 @@ type NormalizedProjection = {
 	mode: "includeAllBut" | "excludeAllBut";
 	mode: "includeAllBut" | "excludeAllBut";
 };
 };
 
 
-type SchemaDocumentSimpleValue = string | number | boolean | Date | ObjectId;
-
 interface MongoFilter {
 interface MongoFilter {
-	[property: string]:
-		| SchemaDocumentSimpleValue
-		| SchemaDocumentSimpleValue[]
-		| MongoFilter;
+	[property: string]: AttributeValue | AttributeValue[] | MongoFilter;
 }
 }
 
 
 // WIP
 // WIP
@@ -57,87 +53,71 @@ export default class DataModule extends BaseModule {
 	/**
 	/**
 	 * startup - Startup data module
 	 * startup - Startup data module
 	 */
 	 */
-	public override startup(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			async.waterfall(
-				[
-					async () => super.startup(),
-
-					async () => {
-						const mongoUrl = config.get<string>("mongo.url");
-
-						this.mongoClient = new MongoClient(mongoUrl);
-						await this.mongoClient.connect();
-						this.mongoDb = this.mongoClient.db();
-					},
-
-					async () => this.loadCollections(),
-
-					async () => {
-						const { url, password } = config.get<{
-							url: string;
-							password: string;
-						}>("redis");
-
-						this.redisClient = createClient({
-							url,
-							password
-						});
-
-						return this.redisClient.connect();
-					},
-
-					async () => {
-						if (!this.redisClient)
-							throw new Error("Redis connection not established");
-
-						return this.redisClient.sendCommand([
-							"CONFIG",
-							"GET",
-							"notify-keyspace-events"
-						]);
-					},
-
-					async (redisConfigResponse: string[]) => {
-						if (
-							!(
-								Array.isArray(redisConfigResponse) &&
-								redisConfigResponse[1] === "xE"
-							)
-						)
-							throw new Error(
-								`notify-keyspace-events is NOT configured correctly! It is set to: ${
-									(Array.isArray(redisConfigResponse) &&
-										redisConfigResponse[1]) ||
-									"unknown"
-								}`
-							);
-					},
+	public override async startup() {
+		return async.waterfall<void>([
+			async () => super.startup(),
 
 
-					async () => super.started()
-				],
-				err => {
-					if (err) reject(err);
-					else resolve();
-				}
-			);
-		});
+			async () => {
+				const mongoUrl = config.get<string>("mongo.url");
+
+				this.mongoClient = new MongoClient(mongoUrl);
+				await this.mongoClient.connect();
+				this.mongoDb = this.mongoClient.db();
+			},
+
+			async () => this.loadCollections(),
+
+			async () => {
+				const { url } = config.get<{
+					url: string;
+				}>("redis");
+
+				this.redisClient = createClient({
+					url
+				});
+
+				return this.redisClient.connect();
+			},
+
+			async () => {
+				if (!this.redisClient)
+					throw new Error("Redis connection not established");
+
+				return this.redisClient.sendCommand([
+					"CONFIG",
+					"GET",
+					"notify-keyspace-events"
+				]);
+			},
+
+			async (redisConfigResponse: string[]) => {
+				if (
+					!(
+						Array.isArray(redisConfigResponse) &&
+						redisConfigResponse[1] === "xE"
+					)
+				)
+					throw new Error(
+						`notify-keyspace-events is NOT configured correctly! It is set to: ${
+							(Array.isArray(redisConfigResponse) &&
+								redisConfigResponse[1]) ||
+							"unknown"
+						}`
+					);
+			},
+
+			async () => super.started()
+		]);
 	}
 	}
 
 
 	/**
 	/**
 	 * shutdown - Shutdown data module
 	 * shutdown - Shutdown data module
 	 */
 	 */
-	public override shutdown(): Promise<void> {
-		return new Promise(resolve => {
-			super
-				.shutdown()
-				.then(async () => {
-					// TODO: Ensure the following shutdown correctly
-					if (this.redisClient) await this.redisClient.quit();
-					if (this.mongoClient) await this.mongoClient.close(false);
-				})
-				.finally(() => resolve());
-		});
+	public override async shutdown() {
+		await super.shutdown();
+		// TODO: Ensure the following shutdown correctly
+		if (this.redisClient) await this.redisClient.quit();
+		if (this.mongoClient) await this.mongoClient.close(false);
 	}
 	}
 
 
 	/**
 	/**
@@ -146,24 +126,16 @@ export default class DataModule extends BaseModule {
 	 * @param collectionName - Name of the collection
 	 * @param collectionName - Name of the collection
 	 * @returns Collection
 	 * @returns Collection
 	 */
 	 */
-	private loadCollection<T extends keyof Collections>(
+	private async loadCollection<T extends keyof Collections>(
 		collectionName: T
 		collectionName: T
-	): Promise<{
-		schema: Collections[T]["schema"];
-		collection: Collections[T]["collection"];
-	}> {
-		return new Promise(resolve => {
-			import(`../collections/${collectionName.toString()}`).then(
-				({ default: schema }: { default: Schema }) => {
-					resolve({
-						schema,
-						collection: this.mongoDb!.collection(
-							collectionName.toString()
-						)
-					});
-				}
-			);
-		});
+	) {
+		const { default: schema }: { default: Schema } = await import(
+			`../collections/${collectionName.toString()}`
+		);
+		return {
+			schema,
+			collection: this.mongoDb!.collection(collectionName.toString())
+		};
 	}
 	}
 
 
 	/**
 	/**
@@ -171,21 +143,11 @@ export default class DataModule extends BaseModule {
 	 *
 	 *
 	 * @returns Promise
 	 * @returns Promise
 	 */
 	 */
-	private loadCollections(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			const fetchCollections = async () => ({
-				abc: await this.loadCollection("abc"),
-				station: await this.loadCollection("station")
-			});
-			fetchCollections()
-				.then(collections => {
-					this.collections = collections;
-					resolve();
-				})
-				.catch(err => {
-					reject(new Error(err));
-				});
-		});
+	private async loadCollections() {
+		this.collections = {
+			abc: await this.loadCollection("abc"),
+			station: await this.loadCollection("station")
+		};
 	}
 	}
 
 
 	/**
 	/**
@@ -519,10 +481,7 @@ export default class DataModule extends BaseModule {
 		return newAllowedRestricted;
 		return newAllowedRestricted;
 	}
 	}
 
 
-	private getCastedValue(
-		value: SchemaDocumentSimpleValue,
-		schemaType: Types
-	) {
+	private getCastedValue(value: AttributeValue, schemaType: Types) {
 		if (schemaType === Types.String) {
 		if (schemaType === Types.String) {
 			// Check if value is a string, and if not, convert the value to a string
 			// Check if value is a string, and if not, convert the value to a string
 			const castedValue =
 			const castedValue =
@@ -548,7 +507,7 @@ export default class DataModule extends BaseModule {
 			const castedValue =
 			const castedValue =
 				Object.prototype.toString.call(value) === "[object Date]"
 				Object.prototype.toString.call(value) === "[object Date]"
 					? (value as Date)
 					? (value as Date)
-					: new Date(value.toString());
+					: new Date(value);
 			// TODO possibly allow this via a validate boolean option?
 			// TODO possibly allow this via a validate boolean option?
 			// We don't allow invalid dates, so throw an error
 			// We don't allow invalid dates, so throw an error
 			if (new Date(castedValue).toString() === "Invalid Date")
 			if (new Date(castedValue).toString() === "Invalid Date")
@@ -1081,7 +1040,7 @@ export default class DataModule extends BaseModule {
 	 * @param payload - Payload
 	 * @param payload - Payload
 	 * @returns Returned object
 	 * @returns Returned object
 	 */
 	 */
-	public find<CollectionNameType extends keyof Collections>(
+	public async find<CollectionNameType extends keyof Collections>(
 		context: JobContext,
 		context: JobContext,
 		{
 		{
 			collection, // Collection name
 			collection, // Collection name
@@ -1101,174 +1060,161 @@ export default class DataModule extends BaseModule {
 			page?: number;
 			page?: number;
 			useCache?: boolean;
 			useCache?: boolean;
 		}
 		}
-	): Promise<Document | Document[] | null> {
-		return new Promise((resolve, reject) => {
-			let queryHash: string | null = null;
-			let cacheable = useCache !== false;
-
-			let schema: Schema;
-
-			let normalizedProjection: NormalizedProjection;
-
-			let mongoFilter: MongoFilter;
-			let mongoProjection: ProjectionObject;
-
-			async.waterfall(
-				[
-					// Verify page and limit parameters
-					async () => {
-						if (page < 1)
-							throw new Error("Page must be at least 1");
-						if (limit < 1)
-							throw new Error("Limit must be at least 1");
-						if (limit > 100)
-							throw new Error(
-								"Limit must not be greater than 100"
-							);
-					},
-
-					// Verify whether the collection exists, and get the schema
-					async () => {
-						if (!collection)
-							throw new Error("No collection specified");
-						if (this.collections && !this.collections[collection])
-							throw new Error("Collection not found");
-
-						schema = this.collections![collection].schema;
-					},
-
-					// Normalize the projection into something we understand, and which throws an error if we have any path collisions
-					async () => {
-						normalizedProjection =
-							this.normalizeProjection(projection);
-					},
-
-					// TOOD validate the projection based on the schema here
-
-					// Parse the projection into a mongo projection, and returns whether this query can be cached or not
-					async () => {
-						const parsedProjection = await this.parseFindProjection(
-							normalizedProjection,
-							schema.getDocument(),
-							allowedRestricted
-						);
+	) {
+		let queryHash: string | null = null;
+		let cacheable = useCache !== false;
 
 
-						cacheable = cacheable && parsedProjection.canCache;
-						mongoProjection = parsedProjection.mongoProjection;
-					},
+		let schema: Schema;
 
 
-					// Parse the filter into a mongo filter, which also validates whether the filter is legal or not, and returns whether this query can be cached or not
-					async () => {
-						const parsedFilter = await this.parseFindFilter(
-							filter,
-							schema.getDocument(),
-							allowedRestricted
-						);
+		let normalizedProjection: NormalizedProjection;
 
 
-						cacheable = cacheable && parsedFilter.canCache;
-						mongoFilter = parsedFilter.mongoFilter;
-					},
-
-					// If we can use cache, get from the cache, and if we get results return those
-					async () => {
-						// If we're allowed to cache, and the filter doesn't reference any restricted fields, try to cache the query and its response
-						if (cacheable) {
-							// Turn the query object into a sha1 hash that can be used as a Redis key
-							queryHash = hash(
-								{
-									collection,
-									mongoFilter,
-									limit,
-									page
-								},
-								{
-									algorithm: "sha1"
-								}
-							);
+		let mongoFilter: MongoFilter;
+		let mongoProjection: ProjectionObject;
 
 
-							// Check if the query hash already exists in Redis, and get it if it is
-							const cachedQuery = await this.redisClient?.GET(
-								`query.find.${queryHash}`
-							);
+		return async.waterfall<Document | Document[] | null>([
+			// Verify page and limit parameters
+			async () => {
+				if (page < 1) throw new Error("Page must be at least 1");
+				if (limit < 1) throw new Error("Limit must be at least 1");
+				if (limit > 100)
+					throw new Error("Limit must not be greater than 100");
+			},
 
 
-							// Return the mongoFilter along with the cachedDocuments, if any
-							return {
-								cachedDocuments: cachedQuery
-									? JSON.parse(cachedQuery)
-									: null
-							};
-						}
+			// Verify whether the collection exists, and get the schema
+			async () => {
+				if (!collection) throw new Error("No collection specified");
+				if (this.collections && !this.collections[collection])
+					throw new Error("Collection not found");
 
 
-						// We can't use the cache, so just continue with no cached documents
-						return { cachedDocuments: null };
-					},
-
-					// Get documents from Mongo if we got no cached documents
-					async ({
-						cachedDocuments
-					}: {
-						cachedDocuments: Document[] | null;
-					}) => {
-						// We got cached documents, so continue with those
-						if (cachedDocuments) {
-							cacheable = false;
-							return cachedDocuments;
-						}
+				schema = this.collections![collection].schema;
+			},
 
 
-						// TODO, add mongo projection. Make sure to keep in mind caching with queryHash.
+			// Normalize the projection into something we understand, and which throws an error if we have any path collisions
+			async () => {
+				normalizedProjection = this.normalizeProjection(projection);
+			},
 
 
-						const totalCount = await this.collections?.[
-							collection
-						].collection.countDocuments({ $expr: mongoFilter });
-						if (totalCount === 0) return [];
-						const lastPage = Math.ceil(totalCount / limit);
-						if (lastPage < page)
-							throw new Error(
-								`The last page available is ${lastPage}`
-							);
+			// TOOD validate the projection based on the schema here
 
 
-						// Create the Mongo cursor and then return the promise that gets the array of documents
-						return this.collections?.[collection].collection
-							.find(mongoFilter, mongoProjection)
-							.limit(limit)
-							.skip((page - 1) * limit)
-							.toArray();
-					},
-
-					// Add documents to the cache
-					async (documents: Document[]) => {
-						// Adds query results to cache but doesnt await
-						if (cacheable && queryHash) {
-							this.redisClient!.SET(
-								`query.find.${queryHash}`,
-								JSON.stringify(documents),
-								{
-									EX: 60
-								}
-							);
-						}
-						return documents;
-					},
-
-					// Strips the document of any unneeded properties or properties that are restricted
-					async (documents: Document[]) =>
-						async.map(documents, async (document: Document) =>
-							this.stripDocument(
-								document,
-								schema.getDocument(),
-								normalizedProjection,
-								allowedRestricted
-							)
+			// Parse the projection into a mongo projection, and returns whether this query can be cached or not
+			async () => {
+				const parsedProjection = await this.parseFindProjection(
+					normalizedProjection,
+					schema.getDocument(),
+					allowedRestricted
+				);
+
+				cacheable = cacheable && parsedProjection.canCache;
+				mongoProjection = parsedProjection.mongoProjection;
+			},
+
+			// Parse the filter into a mongo filter, which also validates whether the filter is legal or not, and returns whether this query can be cached or not
+			async () => {
+				const parsedFilter = await this.parseFindFilter(
+					filter,
+					schema.getDocument(),
+					allowedRestricted
+				);
+
+				cacheable = cacheable && parsedFilter.canCache;
+				mongoFilter = parsedFilter.mongoFilter;
+			},
+
+			// If we can use cache, get from the cache, and if we get results return those
+			async () => {
+				// If we're allowed to cache, and the filter doesn't reference any restricted fields, try to cache the query and its response
+				if (cacheable) {
+					// Turn the query object into a md5 hash that can be used as a Redis key
+					queryHash = createHash("md5")
+						.update(
+							JSON.stringify({
+								collection,
+								mongoFilter,
+								limit,
+								page
+							})
 						)
 						)
-				],
-				(err, documents?: any[]) => {
-					if (err) reject(err);
-					else if (!documents || documents!.length === 0)
-						resolve(limit === 1 ? null : []);
-					else resolve(limit === 1 ? documents![0] : documents);
+						.digest("hex");
+
+					// Check if the query hash already exists in Redis, and get it if it is
+					const cachedQuery = await this.redisClient?.GET(
+						`query.find.${queryHash}`
+					);
+
+					// Return the mongoFilter along with the cachedDocuments, if any
+					return {
+						cachedDocuments: cachedQuery
+							? JSON.parse(cachedQuery)
+							: null
+					};
 				}
 				}
-			);
-		});
+
+				// We can't use the cache, so just continue with no cached documents
+				return { cachedDocuments: null };
+			},
+
+			// Get documents from Mongo if we got no cached documents
+			async ({
+				cachedDocuments
+			}: {
+				cachedDocuments: Document[] | null;
+			}) => {
+				// We got cached documents, so continue with those
+				if (cachedDocuments) {
+					cacheable = false;
+					return cachedDocuments;
+				}
+
+				// TODO, add mongo projection. Make sure to keep in mind caching with queryHash.
+
+				const totalCount = await this.collections?.[
+					collection
+				].collection.countDocuments({ $expr: mongoFilter });
+				if (totalCount === 0 || totalCount === undefined) return [];
+				const lastPage = Math.ceil(totalCount / limit);
+				if (lastPage < page)
+					throw new Error(`The last page available is ${lastPage}`);
+
+				// Create the Mongo cursor and then return the promise that gets the array of documents
+				return this.collections?.[collection].collection
+					.find(mongoFilter, mongoProjection)
+					.limit(limit)
+					.skip((page - 1) * limit)
+					.toArray();
+			},
+
+			// Add documents to the cache
+			async (documents: Document[]) => {
+				// Adds query results to cache but doesnt await
+				if (cacheable && queryHash) {
+					this.redisClient!.SET(
+						`query.find.${queryHash}`,
+						JSON.stringify(documents),
+						{
+							EX: 60
+						}
+					);
+				}
+				return documents;
+			},
+
+			// Strips the document of any unneeded properties or properties that are restricted
+			async (documents: Document[]) =>
+				async.map(documents, async (document: Document) =>
+					this.stripDocument(
+						document,
+						schema.getDocument(),
+						normalizedProjection,
+						allowedRestricted
+					)
+				),
+
+			async (documents: Document[]) => {
+				if (!documents || documents!.length === 0)
+					return limit === 1 ? null : [];
+				return limit === 1 ? documents![0] : documents;
+			}
+		]);
 	}
 	}
 }
 }
 
 

+ 0 - 62
backend/src/modules/OtherModule.ts

@@ -1,62 +0,0 @@
-import JobContext from "src/JobContext";
-import { UniqueMethods } from "../types/Modules";
-import BaseModule from "../BaseModule";
-import ModuleManager from "../ModuleManager";
-
-export default class OtherModule extends BaseModule {
-	/**
-	 * Other Module
-	 *
-	 * @param moduleManager - Module manager class
-	 */
-	public constructor(moduleManager: ModuleManager) {
-		super(moduleManager, "others");
-	}
-
-	/**
-	 * startup - Startup other module
-	 */
-	public override startup(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			super
-				.startup()
-				.then(() => {
-					super.started();
-					resolve();
-				})
-				.catch(err => reject(err));
-		});
-	}
-
-	/**
-	 * doThing - Do thing
-	 *
-	 * @param payload - Payload
-	 * @returns Returned object
-	 */
-	public doThing(
-		context: JobContext,
-		payload: { test: string; test2: number }
-	): Promise<{
-		res: number;
-	}> {
-		return new Promise((resolve, reject) => {
-			const { test, test2 } = payload;
-			// console.log("doThing", test, test2);
-			setTimeout(
-				() =>
-					Math.round(Math.random())
-						? resolve({ res: 123 })
-						: reject(),
-				Math.random() * 1000
-			);
-		});
-	}
-}
-
-export type OtherModuleJobs = {
-	[Property in keyof UniqueMethods<OtherModule>]: {
-		payload: Parameters<UniqueMethods<OtherModule>[Property]>[1];
-		returns: Awaited<ReturnType<UniqueMethods<OtherModule>[Property]>>;
-	};
-};

+ 20 - 43
backend/src/modules/StationModule.ts

@@ -1,4 +1,4 @@
-import JobContext from "src/JobContext";
+import JobContext from "../JobContext";
 import { UniqueMethods } from "../types/Modules";
 import { UniqueMethods } from "../types/Modules";
 import BaseModule from "../BaseModule";
 import BaseModule from "../BaseModule";
 import ModuleManager from "../ModuleManager";
 import ModuleManager from "../ModuleManager";
@@ -16,17 +16,10 @@ export default class StationModule extends BaseModule {
 	/**
 	/**
 	 * startup - Startup station module
 	 * startup - Startup station module
 	 */
 	 */
-	public override startup(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			super
-				.startup()
-				.then(() => {
-					this.log("Station Startup");
-					super.started();
-					resolve();
-				})
-				.catch(err => reject(err));
-		});
+	public override async startup() {
+		await super.startup();
+		this.log("Station Startup");
+		await super.started();
 	}
 	}
 
 
 	/**
 	/**
@@ -34,43 +27,27 @@ export default class StationModule extends BaseModule {
 	 *
 	 *
 	 * @param payload - Payload
 	 * @param payload - Payload
 	 */
 	 */
-	public addToQueue(
-		context: JobContext,
-		payload: { songId: string }
-	): Promise<void> {
-		return new Promise((resolve, reject) => {
-			const { songId } = payload;
-			// console.log(`Adding song ${songId} to the queue.`);
-			setTimeout(
-				() => (Math.round(Math.random()) ? resolve() : reject()),
-				Math.random() * 1000
-			);
-		});
+	public async addToQueue(context: JobContext, payload: { songId: string }) {
+		const { songId } = payload;
+		// console.log(`Adding song ${songId} to the queue.`);
+		setTimeout(() => {
+			if (Math.round(Math.random())) throw new Error();
+		}, Math.random() * 1000);
 	}
 	}
 
 
-	public addA(context: JobContext): Promise<{ number: number }> {
-		return new Promise<{ number: number }>(resolve => {
-			context.log("ADDA");
-			context.runJob("stations", "addB", {}, { priority: 5 }).then(() => {
-				resolve({ number: 123 });
-			});
-		});
+	public async addA(context: JobContext) {
+		context.log("ADDA");
+		await context.runJob("stations", "addB", {}, { priority: 5 });
+		return { number: 123 };
 	}
 	}
 
 
-	public addB(context: JobContext): Promise<void> {
-		return new Promise<void>(resolve => {
-			context.log("ADDB");
-			context.runJob("stations", "addC", {}).then(() => {
-				resolve();
-			});
-		});
+	public async addB(context: JobContext) {
+		context.log("ADDB");
+		await context.runJob("stations", "addC", {});
 	}
 	}
 
 
-	public addC(context: JobContext): Promise<void> {
-		return new Promise<void>(resolve => {
-			context.log("ADDC");
-			resolve();
-		});
+	public addC(context: JobContext) {
+		context.log("ADDC");
 	}
 	}
 }
 }
 
 

+ 6 - 0
backend/src/types/Attribute.ts

@@ -1,4 +1,5 @@
 import { Types } from "../Schema";
 import { Types } from "../Schema";
+import { AttributeValue } from "./AttributeValue";
 import { Document } from "./Document";
 import { Document } from "./Document";
 
 
 export type Attribute = {
 export type Attribute = {
@@ -7,4 +8,9 @@ export type Attribute = {
 	restricted: boolean;
 	restricted: boolean;
 	item?: Pick<Attribute, "type" | "item" | "schema">;
 	item?: Pick<Attribute, "type" | "item" | "schema">;
 	schema?: Document;
 	schema?: Document;
+	defaultValue?: AttributeValue;
+	unique?: boolean;
+	min?: number;
+	max?: number;
+	enumValues?: AttributeValue[];
 };
 };

+ 3 - 0
backend/src/types/AttributeValue.ts

@@ -0,0 +1,3 @@
+import { ObjectId } from "mongodb";
+
+export type AttributeValue = string | number | boolean | Date | ObjectId;

+ 0 - 5
backend/src/types/Modules.ts

@@ -1,5 +1,4 @@
 import DataModule, { DataModuleJobs } from "../modules/DataModule";
 import DataModule, { DataModuleJobs } from "../modules/DataModule";
-import OtherModule, { OtherModuleJobs } from "../modules/OtherModule";
 import StationModule, { StationModuleJobs } from "../modules/StationModule";
 import StationModule, { StationModuleJobs } from "../modules/StationModule";
 import ModuleManager from "../ModuleManager";
 import ModuleManager from "../ModuleManager";
 import BaseModule from "../BaseModule";
 import BaseModule from "../BaseModule";
@@ -14,9 +13,6 @@ export type Jobs = {
 	data: {
 	data: {
 		[Property in keyof DataModuleJobs]: DataModuleJobs[Property];
 		[Property in keyof DataModuleJobs]: DataModuleJobs[Property];
 	};
 	};
-	others: {
-		[Property in keyof OtherModuleJobs]: OtherModuleJobs[Property];
-	};
 	stations: {
 	stations: {
 		[Property in keyof StationModuleJobs]: StationModuleJobs[Property];
 		[Property in keyof StationModuleJobs]: StationModuleJobs[Property];
 	};
 	};
@@ -24,7 +20,6 @@ export type Jobs = {
 
 
 export type Modules = {
 export type Modules = {
 	data: DataModule & typeof BaseModule;
 	data: DataModule & typeof BaseModule;
-	others: OtherModule & typeof BaseModule;
 	stations: StationModule & typeof BaseModule;
 	stations: StationModule & typeof BaseModule;
 };
 };
 
 

+ 7 - 0
backend/src/types/async.d.ts

@@ -0,0 +1,7 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import async from "async";
+
+declare module "async" {
+	// eslint-disable-next-line @typescript-eslint/ban-types
+	export function waterfall<T>(tasks: Function[]): Promise<T>;
+}