Quellcode durchsuchen

refactor: Add frontend model class and make store generic

Owen Diffey vor 1 Jahr
Ursprung
Commit
f2625e650a

+ 86 - 0
frontend/src/Model.ts

@@ -0,0 +1,86 @@
+import { useWebsocketStore } from "./stores/websocket";
+
+const { runJob } = useWebsocketStore();
+
+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;
+		this._uses = 0;
+
+		Object.assign(this, data);
+	}
+
+	public getName(): string {
+		return this._name;
+	}
+
+	public async getPermissions(refresh = false): Promise<object> {
+		if (refresh === false && this._permissions) return this._permissions;
+
+		this._permissions = await runJob("api.getUserModelPermissions", {
+			modelName: this._name,
+			modelId: this._id
+		});
+
+		return this._permissions;
+	}
+
+	public async refreshPermissions(): Promise<void> {
+		if (this._permissions) this.getPermissions(true);
+	}
+
+	public async hasPermission(permission: string): Promise<boolean> {
+		const permissions = await this.getPermissions();
+
+		return !!permissions[permission];
+	}
+
+	public getSubscriptions() {
+		return this._subscriptions;
+	}
+
+	public setSubscriptions(updated: string, deleted: string): void {
+		this._subscriptions = { updated, deleted };
+	}
+
+	public getUses(): number {
+		return this._uses;
+	}
+
+	public addUse(): void {
+		this._uses += 1;
+	}
+
+	public removeUse(): void {
+		this._uses -= 1;
+	}
+
+	public toJSON(): object {
+		return Object.fromEntries(
+			Object.entries(this).filter(
+				([key, value]) =>
+					(!key.startsWith("_") || key === "_id") &&
+					typeof value !== "function"
+			)
+		);
+	}
+
+	public async update(query: object) {
+		return runJob(`data.${this.getName()}.updateById`, {
+			_id: this._id,
+			query
+		});
+	}
+
+	public async delete() {
+		return runJob(`data.${this.getName()}.deleteById`, { _id: this._id });
+	}
+}

+ 12 - 18
frontend/src/components/AdvancedTable.vue

@@ -24,6 +24,7 @@ import {
 	TableBulkActions
 	TableBulkActions
 } from "@/types/advancedTable";
 } from "@/types/advancedTable";
 import { useEvents } from "@/composables/useEvents";
 import { useEvents } from "@/composables/useEvents";
+import Model from "@/Model";
 
 
 const { dragBox, setInitialBox, onDragBox, resetBoxPosition } = useDragBox();
 const { dragBox, setInitialBox, onDragBox, resetBoxPosition } = useDragBox();
 
 
@@ -213,29 +214,19 @@ const hasCheckboxes = computed(
 const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
 const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
 
 
 const onUpdatedCallback = ({ doc }) => {
 const onUpdatedCallback = ({ doc }) => {
-	const docRow = rows.value.find(_row => _row._id === doc._id);
 	const docRowIndex = rows.value.findIndex(_row => _row._id === doc._id);
 	const docRowIndex = rows.value.findIndex(_row => _row._id === doc._id);
 
 
-	if (!docRow) return;
+	if (docRowIndex === -1) return;
 
 
-	rows.value[docRowIndex] = {
-		...docRow,
-		...doc,
-		updated: true
-	};
+	Object.assign(rows.value[docRowIndex], doc, { updated: true });
 };
 };
 
 
 const onDeletedCallback = ({ oldDoc }) => {
 const onDeletedCallback = ({ oldDoc }) => {
-	const docRow = rows.value.find(_row => _row._id === oldDoc._id);
 	const docRowIndex = rows.value.findIndex(_row => _row._id === oldDoc._id);
 	const docRowIndex = rows.value.findIndex(_row => _row._id === oldDoc._id);
 
 
-	if (!docRow) return;
+	if (docRowIndex === -1) return;
 
 
-	rows.value[docRowIndex] = {
-		...docRow,
-		selected: false,
-		removed: true
-	};
+	Object.assign(rows.value[docRowIndex], { selected: false, removed: true });
 };
 };
 
 
 const unsubscribe = async (_subscriptions?) => {
 const unsubscribe = async (_subscriptions?) => {
@@ -312,10 +303,13 @@ const getData = async () => {
 		operator: appliedFilterOperator.value
 		operator: appliedFilterOperator.value
 	});
 	});
 
 
