Olivier Massot 3 years ago
parent
commit
485232f3cf

+ 2 - 2
models/ApiResource.ts

@@ -1,10 +1,10 @@
-import {Model, ModelOptions} from "pinia-orm";
-import {randomUUID} from "crypto";
+import {Model} from "pinia-orm";
 
 /**
  * Base class for resources that can be fetched from the API
  */
 export class ApiResource extends Model {
+
     /**
      * Is it a newly created entity?
      *

+ 20 - 0
models/Core/Tagg.ts

@@ -0,0 +1,20 @@
+import ApiModel from "~/models/ApiModel";
+import {Attr, Str, Uid} from "pinia-orm/dist/decorators";
+
+/**
+ * AP2i Model : Tagg
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/Entity/Core/Tagg.php
+ */
+export class Tagg extends ApiModel {
+    static entity = 'taggs'
+
+    @Uid()
+    declare id: number | string | null
+
+    @Str('')
+    declare label: string
+
+    @Attr([])
+    declare organization: []
+}

+ 0 - 9
pages/organization.vue

@@ -1,9 +0,0 @@
-<template>
-  <main>
-    <h1>Organization</h1>
-    <NuxtPage/>
-  </main>
-</template>
-
-<script setup lang="ts">
-</script>

+ 0 - 0
pages/organization/id_.vue


+ 0 - 63
pages/organization/index.vue

@@ -1,63 +0,0 @@
-<template>
-  <main>
-    <p>Nom : {{ country }}</p>
-
-    <div>
-      <button @click="fetchPrevious">Previous</button>
-      <button @click="fetchNext">Next</button>
-    </div>
-    <nuxt-link to="/form">Edit</nuxt-link>
-  </main>
-</template>
-
-<script setup lang="ts">
-import {useEntityManager} from "~/composables/data/useEntityManager";
-import {Ref} from "@vue/reactivity";
-import {Country} from "~/models/Core/Country";
-import ApiResource from "~/models/ApiResource";
-
-const id: Ref<number> = ref(2)
-const em = useEntityManager()
-const country: Ref<ApiResource | null> = ref(null)
-
-const fetchCountry = async function () {
-  console.log('fetch ', id.value)
-  country.value = await em.fetch(Country, id.value)
-}
-await fetchCountry()
-
-const fetchNext = () => {
-  id.value += 1
-  console.log('next')
-  fetchCountry()
-}
-
-const fetchPrevious = async function () {
-  id.value -= 1
-  console.log('previous')
-  await fetchCountry()
-}
-
-
-</script>
-
-<style>
-a {
-  color: blue;
-}
-a:hover {
-  text-decoration: underline;
-}
-
-button {
-  border: grey solid 1px;
-  padding: 5px;
-  margin: 5px;
-}
-button:hover {
-  text-decoration: underline;
-}
-button:focus {
-  background-color: lightgrey;
-}
-</style>

+ 11 - 0
pages/poc.vue

@@ -0,0 +1,11 @@
+<template>
+  <main>
+    <div class="pa-8">
+      <h1>Organization</h1>
+      <NuxtPage/>
+    </div>
+  </main>
+</template>
+
+<script setup lang="ts">
+</script>

+ 87 - 0
pages/poc/[id].vue

@@ -0,0 +1,87 @@
+<template>
+  <main>
+    <p>Edit :{{ file }}</p>
+
+    <form @submit.prevent="" class="my-3">
+      <input v-model="file.name" type="text" />
+
+      <select v-model="file.status">
+        <option value="PENDING">Pending</option>
+        <option value="READY">Ready</option>
+        <option value="DELETED">Deleted</option>
+        <option value="ERROR">Error</option>
+      </select>
+
+      <button @click="cancelAndGoBack">Annuler</button>
+      <button type="submit" value="Enregistrer" @click="save">Enregistrer</button>
+
+      <button type="submit" value="Supprimer" @click="deleteAndGoBack" class="mt-5">Supprimer</button>
+    </form>
+  </main>
+</template>
+
+<script setup lang="ts">
+import {useEntityManager} from "~/composables/data/useEntityManager";
+import {ref, Ref} from "@vue/reactivity";
+import ApiResource from "~/models/ApiResource";
+import {File} from "~/models/Core/File";
+
+const route = useRoute()
+
+const id: Ref<number> = ref(parseInt(route.params.id as string))
+
+const em = useEntityManager()
+//@ts-ignore
+let file: ApiResource = reactive(await em.fetch(File, id.value))
+
+const save = async () => {
+  console.log('save')
+  //@ts-ignore
+  await em.persist(File, file)
+  navigateTo('/poc')
+}
+
+const cancelAndGoBack = async () => {
+  if (em.isNewEntity(File, id.value)) {
+    await em.delete(File, file)
+  } else {
+    em.reset(File, file)
+  }
+  navigateTo('/poc')
+}
+
+const deleteAndGoBack = async () => {
+  await em.delete(File, file)
+  navigateTo('/poc')
+}
+
+</script>
+
+<style>
+a {
+  color: blue;
+  cursor: pointer;
+}
+a:hover {
+  text-decoration: underline;
+}
+
+button {
+  border: grey solid 1px;
+  padding: 5px;
+  margin: 5px;
+  cursor: pointer;
+}
+button:hover {
+  text-decoration: underline;
+}
+button:focus {
+  background-color: lightgrey;
+}
+
+form {
+  display: flex;
+  flex-direction: column;
+  max-width: 500px;
+}
+</style>

+ 64 - 0
pages/poc/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <main>
+    <p>{{ file }}</p>
+
+    <div class="ma-3">
+      <button @click="fetchPrevious" class="mr-3">Previous</button>
+      <button @click="fetchNext">Next</button>
+    </div>
+    <div class="ma-3">
+      <nuxt-link :to="'/poc/' + file.id" class="mr-3">Edit</nuxt-link>
+      <nuxt-link to="/poc/new">New</nuxt-link>
+    </div>
+  </main>
+</template>
+
+<script setup lang="ts">
+  import {useEntityManager} from "~/composables/data/useEntityManager";
+  import {Ref} from "@vue/reactivity";
+  import ApiResource from "~/models/ApiResource";
+  import {File} from "~/models/Core/File";
+
+  const id: Ref<number> = ref(726900)
+  const em = useEntityManager()
+  const file: Ref<ApiResource | null> = ref(null)
+
+  const fetch = async function () {
+    console.log('fetch file ', id.value)
+    file.value = await em.fetch(File, id.value)
+  }
+  await fetch()
+
+  const fetchNext = () => {
+    id.value += 1
+    fetch()
+  }
+
+  const fetchPrevious = async function () {
+    id.value -= 1
+    await fetch()
+  }
+</script>
+
+<style>
+a {
+  color: blue;
+  cursor: pointer;
+}
+a:hover {
+  text-decoration: underline;
+}
+
+button {
+  border: grey solid 1px;
+  padding: 5px;
+  margin: 5px;
+  cursor: pointer;
+}
+button:hover {
+  text-decoration: underline;
+}
+button:focus {
+  background-color: lightgrey;
+}
+</style>

+ 72 - 0
pages/poc/new.vue

@@ -0,0 +1,72 @@
+<template>
+  <main>
+    <p>New :{{ file }}</p>
+
+    <form @submit.prevent="" class="my-3">
+      <input v-model="file.name" type="text" />
+
+      <select v-model="file.status">
+        <option value="PENDING">Pending</option>
+        <option value="READY">Ready</option>
+        <option value="DELETED">Deleted</option>
+        <option value="ERROR">Error</option>
+      </select>
+
+      <button @click="cancelAndGoBack">Annuler</button>
+      <button type="submit" value="Enregistrer" @click="save">Enregistrer</button>
+    </form>
+  </main>
+</template>
+
+<script setup lang="ts">
+import {useEntityManager} from "~/composables/data/useEntityManager";
+import {ref, Ref} from "@vue/reactivity";
+import ApiResource from "~/models/ApiResource";
+import {File} from "~/models/Core/File";
+
+const em = useEntityManager()
+//@ts-ignore
+let file: ApiResource = reactive(await em.new(File))
+
+const save = async () => {
+  console.log('save')
+  //@ts-ignore
+  await em.persist(File, file)
+  navigateTo('/poc')
+}
+
+const cancelAndGoBack = async () => {
+  await em.delete(File, file)
+  navigateTo('/poc')
+}
+
+</script>
+
+<style>
+a {
+  color: blue;
+  cursor: pointer;
+}
+a:hover {
+  text-decoration: underline;
+}
+
+button {
+  border: grey solid 1px;
+  padding: 5px;
+  margin: 5px;
+  cursor: pointer;
+}
+button:hover {
+  text-decoration: underline;
+}
+button:focus {
+  background-color: lightgrey;
+}
+
+form {
+  display: flex;
+  flex-direction: column;
+  max-width: 500px;
+}
+</style>

+ 1 - 1
services/data/apiRequestService.ts

@@ -71,7 +71,7 @@ class ApiRequestService {
         url: string,
         query: AssociativeArray | null = null
     ) {
-        return await this.request(HTTP_METHOD.GET, url, null, null, query)
+        return await this.request(HTTP_METHOD.DELETE, url, null, null, query)
     }
 
     /**

+ 5 - 2
services/data/connector/ohMyFetchConnector.ts

@@ -1,4 +1,4 @@
-import {AssociativeArray, Connector, HTTP_METHOD} from "../data";
+import {AssociativeArray, Connector, HTTP_METHOD} from "../data.d";
 import {$Fetch} from "nitropack";
 import {FetchOptions} from "ohmyfetch";
 
@@ -32,13 +32,16 @@ class OhMyFetchConnector implements Connector {
         params: AssociativeArray | null = null,
         query: AssociativeArray | null = null
     ) {
-        const config: FetchOptions = { body }
+        const config: FetchOptions = { method }
         if (params) {
             config.params = params
         }
         if (query) {
             config.query = query
         }
+        if (method === HTTP_METHOD.POST || method === HTTP_METHOD.PUT) {
+            config.body = body
+        }
 
         return this.fetch(url, config)
     }

+ 86 - 71
services/data/entityManager.ts

@@ -9,6 +9,7 @@ import {useProfileAccessStore} from "~/store/profile/access";
 import ApiResource from "~/models/ApiResource";
 import {MyProfile} from "~/models/Access/MyProfile";
 import { v4 as uuid4 } from 'uuid';
+import {useOrmStore} from "~/store/orm";
 
 /**
  * Entity manager: make operations on the models defined with the Pinia-Orm library
@@ -16,12 +17,10 @@ import { v4 as uuid4 } from 'uuid';
  * @see https://pinia-orm.codedredd.de/
  */
 class EntityManager {
-    private initialState: Map<string, Map<number, ApiResource>>;
     private apiRequestService: ApiRequestService;
 
     public constructor(apiRequestService: ApiRequestService) {
         this.apiRequestService = apiRequestService
-        this.initialState = new Map<string, Map<number, ApiResource>>()
     }
 
     /**
@@ -33,6 +32,35 @@ class EntityManager {
         return useRepo(model)
     }
 
+    /**
+     * Create a new instance of the given model
+     *
+     * @param model
+     * @param properties
+     */
+    public new(model: typeof ApiResource, properties: object = {}) {
+        const repository = this.getRepository(model)
+
+        const entity = repository.make(properties)
+
+        // @ts-ignore
+        if (!properties.hasOwnProperty('id') || !properties.id) {
+            // Object has no id yet, we give him a temporary one
+            entity.id = 'tmp' + uuid4()
+        }
+        repository.save(entity)
+
+        EntityManager.saveInitialState(model, entity)
+
+        return entity
+    }
+
+    private update(entity: ApiResource, newEntity: ApiResource) {
+        // On met à jour l'entité par référence, pour maintenir la réactivité lorsque l'entité est réactive
+        // @see http://underscorejs.org/#extend
+        useExtend(entity, newEntity)
+    }
+
     /**
      * Fetch one Entity / ApiResource by its id, save it to the store and returns it
      *
@@ -48,7 +76,6 @@ class EntityManager {
         // If the entity is already in the store and forceRefresh is false, return the object in store
         if (!forceRefresh) {
             const item = repository.find(id)
-            console.log(model, id, ' => item : ', item)
             if (item && typeof item !== 'undefined') {
                 return item
             }
@@ -80,7 +107,6 @@ class EntityManager {
      * @param entity
      */
     public async persist(model: typeof ApiModel, entity: ApiModel) {
-
         const repository = this.getRepository(model)
 
         let url = UrlBuilder.join('api', model.entity)
@@ -92,13 +118,14 @@ class EntityManager {
             url = UrlBuilder.join(url, String(entity.id))
             response = await this.apiRequestService.put(url, data)
         } else {
+            delete data.id
             response = await this.apiRequestService.post(url, data)
         }
 
-        const attributes = await HydraDenormalizer.denormalize(response)
-        const returnedEntity = this.new(model, attributes)
+        const hydraResponse = await HydraDenormalizer.denormalize(response)
+        const returnedEntity = this.new(model, hydraResponse.data)
 
-        this.storeInitialState(returnedEntity)
+        EntityManager.saveInitialState(model, returnedEntity)
 
         // Save data into the store
         repository.save(returnedEntity)
@@ -107,69 +134,46 @@ class EntityManager {
             await this.refreshProfile()
         }
 
-        return returnedEntity
+        this.update(entity, returnedEntity)
     }
 
     /**
      * Delete the entity from the datasource via the API
      *
      * @param model
-     * @param id
+     * @param entity
      */
-    public async delete(model: typeof ApiModel, id: number) {
+    public async delete(model: typeof ApiModel, entity: ApiResource) {
         const repository = this.getRepository(model)
-        const entity = repository.find(id) as ApiModel
-
-        if (!entity || typeof entity === 'undefined') {
-            throw new Error(model + ' ' + id + ' does not exists in store')
-        }
 
         // If object has been persisted to the datasource, send a delete request
         if (!entity.isNew()) {
-            const url = UrlBuilder.join('api', model.entity, String(id))
+            const url = UrlBuilder.join('api', model.entity, String(entity.id))
             await this.apiRequestService.delete(url)
         }
 
         // update the store
-        repository.destroy(id)
+        repository.destroy(entity.id)
     }
 
     /**
-     * Create a new instance of the given model
+     * Reset the entity to its initial state (i.e. the state it had when it was fetched from the API)
      *
      * @param model
-     * @param properties
+     * @param entity
      */
-    public new(model: typeof ApiResource, properties: object = {}) {
-        const repository = this.getRepository(model)
-
-        const entity = repository.make(properties)
-
-        if (!entity.id) {
-            // Object has no id yet, we give him a temporary one
-            entity.id = 'tmp' + uuid4()
+    public reset(model: typeof ApiResource, entity: ApiResource) {
+        const initialEntity = EntityManager.getInitialStateOf(model, entity.id)
+        if (initialEntity === null) {
+            throw new Error('no initial state recorded for this object - abort [' + model.entity + '/' + entity.id + ']')
         }
-        repository.save(entity)
 
-        this.storeInitialState(entity)
+        EntityManager.saveInitialState(model, initialEntity)
 
-        return entity
-    }
-
-    /**
-     * Reset the entity to its initial state (i.e. the state it had when it was fetched from the API)
-     *
-     * @param entity
-     */
-    public reset(entity: ApiResource) {
-        const state = this.getInitialStateOf(entity)
-        if (state === null) {
-            console.log('no initial state recorded for this object - abort', entity)
-            return
-        }
+        const repository = this.getRepository(model)
+        repository.save(initialEntity)
 
-        const repository = this.getRepository(EntityManager.getModelOf(entity))
-        repository.save(state)
+        this.update(entity, initialEntity)
     }
 
     /**
@@ -179,9 +183,9 @@ class EntityManager {
         const response = await this.apiRequestService.get('api/my_profile')
 
         // deserialize the response
-        const attributes = await HydraDenormalizer.denormalize(response).data
+        const hydraResponse = await HydraDenormalizer.denormalize(response)
 
-        const profile = this.new(MyProfile, attributes)
+        const profile = this.new(MyProfile, hydraResponse.data)
 
         const profileAccessStore = useProfileAccessStore()
         profileAccessStore.setProfile(profile)
@@ -197,41 +201,52 @@ class EntityManager {
         repository.flush()
     }
 
-    private storeInitialState(entity: ApiResource) {
-        const model = EntityManager.getModelOf(entity)
+    /**
+     * Is the entity a new one, or does it already exist in the data source (=API)
+     *
+     * @param model
+     * @param id
+     */
+    public isNewEntity(model: typeof ApiModel, id: number | string): boolean {
+        const repository = this.getRepository(model)
 
-        if (!this.initialState.has(model.entity)) {
-            this.initialState.set(model.entity, new Map())
+        const item = repository.find(id)
+        if (!item || typeof item === 'undefined') {
+            console.error(model.entity + '/' + id, ' does not exist!')
+            return false
         }
 
-        // @ts-ignore
-        this.initialState.get(model.entity).set(entity.id, useCloneDeep(entity))
-
-        // TODO: voir si structuredClone est compatible avec les navigateurs supportés, et si ça vaut le coup
-        //       de l'utiliser à la place du clonedeep de lodash
-        //        >> https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
+        return item.isNew()
     }
 
-    private getInitialStateOf(entity: ApiResource): ApiResource | null {
-        const model = EntityManager.getModelOf(entity)
-
-        // @ts-ignore
-        if (!this.initialState.has(model.entity) || !this.initialState.get(model.entity).has(entity.id)) {
-            return null
-        }
-
-        // @ts-ignore
-        return this.initialState.get(model.entity).get(entity.id)
+    /**
+     * Save the state of the entity in the store, so this state could be be restored later
+     *
+     * @param model
+     * @param entity
+     * @private
+     */
+    private static saveInitialState(model: typeof ApiResource, entity: ApiResource) {
+        useOrmStore().storeInitialValue(model, entity)
     }
 
     /**
-     * Return the model (=class) of the given entity
+     * Return the saved state of the entity from the store
      *
-     * @param entity
+     * @param model
+     * @param id
      * @private
      */
-    private static getModelOf(entity: ApiResource): typeof ApiResource {
-        return Object.getPrototypeOf(entity)
+    private static getInitialStateOf(model: typeof ApiResource, id: number): ApiResource | null {
+        const ormStore = useOrmStore()
+
+        //@ts-ignore
+        if (!ormStore.initialValues.has(model.entity) || !ormStore.initialValues.get(model.entity).has(id)) {
+            return null
+        }
+
+        //@ts-ignore
+        return ormStore.initialValues.get(model.entity).get(id)
     }
 }
 

+ 28 - 0
store/orm.ts

@@ -0,0 +1,28 @@
+import {ormState} from '~/types/interfaces'
+import {defineStore} from "pinia";
+import ApiResource from "~/models/ApiResource";
+
+export const useOrmStore = defineStore('orm', {
+    state: (): ormState => {
+        return {
+            /**
+             * Store the initial value of the entities, just after the fetch, to make reinitialization possible
+             */
+            initialValues: new Map<string, Map<number, ApiResource>>()
+        }
+    },
+    actions: {
+        storeInitialValue(model: typeof ApiResource, entity: ApiResource) {
+            if (!this.initialValues.has(model.entity)) {
+                this.initialValues.set(model.entity, new Map())
+            }
+
+            // @ts-ignore
+            this.initialValues.get(model.entity).set(entity.id, useCloneDeep(entity))
+
+            // TODO: voir si structuredClone est compatible avec les navigateurs supportés, et si ça vaut le coup
+            //       de l'utiliser à la place du clonedeep de lodash
+            //        >> https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
+        }
+    }
+})

+ 5 - 0
types/interfaces.d.ts

@@ -12,6 +12,7 @@ import {
   QUERY_TYPE,
   TYPE_ALERT,
 } from '~/types/enums'
+import ApiResource from "~/models/ApiResource";
 
 /**
  * Upgrade du @nuxt/types pour TypeScript
@@ -88,6 +89,10 @@ interface pageState {
   alerts: Array<Alert>
 }
 
+interface ormState {
+  initialValues: Map<string, Map<number, ApiResource>>
+}
+
 interface Historical {
   future?: boolean
   past?: boolean