Model.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. /* eslint max-classes-per-file: 0 */
  2. import { forEachIn } from "@common/utils/forEachIn";
  3. import { useModelStore } from "./stores/model";
  4. import { useWebsocketStore } from "./stores/websocket";
  5. class DeferredPromise<T = any> {
  6. promise: Promise<T>;
  7. reject;
  8. resolve;
  9. // eslint-disable-next-line require-jsdoc
  10. constructor() {
  11. this.promise = new Promise<T>((resolve, reject) => {
  12. this.reject = reject;
  13. this.resolve = resolve;
  14. });
  15. }
  16. }
  17. interface ModelPermissionFetcherRequest {
  18. promise: DeferredPromise;
  19. payload: {
  20. modelName: string;
  21. modelId: string;
  22. };
  23. }
  24. /**
  25. * Class used for fetching model permissions in bulk, every 25ms max
  26. * So if there's 200 models loaded, it would do only 1 request to fetch model permissions, not 200 separate ones
  27. */
  28. class ModelPermissionFetcher {
  29. private static requestsQueued: ModelPermissionFetcherRequest[] = [];
  30. private static timeoutActive = false;
  31. private static fetch() {
  32. // If there is no other timeout running, indicate we will run one. Otherwise, return, as a timeout is already running
  33. if (!this.timeoutActive) this.timeoutActive = true;
  34. else return;
  35. setTimeout(() => {
  36. // Reset timeout active, so another one can run
  37. this.timeoutActive = false;
  38. // Make a copy of all requests currently queued, and then take those requests out of the queue so we can request them
  39. const requests = this.requestsQueued;
  40. this.requestsQueued = [];
  41. // Splits the requests per model
  42. const requestsPerModel = {};
  43. requests.forEach(request => {
  44. const { modelName } = request.payload;
  45. if (!Array.isArray(requestsPerModel[modelName]))
  46. requestsPerModel[modelName] = [];
  47. requestsPerModel[modelName].push(request);
  48. });
  49. const modelNames = Object.keys(requestsPerModel);
  50. const { runJob } = useWebsocketStore();
  51. // Runs the requests per model
  52. forEachIn(modelNames, async modelName => {
  53. // Gets a unique list of all model ids for the current model that we want to request permissions for
  54. const modelIds = Array.from(
  55. new Set(
  56. requestsPerModel[modelName].map(
  57. request => request.payload.modelId
  58. )
  59. )
  60. );
  61. const result = await runJob("data.users.getModelPermissions", {
  62. modelName,
  63. modelIds
  64. });
  65. const requests = requestsPerModel[modelName];
  66. // For all requests, resolve the deferred promise with the returned permissions for the model that request requested
  67. requests.forEach(request => {
  68. const { payload, promise } = request;
  69. const { modelId } = payload;
  70. promise.resolve(result[modelId]);
  71. });
  72. });
  73. }, 25);
  74. }
  75. public static fetchModelPermissions(modelName, modelId) {
  76. return new Promise(resolve => {
  77. const promise = new DeferredPromise();
  78. // Listens for the deferred promise response, before we actually push and fetch
  79. promise.promise.then(result => {
  80. resolve(result);
  81. });
  82. // Pushes the request to the queue
  83. this.requestsQueued.push({
  84. payload: {
  85. modelName,
  86. modelId
  87. },
  88. promise
  89. });
  90. // Calls the fetch function, which will start a timeout if one isn't already running, which will actually request the permissions
  91. this.fetch();
  92. });
  93. }
  94. }
  95. export default class Model {
  96. private _permissions?: object;
  97. private _subscriptions?: { updated: string; deleted: string };
  98. private _uses: number;
  99. private _loadedRelations: string[];
  100. constructor(data: object) {
  101. this._uses = 0;
  102. this._loadedRelations = [];
  103. Object.assign(this, data);
  104. }
  105. private async _getRelations(
  106. model?: object,
  107. path?: string
  108. ): Promise<string[]> {
  109. const relationPaths = await Object.entries(model ?? this)
  110. .filter(
  111. ([key, value]) =>
  112. !key.startsWith("_") &&
  113. (typeof value === "object" || Array.isArray(value))
  114. )
  115. .reduce(async (_modelIds, [key, value]) => {
  116. const paths = await _modelIds;
  117. path = path ? `${path}.${key}` : key;
  118. if (typeof value === "object" && value._id) paths.push(path);
  119. else if (Array.isArray(value))
  120. await forEachIn(value, async item => {
  121. if (typeof item !== "object") return;
  122. if (item._id) paths.push(path);
  123. else
  124. paths.push(
  125. ...(await this._getRelations(item, path))
  126. );
  127. });
  128. else paths.push(...(await this._getRelations(value, path)));
  129. return paths;
  130. }, Promise.resolve([]));
  131. return relationPaths.filter(
  132. (relationPath, index) =>
  133. relationPaths.indexOf(relationPath) === index
  134. );
  135. }
  136. private async _getRelation(key: string) {
  137. let relation = JSON.parse(JSON.stringify(this));
  138. key.split(".").forEach(property => {
  139. if (Number.isInteger(property))
  140. property = Number.parseInt(property);
  141. relation = relation[property];
  142. });
  143. return relation;
  144. }
  145. private async _loadRelation(
  146. model: object,
  147. path: string,
  148. force: boolean,
  149. pathParts?: string[]
  150. ): Promise<void> {
  151. const parts = path.split(".");
  152. let [head] = parts;
  153. const [, ...rest] = parts;
  154. let [next] = rest;
  155. if (Number.isInteger(head)) head = Number.parseInt(head);
  156. if (Number.isInteger(next)) next = Number.parseInt(next);
  157. pathParts ??= [];
  158. pathParts.push(head);
  159. if (Array.isArray(model[head])) {
  160. await forEachIn(
  161. model[head],
  162. async (item, index) => {
  163. let itemPath = `${index}`;
  164. if (rest.length > 0) itemPath += `.${rest.join(".")}`;
  165. await this._loadRelation(model[head], itemPath, force, [
  166. ...pathParts
  167. ]);
  168. },
  169. { concurrency: 1 }
  170. );
  171. return;
  172. }
  173. if (rest.length > 0 && model[next] === null) {
  174. await this._loadRelation(
  175. model[head],
  176. rest.join("."),
  177. force,
  178. pathParts
  179. );
  180. return;
  181. }
  182. const fullPath = pathParts.join(".");
  183. if (force || !this._loadedRelations.includes(fullPath)) {
  184. const { findById, registerModel } = useModelStore();
  185. const data = await findById(model[head]._name, model[head]._id);
  186. const registeredModel = await registerModel(data);
  187. model[head] = registeredModel;
  188. this._loadedRelations.push(fullPath);
  189. }
  190. if (rest.length === 0) return;
  191. await model[head].loadRelations(rest.join("."));
  192. }
  193. public async loadRelations(
  194. relations?: string | string[],
  195. force = false
  196. ): Promise<void> {
  197. if (relations)
  198. relations = Array.isArray(relations) ? relations : [relations];
  199. await forEachIn(relations ?? [], async path => {
  200. await this._loadRelation(this, path, force);
  201. });
  202. }
  203. public async unregisterRelations(): Promise<void> {
  204. const { unregisterModels } = useModelStore();
  205. const relations = {};
  206. await forEachIn(this._loadedRelations, async path => {
  207. const relation = await this._getRelation(path);
  208. const { _name: modelName, _id: modelId } = relation;
  209. relations[modelName] ??= [];
  210. relations[modelName].push(modelId);
  211. });
  212. const modelNames = Object.keys(relations);
  213. await forEachIn(modelNames, async modelName => {
  214. await unregisterModels(modelName, relations[modelName]);
  215. });
  216. }
  217. public async updateData(data: object) {
  218. await this.unregisterRelations();
  219. Object.assign(this, data);
  220. await this.loadRelations(this._loadedRelations, true);
  221. await this.refreshPermissions();
  222. }
  223. public getName(): string {
  224. return this._name;
  225. }
  226. public getId(): string {
  227. return this._id;
  228. }
  229. public async getPermissions(refresh = false): Promise<object> {
  230. if (refresh === false && this._permissions) return this._permissions;
  231. this._permissions = await ModelPermissionFetcher.fetchModelPermissions(
  232. this.getName(),
  233. this._id
  234. );
  235. return this._permissions;
  236. }
  237. public async refreshPermissions(): Promise<void> {
  238. if (this._permissions) this.getPermissions(true);
  239. }
  240. public async hasPermission(permission: string): Promise<boolean> {
  241. const permissions = await this.getPermissions();
  242. return !!permissions[permission];
  243. }
  244. public getSubscriptions() {
  245. return this._subscriptions;
  246. }
  247. public setSubscriptions(updated: string, deleted: string): void {
  248. this._subscriptions = { updated, deleted };
  249. }
  250. public getUses(): number {
  251. return this._uses;
  252. }
  253. public addUse(): void {
  254. this._uses += 1;
  255. }
  256. public removeUse(): void {
  257. this._uses -= 1;
  258. }
  259. public toJSON(): object {
  260. return Object.fromEntries(
  261. Object.entries(this).filter(
  262. ([key, value]) =>
  263. (!key.startsWith("_") || key === "_id") &&
  264. typeof value !== "function"
  265. )
  266. );
  267. }
  268. public async update(query: object) {
  269. const { runJob } = useWebsocketStore();
  270. return runJob(`data.${this.getName()}.updateById`, {
  271. _id: this.getId(),
  272. query
  273. });
  274. }
  275. public async delete() {
  276. const { runJob } = useWebsocketStore();
  277. return runJob(`data.${this.getName()}.deleteById`, { _id: this._id });
  278. }
  279. }