model.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import { reactive, ref, computed } 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. export const useModelStore = defineStore("model", () => {
  8. const { runJob, subscribe, subscribeMany, unsubscribe } =
  9. useWebsocketStore();
  10. const models = ref([]);
  11. const permissions = ref({});
  12. const createdSubcription = ref(null);
  13. const subscriptions = ref({
  14. created: {},
  15. updated: {},
  16. deleted: {}
  17. });
  18. const loadedModelIds = computed(() =>
  19. models.value.map(model => `${model._name}.${model._id}`)
  20. );
  21. const getUserModelPermissions = async (modelName: string) => {
  22. if (permissions.value[modelName]) return permissions.value[modelName];
  23. const data = await runJob("data.users.getModelPermissions", {
  24. modelName
  25. });
  26. permissions.value[modelName] = data;
  27. return permissions.value[modelName];
  28. };
  29. const hasPermission = async (modelName: string, permission: string) => {
  30. const data = await getUserModelPermissions(modelName);
  31. return !!data[permission];
  32. };
  33. const unregisterModels = async modelIds =>
  34. forEachIn(
  35. Array.isArray(modelIds) ? modelIds : [modelIds],
  36. async modelId => {
  37. const model = models.value.find(model => model._id === modelId);
  38. if (!model || model.getUses() > 1) {
  39. model?.removeUse();
  40. return;
  41. }
  42. await model.unregisterRelations();
  43. const { updated, deleted } = model.getSubscriptions() ?? {};
  44. if (updated)
  45. await unsubscribe(
  46. `model.${model.getName()}.updated.${modelId}`,
  47. updated
  48. );
  49. if (deleted)
  50. await unsubscribe(
  51. `model.${model.getName()}.deleted.${modelId}`,
  52. deleted
  53. );
  54. models.value.splice(
  55. models.value.findIndex(model => model._id === modelId),
  56. 1
  57. );
  58. }
  59. );
  60. const onCreatedCallback = async (modelName: string, data) => {
  61. if (!subscriptions.value.created[modelName]) return;
  62. await forEachIn(
  63. Object.values(subscriptions.value.created[modelName]),
  64. async subscription => subscription(data) // TODO: Error handling
  65. );
  66. };
  67. const onCreated = async (
  68. modelName: string,
  69. callback: (data?: any) => any
  70. ) => {
  71. if (!createdSubcription.value)
  72. createdSubcription.value = await subscribe(
  73. `model.${modelName}.created`,
  74. data => onCreatedCallback(modelName, data)
  75. );
  76. const uuid = generateUuid();
  77. subscriptions.value.created[modelName] ??= {};
  78. subscriptions.value.created[modelName][uuid] = callback;
  79. return uuid;
  80. };
  81. const onUpdated = async (
  82. modelName: string,
  83. callback: (data?: any) => any
  84. ) => {
  85. const uuid = generateUuid();
  86. subscriptions.value.updated[modelName] ??= {};
  87. subscriptions.value.updated[modelName][uuid] = callback;
  88. return uuid;
  89. };
  90. const onUpdatedCallback = async (modelName: string, { doc }) => {
  91. const model = models.value.find(model => model._id === doc._id);
  92. if (model) model.updateData(doc);
  93. if (!subscriptions.value.updated[modelName]) return;
  94. await forEachIn(
  95. Object.values(subscriptions.value.updated[modelName]),
  96. async subscription => subscription(data) // TODO: Error handling
  97. );
  98. };
  99. const onDeleted = async (
  100. modelName: string,
  101. callback: (data?: any) => any
  102. ) => {
  103. const uuid = generateUuid();
  104. subscriptions.value.deleted[modelName] ??= {};
  105. subscriptions.value.deleted[modelName][uuid] = callback;
  106. return uuid;
  107. };
  108. const onDeletedCallback = async (modelName: string, data) => {
  109. const { oldDoc } = data;
  110. if (subscriptions.value.deleted[modelName])
  111. await forEachIn(
  112. Object.values(subscriptions.value.deleted[modelName]),
  113. async subscription => subscription(data) // TODO: Error handling
  114. );
  115. const index = models.value.findIndex(model => model._id === oldDoc._id);
  116. if (index > -1) await unregisterModels(oldDoc._id);
  117. };
  118. const removeCallback = async (
  119. modelName: string,
  120. type: "created" | "updated" | "deleted",
  121. uuid: string
  122. ) => {
  123. if (
  124. !subscriptions.value[type][modelName] ||
  125. !subscriptions.value[type][modelName][uuid]
  126. )
  127. return;
  128. delete subscriptions.value[type][modelName][uuid];
  129. if (
  130. type === "created" &&
  131. Object.keys(subscriptions.value.created[modelName]).length === 0
  132. ) {
  133. await unsubscribe(
  134. `model.${modelName}.created`,
  135. createdSubcription.value
  136. );
  137. createdSubcription.value = null;
  138. }
  139. };
  140. const registerModels = async (
  141. docs,
  142. relations?: Record<string, string | string[]>
  143. ) => {
  144. const documents = Array.isArray(docs) ? docs : [docs];
  145. const existingsRefs = documents.filter(document =>
  146. models.value.find(
  147. model =>
  148. model._id === document._id && model._name === document._name
  149. )
  150. );
  151. await forEachIn(existingsRefs, async model => {
  152. model.addUse();
  153. if (relations && relations[model._name])
  154. await model.loadRelations(relations[model._name]);
  155. });
  156. if (documents.length === existingsRefs.length) return existingsRefs;
  157. const missingDocuments = documents.filter(
  158. document =>
  159. !loadedModelIds.value.includes(
  160. `${document._name}.${document._id}`
  161. )
  162. );
  163. const channels = Object.fromEntries(
  164. missingDocuments.flatMap(document => [
  165. [
  166. `model.${document._name}.updated.${document._id}`,
  167. data => onUpdatedCallback(document._name, data)
  168. ],
  169. [
  170. `model.${document._name}.deleted.${document._id}`,
  171. data => onDeletedCallback(document._name, data)
  172. ]
  173. ])
  174. );
  175. const subscriptions = Object.entries(await subscribeMany(channels));
  176. const newRefs = await forEachIn(missingDocuments, async document => {
  177. const refSubscriptions = subscriptions.filter(([, { channel }]) =>
  178. channel.endsWith(document._id)
  179. );
  180. const [updated] = refSubscriptions.find(([, { channel }]) =>
  181. channel.includes("updated")
  182. );
  183. const [deleted] = refSubscriptions.find(([, { channel }]) =>
  184. channel.includes("deleted")
  185. );
  186. if (!updated || !deleted) return null;
  187. const model = reactive(new Model(document));
  188. model.setSubscriptions(updated, deleted);
  189. model.addUse();
  190. if (relations && relations[model._name])
  191. await model.loadRelations(relations[model._name]);
  192. return model;
  193. });
  194. models.value.push(...newRefs);
  195. return existingsRefs.concat(newRefs);
  196. };
  197. const findById = async (modelName: string, _id) => {
  198. const existingModel = models.value.find(model => model._id === _id);
  199. if (existingModel) return existingModel;
  200. return runJob(`data.${modelName}.findById`, { _id });
  201. };
  202. const findManyById = async (modelName: string, _ids: string[]) => {
  203. const existingModels = models.value.filter(model =>
  204. _ids.includes(model._id)
  205. );
  206. const existingIds = existingModels.map(model => model._id);
  207. const missingIds = _ids.filter(_id => !existingIds.includes(_id));
  208. let fetchedModels = [];
  209. if (missingIds.length > 0)
  210. fetchedModels = (await runJob(`data.${modelName}.findManyById`, {
  211. _ids: missingIds
  212. })) as unknown[];
  213. const allModels = existingModels.concat(fetchedModels);
  214. return Object.fromEntries(
  215. _ids.map(_id => [_id, allModels.find(model => model._id === _id)])
  216. );
  217. };
  218. const loadModels = async (
  219. modelName: string,
  220. modelIdsOrModelId: string | string[],
  221. relations?: Record<string, string | string[]>
  222. ) => {
  223. const modelIds = Array.isArray(modelIdsOrModelId)
  224. ? modelIdsOrModelId
  225. : [modelIdsOrModelId];
  226. const missingModelIds = modelIds.filter(
  227. modelId => !loadedModelIds.value.includes(`${modelName}.${modelId}`)
  228. );
  229. const missingModels = Object.values(
  230. await findManyById(modelName, missingModelIds)
  231. );
  232. await registerModels(missingModels, relations);
  233. return models.value.filter(
  234. model => modelIds.includes(model._id) && model._name === modelName
  235. );
  236. };
  237. return {
  238. models,
  239. permissions,
  240. subscriptions,
  241. onCreated,
  242. onUpdated,
  243. onDeleted,
  244. removeCallback,
  245. registerModels,
  246. unregisterModels,
  247. getUserModelPermissions,
  248. hasPermission,
  249. findById,
  250. findManyById,
  251. loadModels
  252. };
  253. });