Skip to content

Commit ef94ae0

Browse files
pedepwopian
authored andcommitted
feat(kitsu): Configurable modern query serializer
1 parent 240a00e commit ef94ae0

4 files changed

Lines changed: 98 additions & 12 deletions

File tree

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,40 @@
33
*
44
* @param {string} value Right-hand side of the query
55
* @param {string} key Left-hand side of the query
6+
* @param {boolean} traditional use traditional array key serializer
7+
*
68
* @returns {string} URL query string
79
* @private
810
*/
9-
function queryFormat (value, key) {
10-
if (value !== null && Array.isArray(value)) return value.map(v => queryFormat(v, key)).join('&')
11-
else if (value !== null && typeof value === 'object') return query(value, key)
11+
function queryFormat (value, key, traditional) {
12+
if (traditional && value !== null && Array.isArray(value)) return value.map(v => queryFormat(v, key, traditional)).join('&')
13+
if (!traditional && value !== null && Array.isArray(value)) return value.map(v => queryFormat(v, `${key}[]`, traditional)).join('&')
14+
else if (value !== null && typeof value === 'object') return query(value, key, traditional)
1215
else return encodeURIComponent(key) + '=' + encodeURIComponent(value)
1316
}
1417

18+
/**
19+
* Formats key names to correct array syntax
20+
*
21+
* @param {string} [param] Parameter name to parse
22+
*
23+
* @returns {string} Key name in nested query-param format with optional array style suffix
24+
* @private
25+
*/
26+
export function paramKeyName (param) {
27+
if ([ '[]', '][' ].includes(param.slice(-2))) {
28+
return `[${param.slice(0, -2)}][]`
29+
}
30+
31+
return `[${param}]`
32+
}
33+
1534
/**
1635
* Constructs a URL query string for JSON:API parameters
1736
*
1837
* @param {Object} [params] Parameters to parse
1938
* @param {string} [prefix] Prefix for nested parameters - used internally
39+
* @param {boolean} [traditional=true] Use the traditional (default) or modern param serializer. Set to false if your server is running Ruby on Rails or other modern web frameworks
2040
* @returns {string} URL query string
2141
*
2242
* @example
@@ -31,12 +51,13 @@ function queryFormat (value, key) {
3151
* })
3252
* // filter%5Bslug%5D=cowboy-bebop&filter%5Btitle%5D%5Bvalue%5D=foo&sort=-id
3353
*/
34-
export function query (params, prefix = null) {
54+
55+
export function query (params, prefix = null, traditional = true) {
3556
const str = []
3657

3758
for (const param in params) {
3859
str.push(
39-
queryFormat(params[param], prefix ? `${prefix}[${param}]` : param)
60+
queryFormat(params[param], prefix ? `${prefix}${paramKeyName(param)}` : param, traditional)
4061
)
4162
}
4263
return str.join('&')

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe('kitsu-core', () => {
5959
})).toEqual('fields%5Babc%5D%5Bdef%5D%5Bghi%5D%5Bjkl%5D=mno')
6060
})
6161

