Skip to content

Commit 72dc2be

Browse files
committed
feat(kitsu-core): implement opt-in data hoisting when deserialising response
1 parent 9039f9b commit 72dc2be

8 files changed

Lines changed: 527 additions & 49 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"size-limit": "^8.2.0",
5959
"typescript": "^5.9.0"
6060
},
61+
"resolutions": {
62+
"@types/minimatch": "5.1.2"
63+
},
6164
"jest": {
6265
"coverageThreshold": {
6366
"global": {

packages/kitsu-core/README.md

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,40 +99,42 @@ All code released under [MIT]
9999
* [deattribute](#deattribute)
100100
* [Parameters](#parameters-1)
101101
* [Examples](#examples-1)
102-
* [deserialise](#deserialise)
102+
* [hoist](#hoist)
103103
* [Parameters](#parameters-2)
104+
* [deserialise](#deserialise)
105+
* [Parameters](#parameters-3)
104106
* [Examples](#examples-2)
105107
* [error](#error)
106-
* [Parameters](#parameters-3)
108+
* [Parameters](#parameters-4)
107109
* [Examples](#examples-3)
108110
* [filterIncludes](#filterincludes)
109-
* [Parameters](#parameters-4)
111+
* [Parameters](#parameters-5)
110112
* [Examples](#examples-4)
111113
* [kebab](#kebab)
112-
* [Parameters](#parameters-5)
114+
* [Parameters](#parameters-6)
113115
* [Examples](#examples-5)
114116
* [linkRelationships](#linkrelationships)
115-
* [Parameters](#parameters-6)
117+
* [Parameters](#parameters-7)
116118
* [Examples](#examples-6)
117119
* [isDeepEqual](#isdeepequal)
118-
* [Parameters](#parameters-7)
120+
* [Parameters](#parameters-8)
119121
* [Examples](#examples-7)
120122
* [query](#query)
121-
* [Parameters](#parameters-8)
123+
* [Parameters](#parameters-9)
122124
* [Examples](#examples-8)
123125
* [serialise](#serialise)
124-
* [Parameters](#parameters-9)
126+
* [Parameters](#parameters-10)
125127
* [Examples](#examples-9)
126128
* [snake](#snake)
127-
* [Parameters](#parameters-10)
129+
* [Parameters](#parameters-11)
128130
* [Examples](#examples-10)
129131
* [splitModel](#splitmodel)
130-
* [Parameters](#parameters-11)
132+
* [Parameters](#parameters-12)
131133
* [Examples](#examples-11)
132134

133135
### camel
134136

135-
[packages/kitsu-core/src/camel/index.js:14-14](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/camel/index.js#L14-L14 "Source code on GitHub")
137+
[packages/kitsu-core/src/camel/index.js:14-14](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/camel/index.js#L14-L14 "Source code on GitHub")
136138

137139
Converts kebab-case and snake\_case into camelCase
138140

@@ -158,7 +160,7 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
158160

159161
### deattribute
160162

161-
[packages/kitsu-core/src/deattribute/index.js:29-51](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/deattribute/index.js#L29-L51 "Source code on GitHub")
163+
[packages/kitsu-core/src/deattribute/index.js:29-51](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/deattribute/index.js#L29-L51 "Source code on GitHub")
162164

163165
Hoists attributes to be top-level
164166

@@ -198,15 +200,35 @@ const output = deattribute(data) // { id: '1', type: 'users', slug: 'wopian' }
198200

199201
Returns **([Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object) | [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)>)** Deattributed resource data
200202

203+
### hoist
204+
205+
[packages/kitsu-core/src/deserialise/index.js:24-77](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/deserialise/index.js#L24-L77 "Source code on GitHub")
206+
207+
Recursively traverses and clones an object or array, handling cyclic references.
208+
If the object is a wrapper of the form { data: ... }, it unwraps and processes the `data` property.
209+
210+
#### Parameters
211+
212+
* `object` **any** The input to hoist (object or array)
213+
214+
Returns **any** The hoisted object or array
215+
201216
### deserialise
202217

203-
[packages/kitsu-core/src/deserialise/index.js:63-78](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/deserialise/index.js#L63-L78 "Source code on GitHub")
218+
[packages/kitsu-core/src/deserialise/index.js:150-170](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/deserialise/index.js#L150-L170 "Source code on GitHub")
204219

205220
Deserialises a JSON-API response
206221

207222
#### Parameters
208223

209224
* `response` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** The raw JSON:API response object
225+
* `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** Deserialisation options (optional, default `{}`)
226+
227+
* `options.hoistData` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** If enabled, the contents of the `data` property will be hoisted to the parent. This provides a flatter response object, but removes access to `links` and `meta` properties. It will transform:```js
228+
{ data: { id: '1', type: 'people', coworkers: data: [ { id: '2', type: 'people' } ] } }
229+
```into the following:```js
230+
{ id: '1', type: 'people', coworkers: [ { id: '2', type: 'people' } ] }
231+
``` (optional, default `false`)
210232

211233
#### Examples
212234

@@ -250,7 +272,7 @@ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
250272

251273
### error
252274

253-
[packages/kitsu-core/src/error/index.js:27-33](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/error/index.js#L27-L33 "Source code on GitHub")
275+
[packages/kitsu-core/src/error/index.js:27-33](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/error/index.js#L27-L33 "Source code on GitHub")
254276

255277
Uniform error handling for Axios, JSON:API and internal package errors. Mutated Error object is rethrown to the caller.
256278

@@ -287,7 +309,7 @@ error({
287309

288310
### filterIncludes
289311

290-
[packages/kitsu-core/src/filterIncludes/index.js:33-46](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/filterIncludes/index.js#L33-L46 "Source code on GitHub")
312+
[packages/kitsu-core/src/filterIncludes/index.js:33-46](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/filterIncludes/index.js#L33-L46 "Source code on GitHub")
291313

292314
Filters includes for the specific relationship requested
293315

@@ -327,7 +349,7 @@ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
327349

328350
### kebab
329351

330-
[packages/kitsu-core/src/kebab/index.js:11-11](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/kebab/index.js#L11-L11 "Source code on GitHub")
352+
[packages/kitsu-core/src/kebab/index.js:11-11](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/kebab/index.js#L11-L11 "Source code on GitHub")
331353

332354
Converts camelCase into kebab-case
333355

@@ -345,7 +367,7 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
345367

346368
### linkRelationships
347369

348-
[packages/kitsu-core/src/linkRelationships/index.js:144-164](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/linkRelationships/index.js#L144-L164 "Source code on GitHub")
370+
[packages/kitsu-core/src/linkRelationships/index.js:144-164](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/linkRelationships/index.js#L144-L164 "Source code on GitHub")
349371

350372
Links relationships to included data
351373

@@ -385,7 +407,7 @@ Returns **any** Parsed data
385407

386408
### isDeepEqual
387409

388-
[packages/kitsu-core/src/deepEqual/index.js:18-42](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/deepEqual/index.js#L18-L42 "Source code on GitHub")
410+
[packages/kitsu-core/src/deepEqual/index.js:18-42](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/deepEqual/index.js#L18-L42 "Source code on GitHub")
389411

390412
Compare two objects equality
391413

@@ -414,7 +436,7 @@ Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/
414436

415437
### query
416438

417-
[packages/kitsu-core/src/query/index.js:57-66](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/query/index.js#L57-L66 "Source code on GitHub")
439+
[packages/kitsu-core/src/query/index.js:57-66](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/query/index.js#L57-L66 "Source code on GitHub")
418440

419441
Constructs a URL query string for JSON:API parameters
420442

@@ -443,7 +465,7 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
443465

444466
### serialise
445467

446-
[packages/kitsu-core/src/serialise/index.js:210-221](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/serialise/index.js#L210-L221 "Source code on GitHub")
468+
[packages/kitsu-core/src/serialise/index.js:210-221](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/serialise/index.js#L210-L221 "Source code on GitHub")
447469

448470
Serialises an object into a JSON-API structure
449471

@@ -488,7 +510,7 @@ Returns **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
488510

489511
### snake
490512

491-
[packages/kitsu-core/src/snake/index.js:11-11](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/snake/index.js#L11-L11 "Source code on GitHub")
513+
[packages/kitsu-core/src/snake/index.js:11-11](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/snake/index.js#L11-L11 "Source code on GitHub")
492514

493515
Converts camelCase into snake\_case
494516

@@ -506,7 +528,7 @@ Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/G
506528

507529
### splitModel
508530

509-
[packages/kitsu-core/src/splitModel/index.js:29-39](https://github.com/wopian/kitsu/blob/a794fdd4c0266be8d8404455ebf2847e47e66a64/packages/kitsu-core/src/splitModel/index.js#L29-L39 "Source code on GitHub")
531+
[packages/kitsu-core/src/splitModel/index.js:29-39](https://github.com/wopian/kitsu/blob/9039f9be49ca8ba032ad6b7099316b6751e18198/packages/kitsu-core/src/splitModel/index.js#L29-L39 "Source code on GitHub")
510532

511533
Split model name from the model's resource URL
512534

packages/kitsu-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"unpkg": "dist/index.browser.js",
1818
"jsdelivr": "dist/index.browser.js",
1919
"types": "types/index.d.ts",
20+
"type": "module",
2021
"homepage": "https://github.com/wopian/kitsu/tree/master/packages/kitsu-core#readme",
2122
"repository": "https://github.com/wopian/kitsu",
2223
"bugs": {

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

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,84 @@
11
import { deattribute } from '../deattribute'
22
import { linkRelationships } from '../linkRelationships'
33

4+
/**
5+
* Hoists the contents of `data` properties to their parent object recursively.
6+
* This provides a flatter response object, but removes access to `links` and `meta`
7+
* properties.
8+
*
9+
* @param {Object} response The response object
10+
* @returns {Object} The response with `data` properties hoisted
11+
* @private
12+
*/
13+
function hoistData (response) {
14+
const seen = new WeakMap()
15+
const hasOwn = Object.prototype.hasOwnProperty
16+
17+
/**
18+
* Recursively traverses and clones an object or array, handling cyclic references.
19+
* If the object is a wrapper of the form { data: ... }, it unwraps and processes the `data` property.
20+
*
21+
* @param {any} object The input to hoist (object or array)
22+
* @returns {any} The hoisted object or array
23+
*/
24+
function hoist (object) {
25+
if (object && typeof object === 'object') {
26+
if (seen.has(object)) {
27+
return seen.get(object)
28+
}
29+
30+
let out
31+
32+
if (Array.isArray(object)) {
33+
out = []
34+
seen.set(object, out)
35+
36+
let i = 0
37+
38+
for (const item of object) {
39+
out[i] = hoist(item)
40+
i++
41+
}
42+
43+
return out
44+
}
45+
46+
// Check for { data: ... } wrapper without allocating Object.keys
47+
let onlyData = false
48+
49+
for (const key in object) {
50+
if (!hasOwn.call(object, key)) continue
51+
52+
if (key === 'data' && !onlyData) {
53+
onlyData = true
54+
} else {
55+
onlyData = false
56+
break
57+
}
58+
}
59+
60+
if (onlyData) {
61+
return hoist(object.data)
62+
}
63+
64+
out = {}
65+
seen.set(object, out)
66+
67+
for (const key in object) {
68+
if (hasOwn.call(object, key)) {
69+
out[key] = hoist(object[key])
70+
}
71+
}
72+
73+
return out
74+
}
75+
76+
return object
77+
}
78+
79+
return hoist(response)
80+
}
81+
482
/**
583
* Deserialises an array from a JSON-API structure
684
*
@@ -28,6 +106,15 @@ function deserialiseArray (response) {
28106
* Deserialises a JSON-API response
29107
*
30108
* @param {Object} response The raw JSON:API response object
109+
* @param {Object} [options={}] Deserialisation options
110+
* @param {boolean} [options.hoistData=false] If enabled, the contents of the `data` property will be hoisted to the parent. This provides a flatter response object, but removes access to `links` and `meta` properties. It will transform:
111+
* ```js
112+
* { data: { id: '1', type: 'people', coworkers: data: [ { id: '2', type: 'people' } ] } }
113+
* ```
114+
* into the following:
115+
* ```js
116+
* { id: '1', type: 'people', coworkers: [ { id: '2', type: 'people' } ] }
117+
* ```
31118
* @returns {Object} The deserialised response
32119
*
33120
* @example <caption>Deserialise with a basic data object</caption>
@@ -60,7 +147,7 @@ function deserialiseArray (response) {
60147
* ]
61148
* }) // { data: { id: '1', user: { data: { type: 'users', id: '2', slug: 'wopian' } } } }
62149
*/
63-
export function deserialise (response) {
150+
export function deserialise (response, options = { hoistData: false }) {
64151
if (!response) return
65152

66153
// Collection of resources
@@ -74,5 +161,10 @@ export function deserialise (response) {
74161
// Move attributes to the parent object
75162
if (response.data?.attributes) response.data = deattribute(response.data)
76163

164+
// Hoist data to flatten the response structure if requested
165+
if (options.hoistData && response.data) {
166+
response = hoistData(response)
167+
}
168+
77169
return response
78170
}

0 commit comments

Comments
 (0)