فهرست منبع

fetchAll and pagination ok

Olivier Massot 3 سال پیش
والد
کامیت
bfceff6720

+ 36 - 15
pages/poc/index.vue

@@ -1,13 +1,18 @@
 <template>
   <main>
-    <p>{{ file }}</p>
+    <ul>
+      <li v-for="file in files">
+        <nuxt-link :to="'/poc/' + file.id" class="mr-3">{{ file.name }}</nuxt-link>
+      </li>
+    </ul>
 
     <div class="ma-3">
-      <button @click="fetchPrevious" class="mr-3">Previous</button>
-      <button @click="fetchNext">Next</button>
+      <button @click="goToPreviousPage" class="mr-3">Previous page ({{ previousPage }})</button>
+      <span class="mx-3">Page : {{ page }}</span>
+      <button @click="goToNextPage">Next page ({{ nextPage }})</button>
+      <span class="mx-2"> (Last page : {{ lastPage }})</span>
     </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>
@@ -21,23 +26,39 @@
 
   const id: Ref<number> = ref(726900)
   const em = useEntityManager()
-  const file: Ref<ApiResource | null> = ref(null)
+  let files: Array<ApiResource> = reactive([])
+  const page: Ref<number> = ref(1)
 
-  const fetch = async function () {
-    console.log('fetch file ', id.value)
-    file.value = await em.fetch(File, id.value)
+  const firstPage: Ref<number> = ref(1)
+  const lastPage: Ref<number> = ref(1)
+  const previousPage: Ref<number | null> = ref(null)
+  const nextPage: Ref<number | null> = ref(null)
+
+  const fetchAll = async function() {
+    const collection = await em.fetchAll(File, page.value)
+
+    // On met à jour l'array sans la remplacer pour ne pas perdre la réactivité
+    em.reactiveUpdate(files, collection.items)
+
+    firstPage.value = collection.firstPage || 1
+    lastPage.value = collection.lastPage || 1
+    previousPage.value = collection.previousPage || null
+    nextPage.value = collection.nextPage || null
   }
-  await fetch()
+  await fetchAll()
 
-  const fetchNext = () => {
-    id.value += 1
-    fetch()
+  const goToPreviousPage = async function () {
+    if (page.value > 1) {
+      page.value--
+      await fetchAll()
+    }
   }
 
-  const fetchPrevious = async function () {
-    id.value -= 1
-    await fetch()
+  const goToNextPage = async function () {
+    page.value++
+    await fetchAll()
   }
+
 </script>
 
 <style>

+ 62 - 12
services/data/entityManager.ts

@@ -1,6 +1,6 @@
 import ApiRequestService from "./apiRequestService";
 import {Repository, useRepo} from "pinia-orm";
-import UrlBuilder from "~/services/utils/urlBuilder";
+import Url from "~/services/utils/url";
 import ModelNormalizer from "./serializer/normalizer/modelNormalizer";
 import HydraDenormalizer from "./serializer/denormalizer/hydraDenormalizer";
 import ApiModel from "~/models/ApiModel";
@@ -8,7 +8,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 {AssociativeArray} from "~/types/data.d";
+import {AssociativeArray, Collection} from "~/types/data.d";
 
 /**
  * Entity manager: make operations on the models defined with the Pinia-Orm library
@@ -56,14 +56,41 @@ class EntityManager {
         return entity
     }
 
-    private reactiveUpdate(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
+    /**
+     * On met à jour directement l'entité par référence ou la liste d'entités,
+     * pour maintenir la réactivité lorsque l'entité ou l'array est déclarée comme réactive
+     *
+     * Attention à ce que le sujet et la nouvelle valeur soient des objets de même type
+     *
+     * @see http://underscorejs.org/#extend
+     * @param subject
+     * @param newValue
+     */
+    public reactiveUpdate(subject: ApiResource | Array<ApiResource>, newValue: ApiResource | Array<ApiResource>) {
+        if (typeof subject !== typeof newValue) { // TODO: remplacer par des règles typescript
+            console.log('Error : subject and new value have to share the same type')
+            return
+        }
+        if (Array.isArray(subject)) {
+            this.reactiveUpdateArray(subject as Array<ApiResource>, newValue as Array<ApiResource>)
+        } else {
+            this.reactiveUpdateItem(subject as ApiResource, newValue as ApiResource)
+        }
+    }
+
+    private reactiveUpdateItem(entity: ApiResource, newEntity: ApiResource) {
         useExtend(entity, newEntity)
     }
 
+    private reactiveUpdateArray(items: Array<ApiResource>, newItems: Array<ApiResource>) {
+        items.length = 0
+        newItems.forEach((f: ApiResource) => {
+            items.push(f)
+        })
+    }
+
     /**
-     * Fetch one Entity / ApiResource by its id, save it to the store and returns it
+     * Fetch an Entity / 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
@@ -83,7 +110,7 @@ class EntityManager {
         }
 
         // Else, get the object from the API
-        const url = UrlBuilder.join('api', model.entity, String(id))
+        const url = Url.join('api', model.entity, String(id))
 
         const response = await this.apiRequestService.get(url)
 
@@ -97,8 +124,31 @@ class EntityManager {
         // TODO: implement
     }
 
-    public fetchAll(model: typeof ApiResource) {
-        // TODO: implement
+    public async fetchAll(model: typeof ApiResource, page: number = 1): Promise<Collection> {
+        let url = Url.join('api', model.entity)
+
+        if (page !== 1) {
+            url = Url.join(url, '?page=' + page)
+        }
+        console.log(url)
+
+        const response = await this.apiRequestService.get(url)
+
+        // deserialize the response
+        const collection = HydraDenormalizer.denormalize(response)
+
+        const items = collection.data.map((attributes: object) => {
+            return this.new(model, attributes)
+        })
+
+        return {
+            items,
+            totalItems: collection.metadata.totalItems,
+            firstPage: collection.metadata.firstPage,
+            lastPage: collection.metadata.lastPage,
+            nextPage: collection.metadata.nextPage,
+            previousPage: collection.metadata.previousPage,
+        }
     }
 
     /**
@@ -110,13 +160,13 @@ class EntityManager {
     public async persist(model: typeof ApiModel, entity: ApiModel) {
         const repository = this.getRepository(model)
 
-        let url = UrlBuilder.join('api', model.entity)
+        let url = Url.join('api', model.entity)
         let response
 
         const data = ModelNormalizer.normalize(entity)
 
         if (!entity.isNew()) {
-            url = UrlBuilder.join(url, String(entity.id))
+            url = Url.join(url, String(entity.id))
             response = await this.apiRequestService.put(url, data)
         } else {
             delete data.id
@@ -149,7 +199,7 @@ class EntityManager {
 
         // If object has been persisted to the datasource, send a delete request
         if (!entity.isNew()) {
-            const url = UrlBuilder.join('api', model.entity, String(entity.id))
+            const url = Url.join('api', model.entity, String(entity.id))
             await this.apiRequestService.delete(url)
         }
 

+ 2 - 2
services/data/enumManager.ts

@@ -1,5 +1,5 @@
 import ApiRequestService from "./apiRequestService";
-import UrlBuilder from "~/services/utils/urlBuilder";
+import Url from "~/services/utils/url";
 import HydraDenormalizer from "~/services/data/serializer/denormalizer/hydraDenormalizer";
 import {AssociativeArray} from "~/types/data.d";
 
@@ -11,7 +11,7 @@ class EnumManager {
     }
 
     public async fetch(enumName: string): Promise<Array<AssociativeArray>> {
-        const url = UrlBuilder.join('api', 'enum', enumName)
+        const url = Url.join('api', 'enum', enumName)
 
         const response = await this.apiRequestService.get(url)
 

+ 16 - 25
services/data/serializer/denormalizer/hydraDenormalizer.ts

@@ -1,4 +1,5 @@
 import {AnyJson, ApiResponse, HydraMetadata, METADATA_TYPE} from "~/types/data.d";
+import Url from "~/services/utils/url";
 
 /**
  * Classe permettant d'assurer la dé-normalization d'un objet Hydra en JSON
@@ -27,7 +28,7 @@ class HydraDenormalizer {
   private static parseItem (hydraData: AnyJson): ApiResponse {
     return {
       data: hydraData,
-      metadata: HydraDenormalizer.definedMetadataForItem(hydraData)
+      metadata: HydraDenormalizer.constructMetadataForItem(hydraData)
     }
   }
 
@@ -39,13 +40,12 @@ class HydraDenormalizer {
   private static parseCollection (hydraData: AnyJson): ApiResponse {
     const collectionResponse:ApiResponse = {
       data:hydraData['hydra:member'],
-      metadata : HydraDenormalizer.definedMetadataForCollection(hydraData)
+      metadata : HydraDenormalizer.constructMetadataForCollection(hydraData)
     }
 
     // collectionResponse.order = {}
     // collectionResponse.search = {}
 
-
     // Populate href property for all elements of the collection
     for (const key in collectionResponse.data) {
       const value = collectionResponse.data[key]
@@ -72,7 +72,7 @@ class HydraDenormalizer {
    *
    * @param {AnyJson} hydraData
    */
-  private static definedMetadataForItem (hydraData: AnyJson): AnyJson {
+  private static constructMetadataForItem (hydraData: AnyJson): AnyJson {
     const metadata: HydraMetadata = {}
 
     // if (hydraData['hydra:previous']) {
@@ -102,16 +102,16 @@ class HydraDenormalizer {
    * @param data
    * @private
    */
-  private static  definedMetadataForCollection(data:AnyJson){
-    const metadata:HydraMetadata = {
+  private static  constructMetadataForCollection(data:AnyJson){
+    const metadata: HydraMetadata = {
       totalItems: data['hydra:totalItems']
     }
 
     if(data['hydra:view']){
-      metadata.firstPage = HydraDenormalizer.getPageNumber(data['hydra:view']['hydra:first'])
-      metadata.lastPage = HydraDenormalizer.getPageNumber(data['hydra:view']['hydra:last'])
-      metadata.nextPage = HydraDenormalizer.getPageNumber(data['hydra:view']['hydra:next'])
-      metadata.previousPage = HydraDenormalizer.getPageNumber(data['hydra:view']['hydra:previous'])
+      metadata.firstPage = HydraDenormalizer.extractPageFromUri(data['hydra:view']['hydra:first'], 1) as number
+      metadata.lastPage = HydraDenormalizer.extractPageFromUri(data['hydra:view']['hydra:last'], 1) as number
+      metadata.nextPage = HydraDenormalizer.extractPageFromUri(data['hydra:view']['hydra:next']) ?? undefined
+      metadata.previousPage = HydraDenormalizer.extractPageFromUri(data['hydra:view']['hydra:previous']) ?? undefined
     }
 
     metadata.type = METADATA_TYPE.COLLECTION
@@ -119,21 +119,6 @@ class HydraDenormalizer {
     return metadata
   }
 
-  /**
-   * Parse an URI to retrieve the page number
-   *
-   * @param uri
-   * @private
-   */
-  private static getPageNumber(uri:string):number {
-    if(uri){
-      const urlParams = new URLSearchParams(uri);
-      const page = urlParams.get('page');
-      return page ? parseInt(page) : 0
-    }
-    return 0
-  }
-
   /**
    * Hydrate l'objet JSON de façon récursive (afin de gérer les objet nested)
    *
@@ -147,6 +132,12 @@ class HydraDenormalizer {
       }
     }
   }
+
+  private static extractPageFromUri(uri: string, default_: number | null = null): number | null {
+    const url = 'https://foo' + uri // Pour que l'uri partielle soit parsée, on doit y ajouter une url de base bidon
+    const page = Url.getParameter(url, 'page')
+    return page ? parseInt(page) : default_
+  }
 }
 
 export default HydraDenormalizer

+ 57 - 0
services/utils/url.ts

@@ -0,0 +1,57 @@
+import {FileArgs, ImageArgs, ListArgs, UrlArgs} from '~/types/interfaces'
+import {QUERY_TYPE} from '~/types/enums'
+import TypesTesting from "~/services/utils/typesTesting";
+
+/**
+ * Classe permettant de construire une URL pour l'interrogation d'une API externe
+ */
+class Url {
+  /**
+   * Concatenate a base url and a tail
+   * @param base
+   * @param tails
+   * @private
+   */
+  public static join (base: string, ...tails: string[]): string {
+    let url = base
+    tails.forEach((tail: string) => {
+      url = url.replace(/^|\/$/g, '') + '/' + tail.replace(/^\/?|$/g, '')
+    })
+    return url
+  }
+
+  /**
+   * Prepend the 'https://' part if neither 'http://' of 'https://' is present, else: does nothing
+   *
+   * @param url
+   */
+  public static prependHttps (url: string): string {
+    if (!url.match(/^https?:\/\/.*/)) {
+      url = 'https://' + url;
+    }
+    return url;
+  }
+
+  /**
+   * Parse an URI to retrieve the page number
+   *
+   * @param uri
+   * @param parameter
+   * @param default_
+   * @private
+   */
+  public static getParameter(
+      uri: string,
+      parameter: string,
+      default_: string | null = null
+  ): string | null {
+
+    const urlParams = new URL(uri).searchParams;
+    const res = urlParams.get('page');
+
+    return res ?? default_
+  }
+
+}
+
+export default Url

+ 0 - 112
services/utils/urlBuilder.ts

@@ -1,112 +0,0 @@
-import {FileArgs, ImageArgs, ListArgs, UrlArgs} from '~/types/interfaces'
-import {QUERY_TYPE} from '~/types/enums'
-import TypesTesting from "~/services/utils/typesTesting";
-
-/**
- * Classe permettant de construire une URL pour l'interrogation d'une API externe
- */
-class UrlBuilder {
-  static ROOT = '/api/'
-
-  /**
-   * Concatenate a base url and a tail
-   * @param base
-   * @param tails
-   * @private
-   */
-  public static join (base: string, ...tails: string[]): string {
-    let url = base
-    tails.forEach((tail: string) => {
-      url = url.replace(/^|\/$/g, '') + '/' + tail.replace(/^\/?|$/g, '')
-    })
-    return url
-  }
-
-  /**
-   * Prepend the 'https://' part if neither 'http://' of 'https://' is present, else: does nothing
-   *
-   * @param url
-   */
-  public static prependHttps (url: string): string {
-    if (!url.match(/^https?:\/\/.*/)) {
-      url = 'https://' + url;
-    }
-    return url;
-  }
-
-  /**
-   * Construction d'une URL "image" qui ira concaténer l'id de l'image à downloeader passé en paramètre avec la ROOT Url définie
-   * @param {ImageArgs} imgArgs
-   * @param {string} baseUrl
-   * @return {string}
-   */
-  private static getImageUrl (imgArgs: ImageArgs, baseUrl: string = ''): string {
-    const downloadUrl = `files/${imgArgs.id}/download/${imgArgs.height}x${imgArgs.width}`
-    return UrlBuilder.join(baseUrl, UrlBuilder.ROOT, downloadUrl)
-  }
-
-  /**
-   * Construction d'une URL qui ira concaténer la base URL avec le Root et l'uri files
-   * @param args
-   * @param baseUrl
-   * @private
-   */
-  private static getFileUrl (args: FileArgs, baseUrl: string = ''): string {
-    return UrlBuilder.join(baseUrl, UrlBuilder.ROOT, `download/${args.fileId}`)
-  }
-
-  /**
-   * Main méthode qui appellera les méthode privées correspondantes (getUrlOptionsImage, getUrlOptionsLists)
-   * @param {UrlArgs} args
-   * @return {string}
-   */
-  public static buildOptions(args: UrlArgs): Array<string> {
-    let options: Array<string> = []
-
-    if (args.type === QUERY_TYPE.IMAGE){
-      options = [...options, this.getUrlOptionsImage()]
-    }
-
-    if (TypesTesting.isDataProviderArgs(args) && args.listArgs !== undefined) {
-      options = [...options, ...this.getUrlOptionsLists(args.listArgs)]
-    }
-
-    return options
-  }
-
-  /**
-   * Une image doit toujours avoir le time en options pour éviter les problème de cache
-   * @private
-   */
-  private static getUrlOptionsImage(): string {
-    return new Date().getTime().toString()
-  }
-
-  /**
-   * Fonction renvoyant le tableau d'options d'une list
-   *
-   * @param listArgs
-   * @private
-   */
-  private static getUrlOptionsLists(listArgs: ListArgs): Array<string> {
-    const options: Array<string> = []
-
-    if (listArgs.itemsPerPage) {
-      options.push(`itemsPerPage=${listArgs.itemsPerPage}`)
-    }
-
-    if (listArgs.page) {
-      options.push(`page=${listArgs.page}`)
-    }
-
-    if (listArgs.filters) {
-      for(const filter of listArgs.filters){
-        options.push(`${filter.key}=${filter.value}`)
-      }
-    }
-
-    return options
-  }
-}
-
-export default UrlBuilder

+ 10 - 0
types/data.d.ts

@@ -1,3 +1,4 @@
+import ApiResource from "~/models/ApiResource";
 
 type AnyJson = Record<string, any>
 
@@ -46,4 +47,13 @@ interface ApiCollection extends ApiResponse {
     metadata: HydraMetadata
 }
 
+interface Collection {
+    items: Array<ApiResource>
+    totalItems: number | undefined
+    firstPage: number | undefined
+    lastPage: number | undefined
+    nextPage: number | undefined
+    previousPage: number | undefined
+}
+