model.ts 8.5 KB

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