62-
it('builds list parameters', () => {
62+
it('builds list parameters in traditional mode', () => {
6363
expect.assertions(1)
6464
expect(query({
6565
filter: {
@@ -68,13 +68,50 @@ describe('kitsu-core', () => {
6868
})).toEqual('filter%5Bid_in%5D=1&filter%5Bid_in%5D=2&filter%5Bid_in%5D=3')
6969
})
7070

71-
it('builds nested list parameters', () => {
71+
it('builds nested list parameters in traditional mode', () => {
7272
expect.assertions(1)
7373
expect(query({
7474
filter: {
7575
users: [ { id: 1, type: 'users' }, { id: 2, type: 'users' } ]
7676
}
7777
})).toEqual('filter%5Busers%5D%5Bid%5D=1&filter%5Busers%5D%5Btype%5D=users&filter%5Busers%5D%5Bid%5D=2&filter%5Busers%5D%5Btype%5D=users')
7878
})
79+
80+
it('builds list parameters in modern mode', () => {
81+
expect.assertions(1)
82+
expect(query({
83+
filter: {
84+
id_in: [ 1, 2, 3 ]
85+
}
86+
}, null, false)).toEqual('filter%5Bid_in%5D%5B%5D=1&filter%5Bid_in%5D%5B%5D=2&filter%5Bid_in%5D%5B%5D=3')
87+
})
88+
89+
it('builds nested list parameters in modern mode', () => {
90+
expect.assertions(1)
91+
expect(query({
92+
filter: {
93+
users: [ { id: 1, type: 'users' }, { id: 2, type: 'users' } ]
94+
}
95+
}, null, false)).toEqual('filter%5Busers%5D%5B%5D%5Bid%5D=1&filter%5Busers%5D%5B%5D%5Btype%5D=users&filter%5Busers%5D%5B%5D%5Bid%5D=2&filter%5Busers%5D%5B%5D%5Btype%5D=users')
96+
})
97+
98+
it('parses list-style keys', () => {
99+
expect.assertions(1)
100+
expect(query({
101+
filter: {
102+
'id_in[]': [ 1, 2 ],
103+
'parent_id_in][': [ 3, 4 ]
104+
}
105+
})).toEqual('filter%5Bid_in%5D%5B%5D=1&filter%5Bid_in%5D%5B%5D=2&filter%5Bparent_id_in%5D%5B%5D=3&filter%5Bparent_id_in%5D%5B%5D=4')
106+
})
107+
108+
it('preserves square-brackets in key names in modern mode', () => {
109+
expect.assertions(1)
110+
expect(query({
111+
filter: {
112+
'id_in[]': [ 1, 2 ]
113+
}
114+
}, null, false)).toEqual('filter%5Bid_in%5D%5B%5D%5B%5D=1&filter%5Bid_in%5D%5B%5D%5B%5D=2')
115+
})
79116
})
80117
})

packages/kitsu/src/index.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import pluralise from 'pluralize'
88
* @param {Object} [options] Options
99
* @param {string} [options.baseURL=https://kitsu.io/api/edge] Set the API endpoint
1010
* @param {Object} [options.headers] Additional headers to send with the requests
11+
* @param {'traditional'|'modern'|Function} [options.query=traditional] Query serializer function to use. This will impact the say keys are serialized when passing arrays as query parameters. 'modern' is recommended for new projects.
1112
* @param {boolean} [options.camelCaseTypes=true] If enabled, `type` will be converted to camelCase from kebab-casae or snake_case
1213
* @param {'kebab'|'snake'|'none'} [options.resourceCase=kebab] Case to convert camelCase to. `kebab` - `/library-entries`; `snake` - /library_entries`; `none` - `/libraryEntries`
1314
* @param {boolean} [options.pluralize=true] If enabled, `/user` will become `/users` in the URL request and `type` will be pluralized in POST, PATCH and DELETE requests
@@ -29,6 +30,11 @@ import pluralise from 'pluralize'
2930
*/
3031
export default class Kitsu {
3132
constructor (options = {}) {
33+
const traditional = typeof options.query === 'string' ? options.query === 'traditional' : true
34+
this.query = typeof options.query === 'function'
35+
? options.query
36+
: obj => query(obj, null, traditional)
37+
3238
if (options.camelCaseTypes === false) this.camel = s => s
3339
else this.camel = camel
3440

@@ -229,7 +235,7 @@ export default class Kitsu {
229235
const { data, headers: responseHeaders } = await this.axios.get(url, {
230236
headers,
231237
params,
232-
paramsSerializer: /* istanbul ignore next */ p => query(p),
238+
paramsSerializer: /* istanbul ignore next */ p => this.query(p),
233239
...config.axiosOptions
234240
})
235241

@@ -292,7 +298,7 @@ export default class Kitsu {
292298
{
293299
headers,
294300
params,
295-
paramsSerializer: /* istanbul ignore next */ p => query(p),
301+
paramsSerializer: /* istanbul ignore next */ p => this.query(p),
296302
...config.axiosOptions
297303
}
298304
)
@@ -352,7 +358,7 @@ export default class Kitsu {
352358
{
353359
headers,
354360
params,
355-
paramsSerializer: /* istanbul ignore next */ p => query(p),
361+
paramsSerializer: /* istanbul ignore next */ p => this.query(p),
356362
...config.axiosOptions
357363
}
358364
)
@@ -407,7 +413,7 @@ export default class Kitsu {
407413
}),
408414
headers,
409415
params,
410-
paramsSerializer: /* istanbul ignore next */ p => query(p),
416+
paramsSerializer: /* istanbul ignore next */ p => this.query(p),
411417
...config.axiosOptions
412418
})
413419

@@ -517,7 +523,7 @@ export default class Kitsu {
517523
}),
518524
headers: { ...this.headers, ...headers },
519525
params,
520-
paramsSerializer: /* istanbul ignore next */ p => query(p),
526+
paramsSerializer: /* istanbul ignore next */ p => this.query(p),
521527
...axiosOptions
522528
})
523529

packages/kitsu/src/index.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,28 @@ describe('kitsu', () => {
133133
expect(api.axios.defaults.baseURL).toBe('https://example.api')
134134
})
135135

136+
it('uses the query serializer provided in constructor', () => {
137+
expect.assertions(1)
138+
const stringsOnlyQuery = obj => Object.keys(obj).reduce((acc, key) =>
139+
typeof obj[key] === 'string' ? [ ...acc, `${key}=${obj[key]}` ] : acc, []
140+
).join('&')
141+
142+
const api = new Kitsu({ query: stringsOnlyQuery })
143+
expect(api.query({ a: 1, b: 'str', c: 3, d: 'ing' })).toBe('b=str&d=ing')
144+
})
145+
146+
it('uses the traditional query serializer by default', () => {
147+
expect.assertions(1)
148+
const api = new Kitsu({})
149+
expect(api.query({ id_in: [ 1, 2, 3 ] })).toBe('id_in=1&id_in=2&id_in=3')
150+
})
151+
152+
it('uses the modern query serializer if query option is "modern"', () => {
153+
expect.assertions(1)
154+
const api = new Kitsu({ query: 'modern' })
155+
expect(api.query({ id_in: [ 1, 2, 3 ] })).toBe('id_in%5B%5D=1&id_in%5B%5D=2&id_in%5B%5D=3')
156+
})
157+
136158
it('uses provided axios options', () => {
137159
expect.assertions(1)
138160
const api = new Kitsu({ axiosOptions: { withCredentials: true } })

0 commit comments

Comments
 (0)