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 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
+ }
@@ -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,8 @@
1
+ export default body => JSON.stringify(body, (key, value) => {
2
+ if (value instanceof Error) {
3
+ return value.stack.split(/\r?\n/g)
4
+ } else if (typeof value === 'bigint') {
5
+ return value.toString()
6
+ }
7
+ return value
8
+ })
@@ -0,0 +1 @@
1
+ export default (...fns) => (input) => fns.reduce((chain, f) => f(chain), input)
@@ -0,0 +1,4 @@
1
+ export default object => Object.fromEntries(
2
+ Object.entries(object)
3
+ .filter(entry => entry[1] !== undefined)
4
+ )
@@ -0,0 +1,7 @@
1
+ export default (error, status = 0) => ({
2
+ status,
3
+ error: typeof error === 'string'
4
+ ? { message: error }
5
+ : error,
6
+ headers: new Headers()
7
+ })
@@ -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,4 @@
1
+ export { default as accessToken } from './accessToken.js'
2
+ export { default as encodeBody } from './encodeBody.js'
3
+ export { default as commonReply } from './commonReply.js'
4
+ export { default as jsonReply } from './jsonReply.js'
@@ -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
+ }
@@ -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
+ };