Explorar o código

refactor: fixed TypeScript errors in DataModule

Kristian Vos %!s(int64=2) %!d(string=hai) anos
pai
achega
72895f6006
Modificáronse 3 ficheiros con 132 adicións e 50 borrados
  1. 18 16
      backend/src/main.ts
  2. 113 33
      backend/src/modules/DataModule.ts
  3. 1 1
      backend/src/types/AttributeValue.ts

+ 18 - 16
backend/src/main.ts

@@ -1,3 +1,5 @@
+// @ts-ignore
+
 import * as readline from "node:readline";
 import ModuleManager from "./ModuleManager";
 import LogBook from "./LogBook";
@@ -165,22 +167,22 @@ setTimeout(async () => {
 	// 	.then(console.log)
 	// 	.catch(console.error);
 
-	// logBook.log("Find for testing with $in");
-	// await moduleManager
-	// 	.runJob("data", "find", {
-	// 		collection: "abc",
-	// 		filter: {
-	// 			"songs._id": "6371212daf4e9f8fb14444b0"
-	// 		},
-	// 		allowedRestricted: true,
-	// 		// projection: {
-	// 		// 	// songs: true,
-	// 		// 	// someNumbers: false
-	// 		// },
-	// 		limit: 1
-	// 	})
-	// 	.then(console.log)
-	// 	.catch(console.error);
+	logBook.log("Find for testing with $in");
+	await moduleManager
+		.runJob("data", "find", {
+			collection: "abc",
+			filter: {
+				"songs._id": "6371212daf4e9f8fb14444b0"
+			},
+			allowedRestricted: true,
+			// projection: {
+			// 	// songs: true,
+			// 	// someNumbers: false
+			// },
+			limit: 1
+		})
+		.then(console.log)
+		.catch(console.error);
 
 	// logBook.log("Find for testing with $in with numbers");
 	// await moduleManager

+ 113 - 33
backend/src/modules/DataModule.ts

@@ -11,6 +11,10 @@ import { Document as SchemaDocument } from "../types/Document";
 import { UniqueMethods } from "../types/Modules";
 import { AttributeValue } from "../types/AttributeValue";
 
+type Entries<T> = {
+	[K in keyof T]: [K, T[K]];
+}[keyof T][];
+
 interface ProjectionObject {
 	[property: string]: boolean | string[] | ProjectionObject;
 }
@@ -23,14 +27,24 @@ type NormalizedProjection = {
 };
 
 interface MongoFilter {
-	[property: string]: AttributeValue | AttributeValue[] | MongoFilter;
+	[property: string]:
+		| AttributeValue
+		| AttributeValue[]
+		| MongoFilter
+		| MongoFilter[];
 }
 
 // WIP
 interface Document {
-	[property: string]: null;
+	[property: string]:
+		| AttributeValue
+		| AttributeValue[]
+		| Document
+		| Document[];
 }
 
+type AllowedRestricted = boolean | string[] | null | undefined;
+
 export default class DataModule extends BaseModule {
 	private collections?: Collections;
 
@@ -207,7 +221,7 @@ export default class DataModule extends BaseModule {
 	/**
 	 * Flatten the projection we've given (which can be an array of an object) into an array with key/value pairs
 	 *
-	 * @param projection
+	 * @param projection - Projection
 	 * @returns
 	 */
 	private flattenProjection(projection: Projection): [string, boolean][] {
@@ -258,7 +272,7 @@ export default class DataModule extends BaseModule {
 	private async parseFindProjection(
 		projection: NormalizedProjection,
 		schema: SchemaDocument,
-		allowedRestricted: any
+		allowedRestricted: AllowedRestricted
 	) {
 		// The mongo projection object we're going to build
 		const mongoProjection: ProjectionObject = {};
@@ -331,7 +345,10 @@ export default class DataModule extends BaseModule {
 	 * @param property - Property name
 	 * @returns
 	 */
-	private allowedByRestricted(allowedRestricted: any, property: string) {
+	private allowedByRestricted(
+		allowedRestricted: AllowedRestricted,
+		property: string
+	) {
 		// All restricted properties are allowed, so allow
 		if (allowedRestricted === true) return true;
 		// No restricted properties are allowed, so don't allow
@@ -433,14 +450,14 @@ export default class DataModule extends BaseModule {
 	 * @returns Array or Object
 	 */
 	private getDeeperAllowedRestricted(
-		allowedRestricted: any,
+		allowedRestricted: AllowedRestricted,
 		currentKey: string
-	) {
+	): AllowedRestricted {
 		//
 		if (typeof allowedRestricted === "boolean") return allowedRestricted;
 		if (!Array.isArray(allowedRestricted)) return false;
 
-		const newAllowedRestricted = allowedRestricted
+		const newAllowedRestricted: string[] = <string[]>allowedRestricted
 			// Go through all key/values
 			.map(key => {
 				// If a key has no ".", it has no deeper level, so return false
@@ -466,7 +483,9 @@ export default class DataModule extends BaseModule {
 		return newAllowedRestricted;
 	}
 
-	private getCastedValue(value: AttributeValue, schemaType: Types) {
+	private getCastedValue(value: unknown, schemaType: Types): AttributeValue {
+		if (value === null || value === undefined) return null;
+
 		if (schemaType === Types.String) {
 			// Check if value is a string, and if not, convert the value to a string
 			const castedValue =
@@ -492,7 +511,7 @@ export default class DataModule extends BaseModule {
 			const castedValue =
 				Object.prototype.toString.call(value) === "[object Date]"
 					? (value as Date)
-					: new Date(value);
+					: new Date(value.toString());
 			// TODO possibly allow this via a validate boolean option?
 			// We don't allow invalid dates, so throw an error
 			if (new Date(castedValue).toString() === "Invalid Date")
@@ -534,9 +553,9 @@ export default class DataModule extends BaseModule {
 	 * 			and whether query includes restricted attributes
 	 */
 	private async parseFindFilter(
-		filter: any,
+		filter: MongoFilter,
 		schema: SchemaDocument,
-		allowedRestricted: boolean | string[] | null | undefined,
+		allowedRestricted: AllowedRestricted,
 		options?: {
 			operators?: boolean;
 		}
@@ -603,19 +622,28 @@ export default class DataModule extends BaseModule {
 						// Run parseFindQuery again for child objects and add them to the mongo filter operator array
 						await Promise.all(
 							value.map(async _value => {
+								// Value must be an actual object, so if it's not, throw an error
+								if (
+									Object.prototype.toString.call(value) !==
+									"[object Object]"
+								)
+									throw Error("not an object");
+
 								const {
 									mongoFilter: _mongoFilter,
 									containsRestrictedProperties:
 										_containsRestrictedProperties
 								} = await this.parseFindFilter(
-									_value,
+									_value as MongoFilter,
 									schema,
 									allowedRestricted,
 									options
 								);
 
 								// Actually add the returned filter object to the mongo filter we're building
-								mongoFilter[key].push(_mongoFilter);
+								(<MongoFilter[]>mongoFilter[key]).push(
+									_mongoFilter
+								);
 								if (_containsRestrictedProperties)
 									containsRestrictedProperties = true;
 							})
@@ -689,10 +717,6 @@ export default class DataModule extends BaseModule {
 
 						// Handle the $in value operator
 						if (operator === "$in") {
-							mongoFilter[currentKey] = {
-								$in: []
-							};
-
 							// Decide what type should be for the values for $in
 							let { type } = schema[currentKey];
 							// We don't allow schema type for $in
@@ -704,10 +728,17 @@ export default class DataModule extends BaseModule {
 							if (type === Types.Array)
 								type = schema[key].item!.type;
 
+							const value$in = (<{ $in: AttributeValue[] }>value)
+								.$in;
+							let filter$in: AttributeValue[] = [];
+
+							if (!Array.isArray(value$in))
+								throw new Error("$in musr be array");
+
 							// Loop through all $in array items, check if they're not null/undefined, cast them, and return a new array
-							if (value.$in.length > 0)
-								mongoFilter[currentKey].$in = await Promise.all(
-									value.$in.map(async (_value: any) => {
+							if (value$in.length > 0)
+								filter$in = await Promise.all(
+									value$in.map(async _value => {
 										const isNullOrUndefined =
 											_value === null ||
 											_value === undefined;
@@ -724,6 +755,8 @@ export default class DataModule extends BaseModule {
 										return castedValue;
 									})
 								);
+
+							mongoFilter[currentKey] = { $in: filter$in };
 						} else
 							throw new Error(
 								`Unhandled operator "${operator}", this should never happen!`
@@ -742,6 +775,13 @@ export default class DataModule extends BaseModule {
 							};
 						} else subFilter = value;
 
+						// Sub-filter must be an actual object, so if it's not, throw an error
+						if (
+							Object.prototype.toString.call(subFilter) !==
+							"[object Object]"
+						)
+							throw Error("not an object");
+
 						// Get the allowedRestricted for the next layer
 						const deeperAllowedRestricted =
 							this.getDeeperAllowedRestricted(
@@ -755,7 +795,7 @@ export default class DataModule extends BaseModule {
 							containsRestrictedProperties:
 								_containsRestrictedProperties
 						} = await this.parseFindFilter(
-							subFilter,
+							subFilter as MongoFilter,
 							schema[currentKey].schema!,
 							deeperAllowedRestricted,
 							options
@@ -792,6 +832,13 @@ export default class DataModule extends BaseModule {
 								};
 							} else subFilter = value;
 
+							// Sub-filter must be an actual object, so if it's not, throw an error
+							if (
+								Object.prototype.toString.call(subFilter) !==
+								"[object Object]"
+							)
+								throw Error("not an object");
+
 							// Get the allowedRestricted for the next layer
 							const deeperAllowedRestricted =
 								this.getDeeperAllowedRestricted(
@@ -804,7 +851,7 @@ export default class DataModule extends BaseModule {
 								containsRestrictedProperties:
 									_containsRestrictedProperties
 							} = await this.parseFindFilter(
-								subFilter,
+								subFilter as MongoFilter,
 								schema[currentKey].item!.schema!,
 								deeperAllowedRestricted,
 								options
@@ -817,8 +864,18 @@ export default class DataModule extends BaseModule {
 						else {
 							// TODO possibly handle if a user gives some weird value here, like an object or array or $ operator
 
+							// Value must not be an array, so if it is, throw an error
+							if (Array.isArray(value)) throw Error("an array");
+
+							// Value must not be an actual object, so if it is, throw an error
+							if (
+								Object.prototype.toString.call(value) ===
+								"[object Object]"
+							)
+								throw Error("an object");
+
 							mongoFilter[currentKey] = this.getCastedValue(
-								value,
+								value as AttributeValue,
 								itemType
 							);
 						}
@@ -838,8 +895,18 @@ export default class DataModule extends BaseModule {
 						else {
 							const schemaType = schema[key].type;
 
+							// Value must not be an array, so if it is, throw an error
+							if (Array.isArray(value)) throw Error("an array");
+
+							// Value must not be an actual object, so if it is, throw an error
+							if (
+								Object.prototype.toString.call(value) ===
+								"[object Object]"
+							)
+								throw Error("an object");
+
 							mongoFilter[key] = this.getCastedValue(
-								value,
+								value as AttributeValue,
 								schemaType
 							);
 						}
@@ -867,14 +934,14 @@ export default class DataModule extends BaseModule {
 		document: Document,
 		schema: SchemaDocument,
 		projection: NormalizedProjection,
-		allowedRestricted: boolean | string[] | null | undefined
+		allowedRestricted: AllowedRestricted
 	): Promise<Document> {
 		// TODO possibly do different things with required properties?
 		// TODO possibly do different things with properties with default?
 
 		const unfilteredEntries = Object.entries(document);
 		// Go through all properties in the document to decide whether to allow it or not, and possibly casts the value to its property type
-		const filteredEntries = [];
+		const filteredEntries: Entries<Document> = [];
 		await Promise.all(
 			unfilteredEntries.map(async ([key, value]) => {
 				// If the property does not exist in the schema, return the memo, so we won't return the key/value in the stripped document
@@ -902,6 +969,13 @@ export default class DataModule extends BaseModule {
 						return;
 					}
 
+					// Value must be an actual object, so if it's not, throw an error
+					if (
+						Object.prototype.toString.call(value) !==
+						"[object Object]"
+					)
+						throw Error("not an object");
+
 					// Get the projection for the next layer
 					const deeperProjection = this.getDeeperProjection(
 						projection,
@@ -913,7 +987,7 @@ export default class DataModule extends BaseModule {
 
 					// Generate a stripped document/object for the current key/value
 					const strippedDocument = await this.stripDocument(
-						value,
+						value as Document, // We can be sure the value is a document, so this is for TypeScript to be happy
 						schema[key].schema!,
 						deeperProjection,
 						deeperAllowedRestricted
@@ -949,7 +1023,7 @@ export default class DataModule extends BaseModule {
 					// The type of the array items
 					const itemType = schema[key].item!.type;
 
-					const items = await Promise.all(
+					const items = (await Promise.all(
 						value.map(async item => {
 							// Handle schema objects inside an array
 							if (itemType === Types.Schema) {
@@ -957,6 +1031,13 @@ export default class DataModule extends BaseModule {
 								// If item is falsy, it can't be an object, so just return null
 								if (!item) return null;
 
+								// Item must be an actual object, so if it's not, throw an error
+								if (
+									Object.prototype.toString.call(item) !==
+									"[object Object]"
+								)
+									throw Error("not an object");
+
 								// Get the projection for the next layer
 								const deeperProjection =
 									this.getDeeperProjection(projection, key);
@@ -970,7 +1051,7 @@ export default class DataModule extends BaseModule {
 								// Generate a stripped document/object for the current key/value
 								const strippedDocument =
 									await this.stripDocument(
-										item,
+										item as Document, // We can be sure the item is a document, so this is for TypeScript to be happy
 										schema[key].item!.schema!,
 										deeperProjection,
 										deeperAllowedRestricted
@@ -1005,7 +1086,7 @@ export default class DataModule extends BaseModule {
 								return castedValue;
 							}
 						})
-					);
+					)) as AttributeValue[] | Document[];
 
 					filteredEntries.push([key, items]);
 					return;
@@ -1054,10 +1135,9 @@ export default class DataModule extends BaseModule {
 			useCache = true
 		}: {
 			collection: CollectionNameType;
-			filter: Record<string, any>;
+			filter: MongoFilter;
 			projection?: Projection;
 			allowedRestricted?: boolean | string[];
-			values?: Record<string, any>;
 			limit?: number;
 			page?: number;
 			useCache?: boolean;

+ 1 - 1
backend/src/types/AttributeValue.ts

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