model.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import { reactive, ref, readonly } from "vue";
  2. import { defineStore } from "pinia";
  3. import { generateUuid } from "@common/utils/generateUuid";
  4. import { forEachIn } from "@common/utils/forEachIn";
  5. import { useWebsocketStore } from "./websocket";
  6. import Model from "@/Model";
  7. /**
  8. * Pinia store for managing models
  9. */
  10. export const useModelStore = defineStore("model", () => {
  11. const { runJob, subscribe, subscribeMany, unsubscribe, unsubscribeMany } =
  12. useWebsocketStore();
  13. const models = reactive({});
  14. const permissions = reactive({});
  15. const createdSubcription = ref(null);
  16. const subscriptions = reactive({
  17. created: {},
  18. updated: {},
  19. deleted: {}
  20. });
  21. /**
  22. * Returns generic model permissions for the current user for a specific model type
  23. */
  24. const getUserModelPermissions = async (modelName: string) => {
  25. if (permissions[modelName]) return permissions[modelName];
  26. const data = await runJob("data.users.getModelPermissions", {
  27. modelName
  28. });
  29. permissions[modelName] = data;
  30. return permissions[modelName];
  31. };
  32. /**
  33. * Checks if we have a specific generic permission for a specific model type
  34. */
  35. const hasPermission = async (modelName: string, permission: string) => {
  36. const data = await getUserModelPermissions(modelName);
  37. return !!data[permission];
  38. };
  39. /**
  40. * This functions gets called when the backend notifies us that a model was created
  41. * We then notify every subscription in the frontend about it
  42. */
  43. const onCreatedCallback = async (modelName: string, data) => {
  44. if (!subscriptions.created[modelName]) return;
  45. await forEachIn(
  46. Object.values(subscriptions.created[modelName]),
  47. async subscription => subscription(data) // TODO: Error handling
  48. );
  49. };
  50. /**
  51. * This functions gets called when the backend notifies us that a model was updated
  52. * We then notify every subscription in the frontend about it
  53. */
  54. const onUpdatedCallback = async (modelName: string, { doc }) => {
  55. const model = models[modelName] && models[modelName][doc._id];
  56. if (model) model.updateData(doc);
  57. if (!subscriptions.updated[modelName]) return;
  58. await forEachIn(
  59. Object.values(subscriptions.updated[modelName]),
  60. async subscription => subscription(data) // TODO: Error handling
  61. );
  62. };
  63. /**
  64. * This functions gets called when the backend notifies us that a model was deleted
  65. * We then notify every subscription in the frontend about it
  66. */
  67. const onDeletedCallback = async (modelName: string, data) => {
  68. const { oldDoc } = data;
  69. if (subscriptions.deleted[modelName])
  70. await forEachIn(
  71. Object.values(subscriptions.deleted[modelName]),
  72. async subscription => subscription(data) // TODO: Error handling
  73. );
  74. const model = models[modelName] && models[modelName][oldDoc._id];
  75. // TODO how does this work with addUse?
  76. if (model) await unregisterModels(modelName, oldDoc._id); // eslint-disable-line no-use-before-define
  77. };
  78. /**
  79. * Subscribes the provided callback to model creation events
  80. * The provided callback will be called when the backend notifies us that a model of the provided type is created
  81. */
  82. const onCreated = async (
  83. modelName: string,
  84. callback: (data?: any) => any
  85. ) => {
  86. if (!createdSubcription.value)
  87. createdSubcription.value = await subscribe(
  88. `model.${modelName}.created`,
  89. data => onCreatedCallback(modelName, data)
  90. );
  91. const uuid = generateUuid();
  92. subscriptions.created[modelName] ??= {};
  93. subscriptions.created[modelName][uuid] = callback;
  94. return uuid;
  95. };
  96. /**
  97. * Subscribes the provided callback to model update events
  98. * The provided callback will be called when the backend notifies us that a model of the provided type is updated
  99. */
  100. const onUpdated = async (
  101. modelName: string,
  102. callback: (data?: any) => any
  103. ) => {
  104. const uuid = generateUuid();
  105. subscriptions.updated[modelName] ??= {};
  106. subscriptions.updated[modelName][uuid] = callback;
  107. return uuid;
  108. };
  109. /**
  110. * Subscribes the provided callback to model deletion events
  111. * The provided callback will be called when the backend notifies us that a model of the provided type is deleted
  112. */
  113. const onDeleted = async (
  114. modelName: string,
  115. callback: (data?: any) => any
  116. ) => {
  117. const uuid = generateUuid();
  118. subscriptions.deleted[modelName] ??= {};
  119. subscriptions.deleted[modelName][uuid] = callback;
  120. return uuid;
  121. };
  122. /**
  123. * Allows removing a specific subscription, so the callback of that subscription is no longer called when the backend notifies us
  124. * For type created, we also unsubscribe to events from the backend
  125. * For type updated/deleted, we are subscribed to specific model id updated/deleted events, those are not unsubscribed when
  126. * there's no subscriptions in the frontend actually using them
  127. */
  128. const removeCallback = async (
  129. modelName: string,
  130. type: "created" | "updated" | "deleted",
  131. uuid: string
  132. ) => {
  133. if (
  134. !subscriptions[type][modelName] ||
  135. !subscriptions[type][modelName][uuid]
  136. )
  137. return;
  138. delete subscriptions[type][modelName][uuid];
  139. if (
  140. type === "created" &&
  141. Object.keys(subscriptions.created[modelName]).length === 0
  142. ) {
  143. await unsubscribe(
  144. `model.${modelName}.created`,
  145. createdSubcription.value
  146. );
  147. createdSubcription.value = null;
  148. }
  149. };
  150. /**
  151. * Returns the model for the provided name and id
  152. * First tries to get the model from the already loaded models
  153. * If it's not already loaded, it fetches it from the backend
  154. *
  155. * Does not register the model that was fetched
  156. *
  157. * // TODO return value?
  158. */
  159. const findById = async (modelName: string, modelId: string) => {
  160. const existingModel = models[modelName] && models[modelName][modelId];
  161. if (existingModel) return existingModel;
  162. return runJob(`data.${modelName}.findById`, { _id: modelId });
  163. };
  164. /**
  165. * Returns a list of models based on the provided model name and ids
  166. * First tries to get all models from the already loaded models
  167. * If after that we miss any models, we fetch those from the backend, and return everything we found
  168. *
  169. * Does not register the models that were fetched
  170. */
  171. const findManyById = async (modelName: string, modelIds: string[]) => {
  172. const existingModels = modelIds
  173. .map(modelId => models[modelName] && models[modelName][modelId])
  174. .filter(model => !!model);
  175. const existingModelIds = existingModels.map(model => model._id);
  176. const missingModelIds = modelIds.filter(
  177. _id => !existingModelIds.includes(_id)
  178. );
  179. let fetchedModels = [];
  180. if (missingModelIds.length > 0)
  181. fetchedModels = (await runJob(`data.${modelName}.findManyById`, {
  182. _ids: missingModelIds
  183. })) as unknown[];
  184. const allModels = existingModels.concat(fetchedModels);
  185. // Warning: returns models and direct results
  186. return Object.fromEntries(
  187. modelIds.map(modelId => [
  188. modelId,
  189. allModels.find(model => model._id === modelId)
  190. ])
  191. );
  192. };
  193. /**
  194. * Removes models locally if no one else is still using it
  195. * Also unsubscribes to any updated/deleted subscriptions
  196. */
  197. const unregisterModels = async (
  198. modelName: string,
  199. modelIdOrModelIds: string | string[]
  200. ) => {
  201. const modelIds = Array.isArray(modelIdOrModelIds)
  202. ? modelIdOrModelIds
  203. : [modelIdOrModelIds];
  204. const removeModels = [];
  205. await forEachIn(modelIds, async modelId => {
  206. if (!models[modelName] || !models[modelName][modelId]) return;
  207. const model = models[modelName][modelId];
  208. model.removeUse();
  209. if (model.getUses() > 1) return;
  210. // TODO only do this after a grace period
  211. removeModels.push(model);
  212. });
  213. if (removeModels.length === 0) return;
  214. await forEachIn(removeModels, async model =>
  215. model.unregisterRelations()
  216. );
  217. const subscriptions = Object.fromEntries(
  218. removeModels.flatMap(model => {
  219. const { updated, deleted } = model.getSubscriptions() ?? {};
  220. return [
  221. [
  222. updated,
  223. `model.${model.getName()}.updated.${model.getId()}`
  224. ],
  225. [
  226. deleted,
  227. `model.${model.getName()}.deleted.${model.getId()}`
  228. ]
  229. ];
  230. })
  231. );
  232. await unsubscribeMany(subscriptions);
  233. await forEachIn(removeModels, async removeModel => {
  234. const { _id: modelIdToRemove } = removeModel;
  235. if (!models[modelName] || !models[modelName][modelIdToRemove])
  236. return;
  237. delete models[modelName][modelIdToRemove];
  238. });
  239. console.log("After unregister", JSON.parse(JSON.stringify(models)));
  240. };
  241. /**
  242. * Registers models/documents
  243. * If any models/documents already exist, increments the use counter, and tries to load any potentially missing relations
  244. * For documents that don't already exist, registers them locally, adds subscriptions for updated/deleted events,
  245. * increments the use counter, and loads any potential relations
  246. */
  247. const registerModels = async (
  248. documentsOrModels: any[],
  249. relations?: Record<string, string | string[]>
  250. ): Promise<Model[]> => {
  251. console.info("Register models", documentsOrModels, relations);
  252. console.log(123123, documentsOrModels);
  253. const existingModels = documentsOrModels
  254. .map(({ _name, _id }) =>
  255. models[_name] ? models[_name][_id] ?? null : null
  256. )
  257. .filter(model => !!model);
  258. await forEachIn(existingModels, async model => {
  259. model.addUse();
  260. if (relations && relations[model._name])
  261. await model.loadRelations(relations[model._name]);
  262. });
  263. if (documentsOrModels.length === existingModels.length)
  264. return existingModels;
  265. const missingDocuments = documentsOrModels.filter(
  266. ({ _name, _id }) => !models[_name] || !models[_name][_id]
  267. );
  268. const channels = Object.fromEntries(
  269. missingDocuments.flatMap(({ _name, _id }) => [
  270. [
  271. `model.${_name}.updated.${_id}`,
  272. data => onUpdatedCallback(_name, data)
  273. ],
  274. [
  275. `model.${_name}.deleted.${_id}`,
  276. data => onDeletedCallback(_name, data)
  277. ]
  278. ])
  279. );
  280. const subscriptions = Object.entries(await subscribeMany(channels));
  281. const newModels = await forEachIn(missingDocuments, async document => {
  282. const { _name, _id } = document;
  283. const modelSubscriptions = subscriptions.filter(
  284. ([, { channel }]) =>
  285. channel.startsWith(`model.${_name}`) &&
  286. channel.endsWith(`.${_id}`)
  287. );
  288. const [updated] = modelSubscriptions.find(([, { channel }]) =>
  289. channel.includes(".updated.")
  290. );
  291. const [deleted] = modelSubscriptions.find(([, { channel }]) =>
  292. channel.includes(".deleted.")
  293. );
  294. if (!updated || !deleted) return null;
  295. const model = reactive(new Model(document));
  296. model.setSubscriptions(updated, deleted);
  297. model.addUse();
  298. // TODO what if relations are relevant for some registers, but not others? Unregister if no register relies on a relation
  299. if (relations && relations[_name])
  300. await model.loadRelations(relations[_name]);
  301. if (!models[_name]) {
  302. models[_name] = {};
  303. }
  304. models[_name][_id] = model;
  305. return model;
  306. });
  307. return existingModels.concat(newModels);
  308. };
  309. /**
  310. * Registers a model or document
  311. * Helper function to be able to register a single model/document, simply calls registerModels
  312. */
  313. const registerModel = async (
  314. documentOrModel: any,
  315. relations?: Record<string, string | string[]>
  316. ): Promise<Model> => {
  317. const [model] = await registerModels([documentOrModel], relations);
  318. return model;
  319. };
  320. /**
  321. * Loads one or more models for a provided model name and a provided model id or model ids, optionally including any relations
  322. * First fetches models from the already loaded models
  323. * Tries to fetch any missing models from the backend
  324. */
  325. const loadModels = async (
  326. modelName: string,
  327. modelIdsOrModelId: string | string[],
  328. relations?: Record<string, string | string[]>
  329. ): Promise<Map<string, Model | null>> => {
  330. const modelIds = Array.isArray(modelIdsOrModelId)
  331. ? modelIdsOrModelId
  332. : [modelIdsOrModelId];
  333. const existingModels = modelIds
  334. .map(_id => models[modelName] && models[modelName][_id])
  335. .filter(model => !!model);
  336. const existingModelIds = existingModels.map(model => model._id);
  337. const missingModelIds = modelIds.filter(
  338. _id => !existingModelIds.includes(_id)
  339. );
  340. console.info(
  341. "Load models",
  342. structuredClone(modelIds),
  343. structuredClone(existingModels),
  344. structuredClone(missingModelIds)
  345. );
  346. const fetchedModels = await findManyById(modelName, missingModelIds);
  347. console.log(999, modelName, missingModelIds, fetchedModels);
  348. const registeredModels = await registerModels(
  349. Object.values(fetchedModels)
  350. .filter(model => !!model)
  351. .concat(existingModels),
  352. relations
  353. );
  354. const modelsNotFound = modelIds
  355. .filter(
  356. modelId =>
  357. !registeredModels.find(model => model.getId() === modelId)
  358. )
  359. .map(modelId => [modelId, null]);
  360. console.log(123, registeredModels, modelsNotFound, fetchedModels);
  361. return Object.fromEntries(
  362. registeredModels
  363. .map(model => [model.getId(), model])
  364. .concat(modelsNotFound)
  365. );
  366. };
  367. return {
  368. models: readonly(models),
  369. permissions: readonly(permissions),
  370. subscriptions: readonly(subscriptions),
  371. onCreated,
  372. onUpdated,
  373. onDeleted,
  374. removeCallback,
  375. registerModel,
  376. registerModels,
  377. unregisterModels,
  378. loadModels,
  379. findById,
  380. findManyById,
  381. hasPermission
  382. };
  383. });