Skip to content

Commit d487dd7

Browse files
committed
avoid interpreting array properties as embedded collections
1 parent cd8dbc9 commit d487dd7

File tree

5 files changed

+240
-5
lines changed

5 files changed

+240
-5
lines changed

src/StoreValue.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class StoreValue extends CanHaveItems {
2222
if (key === 'allItems' && isCollection(data)) return
2323
if (key === 'items' && isCollection(data)) {
2424
this.addItemsGetter(data[key], data._meta.self, key)
25-
} else if (Array.isArray(value)) {
25+
} else if (Array.isArray(value) && value.length > 0 && isEntityReference(value[0])) { // need min. 1 item to detect an embedded collection
2626
this[key] = () => new EmbeddedCollection(value, data._meta.self, key, { get, reload, isUnknown }, config, data._meta.load)
2727
} else if (isEntityReference(value)) {
2828
this[key] = () => this.apiActions.get(value.href)

tests/resources/array-property.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"serverResponse": {
3+
"id": 1,
4+
"arrayProperty": [
5+
{
6+
"a": 1,
7+
"nested": [
8+
{
9+
"b": 2
10+
}
11+
]
12+
}
13+
],
14+
"emptyArray": [],
15+
"_links": {
16+
"self": {
17+
"href": "/camps/1"
18+
}
19+
}
20+
},
21+
"storeState": {
22+
"/camps/1": {
23+
"id": 1,
24+
"arrayProperty": [
25+
{
26+
"a": 1,
27+
"nested": [
28+
{
29+
"b": 2
30+
}
31+
]
32+
}
33+
],
34+
"emptyArray": [],
35+
"_meta": {
36+
"self": "/camps/1"
37+
}
38+
}
39+
}
40+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
"serverResponse": {
3+
"id": 1,
4+
"_embedded": {
5+
"periods": [
6+
{
7+
"id": 104,
8+
"start": "01-01-2019",
9+
"end": "03-01-2019",
10+
"_links": {
11+
"self": {
12+
"href": "/periods/104"
13+
},
14+
"camp": {
15+
"href": "/camps/1"
16+
},
17+
"activities": {
18+
"href": "/periods/104/activities"
19+
}
20+
}
21+
},
22+
{
23+
"id": 128,
24+
"start": "12-04-2019",
25+
"end": "19-04-2019",
26+
"_links": {
27+
"self": {
28+
"href": "/periods/128"
29+
},
30+
"camp": {
31+
"href": "/camps/1"
32+
},
33+
"activities": {
34+
"href": "/periods/128/activities"
35+
}
36+
}
37+
}
38+
]
39+
},
40+
"_links": {
41+
"self": {
42+
"href": "/camps/1"
43+
},
44+
"periods": {
45+
"href": "/camps/1/periods"
46+
}
47+
}
48+
},
49+
"storeState": {
50+
"/periods/104": {
51+
"id": 104,
52+
"start": "01-01-2019",
53+
"end": "03-01-2019",
54+
"camp": {
55+
"href": "/camps/1"
56+
},
57+
"activities": {
58+
"href": "/periods/104/activities"
59+
},
60+
"_meta": {
61+
"self": "/periods/104"
62+
}
63+
},
64+
"/periods/128": {
65+
"id": 128,
66+
"start": "12-04-2019",
67+
"end": "19-04-2019",
68+
"camp": {
69+
"href": "/camps/1"
70+
},
71+
"activities": {
72+
"href": "/periods/128/activities"
73+
},
74+
"_meta": {
75+
"self": "/periods/128"
76+
}
77+
},
78+
"/camps/1": {
79+
"id": 1,
80+
"periods": {
81+
"href": "/camps/1/periods"
82+
},
83+
"_meta": {
84+
"self": "/camps/1"
85+
}
86+
},
87+
"/camps/1/periods": {
88+
"_meta": {
89+
"self": "/camps/1/periods"
90+
},
91+
"items": [
92+
{
93+
"href": "/periods/104"
94+
},
95+
{
96+
"href": "/periods/128"
97+
}
98+
]
99+
}
100+
}
101+
}

tests/resources/object-property.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"serverResponse": {
3+
"id": 1,
4+
"objectProperty": {
5+
"a": 1,
6+
"nested": {
7+
"b": 2
8+
}
9+
},
10+
"emptyObject": {},
11+
"_links": {
12+
"self": {
13+
"href": "/camps/1"
14+
}
15+
}
16+
},
17+
"storeState": {
18+
"/camps/1": {
19+
"id": 1,
20+
"objectProperty": {
21+
"a": 1,
22+
"nested": {
23+
"b": 2
24+
}
25+
},
26+
"emptyObject": {},
27+
"_meta": {
28+
"self": "/camps/1"
29+
}
30+
}
31+
}
32+
}

tests/store.spec.js

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import { cloneDeep } from 'lodash'
99
import embeddedSingleEntity from './resources/embedded-single-entity'
1010
import referenceToSingleEntity from './resources/reference-to-single-entity'
1111
import embeddedCollection from './resources/embedded-collection'
12+
import embeddedLinkedCollection from './resources/embedded-linked-collection'
1213
import linkedSingleEntity from './resources/linked-single-entity'
1314
import linkedCollection from './resources/linked-collection'
1415
import collectionFirstPage from './resources/collection-firstPage'
1516
import collectionPage1 from './resources/collection-page1'
1617
import circularReference from './resources/circular-reference'
1718
import multipleReferencesToUser from './resources/multiple-references-to-user'
1819
import templatedLink from './resources/templated-link'
20+
import objectProperty from './resources/object-property'
21+
import arrayProperty from './resources/array-property'
1922

