make-service 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 +227 -0
- package/esm/_dnt.shims.js +70 -0
- package/esm/constants.js +10 -0
- package/esm/index.js +1 -0
- package/esm/internals.js +29 -0
- package/esm/make-service.js +126 -0
- package/esm/package.json +3 -0
- package/esm/types.js +1 -0
- package/package.json +33 -0
- package/script/_dnt.shims.js +80 -0
- package/script/constants.js +13 -0
- package/script/index.js +10 -0
- package/script/internals.js +34 -0
- package/script/make-service.js +157 -0
- package/script/package.json +3 -0
- package/script/types.js +2 -0
- package/types/_dnt.shims.d.ts +13 -0
- package/types/constants.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/internals.d.ts +15 -0
- package/types/make-service.d.ts +77 -0
- package/types/types.d.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Gustavo Guichard
|
|
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,227 @@
|
|
|
1
|
+
# make-service
|
|
2
|
+
|
|
3
|
+
A type-safe thin wrapper around the `fetch` API to better interact with external APIs.
|
|
4
|
+
|
|
5
|
+
It adds a set of little features and allows you to parse responses with [zod](https://github.com/colinhacks/zod).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
- 🤩 Type-safe return of `response.json()` and `response.text()`. Defaults to `unknown` instead of `any`.
|
|
9
|
+
- 🚦 Easily setup an API with a `baseURL` and common `headers` for every request.
|
|
10
|
+
- 🏗️ Compose URL from the base by just calling the endpoints and an object-like `query`.
|
|
11
|
+
- 🧙♀️ Automatically stringifies the `body` of a request so you can give it a JSON-like structure.
|
|
12
|
+
- 🐛 Accepts a `trace` function for debugging.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const api = makeService("https://example.com/api", {
|
|
18
|
+
Authorization: "Bearer 123",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const response = await api.get("/users")
|
|
22
|
+
const users = await response.json(usersSchema);
|
|
23
|
+
// ^? User[]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install make-service
|
|
30
|
+
```
|
|
31
|
+
Or you can use it with Deno:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { makeService } from "https://deno.land/x/make_service/mod.ts";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
# Public API
|
|
38
|
+
|
|
39
|
+
This library exports the `makeService` function and all primitives used to build it. You can use the primitives as you wish but the `makeService` will have all the features combined.
|
|
40
|
+
|
|
41
|
+
## addQueryToInput
|
|
42
|
+
|
|
43
|
+
Adds an object of query parameters to a string or URL.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { addQueryToInput } from 'make-service'
|
|
47
|
+
|
|
48
|
+
const input = addQueryToInput("https://example.com", { page: "1" })
|
|
49
|
+
// input = "https://example.com?page=1"
|
|
50
|
+
|
|
51
|
+
const input = addQueryToInput("https://example.com?page=1", { admin: "true" })
|
|
52
|
+
// input = "https://example.com?page=1&admin=true"
|
|
53
|
+
|
|
54
|
+
const input = addQueryToInput(new URL("https://example.com"), { page: "1" })
|
|
55
|
+
// input.toString() = "https://example.com?page=1"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## makeGetApiUrl
|
|
59
|
+
|
|
60
|
+
Creates a function that will add an endpoint and a query to the base URL.
|
|
61
|
+
It uses the `addQueryToInput` function internally.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { makeGetApiUrl } from 'make-service'
|
|
65
|
+
|
|
66
|
+
const getApiUrl = makeGetApiUrl("https://example.com/api")
|
|
67
|
+
|
|
68
|
+
const url = getApiUrl("/users", { page: "1" })
|
|
69
|
+
// url = "https://example.com/api/users?page=1"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## ensureStringBody
|
|
73
|
+
|
|
74
|
+
Ensures that the body is a string. If it's not, it will be stringified.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { ensureStringBody } from 'make-service'
|
|
78
|
+
|
|
79
|
+
const body1 = ensureStringBody({ foo: "bar" })
|
|
80
|
+
// body1 = '{"foo":"bar"}'
|
|
81
|
+
await fetch("https://example.com/api/users", {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: body1
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const body2 = ensureStringBody('{"foo":"bar"}')
|
|
87
|
+
// body2 = '{"foo":"bar"}'
|
|
88
|
+
await fetch("https://example.com/api/users", {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: body2
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## typedResponse
|
|
95
|
+
|
|
96
|
+
A type-safe wrapper around the `Response` object. It adds a `json` and `text` method that will parse the response with a given zod schema. If you don't provide a schema, it will return `unknown` instead of `any`, then you can also give it a generic to type cast the result.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { typedResponse } from 'make-service'
|
|
100
|
+
|
|
101
|
+
// With JSON
|
|
102
|
+
const response = new Response(JSON.stringify({ foo: "bar" }))
|
|
103
|
+
const json = await typedResponse(response).json()
|
|
104
|
+
// ^? unknown
|
|
105
|
+
const json = await typedResponse(response).json<{ foo: string }>()
|
|
106
|
+
// ^? { foo: string }
|
|
107
|
+
const json = await typedResponse(response).json(z.object({ foo: z.string() }))
|
|
108
|
+
// ^? { foo: string }
|
|
109
|
+
|
|
110
|
+
// With text
|
|
111
|
+
const response = new Response("foo")
|
|
112
|
+
const text = await typedResponse(response).text()
|
|
113
|
+
// ^? string
|
|
114
|
+
const text = await typedResponse(response).text<`foo${string}`>()
|
|
115
|
+
// ^? `foo${string}`
|
|
116
|
+
const text = await typedResponse(response).text(z.string().email())
|
|
117
|
+
// ^? string
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## enhancedFetch
|
|
121
|
+
|
|
122
|
+
A wrapper around the `fetch` API.
|
|
123
|
+
It uses the `addQueryToInput`, `ensureStringBody` function internally and returns a `typedResponse` instead of a `Response`.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { enhancedFetch } from 'make-service'
|
|
127
|
+
|
|
128
|
+
const response = await enhancedFetch("https://example.com/api/users", {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: { some: { object: { as: { body } } } }
|
|
131
|
+
})
|
|
132
|
+
const json = await response.json()
|
|
133
|
+
// ^? unknown
|
|
134
|
+
// You can pass it a generic or schema to type the result
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This function accepts the same arguments as the `fetch` API - with exception of JSON-like body -, and it also accepts an object-like `query` and a `trace` function that will be called with the `input` and `requestInit` arguments.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { enhancedFetch } from 'make-service'
|
|
141
|
+
|
|
142
|
+
await enhancedFetch("https://example.com/api/users", {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: { some: { object: { as: { body } } } },
|
|
145
|
+
query: { page: "1" },
|
|
146
|
+
trace: (input, requestInit) => console.log(input, requestInit)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// The trace function will be called with the following arguments:
|
|
150
|
+
// "https://example.com/api/users?page=1"
|
|
151
|
+
// {
|
|
152
|
+
// method: 'POST',
|
|
153
|
+
// body: '{"some":{"object":{"as":{"body":{}}}}}',
|
|
154
|
+
// headers: { 'content-type': 'application/json' }
|
|
155
|
+
// }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Notice: the `enhancedFetch` adds a `'content-type': 'application/json'` header by default.
|
|
159
|
+
|
|
160
|
+
# makeService
|
|
161
|
+
|
|
162
|
+
The main function of this lib is built on top of the previous primitives and it allows you to create an "API" object with a `baseURL` and common `headers` for every request.
|
|
163
|
+
|
|
164
|
+
This "api" object can be called with every HTTP method and it will return a `typedResponse` object as it uses the `enhancedFetch` internally.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { makeService } from 'make-service'
|
|
168
|
+
|
|
169
|
+
const api = makeService("https://example.com/api", {
|
|
170
|
+
authorization: "Bearer 123"
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const response = await api.get("/users")
|
|
174
|
+
const json = await response.json()
|
|
175
|
+
// ^? unknown
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
On the example above, the `api.get` will call the `enhancedFetch` with the following arguments:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// "https://example.com/api/users"
|
|
182
|
+
// {
|
|
183
|
+
// method: 'GET',
|
|
184
|
+
// headers: {
|
|
185
|
+
// 'content-type': 'application/json',
|
|
186
|
+
// 'authorization': 'Bearer 123',
|
|
187
|
+
// }
|
|
188
|
+
// }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `api` object can be called with the same arguments as the `enhancedFetch`, such as `query`, object-like `body`, and `trace`.
|
|
192
|
+
|
|
193
|
+
Its `typedResponse` can also be parsed with a zod schema. Here follows a little more complex example:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const response = await api.get("/users", {
|
|
197
|
+
query: { search: "John" },
|
|
198
|
+
trace: (...args: any[]) => console.log(...args)
|
|
199
|
+
})
|
|
200
|
+
const json = await response.json(
|
|
201
|
+
z.object({
|
|
202
|
+
data: z.object({
|
|
203
|
+
users: z.array(z.object({
|
|
204
|
+
name: z.string()
|
|
205
|
+
}))
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
// transformed and catched
|
|
209
|
+
.transform(({ data: { users } }) => users)
|
|
210
|
+
.catch([])
|
|
211
|
+
)
|
|
212
|
+
// type of json will be { name: string }[]
|
|
213
|
+
// the URL called will be "https://example.com/api/users?search=John"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
It accepts more HTTP verbs:
|
|
217
|
+
```ts
|
|
218
|
+
await api.post("/users", { body: { name: "John" } })
|
|
219
|
+
await api.put("/users/1", { body: { name: "John" } })
|
|
220
|
+
await api.patch("/users/1", { body: { name: "John" } })
|
|
221
|
+
await api.delete("/users/1")
|
|
222
|
+
await api.head("/users")
|
|
223
|
+
await api.options("/users")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Thank you
|
|
227
|
+
I really appreciate your feedback and contributions. If you have any questions, feel free to open an issue or contact me on [Twitter](https://twitter.com/gugaguichard).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Deno } from "@deno/shim-deno";
|
|
2
|
+
export { Deno } from "@deno/shim-deno";
|
|
3
|
+
import { fetch, File, FormData, Headers, Request, Response } from "undici";
|
|
4
|
+
export { fetch, File, FormData, Headers, Request, Response } from "undici";
|
|
5
|
+
const dntGlobals = {
|
|
6
|
+
Deno,
|
|
7
|
+
fetch,
|
|
8
|
+
File,
|
|
9
|
+
FormData,
|
|
10
|
+
Headers,
|
|
11
|
+
Request,
|
|
12
|
+
Response,
|
|
13
|
+
};
|
|
14
|
+
export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
15
|
+
// deno-lint-ignore ban-types
|
|
16
|
+
function createMergeProxy(baseObj, extObj) {
|
|
17
|
+
return new Proxy(baseObj, {
|
|
18
|
+
get(_target, prop, _receiver) {
|
|
19
|
+
if (prop in extObj) {
|
|
20
|
+
return extObj[prop];
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
return baseObj[prop];
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
set(_target, prop, value) {
|
|
27
|
+
if (prop in extObj) {
|
|
28
|
+
delete extObj[prop];
|
|
29
|
+
}
|
|
30
|
+
baseObj[prop] = value;
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
deleteProperty(_target, prop) {
|
|
34
|
+
let success = false;
|
|
35
|
+
if (prop in extObj) {
|
|
36
|
+
delete extObj[prop];
|
|
37
|
+
success = true;
|
|
38
|
+
}
|
|
39
|
+
if (prop in baseObj) {
|
|
40
|
+
delete baseObj[prop];
|
|
41
|
+
success = true;
|
|
42
|
+
}
|
|
43
|
+
return success;
|
|
44
|
+
},
|
|
45
|
+
ownKeys(_target) {
|
|
46
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
47
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
48
|
+
const extKeysSet = new Set(extKeys);
|
|
49
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
50
|
+
},
|
|
51
|
+
defineProperty(_target, prop, desc) {
|
|
52
|
+
if (prop in extObj) {
|
|
53
|
+
delete extObj[prop];
|
|
54
|
+
}
|
|
55
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
59
|
+
if (prop in extObj) {
|
|
60
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
has(_target, prop) {
|
|
67
|
+
return prop in extObj || prop in baseObj;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
package/esm/constants.js
ADDED
package/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addQueryToInput, ensureStringBody, enhancedFetch, makeService, makeGetApiUrl, typedResponse, } from './make-service.js';
|
package/esm/internals.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HTTP_METHODS } from './constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* It returns the JSON object or throws an error if the response is not ok.
|
|
4
|
+
* @param response the Response to be parsed
|
|
5
|
+
* @returns the response.json method that accepts a type or Zod schema for a typed json response
|
|
6
|
+
*/
|
|
7
|
+
function getJson(response) {
|
|
8
|
+
return async (schema) => {
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(await response.text());
|
|
11
|
+
}
|
|
12
|
+
const json = await response.json();
|
|
13
|
+
return schema ? schema.parse(json) : json;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @param response the Response to be parsed
|
|
18
|
+
* @returns the response.text method that accepts a type or Zod schema for a typed response
|
|
19
|
+
*/
|
|
20
|
+
function getText(response) {
|
|
21
|
+
return async (schema) => {
|
|
22
|
+
const text = await response.text();
|
|
23
|
+
return schema ? schema.parse(text) : text;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function isHTTPMethod(method) {
|
|
27
|
+
return HTTP_METHODS.includes(method);
|
|
28
|
+
}
|
|
29
|
+
export { getJson, getText, isHTTPMethod };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
2
|
+
import { getJson, getText, isHTTPMethod } from './internals.js';
|
|
3
|
+
/**
|
|
4
|
+
* @param input a string or URL to which the query parameters will be added
|
|
5
|
+
* @param searchParams the query parameters
|
|
6
|
+
* @returns the input with the query parameters added with the same type as the input
|
|
7
|
+
*/
|
|
8
|
+
function addQueryToInput(input, searchParams) {
|
|
9
|
+
if (!searchParams)
|
|
10
|
+
return input;
|
|
11
|
+
if (searchParams && typeof input === 'string') {
|
|
12
|
+
const separator = input.includes('?') ? '&' : '?';
|
|
13
|
+
return `${input}${separator}${new URLSearchParams(searchParams)}`;
|
|
14
|
+
}
|
|
15
|
+
if (searchParams && input instanceof URL) {
|
|
16
|
+
input.search = new URLSearchParams(searchParams).toString();
|
|
17
|
+
}
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @param baseURL the base path to the API
|
|
22
|
+
* @returns a function that receives a path and an object of query parameters and returns a URL
|
|
23
|
+
*/
|
|
24
|
+
function makeGetApiUrl(baseURL) {
|
|
25
|
+
return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* It hacks the Response object to add typed json and text methods
|
|
29
|
+
* @param response the Response to be proxied
|
|
30
|
+
* @returns a Response with typed json and text methods
|
|
31
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
32
|
+
* const users = await response.json(userSchema);
|
|
33
|
+
* // ^? User[]
|
|
34
|
+
* const untyped = await response.json();
|
|
35
|
+
* // ^? unknown
|
|
36
|
+
* const text = await response.text();
|
|
37
|
+
* // ^? string
|
|
38
|
+
* const typedJson = await response.json<User[]>();
|
|
39
|
+
* // ^? User[]
|
|
40
|
+
*/
|
|
41
|
+
function typedResponse(response) {
|
|
42
|
+
return new Proxy(response, {
|
|
43
|
+
get(target, prop) {
|
|
44
|
+
if (prop === 'json')
|
|
45
|
+
return getJson(target);
|
|
46
|
+
if (prop === 'text')
|
|
47
|
+
return getText(target);
|
|
48
|
+
return target[prop];
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @param body the JSON-like body of the request
|
|
54
|
+
* @returns the body stringified if it is not a string
|
|
55
|
+
*/
|
|
56
|
+
function ensureStringBody(body) {
|
|
57
|
+
if (typeof body === 'undefined')
|
|
58
|
+
return;
|
|
59
|
+
if (typeof body === 'string')
|
|
60
|
+
return body;
|
|
61
|
+
return JSON.stringify(body);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @param input a string or URL to be fetched
|
|
66
|
+
* @param options the options to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
|
|
67
|
+
* @param options.body the body of the request. It will be automatically stringified so you can send a JSON-like object
|
|
68
|
+
* @param options.query the query parameters to be added to the URL
|
|
69
|
+
* @param options.trace a function that receives the URL and the requestInit and can be used to log the request
|
|
70
|
+
* @returns a Response with typed json and text methods
|
|
71
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
72
|
+
* const users = await response.json(userSchema);
|
|
73
|
+
* // ^? User[]
|
|
74
|
+
* const untyped = await response.json();
|
|
75
|
+
* // ^? unknown
|
|
76
|
+
*/
|
|
77
|
+
async function enhancedFetch(input, options) {
|
|
78
|
+
const { query, trace, ...reqInit } = options ?? {};
|
|
79
|
+
const headers = { 'content-type': 'application/json', ...reqInit.headers };
|
|
80
|
+
const url = addQueryToInput(input, query);
|
|
81
|
+
const body = ensureStringBody(reqInit.body);
|
|
82
|
+
const requestInit = { ...reqInit, headers, body };
|
|
83
|
+
trace?.(url, requestInit);
|
|
84
|
+
const request = new dntShim.Request(url, requestInit);
|
|
85
|
+
const response = await dntShim.fetch(request);
|
|
86
|
+
return typedResponse(response);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
*
|
|
90
|
+
* @param baseURL the base URL to the API
|
|
91
|
+
* @param baseHeaders any headers that should be sent with every request
|
|
92
|
+
* @returns an API object with HTTP methods that are functions that receive a path and options and return a serialized json response that can be typed or not.
|
|
93
|
+
* @example const headers = { Authorization: "Bearer 123" }
|
|
94
|
+
* const api = makeService("https://example.com/api", headers);
|
|
95
|
+
* const response = await api.get("/users")
|
|
96
|
+
* const users = await response.json(userSchema);
|
|
97
|
+
* // ^? User[]
|
|
98
|
+
*/
|
|
99
|
+
function makeService(baseURL, baseHeaders) {
|
|
100
|
+
/**
|
|
101
|
+
* A function that receives a path and options and returns a serialized json response that can be typed or not.
|
|
102
|
+
* @param method the HTTP method
|
|
103
|
+
* @returns the API function for the given HTTP method
|
|
104
|
+
*/
|
|
105
|
+
const api = (method) => {
|
|
106
|
+
return async (path, options = {}) => {
|
|
107
|
+
const response = await enhancedFetch(`${baseURL}${path}`, {
|
|
108
|
+
...options,
|
|
109
|
+
method,
|
|
110
|
+
headers: { ...baseHeaders, ...options?.headers },
|
|
111
|
+
});
|
|
112
|
+
return response;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* It returns a proxy that returns the api function for each HTTP method
|
|
117
|
+
*/
|
|
118
|
+
return new Proxy({}, {
|
|
119
|
+
get(_target, prop) {
|
|
120
|
+
if (isHTTPMethod(prop))
|
|
121
|
+
return api(prop);
|
|
122
|
+
throw new Error(`Invalid HTTP method: ${prop.toString()}`);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export { addQueryToInput, ensureStringBody, enhancedFetch, makeService, makeGetApiUrl, typedResponse, };
|
package/esm/package.json
ADDED
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"module": "./esm/index.js",
|
|
3
|
+
"main": "./script/index.js",
|
|
4
|
+
"types": "./types/index.d.ts",
|
|
5
|
+
"name": "make-service",
|
|
6
|
+
"version": "0.0.1",
|
|
7
|
+
"description": "Some utilities to extend the \"fetch\" API to better interact with external APIs.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Gustavo Guichard",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/gustavoguichard/make-service/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/gustavoguichard/make-service",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./types/index.d.ts",
|
|
18
|
+
"default": "./esm/index.js"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./types/index.d.ts",
|
|
22
|
+
"default": "./script/index.js"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@deno/shim-deno": "~0.12.0",
|
|
28
|
+
"undici": "^5.14.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^18.11.9"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dntGlobalThis = exports.Response = exports.Request = exports.Headers = exports.FormData = exports.File = exports.fetch = exports.Deno = void 0;
|
|
4
|
+
const shim_deno_1 = require("@deno/shim-deno");
|
|
5
|
+
var shim_deno_2 = require("@deno/shim-deno");
|
|
6
|
+
Object.defineProperty(exports, "Deno", { enumerable: true, get: function () { return shim_deno_2.Deno; } });
|
|
7
|
+
const undici_1 = require("undici");
|
|
8
|
+
var undici_2 = require("undici");
|
|
9
|
+
Object.defineProperty(exports, "fetch", { enumerable: true, get: function () { return undici_2.fetch; } });
|
|
10
|
+
Object.defineProperty(exports, "File", { enumerable: true, get: function () { return undici_2.File; } });
|
|
11
|
+
Object.defineProperty(exports, "FormData", { enumerable: true, get: function () { return undici_2.FormData; } });
|
|
12
|
+
Object.defineProperty(exports, "Headers", { enumerable: true, get: function () { return undici_2.Headers; } });
|
|
13
|
+
Object.defineProperty(exports, "Request", { enumerable: true, get: function () { return undici_2.Request; } });
|
|
14
|
+
Object.defineProperty(exports, "Response", { enumerable: true, get: function () { return undici_2.Response; } });
|
|
15
|
+
const dntGlobals = {
|
|
16
|
+
Deno: shim_deno_1.Deno,
|
|
17
|
+
fetch: undici_1.fetch,
|
|
18
|
+
File: undici_1.File,
|
|
19
|
+
FormData: undici_1.FormData,
|
|
20
|
+
Headers: undici_1.Headers,
|
|
21
|
+
Request: undici_1.Request,
|
|
22
|
+
Response: undici_1.Response,
|
|
23
|
+
};
|
|
24
|
+
exports.dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
25
|
+
// deno-lint-ignore ban-types
|
|
26
|
+
function createMergeProxy(baseObj, extObj) {
|
|
27
|
+
return new Proxy(baseObj, {
|
|
28
|
+
get(_target, prop, _receiver) {
|
|
29
|
+
if (prop in extObj) {
|
|
30
|
+
return extObj[prop];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return baseObj[prop];
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
set(_target, prop, value) {
|
|
37
|
+
if (prop in extObj) {
|
|
38
|
+
delete extObj[prop];
|
|
39
|
+
}
|
|
40
|
+
baseObj[prop] = value;
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
deleteProperty(_target, prop) {
|
|
44
|
+
let success = false;
|
|
45
|
+
if (prop in extObj) {
|
|
46
|
+
delete extObj[prop];
|
|
47
|
+
success = true;
|
|
48
|
+
}
|
|
49
|
+
if (prop in baseObj) {
|
|
50
|
+
delete baseObj[prop];
|
|
51
|
+
success = true;
|
|
52
|
+
}
|
|
53
|
+
return success;
|
|
54
|
+
},
|
|
55
|
+
ownKeys(_target) {
|
|
56
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
57
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
58
|
+
const extKeysSet = new Set(extKeys);
|
|
59
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
60
|
+
},
|
|
61
|
+
defineProperty(_target, prop, desc) {
|
|
62
|
+
if (prop in extObj) {
|
|
63
|
+
delete extObj[prop];
|
|
64
|
+
}
|
|
65
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
69
|
+
if (prop in extObj) {
|
|
70
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
has(_target, prop) {
|
|
77
|
+
return prop in extObj || prop in baseObj;
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HTTP_METHODS = void 0;
|
|
4
|
+
const HTTP_METHODS = [
|
|
5
|
+
'get',
|
|
6
|
+
'post',
|
|
7
|
+
'put',
|
|
8
|
+
'delete',
|
|
9
|
+
'patch',
|
|
10
|
+
'options',
|
|
11
|
+
'head',
|
|
12
|
+
];
|
|
13
|
+
exports.HTTP_METHODS = HTTP_METHODS;
|
package/script/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typedResponse = exports.makeGetApiUrl = exports.makeService = exports.enhancedFetch = exports.ensureStringBody = exports.addQueryToInput = void 0;
|
|
4
|
+
var make_service_js_1 = require("./make-service.js");
|
|
5
|
+
Object.defineProperty(exports, "addQueryToInput", { enumerable: true, get: function () { return make_service_js_1.addQueryToInput; } });
|
|
6
|
+
Object.defineProperty(exports, "ensureStringBody", { enumerable: true, get: function () { return make_service_js_1.ensureStringBody; } });
|
|
7
|
+
Object.defineProperty(exports, "enhancedFetch", { enumerable: true, get: function () { return make_service_js_1.enhancedFetch; } });
|
|
8
|
+
Object.defineProperty(exports, "makeService", { enumerable: true, get: function () { return make_service_js_1.makeService; } });
|
|
9
|
+
Object.defineProperty(exports, "makeGetApiUrl", { enumerable: true, get: function () { return make_service_js_1.makeGetApiUrl; } });
|
|
10
|
+
Object.defineProperty(exports, "typedResponse", { enumerable: true, get: function () { return make_service_js_1.typedResponse; } });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHTTPMethod = exports.getText = exports.getJson = void 0;
|
|
4
|
+
const constants_js_1 = require("./constants.js");
|
|
5
|
+
/**
|
|
6
|
+
* It returns the JSON object or throws an error if the response is not ok.
|
|
7
|
+
* @param response the Response to be parsed
|
|
8
|
+
* @returns the response.json method that accepts a type or Zod schema for a typed json response
|
|
9
|
+
*/
|
|
10
|
+
function getJson(response) {
|
|
11
|
+
return async (schema) => {
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(await response.text());
|
|
14
|
+
}
|
|
15
|
+
const json = await response.json();
|
|
16
|
+
return schema ? schema.parse(json) : json;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
exports.getJson = getJson;
|
|
20
|
+
/**
|
|
21
|
+
* @param response the Response to be parsed
|
|
22
|
+
* @returns the response.text method that accepts a type or Zod schema for a typed response
|
|
23
|
+
*/
|
|
24
|
+
function getText(response) {
|
|
25
|
+
return async (schema) => {
|
|
26
|
+
const text = await response.text();
|
|
27
|
+
return schema ? schema.parse(text) : text;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
exports.getText = getText;
|
|
31
|
+
function isHTTPMethod(method) {
|
|
32
|
+
return constants_js_1.HTTP_METHODS.includes(method);
|
|
33
|
+
}
|
|
34
|
+
exports.isHTTPMethod = isHTTPMethod;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.typedResponse = exports.makeGetApiUrl = exports.makeService = exports.enhancedFetch = exports.ensureStringBody = exports.addQueryToInput = void 0;
|
|
27
|
+
const dntShim = __importStar(require("./_dnt.shims.js"));
|
|
28
|
+
const internals_js_1 = require("./internals.js");
|
|
29
|
+
/**
|
|
30
|
+
* @param input a string or URL to which the query parameters will be added
|
|
31
|
+
* @param searchParams the query parameters
|
|
32
|
+
* @returns the input with the query parameters added with the same type as the input
|
|
33
|
+
*/
|
|
34
|
+
function addQueryToInput(input, searchParams) {
|
|
35
|
+
if (!searchParams)
|
|
36
|
+
return input;
|
|
37
|
+
if (searchParams && typeof input === 'string') {
|
|
38
|
+
const separator = input.includes('?') ? '&' : '?';
|
|
39
|
+
return `${input}${separator}${new URLSearchParams(searchParams)}`;
|
|
40
|
+
}
|
|
41
|
+
if (searchParams && input instanceof URL) {
|
|
42
|
+
input.search = new URLSearchParams(searchParams).toString();
|
|
43
|
+
}
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
exports.addQueryToInput = addQueryToInput;
|
|
47
|
+
/**
|
|
48
|
+
* @param baseURL the base path to the API
|
|
49
|
+
* @returns a function that receives a path and an object of query parameters and returns a URL
|
|
50
|
+
*/
|
|
51
|
+
function makeGetApiUrl(baseURL) {
|
|
52
|
+
return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
|
|
53
|
+
}
|
|
54
|
+
exports.makeGetApiUrl = makeGetApiUrl;
|
|
55
|
+
/**
|
|
56
|
+
* It hacks the Response object to add typed json and text methods
|
|
57
|
+
* @param response the Response to be proxied
|
|
58
|
+
* @returns a Response with typed json and text methods
|
|
59
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
60
|
+
* const users = await response.json(userSchema);
|
|
61
|
+
* // ^? User[]
|
|
62
|
+
* const untyped = await response.json();
|
|
63
|
+
* // ^? unknown
|
|
64
|
+
* const text = await response.text();
|
|
65
|
+
* // ^? string
|
|
66
|
+
* const typedJson = await response.json<User[]>();
|
|
67
|
+
* // ^? User[]
|
|
68
|
+
*/
|
|
69
|
+
function typedResponse(response) {
|
|
70
|
+
return new Proxy(response, {
|
|
71
|
+
get(target, prop) {
|
|
72
|
+
if (prop === 'json')
|
|
73
|
+
return (0, internals_js_1.getJson)(target);
|
|
74
|
+
if (prop === 'text')
|
|
75
|
+
return (0, internals_js_1.getText)(target);
|
|
76
|
+
return target[prop];
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
exports.typedResponse = typedResponse;
|
|
81
|
+
/**
|
|
82
|
+
* @param body the JSON-like body of the request
|
|
83
|
+
* @returns the body stringified if it is not a string
|
|
84
|
+
*/
|
|
85
|
+
function ensureStringBody(body) {
|
|
86
|
+
if (typeof body === 'undefined')
|
|
87
|
+
return;
|
|
88
|
+
if (typeof body === 'string')
|
|
89
|
+
return body;
|
|
90
|
+
return JSON.stringify(body);
|
|
91
|
+
}
|
|
92
|
+
exports.ensureStringBody = ensureStringBody;
|
|
93
|
+
/**
|
|
94
|
+
*
|
|
95
|
+
* @param input a string or URL to be fetched
|
|
96
|
+
* @param options the options to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
|
|
97
|
+
* @param options.body the body of the request. It will be automatically stringified so you can send a JSON-like object
|
|
98
|
+
* @param options.query the query parameters to be added to the URL
|
|
99
|
+
* @param options.trace a function that receives the URL and the requestInit and can be used to log the request
|
|
100
|
+
* @returns a Response with typed json and text methods
|
|
101
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
102
|
+
* const users = await response.json(userSchema);
|
|
103
|
+
* // ^? User[]
|
|
104
|
+
* const untyped = await response.json();
|
|
105
|
+
* // ^? unknown
|
|
106
|
+
*/
|
|
107
|
+
async function enhancedFetch(input, options) {
|
|
108
|
+
const { query, trace, ...reqInit } = options ?? {};
|
|
109
|
+
const headers = { 'content-type': 'application/json', ...reqInit.headers };
|
|
110
|
+
const url = addQueryToInput(input, query);
|
|
111
|
+
const body = ensureStringBody(reqInit.body);
|
|
112
|
+
const requestInit = { ...reqInit, headers, body };
|
|
113
|
+
trace?.(url, requestInit);
|
|
114
|
+
const request = new dntShim.Request(url, requestInit);
|
|
115
|
+
const response = await dntShim.fetch(request);
|
|
116
|
+
return typedResponse(response);
|
|
117
|
+
}
|
|
118
|
+
exports.enhancedFetch = enhancedFetch;
|
|
119
|
+
/**
|
|
120
|
+
*
|
|
121
|
+
* @param baseURL the base URL to the API
|
|
122
|
+
* @param baseHeaders any headers that should be sent with every request
|
|
123
|
+
* @returns an API object with HTTP methods that are functions that receive a path and options and return a serialized json response that can be typed or not.
|
|
124
|
+
* @example const headers = { Authorization: "Bearer 123" }
|
|
125
|
+
* const api = makeService("https://example.com/api", headers);
|
|
126
|
+
* const response = await api.get("/users")
|
|
127
|
+
* const users = await response.json(userSchema);
|
|
128
|
+
* // ^? User[]
|
|
129
|
+
*/
|
|
130
|
+
function makeService(baseURL, baseHeaders) {
|
|
131
|
+
/**
|
|
132
|
+
* A function that receives a path and options and returns a serialized json response that can be typed or not.
|
|
133
|
+
* @param method the HTTP method
|
|
134
|
+
* @returns the API function for the given HTTP method
|
|
135
|
+
*/
|
|
136
|
+
const api = (method) => {
|
|
137
|
+
return async (path, options = {}) => {
|
|
138
|
+
const response = await enhancedFetch(`${baseURL}${path}`, {
|
|
139
|
+
...options,
|
|
140
|
+
method,
|
|
141
|
+
headers: { ...baseHeaders, ...options?.headers },
|
|
142
|
+
});
|
|
143
|
+
return response;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* It returns a proxy that returns the api function for each HTTP method
|
|
148
|
+
*/
|
|
149
|
+
return new Proxy({}, {
|
|
150
|
+
get(_target, prop) {
|
|
151
|
+
if ((0, internals_js_1.isHTTPMethod)(prop))
|
|
152
|
+
return api(prop);
|
|
153
|
+
throw new Error(`Invalid HTTP method: ${prop.toString()}`);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
exports.makeService = makeService;
|
package/script/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Deno } from "@deno/shim-deno";
|
|
2
|
+
export { Deno } from "@deno/shim-deno";
|
|
3
|
+
import { fetch, File, FormData, Headers, Request, Response } from "undici";
|
|
4
|
+
export { fetch, File, FormData, Headers, Request, Response, type BodyInit, type HeadersInit, type RequestInit, type ResponseInit } from "undici";
|
|
5
|
+
export declare const dntGlobalThis: Omit<typeof globalThis, "Deno" | "fetch" | "File" | "FormData" | "Headers" | "Request" | "Response"> & {
|
|
6
|
+
Deno: typeof Deno;
|
|
7
|
+
fetch: typeof fetch;
|
|
8
|
+
File: typeof File;
|
|
9
|
+
FormData: typeof FormData;
|
|
10
|
+
Headers: typeof Headers;
|
|
11
|
+
Request: typeof Request;
|
|
12
|
+
Response: typeof Response;
|
|
13
|
+
};
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addQueryToInput, ensureStringBody, enhancedFetch, makeService, makeGetApiUrl, typedResponse, } from './make-service.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
2
|
+
import { HTTPMethod, Schema } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* It returns the JSON object or throws an error if the response is not ok.
|
|
5
|
+
* @param response the Response to be parsed
|
|
6
|
+
* @returns the response.json method that accepts a type or Zod schema for a typed json response
|
|
7
|
+
*/
|
|
8
|
+
declare function getJson(response: dntShim.Response): <T = unknown>(schema?: Schema<T> | undefined) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* @param response the Response to be parsed
|
|
11
|
+
* @returns the response.text method that accepts a type or Zod schema for a typed response
|
|
12
|
+
*/
|
|
13
|
+
declare function getText(response: dntShim.Response): <T extends string = string>(schema?: Schema<T> | undefined) => Promise<T>;
|
|
14
|
+
declare function isHTTPMethod(method: string | symbol): method is HTTPMethod;
|
|
15
|
+
export { getJson, getText, isHTTPMethod };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
3
|
+
import { getJson, getText } from './internals.js';
|
|
4
|
+
import { JSONValue, SearchParams } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* @param input a string or URL to which the query parameters will be added
|
|
7
|
+
* @param searchParams the query parameters
|
|
8
|
+
* @returns the input with the query parameters added with the same type as the input
|
|
9
|
+
*/
|
|
10
|
+
declare function addQueryToInput(input: string | URL, searchParams?: SearchParams): string | URL;
|
|
11
|
+
/**
|
|
12
|
+
* @param baseURL the base path to the API
|
|
13
|
+
* @returns a function that receives a path and an object of query parameters and returns a URL
|
|
14
|
+
*/
|
|
15
|
+
declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: SearchParams) => string | URL;
|
|
16
|
+
/**
|
|
17
|
+
* It hacks the Response object to add typed json and text methods
|
|
18
|
+
* @param response the Response to be proxied
|
|
19
|
+
* @returns a Response with typed json and text methods
|
|
20
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
21
|
+
* const users = await response.json(userSchema);
|
|
22
|
+
* // ^? User[]
|
|
23
|
+
* const untyped = await response.json();
|
|
24
|
+
* // ^? unknown
|
|
25
|
+
* const text = await response.text();
|
|
26
|
+
* // ^? string
|
|
27
|
+
* const typedJson = await response.json<User[]>();
|
|
28
|
+
* // ^? User[]
|
|
29
|
+
*/
|
|
30
|
+
declare function typedResponse(response: dntShim.Response): Omit<dntShim.Response, "json" | "text"> & {
|
|
31
|
+
json: ReturnType<typeof getJson>;
|
|
32
|
+
text: ReturnType<typeof getText>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @param body the JSON-like body of the request
|
|
36
|
+
* @returns the body stringified if it is not a string
|
|
37
|
+
*/
|
|
38
|
+
declare function ensureStringBody(body?: JSONValue): string | undefined;
|
|
39
|
+
type Options = Omit<dntShim.RequestInit, 'body'> & {
|
|
40
|
+
body?: JSONValue;
|
|
41
|
+
query?: SearchParams;
|
|
42
|
+
trace?: (...args: Parameters<typeof dntShim.fetch>) => void;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param input a string or URL to be fetched
|
|
47
|
+
* @param options the options to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
|
|
48
|
+
* @param options.body the body of the request. It will be automatically stringified so you can send a JSON-like object
|
|
49
|
+
* @param options.query the query parameters to be added to the URL
|
|
50
|
+
* @param options.trace a function that receives the URL and the requestInit and can be used to log the request
|
|
51
|
+
* @returns a Response with typed json and text methods
|
|
52
|
+
* @example const response = await fetch("https://example.com/api/users");
|
|
53
|
+
* const users = await response.json(userSchema);
|
|
54
|
+
* // ^? User[]
|
|
55
|
+
* const untyped = await response.json();
|
|
56
|
+
* // ^? unknown
|
|
57
|
+
*/
|
|
58
|
+
declare function enhancedFetch(input: string | URL, options?: Options): Promise<Omit<dntShim.Response, "json" | "text"> & {
|
|
59
|
+
json: <T = unknown>(schema?: import("./types.js").Schema<T> | undefined) => Promise<T>;
|
|
60
|
+
text: <T_1 extends string = string>(schema?: import("./types.js").Schema<T_1> | undefined) => Promise<T_1>;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
* @param baseURL the base URL to the API
|
|
65
|
+
* @param baseHeaders any headers that should be sent with every request
|
|
66
|
+
* @returns an API object with HTTP methods that are functions that receive a path and options and return a serialized json response that can be typed or not.
|
|
67
|
+
* @example const headers = { Authorization: "Bearer 123" }
|
|
68
|
+
* const api = makeService("https://example.com/api", headers);
|
|
69
|
+
* const response = await api.get("/users")
|
|
70
|
+
* const users = await response.json(userSchema);
|
|
71
|
+
* // ^? User[]
|
|
72
|
+
*/
|
|
73
|
+
declare function makeService(baseURL: string, baseHeaders?: dntShim.HeadersInit): Record<"get" | "post" | "put" | "delete" | "patch" | "options" | "head", (path: string, options?: Omit<Options, 'method'>) => Promise<Omit<dntShim.Response, "json" | "text"> & {
|
|
74
|
+
json: <T = unknown>(schema?: import("./types.js").Schema<T> | undefined) => Promise<T>;
|
|
75
|
+
text: <T_1 extends string = string>(schema?: import("./types.js").Schema<T_1> | undefined) => Promise<T_1>;
|
|
76
|
+
}>>;
|
|
77
|
+
export { addQueryToInput, ensureStringBody, enhancedFetch, makeService, makeGetApiUrl, typedResponse, };
|
package/types/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { HTTP_METHODS } from './constants.js';
|
|
3
|
+
type Schema<T> = {
|
|
4
|
+
parse: (d: unknown) => T;
|
|
5
|
+
};
|
|
6
|
+
type JSONValue = string | number | boolean | {
|
|
7
|
+
[x: string]: JSONValue;
|
|
8
|
+
} | Array<JSONValue>;
|
|
9
|
+
type SearchParams = ConstructorParameters<typeof URLSearchParams>[0];
|
|
10
|
+
type HTTPMethod = typeof HTTP_METHODS[number];
|
|
11
|
+
export type { HTTPMethod, JSONValue, Schema, SearchParams };
|