-	rows.value = data.data.map(row => ({
-		...row,
-		selected: false
-	}));
+	rows.value = data.data.map(
+		row =>
+			new Model(props.model, {
+				...row,
+				selected: false
+			})
+	);
 	count.value = data.count;
 	count.value = data.count;
 
 
 	return subscribe();
 	return subscribe();

+ 11 - 14
frontend/src/components/modals/EditNews.vue

@@ -5,10 +5,11 @@ import DOMPurify from "dompurify";
 import Toast from "toasters";
 import Toast from "toasters";
 import { formatDistance } from "date-fns";
 import { formatDistance } from "date-fns";
 import { useModalsStore } from "@/stores/modals";
 import { useModalsStore } from "@/stores/modals";
-import { useNewsModelStore } from "@/stores/models/news";
 import { useForm } from "@/composables/useForm";
 import { useForm } from "@/composables/useForm";
 import { useEvents } from "@/composables/useEvents";
 import { useEvents } from "@/composables/useEvents";
 import { useModels } from "@/composables/useModels";
 import { useModels } from "@/composables/useModels";
+import { useWebsocketStore } from "@/stores/websocket";
+import { useModelStore } from "@/stores/model";
 
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SaveButton = defineAsyncComponent(
 const SaveButton = defineAsyncComponent(
@@ -25,15 +26,16 @@ const props = defineProps({
 	sector: { type: String, default: "admin" }
 	sector: { type: String, default: "admin" }
 });
 });
 
 
+const { runJob } = useWebsocketStore();
+
+const { findById } = useModelStore();
+
 const { onReady } = useEvents();
 const { onReady } = useEvents();
 
 
 const { closeCurrentModal } = useModalsStore();
 const { closeCurrentModal } = useModalsStore();
 
 
 const { registerModels, onDeleted } = useModels();
 const { registerModels, onDeleted } = useModels();
 
 
-const newsStore = useNewsModelStore();
-const { create, findById, updateById, hasPermission } = newsStore;
-
 const createdBy = ref();
 const createdBy = ref();
 const createdAt = ref(0);
 const createdAt = ref(0);
 
 
@@ -83,8 +85,8 @@ const { inputs, save, setModelValues } = useForm(
 			};
 			};
 
 
 			const method = props.createNews
 			const method = props.createNews
-				? create(query)
-				: updateById(props.newsId, query);
+				? runJob(`data.news.create`, { query })
+				: runJob(`data.news.updateById`, { _id: props.newsId, query });
 			method.then(resolve).catch(reject);
 			method.then(resolve).catch(reject);
 		} else {
 		} else {
 			if (status === "unchanged") new Toast(messages.unchanged);
 			if (status === "unchanged") new Toast(messages.unchanged);
@@ -114,32 +116,27 @@ onMounted(async () => {
 
 
 	await onReady(async () => {
 	await onReady(async () => {
 		if (props.newsId && !props.createNews) {
 		if (props.newsId && !props.createNews) {
-			const data = await findById(props.newsId).catch(() => {
+			const data = await findById("news", props.newsId).catch(() => {
 				new Toast("News with that ID not found.");
 				new Toast("News with that ID not found.");
 				closeCurrentModal();
 				closeCurrentModal();
 			});
 			});
 
 
 			if (!data) return;
 			if (!data) return;
 
 
-			const [model] = await registerModels(newsStore, data);
+			const [model] = await registerModels("news", data);
 
 
 			setModelValues(model, ["markdown", "status", "showToNewUsers"]);
 			setModelValues(model, ["markdown", "status", "showToNewUsers"]);
 
 
 			createdBy.value = model.createdBy;
 			createdBy.value = model.createdBy;
 			createdAt.value = model.createdAt;
 			createdAt.value = model.createdAt;
 
 
-			await onDeleted(newsStore, ({ oldDoc }) => {
+			await onDeleted("news", ({ oldDoc }) => {
 				if (oldDoc._id !== props.newsId) return;
 				if (oldDoc._id !== props.newsId) return;
 
 
 				new Toast("News item has been deleted.");
 				new Toast("News item has been deleted.");
 				closeCurrentModal();
 				closeCurrentModal();
 			});
 			});
 		}
 		}
-
-		console.log(
-			43534543,
-			await hasPermission("data.news.published", props.newsId)
-		);
 	});
 	});
 });
 });
 </script>
 </script>

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

@@ -3,9 +3,9 @@ import { defineAsyncComponent, onMounted, ref } from "vue";
 import { formatDistance } from "date-fns";
 import { formatDistance } from "date-fns";
 import { marked } from "marked";
 import { marked } from "marked";
 import dompurify from "dompurify";
 import dompurify from "dompurify";
