entityManager.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import ApiRequestService from "./apiRequestService"
  2. import {Repository} from "pinia-orm"
  3. import UrlUtils from "~/services/utils/urlUtils"
  4. import ApiModel from "~/models/ApiModel"
  5. import ApiResource from "~/models/ApiResource"
  6. import {v4 as uuid4} from 'uuid'
  7. import type {AssociativeArray, Collection} from "~/types/data.d"
  8. import models from "~/models/models";
  9. import * as _ from "lodash-es"
  10. import HydraNormalizer from "~/services/data/normalizer/hydraNormalizer";
  11. /**
  12. * Entity manager: make operations on the models defined with the Pinia-Orm library
  13. *
  14. * TODO: améliorer le typage des méthodes sur le modèle de find()
  15. * @see https://pinia-orm.codedredd.de/
  16. */
  17. class EntityManager {
  18. protected CLONE_PREFIX = '_clone_'
  19. /**
  20. * In instance of ApiRequestService
  21. * @protected
  22. */
  23. protected apiRequestService: ApiRequestService
  24. /**
  25. * A method to retrieve the repository of a given ApiResource
  26. * @protected
  27. */
  28. protected _getRepo: (model: typeof ApiResource) => Repository<ApiResource>
  29. public constructor(
  30. apiRequestService: ApiRequestService,
  31. getRepo: (model: typeof ApiResource) => Repository<ApiResource>
  32. ) {
  33. this.apiRequestService = apiRequestService
  34. this._getRepo = getRepo
  35. }
  36. /**
  37. * Return the repository for the model
  38. *
  39. * @param model
  40. */
  41. public getRepository(model: typeof ApiResource): Repository<ApiResource> {
  42. return this._getRepo(model)
  43. }
  44. /**
  45. * Cast an object as an ApiResource
  46. * This in used internally to ensure the object is recognized as an ApiResource
  47. *
  48. * @param model
  49. * @param instance
  50. * @protected
  51. */
  52. // noinspection JSMethodCanBeStatic
  53. protected cast(model: typeof ApiResource, instance: ApiResource): ApiResource {
  54. return new model(instance)
  55. }
  56. /**
  57. * Return the model class with the given entity name
  58. *
  59. * @param entityName
  60. */
  61. public getModelFor(entityName: string): typeof ApiResource {
  62. return models[entityName]
  63. }
  64. /**
  65. * Return the model class from an Opentalent Api IRI
  66. *
  67. * @param iri An IRI of the form .../api/<entity>/...
  68. */
  69. public getModelFromIri(iri: string): typeof ApiResource {
  70. const matches = iri.match(/^\/api\/(\w+)\/.*/)
  71. if (!matches || !matches[1]) {
  72. throw new Error('cannot parse the IRI')
  73. }
  74. return this.getModelFor(matches[1])
  75. }
  76. /**
  77. * Create a new instance of the given model
  78. *
  79. * @param model
  80. * @param properties
  81. */
  82. public newInstance(model: typeof ApiResource, properties: object = {}): ApiResource {
  83. const repository = this.getRepository(model)
  84. let instance = repository.make(properties)
  85. // Keep track of the model
  86. // TODO : attendre de voir si utile ou non
  87. // instance.setModel(model)
  88. // @ts-ignore
  89. if (!properties.hasOwnProperty('id') || !properties.id) {
  90. // Object has no id yet, we give him a temporary one
  91. instance.id = 'tmp' + uuid4()
  92. }
  93. return this.save(model, instance, true)
  94. }
  95. /**
  96. * Save the model instance into the store
  97. *
  98. * @param model
  99. * @param instance
  100. * @param permanent Is the change already persisted in the datasource? If this is the case, the initial state of this
  101. * record is also updated.
  102. */
  103. public save(model: typeof ApiResource, instance: ApiResource, permanent: boolean = false): ApiResource {
  104. instance = this.cast(model, instance)
  105. if (permanent) {
  106. this.saveInitialState(model, instance)
  107. }
  108. return this.getRepository(model).save(instance)
  109. }
  110. /**
  111. * Find the model instance in the store
  112. * TODO: comment réagit la fonction si l'id n'existe pas dans le store?
  113. *
  114. * @param model
  115. * @param id
  116. */
  117. // @ts-ignore
  118. public find<T extends ApiResource>(model: typeof T, id: number | string): T {
  119. const repository = this.getRepository(model)
  120. return repository.find(id) as T
  121. }
  122. /**
  123. * Fetch an ApiModel / ApiResource by its id, save it to the store and returns it
  124. *
  125. * @param model Model of the object to fetch
  126. * @param id Id of the object to fetch
  127. * @param forceRefresh Force a new get request to the api ;
  128. * current object in store will be overwritten if it exists
  129. */
  130. public async fetch(model: typeof ApiResource, id: number, forceRefresh: boolean = false): Promise<ApiResource> {
  131. // If the model instance is already in the store and forceRefresh is false, return the object in store
  132. if (!forceRefresh) {
  133. const item = this.find(model, id)
  134. if (item && typeof item !== 'undefined') {
  135. return item
  136. }
  137. }
  138. // Else, get the object from the API
  139. const url = UrlUtils.join('api', model.entity, String(id))
  140. const response = await this.apiRequestService.get(url)
  141. // deserialize the response
  142. const attributes = HydraNormalizer.denormalize(response, model).data as object
  143. return this.newInstance(model, attributes)
  144. }
  145. /**
  146. * Fetch a collection of model instances
  147. * The content of `query` is converted into a query-string in the request URL
  148. *
  149. * @param model
  150. * @param query
  151. * @param parent
  152. */
  153. public async fetchCollection(model: typeof ApiResource, parent: ApiResource | null, query: AssociativeArray | null = null): Promise<Collection> {
  154. let url
  155. if (parent !== null) {
  156. url = UrlUtils.join('api', parent.entity, '' + parent.id, model.entity)
  157. } else {
  158. url = UrlUtils.join('api', model.entity)
  159. }
  160. const response = await this.apiRequestService.get(url, query)
  161. // deserialize the response
  162. const apiCollection = HydraNormalizer.denormalize(response, model)
  163. const items = apiCollection.data.map((attributes: object) => {
  164. return this.newInstance(model, attributes)
  165. })
  166. return {
  167. items,
  168. totalItems: apiCollection.metadata.totalItems,
  169. pagination: {
  170. first: apiCollection.metadata.firstPage || 1,
  171. last: apiCollection.metadata.lastPage || 1,
  172. next: apiCollection.metadata.nextPage || undefined,
  173. previous: apiCollection.metadata.previousPage || undefined,
  174. }
  175. }
  176. }
  177. /**
  178. * Persist the model instance as it is in the store into the data source via the API
  179. *
  180. * @param model
  181. * @param instance
  182. */
  183. public async persist(model: typeof ApiModel, instance: ApiModel) {
  184. // Recast in case class definition has been "lost"
  185. // TODO: attendre de voir si cette ligne est nécessaire
  186. instance = this.cast(model, instance)
  187. let url = UrlUtils.join('api', model.entity)
  188. let response
  189. const data: any = HydraNormalizer.normalizeEntity(instance)
  190. if (!instance.isNew()) {
  191. url = UrlUtils.join(url, String(instance.id))
  192. response = await this.apiRequestService.put(url, data)
  193. } else {
  194. delete data.id
  195. response = await this.apiRequestService.post(url, data)
  196. }
  197. const hydraResponse = await HydraNormalizer.denormalize(response, model)
  198. const newInstance = this.newInstance(model, hydraResponse.data)
  199. // Si l'instance était nouvelle avant d'être persistée, elle vient désormais de recevoir un id définitif. On
  200. // peut donc supprimer l'instance temporaire.
  201. if (instance.isNew()) {
  202. this.removeTempAfterPersist(model, instance.id)
  203. }
  204. return newInstance
  205. }
  206. /**
  207. * Send an update request (PUT) to the API with the given data on an existing model instance
  208. *
  209. * @param model
  210. * @param id
  211. * @param data
  212. */
  213. public async patch(model: typeof ApiModel, id: number, data: AssociativeArray) {
  214. let url = UrlUtils.join('api', model.entity, ''+id)
  215. const body = JSON.stringify(data)
  216. const response = await this.apiRequestService.put(url, body)
  217. const hydraResponse = await HydraNormalizer.denormalize(response, model)
  218. return this.newInstance(model, hydraResponse.data)
  219. }
  220. /**
  221. * Delete the model instance from the datasource via the API
  222. *
  223. * @param model
  224. * @param instance
  225. */
  226. public async delete(model: typeof ApiModel, instance: ApiResource) {
  227. const repository = this.getRepository(model)
  228. // If object has been persisted to the datasource, send a delete request
  229. if (!instance.isNew()) {
  230. const url = UrlUtils.join('api', model.entity, String(instance.id))
  231. await this.apiRequestService.delete(url)
  232. }
  233. // reactiveUpdate the store
  234. repository.destroy(instance.id)
  235. }
  236. /**
  237. * Reset the model instance to its initial state (i.e. the state it had when it was fetched from the API)
  238. *
  239. * @param model
  240. * @param instance
  241. */
  242. public reset(model: typeof ApiResource, instance: ApiResource) {
  243. const initialInstance = this.getInitialStateOf(model, instance.id)
  244. if (initialInstance === null) {
  245. throw new Error('no initial state recorded for this object - abort [' + model.entity + '/' + instance.id + ']')
  246. }
  247. const repository = this.getRepository(model)
  248. repository.save(initialInstance)
  249. return initialInstance
  250. }
  251. /**
  252. * Delete all records in the repository of the model
  253. *
  254. * @param model
  255. */
  256. public flush(model: typeof ApiModel) {
  257. const repository = this.getRepository(model)
  258. repository.flush()
  259. }
  260. /**
  261. * Is the model instance a new one, or does it already exist in the data source (=API)
  262. *
  263. * This is a convenient way of testing a model instance you did not already fetch, else prefer the use of the
  264. * isNew() method of ApiResource
  265. *
  266. * @param model
  267. * @param id
  268. */
  269. public isNewInstance(model: typeof ApiModel, id: number | string): boolean {
  270. const repository = this.getRepository(model)
  271. const item = repository.find(id)
  272. if (!item || typeof item === 'undefined') {
  273. // TODO: est-ce qu'il ne faudrait pas lever une erreur ici plutôt?
  274. console.error(model.entity + '/' + id + ' does not exist!')
  275. return false
  276. }
  277. return item.isNew()
  278. }
  279. /**
  280. * Save the state of the model instance in the store, so this state could be restored later
  281. *
  282. * @param model
  283. * @param instance
  284. * @private
  285. */
  286. protected saveInitialState(model: typeof ApiResource, instance: ApiResource) {
  287. const repository = this.getRepository(model)
  288. // Clone and prefix id
  289. const clone = _.cloneDeep(instance)
  290. clone.id = this.CLONE_PREFIX + clone.id
  291. repository.save(clone)
  292. }
  293. /**
  294. * Return the saved state of the model instance from the store
  295. *
  296. * @param model
  297. * @param id
  298. * @private
  299. */
  300. protected getInitialStateOf(model: typeof ApiResource, id: string | number): ApiResource | null {
  301. const repository = this.getRepository(model)
  302. // Find the clone by id
  303. const instance = repository.find(this.CLONE_PREFIX + id)
  304. if (instance === null) {
  305. return null
  306. }
  307. // Restore the initial id
  308. instance.id = id
  309. return instance
  310. }
  311. /**
  312. * Delete the temporary model instance from the repo after it was persisted via the api, replaced by the instance
  313. * that has been returned by the api with is definitive id.
  314. *
  315. * @param model
  316. * @param tempInstanceId
  317. * @private
  318. */
  319. protected removeTempAfterPersist(model: typeof ApiResource, tempInstanceId: number | string) {
  320. const repository = this.getRepository(model)
  321. const instance = repository.find(tempInstanceId)
  322. if (!instance || typeof instance === 'undefined') {
  323. // TODO: il vaudrait peut-être mieux lever une erreur ici?
  324. console.error(model.entity + '/' + tempInstanceId + ' does not exist!')
  325. return
  326. }
  327. if (!instance.isNew()) {
  328. throw new Error('Error: Can not remove a non-temporary model instance')
  329. }
  330. repository.destroy(tempInstanceId)
  331. repository.destroy(this.CLONE_PREFIX + tempInstanceId)
  332. }
  333. }
  334. export default EntityManager