123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- /* eslint max-classes-per-file: 0 */
- import { forEachIn } from "@common/utils/forEachIn";
- import { useModelStore } from "./stores/model";
- import { useWebsocketStore } from "./stores/websocket";
- class DeferredPromise<T = any> {
- promise: Promise<T>;
- reject;
- resolve;
- // eslint-disable-next-line require-jsdoc
- constructor() {
- this.promise = new Promise<T>((resolve, reject) => {
- this.reject = reject;
- this.resolve = resolve;
- });
- }
- }
- interface ModelPermissionFetcherRequest {
- promise: DeferredPromise;
- payload: {
- modelName: string;
- modelId: string;
- };
- }
- /**
- * Class used for fetching model permissions in bulk, every 25ms max
- * So if there's 200 models loaded, it would do only 1 request to fetch model permissions, not 200 separate ones
- */
- class ModelPermissionFetcher {
- private static requestsQueued: ModelPermissionFetcherRequest[] = [];
- private static timeoutActive = false;
- private static fetch() {
- // If there is no other timeout running, indicate we will run one. Otherwise, return, as a timeout is already running
- if (!this.timeoutActive) this.timeoutActive = true;
- else return;
- setTimeout(() => {
- // Reset timeout active, so another one can run
- this.timeoutActive = false;
- // Make a copy of all requests currently queued, and then take those requests out of the queue so we can request them
- const requests = this.requestsQueued;
- this.requestsQueued = [];
- // Splits the requests per model
- const requestsPerModel = {};
- requests.forEach(request => {
- const { modelName } = request.payload;
- if (!Array.isArray(requestsPerModel[modelName]))
- requestsPerModel[modelName] = [];
- requestsPerModel[modelName].push(request);
- });
- const modelNames = Object.keys(requestsPerModel);
- const { runJob } = useWebsocketStore();
- // Runs the requests per model
- forEachIn(modelNames, async modelName => {
- // Gets a unique list of all model ids for the current model that we want to request permissions for
- const modelIds = Array.from(
- new Set(
- requestsPerModel[modelName].map(
- request => request.payload.modelId
- )
- )
- );
- const result = await runJob("data.users.getModelPermissions", {
- modelName,
- modelIds
- });
- const requests = requestsPerModel[modelName];
- // For all requests, resolve the deferred promise with the returned permissions for the model that request requested
- requests.forEach(request => {
- const { payload, promise } = request;
- const { modelId } = payload;
- promise.resolve(result[modelId]);
- });
- });
- }, 25);
- }
- public static fetchModelPermissions(modelName, modelId) {
- return new Promise(resolve => {
- const promise = new DeferredPromise();
- // Listens for the deferred promise response, before we actually push and fetch
- promise.promise.then(result => {
- resolve(result);
- });
- // Pushes the request to the queue
- this.requestsQueued.push({
- payload: {
- modelName,
- modelId
- },
- promise
- });
- // Calls the fetch function, which will start a timeout if one isn't already running, which will actually request the permissions
- this.fetch();
- });
- }
- }
- export default class Model {
- private _permissions?: object;
- private _subscriptions?: { updated: string; deleted: string };
- private _uses: number;
- private _loadedRelations: string[];
- constructor(data: object) {
- this._uses = 0;
- this._loadedRelations = [];
- Object.assign(this, data);
- }
- 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;
- path = path ? `${path}.${key}` : key;
- if (typeof value === "object" && value._id) paths.push(path);
- else if (Array.isArray(value))
- await forEachIn(value, async item => {
- if (typeof item !== "object") return;
- if (item._id) paths.push(path);
- else
- paths.push(
- ...(await this._getRelations(item, path))
- );
- });
- else paths.push(...(await this._getRelations(value, path)));
- return paths;
- }, Promise.resolve([]));
- return relationPaths.filter(
- (relationPath, index) =>
- relationPaths.indexOf(relationPath) === index
- );
- }
- 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> {
- const parts = path.split(".");
- let [head] = parts;
- const [, ...rest] = parts;
- let [next] = rest;
- if (Number.isInteger(head)) head = Number.parseInt(head);
- if (Number.isInteger(next)) next = Number.parseInt(next);
- pathParts ??= [];
- pathParts.push(head);
- if (Array.isArray(model[head])) {
- await forEachIn(
- model[head],
- async (item, index) => {
- let itemPath = `${index}`;
- if (rest.length > 0) itemPath += `.${rest.join(".")}`;
- await this._loadRelation(model[head], itemPath, force, [
- ...pathParts
- ]);
- },
- { concurrency: 1 }
- );
- 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, registerModel } = useModelStore();
- const data = await findById(model[head]._name, model[head]._id);
- const registeredModel = await registerModel(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 forEachIn(relations ?? [], async path => {
- await this._loadRelation(this, path, force);
- });
- }
- public async unregisterRelations(): Promise<void> {
- const { unregisterModels } = useModelStore();
- const relations = {};
- await forEachIn(this._loadedRelations, async path => {
- const relation = await this._getRelation(path);
- const { _name: modelName, _id: modelId } = relation;
- relations[modelName] ??= [];
- relations[modelName].push(modelId);
- });
- const modelNames = Object.keys(relations);
- await forEachIn(modelNames, async modelName => {
- await unregisterModels(modelName, relations[modelName]);
- });
- }
- public async updateData(data: object) {
- await this.unregisterRelations();
- Object.assign(this, data);
- await this.loadRelations(this._loadedRelations, true);
- await this.refreshPermissions();
- }
- public getName(): string {
- return this._name;
- }
- public getId(): string {
- return this._id;
- }
- public async getPermissions(refresh = false): Promise<object> {
- if (refresh === false && this._permissions) return this._permissions;
- this._permissions = await ModelPermissionFetcher.fetchModelPermissions(
- this.getName(),
- 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) {
- const { runJob } = useWebsocketStore();
- return runJob(`data.${this.getName()}.updateById`, {
- _id: this.getId(),
- query
- });
- }
- public async delete() {
- const { runJob } = useWebsocketStore();
- return runJob(`data.${this.getName()}.deleteById`, { _id: this._id });
- }
- }
|