Bläddra i källkod

add entityManager.test.ts

Olivier Massot 2 år sedan
förälder
incheckning
7c2a602fa6
3 ändrade filer med 877 tillägg och 19 borttagningar
  1. 1 1
      models/ApiResource.ts
  2. 34 18
      services/data/entityManager.ts
  3. 842 0
      tests/units/services/entityManager.test.ts

+ 1 - 1
models/ApiResource.ts

@@ -11,7 +11,7 @@ export class ApiResource extends Model {
         return this._model
     }
 
-    public setModel(model: typeof ApiResource ) {
+    public setModel(model: typeof ApiResource) {
         this._model = model
     }
 

+ 34 - 18
services/data/entityManager.ts

@@ -17,9 +17,9 @@ import _ from "lodash"
  * @see https://pinia-orm.codedredd.de/
  */
 class EntityManager {
-    private CLONE_PREFIX = '_clone_'
+    protected CLONE_PREFIX = '_clone_'
 
-    private apiRequestService: ApiRequestService
+    protected apiRequestService: ApiRequestService
 
     public constructor(
         apiRequestService: ApiRequestService
@@ -37,8 +37,16 @@ class EntityManager {
         return useRepo(model)
     }
 
+    /**
+     * Cast an object as an ApiResource
+     * This in used internally to ensure the object is recognized as an ApiResource
+     *
+     * @param model
+     * @param entity
+     * @protected
+     */
     // noinspection JSMethodCanBeStatic
-    private cast(model: typeof ApiResource, entity: ApiResource): ApiResource {
+    protected cast(model: typeof ApiResource, entity: ApiResource): ApiResource {
         return new model(entity)
     }
 
@@ -73,11 +81,9 @@ class EntityManager {
     public newInstance(model: typeof ApiResource, properties: object = {}): ApiResource {
         const repository = this.getRepository(model)
 
-        //@todo : make renvoi un model donc peut etre une confusion ?
-        //@todo: pourquoi faire un make ? pourquoi ne pas utiliser le new ? En plus si les propriétés ne sont pas nulles, on peut directement passer au save
         let entity = repository.make(properties)
 
-        // Keep track of the entitie's model
+        // Keep track of the entity's model
         entity.setModel(model)
 
         // @ts-ignore
@@ -102,9 +108,9 @@ class EntityManager {
         return this.getRepository(model).save(entity)
     }
 
-
     /**
      * Find the entity into the store
+     * TODO: comment réagit la fonction si l'id n'existe pas?
      *
      * @param model
      * @param id
@@ -135,17 +141,14 @@ class EntityManager {
 
         // 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 = HydraDenormalizer.denormalize(response).data as object
-
         return this.newInstance(model, attributes)
     }
 
     /**
-     * @todo: avec la nouvelle version de API Platform ça va pas mal avec la nouvelle gestion des sous resources...
      * Fetch a collection of entity
      * The content of `query` is converted into a query-string in the request URL
      *
@@ -155,6 +158,7 @@ class EntityManager {
      */
     public async fetchCollection(model: typeof ApiResource, parent: ApiResource | null, query: AssociativeArray = []): Promise<Collection> {
         let url
+
         if (parent !== null) {
             url = UrlUtils.join('api', parent.entity, '' + parent.id, model.entity)
         } else {
@@ -166,7 +170,6 @@ class EntityManager {
         // deserialize the response
         const apiCollection = HydraDenormalizer.denormalize(response)
 
-        //@todo: le map ne doit pas être nécessaire car on peut passer directement une collection au save.
         const items = apiCollection.data.map((attributes: object) => {
             return this.newInstance(model, attributes)
         })
@@ -183,7 +186,15 @@ class EntityManager {
         }
     }
 
-    private async saveResponseAsEntity(model: typeof ApiModel, response: Response) {
+    /**
+     * Créé une entité à partir d'une réponse de l'api au format Hydra, l'enregistre
+     * dans le store et la retourne
+     *
+     * @param model
+     * @param response
+     * @protected
+     */
+    protected async saveResponseAsEntity(model: typeof ApiModel, response: Response) {
         const repository = this.getRepository(model)
 
         const hydraResponse = await HydraDenormalizer.denormalize(response)
@@ -302,7 +313,7 @@ class EntityManager {
      *
      * @param model
      */
-    public async flush(model: typeof ApiModel) {
+    public flush(model: typeof ApiModel) {
         const repository = this.getRepository(model)
         repository.flush()
     }
@@ -310,6 +321,9 @@ class EntityManager {
     /**
      * Is the entity a new one, or does it already exist in the data source (=API)
      *
+     * This is a convenient way of testing an entity you did not already fetch, else prefer the use of the
+     * isNew() method of ApiResource
+     *
      * @param model
      * @param id
      */
@@ -318,7 +332,8 @@ class EntityManager {
 
         const item = repository.find(id)
         if (!item || typeof item === 'undefined') {
-            console.error(model.entity + '/' + id, ' does not exist!')
+            // TODO: est-ce qu'il ne faudrait pas lever une erreur ici plutôt?
+            console.error(model.entity + '/' + id + ' does not exist!')
             return false
         }
 
@@ -332,7 +347,7 @@ class EntityManager {
      * @param entity
      * @private
      */
-    private saveInitialState(model: typeof ApiResource, entity: ApiResource) {
+    protected saveInitialState(model: typeof ApiResource, entity: ApiResource) {
         const repository = this.getRepository(model)
 
         // Clone and prefix id
@@ -349,7 +364,7 @@ class EntityManager {
      * @param id
      * @private
      */
-    private getInitialStateOf(model: typeof ApiResource, id: string | number): ApiResource | null {
+    protected getInitialStateOf(model: typeof ApiResource, id: string | number): ApiResource | null {
         const repository = this.getRepository(model)
 
         // Find the clone by id
@@ -371,12 +386,13 @@ class EntityManager {
      * @param tempEntityId
      * @private
      */
-    private removeTempAfterPersist(model: typeof ApiResource, tempEntityId: number) {
+    protected removeTempAfterPersist(model: typeof ApiResource, tempEntityId: number | string) {
         const repository = this.getRepository(model)
 
         const entity = repository.find(tempEntityId)
         if (!entity || typeof entity === 'undefined') {
-            console.error(model.entity + '/' + tempEntityId, ' does not exist!')
+            // TODO: il vaudrait peut-être mieux lever une erreur ici?
+            console.error(model.entity + '/' + tempEntityId + ' does not exist!')
             return
         }
         if (!entity.isNew()) {

+ 842 - 0
tests/units/services/entityManager.test.ts

@@ -0,0 +1,842 @@
+import { describe, test, it, expect } from 'vitest'
+import EntityManager from "~/services/data/entityManager";
+import ApiResource from "~/models/ApiResource";
+import ApiModel from "~/models/ApiModel";
+import ApiRequestService from "~/services/data/apiRequestService";
+import {Element, Repository} from "pinia-orm";
+import {ApiResponse} from "~/types/data";
+import HydraDenormalizer from "~/services/data/normalizer/hydraDenormalizer";
+
+class TestableEntityManager extends EntityManager {
+    public cast(model: typeof ApiResource, entity: ApiResource): ApiResource { return super.cast(model, entity) }
+    public async saveResponseAsEntity(model: typeof ApiModel, response: Response) { return super.saveResponseAsEntity(model, response) }
+    public saveInitialState(model: typeof ApiResource, entity: ApiResource) { return super.saveInitialState(model, entity) }
+    public getInitialStateOf(model: typeof ApiResource, id: string | number): ApiResource | null { return super.getInitialStateOf(model, id) }
+    public removeTempAfterPersist(model: typeof ApiResource, tempEntityId: number | string) { return super.removeTempAfterPersist(model, tempEntityId) }
+}
+
+class DummyApiResource extends ApiResource {
+    static entity = 'dummyResource'
+
+}
+
+class DummyApiModel extends ApiModel {
+    static entity = 'dummyModel'
+}
+
+let apiRequestService: ApiRequestService
+let entityManager: TestableEntityManager
+
+beforeEach(() => {
+    // @ts-ignore
+    apiRequestService = vi.fn() as ApiRequestService
+
+    entityManager = new TestableEntityManager(apiRequestService)
+
+    // TODO: s'assurer que les mocks globaux sont bien réinitialisés après les tests, en particulier les fonctions de console
+})
+
+describe('getRepository', () => {
+    // TODO: à revoir
+})
+
+describe('cast', () => {
+    test('simple cast', () => {
+        // @ts-ignore
+        const result = entityManager.cast(DummyApiResource, { id: 1 })
+
+        expect(result instanceof DummyApiResource).toEqual(true)
+    })
+})
+
+describe('getModelFor', () => {
+    // TODO: à revoir
+})
+
+describe('getModelFromIri', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        entityManager.getModelFor = vi.fn((entityName: string) => entityName === 'dummy' ? DummyApiResource : null)
+
+        // @ts-ignore
+        const result = entityManager.getModelFromIri('/api/dummy/123')
+
+        expect(result).toEqual(DummyApiResource)
+    })
+    test('invalide Iri', () => {
+        expect(() => entityManager.getModelFromIri('/invalid')).toThrowError('cannot parse the IRI')
+    })
+})
+
+describe('newInstance', () => {
+
+    test('simple call', () => {
+        const properties = { 'id': 1 }
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        entityManager.saveInitialState = vi.fn((model: typeof ApiResource, entity: ApiResource) => null)
+
+        // @ts-ignore
+        const entity = new DummyApiResource(properties)
+        entity.setModel = vi.fn((model: typeof ApiResource) => null)
+
+        // @ts-ignore
+        repo.make = vi.fn((properties: object) => {
+            // @ts-ignore
+            entity.id = properties.id
+            return entity
+        })
+        // @ts-ignore
+        repo.save = vi.fn((record: Element) => entity)
+
+        const result = entityManager.newInstance(DummyApiResource, properties)
+
+        expect(repo.make).toHaveBeenCalledWith(properties)
+        expect(entity.setModel).toHaveBeenCalledWith(DummyApiResource)
+        expect(repo.save).toHaveBeenCalledWith(entity)
+        expect(entityManager.saveInitialState).toHaveBeenCalledWith(DummyApiResource, entity)
+
+        expect(result.id).toEqual(properties.id)
+    })
+
+    test('with no id provided', () => {
+        const properties = { 'name': 'bob' }
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        entityManager.saveInitialState = vi.fn((model: typeof ApiResource, entity: ApiResource) => null)
+
+        // @ts-ignore
+        const entity = new DummyApiResource(properties)
+        entity.setModel = vi.fn((model: typeof ApiResource) => null)
+
+        // @ts-ignore
+        repo.make = vi.fn((properties: object) => {
+            // @ts-ignore
+            entity.name = properties.name
+            return entity
+        })
+        // @ts-ignore
+        repo.save = vi.fn((record: Element) => entity)
+
+        const result = entityManager.newInstance(DummyApiResource, properties)
+
+        expect(
+            result.id,
+            'id is \'tmp\' followed by a valid uuid-V4'
+        ).toMatch(/tmp[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}/)
+
+        expect(result.name).toEqual(properties.name)
+    })
+})
+
+describe('save', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.save = vi.fn((record: Element) => entity)
+
+        const entity = new DummyApiResource()
+        entityManager.save(DummyApiResource, entity)
+
+        expect(repo.save).toHaveBeenCalledWith(entity)
+    })
+})
+
+describe('find', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        const entity = new DummyApiResource({ id: 1 })
+        // @ts-ignore
+        repo.find = vi.fn((id: string | number) => entity)
+
+        entityManager.find(DummyApiResource, 1)
+
+        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiResource)
+        expect(repo.find).toHaveBeenCalledWith(1)
+    })
+})
+
+describe('fetch', () => {
+    test('not in store, no force refresh', async () => {
+
+        const properties = { id: 1 }
+
+        const entity = new DummyApiResource({ id: 1 })
+
+        // @ts-ignore
+        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => undefined)
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => properties)
+
+        // @ts-ignore
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => entity)
+
+        const result = await entityManager.fetch(DummyApiResource, 1)
+
+        expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1)
+        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1')
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, properties)
+
+        expect(result).toEqual(entity)
+    })
+
+    test('in store, no force refresh', async () => {
+
+        const properties = { id: 1 }
+
+        const entity = new DummyApiResource({ id: 1 })
+
+        // @ts-ignore
+        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => entity)
+
+        const result = await entityManager.fetch(DummyApiResource, 1)
+
+        expect(entityManager.find).toHaveBeenCalledWith(DummyApiResource, 1)
+        expect(result).toEqual(entity)
+    })
+
+    test('in store, but with force refresh', async () => {
+
+        const properties = { id: 1 }
+
+        const entity = new DummyApiResource({ id: 1 })
+
+        // @ts-ignore
+        entityManager.find = vi.fn((model: typeof ApiResource, id: number) => undefined)
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => properties)
+
+        // @ts-ignore
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => entity)
+
+        const result = await entityManager.fetch(DummyApiResource, 1, true)
+
+        expect(entityManager.find).toHaveBeenCalledTimes(0)
+        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource/1')
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, properties)
+
+        expect(result).toEqual(entity)
+    })
+})
+
+describe('fetchCollection', () => {
+    test('simple call', async () => {
+
+        const collection = {
+            '@type': 'hydra:Collection',
+            'hydra:totalItems': 3,
+            'hydra:member': [
+                {id: 1},
+                {id: 2},
+                {id: 3},
+            ]
+        }
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => collection)
+
+        // @ts-ignore
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
+            return new DummyApiResource(props)
+        })
+
+        const result = await entityManager.fetchCollection(DummyApiResource, null)
+
+        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', [])
+        expect(entityManager.newInstance).toHaveBeenCalledTimes(3)
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 1})
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 2})
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiResource, {id: 3})
+
+        expect(result.items).toEqual([
+            new DummyApiResource({id: 1}),
+            new DummyApiResource({id: 2}),
+            new DummyApiResource({id: 3})
+        ])
+
+        expect(result.pagination, 'default pagination').toEqual({
+            first: 1,
+            last: 1,
+            next: undefined,
+            previous: undefined
+        })
+    })
+
+    test('with a parent', async () => {
+
+        const collection = {
+            '@type': 'hydra:Collection',
+            'hydra:totalItems': 3,
+            'hydra:member': [
+                {id: 1},
+                {id: 2},
+                {id: 3},
+            ]
+        }
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => collection)
+
+        // @ts-ignore
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
+            return new DummyApiResource(props)
+        })
+
+        const parent = new DummyApiModel()
+        parent.id = 100
+        parent.entity = 'dummyModel'  // TODO: je ne comprend pas pqoi cette ligne est nécessaire...
+
+        await entityManager.fetchCollection(DummyApiResource, parent)
+
+        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyModel/100/dummyResource', [])
+    })
+
+    test('with a query', async () => {
+
+        const collection = {
+            '@type': 'hydra:Collection',
+            'hydra:totalItems': 3,
+            'hydra:member': [
+                {id: 1},
+                {id: 2},
+                {id: 3},
+            ]
+        }
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => collection)
+
+        // @ts-ignore
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, props: object) => {
+            return new DummyApiResource(props)
+        })
+
+        await entityManager.fetchCollection(DummyApiResource, null, { page: 10 })
+
+        expect(apiRequestService.get).toHaveBeenCalledWith('api/dummyResource', { page: 10 })
+    })
+
+    test('with pagination', async () => {
+
+        const collection = {
+            '@type': 'hydra:Collection',
+            'hydra:totalItems': 1000,
+            'hydra:member': [],
+            'hydra:view': {
+                "@id": "/api/subdomains?organization=498&page=50",
+                'hydra:first': '/api/subdomains?organization=498&page=1',
+                'hydra:last': '/api/subdomains?organization=498&page=100',
+                'hydra:next': '/api/subdomains?organization=498&page=51',
+                'hydra:previous': '/api/subdomains?organization=498&page=49'
+            }
+        }
+
+        // @ts-ignore
+        apiRequestService.get = vi.fn(async (url: string) => collection)
+
+        const result = await entityManager.fetchCollection(DummyApiResource, null)
+
+        expect(result.totalItems).toEqual(1000)
+        expect(result.pagination.first).toEqual(1)
+        expect(result.pagination.last).toEqual(100)
+        expect(result.pagination.previous).toEqual(49)
+        expect(result.pagination.next).toEqual(51)
+    })
+})
+
+describe('saveResponseAsEntity', () => {
+    test('simple call', async () => {
+
+        const entity = new DummyApiModel({id: 1})
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        const response = {id: 1} as Response
+
+        entityManager.newInstance = vi.fn((model: typeof ApiResource, properties: object) => {
+            return entity
+        })
+
+        entityManager.saveInitialState = vi.fn((model: typeof ApiResource, entity: ApiResource) => null)
+
+        // @ts-ignore
+        repo.save = vi.fn((data: any) => null)
+
+        const result = await entityManager.saveResponseAsEntity(DummyApiModel, response)
+
+        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
+        expect(entityManager.newInstance).toHaveBeenCalledWith(DummyApiModel, {id: 1})
+        expect(entityManager.saveInitialState).toHaveBeenCalledWith(DummyApiModel, entity)
+        expect(repo.save).toHaveBeenCalledWith(entity)
+
+        expect(result).toEqual(entity)
+    })
+})
+
+
+describe('persist', () => {
+    test('new entity (POST)', async () => {
+        const entity = new DummyApiModel({id: 'tmp1', name: 'bob'})
+        entity.isNew = vi.fn(() => true)
+
+        // @ts-ignore
+        entity.$toJson = vi.fn(() => {
+            return {id: 'tmp1', name: 'bob'}
+        })
+
+        entityManager.cast = vi.fn((model: typeof ApiResource, entity: ApiResource): ApiResource => entity)
+
+        const response = { id: 1, name: 'bob' }
+        // @ts-ignore
+        apiRequestService.post = vi.fn((url, data) => response)
+
+        // @ts-ignore
+        entityManager.saveResponseAsEntity = vi.fn((model, response) => {
+            const newEntity = new DummyApiModel(response)
+            // @ts-ignore
+            newEntity.id = response.id
+            // @ts-ignore
+            newEntity.name = response.name
+
+            return newEntity
+        })
+
+        entityManager.removeTempAfterPersist = vi.fn()
+
+        const result = await entityManager.persist(DummyApiModel, entity)
+
+        // temp id should have been purged from the posted data
+        expect(apiRequestService.post).toHaveBeenCalledWith('api/dummyModel', {name: 'bob'})
+        expect(entityManager.saveResponseAsEntity).toHaveBeenCalledWith(DummyApiModel, response)
+        expect(entityManager.removeTempAfterPersist).toHaveBeenCalledWith(DummyApiModel, entity.id)
+
+        expect(result.id).toEqual(1)
+        expect(result.name).toEqual('bob')
+    })
+
+    test('existing entity (PUT)', async () => {
+        const props = {id: 1, name: 'bob'}
+        const entity = new DummyApiModel(props)
+        entity.id = 1
+        entity.isNew = vi.fn(() => false)
+
+        // @ts-ignore
+        entity.$toJson = vi.fn(() => props)
+
+        entityManager.cast = vi.fn((model: typeof ApiResource, entity: ApiResource): ApiResource => entity)
+
+        // @ts-ignore
+        apiRequestService.put = vi.fn((url, data) => props)
+
+        // @ts-ignore
+        entityManager.saveResponseAsEntity = vi.fn((model, response) => {
+            const newEntity = new DummyApiModel(response)
+            // @ts-ignore
+            newEntity.id = response.id
+            // @ts-ignore
+            newEntity.name = response.name
+
+            return newEntity
+        })
+
+        entityManager.removeTempAfterPersist = vi.fn()
+
+        const result = await entityManager.persist(DummyApiModel, entity)
+
+        expect(apiRequestService.put).toHaveBeenCalledWith('api/dummyModel/1', {id: 1, name: 'bob'})
+        expect(entityManager.saveResponseAsEntity).toHaveBeenCalledWith(DummyApiModel, props)
+        expect(entityManager.removeTempAfterPersist).toHaveBeenCalledTimes(0)
+
+        expect(result.id).toEqual(1)
+        expect(result.name).toEqual('bob')
+    })
+})
+
+
+describe('patch', () => {
+    test('simple call', async () => {
+        const props = {id: 1, name: 'bobby'}
+
+        // @ts-ignore
+        apiRequestService.put = vi.fn((url, data) => props)
+
+        // @ts-ignore
+        entityManager.saveResponseAsEntity = vi.fn((model, response) => {
+            const newEntity = new DummyApiModel(response)
+            // @ts-ignore
+            newEntity.id = response.id
+            // @ts-ignore
+            newEntity.name = response.name
+
+            return newEntity
+        })
+
+        const result = await entityManager.patch(DummyApiModel, 1, {name: 'bobby'})
+
+        expect(apiRequestService.put).toHaveBeenCalledWith('api/dummyModel/1', '{"name":"bobby"}')
+        expect(entityManager.saveResponseAsEntity).toHaveBeenCalledWith(DummyApiModel, {id: 1, name: 'bobby'})
+
+        expect(result.id).toEqual(1)
+        expect(result.name).toEqual('bobby')
+    })
+})
+
+
+describe('delete', () => {
+    test('delete non persisted entity', () => {
+        const entity = new DummyApiModel()
+        entity.isNew = vi.fn(() => true)
+        entity.id = 'tmp123'
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        apiRequestService.delete = vi.fn()
+
+        // @ts-ignore
+        repo.destroy = vi.fn((id: number) => null)
+
+        entityManager.delete(DummyApiModel, entity)
+
+        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
+        expect(apiRequestService.delete).toHaveBeenCalledTimes(0)
+        expect(repo.destroy).toHaveBeenCalledWith('tmp123')
+    })
+
+    test('delete persisted entity', async () => {
+        const entity = new DummyApiModel()
+        entity.isNew = vi.fn(() => false)
+        entity.id = 1
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        apiRequestService.delete = vi.fn((id: number) => null)
+
+        // @ts-ignore
+        repo.destroy = vi.fn((id: number) => null)
+
+        await entityManager.delete(DummyApiModel, entity)
+
+        expect(entityManager.getRepository).toHaveBeenCalledWith(DummyApiModel)
+        expect(apiRequestService.delete).toHaveBeenCalledWith('api/dummyModel/1')
+        expect(repo.destroy).toHaveBeenCalledWith(1)
+    })
+})
+
+describe('reset', () => {
+    test('simple call', () => {
+        const entity = new DummyApiModel()
+        entity.id = 1
+        entity.name = 'paul'
+
+        const initialEntity = new DummyApiModel()
+        initialEntity.id = 1
+        initialEntity.name = 'serges'
+
+        entityManager.getInitialStateOf = vi.fn((model: typeof ApiResource, id: string | number) => initialEntity)
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        repo.save = vi.fn((data: any) => null)
+
+        const result = entityManager.reset(DummyApiModel, entity)
+
+        expect(entityManager.getInitialStateOf).toHaveBeenCalledWith(DummyApiModel, 1)
+        expect(repo.save).toHaveBeenCalledWith(initialEntity)
+        expect(result).toEqual(initialEntity)
+    })
+
+    test('no initial state stored', () => {
+        const entity = new DummyApiModel()
+        entity.id = 1
+
+        entityManager.getInitialStateOf = vi.fn((model: typeof ApiResource, id: string | number) => null)
+
+        expect(() => entityManager.reset(DummyApiModel, entity)).toThrowError(
+            'no initial state recorded for this object - abort [dummyModel/1]'
+        )
+    })
+})
+
+describe('flush', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        repo.flush = vi.fn()
+
+        entityManager.flush(DummyApiModel)
+
+        expect(repo.flush).toHaveBeenCalled()
+    })
+})
+
+describe('isNewEntity', () => {
+    test('with new entity', () => {
+        const entity = new DummyApiModel()
+        entity.isNew = vi.fn(() => true)
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number) => entity)
+
+        const result = entityManager.isNewEntity(DummyApiModel, 1)
+
+        expect(result).toBeTruthy()
+    })
+
+    test('with existing entity', () => {
+        const entity = new DummyApiModel()
+        entity.isNew = vi.fn(() => false)
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number) => entity)
+
+        const result = entityManager.isNewEntity(DummyApiModel, 1)
+
+        expect(result).toBeFalsy()
+    })
+
+    test('non-existing entity', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiModel ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number) => null)
+
+        console.error = vi.fn()
+
+        const result = entityManager.isNewEntity(DummyApiModel, 1)
+
+        expect(result).toBeFalsy()
+
+        expect(console.error).toHaveBeenCalledWith('dummyModel/1 does not exist!')
+    })
+})
+
+describe('saveInitialState', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        const entity = { id: 1, name: 'bob' } as DummyApiResource
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.save = vi.fn((record: Element) => null)
+
+        entityManager.saveInitialState(DummyApiResource, entity)
+
+        expect(repo.save).toHaveBeenCalledWith({ id: '_clone_1', name: 'bob' })
+        expect(entity.id).toEqual(1)
+    })
+})
+
+describe('getInitialStateOf', () => {
+    test('with initial state', () => {
+        // @ts-ignore
+        const entity = { id: 1, name: 'bob' } as DummyApiResource
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number | string) => {
+            // @ts-ignore
+            return { id: 1, name: 'robert' } as DummyApiResource
+        })
+
+        const result = entityManager.getInitialStateOf(DummyApiResource, 1) as DummyApiResource
+
+        expect(repo.find).toHaveBeenCalledWith('_clone_1')
+        expect(result.id).toEqual(1)
+        expect(result.name).toEqual('robert')
+    })
+
+    test('without initial state', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number | string) => null)
+
+        const result = entityManager.getInitialStateOf(DummyApiResource, 1) as DummyApiResource
+
+        expect(repo.find).toHaveBeenCalledWith('_clone_1')
+        expect(result).toEqual(null)
+    })
+})
+
+describe('removeTempAfterPersist', () => {
+    test('simple call', () => {
+        // @ts-ignore
+        const entity = new DummyApiResource()
+        entity.id = 'tmp123'
+        entity.isNew = vi.fn(() => true)
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number | string) => entity)
+
+        // @ts-ignore
+        repo.destroy = vi.fn()
+
+        entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')
+
+        expect(repo.destroy).toHaveBeenCalledWith('tmp123')
+        expect(repo.destroy).toHaveBeenCalledWith('_clone_tmp123')
+    })
+
+    test('entity is not temporary', () => {
+        // @ts-ignore
+        const entity = new DummyApiResource()
+        entity.id = 1
+        entity.isNew = vi.fn(() => false)
+
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number | string) => entity)
+
+        // @ts-ignore
+        repo.destroy = vi.fn()
+
+        expect(() => entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')).toThrowError(
+            'Error: Can not remove a non-temporary entity'
+        )
+
+        expect(repo.destroy).toHaveBeenCalledTimes(0)
+    })
+
+    test('entity does not exist', () => {
+        // @ts-ignore
+        const repo = vi.fn() as Repository<ApiResource>
+
+        // @ts-ignore
+        entityManager.getRepository = vi.fn((model: typeof ApiResource) => {
+            return model === DummyApiResource ? repo : null
+        })
+
+        // @ts-ignore
+        repo.find = vi.fn((id: number | string) => null)
+
+        // @ts-ignore
+        repo.destroy = vi.fn()
+
+        console.error = vi.fn()
+
+        entityManager.removeTempAfterPersist(DummyApiResource, 'tmp123')
+
+        expect(repo.destroy).toHaveBeenCalledTimes(0)
+        expect(console.error).toHaveBeenCalledWith('dummyResource/tmp123 does not exist!')
+    })
+})