Skip to content

Commit 920ece3

Browse files
committed
feat(kitsu-core): support the bulk extension specification (serialise arrays)
Closes #336
1 parent 66095cc commit 920ece3

2 files changed

Lines changed: 106 additions & 33 deletions

File tree

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

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,27 @@ import { error } from '../error'
44
* Checks if data is valid for serialisation
55
*
66
* @param {Object} obj The data
7-
* @param {string} method Request type
7+
* @param {string} method Request type - `PATCH` or `POST`
88
* @private
99
*/
10-
function isValid (obj, method, type) {
11-
// Check if obj is not an object or empty
12-
if (obj.constructor !== Object || Object.keys(obj).length === 0) {
13-
throw new Error(`${method} requires a JSON object body`)
14-
}
15-
// A POST request is the only request to not require an ID
16-
if (method !== 'POST' && !obj.id) {
17-
throw new Error(`${method} requires an ID for the ${type} type`)
10+
function isValid (isArray, type, payload, method) {
11+
const requireID = new Error(`${method} requires an ID for the ${type} type`)
12+
13+
if (isArray) {
14+
// A POST request is the only request to not require an ID in spec
15+
if (method !== 'POST' && payload.length > 0) {
16+
for (const resource of payload) {
17+
if (!resource.id) throw requireID
18+
}
19+
}
20+
} else {
21+
if (payload.constructor !== Object || Object.keys(payload).length === 0) {
22+
throw new Error(`${method} requires a JSON object body`)
23+
}
24+
// A POST request is the only request to not require an ID in spec
25+
if (method !== 'POST' && !payload.id) {
26+
throw requireID
27+
}
1828
}
1929
}
2030

@@ -86,11 +96,44 @@ function hasID (node) {
8696
return Object.prototype.hasOwnProperty.call(node, 'id')
8797
}
8898

99+
function serialiseRootArray (type, payload, method, options) {
100+
isValid(true, type, payload, method)
101+
const data = []
102+
for (const resource of payload) {
103+
data.push(serialiseRootObject(type, resource, method, options).data)
104+
}
105+
return { data }
106+
}
107+
108+
function serialiseRootObject (type, payload, method, options) {
109+
isValid(false, type, payload, method)
110+
let data = { type }
111+
112+
if (payload?.id) data.id = String(payload.id)
113+
114+
for (const key in payload) {
115+
const node = payload[key]
116+
const nodeType = options.pluralTypes(options.camelCaseTypes(key))
117+
// 1. Skip null nodes, 2. Only grab objects, 3. Filter to only serialise relationable objects
118+
if (node !== null && node.constructor === Object && hasID(node)) {
119+
data = serialiseObject(node, nodeType, key, data, method)
120+
// 1. Skip null nodes, 2. Only grab arrays, 3. Filter to only serialise relationable arrays
121+
} else if (node !== null && Array.isArray(node) && (node.length > 0 && hasID(node[0]))) {
122+
data = serialiseArray(node, nodeType, key, data, method)
123+
// 1. Don't place id/key inside attributes object
124+
} else if (key !== 'id' && key !== 'type') {
125+
data = serialiseAttr(node, key, data)
126+
}
127+
}
128+
129+
return { data }
130+
}
131+
89132
/**
90133
* Serialises an object into a JSON-API structure
91134
*
92135
* @param {string} model Request model
93-
* @param {Object} obj The data
136+
* @param {Object|Array} data The data
94137
* @param {string} method Request type (PATCH, POST, DELETE)
95138
* @param {Object} options Optional configuration for camelCase and pluralisation handling
96139
* @param {Function} options.camelCaseTypes Convert library-entries and library_entries to libraryEntries (default no conversion). To use parameter, import camel from kitsu-core
@@ -116,36 +159,17 @@ function hasID (node) {
116159
* // { data: { id: '1', type: 'anime', attributes: { slug: 'shirobako' } } }
117160
* const output = serialise(model, obj, 'PATCH')
118161
*/
119-
export function serialise (model, obj = {}, method = 'POST', options = {}) {
162+
export function serialise (model, data = {}, method = 'POST', options = {}) {
120163
try {
121164
if (!options.camelCaseTypes) options.camelCaseTypes = s => s
122165
if (!options.pluralTypes) options.pluralTypes = s => s
123166
// Delete relationship to-one (data: null) or to-many (data: [])
124-
if (obj === null || (Array.isArray(obj) && obj.length === 0)) return { data: obj }
167+
if (data === null || (Array.isArray(data) && data.length === 0)) return { data }
125168

126169
const type = options.pluralTypes(options.camelCaseTypes(model))
127-
let data = { type }
128-
129-
isValid(obj, method, type)
130-
131-
if (obj.id) data.id = String(obj.id)
132-
133-
for (const key in obj) {
134-
const node = obj[key]
135-
const nodeType = options.pluralTypes(options.camelCaseTypes(key))
136-
// 1. Skip null nodes, 2. Only grab objects, 3. Filter to only serialise relationable objects
137-
if (node !== null && node.constructor === Object && hasID(node)) {
138-
data = serialiseObject(node, nodeType, key, data, method)
139-
// 1. Skip null nodes, 2. Only grab arrays, 3. Filter to only serialise relationable arrays
140-
} else if (node !== null && Array.isArray(node) && (node.length > 0 && hasID(node[0]))) {
141-
data = serialiseArray(node, nodeType, key, data, method)
142-
// 1. Don't place id/key inside attributes object
143-
} else if (key !== 'id' && key !== 'type') {
144-
data = serialiseAttr(node, key, data)
145-
}
146-
}
147170

148-
return { data }
171+
if (Array.isArray(data) && data?.length > 0) return serialiseRootArray(type, data, method, options)
172+
else return serialiseRootObject(type, data, method, options)
149173
} catch (E) {
150174
throw error(E)
151175
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ describe('kitsu-core', () => {
251251
.toThrowError('PATCH requires an ID for the user type')
252252
})
253253

254+
it('throws an error when missing ID in array', () => {
255+
expect.assertions(1)
256+
expect(() => serialise('user', [ { theme: 'dark' } ], 'PATCH'))
257+
.toThrowError('PATCH requires an ID for the user type')
258+
})
259+
254260
it('serialises strings/numbers/booleans into attributes', () => {
255261
expect.assertions(1)
256262
const input = serialise('resourceModel', {
@@ -314,6 +320,29 @@ describe('kitsu-core', () => {
314320
})
315321
})
316322

323+
it('serialises type objects into relationships inside arrays', () => {
324+
expect.assertions(1)
325+
const input = serialise('resourceModel', [ {
326+
object: {
327+
id: '1',
328+
type: 'relationshipModel'
329+
}
330+
} ])
331+
expect(input).toEqual({
332+
data: [ {
333+
type: 'resourceModel',
334+
relationships: {
335+
object: {
336+
data: {
337+
id: '1',
338+
type: 'relationshipModel'
339+
}
340+
}
341+
}
342+
} ]
343+
})
344+
})
345+
317346
it('serialises bare arrays into attributes', () => {
318347
expect.assertions(1)
319348
const input = serialise('resourceModel', {
@@ -373,5 +402,25 @@ describe('kitsu-core', () => {
373402
data: []
374403
})
375404
})
405+
406+
it('serialises a data array without ID (POST)', () => {
407+
expect.assertions(1)
408+
const resource = { content: 'some new content' }
409+
const resourceOutput = { type: 'posts', attributes: { content: 'some new content' } }
410+
const input = serialise('posts', [ resource, resource ])
411+
expect(input).toEqual({
412+
data: [ resourceOutput, resourceOutput ]
413+
})
414+
})
415+
416+
it('serialises a data array with ID (PATCH/DELETE)', () => {
417+
expect.assertions(1)
418+
const resource = { id: '1', content: 'some new content' }
419+
const resourceOutput = { id: '1', type: 'posts', attributes: { content: 'some new content' } }
420+
const input = serialise('posts', [ resource, resource ])
421+
expect(input).toEqual({
422+
data: [ resourceOutput, resourceOutput ]
423+
})
424+
})
376425
})
377426
})

0 commit comments

Comments
 (0)