瀏覽代碼

refactor: Include model name in object and references to replace _relations

Owen Diffey 1 年之前
父節點
當前提交
209beef162

+ 41 - 13
backend/src/modules/DataModule.ts

@@ -376,22 +376,50 @@ export default class DataModule extends BaseModule {
 
 		await this._registerEvents(modelName, schema);
 
-		schema.set("toObject", { virtuals: true });
-		schema.set("toJSON", { virtuals: true });
+		schema.set("toObject", { getters: true, virtuals: true });
+		schema.set("toJSON", { getters: true, virtuals: true });
 
-		const relations = Object.fromEntries(
+		schema.virtual("_name").get(() => modelName);
+
+		await Promise.all(
 			Object.entries(schema.paths)
-				.filter(([, type]) => type instanceof SchemaTypes.ObjectId)
-				.map(([key, type]) => [
-					key,
-					{
-						model: type.options.ref
-					}
-				])
-		);
+				.filter(
+					([, type]) =>
+						type instanceof SchemaTypes.ObjectId ||
+						(type instanceof SchemaTypes.Array &&
+							type.caster instanceof SchemaTypes.ObjectId)
+				)
+				.map(async ([key, type]) => {
+					const { ref } =
+						(type instanceof SchemaTypes.ObjectId
+							? type?.options
+							: type.caster?.options) ?? {};
+
+					if (ref)
+						schema.path(key).get(value => {
+							if (value && type instanceof SchemaTypes.ObjectId)
+								return {
+									_id: value,
+									_name: ref
+								};
+
+							if (
+								Array.isArray(value) &&
+								type instanceof SchemaTypes.Array
+							)
+								return value.map(item =>
+									item === null
+										? null
+										: {
+												_id: value,
+												_name: ref
+										  }
+								);
 
-		if (Object.keys(relations).length > 0)
-			schema.virtual("_relations").get(() => relations);
+							return value;
+						});
+				})
+		);
 
 		return this._mongoConnection.model(modelName.toString(), schema);
 	}

+ 90 - 24
frontend/src/Model.ts

@@ -2,54 +2,120 @@ import { useModelStore } from "./stores/model";
 import { useWebsocketStore } from "./stores/websocket";
 
 export default class Model {
-	private _name: string;
-
 	private _permissions?: object;
 
 	private _subscriptions?: { updated: string; deleted: string };
 
 	private _uses: number;
 
-	constructor(name: string, data: object) {
-		this._name = name;
+	constructor(data: object) {
 		this._uses = 0;
 
 		Object.assign(this, data);
 	}
 
-	public async loadRelations(): Promise<void> {
-		if (!this._relations) return;
-
+	private async _loadRelation(relation: object): Promise<object> {
 		const { findById, registerModels } = useModelStore();
 
-		await Promise.all(
-			Object.entries(this._relations).map(
-				async ([key, { model: modelName }]) => {
-					const data = await findById(modelName, this[key]);
+		const data = await findById(relation._name, relation._id);
 
-					const [model] = await registerModels(modelName, data);
+		const [model] = await registerModels(data);
+
+		return model;
+	}
 
-					this[key] = 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)];
+					})
 			)
 		);
+
+		Object.assign(model, relations);
+
+		return model;
+	}
+
+	public async loadRelations(): Promise<void> {
+		await this._loadRelations(this);
 	}
 
-	public async unloadRelations(): Promise<void> {
-		if (!this._relations) return;
+	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);
+				else if (Array.isArray(value))
+					await Promise.all(
+						value.map(async item => {
+							if (typeof item !== "object") return;
+
+							if (item._id) modelIds.push(item._id);
+							else
+								modelIds.push(
+									...(await this._getRelationIds(item))
+								);
+						})
+					);
+				else modelIds.push(...(await this._getRelationIds(value)));
+
+				return modelIds;
+			}, Promise.resolve([]));
+
+		return relationIds.filter(
+			(relationId, index) => relationIds.indexOf(relationId) === index
+		);
+	}
 