2023
async function letNetworkRequestFinish () {
2124
await new Promise(resolve => {
@@ -29,11 +32,9 @@ let vm
2932
let stateCopy
3033

3134
describe('API store', () => {
32-
3335
([true, false]).forEach(avoidNPlusOneRequests => {
3436
const title = avoidNPlusOneRequests ? 'avoiding n+1 queries' : 'not avoiding n+1 queries'
3537
describe(title, () => {
36-
3738
beforeAll(() => {
3839
axios.defaults.baseURL = 'http://localhost'
3940
Vue.use(Vuex)
@@ -112,6 +113,27 @@ describe('API store', () => {
112113
done()
113114
})
114115

116+
it('imports embedded collection with link', async done => {
117+
// given
118+
axiosMock.onGet('http://localhost/camps/1').reply(200, embeddedLinkedCollection.serverResponse)
119+
120+
// when
121+
vm.api.get('/camps/1')
122+
123+
// then
124+
expect(vm.$store.state.api).toMatchObject({ '/camps/1': { _meta: { self: '/camps/1', loading: true } } })
125+
await letNetworkRequestFinish()
126+
expect(vm.$store.state.api).toMatchObject(embeddedLinkedCollection.storeState)
127+
expect(vm.api.get('/camps/1')._meta.self).toEqual('http://localhost/camps/1')
128+
expect(vm.api.get('/camps/1').periods().items[0]._meta.self).toEqual('http://localhost/periods/104')
129+
expect(vm.api.get('/camps/1').periods().items[1]._meta.self).toEqual('http://localhost/periods/128')
130+
expect(vm.api.get('/periods/104')._meta.self).toEqual('http://localhost/periods/104')
131+
expect(vm.api.get('/periods/104').camp()._meta.self).toEqual('http://localhost/camps/1')
132+
expect(vm.api.get('/periods/128')._meta.self).toEqual('http://localhost/periods/128')
133+
expect(vm.api.get('/periods/128').camp()._meta.self).toEqual('http://localhost/camps/1')
134+
done()
135+
})
136+
115137
it('imports linked single entity', async done => {
116138
// given
117139
axiosMock.onGet('http://localhost/camps/1').reply(200, linkedSingleEntity.serverResponse)
@@ -474,7 +496,7 @@ describe('API store', () => {
474496
expect(() => vm.api.get({})._meta)
475497

476498
// then
477-
.toThrow(Error)
499+
.toThrow(Error)
478500
})
479501

480502
it('purges and later re-fetches a URI from the store', async done => {
@@ -730,7 +752,7 @@ describe('API store', () => {
730752
const bookResponse = {
731753
id: 555,
732754
_embedded: {
733-
chapters: [ chapter1Response, chapter2Response, chapter3Response ]
755+
chapters: [chapter1Response, chapter2Response, chapter3Response]
734756
},
735757
_links: {
736758
self: {
@@ -1317,6 +1339,46 @@ describe('API store', () => {
13171339
// then
13181340
return expect(load).rejects.toThrow('Failed Validation')
13191341
})
1342+
1343+
it('can handle object property', async done => {
1344+
// given
1345+
axiosMock.onGet('http://localhost/camps/1').reply(200, objectProperty.serverResponse)
1346+
1347+
// when
1348+
vm.api.get('/camps/1')
1349+
await letNetworkRequestFinish()
1350+
1351+
// then
1352+
expect(vm.$store.state.api).toMatchObject(objectProperty.storeState)
1353+
1354+
expect(vm.api.get('/camps/1').objectProperty).toBeInstanceOf(Object)
1355+
expect(vm.api.get('/camps/1').objectProperty.a).toEqual(1)
1356+
expect(vm.api.get('/camps/1').objectProperty.nested.b).toEqual(2)
1357+
1358+
expect(vm.api.get('/camps/1').emptyObject).toBeInstanceOf(Object)
1359+
expect(vm.api.get('/camps/1').emptyObject).toEqual({})
1360+
done()
1361+
})
1362+
1363+
it('can handle array property', async done => {
1364+
// given
1365+
axiosMock.onGet('http://localhost/camps/1').reply(200, arrayProperty.serverResponse)
1366+
1367+
// when
1368+
vm.api.get('/camps/1')
1369+
await letNetworkRequestFinish()
1370+
1371+
// then
1372+
expect(vm.$store.state.api).toMatchObject(arrayProperty.storeState)
1373+
1374+
expect(vm.api.get('/camps/1').arrayProperty).toBeInstanceOf(Array)
1375+
expect(vm.api.get('/camps/1').arrayProperty[0].a).toEqual(1)
1376+
expect(vm.api.get('/camps/1').arrayProperty[0].nested[0].b).toEqual(2)
1377+
1378+
expect(vm.api.get('/camps/1').emptyArray).toBeInstanceOf(Array)
1379+
expect(vm.api.get('/camps/1').emptyArray).toEqual([])
1380+
done()
1381+
})
13201382
})
13211383
})
13221384
})

0 commit comments

Comments
 (0)