-import { useNewsModelStore } from "@/stores/models/news";
 import { useModels } from "@/composables/useModels";
 import { useModels } from "@/composables/useModels";
 import { useModalsStore } from "@/stores/modals";
 import { useModalsStore } from "@/stores/modals";
+import { useWebsocketStore } from "@/stores/websocket";
 
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const UserLink = defineAsyncComponent(
 const UserLink = defineAsyncComponent(
@@ -16,9 +16,9 @@ defineProps({
 	modalUuid: { type: String, required: true }
 	modalUuid: { type: String, required: true }
 });
 });
 
 
-const { registerModels, onDeleted } = useModels();
+const { runJob } = useWebsocketStore();
 
 
-const newsStore = useNewsModelStore();
+const { registerModels, onDeleted } = useModels();
 
 
 const { closeCurrentModal } = useModalsStore();
 const { closeCurrentModal } = useModalsStore();
 
 
@@ -29,7 +29,10 @@ onMounted(async () => {
 
 
 	const newUser = !firstVisited;
 	const newUser = !firstVisited;
 
 
-	const [model] = await newsStore.newest(newUser, 1);
+	const [model] = await runJob("data.news.newest", {
+		showToNewUsers: newUser,
+		limit: 1
+	});
 
 
 	if (model && newUser) {
 	if (model && newUser) {
 		firstVisited = Date.now().toString();
 		firstVisited = Date.now().toString();
@@ -48,11 +51,11 @@ onMounted(async () => {
 
 
 	localStorage.setItem("whatIsNew", Date.parse(model.createdAt).toString());
 	localStorage.setItem("whatIsNew", Date.parse(model.createdAt).toString());
 
 
-	const [_model] = await registerModels(newsStore, model);
+	const [_model] = await registerModels("news", model);
 
 
 	news.value = _model;
 	news.value = _model;
 
 
-	await onDeleted(newsStore, ({ oldDoc }) => {
+	await onDeleted("news", ({ oldDoc }) => {
 		if (oldDoc._id === news.value?._id) closeCurrentModal(true);
 		if (oldDoc._id === news.value?._id) closeCurrentModal(true);
 	});
 	});
 
 

+ 58 - 81
frontend/src/composables/useModels.ts

@@ -1,135 +1,112 @@
 import { onBeforeUnmount, ref } from "vue";
 import { onBeforeUnmount, ref } from "vue";
+import { useModelStore } from "@/stores/model";
 
 
 export const useModels = () => {
 export const useModels = () => {
+	const modelStore = useModelStore();
+
 	const models = ref([]);
 	const models = ref([]);
 	const subscriptions = ref({
 	const subscriptions = ref({
-		created: [],
-		updated: [],
-		deleted: []
+		created: {},
+		updated: {},
+		deleted: {}
 	});
 	});
 
 
-	const onCreated = async (store, callback: (data?: any) => any) => {
-		const uuid = await store.onCreated(callback);
+	const onCreated = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		const uuid = await modelStore.onCreated(modelName, callback);
 
 
-		subscriptions.value.created.push({
-			store,
-			uuid
-		});
+		subscriptions.value.created[modelName] ??= [];
+		subscriptions.value.created[modelName].push(uuid);
 	};
 	};
 
 
-	const onUpdated = async (store, callback: (data?: any) => any) => {
-		const uuid = await store.onUpdated(callback);
+	const onUpdated = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		const uuid = await modelStore.onUpdated(modelName, callback);
 
 
-		subscriptions.value.updated.push({
-			store,
-			uuid
-		});
+		subscriptions.value.updated[modelName] ??= [];
+		subscriptions.value.updated[modelName].push(uuid);
 	};
 	};
 
 
-	const onDeleted = async (store, callback: (data?: any) => any) => {
-		const uuid = await store.onDeleted(callback);
+	const onDeleted = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		const uuid = await modelStore.onDeleted(modelName, callback);
 
 
-		subscriptions.value.deleted.push({
-			store,
-			uuid
-		});
+		subscriptions.value.deleted[modelName] ??= [];
+		subscriptions.value.deleted[modelName].push(uuid);
 	};
 	};
 
 
 	const removeCallback = async (
 	const removeCallback = async (
-		store,
+		modelName: string,
 		type: "created" | "updated" | "deleted",
 		type: "created" | "updated" | "deleted",
 		uuid: string
 		uuid: string
 	) => {
 	) => {
 		if (
 		if (
-			!subscriptions.value[type].find(
-				subscription =>
-					subscription.store === store && subscription.uuid === uuid
+			!subscriptions.value[type][modelName] ||
+			!subscriptions.value[type][modelName].find(
+				subscription => subscription === uuid
 			)
 			)
 		)
 		)
 			return;
 			return;
 
 
-		await store.removeCallback(type, uuid);
+		await modelStore.removeCallback(modelName, type, uuid);
 
 
-		delete subscriptions.value[type][uuid];
+		delete subscriptions.value[type][modelName][uuid];
 	};
 	};
 
 
-	const registerModels = async (store, storeModels: any[]) => {
-		let storeIndex = models.value.findIndex(model => model.store === store);
-
-		const registeredModels = await store.registerModels(storeModels);
-
-		if (storeIndex < 0) {
-			models.value.push({
-				store,
-				models: registeredModels
-			});
-
-			await onDeleted(store, ({ oldDoc }) => {
-				storeIndex = models.value.findIndex(
-					model => model.store === store
-				);
-
-				if (storeIndex < 0) return;
+	const registerModels = async (modelName: string, storeModels: any[]) => {
+		const registeredModels = await modelStore.registerModels(
+			modelName,
+			storeModels
+		);
 
 
-				const modelIndex = models.value[storeIndex].models.findIndex(
-					model => model._id === oldDoc._id
-				);
+		models.value.push(...registeredModels);
 
 
-				if (modelIndex < 0) return;
+		await onDeleted(modelName, ({ oldDoc }) => {
+			if (!models.value[modelName]) return;
 
 
-				delete models.value[storeIndex].models[modelIndex];
-			});
+			const modelIndex = models.value[modelName].findIndex(
+				model => model._id === oldDoc._id
+			);
 
 
-			return registeredModels;
-		}
+			if (modelIndex < 0) return;
 
 
-		models.value[storeIndex].models = [
-			...models.value[storeIndex].models,
-			registeredModels
-		];
+			delete models.value[modelName][modelIndex];
+		});
 
 
 		return registeredModels;
 		return registeredModels;
 	};
 	};
 
 
-	const unregisterModels = async (store, modelIds: string[]) => {
-		const storeIndex = models.value.findIndex(
-			model => model.store === store
-		);
-
-		if (storeIndex < 0) return;
-
-		const storeModels = models.value[storeIndex].models;
-
-		await store.unregisterModels(
-			storeModels
-				.filter(model => modelIds.includes(model._id))
-				.map(model => model._id)
+	const unregisterModels = async (modelIds: string[]) => {
+		await modelStore.unregisterModels(
+			modelIds.filter(modelId =>
+				models.value.find(model => modelId === model._id)
+			)
 		);
 		);
 
 
-		models.value[storeIndex].modelIds = storeModels.filter(
+		models.value = models.value.filter(
 			model => !modelIds.includes(model._id)
 			model => !modelIds.includes(model._id)
 		);
 		);
 	};
 	};
 
 
 	onBeforeUnmount(async () => {
 	onBeforeUnmount(async () => {
 		await Promise.all(
 		await Promise.all(
-			Object.entries(subscriptions.value).map(
-				async ([type, _subscriptions]) =>
+			Object.entries(subscriptions.value).map(async ([type, uuids]) =>
+				Object.entries(uuids).map(async ([modelName, _subscriptions]) =>
 					Promise.all(
 					Promise.all(
-						_subscriptions.map(({ store, uuid }) =>
-							removeCallback(store, type, uuid)
+						_subscriptions.map(uuid =>
+							removeCallback(modelName, type, uuid)
 						)
 						)
 					)
 					)
-			)
-		);
-		await Promise.all(
-			models.value.map(({ store, models: storeModels }) =>
-				unregisterModels(
-					store,
-					storeModels.map(model => model._id)
 				)
 				)
 			)
 			)
 		);
 		);
+		await unregisterModels(models.value.map(model => model._id));
 	});
 	});
 
 
 	return {
 	return {

+ 8 - 14
frontend/src/pages/Admin/News.vue

@@ -3,7 +3,7 @@ import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
 import Toast from "toasters";
 import { useModalsStore } from "@/stores/modals";
 import { useModalsStore } from "@/stores/modals";
 import { TableColumn, TableFilter } from "@/types/advancedTable";
 import { TableColumn, TableFilter } from "@/types/advancedTable";
-import { useNewsModelStore } from "@/stores/models/news";
+import { useModelStore } from "@/stores/model";
 
 
 const AdvancedTable = defineAsyncComponent(
 const AdvancedTable = defineAsyncComponent(
 	() => import("@/components/AdvancedTable.vue")
 	() => import("@/components/AdvancedTable.vue")
@@ -109,10 +109,10 @@ const filters = ref<TableFilter[]>([
 
 
 const { openModal } = useModalsStore();
 const { openModal } = useModalsStore();
 
 
-const { deleteById, hasPermission } = useNewsModelStore();
+const { hasPermission } = useModelStore();
 
 
-const remove = async (_id: string) => {
-	const res = await deleteById(_id);
+const remove = async item => {
+	const res = await item.delete();
 	new Toast(res.message);
 	new Toast(res.message);
 };
 };
 </script>
 </script>
@@ -127,7 +127,7 @@ const remove = async (_id: string) => {
 			</div>
 			</div>
 			<div class="button-row">
 			<div class="button-row">
 				<button
 				<button
-					v-if="hasPermission('data.news.create')"
+					v-if="hasPermission('news', 'data.news.create')"
 					class="is-primary button"
 					class="is-primary button"
 					@click="
 					@click="
 						openModal({
 						openModal({
@@ -151,10 +151,7 @@ const remove = async (_id: string) => {
 				<div class="row-options">
 				<div class="row-options">
 					<button
 					<button
 						v-if="
 						v-if="
-							hasPermission(
-								'data.news.updateById',
-								slotProps.item._id
-							)
+							slotProps.item.hasPermission('data.news.updateById')
 						"
 						"
 						class="button is-primary icon-with-button material-icons"
 						class="button is-primary icon-with-button material-icons"
 						@click="
 						@click="
@@ -170,12 +167,9 @@ const remove = async (_id: string) => {
 					</button>
 					</button>
 					<quick-confirm
 					<quick-confirm
 						v-if="
 						v-if="
-							hasPermission(
-								'data.news.deleteById',
-								slotProps.item._id
-							)
+							slotProps.item.hasPermission('data.news.deleteById')
 						"
 						"
-						@confirm="remove(slotProps.item._id)"
+						@confirm="remove(slotProps.item)"
 						:disabled="slotProps.item.removed"
 						:disabled="slotProps.item.removed"
 					>
 					>
 						<button
 						<button

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

@@ -11,9 +11,9 @@ import {
 	NewsRemovedResponse
 	NewsRemovedResponse
 } from "@musare_types/events/NewsEvents";
 } from "@musare_types/events/NewsEvents";
 import { GetPublishedNewsResponse } from "@musare_types/actions/NewsActions";
 import { GetPublishedNewsResponse } from "@musare_types/actions/NewsActions";
-import { useNewsModelStore } from "@/stores/models/news";
 import { useEvents } from "@/composables/useEvents";
 import { useEvents } from "@/composables/useEvents";
 import { useModels } from "@/composables/useModels";
 import { useModels } from "@/composables/useModels";
+import { useWebsocketStore } from "@/stores/websocket";
 
 
 const MainHeader = defineAsyncComponent(
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
 	() => import("@/components/MainHeader.vue")
@@ -25,9 +25,9 @@ const UserLink = defineAsyncComponent(
 	() => import("@/components/UserLink.vue")
 	() => import("@/components/UserLink.vue")
 );
 );
 
 
+const { runJob } = useWebsocketStore();
 const { onReady } = useEvents();
 const { onReady } = useEvents();
-const { registerModels, onCreated, onDeleted } = useModels();
-const newsStore = useNewsModelStore();
+const { registerModels, onCreated, onDeleted, subscriptions } = useModels();
 
 
 const news = ref<NewsModel[]>([]);
 const news = ref<NewsModel[]>([]);
 
 
@@ -46,15 +46,18 @@ onMounted(async () => {
 	});
 	});
 
 
 	await onReady(async () => {
 	await onReady(async () => {
-		news.value = await registerModels(newsStore, await newsStore.newest());
+		news.value = await registerModels(
+			"news",
+			await runJob("data.news.newest", {})
+		);
 	});
 	});
 
 
-	await onCreated(newsStore, async ({ doc }) => {
-		const [newDoc] = await registerModels(newsStore, [doc]);
+	await onCreated("news", async ({ doc }) => {
+		const [newDoc] = await registerModels("news", [doc]);
 		news.value.unshift(newDoc);
 		news.value.unshift(newDoc);
 	});
 	});
 
 
-	await onDeleted(newsStore, async ({ oldDoc }) => {
+	await onDeleted("news", async ({ oldDoc }) => {
 		const index = news.value.findIndex(doc => doc._id === oldDoc._id);
 		const index = news.value.findIndex(doc => doc._id === oldDoc._id);
 
 
 		if (index < 0) return;
 		if (index < 0) return;

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

@@ -0,0 +1,234 @@
+import { reactive, ref } from "vue";
+import { defineStore } from "pinia";
+import { useWebsocketStore } from "./websocket";
+import utils from "@/utils";
+import Model from "@/Model";
+
+export const useModelStore = defineStore("model", () => {
+	const { runJob, subscribe, unsubscribe } = useWebsocketStore();
+
+	const models = ref([]);
+	const permissions = ref(null);
+	const createdSubcription = ref(null);
+	const subscriptions = ref({
+		created: {},
+		updated: {},
+		deleted: {}
+	});
+
+	const getUserModelPermissions = async (modelName: string) => {
+		if (permissions.value) return permissions.value;
+
+		const data = await runJob("api.getUserModelPermissions", {
+			modelName
+		});
+
+		permissions.value = data;
+
+		return permissions.value;
+	};
+
+	const hasPermission = async (modelName: string, permission: string) => {
+		const data = await getUserModelPermissions(modelName);
+
+		return !!data[permission];
+	};
+
+	const onCreatedCallback = async (modelName: string, data) => {
+		if (!subscriptions.value.created[modelName]) return;
+
+		await Promise.all(
+			Object.values(subscriptions.value.created[modelName]).map(
+				async subscription => subscription(data) // TODO: Error handling
+			)
+		);
+	};
+
+	const onCreated = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		if (!createdSubcription.value)
+			createdSubcription.value = await subscribe(
+				`model.${modelName}.created`,
+				data => onCreatedCallback(modelName, data)
+			);
+
+		const uuid = utils.guid();
+
+		subscriptions.value.created[modelName] ??= {};
+		subscriptions.value.created[modelName][uuid] = callback;
+
+		return uuid;
+	};
+
+	const onUpdated = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		const uuid = utils.guid();
+
+		subscriptions.value.updated[modelName] ??= {};
+		subscriptions.value.updated[modelName][uuid] = callback;
+
+		return uuid;
+	};
+
+	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();
+
+		if (!subscriptions.value.updated[modelName]) return;
+
+		await Promise.all(
+			Object.values(subscriptions.value.updated[modelName]).map(
+				async subscription => subscription(data) // TODO: Error handling
+			)
+		);
+	};
+
+	const onDeleted = async (
+		modelName: string,
+		callback: (data?: any) => any
+	) => {
+		const uuid = utils.guid();
+
+		subscriptions.value.deleted[modelName] ??= {};
+		subscriptions.value.deleted[modelName][uuid] = callback;
+
+		return uuid;
+	};
+
+	const onDeletedCallback = async (modelName: string, data) => {
+		const { oldDoc } = data;
+
+		if (subscriptions.value.deleted[modelName])
+			await Promise.all(
+				Object.values(subscriptions.value.deleted[modelName]).map(
+					async subscription => subscription(data) // TODO: Error handling
+				)
+			);
+
+		const index = models.value.findIndex(model => model._id === oldDoc._id);
+		if (index > -1) await unregisterModels(oldDoc._id);
+	};
+
+	const removeCallback = async (
+		modelName: string,
+		type: "created" | "updated" | "deleted",
+		uuid: string
+	) => {
+		if (
+			!subscriptions.value[type][modelName] ||
+			!subscriptions.value[type][modelName][uuid]
+		)
+			return;
+
+		delete subscriptions.value[type][modelName][uuid];
+
+		if (
+			type === "created" &&
+			Object.keys(subscriptions.value.created[modelName]).length === 0
+		) {
+			await unsubscribe(
+				`model.${modelName}.created`,
+				createdSubcription.value
+			);
+
+			createdSubcription.value = null;
+		}
+	};
+
+	const registerModels = async (modelName: string, 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));
+
+				docRef.addUse();
+
+				if (existingRef) return docRef;
+
+				models.value.push(docRef);
+
+				const updatedUuid = await subscribe(
+					`model.${modelName}.updated.${_doc._id}`,
+					data => onUpdatedCallback(modelName, data)
+				);
+
+				const deletedUuid = await subscribe(
+					`model.${modelName}.deleted.${_doc._id}`,
+					data => onDeletedCallback(modelName, data)
+				);
+
+				docRef.setSubscriptions(updatedUuid, deletedUuid);
+
+				return docRef;
+			})
+		);
+
+	const unregisterModels = async modelIds =>
+		Promise.all(
+			(Array.isArray(modelIds) ? modelIds : [modelIds]).map(
+				async modelId => {
+					const model = models.value.find(
+						model => model._id === modelId
+					);
+
+					if (!model || model.getUses() > 1) {
+						model.removeUse();
+
+						return;
+					}
+
+					const { updated, deleted } = model.getSubscriptions() ?? {};
+
+					if (updated)
+						await unsubscribe(
+							`model.${model.getName()}.updated.${modelId}`,
+							updated
+						);
+
+					if (deleted)
+						await unsubscribe(
+							`model.${model.getName()}.deleted.${modelId}`,
+							deleted
+						);
+
+					models.value.splice(
+						models.value.findIndex(model => model._id === modelId),
+						1
+					);
+				}
+			)
+		);
+
+	const findById = async (modelName: string, _id) => {
+		const existingModel = models.value.find(model => model._id === _id);
+
+		if (existingModel) return existingModel;
+
+		return runJob(`data.${modelName}.findById`, { _id });
+	};
+
+	return {
+		models,
+		permissions,
+		subscriptions,
+		onCreated,
+		onUpdated,
+		onDeleted,
+		removeCallback,
+		registerModels,
+		unregisterModels,
+		getUserModelPermissions,
+		hasPermission,
+		findById
+	};
+});

+ 0 - 265
frontend/src/stores/models/model.ts

@@ -1,265 +0,0 @@
-import { reactive, ref } from "vue";
-import { useWebsocketStore } from "../websocket";
-import utils from "@/utils";
-
-export const createModelStore = modelName => {
-	const { runJob, subscribe, unsubscribe } = useWebsocketStore();
-
-	const models = ref([]);
-	const modelUses = ref({});
-	const permissions = ref(null);
-	const modelPermissions = ref({});
-	const createdSubcription = ref(null);
-	const subscriptions = ref({
-		models: {},
-		created: {},
-		updated: {},
-		deleted: {}
-	});
-
-	const fetchUserModelPermissions = async (_id?: string) => {
-		const data = await runJob("api.getUserModelPermissions", {
-			modelName,
-			modelId: _id
-		});
-
-		if (_id) {
-			modelPermissions.value[_id] = data;
-
-			return modelPermissions.value[_id];
-		}
-
-		permissions.value = data;
-
-		return permissions.value;
-	};
-
-	const getUserModelPermissions = async (_id?: string) => {
-		if (!_id && permissions.value) return permissions.value;
-
-		if (_id && modelPermissions.value[_id])
-			return modelPermissions.value[_id];
-
-		return fetchUserModelPermissions(_id);
-	};
-
-	const hasPermission = async (permission: string, _id?: string) => {
-		const data = await getUserModelPermissions(_id);
-
-		return !!data[permission];
-	};
-
-	const onCreatedCallback = async data => {
-		await Promise.all(
-			Object.values(subscriptions.value.created).map(
-				async subscription => subscription(data) // TODO: Error handling
-			)
-		);
-	};
-
-	const onCreated = async (callback: (data?: any) => any) => {
-		if (!createdSubcription.value)
-			createdSubcription.value = await subscribe(
-				`model.${modelName}.created`,
-				onCreatedCallback
-			);
-
-		const uuid = utils.guid();
-
-		subscriptions.value.created[uuid] = callback;
-
-		return uuid;
-	};
-
-	const onUpdated = async (callback: (data?: any) => any) => {
-		const uuid = utils.guid();
-
-		subscriptions.value.updated[uuid] = callback;
-
-		return uuid;
-	};
-
-	const onUpdatedCallback = async ({ doc }) => {
-		const index = models.value.findIndex(model => model._id === doc._id);
-		if (index > -1) Object.assign(models.value[index], doc);
-
-		if (modelPermissions.value[doc._id])
-			await fetchUserModelPermissions(doc._id);
-
-		await Promise.all(
-			Object.values(subscriptions.value.updated).map(
-				async subscription => subscription(data) // TODO: Error handling
-			)
-		);
-	};
-
-	const onDeleted = async (callback: (data?: any) => any) => {
-		const uuid = utils.guid();
-
-		subscriptions.value.deleted[uuid] = callback;
-
-		return uuid;
-	};
-
-	const onDeletedCallback = async data => {
-		const { oldDoc } = data;
-
-		await Promise.all(
-			Object.values(subscriptions.value.deleted).map(
-				async subscription => subscription(data) // TODO: Error handling
-			)
-		);
-
-		const index = models.value.findIndex(model => model._id === oldDoc._id);
-		if (index > -1) await unregisterModels(oldDoc._id);
-
-		if (modelPermissions.value[oldDoc._id])
-			delete modelPermissions.value[oldDoc._id];
-	};
-
-	const removeCallback = async (
-		type: "created" | "updated" | "deleted",
-		uuid: string
-	) => {
-		if (!subscriptions.value[type][uuid]) return;
-
-		delete subscriptions.value[type][uuid];
-
-		if (
-			type === "created" &&
-			Object.keys(subscriptions.value.created).length === 0
-		) {
-			await unsubscribe(
-				`model.${modelName}.created`,
-				createdSubcription.value
-			);
-
-			createdSubcription.value = null;
-		}
-	};
-
-	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(_doc);
-
-				if (!existingRef) {
-					models.value.push(docRef);
-				}
-
-				modelUses.value[_doc._id] =
-					(modelUses.value[_doc._id] ?? 0) + 1;
-
-				if (subscriptions.value.models[_doc._id]) return docRef;
-
-				const updatedChannel = `model.${modelName}.updated.${_doc._id}`;
-				const updatedUuid = await subscribe(
-					updatedChannel,
-					onUpdatedCallback
-				);
-				const updated = {
-					channel: updatedChannel,
-					callback: onUpdatedCallback,
-					uuid: updatedUuid
-				};
-
-				const deletedChannel = `model.${modelName}.deleted.${_doc._id}`;
-				const deletedUuid = await subscribe(
-					deletedChannel,
-					onDeletedCallback
-				);
-				const deleted = {
-					channel: deletedChannel,
-					callback: onDeletedCallback,
-					uuid: deletedUuid
-				};
-
-				subscriptions.value.models[_doc._id] = {
-					updated,
-					deleted
-				};
-
-				return docRef;
-			})
-		);
-
-	const unregisterModels = async modelIds =>
-		Promise.all(
-			(Array.isArray(modelIds) ? modelIds : [modelIds]).map(
-				async modelId => {
-					if (
-						models.value.findIndex(
-							model => model._id === modelId
-						) === -1
-					)
-						return;
-
-					if (modelUses.value[modelId] > 1) {
-						modelUses.value[modelId] -= 1;
-
-						return;
-					}
-
-					const { updated, deleted } =
-						subscriptions.value.models[modelId];
-
-					await unsubscribe(updated.channel, updated.uuid);
-
-					await unsubscribe(deleted.channel, deleted.uuid);
-
-					delete subscriptions.value.models[modelId];
-
-					models.value.splice(
-						models.value.findIndex(model => model._id === modelId),
-						1
-					);
-
-					if (modelPermissions.value[modelId])
-						delete modelPermissions.value[modelId];
-
-					if (modelUses.value[modelId])
-						delete modelUses.value[modelId];
-				}
-			)
-		);
-
-	const create = async query => runJob(`data.${modelName}.create`, { query });
-
-	const findById = async _id => {
-		const existingModel = models.value.find(model => model._id === _id);
-
-		if (existingModel) return existingModel;
-
-		return runJob(`data.${modelName}.findById`, { _id });
-	};
-
-	const updateById = async (_id, query) =>
-		runJob(`data.${modelName}.updateById`, { _id, query });
-
-	const deleteById = async _id =>
-		runJob(`data.${modelName}.deleteById`, { _id });
-
-	return {
-		models,
-		permissions,
-		modelPermissions,
-		subscriptions,
-		onCreated,
-		onUpdated,
-		onDeleted,
-		removeCallback,
-		registerModels,
-		unregisterModels,
-		fetchUserModelPermissions,
-		getUserModelPermissions,
-		hasPermission,
-		create,
-		findById,
-		updateById,
-		deleteById
-	};
-};

+ 0 - 20
frontend/src/stores/models/news.ts

@@ -1,20 +0,0 @@
-import { defineStore } from "pinia";
-import { useWebsocketStore } from "../websocket";
-import { createModelStore } from "./model";
-
-export const useNewsModelStore = defineStore("newsModel", () => {
-	const { runJob } = useWebsocketStore();
-
-	const modelStore = createModelStore("news");
-
-	const published = async () => runJob("data.news.published", {});
-
-	const newest = async (showToNewUsers?, limit?) =>
-		runJob("data.news.newest", { showToNewUsers, limit });
-
-	return {
-		...modelStore,
-		published,
-		newest
-	};
-});