소스 검색

Merge branch 'feature/V8-6040-analyse---entity-manger--voir-si' into develop

Olivier Massot 1 년 전
부모
커밋
e427878497

+ 2 - 2
components/Layout/Header/Notification.vue

@@ -233,9 +233,9 @@ const markNotificationAsRead = (notification: Notification) => {
     isRead: true,
     isRead: true,
   })
   })
 
 
-  em.persist(NotificationUsers, notificationUsers)
+  em.persist(notificationUsers)
   notification.notificationUsers = ['read']
   notification.notificationUsers = ['read']
-  em.save(Notification, notification)
+  em.save(notification)
 }
 }
 
 
 /**
 /**

+ 5 - 10
components/Ui/Button/Delete.vue

@@ -29,20 +29,15 @@ Bouton Delete avec modale de confirmation de la suppression
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
+import type { Ref, PropType } from 'vue'
 import { TYPE_ALERT } from '~/types/enum/enums'
 import { TYPE_ALERT } from '~/types/enum/enums'
-import type { Ref } from '@vue/reactivity'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 import ApiResource from '~/models/ApiResource'
 import ApiResource from '~/models/ApiResource'
 import { usePageStore } from '~/stores/page'
 import { usePageStore } from '~/stores/page'
-import ApiModel from '~/models/ApiModel'
 
 
 const props = defineProps({
 const props = defineProps({
-  model: {
-    type: Function as any as () => typeof ApiModel,
-    required: true,
-  },
   entity: {
   entity: {
-    type: Object as () => ApiResource,
+    type: Object as PropType<ApiResource>,
     required: true,
     required: true,
   },
   },
   flat: {
   flat: {
@@ -58,10 +53,10 @@ const { em } = useEntityManager()
 
 
 const deleteItem = async () => {
 const deleteItem = async () => {
   try {
   try {
-    //@ts-ignore
-    await em.delete(props.model, props.entity)
+    await em.delete(props.entity)
     usePageStore().addAlert(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
     usePageStore().addAlert(TYPE_ALERT.SUCCESS, ['deleteSuccess'])
-  } catch (error: any) {
+  } catch (error) {
+    // @ts-expect-error error is supposed to have a message prop
     usePageStore().addAlert(TYPE_ALERT.ALERT, [error.message])
     usePageStore().addAlert(TYPE_ALERT.ALERT, [error.message])
     throw error
     throw error
   }
   }

+ 1 - 1
components/Ui/Card.vue

@@ -24,7 +24,7 @@ Container de type Card
         </NuxtLink>
         </NuxtLink>
       </v-btn>
       </v-btn>
 
 
-      <UiButtonDelete v-if="withDeleteAction" :model="model" :entity="entity" />
+      <UiButtonDelete v-if="withDeleteAction" :entity="entity" />
 
 
       <slot name="card.action" />
       <slot name="card.action" />
     </v-card-actions>
     </v-card-actions>

+ 2 - 2
components/Ui/Form.vue

@@ -232,7 +232,7 @@ const submit = async (next: string | null = null) => {
     usePageStore().loading = true
     usePageStore().loading = true
 
 
     // TODO: est-ce qu'il faut re-fetch l'entité après le persist?
     // TODO: est-ce qu'il faut re-fetch l'entité après le persist?
-    const updatedEntity = await em.persist(props.model, props.entity)
+    const updatedEntity = await em.persist(props.entity)
 
 
     if (props.refreshProfile) {
     if (props.refreshProfile) {
       await refreshProfile()
       await refreshProfile()
@@ -362,7 +362,7 @@ const actions = computed(() => {
  */
  */
 const onFormChange = async () => {
 const onFormChange = async () => {
   if (isValid.value) {
   if (isValid.value) {
-    em.save(props.model, props.entity)
+    em.save(props.entity)
     setIsDirty(true)
     setIsDirty(true)
 
 
     if (props.onChanged) {
     if (props.onChanged) {

+ 1 - 1
components/Ui/Input/Image.vue

@@ -383,7 +383,7 @@ const saveExistingImage = async () => {
     width: cropperConfig.value.width,
     width: cropperConfig.value.width,
   })
   })
 
 
-  await em.persist(File, file.value)
+  await em.persist(file.value)
 }
 }
 
 
 /**
 /**

+ 8 - 8
middleware/routing.global.ts

@@ -16,14 +16,14 @@ export default defineNuxtRouteMiddleware((to, _) => {
     const name: string = routeName?.toString() ?? ''
     const name: string = routeName?.toString() ?? ''
 
 
     // <<- TODO: remove after 2.5 release
     // <<- TODO: remove after 2.5 release
-    const runtimeConfig = useRuntimeConfig()
-    if (
-      runtimeConfig.public.env === 'production' &&
-      (name === 'cmf_licence_page' || name === 'parameters_page')
-    ) {
-      const { redirectToHome } = useRedirect()
-      redirectToHome()
-    }
+    // const runtimeConfig = useRuntimeConfig()
+    // if (
+    //   runtimeConfig.public.env === 'production' &&
+    //   (name === 'cmf_licence_page' || name === 'parameters_page')
+    // ) {
+    //   const { redirectToHome } = useRedirect()
+    //   redirectToHome()
+    // }
     // ->>
     // ->>
 
 
     if (
     if (

+ 6 - 2
models/Access/Access.ts

@@ -1,4 +1,4 @@
-import { HasOne, Num, Uid, Attr } from 'pinia-orm/dist/decorators'
+import { Num, Uid, Attr, Str } from 'pinia-orm/dist/decorators'
 import type { Historical } from '~/types/interfaces'
 import type { Historical } from '~/types/interfaces'
 import Person from '~/models/Person/Person'
 import Person from '~/models/Person/Person'
 import ApiModel from '~/models/ApiModel'
 import ApiModel from '~/models/ApiModel'
@@ -16,7 +16,8 @@ export default class Access extends ApiModel {
   @Uid()
   @Uid()
   declare id: number | string
   declare id: number | string
 
 
-  @HasOne(() => Person, 'accessId')
+  @Attr(null)
+  @IriEncoded(Person)
   declare person: Person | null
   declare person: Person | null
 
 
   @Num(0)
   @Num(0)
@@ -28,4 +29,7 @@ export default class Access extends ApiModel {
   @Attr(null)
   @Attr(null)
   @IriEncoded(Organization)
   @IriEncoded(Organization)
   declare organization: number | null
   declare organization: number | null
+
+  @Str('')
+  declare updateDate: string
 }
 }

+ 2 - 2
models/Core/Notification.ts

@@ -19,8 +19,8 @@ export default class Notification extends ApiModel {
   @Attr({})
   @Attr({})
   declare message: NotificationMessage | null
   declare message: NotificationMessage | null
 
 
-  @Str('')
-  declare createDate: string
+  @Str(null)
+  declare createDate: string | null
 
 
   @Str(null)
   @Str(null)
   declare type: string | null
   declare type: string | null

+ 1 - 1
pages/cmf_licence_structure.vue

@@ -87,7 +87,7 @@ const submit = async () => {
 
 
   try {
   try {
     // Send the export request and get the receipt
     // Send the export request and get the receipt
-    const receipt = await em.persist(LicenceCmfOrganizationER, exportRequest)
+    const receipt = await em.persist(exportRequest)
     if (receipt.fileId === null) {
     if (receipt.fileId === null) {
       throw new Error("Missing file's id, abort")
       throw new Error("Missing file's id, abort")
     }
     }

+ 52 - 0
pages/dev/poc_persist.vue

@@ -0,0 +1,52 @@
+<template>
+  <div>
+    <h1>POC Persist</h1>
+
+    <v-btn v-if="!pending" @click="onUpdateClick">Update access</v-btn>
+
+    <v-btn @click="onCreateClick">Create Notification</v-btn>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useEntityManager } from '~/composables/data/useEntityManager'
+import { useAccessProfileStore } from '~/stores/accessProfile'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import Access from '~/models/Access/Access'
+import Notification from '~/models/Core/Notification'
+
+definePageMeta({
+  layout: false,
+})
+
+const { em } = useEntityManager()
+
+const accessProfile = useAccessProfileStore()
+
+const accessId = accessProfile.currentAccessId
+
+const { fetch } = useEntityFetch()
+
+const { data: access, pending } = await fetch(Access, accessId)
+
+const onUpdateClick = () => {
+  if (access.value === null) {
+    throw new Error('access is null')
+  }
+
+  access.value.updateDate = new Date().toISOString()
+  console.log(access.value.id, access.value.updateDate)
+
+  em.persist(access.value)
+}
+
+const onCreateClick = async () => {
+  const notif = em.newInstance(Notification, { name: 'foo', message: ['bar'] })
+
+  // const notif = new Notification({ name: 'foo', message: ['bar'] })
+
+  const createdNotif = await em.persist(notif)
+
+  console.log(createdNotif)
+}
+</script>

+ 0 - 1
pages/parameters/attendances.vue

@@ -63,7 +63,6 @@
                 @click="goToEditPage(reason.id as number)"
                 @click="goToEditPage(reason.id as number)"
               />
               />
               <UiButtonDelete
               <UiButtonDelete
-                :model="AttendanceBookingReason"
                 :entity="reason"
                 :entity="reason"
                 :flat="true"
                 :flat="true"
                 class="cycle-edit-icon"
                 class="cycle-edit-icon"

+ 0 - 1
pages/parameters/education_timings/index.vue

@@ -22,7 +22,6 @@
                 @click="goToEditPage(timing.id as number)"
                 @click="goToEditPage(timing.id as number)"
               />
               />
               <UiButtonDelete
               <UiButtonDelete
-                :model="EducationTiming"
                 :entity="timing"
                 :entity="timing"
                 :flat="true"
                 :flat="true"
                 class="cycle-edit-icon"
                 class="cycle-edit-icon"

+ 0 - 1
pages/parameters/residence_areas/index.vue

@@ -25,7 +25,6 @@
                 @click="goToEditPage(residenceArea.id as number)"
                 @click="goToEditPage(residenceArea.id as number)"
               />
               />
               <UiButtonDelete
               <UiButtonDelete
-                :model="ResidenceArea"
                 :entity="residenceArea"
                 :entity="residenceArea"
                 :flat="true"
                 :flat="true"
                 class="cycle-edit-icon"
                 class="cycle-edit-icon"

+ 39 - 18
services/data/entityManager.ts

@@ -121,10 +121,6 @@ class EntityManager {
 
 
     const instance = repository.make(properties)
     const instance = repository.make(properties)
 
 
-    // Keep track of the model
-    // TODO : attendre de voir si utile ou non
-    // instance.setModel(model)
-
     if (
     if (
       !Object.prototype.hasOwnProperty.call(properties, 'id') ||
       !Object.prototype.hasOwnProperty.call(properties, 'id') ||
       // @ts-expect-error Si la première condition passe, on sait que id existe
       // @ts-expect-error Si la première condition passe, on sait que id existe
@@ -134,23 +130,20 @@ class EntityManager {
       instance.id = 'tmp' + uuid4()
       instance.id = 'tmp' + uuid4()
     }
     }
 
 
-    return this.save(model, instance, true)
+    return this.save(instance, true)
   }
   }
 
 
   /**
   /**
    * Save the model instance into the store
    * Save the model instance into the store
    *
    *
-   * @param model
    * @param instance
    * @param instance
    * @param permanent Is the change already persisted in the datasource? If this is the case, the initial state of this
    * @param permanent Is the change already persisted in the datasource? If this is the case, the initial state of this
    *                  record is also updated.
    *                  record is also updated.
    */
    */
-  public save(
-    model: typeof ApiResource,
-    instance: ApiResource,
-    permanent: boolean = false,
-  ): ApiResource {
-    instance = this.cast(model, instance)
+  public save(instance: ApiResource, permanent: boolean = false): ApiResource {
+    const model = instance.constructor as typeof ApiResource
+
+    this.validateEntity(instance)
 
 
     if (permanent) {
     if (permanent) {
       this.saveInitialState(model, instance)
       this.saveInitialState(model, instance)
@@ -270,17 +263,16 @@ class EntityManager {
   /**
   /**
    * Persist the model instance as it is in the store into the data source via the API
    * Persist the model instance as it is in the store into the data source via the API
    *
    *
-   * @param model
    * @param instance
    * @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)
+  public async persist(instance: ApiModel) {
+    const model = instance.constructor as typeof ApiModel
 
 
     let url = UrlUtils.join('api', model.entity)
     let url = UrlUtils.join('api', model.entity)
     let response
     let response
 
 
+    this.validateEntity(instance)
+
     const data: AnyJson = HydraNormalizer.normalizeEntity(instance)
     const data: AnyJson = HydraNormalizer.normalizeEntity(instance)
 
 
     const headers = { profileHash: await this.makeProfileHash() }
     const headers = { profileHash: await this.makeProfileHash() }
@@ -333,9 +325,15 @@ class EntityManager {
    * @param model
    * @param model
    * @param instance
    * @param instance
    */
    */
-  public async delete(model: typeof ApiModel, instance: ApiResource) {
+  public async delete(instance: ApiResource) {
+    const model = instance.constructor as typeof ApiModel
+
+    this.validateEntity(instance)
+
     const repository = this.getRepository(model)
     const repository = this.getRepository(model)
 
 
+    this.validateEntity(instance)
+
     // If object has been persisted to the datasource, send a delete request
     // If object has been persisted to the datasource, send a delete request
     if (!instance.isNew()) {
     if (!instance.isNew()) {
       const url = UrlUtils.join('api', model.entity, String(instance.id))
       const url = UrlUtils.join('api', model.entity, String(instance.id))
@@ -475,6 +473,29 @@ class EntityManager {
     const mask = this._getProfileMask()
     const mask = this._getProfileMask()
     return await ObjectUtils.hash(mask)
     return await ObjectUtils.hash(mask)
   }
   }
+
+  /**
+   * Validate the entity, and throw an error if it's not correctly defined.
+   * @param instance
+   * @protected
+   */
+  protected validateEntity(instance: unknown): void {
+    if (Object.prototype.hasOwnProperty.call(instance, 'id')) {
+      // @ts-expect-error At this point, we're sure there is an id property
+      const id = instance.id
+
+      if (
+        !(typeof id === 'number') &&
+        !(typeof id === 'string' && id.startsWith('tmp'))
+      ) {
+        // The id is a pinia orm Uid, the entity has been created using the `new` keyword (not supported for now)
+        throw new Error(
+          'Definition error for the entity, did you use the entityManager.newInstance(...) method?\n' +
+            JSON.stringify(instance),
+        )
+      }
+    }
+  }
 }
 }
 
 
 export default EntityManager
 export default EntityManager

+ 2 - 1
stores/sse.ts

@@ -12,11 +12,12 @@ export const useSseStore = defineStore('sse', () => {
     const { em } = useEntityManager()
     const { em } = useEntityManager()
 
 
     const model = await em.getModelFromIri(event.iri)
     const model = await em.getModelFromIri(event.iri)
+    const instance = em.newInstance(model, JSON.parse(event.data))
 
 
     switch (event.operation) {
     switch (event.operation) {
       case 'update':
       case 'update':
       case 'create':
       case 'create':
-        await em.save(model, JSON.parse(event.data), true)
+        em.save(instance, true)
         break
         break
 
 
       case 'delete':
       case 'delete':

+ 49 - 13
tests/units/services/data/entityManager.test.ts

@@ -40,6 +40,10 @@ class TestableEntityManager extends EntityManager {
   public makeProfileHash() {
   public makeProfileHash() {
     return super.makeProfileHash()
     return super.makeProfileHash()
   }
   }
+
+  public validateEntity(instance: unknown): void {
+    return super.validateEntity(instance)
+  }
 }
 }
 
 
 const _console: any = {
 const _console: any = {
@@ -187,18 +191,13 @@ describe('newInstance', () => {
 
 
     // @ts-ignore
     // @ts-ignore
     entityManager.save = vi.fn(
     entityManager.save = vi.fn(
-      (model: typeof ApiResource, entity: ApiResource, permanent: boolean) =>
-        entity,
+      (entity: ApiResource, permanent: boolean) => entity,
     )
     )
 
 
     const result = entityManager.newInstance(DummyApiResource, properties)
     const result = entityManager.newInstance(DummyApiResource, properties)
 
 
     expect(repo.make).toHaveBeenCalledWith(properties)
     expect(repo.make).toHaveBeenCalledWith(properties)
-    expect(entityManager.save).toHaveBeenCalledWith(
-      DummyApiResource,
-      entity,
-      true,
-    )
+    expect(entityManager.save).toHaveBeenCalledWith(entity, true)
 
 
     expect(result.id).toEqual(properties.id)
     expect(result.id).toEqual(properties.id)
   })
   })
@@ -251,12 +250,15 @@ describe('save', () => {
       return model === DummyApiResource ? repo : null
       return model === DummyApiResource ? repo : null
     })
     })
 
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     // @ts-ignore
     // @ts-ignore
     repo.save = vi.fn((record: Element) => entity)
     repo.save = vi.fn((record: Element) => entity)
 
 
-    const entity = new DummyApiResource()
-    entityManager.save(DummyApiResource, entity)
+    const entity = new DummyApiResource({ id: 1 })
+    entityManager.save(entity)
 
 
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
     expect(repo.save).toHaveBeenCalledWith(entity)
     expect(repo.save).toHaveBeenCalledWith(entity)
   })
   })
 })
 })
