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

refactor: Load model relations only when required

Owen Diffey 1 год назад
Родитель
Сommit
28ca369686

+ 12 - 3
backend/src/modules/DataModule.ts

@@ -322,6 +322,12 @@ export default class DataModule extends BaseModule {
 				patchEventEmitter.on(event, async ({ doc, oldDoc }) => {
 					const modelId = doc?._id ?? oldDoc?._id;
 
+					const Model = await this.getModel(null, modelName);
+
+					if (doc) doc = Model.hydrate(doc);
+
+					if (oldDoc) oldDoc = Model.hydrate(oldDoc);
+
 					if (!modelId && action !== "created")
 						throw new Error(`Model Id not found for "${event}"`);
 
@@ -397,7 +403,10 @@ export default class DataModule extends BaseModule {
 
 					if (ref)
 						schema.path(key).get(value => {
-							if (value && type instanceof SchemaTypes.ObjectId)
+							if (
+								typeof value === "object" &&
+								type instanceof SchemaTypes.ObjectId
+							)
 								return {
 									_id: value,
 									_name: ref
@@ -411,7 +420,7 @@ export default class DataModule extends BaseModule {
 									item === null
 										? null
 										: {
-												_id: value,
+												_id: item,
 												_name: ref
 										  }
 								);
@@ -458,7 +467,7 @@ export default class DataModule extends BaseModule {
 	 * @returns Model
 	 */
 	public async getModel<ModelName extends keyof Models>(
-		jobContext: JobContext,
+		jobContext?: JobContext,
 		payload: ModelName | { name: ModelName }
 	) {
 		if (!this._models) throw new Error("Models not loaded");

+ 119 - 72
frontend/src/Model.ts

@@ -8,103 +8,150 @@ export default class Model {
 
 	private _uses: number;
 
+	private _loadedRelations: string[];
+
 	constructor(data: object) {
 		this._uses = 0;
+		this._loadedRelations = [];
 
 		Object.assign(this, data);
 	}
 
-	private async _loadRelation(relation: object): Promise<object> {
-		const { findById, registerModels } = useModelStore();
-
-		const data = await findById(relation._name, relation._id);
-
-		const [model] = await registerModels(data);
-
-		return model;
-	}
-
-	private async _loadRelations(model: object): Promise<object> {
-		const relations = Object.fromEntries(
-			await Promise.all(
-				Object.entries(model)
-					.filter(
-						([, value]) =>
-							typeof value === "object" || Array.isArray(value)
-					)
-					.map(async ([key, value]) => {
-						if (
-							typeof value === "object" &&
-							value._id &&
-							value._name
-						)
-							return [key, await this._loadRelation(value)];
-
-						if (Array.isArray(value))
-							return [
-								key,
-								await Promise.all(
-									value.map(async item => {
-										if (typeof item !== "object")
-											return item;
-
-										if (item._id && item._name)
-											return this._loadRelation(item);
-
-										return this._loadRelations(item);
-									})
-								)
-							];
-
-						return [key, await this._loadRelations(value)];
-					})
+	private async _getRelations(
+		model?: object,
+		path?: string
+	): Promise<string[]> {
+		const relationPaths = await Object.entries(model ?? this)
+			.filter(
+				([key, value]) =>
+					!key.startsWith("_") &&
+					(typeof value === "object" || Array.isArray(value))
 			)
-		);
+			.reduce(async (_modelIds, [key, value]) => {
+				const paths = await _modelIds;
 
-		Object.assign(model, relations);
+				path = path ? `${path}.${key}` : key;
 
-		return model;
-	}
-
-	public async loadRelations(): Promise<void> {
-		await this._loadRelations(this);
-	}
-
-	private async _getRelationIds(model: object): Promise<string[]> {
-		const relationIds = await Object.values(model)
-			.filter(value => typeof value === "object" || Array.isArray(value))
-			.reduce(async (_modelIds, value) => {
-				const modelIds = await _modelIds;
-
-				if (typeof value === "object" && value._id)
-					modelIds.push(value._id);
+				if (typeof value === "object" && value._id) paths.push(path);
 				else if (Array.isArray(value))
 					await Promise.all(
 						value.map(async item => {
 							if (typeof item !== "object") return;
 
-							if (item._id) modelIds.push(item._id);
+							if (item._id) paths.push(path);
 							else
-								modelIds.push(
-									...(await this._getRelationIds(item))
+								paths.push(
+									...(await this._getRelations(item, path))
 								);
 						})
 					);
-				else modelIds.push(...(await this._getRelationIds(value)));
+				else paths.push(...(await this._getRelations(value, path)));
 
-				return modelIds;
+				return paths;
 			}, Promise.resolve([]));
 
-		return relationIds.filter(
-			(relationId, index) => relationIds.indexOf(relationId) === index
+		return relationPaths.filter(
+			(relationPath, index) =>
+				relationPaths.indexOf(relationPath) === index
 		);
 	}
 
-	public async unregisterRelations(): Promise<void> {
-		const { unregisterModels } = useModelStore();
+	private async _getRelation(key: string) {
+		let relation = JSON.parse(JSON.stringify(this));
+		key.split(".").forEach(property => {
+			if (Number.isInteger(property))
+				property = Number.parseInt(property);
+
+			relation = relation[property];
+		});
+		return relation;
+	}
+
+	private async _loadRelation(
+		model: object,
+		path: string,
+		force: boolean,
+		pathParts?: string[]
+	): Promise<void> {
+		let [head, ...rest] = path.split(".");
+		let [next] = rest;
+
+		if (Number.isInteger(head)) head = Number.parseInt(head);
+
+		if (Number.isInteger(next)) next = Number.parseInt(next);
+
+		pathParts ??= [];
 
-		const relationIds = await this._getRelationIds(this);
+		pathParts.push(head);
 
+		if (Array.isArray(model[head])) {
+			await Promise.all(
+				model[head].map(async (item, index) => {
+					let itemPath = `${index}`;
+
+					if (rest.length > 0) itemPath += `.${rest.join(".")}`;
+
+					await this._loadRelation(model[head], itemPath, force, [
+						...pathParts
+					]);
+				})
+			);
+
+			return;
+		}
+
+		if (rest.length > 0 && model[next] === null) {
+			await this._loadRelation(
+				model[head],
+				rest.join("."),
+				force,
+				pathParts
+			);
+
+			return;
+		}
+
+		const fullPath = pathParts.join(".");
+
+		if (force || !this._loadedRelations.includes(fullPath)) {
+			const { findById, registerModels } = useModelStore();
+
+			const data = await findById(model[head]._name, model[head]._id);
+
+			const [registeredModel] = await registerModels(data);
+
+			model[head] = registeredModel;
+
+			this._loadedRelations.push(fullPath);
+		}
+
+		if (rest.length === 0) return;
+
+		await model[head].loadRelations(rest.join("."));
+	}
+
+	public async loadRelations(
+		relations?: string | string[],
+		force = false
+	): Promise<void> {
+		if (relations)
+			relations = Array.isArray(relations) ? relations : [relations];
+
+		await Promise.all(
+			(relations ?? []).map(async path => {
+				await this._loadRelation(this, path, force);
+			})
+		);
+	}
+
+	public async unregisterRelations(): Promise<void> {
+		const { unregisterModels } = useModelStore();
+		const relationIds = await Promise.all(
+			this._loadedRelations.map(async path => {
+				const relation = await this._getRelation(path);
+				return relation._id;
+			})
+		);
 		await unregisterModels(relationIds);
 	}
 
@@ -113,7 +160,7 @@ export default class Model {
 
 		Object.assign(this, data);
 
-		await this.loadRelations();
+		await this.loadRelations(this._loadedRelations, true);
 
 		await this.refreshPermissions();
 	}

+ 1 - 1
frontend/src/components/modals/EditNews.vue

@@ -120,7 +120,7 @@ onMounted(async () => {
 
 			if (!data) return;
 
-			const [model] = await registerModels(data);
+			const [model] = await registerModels(data, { news: "createdBy" });
 
 			setModelValues(model, ["markdown", "status", "showToNewUsers"]);
 

+ 1 - 1
frontend/src/components/modals/WhatIsNew.vue

@@ -48,7 +48,7 @@ onMounted(async () => {
 
 	localStorage.setItem("whatIsNew", Date.parse(model.createdAt).toString());
 
-	const [_model] = await registerModels(model);
+	const [_model] = await registerModels(model, { news: "createdBy" });
 
 	news.value = _model;
 

+ 8 - 2
frontend/src/composables/useModels.ts

@@ -66,8 +66,14 @@ export const useModels = () => {
 		delete subscriptions.value[type][modelName][uuid];
 	};
 
-	const registerModels = async (storeModels: any[]) => {
-		const registeredModels = await modelStore.registerModels(storeModels);
+	const registerModels = async (
+		storeModels: any[],
+		relations?: Record<string, string | string[]>
+	) => {
+		const registeredModels = await modelStore.registerModels(
+			storeModels,
+			relations
+		);
 
 		models.value.push(...registeredModels);
 

+ 4 - 1
frontend/src/pages/News.vue

@@ -43,7 +43,10 @@ onMounted(async () => {
 	});
 
 	await onReady(async () => {
-		news.value = await registerModels(await runJob("data.news.newest", {}));
+		news.value = await registerModels(
+			await runJob("data.news.newest", {}),
+			{ news: "createdBy" }
+		);
 	});
 
 	await onCreated("news", async ({ doc }) => {

+ 6 - 2
frontend/src/stores/model.ts

@@ -139,7 +139,10 @@ export const useModelStore = defineStore("model", () => {
 		}
 	};
 
-	const registerModels = async docs =>
+	const registerModels = async (
+		docs,
+		relations?: Record<string, string | string[]>
+	) =>
 		Promise.all(
 			(Array.isArray(docs) ? docs : [docs]).map(async _doc => {
 				const existingRef = models.value.find(
@@ -152,7 +155,8 @@ export const useModelStore = defineStore("model", () => {
 
 				if (existingRef) return docRef;
 
-				await docRef.loadRelations();
+				if (relations && relations[docRef._name])
+					await docRef.loadRelations(relations[docRef._name]);
 
 				models.value.push(docRef);