Model.ts 8.1 KB

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