|
|
@@ -0,0 +1,394 @@
|
|
|
+import ApiRequestService from "./apiRequestService"
|
|
|
+import {Repository} from "pinia-orm"
|
|
|
+import UrlUtils from "~/services/utils/urlUtils"
|
|
|
+import ApiModel from "~/models/ApiModel"
|
|
|
+import ApiResource from "~/models/ApiResource"
|
|
|
+import {v4 as uuid4} from 'uuid'
|
|
|
+import type {AssociativeArray, Collection} from "~/types/data.d"
|
|
|
+import models from "~/models/models";
|
|
|
+import * as _ from "lodash-es"
|
|
|
+import HydraNormalizer from "~/services/data/normalizer/hydraNormalizer";
|
|
|
+
|
|
|
+/**
|
|
|
+ * Entity manager: make operations on the models defined with the Pinia-Orm library
|
|
|
+ *
|
|
|
+ * TODO: améliorer le typage des méthodes sur le modèle de find()
|
|
|
+ * @see https://pinia-orm.codedredd.de/
|
|
|
+ */
|
|
|
+class EntityManager {
|
|
|
+ protected CLONE_PREFIX = '_clone_'
|
|
|
+
|
|
|
+ /**
|
|
|
+ * In instance of ApiRequestService
|
|
|
+ * @protected
|
|
|
+ */
|
|
|
+ protected apiRequestService: ApiRequestService
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A method to retrieve the repository of a given ApiResource
|
|
|
+ * @protected
|
|
|
+ */
|
|
|
+ protected _getRepo: (model: typeof ApiResource) => Repository<ApiResource>
|
|
|
+
|
|
|
+ public constructor(
|
|
|
+ apiRequestService: ApiRequestService,
|
|
|
+ getRepo: (model: typeof ApiResource) => Repository<ApiResource>
|
|
|
+ ) {
|
|
|
+ this.apiRequestService = apiRequestService
|
|
|
+ this._getRepo = getRepo
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the repository for the model
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ */
|
|
|
+ public getRepository(model: typeof ApiResource): Repository<ApiResource> {
|
|
|
+ return this._getRepo(model)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cast an object as an ApiResource
|
|
|
+ * This in used internally to ensure the object is recognized as an ApiResource
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ * @protected
|
|
|
+ */
|
|
|
+ // noinspection JSMethodCanBeStatic
|
|
|
+ protected cast(model: typeof ApiResource, instance: ApiResource): ApiResource {
|
|
|
+ return new model(instance)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the model class with the given entity name
|
|
|
+ *
|
|
|
+ * @param entityName
|
|
|
+ */
|
|
|
+ public getModelFor(entityName: string): typeof ApiResource {
|
|
|
+ return models[entityName]
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the model class from an Opentalent Api IRI
|
|
|
+ *
|
|
|
+ * @param iri An IRI of the form .../api/<entity>/...
|
|
|
+ */
|
|
|
+ public getModelFromIri(iri: string): typeof ApiResource {
|
|
|
+ const matches = iri.match(/^\/api\/(\w+)\/.*/)
|
|
|
+ if (!matches || !matches[1]) {
|
|
|
+ throw new Error('cannot parse the IRI')
|
|
|
+ }
|
|
|
+ return this.getModelFor(matches[1])
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a new instance of the given model
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param properties
|
|
|
+ */
|
|
|
+ public newInstance(model: typeof ApiResource, properties: object = {}): ApiResource {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ let instance = repository.make(properties)
|
|
|
+
|
|
|
+ // Keep track of the model
|
|
|
+ // TODO : attendre de voir si utile ou non
|
|
|
+ // instance.setModel(model)
|
|
|
+
|
|
|
+ // @ts-ignore
|
|
|
+ if (!properties.hasOwnProperty('id') || !properties.id) {
|
|
|
+ // Object has no id yet, we give him a temporary one
|
|
|
+ instance.id = 'tmp' + uuid4()
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.save(model, instance, true)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Save the model instance into the store
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ * @param permanent Is the change already persisted in the datasource? If this is the case, the initial state of this
|
|
|
+ * record is also updated.
|
|
|
+ */
|
|
|
+ public save(model: typeof ApiResource, instance: ApiResource, permanent: boolean = false): ApiResource {
|
|
|
+ instance = this.cast(model, instance)
|
|
|
+
|
|
|
+ if (permanent) {
|
|
|
+ this.saveInitialState(model, instance)
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.getRepository(model).save(instance)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Find the model instance in the store
|
|
|
+ * TODO: comment réagit la fonction si l'id n'existe pas dans le store?
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param id
|
|
|
+ */
|
|
|
+ // @ts-ignore
|
|
|
+ public find<T extends ApiResource>(model: typeof T, id: number | string): T {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+ return repository.find(id) as T
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fetch an ApiModel / ApiResource by its id, save it to the store and returns it
|
|
|
+ *
|
|
|
+ * @param model Model of the object to fetch
|
|
|
+ * @param id Id of the object to fetch
|
|
|
+ * @param forceRefresh Force a new get request to the api ;
|
|
|
+ * current object in store will be overwritten if it exists
|
|
|
+ */
|
|
|
+ public async fetch(model: typeof ApiResource, id: number, forceRefresh: boolean = false): Promise<ApiResource> {
|
|
|
+
|
|
|
+ // If the model instance is already in the store and forceRefresh is false, return the object in store
|
|
|
+ if (!forceRefresh) {
|
|
|
+ const item = this.find(model, id)
|
|
|
+ if (item && typeof item !== 'undefined') {
|
|
|
+ return item
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Else, get the object from the API
|
|
|
+ const url = UrlUtils.join('api', model.entity, String(id))
|
|
|
+ const response = await this.apiRequestService.get(url)
|
|
|
+
|
|
|
+ // deserialize the response
|
|
|
+ const attributes = HydraNormalizer.denormalize(response, model).data as object
|
|
|
+
|
|
|
+ return this.newInstance(model, attributes)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fetch a collection of model instances
|
|
|
+ * The content of `query` is converted into a query-string in the request URL
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param query
|
|
|
+ * @param parent
|
|
|
+ */
|
|
|
+ public async fetchCollection(model: typeof ApiResource, parent: ApiResource | null, query: AssociativeArray | null = null): Promise<Collection> {
|
|
|
+ let url
|
|
|
+
|
|
|
+ if (parent !== null) {
|
|
|
+ url = UrlUtils.join('api', parent.entity, '' + parent.id, model.entity)
|
|
|
+ } else {
|
|
|
+ url = UrlUtils.join('api', model.entity)
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await this.apiRequestService.get(url, query)
|
|
|
+
|
|
|
+ // deserialize the response
|
|
|
+ const apiCollection = HydraNormalizer.denormalize(response, model)
|
|
|
+
|
|
|
+ const items = apiCollection.data.map((attributes: object) => {
|
|
|
+ return this.newInstance(model, attributes)
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ items,
|
|
|
+ totalItems: apiCollection.metadata.totalItems,
|
|
|
+ pagination: {
|
|
|
+ first: apiCollection.metadata.firstPage || 1,
|
|
|
+ last: apiCollection.metadata.lastPage || 1,
|
|
|
+ next: apiCollection.metadata.nextPage || undefined,
|
|
|
+ previous: apiCollection.metadata.previousPage || undefined,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Persist the model instance as it is in the store into the data source via the API
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ */
|
|
|
+ public async persist(model: typeof ApiModel, instance: ApiModel) {
|
|
|
+ // Recast in case class definition has been "lost"
|
|
|
+ // TODO: attendre de voir si cette ligne est nécessaire
|
|
|
+ instance = this.cast(model, instance)
|
|
|
+
|
|
|
+ let url = UrlUtils.join('api', model.entity)
|
|
|
+ let response
|
|
|
+
|
|
|
+ const data: any = HydraNormalizer.normalizeEntity(instance)
|
|
|
+
|
|
|
+ if (!instance.isNew()) {
|
|
|
+ url = UrlUtils.join(url, String(instance.id))
|
|
|
+ response = await this.apiRequestService.put(url, data)
|
|
|
+ } else {
|
|
|
+ delete data.id
|
|
|
+ response = await this.apiRequestService.post(url, data)
|
|
|
+ }
|
|
|
+
|
|
|
+ const hydraResponse = HydraNormalizer.denormalize(response, model)
|
|
|
+
|
|
|
+ const newInstance = this.newInstance(model, hydraResponse.data)
|
|
|
+
|
|
|
+ // Si l'instance était nouvelle avant d'être persistée, elle vient désormais de recevoir un id définitif. On
|
|
|
+ // peut donc supprimer l'instance temporaire.
|
|
|
+ if (instance.isNew()) {
|
|
|
+ this.removeTempAfterPersist(model, instance.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ return newInstance
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Send an update request (PUT) to the API with the given data on an existing model instance
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param id
|
|
|
+ * @param data
|
|
|
+ */
|
|
|
+ public async patch(model: typeof ApiModel, id: number, data: AssociativeArray) {
|
|
|
+ let url = UrlUtils.join('api', model.entity, ''+id)
|
|
|
+
|
|
|
+ const body = JSON.stringify(data)
|
|
|
+ const response = await this.apiRequestService.put(url, body)
|
|
|
+
|
|
|
+ const hydraResponse = await HydraNormalizer.denormalize(response, model)
|
|
|
+ return this.newInstance(model, hydraResponse.data)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete the model instance from the datasource via the API
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ */
|
|
|
+ public async delete(model: typeof ApiModel, instance: ApiResource) {
|
|
|
+
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ // If object has been persisted to the datasource, send a delete request
|
|
|
+ if (!instance.isNew()) {
|
|
|
+ const url = UrlUtils.join('api', model.entity, String(instance.id))
|
|
|
+ await this.apiRequestService.delete(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ // reactiveUpdate the store
|
|
|
+ repository.destroy(instance.id)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reset the model instance to its initial state (i.e. the state it had when it was fetched from the API)
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ */
|
|
|
+ public reset(model: typeof ApiResource, instance: ApiResource) {
|
|
|
+ const initialInstance = this.getInitialStateOf(model, instance.id)
|
|
|
+ if (initialInstance === null) {
|
|
|
+ throw new Error('no initial state recorded for this object - abort [' + model.entity + '/' + instance.id + ']')
|
|
|
+ }
|
|
|
+
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+ repository.save(initialInstance)
|
|
|
+
|
|
|
+ return initialInstance
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete all records in the repository of the model
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ */
|
|
|
+ public flush(model: typeof ApiModel) {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+ repository.flush()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Is the model instance a new one, or does it already exist in the data source (=API)
|
|
|
+ *
|
|
|
+ * This is a convenient way of testing a model instance you did not already fetch, else prefer the use of the
|
|
|
+ * isNew() method of ApiResource
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param id
|
|
|
+ */
|
|
|
+ public isNewInstance(model: typeof ApiModel, id: number | string): boolean {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ const item = repository.find(id)
|
|
|
+ if (!item || typeof item === 'undefined') {
|
|
|
+ // TODO: est-ce qu'il ne faudrait pas lever une erreur ici plutôt?
|
|
|
+ console.error(model.entity + '/' + id + ' does not exist!')
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return item.isNew()
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Save the state of the model instance in the store, so this state could be restored later
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param instance
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ protected saveInitialState(model: typeof ApiResource, instance: ApiResource) {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ // Clone and prefix id
|
|
|
+ const clone = _.cloneDeep(instance)
|
|
|
+ clone.id = this.CLONE_PREFIX + clone.id
|
|
|
+
|
|
|
+ repository.save(clone)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Return the saved state of the model instance from the store
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param id
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ protected getInitialStateOf(model: typeof ApiResource, id: string | number): ApiResource | null {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ // Find the clone by id
|
|
|
+ const instance = repository.find(this.CLONE_PREFIX + id)
|
|
|
+ if (instance === null) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ // Restore the initial id
|
|
|
+ instance.id = id
|
|
|
+ return instance
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete the temporary model instance from the repo after it was persisted via the api, replaced by the instance
|
|
|
+ * that has been returned by the api with is definitive id.
|
|
|
+ *
|
|
|
+ * @param model
|
|
|
+ * @param tempInstanceId
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ protected removeTempAfterPersist(model: typeof ApiResource, tempInstanceId: number | string) {
|
|
|
+ const repository = this.getRepository(model)
|
|
|
+
|
|
|
+ const instance = repository.find(tempInstanceId)
|
|
|
+ if (!instance || typeof instance === 'undefined') {
|
|
|
+ // TODO: il vaudrait peut-être mieux lever une erreur ici?
|
|
|
+ console.error(model.entity + '/' + tempInstanceId + ' does not exist!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!instance.isNew()) {
|
|
|
+ throw new Error('Error: Can not remove a non-temporary model instance')
|
|
|
+ }
|
|
|
+
|
|
|
+ repository.destroy(tempInstanceId)
|
|
|
+ repository.destroy(this.CLONE_PREFIX + tempInstanceId)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default EntityManager
|