Skip to content

Commit e8aacc5

Browse files
committed
feat(kitsu): support arbitrary requests
1 parent e37b7ad commit e8aacc5

2 files changed

Lines changed: 236 additions & 7 deletions

File tree

packages/kitsu/src/index.js

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { camel, deserialise, error, kebab, query, serialise, snake, splitModel }
77
*
88
* @param {Object} options Options
99
* @param {string} options.baseURL Set the API endpoint (default `https://kitsu.io/api/edge`)
10-
* @param {Object} options.headers Additional headers to send with requests
10+
* @param {Object} options.headers Additional headers to send with the requests
1111
* @param {boolean} options.camelCaseTypes If true, the `type` value will be camelCased, e.g `library-entries` and `library_entries` become `libraryEntries` (default `true`)
1212
* @param {string} options.resourceCase `kebab`, `snake` or `none`. If `kebab`, `/libraryEntries` will become `/library-entries`. If `snake`, `/libraryEntries` will become `/library_entries`, If `none`, `/libraryEntries` will be unchanged (default `kebab`)
1313
* @param {boolean} options.pluralize If `true`, `/user` will become `/users` in the URL request and `type` will be pluralized in POST, PATCH and DELETE requests (default `true`)
@@ -90,7 +90,7 @@ export default class Kitsu {
9090
* // Do something before request is sent
9191
* return config
9292
* }, error => {
93-
* // Do something with request error
93+
* // Do something with the request error
9494
* return Promise.reject(error)
9595
* })
9696
* @example <caption>Response Interceptor</caption>
@@ -124,7 +124,7 @@ export default class Kitsu {
124124
* @param {Object} params.filter Filter dataset by attribute values - [JSON:API Filtering](http://jsonapi.org/format/#fetching-filtering)
125125
* @param {string} params.sort Sort dataset by one or more comma separated attributes (prepend `-` for descending order) - [JSON:API Sorting](http://jsonapi.org/format/#fetching-sorting)
126126
* @param {string} params.include Include relationship data - [JSON:API Includes](http://jsonapi.org/format/#fetching-includes)
127-
* @param {Object} headers Additional headers to send with request
127+
* @param {Object} headers Additional headers to send with the request
128128
* @returns {Object} JSON-parsed response
129129
* @example <caption>Getting a resource with JSON:API parameters</caption>
130130
* api.get('users', {
@@ -208,7 +208,7 @@ export default class Kitsu {
208208
* @memberof Kitsu
209209
* @param {string} model Model to update data in
210210
* @param {Object} body Data to send in the request
211-
* @param {Object} headers Additional headers to send with request
211+
* @param {Object} headers Additional headers to send with the request
212212
* @returns {Object} JSON-parsed response
213213
* @example <caption>Update a post</caption>
214214
* api.update('posts', {
@@ -249,7 +249,7 @@ export default class Kitsu {
249249
* @memberof Kitsu
250250
* @param {string} model Model to create a resource under
251251
* @param {Object} body Data to send in the request
252-
* @param {Object} headers Additional headers to send with request
252+
* @param {Object} headers Additional headers to send with the request
253253
* @returns {Object} JSON-parsed response
254254
* @example <caption>Create a post on a user's profile feed</caption>
255255
* api.create('posts', {
@@ -296,7 +296,7 @@ export default class Kitsu {
296296
* @memberof Kitsu
297297
* @param {string} model Model to remove data from
298298
* @param {string|number|Array} id Resource ID to remove. Pass an array of IDs to delete multiple resources (Bulk Extension)
299-
* @param {Object} headers Additional headers to send with request
299+
* @param {Object} headers Additional headers to send with the request
300300
* @returns {Object} JSON-parsed response
301301
* @example <caption>Remove a single resource</caption>
302302
* api.delete('posts', 123)
@@ -343,7 +343,7 @@ export default class Kitsu {
343343
* @param {Object} params JSON-API request queries
344344
* @param {Object} params.fields Return a sparse fieldset with only the included attributes/relationships - [JSON:API Sparse Fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets)
345345
* @param {string} params.include Include relationship data - [JSON:API Includes](http://jsonapi.org/format/#fetching-includes)
346-
* @param {Object} headers Additional headers to send with request
346+
* @param {Object} headers Additional headers to send with the request
347347
* @returns {Object} JSON-parsed response
348348
* @example <caption>Get the authenticated user's resource</caption>
349349
* api.self()
@@ -362,4 +362,83 @@ export default class Kitsu {
362362
throw error(E)
363363
}
364364
}
365+
366+
/**
367+
* Send arbitrary requests
368+
*
369+
* **Note** Planned changes to the `get`, `patch`, `post` and `delete` methods in a future major release may make this method redundent. See https://github.com/wopian/kitsu/issues/415 for details.
370+
*
371+
* @memberof Kitsu
372+
* @param {Object|Array} config.body Data to send in the request
373+
* @param {string} config.method Request method - `GET`, `PATCH`, `POST` or `DELETE` (defaults to `GET`, case-insensitive)
374+
* @param {Object} config.params JSON-API request queries
375+
* @param {Object} config.params.page [JSON:API Pagination](http://jsonapi.org/format/#fetching-pagination)
376+
* @param {number} config.params.page.limit Number of resources to return in request (Max `20` for Kitsu.io except on `libraryEntries` which has a max of `500`)
377+
* @param {number} config.params.page.offset Number of resources to offset the dataset by
378+
* @param {Object} config.params.fields Return a sparse fieldset with only the included attributes/relationships - [JSON:API Sparse Fieldsets](http://jsonapi.org/format/#fetching-sparse-fieldsets)
379+
* @param {Object} config.params.filter Filter dataset by attribute values - [JSON:API Filtering](http://jsonapi.org/format/#fetching-filtering)
380+
* @param {string} config.params.sort Sort dataset by one or more comma separated attributes (prepend `-` for descending order) - [JSON:API Sorting](http://jsonapi.org/format/#fetching-sorting)
381+
* @param {string} config.params.include Include relationship data - [JSON:API Includes](http://jsonapi.org/format/#fetching-includes)
382+
* @param {string} config.type The resource type
383+
* @param {string} config.url The URL path of the resource
384+
* @param {Object} headers Additional headers to send with the request
385+
* @returns {Object} JSON-parsed response
386+
* @example <caption>Raw GET request</caption>
387+
* api.request({
388+
* url: 'anime/1/mappings',
389+
* type: 'mappings',
390+
* params: { filter: { externalSite: 'aozora' } }
391+
* })
392+
* @example <caption>Raw PATCH request</caption>
393+
* api.request({
394+
* method: 'PATCH',
395+
* url: 'anime',
396+
* type: 'anime',
397+
* body: { id: '1', subtype: 'tv' }
398+
* })
399+
* @example <caption>Raw POST request</caption>
400+
* api.request({
401+
* method: 'PATCH',
402+
* url: 'anime',
403+
* type: 'anime',
404+
* body: { subtype: 'tv' }
405+
* })
406+
* @example <caption>Raw DELETE request</caption>
407+
* api.request({
408+
* method: 'DELETE',
409+
* url: 'anime/1',
410+
* type: 'anime',
411+
* body: { id: '1' }
412+
* })
413+
* @example <caption>Bulk Extension support (`PATCH`, `POST` & `DELETE`)</caption>
414+
* api.request({
415+
* method: 'PATCH',
416+
* url: 'anime',
417+
* type: 'anime',
418+
* body: [
419+
* { id: '1', subtype: 'tv' }
420+
* { id: '2', subtype: 'ona' }
421+
* ]
422+
* })
423+
*/
424+
async request ({ body, method, params, type, url }, headers = {}) {
425+
try {
426+
method = method?.toUpperCase() || 'GET'
427+
const { data } = await this.axios.request({
428+
method,
429+
url,
430+
data: [ 'GET', 'DELETE' ].includes(method) ? undefined : serialise(type, body, method, {
431+
camelCaseTypes: this.camel,
432+
pluralTypes: this.plural
433+
}),
434+
params,
435+
paramsSerializer: p => query(p),
436+
headers: Object.assign(this.headers, headers)
437+
})
438+
439+
return deserialise(data)
440+
} catch (E) {
441+
throw error(E)
442+
}
443+
}
365444
}

packages/kitsu/src/request.spec.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import axios from 'axios'
2+
import MockAdapter from 'axios-mock-adapter'
3+
import Kitsu from 'kitsu'
4+
import {
5+
getSingleWithIncludes
6+
} from 'specification'
7+
8+
const mock = new MockAdapter(axios)
9+
10+
const genericRequest = {
11+
data: {
12+
id: '1',
13+
type: 'anime',
14+
subtype: 'tv'
15+
}
16+
}
17+
18+
const genericResponse = {
19+
data: {
20+
id: '1',
21+
type: 'anime',
22+
attributes: { subtype: 'tv' }
23+
}
24+
}
25+
26+
afterEach(() => {
27+
mock.reset()
28+
})
29+
30+
describe('kitsu', () => {
31+
describe('request', () => {
32+
it('sends headers', async done => {
33+
expect.assertions(1)
34+
const api = new Kitsu({ headers: { Authorization: true } })
35+
mock.onGet('/users').reply(config => {
36+
expect(config.headers).toEqual({
37+
Accept: 'application/vnd.api+json',
38+
'Content-Type': 'application/vnd.api+json',
39+
Authorization: true,
40+
extra: true
41+
})
42+
return [ 200, { data: [] } ]
43+
})
44+
api.request({
45+
method: 'GET',
46+
url: 'users',
47+
model: 'users'
48+
}, { extra: true }).catch(err => {
49+
done.fail(err)
50+
})
51+
done()
52+
})
53+
54+
it('sends parameters', async () => {
55+
expect.assertions(1)
56+
const api = new Kitsu()
57+
mock.onGet(`anime/${getSingleWithIncludes.jsonapi.data.id}`, { include: 'author,comments' }).reply(200, getSingleWithIncludes.jsonapi)
58+
const request = await api.request({
59+
method: 'GET',
60+
url: 'anime/1',
61+
params: { include: 'author,comments' }
62+
})
63+
expect(request).toEqual(getSingleWithIncludes.kitsu)
64+
})
65+
66+
it('defaults to a GET request', async () => {
67+
expect.assertions(1)
68+
const api = new Kitsu()
69+
mock.onGet('anime/1').reply(200, genericResponse)
70+
const request = await api.request({
71+
url: 'anime/1'
72+
})
73+
expect(request).toEqual(genericRequest)
74+
})
75+
76+
it('handles method case differences', async () => {
77+
expect.assertions(1)
78+
const api = new Kitsu()
79+
mock.onGet('anime/1').reply(200, genericResponse)
80+
const request = await api.request({
81+
method: 'gEt',
82+
url: 'anime/1'
83+
})
84+
expect(request).toEqual(genericRequest)
85+
})
86+
87+
it('makes PATCH requests', async () => {
88+
expect.assertions(1)
89+
const api = new Kitsu()
90+
mock.onPatch('anime').reply(200, genericResponse)
91+
const request = await api.request({
92+
method: 'patch',
93+
url: 'anime',
94+
type: 'anime',
95+
body: genericResponse.data
96+
})
97+
expect(request).toEqual(genericRequest)
98+
})
99+
100+
it('makes PATCH requests (array)', async () => {
101+
expect.assertions(1)
102+
const api = new Kitsu()
103+
mock.onPatch('anime').reply(200, {
104+
data: [
105+
genericResponse.data,
106+
genericResponse.data
107+
]
108+
})
109+
const request = await api.request({
110+
method: 'patch',
111+
url: 'anime',
112+
type: 'anime',
113+
body: [
114+
genericResponse.data,
115+
genericResponse.data
116+
]
117+
})
118+
expect(request).toEqual({
119+
data: [
120+
genericRequest.data,
121+
genericRequest.data
122+
]
123+
})
124+
})
125+
126+
it('makes POST requests', async () => {
127+
expect.assertions(1)
128+
const api = new Kitsu()
129+
mock.onPost('anime').reply(200, genericResponse)
130+
const request = await api.request({
131+
method: 'post',
132+
url: 'anime',
133+
type: 'anime',
134+
body: genericResponse.data
135+
})
136+
expect(request).toEqual(genericRequest)
137+
})
138+
139+
it('makes DELETE requests', async () => {
140+
expect.assertions(1)
141+
const api = new Kitsu()
142+
mock.onDelete('anime/1').reply(200, genericResponse)
143+
const request = await api.request({
144+
method: 'delete',
145+
url: 'anime/1'
146+
})
147+
expect(request).toEqual(genericRequest)
148+
})
149+
})
150+
})

0 commit comments

Comments
 (0)