fetchja 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +217 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -7
- package/package.json +30 -10
- package/readme.md +306 -18
- package/src/index.js +0 -243
- package/src/utils/camel-case.js +0 -12
- package/src/utils/deattribute.js +0 -28
- package/src/utils/deserialize.js +0 -93
- package/src/utils/error-parser.js +0 -17
- package/src/utils/kebab-case.js +0 -12
- package/src/utils/query-formatter.js +0 -34
- package/src/utils/serialize.js +0 -98
- package/src/utils/snake-case.js +0 -12
- package/src/utils/split-model.js +0 -20
- package/tests/utils/camel-case.test.js +0 -30
- package/tests/utils/kebab-case.test.js +0 -30
- package/tests/utils/snake-case.test.js +0 -30
package/readme.md
CHANGED
|
@@ -1,45 +1,333 @@
|
|
|
1
1
|
# Fetchja
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A tiny, modern, zero-dependency JSON:API client built on the native Fetch API.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Fetchja helps you talk to a [JSON:API](https://jsonapi.org) server. It uses the browser's own `fetch`, so it stays small and fast. You write plain objects, and Fetchja turns them into JSON:API requests. The server answers, and Fetchja turns the answer back into plain objects — with relationships already filled in. It was inspired by [Kitsu](https://github.com/wopian/kitsu), but it has **no dependencies**.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import Fetchja from 'fetchja'
|
|
9
|
+
|
|
10
|
+
const api = new Fetchja({
|
|
11
|
+
baseURL: 'https://api.example.com'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const { data } = await api.get('articles')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
6
18
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
19
|
+
- [Why Fetchja?](#why-fetchja)
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Quick start](#quick-start)
|
|
22
|
+
- [Options](#options)
|
|
23
|
+
- [Methods](#methods)
|
|
24
|
+
- [Relationships and included data](#relationships-and-included-data)
|
|
25
|
+
- [Query parameters](#query-parameters)
|
|
26
|
+
- [Make your own query formatter](#make-your-own-query-formatter)
|
|
27
|
+
- [Custom fetch](#custom-fetch)
|
|
28
|
+
- [Errors](#errors)
|
|
29
|
+
- [Retry on error](#retry-on-error)
|
|
30
|
+
- [Use with TanStack Query](#use-with-tanstack-query)
|
|
31
|
+
- [TypeScript](#typescript)
|
|
32
|
+
- [Plurals](#plurals)
|
|
33
|
+
|
|
34
|
+
## Why Fetchja?
|
|
10
35
|
|
|
11
|
-
|
|
36
|
+
- ⚡️ **No dependencies.** It only uses the built-in `fetch`. Nothing extra to download. About 5 KB when minified.
|
|
37
|
+
- 🧩 **Typed.** It is written in TypeScript and ships its own types. You get autocomplete out of the box.
|
|
38
|
+
- 🔄 **Less boilerplate.** You send and read plain objects. Fetchja does the JSON:API parts for you.
|
|
39
|
+
- 🪶 **Modern and small.** ESM only, `async`/`await`, no base class to extend.
|
|
40
|
+
- 🛡️ **Safe.** It guards against prototype pollution when it reads a response.
|
|
41
|
+
- 🎛️ **Flexible.** Set your own headers, query format, name case, plurals, `fetch`, and error handling.
|
|
12
42
|
|
|
13
|
-
|
|
43
|
+
## Install
|
|
14
44
|
|
|
15
45
|
```bash
|
|
16
|
-
|
|
46
|
+
npm install fetchja
|
|
17
47
|
```
|
|
18
48
|
|
|
19
|
-
|
|
49
|
+
You don't need any other package. Fetchja needs a place where `fetch` exists: Node 18+, Deno, Bun, or any modern browser.
|
|
20
50
|
|
|
21
|
-
|
|
51
|
+
## Quick start
|
|
22
52
|
|
|
23
|
-
```
|
|
53
|
+
```js
|
|
24
54
|
import Fetchja from 'fetchja'
|
|
25
55
|
|
|
26
56
|
const api = new Fetchja({
|
|
27
57
|
baseURL: 'https://api.example.com'
|
|
28
|
-
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// GET /articles
|
|
61
|
+
const { data, meta } = await api.get('articles')
|
|
62
|
+
|
|
63
|
+
// GET /articles/1
|
|
64
|
+
const { data: article } = await api.get('articles/1')
|
|
65
|
+
|
|
66
|
+
console.log(article)
|
|
67
|
+
// { type: 'articles', id: '1', title: 'Hello world', ... }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
You set everything when you create the client:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const api = new Fetchja({ /* options */ })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Option | Type | Default | What it does |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `baseURL` | `string` | — | The start of every URL. You need it (or pass one per request). |
|
|
81
|
+
| `headers` | `Record<string, string>` | JSON:API headers | Headers added to every request. The JSON:API `Accept` and `Content-Type` are set for you. |
|
|
82
|
+
| `fetch` | `typeof fetch` | global `fetch` | Your own fetch function (for example, one that adds a token). |
|
|
83
|
+
| `queryFormatter` | `(params) => string \| URLSearchParams` | built-in | Turns the `params` object into a query string. |
|
|
84
|
+
| `resourceCase` | `'camel' \| 'kebab' \| 'snake' \| 'none'` | `'none'` | How the resource name in the URL is written. |
|
|
85
|
+
| `typeCase` | `'camel' \| 'kebab' \| 'snake' \| 'none'` | `'camel'` | How `type` names are written when you send data. |
|
|
86
|
+
| `pluralize` | `boolean \| ((word) => string)` | `true` (built-in) | Make resource names plural. Use `false` to turn it off, or pass your own function. |
|
|
87
|
+
| `onResponseError` | `(response) => Response \| void` | — | Runs when a request fails, before Fetchja throws. You can retry here. |
|
|
88
|
+
|
|
89
|
+
## Methods
|
|
90
|
+
|
|
91
|
+
| Method | Alias | How to call it | HTTP |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| `get` | `fetch` | `get(model, options?)` | `GET` |
|
|
94
|
+
| `post` | `create` | `post(model, body, options?)` | `POST` |
|
|
95
|
+
| `patch` | `update` | `patch(model, body, options?)` | `PATCH` |
|
|
96
|
+
| `delete` | `remove` | `delete(model, id, options?)` | `DELETE` |
|
|
97
|
+
|
|
98
|
+
Pick the name you like. `api.get` and `api.fetch` do the same thing. So do `create`/`post`, `update`/`patch`, and `remove`/`delete`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// Read a list
|
|
102
|
+
const { data } = await api.get('articles')
|
|
103
|
+
|
|
104
|
+
// Read one
|
|
105
|
+
const { data } = await api.get('articles/1')
|
|
106
|
+
|
|
107
|
+
// Create
|
|
108
|
+
await api.create('article', { title: 'Hello world' })
|
|
109
|
+
|
|
110
|
+
// Update (put the id in the body)
|
|
111
|
+
await api.update('article', { id: '1', title: 'New title' })
|
|
112
|
+
|
|
113
|
+
// Delete
|
|
114
|
+
await api.remove('article', '1') // DELETE /articles/1 (no body)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every call gives you back the data plus a few extra fields:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const response = await api.get('articles/1')
|
|
121
|
+
|
|
122
|
+
response.data // your data (an object, or an array for a list)
|
|
123
|
+
response.meta // the JSON:API `meta`, if the server sent it
|
|
124
|
+
response.status // 200
|
|
125
|
+
response.statusText // 'OK'
|
|
126
|
+
response.headers // the response headers, as a plain object
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Relationships and included data
|
|
130
|
+
|
|
131
|
+
To send a relationship, put an object (or a list of objects) with a `type` and an `id` inside your data. Fetchja moves it to the right place and adds the full resource to `included` for you:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
await api.create('article', {
|
|
135
|
+
title: 'Hello world',
|
|
136
|
+
author: { type: 'people', id: '9' },
|
|
137
|
+
tags: [
|
|
138
|
+
{ type: 'tags', id: '1' },
|
|
139
|
+
{ type: 'tags', id: '2' }
|
|
140
|
+
]
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When you read data back, Fetchja takes the resources from `included` and puts them right inside your data. So you can read a relationship like a normal nested object:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const { data } = await api.get('articles/1', {
|
|
148
|
+
params: { include: 'author' }
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
console.log(data.author.name) // comes from `included`
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Query parameters
|
|
155
|
+
|
|
156
|
+
Pass a `params` object. Fetchja turns nested objects and arrays into JSON:API-style query strings:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
const { data, meta } = await api.get('articles', {
|
|
160
|
+
params: {
|
|
161
|
+
include: ['author', 'comments'],
|
|
162
|
+
fields: { articles: 'title,body' },
|
|
163
|
+
filter: { published: true },
|
|
164
|
+
sort: '-createdAt',
|
|
165
|
+
page: { number: 1, size: 10 }
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This becomes:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
/articles?include[]=author&include[]=comments&fields[articles]=title,body&filter[published]=true&sort=-createdAt&page[number]=1&page[size]=10
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Make your own query formatter
|
|
177
|
+
|
|
178
|
+
Some servers want a different format. For example, they may want `include=author,comments` (one key, values joined by commas) instead of `include[]=...`. You can pass your own function:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import Fetchja from 'fetchja'
|
|
182
|
+
|
|
183
|
+
function commaQueryFormatter (params) {
|
|
184
|
+
const search = new URLSearchParams()
|
|
185
|
+
|
|
186
|
+
for (const key in params) {
|
|
187
|
+
const value = params[key]
|
|
188
|
+
|
|
189
|
+
search.append(
|
|
190
|
+
key,
|
|
191
|
+
Array.isArray(value) ? value.join(',') : String(value)
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return search
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const api = new Fetchja({
|
|
199
|
+
baseURL: 'https://api.example.com',
|
|
200
|
+
queryFormatter: commaQueryFormatter
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await api.get('articles', { params: { include: ['author', 'comments'] } })
|
|
204
|
+
// -> /articles?include=author,comments
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Your function takes the `params` object and returns a string or a `URLSearchParams`.
|
|
208
|
+
|
|
209
|
+
## Custom fetch
|
|
210
|
+
|
|
211
|
+
You can swap in your own fetch function. This is handy for tokens, timeouts, or logging:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const api = new Fetchja({
|
|
215
|
+
baseURL: 'https://api.example.com',
|
|
216
|
+
fetch: (url, init) => myCustomFetch(url, init)
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Errors
|
|
221
|
+
|
|
222
|
+
When a request fails, Fetchja throws a `FetchjaError`. It holds the HTTP status and the JSON:API `errors` list:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
import Fetchja, { FetchjaError } from 'fetchja'
|
|
29
226
|
|
|
30
227
|
try {
|
|
31
|
-
|
|
32
|
-
console.log(response)
|
|
228
|
+
await api.get('articles/999')
|
|
33
229
|
} catch (error) {
|
|
34
|
-
|
|
230
|
+
if (error instanceof FetchjaError) {
|
|
231
|
+
console.log(error.status) // 404
|
|
232
|
+
console.log(error.statusText) // 'Not Found'
|
|
233
|
+
console.log(error.errors) // [{ status: '404', detail: 'Not found' }]
|
|
234
|
+
console.log(error.response) // the raw Response
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Retry on error
|
|
240
|
+
|
|
241
|
+
`onResponseError` runs when the server sends a failing response, before Fetchja throws. The response also gets a `replayRequest()` method. It sends the same request again. This is great for refreshing a token and trying once more:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const api = new Fetchja({
|
|
245
|
+
baseURL: 'https://api.example.com',
|
|
246
|
+
headers: { Authorization: `Bearer ${getToken()}` },
|
|
247
|
+
onResponseError: async (response) => {
|
|
248
|
+
if (response.status === 401) {
|
|
249
|
+
api.headers.Authorization = `Bearer ${await refreshToken()}`
|
|
250
|
+
|
|
251
|
+
return response.replayRequest() // try again with the new token
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Return a `Response` (like the one from `replayRequest()`) to keep going. Return nothing, and Fetchja throws the `FetchjaError`.
|
|
258
|
+
|
|
259
|
+
## Use with TanStack Query
|
|
260
|
+
|
|
261
|
+
Fetchja works well with [TanStack Query](https://tanstack.com/query). Because Fetchja throws on a failing request, TanStack Query sees the error and handles it for you.
|
|
262
|
+
|
|
263
|
+
Reading data:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
import { useQuery } from '@tanstack/react-query'
|
|
267
|
+
import Fetchja from 'fetchja'
|
|
268
|
+
|
|
269
|
+
const api = new Fetchja({ baseURL: 'https://api.example.com' })
|
|
270
|
+
|
|
271
|
+
function useArticles () {
|
|
272
|
+
return useQuery({
|
|
273
|
+
queryKey: ['articles'],
|
|
274
|
+
queryFn: () => api.get('articles')
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Creating data:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
283
|
+
|
|
284
|
+
function useCreateArticle () {
|
|
285
|
+
const queryClient = useQueryClient()
|
|
286
|
+
|
|
287
|
+
return useMutation({
|
|
288
|
+
mutationFn: (article) => api.create('article', article),
|
|
289
|
+
onSuccess: () => {
|
|
290
|
+
queryClient.invalidateQueries({ queryKey: ['articles'] })
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## TypeScript
|
|
297
|
+
|
|
298
|
+
Fetchja is written in TypeScript and brings its own types. You don't need an extra `@types` package.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import Fetchja, { type FetchjaOptions, FetchjaError } from 'fetchja'
|
|
302
|
+
|
|
303
|
+
const options: FetchjaOptions = {
|
|
304
|
+
baseURL: 'https://api.example.com',
|
|
305
|
+
resourceCase: 'kebab'
|
|
35
306
|
}
|
|
307
|
+
|
|
308
|
+
const api = new Fetchja(options)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Plurals
|
|
312
|
+
|
|
313
|
+
By default, Fetchja makes resource names plural with a small built-in helper. It knows the common English rules (and `status` becomes `statuses`, not `statu`). It is also safe to run twice: `articles` stays `articles`.
|
|
314
|
+
|
|
315
|
+
For tricky words (like `person` → `people`), pass a bigger library such as [`pluralize`](https://www.npmjs.com/package/pluralize):
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
import pluralize from 'pluralize'
|
|
319
|
+
|
|
320
|
+
const api = new Fetchja({
|
|
321
|
+
baseURL: 'https://api.example.com',
|
|
322
|
+
pluralize
|
|
323
|
+
})
|
|
36
324
|
```
|
|
37
325
|
|
|
38
|
-
|
|
326
|
+
Or turn it off and use the exact names you write:
|
|
39
327
|
|
|
40
|
-
```
|
|
328
|
+
```js
|
|
41
329
|
const api = new Fetchja({
|
|
42
330
|
baseURL: 'https://api.example.com',
|
|
43
|
-
|
|
331
|
+
pluralize: false
|
|
44
332
|
})
|
|
45
333
|
```
|
package/src/index.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import pluralize from 'pluralize'
|
|
2
|
-
|
|
3
|
-
import { deserialize } from './utils/deserialize.js'
|
|
4
|
-
import { serialize } from './utils/serialize.js'
|
|
5
|
-
|
|
6
|
-
import { errorParser } from './utils/error-parser.js'
|
|
7
|
-
import { queryFormatter } from './utils/query-formatter.js'
|
|
8
|
-
import { splitModel } from './utils/split-model.js'
|
|
9
|
-
|
|
10
|
-
import { camelCase } from './utils/camel-case.js'
|
|
11
|
-
import { kebabCase } from './utils/kebab-case.js'
|
|
12
|
-
import { snakeCase } from './utils/snake-case.js'
|
|
13
|
-
|
|
14
|
-
const jsonType = 'application/vnd.api+json'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Options for Fetchja.
|
|
18
|
-
*
|
|
19
|
-
* @typedef {Object} FetchjaOptions
|
|
20
|
-
* @property {string} baseURL The base URL for all requests.
|
|
21
|
-
* @property {Function} fetchFunction A custom fetch function to use in request.
|
|
22
|
-
* @property {Object} headers The headers to include in all requests.
|
|
23
|
-
* @property {Function} queryFormatter A function to format query parameters.
|
|
24
|
-
* @property {string} resourceCase The case to use for resource names.
|
|
25
|
-
* @property {boolean} pluralize Pluralize resource names.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Fetchja is a simple wrapper around the Fetch API.
|
|
30
|
-
*
|
|
31
|
-
* @class Fetchja
|
|
32
|
-
* @param {FetchjaOptions} [options] Options for Fetchja.
|
|
33
|
-
*/
|
|
34
|
-
export default class Fetchja {
|
|
35
|
-
constructor (options = {
|
|
36
|
-
headers: {}
|
|
37
|
-
}) {
|
|
38
|
-
this.baseURL = options.baseURL
|
|
39
|
-
|
|
40
|
-
// Headers
|
|
41
|
-
this.headers = {
|
|
42
|
-
Accept: jsonType,
|
|
43
|
-
'Content-Type': jsonType,
|
|
44
|
-
...options.headers
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Fetch Function
|
|
48
|
-
this.fetchFunction = options.fetchFunction
|
|
49
|
-
|
|
50
|
-
// Query
|
|
51
|
-
this.queryFormatter = typeof options.queryFormatter === 'function'
|
|
52
|
-
? options.queryFormatter
|
|
53
|
-
: object => queryFormatter(object)
|
|
54
|
-
|
|
55
|
-
// Camel Case Types
|
|
56
|
-
this.camelCaseTypes = options.camelCaseTypes === false
|
|
57
|
-
? string => string
|
|
58
|
-
: camelCase
|
|
59
|
-
|
|
60
|
-
// Resource Case
|
|
61
|
-
const cases = {
|
|
62
|
-
camel: camelCase,
|
|
63
|
-
kebab: kebabCase,
|
|
64
|
-
snake: snakeCase,
|
|
65
|
-
|
|
66
|
-
default: string => string
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
this.resourceCase = cases[options.resourceCase] || cases.default
|
|
70
|
-
|
|
71
|
-
// Pluralise
|
|
72
|
-
this.pluralize = options.pluralize === false
|
|
73
|
-
? string => string
|
|
74
|
-
: pluralize
|
|
75
|
-
|
|
76
|
-
// Interceptors
|
|
77
|
-
this.onResponseError = error => error
|
|
78
|
-
|
|
79
|
-
// Alias
|
|
80
|
-
this.fetch = this.get
|
|
81
|
-
this.update = this.patch
|
|
82
|
-
this.create = this.post
|
|
83
|
-
this.remove = this.delete
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
#splitModel (model) {
|
|
87
|
-
return splitModel(model, {
|
|
88
|
-
resourceCase: this.resourceCase,
|
|
89
|
-
pluralize: this.pluralize
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async request (options = {
|
|
94
|
-
method: 'GET',
|
|
95
|
-
headers: {}
|
|
96
|
-
}) {
|
|
97
|
-
const baseURL = this.baseURL || options.baseURL
|
|
98
|
-
|
|
99
|
-
const url = new URL(
|
|
100
|
-
options.url.startsWith('/') ? options.url.slice(1) : options.url,
|
|
101
|
-
baseURL.endsWith('/') ? baseURL : baseURL + '/'
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
// Params
|
|
105
|
-
if (options.params) {
|
|
106
|
-
url.search = this.queryFormatter(options.params)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Body
|
|
110
|
-
if (options.body) {
|
|
111
|
-
options.body = serialize(options.type, options.body, {
|
|
112
|
-
camelCaseTypes: this.camelCaseTypes,
|
|
113
|
-
pluralTypes: this.pluralize
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Request
|
|
118
|
-
const makeRequest = () => {
|
|
119
|
-
// Headers
|
|
120
|
-
const headers = new Headers({
|
|
121
|
-
...this.headers,
|
|
122
|
-
...options.headers
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// Fetch
|
|
126
|
-
const fetchOptions = {
|
|
127
|
-
method: options.method,
|
|
128
|
-
body: options.body,
|
|
129
|
-
headers
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (typeof this.fetchFunction === 'function') {
|
|
133
|
-
return this.fetchFunction(url, fetchOptions)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return fetch(url, fetchOptions)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
let response = await makeRequest()
|
|
141
|
-
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
response.replayRequest = makeRequest
|
|
144
|
-
|
|
145
|
-
const replayedResponse = await this.onResponseError(response)
|
|
146
|
-
|
|
147
|
-
if (replayedResponse instanceof Response) {
|
|
148
|
-
response = replayedResponse
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!response.ok) {
|
|
152
|
-
throw new Error(response.statusText)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Response Headers
|
|
157
|
-
const responseHeaders = {}
|
|
158
|
-
|
|
159
|
-
for (const [key, value] of response.headers.entries()) {
|
|
160
|
-
responseHeaders[key] = value
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const contentType = responseHeaders['content-type']
|
|
164
|
-
|
|
165
|
-
// Response Data
|
|
166
|
-
const data = contentType && contentType.includes(jsonType)
|
|
167
|
-
? await response.json()
|
|
168
|
-
: {}
|
|
169
|
-
|
|
170
|
-
// Return
|
|
171
|
-
return {
|
|
172
|
-
...(data.errors ? data : deserialize(data)),
|
|
173
|
-
|
|
174
|
-
status: response.status,
|
|
175
|
-
statusText: response.statusText,
|
|
176
|
-
headers: responseHeaders
|
|
177
|
-
}
|
|
178
|
-
} catch (error) {
|
|
179
|
-
throw error
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
get (model, options = { method: 'GET' }) {
|
|
184
|
-
try {
|
|
185
|
-
options.url = model.split('/')
|
|
186
|
-
.map(part => this.resourceCase(part))
|
|
187
|
-
.filter(Boolean)
|
|
188
|
-
.join('/')
|
|
189
|
-
|
|
190
|
-
return this.request(options)
|
|
191
|
-
} catch (error) {
|
|
192
|
-
throw errorParser(error)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
patch (model, body, options = { method: 'PATCH' }) {
|
|
197
|
-
try {
|
|
198
|
-
const [type, url] = this.#splitModel(model)
|
|
199
|
-
|
|
200
|
-
return this.request({
|
|
201
|
-
url: body?.id ? `${url}/${body.id}` : url,
|
|
202
|
-
body,
|
|
203
|
-
type,
|
|
204
|
-
|
|
205
|
-
...options
|
|
206
|
-
})
|
|
207
|
-
} catch (error) {
|
|
208
|
-
throw errorParser(error)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
post (model, body, options = { method: 'POST' }) {
|
|
213
|
-
try {
|
|
214
|
-
const [type, url] = this.#splitModel(model)
|
|
215
|
-
|
|
216
|
-
return this.request({
|
|
217
|
-
url,
|
|
218
|
-
body,
|
|
219
|
-
type,
|
|
220
|
-
|
|
221
|
-
...options
|
|
222
|
-
})
|
|
223
|
-
} catch (error) {
|
|
224
|
-
throw errorParser(error)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
delete (model, id, options = { method: 'DELETE' }) {
|
|
229
|
-
try {
|
|
230
|
-
const [type, url] = this.#splitModel(model)
|
|
231
|
-
|
|
232
|
-
return this.request({
|
|
233
|
-
url: `${url}/${id}`,
|
|
234
|
-
body: { id },
|
|
235
|
-
type,
|
|
236
|
-
|
|
237
|
-
...options
|
|
238
|
-
})
|
|
239
|
-
} catch (error) {
|
|
240
|
-
throw errorParser(error)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/utils/camel-case.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert a string from snake_case and kebab-case to camelCase.
|
|
3
|
-
*
|
|
4
|
-
* @param {string} input The string to convert.
|
|
5
|
-
* @returns {string} The converted string.
|
|
6
|
-
*/
|
|
7
|
-
export function camelCase (input) {
|
|
8
|
-
return input
|
|
9
|
-
.toLowerCase()
|
|
10
|
-
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
11
|
-
.replace(/^(.)/, (char) => char.toLowerCase())
|
|
12
|
-
}
|
package/src/utils/deattribute.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deattribute JSON:API data.
|
|
3
|
-
*
|
|
4
|
-
* @param {Object|Object[]} data The JSON:API data to deattribute.
|
|
5
|
-
* @returns {Object} The deattributed data.
|
|
6
|
-
*/
|
|
7
|
-
export function deattribute (data) {
|
|
8
|
-
if (Array.isArray(data)) {
|
|
9
|
-
return data.map(deattribute)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const output = {
|
|
13
|
-
type: data.type,
|
|
14
|
-
id: data.id
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
for (const key in data.attributes) {
|
|
18
|
-
output[key] = data.attributes[key]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
for (const key in data.relationships) {
|
|
22
|
-
if (data.relationships[key].data) {
|
|
23
|
-
output[key] = data.relationships[key].data
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return output
|
|
28
|
-
}
|