+	public async unregisterRelations(): Promise<void> {
 		const { unregisterModels } = useModelStore();
 
-		const relationIds = Object.fromEntries(
-			Object.entries(this._relations).map(([key, value]) => [
-				this[key]._id,
-				value
-			])
-		);
+		const relationIds = await this._getRelationIds(this);
+
+		await unregisterModels(relationIds);
+	}
+
+	public async updateData(data: object) {
+		await this.unregisterRelations();
+
+		Object.assign(this, data);
 
-		await unregisterModels(Object.values(relationIds));
+		await this.loadRelations();
 
-		Object.apply(this, relationIds);
+		await this.refreshPermissions();
 	}
 
 	public getName(): string {

+ 5 - 9
frontend/src/components/AdvancedTable.vue

@@ -214,19 +214,15 @@ const hasCheckboxes = computed(
 const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
 
 const onUpdatedCallback = ({ doc }) => {
-	const docRowIndex = rows.value.findIndex(_row => _row._id === doc._id);
+	const docRow = rows.value.find(_row => _row._id === doc._id);
 
-	if (docRowIndex === -1) return;
-
-	Object.assign(rows.value[docRowIndex], doc, { updated: true });
+	if (docRow) docRow.updateData({ ...doc, updated: true });
 };
 
 const onDeletedCallback = ({ oldDoc }) => {
-	const docRowIndex = rows.value.findIndex(_row => _row._id === oldDoc._id);
-
-	if (docRowIndex === -1) return;
+	const docRow = rows.value.find(_row => _row._id === oldDoc._id);
 
-	Object.assign(rows.value[docRowIndex], { selected: false, removed: true });
+	Object.assign(docRow, { selected: false, removed: true });
 };
 
 const unsubscribe = async (_subscriptions?) => {
@@ -305,7 +301,7 @@ const getData = async () => {
 
 	rows.value = data.data.map(
 		row =>
-			new Model(props.model, {
+			new Model({
 				...row,
 				selected: false
 			})

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

@@ -120,7 +120,7 @@ onMounted(async () => {
 
 			if (!data) return;
 
-			const [model] = await registerModels("news", data);
+			const [model] = await registerModels(data);
 
 			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("news", model);
+	const [_model] = await registerModels(model);
 
 	news.value = _model;
 

+ 32 - 16
frontend/src/composables/useModels.ts

@@ -10,6 +10,7 @@ export const useModels = () => {
 		updated: {},
 		deleted: {}
 	});
+	const deletedSubscriptions = ref({});
 
 	const onCreated = async (
 		modelName: string,
@@ -19,6 +20,8 @@ export const useModels = () => {
 
 		subscriptions.value.created[modelName] ??= [];
 		subscriptions.value.created[modelName].push(uuid);
+
+		return uuid;
 	};
 
 	const onUpdated = async (
@@ -29,6 +32,8 @@ export const useModels = () => {
 
 		subscriptions.value.updated[modelName] ??= [];
 		subscriptions.value.updated[modelName].push(uuid);
+
+		return uuid;
 	};
 
 	const onDeleted = async (
@@ -39,6 +44,8 @@ export const useModels = () => {
 
 		subscriptions.value.deleted[modelName] ??= [];
 		subscriptions.value.deleted[modelName].push(uuid);
+
+		return uuid;
 	};
 
 	const removeCallback = async (
@@ -59,25 +66,33 @@ export const useModels = () => {
 		delete subscriptions.value[type][modelName][uuid];
 	};
 
-	const registerModels = async (modelName: string, storeModels: any[]) => {
-		const registeredModels = await modelStore.registerModels(
-			modelName,
-			storeModels
-		);
+	const registerModels = async (storeModels: any[]) => {
+		const registeredModels = await modelStore.registerModels(storeModels);
 
 		models.value.push(...registeredModels);
 
-		await onDeleted(modelName, ({ oldDoc }) => {
-			if (!models.value[modelName]) return;
-
-			const modelIndex = models.value[modelName].findIndex(
-				model => model._id === oldDoc._id
-			);
-
-			if (modelIndex < 0) return;
-
-			delete models.value[modelName][modelIndex];
-		});
+		await Promise.all(
+			registeredModels
+				.filter(
+					(model, index) =>
+						!deletedSubscriptions.value[model._name] &&
+						registeredModels.findIndex(
+							storeModel => storeModel._name === model._name
+						) === index
+				)
+				.map(async registeredModel => {
+					deletedSubscriptions.value[registeredModel._name] =
+						await onDeleted(registeredModel._name, ({ oldDoc }) => {
+							const modelIndex = models.value.findIndex(
+								model => model._id === oldDoc._id
+							);
+
+							if (modelIndex < 0) return;
+
+							delete models.value[modelIndex];
+						});
+				})
+		);
 
 		return registeredModels;
 	};
@@ -112,6 +127,7 @@ export const useModels = () => {
 	return {
 		models,
 		subscriptions,
+		deletedSubscriptions,
 		onCreated,
 		onUpdated,
 		onDeleted,

+ 2 - 5
frontend/src/pages/News.vue

@@ -43,14 +43,11 @@ onMounted(async () => {
 	});
 
 	await onReady(async () => {
-		news.value = await registerModels(
-			"news",
-			await runJob("data.news.newest", {})
-		);
+		news.value = await registerModels(await runJob("data.news.newest", {}));
 	});
 
 	await onCreated("news", async ({ doc }) => {
-		const [newDoc] = await registerModels("news", [doc]);
+		const [newDoc] = await registerModels(doc);
 		news.value.unshift(newDoc);
 	});
 

+ 10 - 13
frontend/src/stores/model.ts

@@ -75,10 +75,8 @@ export const useModelStore = defineStore("model", () => {
 	};
 
 	const onUpdatedCallback = async (modelName: string, { doc }) => {
-		const index = models.value.findIndex(model => model._id === doc._id);
-		if (index > -1) Object.assign(models.value[index], doc);
-
-		models.value[index].refreshPermissions();
+		const model = models.value.find(model => model._id === doc._id);
+		if (model) model.updateData(doc);
 
 		if (!subscriptions.value.updated[modelName]) return;
 
@@ -141,15 +139,14 @@ export const useModelStore = defineStore("model", () => {
 		}
 	};
 
-	const registerModels = async (modelName: string, docs) =>
+	const registerModels = async docs =>
 		Promise.all(
 			(Array.isArray(docs) ? docs : [docs]).map(async _doc => {
 				const existingRef = models.value.find(
 					model => model._id === _doc._id
 				);
 
-				const docRef =
-					existingRef ?? reactive(new Model(modelName, _doc));
+				const docRef = existingRef ?? reactive(new Model(_doc));
 
 				docRef.addUse();
 
@@ -160,13 +157,13 @@ export const useModelStore = defineStore("model", () => {
 				models.value.push(docRef);
 
 				const updatedUuid = await subscribe(
-					`model.${modelName}.updated.${_doc._id}`,
-					data => onUpdatedCallback(modelName, data)
+					`model.${docRef._name}.updated.${_doc._id}`,
+					data => onUpdatedCallback(docRef._name, data)
 				);
 
 				const deletedUuid = await subscribe(
-					`model.${modelName}.deleted.${_doc._id}`,
-					data => onDeletedCallback(modelName, data)
+					`model.${docRef._name}.deleted.${_doc._id}`,
+					data => onDeletedCallback(docRef._name, data)
 				);
 
 				docRef.setSubscriptions(updatedUuid, deletedUuid);
@@ -184,12 +181,12 @@ export const useModelStore = defineStore("model", () => {
 					);
 
 					if (!model || model.getUses() > 1) {
-						model.removeUse();
+						model?.removeUse();
 
 						return;
 					}
 
-					await model.unloadRelations();
+					await model.unregisterRelations();
 
 					const { updated, deleted } = model.getSubscriptions() ?? {};