fetchbasequery 0.0.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/package.json +18 -0
- package/src/baseQuery.js +58 -0
- package/src/fetchBaseQuery.js +14 -0
- package/src/helpers/encodeJson.js +8 -0
- package/src/helpers/pipe.js +1 -0
- package/src/helpers/purge.js +4 -0
- package/src/helpers/responseFromError.js +7 -0
- package/src/middlewares/accessToken.js +44 -0
- package/src/middlewares/commonReply.js +14 -0
- package/src/middlewares/encodeBody.js +78 -0
- package/src/middlewares/index.js +4 -0
- package/src/middlewares/jsonReply.js +25 -0
- package/types/index.d.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 h41ku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# fetchBaseQuery
|
|
2
|
+
|
|
3
|
+
Library for easily make HTTP queries without mass of boilerplate code.
|
|
4
|
+
|
|
5
|
+
Usage
|
|
6
|
+
-----
|
|
7
|
+
|
|
8
|
+
Build base query function:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import fetchBaseQuery, { baseQuery, encodeBody, accessToken, jsonReply, commonReply } from 'fetchbasequery'
|
|
12
|
+
|
|
13
|
+
const query = fetchBaseQuery({
|
|
14
|
+
baseUrl: 'https://api.example.org/v1'
|
|
15
|
+
baseQuery,
|
|
16
|
+
logErrors: true,
|
|
17
|
+
middleware: [
|
|
18
|
+
encodeBody(),
|
|
19
|
+
accessToken({
|
|
20
|
+
getAccessToken: () => localStorage.getItem('access_token'),
|
|
21
|
+
setAccessToken: (token) => localStorage.setItem('access_token', token),
|
|
22
|
+
getUpdatedAccessToken: (response) => response.headers.get('X-Access-Token')
|
|
23
|
+
}),
|
|
24
|
+
jsonReply(),
|
|
25
|
+
commonReply()
|
|
26
|
+
]
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Now get easy to make queries:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const response = await query({ url: '/users/me' }) // don't worry about catching errors
|
|
34
|
+
const { status, success, error } = response
|
|
35
|
+
if (success) {
|
|
36
|
+
setUser(response.data)
|
|
37
|
+
} else if (error) {
|
|
38
|
+
setError(response.message)
|
|
39
|
+
} else {
|
|
40
|
+
setError(`Unexpected status code: ${status}`)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
It works fine both on frontend and backend sides. For backend side usage Node.js v18+ is required,
|
|
45
|
+
because the middleware `baseQuery` based on `Fetch API`. But it can be replaced with your own
|
|
46
|
+
implementation based on another API (e.g. based on `axios` etc).
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fetchbasequery",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Library for easily make HTTP queries without mass of boilerplate code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/fetchBaseQuery.js",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
],
|
|
12
|
+
"author": "h41ku <h41ku.c0de@ya.ru>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/baseQuery.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import responseFromError from './helpers/responseFromError.js'
|
|
2
|
+
import purge from './helpers/purge.js'
|
|
3
|
+
|
|
4
|
+
export default async ({ url: endpoint, ...options } = {}) => {
|
|
5
|
+
const {
|
|
6
|
+
baseUrl,
|
|
7
|
+
logErrors,
|
|
8
|
+
...restOptions
|
|
9
|
+
} = {
|
|
10
|
+
logErrors: false,
|
|
11
|
+
baseUrl: globalThis.location
|
|
12
|
+
? globalThis.location.origin
|
|
13
|
+
: undefined,
|
|
14
|
+
...options
|
|
15
|
+
}
|
|
16
|
+
// default options for `fetch()`
|
|
17
|
+
const defaultFetchOptions = {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
cache: 'reload',
|
|
20
|
+
credentials: 'include',
|
|
21
|
+
}
|
|
22
|
+
// extract options which are known by `fetch()`
|
|
23
|
+
const { method, headers, body, mode, credentials, cache, redirect,
|
|
24
|
+
referrer, referrerPolicy, integrity, keepalive, signal } = restOptions
|
|
25
|
+
// combine extracted options and remove undefined properties
|
|
26
|
+
const fetchOptions = purge({ method, headers, body, mode, credentials, cache, redirect,
|
|
27
|
+
referrer, referrerPolicy, integrity, keepalive, signal })
|
|
28
|
+
// do request
|
|
29
|
+
let response
|
|
30
|
+
try {
|
|
31
|
+
let url, origin, prefix = '/'
|
|
32
|
+
if (baseUrl) {
|
|
33
|
+
const url = new URL(baseUrl)
|
|
34
|
+
origin = url.origin
|
|
35
|
+
prefix = url.pathname
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(endpoint)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
url = new URL(
|
|
41
|
+
/\/$/.test(prefix) && /^\//.test(endpoint)
|
|
42
|
+
? prefix + endpoint.substring(1)
|
|
43
|
+
: prefix + endpoint,
|
|
44
|
+
origin
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
response = await fetch(url, {
|
|
48
|
+
...defaultFetchOptions,
|
|
49
|
+
...fetchOptions
|
|
50
|
+
})
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (logErrors) {
|
|
53
|
+
console.error('fetchBaseQuery/query', error)
|
|
54
|
+
}
|
|
55
|
+
return responseFromError(error)
|
|
56
|
+
}
|
|
57
|
+
return response
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// const pipeAsync = (...fns) => (input) => fns.reduce((chain, func) => chain.then(func), Promise.resolve(input))
|
|
2
|
+
import pipe from './helpers/pipe.js'
|
|
3
|
+
import defaultBaseQuery from './baseQuery.js'
|
|
4
|
+
|
|
5
|
+
export default ({ baseQuery, middlewares, ...defaultOptions } = {}) => {
|
|
6
|
+
const pipedQuery = pipe(...(middlewares || []))(baseQuery || defaultBaseQuery)
|
|
7
|
+
return options => pipedQuery({
|
|
8
|
+
...defaultOptions,
|
|
9
|
+
...options
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { defaultBaseQuery as baseQuery }
|
|
14
|
+
export * from './middlewares/index.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default (...fns) => (input) => fns.reduce((chain, f) => f(chain), input)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import responseFromError from '../helpers/responseFromError.js'
|
|
2
|
+
import purge from '../helpers/purge.js'
|
|
3
|
+
|
|
4
|
+
export default (settings = {}) => query => async ({ payload, headers, ...options } = {}) => {
|
|
5
|
+
const {
|
|
6
|
+
getAccessToken,
|
|
7
|
+
setAccessToken,
|
|
8
|
+
isUnauthorized,
|
|
9
|
+
getUpdatedAccessToken
|
|
10
|
+
} = {
|
|
11
|
+
getAccessToken: (payload) => undefined,
|
|
12
|
+
setAccessToken: (token) => {},
|
|
13
|
+
isUnauthorized: (response) => response.status === 401,
|
|
14
|
+
getUpdatedAccessToken: (response) => undefined,
|
|
15
|
+
...settings
|
|
16
|
+
}
|
|
17
|
+
let response
|
|
18
|
+
let status = 0
|
|
19
|
+
try {
|
|
20
|
+
const accessToken = getAccessToken(payload)
|
|
21
|
+
response = await query({
|
|
22
|
+
...options,
|
|
23
|
+
headers: purge({
|
|
24
|
+
...(accessToken ? { Authorization: `bearer ${accessToken}` } : {}),
|
|
25
|
+
...(headers || {})
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
status = response.status
|
|
29
|
+
if (isUnauthorized(response)) {
|
|
30
|
+
setAccessToken(null) // special case to remove access token from storage
|
|
31
|
+
} else {
|
|
32
|
+
const updatedAccessToken = getUpdatedAccessToken(response)
|
|
33
|
+
if (updatedAccessToken) {
|
|
34
|
+
setAccessToken(updatedAccessToken)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (options.logErrors) {
|
|
39
|
+
console.error('fetchBaseQuery/middlewares/accessToken', error)
|
|
40
|
+
}
|
|
41
|
+
return responseFromError(error, status)
|
|
42
|
+
}
|
|
43
|
+
return response
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default () => query => async (options) => {
|
|
2
|
+
const { status, data, error, headers } = await query(options)
|
|
3
|
+
if (error) {
|
|
4
|
+
return { status, error: true, message: error.message, headers }
|
|
5
|
+
} else if (data) {
|
|
6
|
+
if (data.success) {
|
|
7
|
+
return { ...data, status, headers }
|
|
8
|
+
} else if (data.error) {
|
|
9
|
+
return { status, error: true, message: data.message, headers }
|
|
10
|
+
}
|
|
11
|
+
return { status, error: true, message: 'Unexpected format of common response body.', headers }
|
|
12
|
+
}
|
|
13
|
+
return { status, headers }
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import purge from '../helpers/purge.js'
|
|
2
|
+
import encodeJson from '../helpers/encodeJson.js'
|
|
3
|
+
import responseFromError from '../helpers/responseFromError.js'
|
|
4
|
+
|
|
5
|
+
const getEncodedBody = body => (
|
|
6
|
+
typeof blob === 'string'
|
|
7
|
+
|| body instanceof String
|
|
8
|
+
|| body instanceof FormData
|
|
9
|
+
|| body instanceof Blob
|
|
10
|
+
|| body instanceof ArrayBuffer
|
|
11
|
+
|| body instanceof DataView
|
|
12
|
+
|| body instanceof File
|
|
13
|
+
|| body instanceof ReadableStream
|
|
14
|
+
|| body instanceof Int8Array
|
|
15
|
+
|| body instanceof Uint8Array
|
|
16
|
+
|| body instanceof Int16Array
|
|
17
|
+
|| body instanceof Uint16Array
|
|
18
|
+
|| body instanceof Int32Array
|
|
19
|
+
|| body instanceof Uint32Array
|
|
20
|
+
|| body instanceof BigInt64Array
|
|
21
|
+
|| body instanceof BigUint64Array
|
|
22
|
+
? body // raw
|
|
23
|
+
: (
|
|
24
|
+
body instanceof URLSearchParams
|
|
25
|
+
? body.toString()
|
|
26
|
+
: encodeJson(body) // default
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const detectContentType = ({ body }) => {
|
|
31
|
+
if (body instanceof ArrayBuffer
|
|
32
|
+
|| body instanceof DataView
|
|
33
|
+
|| body instanceof ReadableStream
|
|
34
|
+
|| body instanceof Int8Array
|
|
35
|
+
|| body instanceof Uint8Array
|
|
36
|
+
|| body instanceof Int16Array
|
|
37
|
+
|| body instanceof Uint16Array
|
|
38
|
+
|| body instanceof Int32Array
|
|
39
|
+
|| body instanceof Uint32Array
|
|
40
|
+
|| body instanceof BigInt64Array
|
|
41
|
+
|| body instanceof BigUint64Array
|
|
42
|
+
) return 'application/octet-stream'
|
|
43
|
+
if (body instanceof URLSearchParams) return 'application/x-www-form-urlencoded'
|
|
44
|
+
if (body instanceof FormData) return 'multipart/form-data'
|
|
45
|
+
if (body instanceof Object) return 'application/json; charset=utf-8'
|
|
46
|
+
// return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default (settings = {}) => (query) => async ({ body, headers, ...options } = {}) => {
|
|
50
|
+
const {
|
|
51
|
+
contentType,
|
|
52
|
+
encodeBody
|
|
53
|
+
} = {
|
|
54
|
+
contentType: detectContentType,
|
|
55
|
+
encodeBody: getEncodedBody,
|
|
56
|
+
...settings
|
|
57
|
+
}
|
|
58
|
+
let response
|
|
59
|
+
let status = 0
|
|
60
|
+
try {
|
|
61
|
+
response = await query({
|
|
62
|
+
...options,
|
|
63
|
+
headers: purge({
|
|
64
|
+
'Content-Type': contentType instanceof Function
|
|
65
|
+
? contentType({ body })
|
|
66
|
+
: contentType,
|
|
67
|
+
...(headers || {})
|
|
68
|
+
}),
|
|
69
|
+
body: encodeBody(body)
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (options.logErrors) {
|
|
73
|
+
console.error('fetchBaseQuery/middlewares/accessToken', error)
|
|
74
|
+
}
|
|
75
|
+
return responseFromError(error, status)
|
|
76
|
+
}
|
|
77
|
+
return response
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default (settings = {}) => query => async (options = {}) => {
|
|
2
|
+
const {
|
|
3
|
+
isJsonBody
|
|
4
|
+
} = {
|
|
5
|
+
isJsonBody: (response) => /^application\/json/i.test(response.headers.get('Content-Type')),
|
|
6
|
+
...settings
|
|
7
|
+
}
|
|
8
|
+
const response = await query(options)
|
|
9
|
+
const { status, error, headers } = response
|
|
10
|
+
if (error) {
|
|
11
|
+
return { status, error, headers }
|
|
12
|
+
}
|
|
13
|
+
let data
|
|
14
|
+
try {
|
|
15
|
+
if (isJsonBody(response) && response.json) {
|
|
16
|
+
data = await response.json()
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (options.logErrors) {
|
|
20
|
+
console.error('fetchBaseQuery/middlewares/jsonReply', error)
|
|
21
|
+
}
|
|
22
|
+
return { status, error, headers }
|
|
23
|
+
}
|
|
24
|
+
return { status, data, headers }
|
|
25
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type fetchInit = {
|
|
2
|
+
logErrors: boolean | undefined,
|
|
3
|
+
method: string | undefined,
|
|
4
|
+
headers: object | undefined,
|
|
5
|
+
body: string
|
|
6
|
+
| object
|
|
7
|
+
| FormData
|
|
8
|
+
| Blob
|
|
9
|
+
| ArrayBuffer
|
|
10
|
+
| DataView
|
|
11
|
+
| File
|
|
12
|
+
| ReadableStream
|
|
13
|
+
| Int8Array
|
|
14
|
+
| Uint8Array
|
|
15
|
+
| Int16Array
|
|
16
|
+
| Uint16Array
|
|
17
|
+
| Int32Array
|
|
18
|
+
| Uint32Array
|
|
19
|
+
| BigInt64Array
|
|
20
|
+
| BigUint64Array
|
|
21
|
+
| URLSearchParams
|
|
22
|
+
| undefined,
|
|
23
|
+
mode: string | undefined,
|
|
24
|
+
credentials: string | undefined,
|
|
25
|
+
cache: string | undefined,
|
|
26
|
+
redirect: string | undefined,
|
|
27
|
+
referrer: string | undefined,
|
|
28
|
+
referrerPolicy: string | undefined,
|
|
29
|
+
integrity: string | undefined,
|
|
30
|
+
keepalive: boolean | undefined,
|
|
31
|
+
signal: AbortSignal | undefined
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type queryFn = (options: fetchInit | any | undefined) => Promise<any>;
|
|
35
|
+
export type Middleware = (settings: any) => (query: queryFn) => queryFn;
|
|
36
|
+
|
|
37
|
+
export type fetchBaseQueryInit = {
|
|
38
|
+
baseUrl: URL | string | undefined,
|
|
39
|
+
baseQuery: queryFn,
|
|
40
|
+
middlewares: Array<Middleware>
|
|
41
|
+
};
|