Browse Source

feat: Frontend model relation handling

Owen Diffey 1 year ago
parent
commit
69a8f3f30e

+ 1 - 0
backend/src/models/schemas/news/schema.ts

@@ -80,6 +80,7 @@ export const schema = new Schema<NewsSchema, NewsModel, {}, NewsQueryHelpers>(
 		},
 		createdBy: {
 			type: SchemaTypes.ObjectId,
+			ref: "users",
 			required: true
 		}
 	},

+ 2 - 1
backend/src/models/schemas/stations/schema.ts

@@ -82,9 +82,10 @@ export const schema = new Schema<StationSchema, StationModel>(
 		},
 		owner: {
 			type: SchemaTypes.ObjectId,
+			ref: "users",
 			required: false
 		},
-		djs: [{ type: SchemaTypes.ObjectId }],
+		djs: [{ type: SchemaTypes.ObjectId, ref: "users" }],
 		currentSong: {
 			type: SchemaTypes.ObjectId,
 			required: false

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

@@ -6,6 +6,7 @@ import mongoose, {
 	MongooseDefaultQueryMiddleware,
 	MongooseDistinctQueryMiddleware,
 	MongooseQueryOrDocumentMiddleware,
+	SchemaTypes,
 	Types
 } from "mongoose";
 import { patchHistoryPlugin, patchEventEmitter } from "ts-patch-mongoose";
@@ -375,6 +376,23 @@ export default class DataModule extends BaseModule {
 
 		await this._registerEvents(modelName, schema);
 
+		schema.set("toObject", { virtuals: true });
+		schema.set("toJSON", { virtuals: true });
+
+		const relations = Object.fromEntries(
+			Object.entries(schema.paths)
+				.filter(([, type]) => type instanceof SchemaTypes.ObjectId)
+				.map(([key, type]) => [
+					key,
+					{
+						model: type.options.ref
+					}
+				])
+		);
+
+		if (Object.keys(relations).length > 0)
+			schema.virtual("_relations").get(() => relations);
+
 		return this._mongoConnection.model(modelName.toString(), schema);
 	}
 

+ 42 - 2
frontend/src/Model.ts

@@ -1,7 +1,6 @@
+import { useModelStore } from "./stores/model";
 import { useWebsocketStore } from "./stores/websocket";
 
-const { runJob } = useWebsocketStore();
-
 export default class Model {
 	private _name: string;
 
@@ -18,6 +17,41 @@ export default class Model {
 		Object.assign(this, data);
 	}
 
+	public async loadRelations(): Promise<void> {
+		if (!this._relations) return;
+
+		const { findById, registerModels } = useModelStore();
+
+		await Promise.all(
+			Object.entries(this._relations).map(
+				async ([key, { model: modelName }]) => {
+					const data = await findById(modelName, this[key]);
+
+					const [model] = await registerModels(modelName, data);
+
+					this[key] = model;
+				}
+			)
+		);
+	}
+
+	public async unloadRelations(): Promise<void> {
+		if (!this._relations) return;
+
+		const { unregisterModels } = useModelStore();
+
+		const relationIds = Object.fromEntries(
+			Object.entries(this._relations).map(([key, value]) => [
+				this[key]._id,
+				value
+			])
+		);
+
+		await unregisterModels(Object.values(relationIds));
+
+		Object.apply(this, relationIds);
+	}
+
 	public getName(): string {
 		return this._name;
 	}
@@ -25,6 +59,8 @@ export default class Model {
 	public async getPermissions(refresh = false): Promise<object> {
 		if (refresh === false && this._permissions) return this._permissions;
 
+		const { runJob } = useWebsocketStore();
+
 		this._permissions = await runJob("api.getUserModelPermissions", {
 			modelName: this._name,
 			modelId: this._id
@@ -74,6 +110,8 @@ export default class Model {
 	}
 
 	public async update(query: object) {
+		const { runJob } = useWebsocketStore();
+
 		return runJob(`data.${this.getName()}.updateById`, {
 			_id: this._id,
 			query
@@ -81,6 +119,8 @@ export default class Model {
 	}
 
 	public async delete() {
+		const { runJob } = useWebsocketStore();
+
 		return runJob(`data.${this.getName()}.deleteById`, { _id: this._id });
 	}
 }

+ 7 - 8
frontend/src/components/modals/EditNews.vue

@@ -15,9 +15,6 @@ const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SaveButton = defineAsyncComponent(
 	() => import("@/components/SaveButton.vue")
 );
-const UserLink = defineAsyncComponent(
-	() => import("@/components/UserLink.vue")
-);
 
 const props = defineProps({
 	modalUuid: { type: String, required: true },
@@ -201,11 +198,13 @@ onMounted(async () => {
 				/>
 				<div class="right" v-if="createdAt > 0">
 					<span>
-						By
-						<user-link
-							:user-id="createdBy"
-							:alt="createdBy"
-						/> </span
+						By&nbsp;
+						<router-link
+							:to="{ path: `/u/${createdBy.username}` }"
+							:title="createdBy._id"
+						>
+							{{ createdBy.name }}
+						</router-link></span
 					>&nbsp;<span :title="new Date(createdAt).toString()">
 						{{
 							formatDistance(createdAt, new Date(), {

+ 9 - 8
frontend/src/components/modals/WhatIsNew.vue

@@ -8,9 +8,6 @@ import { useModalsStore } from "@/stores/modals";
 import { useWebsocketStore } from "@/stores/websocket";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
-const UserLink = defineAsyncComponent(
-	() => import("@/components/UserLink.vue")
-);
 
 defineProps({
 	modalUuid: { type: String, required: true }
@@ -84,11 +81,15 @@ const { sanitize } = dompurify;
 		</template>
 		<template #footer>
 			<span v-if="news.createdBy">
-				By
-				<user-link
-					:user-id="news.createdBy"
-					:alt="news.createdBy" /></span
-			>&nbsp;<span :title="new Date(news.createdAt).toString()">
+				By&nbsp;
+				<router-link
+					:to="{ path: `/u/${news.createdBy.username}` }"
+					:title="news.createdBy._id"
+				>
+					{{ news.createdBy.name }}
+				</router-link> </span
+			>&nbsp;
+			<span :title="new Date(news.createdAt).toString()">
 				{{
 					formatDistance(new Date(news.createdAt), new Date(), {
 						addSuffix: true

+ 7 - 9
frontend/src/pages/News.vue

@@ -21,9 +21,6 @@ const MainHeader = defineAsyncComponent(
 const MainFooter = defineAsyncComponent(
 	() => import("@/components/MainFooter.vue")
 );
-const UserLink = defineAsyncComponent(
-	() => import("@/components/UserLink.vue")
-);
 
 const { runJob } = useWebsocketStore();
 const { onReady } = useEvents();
@@ -82,13 +79,14 @@ onMounted(async () => {
 					<div v-html="sanitize(marked(item.markdown))"></div>
 					<div class="info">
 						<hr />
-						By
-						<user-link
-							:user-id="item.createdBy"
-							:alt="item.createdBy"
-						/>&nbsp;<span
-							:title="new Date(item.createdAt).toString()"
+						By&nbsp;
+						<router-link
+							:to="{ path: `/u/${item.createdBy.username}` }"
+							:title="item.createdBy._id"
 						>
+							{{ item.createdBy.name }} </router-link
+						>&nbsp;
+						<span :title="new Date(item.createdAt).toString()">
 							{{
 								formatDistance(
 									new Date(item.createdAt),

+ 4 - 0
frontend/src/stores/model.ts

@@ -155,6 +155,8 @@ export const useModelStore = defineStore("model", () => {
 
 				if (existingRef) return docRef;
 
+				await docRef.loadRelations();
+
 				models.value.push(docRef);
 
 				const updatedUuid = await subscribe(
@@ -187,6 +189,8 @@ export const useModelStore = defineStore("model", () => {
 						return;
 					}
 
+					await model.unloadRelations();
+
 					const { updated, deleted } = model.getSubscriptions() ?? {};
 
 					if (updated)