@@ -424,6 +426,12 @@ describe('fetchCollection', () => {
       next: undefined,
       next: undefined,
       previous: undefined,
       previous: undefined,
     })
     })
+
+    // @ts-expect-error Needed to avoid 'Cannot stringify non POJO' occasional bugs
+    // eslint-disable-next-line
+    expect(result.toJSON()).toEqual(
+      'Computed result from fetchCollection at : api/dummyResource',
+    )
   })
   })
 
 
   test('with a parent', async () => {
   test('with a parent', async () => {
@@ -517,6 +525,8 @@ describe('persist', () => {
       (model: typeof ApiResource, entity: ApiResource): ApiResource => entity,
       (model: typeof ApiResource, entity: ApiResource): ApiResource => entity,
     )
     )
 
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     const response = { id: 1, name: 'bob' }
     const response = { id: 1, name: 'bob' }
     // @ts-ignore
     // @ts-ignore
     apiRequestService.post = vi.fn((url, data) => response)
     apiRequestService.post = vi.fn((url, data) => response)
@@ -536,7 +546,7 @@ describe('persist', () => {
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
 
 
-    const result = await entityManager.persist(DummyApiModel, instance)
+    const result = await entityManager.persist(instance)
 
 
     // temp id should have been purged from the posted data
     // temp id should have been purged from the posted data
     expect(apiRequestService.post).toHaveBeenCalledWith(
     expect(apiRequestService.post).toHaveBeenCalledWith(
@@ -561,6 +571,8 @@ describe('persist', () => {
 
 
     expect(result.id).toEqual(1)
     expect(result.id).toEqual(1)
     expect(result.name).toEqual('bob')
     expect(result.name).toEqual('bob')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(instance)
   })
   })
 
 
   test('existing entity (PUT)', async () => {
   test('existing entity (PUT)', async () => {
@@ -593,7 +605,9 @@ describe('persist', () => {
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.removeTempAfterPersist = vi.fn()
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
     entityManager.makeProfileHash = vi.fn(async () => await 'azerty')
 
 
-    const result = await entityManager.persist(DummyApiModel, entity)
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
+    const result = await entityManager.persist(entity)
 
 
     expect(apiRequestService.put).toHaveBeenCalledWith(
     expect(apiRequestService.put).toHaveBeenCalledWith(
       'api/dummyModel/1',
       'api/dummyModel/1',
@@ -612,6 +626,8 @@ describe('persist', () => {
 
 
     expect(result.id).toEqual(1)
     expect(result.id).toEqual(1)
     expect(result.name).toEqual('bob')
     expect(result.name).toEqual('bob')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
   })
   })
 })
 })
 
 
@@ -665,16 +681,20 @@ describe('delete', () => {
       return model === DummyApiModel ? repo : null
       return model === DummyApiModel ? repo : null
     })
     })
 
 
+    entityManager.validateEntity = vi.fn((instance: unknown) => {})
+
     apiRequestService.delete = vi.fn()
     apiRequestService.delete = vi.fn()
 
 
     // @ts-ignore
     // @ts-ignore
     repo.destroy = vi.fn((id: number) => null)
     repo.destroy = vi.fn((id: number) => null)
 
 
-    entityManager.delete(DummyApiModel, entity)
+    entityManager.delete(entity)
 
 
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(apiRequestService.delete).toHaveBeenCalledTimes(0)
     expect(apiRequestService.delete).toHaveBeenCalledTimes(0)
     expect(repo.destroy).toHaveBeenCalledWith('tmp123')
     expect(repo.destroy).toHaveBeenCalledWith('tmp123')
+
+    expect(entityManager.validateEntity).toHaveBeenCalledWith(entity)
   })
   })
 
 
   test('delete persisted entity', async () => {
   test('delete persisted entity', async () => {
@@ -696,7 +716,7 @@ describe('delete', () => {
     // @ts-ignore
     // @ts-ignore
     repo.destroy = vi.fn((id: number) => null)
     repo.destroy = vi.fn((id: number) => null)
 
 
-    await entityManager.delete(DummyApiModel, entity)
+    await entityManager.delete(entity)
 
 
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
     expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1')
     expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1')
@@ -1008,3 +1028,19 @@ describe('makeProfileHash', () => {
     )
     )
   })
   })
 })
 })
+
+describe('validateEntity', () => {
+  test('instance with numeric id', async () => {
+    entityManager.validateEntity({ id: 123 })
+  })
+
+  test('instance with temp id', async () => {
+    entityManager.validateEntity({ id: 'tmpazerty' })
+  })
+
+  test('invalid entity', async () => {
+    expect(() => entityManager.validateEntity({ id: 'azerty' })).toThrowError(
+      'Definition error for the entity, did you use the entityManager.newInstance(...) method?\n{"id":"azerty"}',
+    )
+  })
+})