From d487dd7009c93d9f2f82948322f742503c8adf31 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 21 Nov 2021 16:02:14 +0100 Subject: [PATCH] avoid interpreting array properties as embedded collections --- src/StoreValue.js | 2 +- tests/resources/array-property.json | 40 +++++++ .../resources/embedded-linked-collection.json | 101 ++++++++++++++++++ tests/resources/object-property.json | 32 ++++++ tests/store.spec.js | 70 +++++++++++- 5 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 tests/resources/array-property.json create mode 100644 tests/resources/embedded-linked-collection.json create mode 100644 tests/resources/object-property.json diff --git a/src/StoreValue.js b/src/StoreValue.js index b2c4ef7e..6a76bac2 100644 --- a/src/StoreValue.js +++ b/src/StoreValue.js @@ -22,7 +22,7 @@ class StoreValue extends CanHaveItems { if (key === 'allItems' && isCollection(data)) return if (key === 'items' && isCollection(data)) { this.addItemsGetter(data[key], data._meta.self, key) - } else if (Array.isArray(value)) { + } else if (Array.isArray(value) && value.length > 0 && isEntityReference(value[0])) { // need min. 1 item to detect an embedded collection this[key] = () => new EmbeddedCollection(value, data._meta.self, key, { get, reload, isUnknown }, config, data._meta.load) } else if (isEntityReference(value)) { this[key] = () => this.apiActions.get(value.href) diff --git a/tests/resources/array-property.json b/tests/resources/array-property.json new file mode 100644 index 00000000..a75ea3c7 --- /dev/null +++ b/tests/resources/array-property.json @@ -0,0 +1,40 @@ +{ + "serverResponse": { + "id": 1, + "arrayProperty": [ + { + "a": 1, + "nested": [ + { + "b": 2 + } + ] + } + ], + "emptyArray": [], + "_links": { + "self": { + "href": "/camps/1" + } + } + }, + "storeState": { + "/camps/1": { + "id": 1, + "arrayProperty": [ + { + "a": 1, + "nested": [ + { + "b": 2 + } + ] + } + ], + "emptyArray": [], + "_meta": { + "self": "/camps/1" + } + } + } +} \ No newline at end of file diff --git a/tests/resources/embedded-linked-collection.json b/tests/resources/embedded-linked-collection.json new file mode 100644 index 00000000..d527aeff --- /dev/null +++ b/tests/resources/embedded-linked-collection.json @@ -0,0 +1,101 @@ +{ + "serverResponse": { + "id": 1, + "_embedded": { + "periods": [ + { + "id": 104, + "start": "01-01-2019", + "end": "03-01-2019", + "_links": { + "self": { + "href": "/periods/104" + }, + "camp": { + "href": "/camps/1" + }, + "activities": { + "href": "/periods/104/activities" + } + } + }, + { + "id": 128, + "start": "12-04-2019", + "end": "19-04-2019", + "_links": { + "self": { + "href": "/periods/128" + }, + "camp": { + "href": "/camps/1" + }, + "activities": { + "href": "/periods/128/activities" + } + } + } + ] + }, + "_links": { + "self": { + "href": "/camps/1" + }, + "periods": { + "href": "/camps/1/periods" + } + } + }, + "storeState": { + "/periods/104": { + "id": 104, + "start": "01-01-2019", + "end": "03-01-2019", + "camp": { + "href": "/camps/1" + }, + "activities": { + "href": "/periods/104/activities" + }, + "_meta": { + "self": "/periods/104" + } + }, + "/periods/128": { + "id": 128, + "start": "12-04-2019", + "end": "19-04-2019", + "camp": { + "href": "/camps/1" + }, + "activities": { + "href": "/periods/128/activities" + }, + "_meta": { + "self": "/periods/128" + } + }, + "/camps/1": { + "id": 1, + "periods": { + "href": "/camps/1/periods" + }, + "_meta": { + "self": "/camps/1" + } + }, + "/camps/1/periods": { + "_meta": { + "self": "/camps/1/periods" + }, + "items": [ + { + "href": "/periods/104" + }, + { + "href": "/periods/128" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/resources/object-property.json b/tests/resources/object-property.json new file mode 100644 index 00000000..d4313a8f --- /dev/null +++ b/tests/resources/object-property.json @@ -0,0 +1,32 @@ +{ + "serverResponse": { + "id": 1, + "objectProperty": { + "a": 1, + "nested": { + "b": 2 + } + }, + "emptyObject": {}, + "_links": { + "self": { + "href": "/camps/1" + } + } + }, + "storeState": { + "/camps/1": { + "id": 1, + "objectProperty": { + "a": 1, + "nested": { + "b": 2 + } + }, + "emptyObject": {}, + "_meta": { + "self": "/camps/1" + } + } + } +} \ No newline at end of file diff --git a/tests/store.spec.js b/tests/store.spec.js index 4540fc1f..a95b629a 100644 --- a/tests/store.spec.js +++ b/tests/store.spec.js @@ -9,6 +9,7 @@ import { cloneDeep } from 'lodash' import embeddedSingleEntity from './resources/embedded-single-entity' import referenceToSingleEntity from './resources/reference-to-single-entity' import embeddedCollection from './resources/embedded-collection' +import embeddedLinkedCollection from './resources/embedded-linked-collection' import linkedSingleEntity from './resources/linked-single-entity' import linkedCollection from './resources/linked-collection' import collectionFirstPage from './resources/collection-firstPage' @@ -16,6 +17,8 @@ import collectionPage1 from './resources/collection-page1' import circularReference from './resources/circular-reference' import multipleReferencesToUser from './resources/multiple-references-to-user' import templatedLink from './resources/templated-link' +import objectProperty from './resources/object-property' +import arrayProperty from './resources/array-property' async function letNetworkRequestFinish () { await new Promise(resolve => { @@ -29,11 +32,9 @@ let vm let stateCopy describe('API store', () => { - ([true, false]).forEach(avoidNPlusOneRequests => { const title = avoidNPlusOneRequests ? 'avoiding n+1 queries' : 'not avoiding n+1 queries' describe(title, () => { - beforeAll(() => { axios.defaults.baseURL = 'http://localhost' Vue.use(Vuex) @@ -112,6 +113,27 @@ describe('API store', () => { done() }) + it('imports embedded collection with link', async done => { + // given + axiosMock.onGet('http://localhost/camps/1').reply(200, embeddedLinkedCollection.serverResponse) + + // when + vm.api.get('/camps/1') + + // then + expect(vm.$store.state.api).toMatchObject({ '/camps/1': { _meta: { self: '/camps/1', loading: true } } }) + await letNetworkRequestFinish() + expect(vm.$store.state.api).toMatchObject(embeddedLinkedCollection.storeState) + expect(vm.api.get('/camps/1')._meta.self).toEqual('http://localhost/camps/1') + expect(vm.api.get('/camps/1').periods().items[0]._meta.self).toEqual('http://localhost/periods/104') + expect(vm.api.get('/camps/1').periods().items[1]._meta.self).toEqual('http://localhost/periods/128') + expect(vm.api.get('/periods/104')._meta.self).toEqual('http://localhost/periods/104') + expect(vm.api.get('/periods/104').camp()._meta.self).toEqual('http://localhost/camps/1') + expect(vm.api.get('/periods/128')._meta.self).toEqual('http://localhost/periods/128') + expect(vm.api.get('/periods/128').camp()._meta.self).toEqual('http://localhost/camps/1') + done() + }) + it('imports linked single entity', async done => { // given axiosMock.onGet('http://localhost/camps/1').reply(200, linkedSingleEntity.serverResponse) @@ -474,7 +496,7 @@ describe('API store', () => { expect(() => vm.api.get({})._meta) // then - .toThrow(Error) + .toThrow(Error) }) it('purges and later re-fetches a URI from the store', async done => { @@ -730,7 +752,7 @@ describe('API store', () => { const bookResponse = { id: 555, _embedded: { - chapters: [ chapter1Response, chapter2Response, chapter3Response ] + chapters: [chapter1Response, chapter2Response, chapter3Response] }, _links: { self: { @@ -1317,6 +1339,46 @@ describe('API store', () => { // then return expect(load).rejects.toThrow('Failed Validation') }) + + it('can handle object property', async done => { + // given + axiosMock.onGet('http://localhost/camps/1').reply(200, objectProperty.serverResponse) + + // when + vm.api.get('/camps/1') + await letNetworkRequestFinish() + + // then + expect(vm.$store.state.api).toMatchObject(objectProperty.storeState) + + expect(vm.api.get('/camps/1').objectProperty).toBeInstanceOf(Object) + expect(vm.api.get('/camps/1').objectProperty.a).toEqual(1) + expect(vm.api.get('/camps/1').objectProperty.nested.b).toEqual(2) + + expect(vm.api.get('/camps/1').emptyObject).toBeInstanceOf(Object) + expect(vm.api.get('/camps/1').emptyObject).toEqual({}) + done() + }) + + it('can handle array property', async done => { + // given + axiosMock.onGet('http://localhost/camps/1').reply(200, arrayProperty.serverResponse) + + // when + vm.api.get('/camps/1') + await letNetworkRequestFinish() + + // then + expect(vm.$store.state.api).toMatchObject(arrayProperty.storeState) + + expect(vm.api.get('/camps/1').arrayProperty).toBeInstanceOf(Array) + expect(vm.api.get('/camps/1').arrayProperty[0].a).toEqual(1) + expect(vm.api.get('/camps/1').arrayProperty[0].nested[0].b).toEqual(2) + + expect(vm.api.get('/camps/1').emptyArray).toBeInstanceOf(Array) + expect(vm.api.get('/camps/1').emptyArray).toEqual([]) + done() + }) }) }) })