Skip to content

Commit 5bb705a

Browse files
mgebeilywopianwopian
authored
fix(kitsu-core): Deserialize and link nested relations (#601)
* fix(kistu-core): Deserialize and link nested relations * fix(kitsu-core): Run linter and add jsdoc definitions for previouslyLinked * refactor: remove default value of previouslyLinked in link() Empty object is passed down from root linkRelationships * ci(size-limit): increase warning threshold by 1 kB for CommonJS output * test: check circular JSON exists in output * docs: note previouslyLinked param on linkRelationships is only used internally JSDoc doesn't have anything for this type of thing it seems. Co-authored-by: wopian <james.harris@trutify.com> Co-authored-by: James Harris <wopian@wopian.me>
1 parent 036493b commit 5bb705a

4 files changed

Lines changed: 108 additions & 10 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"eslint": "~7.32.0",
4848
"eslint-config-wopian": "~2.1.0",
4949
"jest": "~27.2.0",
50+
"json-stringify-safe": "~5.0.1",
5051
"lerna": "^3.0.0",
5152
"rollup": "~2.58.0",
5253
"rollup-plugin-delete": "~2.0.0",

packages/kitsu-core/src/linkRelationships/index.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import { filterIncludes } from '../filterIncludes'
99
* @param {string} resource.type Resource type
1010
* @param {Object} [resource.meta] Meta information
1111
* @param {Object[]} included The response included object
12+
* @param {Object} [previouslyLinked] A mapping of already visited resources
1213
* @private
1314
*/
14-
function link ({ id, type, meta }, included) {
15+
function link ({ id, type, meta }, included, previouslyLinked) {
1516
const filtered = filterIncludes(included, { id, type })
16-
if (filtered.relationships) linkRelationships(filtered, included)
17+
previouslyLinked[`${type}#${id}`] = filtered
18+
19+
if (filtered.relationships) {
20+
linkRelationships(filtered, included, previouslyLinked)
21+
}
1722
if (meta) filtered.meta = meta
1823

1924
return deattribute(filtered)
@@ -25,14 +30,16 @@ function link ({ id, type, meta }, included) {
2530
* @param {Object} data The response data object
2631
* @param {Object[]} included The response included object
2732
* @param {string} key Name of the relationship item
33+
* @param {Object} previouslyLinked A mapping of already visited resources
2834
* @private
2935
*/
30-
function linkArray (data, included, key) {
36+
function linkArray (data, included, key, previouslyLinked) {
3137
data[key] = {}
3238
if (data.relationships[key].links) data[key].links = data.relationships[key].links
3339
data[key].data = []
3440
for (const resource of data.relationships[key].data) {
35-
data[key].data.push(link(resource, included))
41+
const cache = previouslyLinked[`${resource.type}#${resource.id}`]
42+
data[key].data.push(cache || link(resource, included, previouslyLinked))
3643
}
3744
delete data.relationships[key]
3845
}
@@ -43,11 +50,14 @@ function linkArray (data, included, key) {
4350
* @param {Object} data The response data object
4451
* @param {Object[]} included The response included object
4552
* @param {string} key Name of the relationship item
53+
* @param {Object} previouslyLinked A mapping of already visited resources
4654
* @private
4755
*/
48-
function linkObject (data, included, key) {
56+
function linkObject (data, included, key, previouslyLinked) {
4957
data[key] = {}
50-
data[key].data = link(data.relationships[key].data, included)
58+
const resource = data.relationships[key].data
59+
const cache = previouslyLinked[`${resource.type}#${resource.id}`]
60+
data[key].data = cache || link(resource, included, previouslyLinked)
5161
if (data.relationships[key].links) data[key].links = data.relationships[key].links
5262
delete data.relationships[key]
5363
}
@@ -70,6 +80,7 @@ function linkAttr (data, key) {
7080
*
7181
* @param {Object} data The response data object
7282
* @param {Object[]} [included] The response included object
83+
* @param {Object} [previouslyLinked] A mapping of already visited resources (internal use only)
7384
* @returns Parsed data
7485
*
7586
* @example
@@ -94,16 +105,16 @@ function linkAttr (data, key) {
94105
* // }
95106
* // }
96107
*/
97-
export function linkRelationships (data, included = []) {
108+
export function linkRelationships (data, included = [], previouslyLinked = {}) {
98109
const { relationships } = data
99110

100111
for (const key in relationships) {
101112
// Relationship contains collection of resources
102113
if (Array.isArray(relationships[key]?.data)) {
103-
linkArray(data, included, key)
114+
linkArray(data, included, key, previouslyLinked)
104115
// Relationship contains a single resource
105116
} else if (relationships[key].data) {
106-
linkObject(data, included, key)
117+
linkObject(data, included, key, previouslyLinked)
107118
} else {
108119
linkAttr(data, key)
109120
}

packages/kitsu-core/src/linkRelationships/index.spec.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import stringify from 'json-stringify-safe'
12
import { linkRelationships } from './'
23

34
describe('kitsu-core', () => {
@@ -88,6 +89,91 @@ describe('kitsu-core', () => {
8889
})
8990
})
9091

92+
it('caches relationships that were previously linked', () => {
93+
expect.assertions(1)
94+
95+
const data = {
96+
id: '1',
97+
type: 'user',
98+
attributes: {
99+
name: 'A user'
100+
},
101+
relationships: {
102+
current_song: {
103+
data: {
104+
id: '1',
105+
type: 'song'
106+
}
107+
}
108+
}
109+
}
110+
111+
const included = [
112+
{
113+
id: '1',
114+
type: 'album',
115+
attributes: {
116+
name: 'Mezmerize'
117+
},
118+
relationships: {
119+
songs: {
120+
data: [
121+
{
122+
id: '1',
123+
type: 'song'
124+
}
125+
]
126+
}
127+
}
128+
},
129+
{
130+
id: '1',
131+
type: 'song',
132+
attributes: {
133+
title: 'Revenga'
134+
},
135+
relationships: {
136+
album: {
137+
data: {
138+
id: '1',
139+
type: 'album'
140+
}
141+
}
142+
}
143+
}
144+
]
145+
146+
const circularResult = linkRelationships(data, included)
147+
const result = JSON.parse(stringify(circularResult))
148+
149+
expect(result).toEqual({
150+
id: '1',
151+
type: 'user',
152+
attributes: {
153+
name: 'A user'
154+
},
155+
current_song: {
156+
data: {
157+
id: '1',
158+
type: 'song',
159+
album: {
160+
data: {
161+
id: '1',
162+
name: 'Mezmerize',
163+
type: 'album',
164+
songs: {
165+
data: [
166+
'[Circular ~.current_song.data]'
167+
]
168+
}
169+
}
170+
},
171+
title: 'Revenga'
172+
}
173+
}
174+
})
175+
})
176+
91177
it('does not deattribute key if theres a relationship (single) with same name (handle invalid JSON:API)', () => {
92178
expect.assertions(1)
93179
const data = {

packages/kitsu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"size-limit": [
5757
{
5858
"path": "./dist/index.js",
59-
"limit": "10 kb",
59+
"limit": "11 kb",
6060
"brotli": true
6161
},
6262
{

0 commit comments

Comments
 (0)