make-service 0.2.0 → 1.0.0-next.2

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/README.md CHANGED
@@ -8,22 +8,49 @@ It adds a set of little features and allows you to parse responses with [zod](ht
8
8
  - 🤩 Type-safe return of `response.json()` and `response.text()`. Defaults to `unknown` instead of `any`.
9
9
  - 🚦 Easily setup an API with a `baseURL` and common `headers` for every request.
10
10
  - 🏗️ Compose URL from the base by just calling the endpoints and an object-like `query`.
11
+ - 🐾 Replaces URL wildcards with an object of `params`.
11
12
  - 🧙‍♀️ Automatically stringifies the `body` of a request so you can give it a JSON-like structure.
12
13
  - 🐛 Accepts a `trace` function for debugging.
13
14
 
14
15
  ## Example
15
16
 
16
17
  ```ts
17
- const api = makeService("https://example.com/api", {
18
+ const service = makeService("https://example.com/api", {
18
19
  Authorization: "Bearer 123",
19
20
  });
20
21
 
21
- const response = await api.get("/users")
22
+ const response = await service.get("/users")
22
23
  const users = await response.json(usersSchema);
23
24
  // ^? User[]
24
25
  ```
25
26
 
26
- ## Installation
27
+ # Table of Contents
28
+ - [Installation](#installation)
29
+ - [API](#api)
30
+ - [makeService](#makeservice)
31
+ - [Type-checking the response body](#type-checking-the-response-body)
32
+ - [Runtime type-checking and parsing the response body](#runtime-type-checking-and-parsing-the-response-body)
33
+ - [Supported HTTP Verbs](#supported-http-verbs)
34
+ - [Headers](#headers)
35
+ - [Passing a function as `baseHeaders`](#passing-a-function-as-baseheaders)
36
+ - [Deleting a previously set header](#deleting-a-previously-set-header)
37
+ - [Base URL](#base-url)
38
+ - [Body](#body)
39
+ - [Query](#query)
40
+ - [Params](#params)
41
+ - [Trace](#trace)
42
+ - [makeFetcher](#makefetcher)
43
+ - [enhancedFetch](#enhancedfetch)
44
+ - [typedResponse](#typedresponse)
45
+ - [Other available primitives](#other-available-primitives)
46
+ - [addQueryToURL](#addquerytourl)
47
+ - [ensureStringBody](#ensurestringbody)
48
+ - [makeGetApiURL](#makegetapiurl)
49
+ - [mergeHeaders](#mergeheaders)
50
+ - [replaceURLParams](#replaceurlparams)
51
+ - [Thank you](#thank-you)
52
+
53
+ # Installation
27
54
 
28
55
  ```sh
29
56
  npm install make-service
@@ -38,25 +65,24 @@ import { makeService } from "https://deno.land/x/make_service/mod.ts";
38
65
 
39
66
  This library exports the `makeService` function and some primitives used to build it. You can use the primitives as you wish but the `makeService` will have all the features combined.
40
67
 
41
- # makeService
42
-
68
+ ## makeService
43
69
  The main function of this lib is built on top of the primitives described in the following sections. It allows you to create a service object with a `baseURL` and common `headers` for every request.
44
70
 
45
- This service object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse) object as it uses the [`enhancedFetch`](#enhancedfetch) internally.
71
+ This service object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse).
46
72
 
47
73
  ```ts
48
74
  import { makeService } from 'make-service'
49
75
 
50
- const api = makeService("https://example.com/api", {
76
+ const service = makeService("https://example.com/api", {
51
77
  authorization: "Bearer 123"
52
78
  })
53
79
 
54
- const response = await api.get("/users")
80
+ const response = await service.get("/users")
55
81
  const json = await response.json()
56
82
  // ^? unknown
57
83
  ```
58
84
 
59
- On the example above, the `api.get` will call the [`enhancedFetch`](#enhancedfetch) with the following arguments:
85
+ On the example above, the request will be sent with the following arguments:
60
86
 
61
87
  ```ts
62
88
  // "https://example.com/api/users"
@@ -69,15 +95,22 @@ On the example above, the `api.get` will call the [`enhancedFetch`](#enhancedfet
69
95
  // }
70
96
  ```
71
97
 
72
- The `api` object can be called with the same arguments as the [`enhancedFetch`](#enhancedfetch), such as `query`, object-like `body`, and `trace`.
98
+ ### Type-checking the response body
99
+ The `response` object returned by the `service` can be type-casted with a given generic type. This will type-check the `response.json()` and `response.text()` methods.
100
+
101
+ ```ts
102
+ const response = await service.get("/users")
103
+ const users = await response.json<{ data: User[] }>()
104
+ // ^? { data: User[] }
105
+ const content = await response.text<`${string}@${string}`>()
106
+ // ^? `${string}@${string}`
107
+ ```
73
108
 
109
+ ### Runtime type-checking and parsing the response body
74
110
  Its [`typedResponse`](#typedresponse) can also be parsed with a zod schema. Here follows a little more complex example:
75
111
 
76
112
  ```ts
77
- const response = await api.get("/users", {
78
- query: { search: "John" },
79
- trace: (url, requestInit) => console.log(url, requestInit),
80
- })
113
+ const response = await service.get("/users")
81
114
  const json = await response.json(
82
115
  z.object({
83
116
  data: z.object({
@@ -91,41 +124,64 @@ const json = await response.json(
91
124
  .catch([])
92
125
  )
93
126
  // type of json will be { name: string }[]
94
- // the URL called will be "https://example.com/api/users?search=John"
127
+
128
+ const content = await response.text(z.string().email())
129
+ // It will throw an error if the response.text is not a valid email
95
130
  ```
131
+ You can transform any `Response` in a `TypedResponse` like that by using the [`typedResponse`](#typedresponse) function.
96
132
 
97
- It accepts more HTTP verbs:
133
+ ### Supported HTTP Verbs
134
+ Other than the `get` it also accepts more HTTP verbs:
98
135
  ```ts
99
- await api.post("/users", { body: { name: "John" } })
100
- await api.put("/users/1", { body: { name: "John" } })
101
- await api.patch("/users/1", { body: { name: "John" } })
102
- await api.delete("/users/1")
103
- await api.head("/users")
104
- await api.options("/users")
136
+ await service.get("/users")
137
+ await service.post("/users", { body: { name: "John" } })
138
+ await service.put("/users/1", { body: { name: "John" } })
139
+ await service.patch("/users/1", { body: { name: "John" } })
140
+ await service.delete("/users/1")
141
+ await service.head("/users")
142
+ await service.options("/users")
105
143
  ```
106
144
 
107
- This function can also correctly merge any sort of `URL`, `URLSearchParams`, and `Headers`.
145
+ ### Headers
146
+ The `headers` argument can be a `Headers` object, a `Record<string, string>`, or an array of `[key, value]` tuples (entries).
147
+ The `baseHeaders` and the `headers` will be merged together, with the `headers` taking precedence.
108
148
 
109
149
  ```ts
110
150
  import { makeService } from 'make-service'
111
151
 
112
- const api = makeService(new URL("https://example.com/api"), new Headers({
113
- authorization: "Bearer 123"
152
+ const service = makeService("https://example.com/api", new Headers({
153
+ authorization: "Bearer 123",
154
+ accept: "*/*",
114
155
  }))
115
156
 
116
- const response = await api.get("/users?admin=true", {
157
+ const response = await service.get("/users", {
117
158
  headers: [['accept', 'application/json']],
118
- query: { page: "2" },
119
159
  })
120
160
 
121
- // It will call "https://example.com/api/users?admin=true&page=2"
161
+ // It will call "https://example.com/api/users"
122
162
  // with headers: { authorization: "Bearer 123", accept: "application/json" }
123
163
  ```
124
164
 
165
+ #### Passing a function as `baseHeaders`
166
+ The given `baseHeaders` can be a sync or async function that will run in every request before it gets merged with the other headers.
167
+ This is particularly useful when you need to send a refreshed token or add a timestamp to the request.
168
+
169
+ ```ts
170
+ import { makeService } from 'make-service'
171
+
172
+ declare getAuthorizationToken: () => Promise<HeadersInit>
173
+
174
+ const service = makeService("https://example.com/api", async () => ({
175
+ authorization: await getAuthorizationToken(),
176
+ }))
177
+
178
+ ```
179
+
180
+ #### Deleting a previously set header
125
181
  In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value:
126
182
  ```ts
127
- const api = makeService("https://example.com/api", { authorization: "Bearer 123" })
128
- const response = await api.get("/users", {
183
+ const service = makeService("https://example.com/api", { authorization: "Bearer 123" })
184
+ const response = await service.get("/users", {
129
185
  headers: new Headers({ authorization: 'undefined', "Content-Type": undefined }),
130
186
  })
131
187
  // headers will be empty.
@@ -135,10 +191,132 @@ Note: Don't forget headers are case insensitive.
135
191
  const headers = new Headers({ 'Content-Type': 'application/json' })
136
192
  Object.fromEntries(headers) // equals to: { 'content-type': 'application/json' }
137
193
  ```
194
+ All the features above are done by using the [`mergeHeaders`](#mergeheaders) function internally.
195
+
196
+
197
+ ### Base URL
198
+ The service function can receive a `string` or `URL` as base `url` and it will be able to merge them correctly with the given path:
199
+
200
+ ```ts
201
+ import { makeService } from 'make-service'
202
+
203
+ const service = makeService(new URL("https://example.com/api"))
204
+
205
+ const response = await service.get("/users?admin=true")
206
+
207
+ // It will call "https://example.com/api/users?admin=true"
208
+ ```
209
+ You can use the [`makeGetApiUrl`](#makegetapiurl) method to do that kind of URL composition.
210
+
211
+ ### Body
212
+ The function can also receive a `body` object that will be stringified and sent as the request body:
213
+
214
+ ```ts
215
+ import { makeService } from 'make-service'
216
+
217
+ const service = makeService("https://example.com/api")
218
+ const response = await service.post("/users", {
219
+ body: { person: { firstName: "John", lastName: "Doe" } },
220
+ })
221
+
222
+ // It will make a POST request to "https://example.com/api/users"
223
+ // with stringified body: "{\"person\":{\"firstName\":\"John\",\"lastName\":\"Doe\"}}"
224
+ ```
225
+
226
+ You can also pass any other accepted `BodyInit` values as body, such as `FormData`, `URLSearchParams`, `Blob`, `ReadableStream`, `ArrayBuffer`, etc.
227
+
228
+ ```ts
229
+ import { makeService } from 'make-service'
230
+
231
+ const service = makeService("https://example.com/api")
232
+ const formData = new FormData([["name", "John"], ["lastName", "Doe"]])
233
+ const response = await service.post("/users", {
234
+ body: formData,
235
+ })
236
+ ```
237
+ This is achieved by using the [`ensureStringBody`](#ensurestringbody) function internally.
238
+
239
+ ### Query
240
+ The service can also receive an `query` object that can be a `string`, a `URLSearchParams`, or an array of entries and it'll add that to the path as queryString:
241
+
242
+ ```ts
243
+ import { makeService } from 'make-service'
244
+
245
+ const service = makeService(new URL("https://example.com/api"))
246
+
247
+ const response = await service.get("/users?admin=true", {
248
+ query: new URLSearchParams({ page: "2" }),
249
+ })
250
+
251
+ // It will call "https://example.com/api/users?admin=true&page=2"
252
+
253
+ // It could also be:
254
+ const response = await service.get("/users?admin=true", {
255
+ query: [["page", "2"]],
256
+ })
257
+ // or:
258
+ const response = await service.get("/users?admin=true", {
259
+ query: "page=2",
260
+ })
261
+ ```
262
+ This is achieved by using the [`addQueryToURL`](#addquerytourl) function internally.
263
+
264
+ ### Params
265
+ The function can also receive a `params` object that will be used to replace the `:param` wildcards in the path:
266
+
267
+ ```ts
268
+ import { makeService } from 'make-service'
269
+
270
+ const service = makeService(new URL("https://example.com/api"))
271
+ const response = await service.get("/users/:id/article/:articleId", {
272
+ params: { id: "2", articleId: "3" },
273
+ })
274
+
275
+ // It will call "https://example.com/api/users/2/article/3"
276
+ ```
277
+ This is achieved by using the [`replaceURLParams`](#replaceurlparams) function internally.
278
+
279
+ ### Trace
280
+ The function can also receive a `trace` function that will be called with the final `url` and `requestInit` arguments.
281
+ Therefore you can know what are the actual arguments that will be passed to the `fetch` API.
282
+
283
+ ```ts
284
+ import { makeService } from 'make-service'
285
+
286
+ const service = makeService("https://example.com/api")
287
+ const response = await service.get("/users/:id", {
288
+ params: { id: "2" },
289
+ query: { page: "2"},
290
+ headers: { Accept: "application/json" },
291
+ trace: (url, requestInit) => {
292
+ console.log("The request was sent to " + url)
293
+ console.log("with the following params: " + JSON.stringify(requestInit))
294
+ },
295
+ })
296
+
297
+ // It will log:
298
+ // "The request was sent to https://example.com/api/users/2?page=2"
299
+ // with the following params: { headers: { "Accept": "application/json", "Content-type": "application/json" } }
300
+ ```
301
+
302
+ ## makeFetcher
303
+ This method is the same as [`makeService`](#make-service) but it doesn't expose the HTTP methods as properties of the returned object.
304
+ This is good for when you want to have a service setup but don't know the methods you'll be calling in advance, like in a proxy.
305
+
306
+ ```ts
307
+ import { makeFetcher } from 'make-service'
308
+
309
+ const fetcher = makeFetcher("https://example.com/api")
310
+ const response = await fetcher("/users", { method: "POST", body: { email: "john@doe.com" } })
311
+ const json = await response.json()
312
+ // ^? unknown
313
+ ```
314
+
315
+ Other than having to pass the method in the `RequestInit` this is going to have all the features of [`makeService`](#make-service).
138
316
 
139
317
  ## enhancedFetch
140
318
 
141
- A wrapper around the `fetch` API.
319
+ A wrapper around the `fetch` service.
142
320
  It returns a [`TypedResponse`](#typedresponse) instead of a `Response`.
143
321
 
144
322
  ```ts
@@ -153,22 +331,23 @@ const json = await response.json()
153
331
  // You can pass it a generic or schema to type the result
154
332
  ```
155
333
 
156
- This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](/src/make-service.ts) -, and it also accepts an object-like [`query`](/src/make-service.ts) and a `trace` function that will be called with the `url` and `requestInit` arguments.
334
+ This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](#body) -, and it also accepts an object of [`params`](#params) to replace URL wildcards, an object-like [`query`](#query), and a [`trace`](#trace) function. Those are all described above in [`makeService`](#make-service).
157
335
 
158
336
  This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
159
337
 
160
338
  ```ts
161
339
  import { enhancedFetch } from 'make-service'
162
340
 
163
- await enhancedFetch("https://example.com/api/users", {
341
+ await enhancedFetch("https://example.com/api/users/:role", {
164
342
  method: 'POST',
165
343
  body: { some: { object: { as: { body } } } },
166
344
  query: { page: "1" },
167
- trace: (url, requestInit) => console.log(url, requestInit)
345
+ params: { role: "admin" },
346
+ trace: console.log,
168
347
  })
169
348
 
170
349
  // The trace function will be called with the following arguments:
171
- // "https://example.com/api/users?page=1"
350
+ // "https://example.com/api/users/admin?page=1"
172
351
  // {
173
352
  // method: 'POST',
174
353
  // body: '{"some":{"object":{"as":{"body":{}}}}}',
@@ -205,5 +384,124 @@ const text = await response.text(z.string().email())
205
384
  // ^? string
206
385
  ```
207
386
 
387
+ # Other available primitives
388
+ This little library has plenty of other useful functions that you can use to build your own services and interactions with external APIs.
389
+
390
+ ## addQueryToURL
391
+ It receives a URL instance or URL string and an object-like query and returns a new URL with the query appended to it.
392
+
393
+ It will preserve the original query if it exists and will also preserve the type of the given URL.
394
+
395
+ ```ts
396
+ import { addQueryToURL } from 'make-service'
397
+
398
+ addQueryToURL("https://example.com/api/users", { page: "2" })
399
+ // https://example.com/api/users?page=2
400
+
401
+ addQueryToURL(
402
+ "https://example.com/api/users?role=admin",
403
+ { page: "2" },
404
+ )
405
+ // https://example.com/api/users?role=admin&page=2
406
+
407
+ addQueryToURL(
408
+ new URL("https://example.com/api/users"),
409
+ { page: "2" },
410
+ )
411
+ // https://example.com/api/users?page=2
412
+
413
+ addQueryToURL(
414
+ new URL("https://example.com/api/users?role=admin"),
415
+ { page: "2" },
416
+ )
417
+ // https://example.com/api/users?role=admin&page=2
418
+ ```
419
+
420
+ ## ensureStringBody
421
+ It accepts any value considered a `BodyInit` (the type of the body in `fetch`, such as `ReadableStream` | `XMLHttpRequestBodyInit` | `null`) and also accepts a JSON-like structure such as a number, string, boolean, array or object.
422
+
423
+ In case it detects a JSON-like structure it will return a stringified version of that payload. Otherwise the type will be preserved.
424
+
425
+ ```ts
426
+ import { ensureStringBody } from 'make-service'
427
+
428
+ ensureStringBody({ foo: "bar" })
429
+ // '{"foo":"bar"}'
430
+ ensureStringBody("foo")
431
+ // 'foo'
432
+ ensureStringBody(1)
433
+ // '1'
434
+ ensureStringBody(true)
435
+ // 'true'
436
+ ensureStringBody(null)
437
+ // null
438
+ ensureStringBody(new ReadableStream())
439
+ // ReadableStream
440
+
441
+ // and so on...
442
+ ```
443
+
444
+ ## makeGetApiURL
445
+ It creates an URL builder for your API. It works similarly to [`makeFetcher`](#makefetcher) but will return the URL instead of a response.
446
+
447
+ You create a `getApiURL` function by giving it a `baseURL` and then it accepts a path and an optional [query](#query) that will be merged into the final URL.
448
+
449
+ ```ts
450
+ import { makeGetApiURL } from 'make-service'
451
+
452
+ const getApiURL = makeGetApiURL("https://example.com/api")
453
+ const url = getApiURL("/users?admin=true", { query: { page: "2" } })
454
+
455
+ // "https://example.com/api/users?admin=true&page=2"
456
+ ```
457
+
458
+ Notice the extra slashes are gonna be added or removed as needed.
459
+ ```ts
460
+ makeGetApiURL("https://example.com/api/")("/users")
461
+ // "https://example.com/api/users"
462
+ makeGetApiURL("https://example.com/api")("users")
463
+ // "https://example.com/api/users"
464
+ ```
465
+
466
+ ## mergeHeaders
467
+ It merges multiple `HeadersInit` objects into a single `Headers` instance.
468
+ They can be of any type that is accepted by the `Headers` constructor, like a `Headers` instance, a plain object, or an array of entries.
469
+
470
+ ```ts
471
+ import { mergeHeaders } from 'make-service'
472
+
473
+ const headers1 = new Headers({ "Content-Type": "application/json" })
474
+ const headers2 = { Accept: "application/json" }
475
+ const headers3 = [["accept", "*/*"]]
476
+
477
+ const merged = mergeHeaders(headers1, headers2, headers3)
478
+ // ^? Headers({ "content-Type": "application/json", "accept": "*/*" })
479
+ ```
480
+
481
+ It will delete previous headers if `undefined` or `"undefined"` is given:
482
+
483
+ ```ts
484
+ import { mergeHeaders } from 'make-service'
485
+
486
+ const headers1 = new Headers({ "Content-Type": "application/json", Accept: "application/json" })
487
+ const headers2 = { accept: undefined }
488
+ const headers3 = [["content-type", "undefined"]]
489
+
490
+ const merged = mergeHeaders(headers1, headers2, headers3)
491
+ // ^? Headers({})
492
+ ```
493
+
494
+ ## replaceURLParams
495
+ This function replaces URL wildcards with the given params.
496
+ ```ts
497
+ import { replaceURLParams } from 'make-service'
498
+
499
+ const url = replaceURLParams(
500
+ "https://example.com/users/:id/posts/:postId",
501
+ { id: "2", postId: "3" },
502
+ )
503
+ // It will return: "https://example.com/users/2/posts/3"
504
+ ```
505
+
208
506
  ## Thank you
209
507
  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).
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
1
+ declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT"];
2
2
 
3
3
  /**
4
4
  * It returns the JSON object or throws an error if the response is not ok.
@@ -23,8 +23,9 @@ type TypedResponse = Omit<Response, 'json' | 'text'> & {
23
23
  json: TypedResponseJson;
24
24
  text: TypedResponseText;
25
25
  };
26
- type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
27
- body?: JSONValue;
26
+ type EnhancedRequestInit = Omit<RequestInit, 'body' | 'method'> & {
27
+ method?: HTTPMethod | Lowercase<HTTPMethod>;
28
+ body?: JSONValue | BodyInit | null;
28
29
  query?: SearchParams;
29
30
  params?: Record<string, string>;
30
31
  trace?: (...args: Parameters<typeof fetch>) => void;
@@ -43,27 +44,6 @@ type PathParams<T extends string> = NoEmpty<T extends `${infer _}:${infer Param}
43
44
  [K in Param]: string;
44
45
  } : {}>;
45
46
 
46
- /**
47
- * It merges multiple HeadersInit objects into a single Headers object
48
- * @param entries Any number of HeadersInit objects
49
- * @returns a new Headers object with the merged headers
50
- */
51
- declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
52
- /**
53
- * @param url a string or URL to which the query parameters will be added
54
- * @param searchParams the query parameters
55
- * @returns the url with the query parameters added with the same type as the url
56
- */
57
- declare function addQueryToUrl(url: string | URL, searchParams?: SearchParams): string | URL;
58
- /**
59
- * @deprecated method renamed to addQueryToUrl
60
- */
61
- declare const addQueryToInput: typeof addQueryToUrl;
62
- /**
63
- * @param baseURL the base path to the API
64
- * @returns a function that receives a path and an object of query parameters and returns a URL
65
- */
66
- declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchParams?: SearchParams) => string | URL;
67
47
  /**
68
48
  * It hacks the Response object to add typed json and text methods
69
49
  * @param response the Response to be proxied
@@ -79,11 +59,6 @@ declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchPara
79
59
  * // ^? User[]
80
60
  */
81
61
  declare function typedResponse(response: Response): TypedResponse;
82
- /**
83
- * @param body the JSON-like body of the request
84
- * @returns the body stringified if it is not a string
85
- */
86
- declare function ensureStringBody(body?: JSONValue): string | undefined;
87
62
  /**
88
63
  *
89
64
  * @param url a string or URL to be fetched
@@ -99,6 +74,18 @@ declare function ensureStringBody(body?: JSONValue): string | undefined;
99
74
  * // ^? unknown
100
75
  */
101
76
  declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
77
+ /**
78
+ *
79
+ * @param baseURL the base URL to be fetched in every request
80
+ * @param baseHeaders any headers that should be sent with every request
81
+ * @returns a function that receive a path and requestInit and return a serialized json response that can be typed or not.
82
+ * @example const headers = { Authorization: "Bearer 123" }
83
+ * const fetcher = makeFetcher("https://example.com/api", headers);
84
+ * const response = await fetcher("/users", { method: "GET" })
85
+ * const users = await response.json(userSchema);
86
+ * // ^? User[]
87
+ */
88
+ declare function makeFetcher(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)): (path: string, requestInit?: EnhancedRequestInit) => Promise<TypedResponse>;
102
89
  /**
103
90
  *
104
91
  * @param baseURL the base URL to the API
@@ -110,6 +97,36 @@ declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestI
110
97
  * const users = await response.json(userSchema);
111
98
  * // ^? User[]
112
99
  */
113
- declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): Record<"delete" | "get" | "post" | "put" | "patch" | "options" | "head", (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>>;
100
+ declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)): Record<"get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "connect", (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>>;
101
+
102
+ /**
103
+ * @param url a string or URL to which the query parameters will be added
104
+ * @param searchParams the query parameters
105
+ * @returns the url with the query parameters added with the same type as the url
106
+ */
107
+ declare function addQueryToURL(url: string | URL, searchParams?: SearchParams): string | URL;
108
+ /**
109
+ * @param body the JSON-like body of the request
110
+ * @returns the body is stringified if it is not a string and it is a JSON-like object. It also accepts other types of BodyInit such as Blob, ReadableStream, etc.
111
+ */
112
+ declare function ensureStringBody<B extends JSONValue | BodyInit | null>(body?: B): B extends JSONValue ? string : B;
113
+ /**
114
+ * @param baseURL the base path to the API
115
+ * @returns a function that receives a path and an object of query parameters and returns a URL
116
+ */
117
+ declare function makeGetApiURL<T extends string | URL>(baseURL: T): (path: string, searchParams?: SearchParams) => T;
118
+ /**
119
+ * It merges multiple HeadersInit objects into a single Headers object
120
+ * @param entries Any number of HeadersInit objects
121
+ * @returns a new Headers object with the merged headers
122
+ */
123
+ declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
124
+ /**
125
+ *
126
+ * @param url the url string or URL object to replace the params
127
+ * @param params the params map to be replaced in the url
128
+ * @returns the url with the params replaced and with the same type as the given url
129
+ */
130
+ declare function replaceURLParams<T extends string | URL>(url: string | URL, params: EnhancedRequestInit['params']): T;
114
131
 
115
- export { EnhancedRequestInit, HTTPMethod, JSONValue, PathParams, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, addQueryToUrl, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
132
+ export { EnhancedRequestInit, HTTPMethod, JSONValue, PathParams, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToURL, enhancedFetch, ensureStringBody, makeFetcher, makeGetApiURL, makeService, mergeHeaders, replaceURLParams, typedResponse };
package/dist/index.js CHANGED
@@ -20,13 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
- addQueryToInput: () => addQueryToInput,
24
- addQueryToUrl: () => addQueryToUrl,
23
+ addQueryToURL: () => addQueryToURL,
25
24
  enhancedFetch: () => enhancedFetch,
26
25
  ensureStringBody: () => ensureStringBody,
27
- makeGetApiUrl: () => makeGetApiUrl,
26
+ makeFetcher: () => makeFetcher,
27
+ makeGetApiURL: () => makeGetApiURL,
28
28
  makeService: () => makeService,
29
29
  mergeHeaders: () => mergeHeaders,
30
+ replaceURLParams: () => replaceURLParams,
30
31
  typedResponse: () => typedResponse
31
32
  });
32
33
  module.exports = __toCommonJS(src_exports);
@@ -39,15 +40,14 @@ var HTTP_METHODS = [
39
40
  "DELETE",
40
41
  "PATCH",
41
42
  "OPTIONS",
42
- "HEAD"
43
+ "HEAD",
44
+ "CONNECT"
45
+ // 'TRACE', it has no support in most browsers yet
43
46
  ];
44
47
 
45
48
  // src/internals.ts
46
49
  function getJson(response) {
47
50
  return async (schema) => {
48
- if (!response.ok) {
49
- throw new Error(await response.text());
50
- }
51
51
  const json = await response.json();
52
52
  return schema ? schema.parse(json) : json;
53
53
  };
@@ -58,32 +58,12 @@ function getText(response) {
58
58
  return schema ? schema.parse(text) : text;
59
59
  };
60
60
  }
61
- function replaceUrlParams(url, params) {
62
- if (!params)
63
- return url;
64
- let urlString = String(url);
65
- Object.entries(params).forEach(([key, value]) => {
66
- urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
67
- });
68
- return url instanceof URL ? new URL(urlString) : urlString;
61
+ function typeOf(t) {
62
+ return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
69
63
  }
70
64
 
71
- // src/make-service.ts
72
- function mergeHeaders(...entries) {
73
- const result = /* @__PURE__ */ new Map();
74
- for (const entry of entries) {
75
- const headers = new Headers(entry);
76
- for (const [key, value] of headers.entries()) {
77
- if (value === void 0 || value === "undefined") {
78
- result.delete(key);
79
- } else {
80
- result.set(key, value);
81
- }
82
- }
83
- }
84
- return new Headers(Array.from(result.entries()));
85
- }
86
- function addQueryToUrl(url, searchParams) {
65
+ // src/primitives.ts
66
+ function addQueryToURL(url, searchParams) {
87
67
  if (!searchParams)
88
68
  return url;
89
69
  if (typeof url === "string") {
@@ -99,14 +79,45 @@ function addQueryToUrl(url, searchParams) {
99
79
  }
100
80
  return url;
101
81
  }
102
- var addQueryToInput = addQueryToUrl;
103
- function makeGetApiUrl(baseURL) {
82
+ function ensureStringBody(body) {
83
+ if (typeof body === "undefined")
84
+ return body;
85
+ if (typeof body === "string")
86
+ return body;
87
+ return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
88
+ }
89
+ function makeGetApiURL(baseURL) {
104
90
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
105
91
  return (path, searchParams) => {
106
- const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
107
- return addQueryToUrl(url, searchParams);
92
+ const url = `${base}/${path}`.replace(/([^https?:]\/)\/+/g, "$1");
93
+ return addQueryToURL(url, searchParams);
108
94
  };
109
95
  }
96
+ function mergeHeaders(...entries) {
97
+ const result = /* @__PURE__ */ new Map();
98
+ for (const entry of entries) {
99
+ const headers = new Headers(entry);
100
+ for (const [key, value] of headers.entries()) {
101
+ if (value === void 0 || value === "undefined") {
102
+ result.delete(key);
103
+ } else {
104
+ result.set(key, value);
105
+ }
106
+ }
107
+ }
108
+ return new Headers(Array.from(result.entries()));
109
+ }
110
+ function replaceURLParams(url, params) {
111
+ if (!params)
112
+ return url;
113
+ let urlString = String(url);
114
+ Object.entries(params).forEach(([key, value]) => {
115
+ urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
116
+ });
117
+ return url instanceof URL ? new URL(urlString) : urlString;
118
+ }
119
+
120
+ // src/api.ts
110
121
  function typedResponse(response) {
111
122
  return new Proxy(response, {
112
123
  get(target, prop) {
@@ -118,13 +129,6 @@ function typedResponse(response) {
118
129
  }
119
130
  });
120
131
  }
121
- function ensureStringBody(body) {
122
- if (typeof body === "undefined")
123
- return;
124
- if (typeof body === "string")
125
- return body;
126
- return JSON.stringify(body);
127
- }
128
132
  async function enhancedFetch(url, requestInit) {
129
133
  var _a, _b;
130
134
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
@@ -134,42 +138,49 @@ async function enhancedFetch(url, requestInit) {
134
138
  },
135
139
  (_a = reqInit.headers) != null ? _a : {}
136
140
  );
137
- const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
138
- const fullUrl = addQueryToUrl(withParams, query);
141
+ const withParams = replaceURLParams(url, (_b = reqInit.params) != null ? _b : {});
142
+ const fullURL = addQueryToURL(withParams, query);
139
143
  const body = ensureStringBody(reqInit.body);
140
144
  const enhancedReqInit = { ...reqInit, headers, body };
141
- trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
142
- const response = await fetch(fullUrl, enhancedReqInit);
145
+ trace == null ? void 0 : trace(fullURL, enhancedReqInit);
146
+ const response = await fetch(fullURL, enhancedReqInit);
143
147
  return typedResponse(response);
144
148
  }
145
- function makeService(baseURL, baseHeaders) {
146
- const service = (method) => {
147
- return async (path, requestInit = {}) => {
148
- var _a;
149
- const url = makeGetApiUrl(baseURL)(path);
150
- const response = await enhancedFetch(url, {
151
- ...requestInit,
152
- method,
153
- headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
154
- });
155
- return response;
156
- };
149
+ function makeFetcher(baseURL, baseHeaders) {
150
+ return async (path, requestInit = {}) => {
151
+ var _a;
152
+ const url = makeGetApiURL(baseURL)(path);
153
+ const response = await enhancedFetch(url, {
154
+ ...requestInit,
155
+ headers: mergeHeaders(
156
+ typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
157
+ (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
158
+ )
159
+ });
160
+ return response;
157
161
  };
158
- let api = {};
162
+ }
163
+ function makeService(baseURL, baseHeaders) {
164
+ const fetcher = makeFetcher(baseURL, baseHeaders);
165
+ function appliedService(method) {
166
+ return async (path, requestInit = {}) => fetcher(path, { ...requestInit, method });
167
+ }
168
+ let service = {};
159
169
  for (const method of HTTP_METHODS) {
160
170
  const lowerMethod = method.toLowerCase();
161
- api[lowerMethod] = service(method);
171
+ service[lowerMethod] = appliedService(method);
162
172
  }
163
- return api;
173
+ return service;
164
174
  }
165
175
  // Annotate the CommonJS export names for ESM import in node:
166
176
  0 && (module.exports = {
167
- addQueryToInput,
168
- addQueryToUrl,
177
+ addQueryToURL,
169
178
  enhancedFetch,
170
179
  ensureStringBody,
171
- makeGetApiUrl,
180
+ makeFetcher,
181
+ makeGetApiURL,
172
182
  makeService,
173
183
  mergeHeaders,
184
+ replaceURLParams,
174
185
  typedResponse
175
186
  });
package/dist/index.mjs CHANGED
@@ -6,15 +6,14 @@ var HTTP_METHODS = [
6
6
  "DELETE",
7
7
  "PATCH",
8
8
  "OPTIONS",
9
- "HEAD"
9
+ "HEAD",
10
+ "CONNECT"
11
+ // 'TRACE', it has no support in most browsers yet
10
12
  ];
11
13
 
12
14
  // src/internals.ts
13
15
  function getJson(response) {
14
16
  return async (schema) => {
15
- if (!response.ok) {
16
- throw new Error(await response.text());
17
- }
18
17
  const json = await response.json();
19
18
  return schema ? schema.parse(json) : json;
20
19
  };
@@ -25,32 +24,12 @@ function getText(response) {
25
24
  return schema ? schema.parse(text) : text;
26
25
  };
27
26
  }
28
- function replaceUrlParams(url, params) {
29
- if (!params)
30
- return url;
31
- let urlString = String(url);
32
- Object.entries(params).forEach(([key, value]) => {
33
- urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
34
- });
35
- return url instanceof URL ? new URL(urlString) : urlString;
27
+ function typeOf(t) {
28
+ return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
36
29
  }
37
30
 
38
- // src/make-service.ts
39
- function mergeHeaders(...entries) {
40
- const result = /* @__PURE__ */ new Map();
41
- for (const entry of entries) {
42
- const headers = new Headers(entry);
43
- for (const [key, value] of headers.entries()) {
44
- if (value === void 0 || value === "undefined") {
45
- result.delete(key);
46
- } else {
47
- result.set(key, value);
48
- }
49
- }
50
- }
51
- return new Headers(Array.from(result.entries()));
52
- }
53
- function addQueryToUrl(url, searchParams) {
31
+ // src/primitives.ts
32
+ function addQueryToURL(url, searchParams) {
54
33
  if (!searchParams)
55
34
  return url;
56
35
  if (typeof url === "string") {
@@ -66,14 +45,45 @@ function addQueryToUrl(url, searchParams) {
66
45
  }
67
46
  return url;
68
47
  }
69
- var addQueryToInput = addQueryToUrl;
70
- function makeGetApiUrl(baseURL) {
48
+ function ensureStringBody(body) {
49
+ if (typeof body === "undefined")
50
+ return body;
51
+ if (typeof body === "string")
52
+ return body;
53
+ return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
54
+ }
55
+ function makeGetApiURL(baseURL) {
71
56
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
72
57
  return (path, searchParams) => {
73
- const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
74
- return addQueryToUrl(url, searchParams);
58
+ const url = `${base}/${path}`.replace(/([^https?:]\/)\/+/g, "$1");
59
+ return addQueryToURL(url, searchParams);
75
60
  };
76
61
  }
62
+ function mergeHeaders(...entries) {
63
+ const result = /* @__PURE__ */ new Map();
64
+ for (const entry of entries) {
65
+ const headers = new Headers(entry);
66
+ for (const [key, value] of headers.entries()) {
67
+ if (value === void 0 || value === "undefined") {
68
+ result.delete(key);
69
+ } else {
70
+ result.set(key, value);
71
+ }
72
+ }
73
+ }
74
+ return new Headers(Array.from(result.entries()));
75
+ }
76
+ function replaceURLParams(url, params) {
77
+ if (!params)
78
+ return url;
79
+ let urlString = String(url);
80
+ Object.entries(params).forEach(([key, value]) => {
81
+ urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
82
+ });
83
+ return url instanceof URL ? new URL(urlString) : urlString;
84
+ }
85
+
86
+ // src/api.ts
77
87
  function typedResponse(response) {
78
88
  return new Proxy(response, {
79
89
  get(target, prop) {
@@ -85,13 +95,6 @@ function typedResponse(response) {
85
95
  }
86
96
  });
87
97
  }
88
- function ensureStringBody(body) {
89
- if (typeof body === "undefined")
90
- return;
91
- if (typeof body === "string")
92
- return body;
93
- return JSON.stringify(body);
94
- }
95
98
  async function enhancedFetch(url, requestInit) {
96
99
  var _a, _b;
97
100
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
@@ -101,41 +104,48 @@ async function enhancedFetch(url, requestInit) {
101
104
  },
102
105
  (_a = reqInit.headers) != null ? _a : {}
103
106
  );
104
- const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
105
- const fullUrl = addQueryToUrl(withParams, query);
107
+ const withParams = replaceURLParams(url, (_b = reqInit.params) != null ? _b : {});
108
+ const fullURL = addQueryToURL(withParams, query);
106
109
  const body = ensureStringBody(reqInit.body);
107
110
  const enhancedReqInit = { ...reqInit, headers, body };
108
- trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
109
- const response = await fetch(fullUrl, enhancedReqInit);
111
+ trace == null ? void 0 : trace(fullURL, enhancedReqInit);
112
+ const response = await fetch(fullURL, enhancedReqInit);
110
113
  return typedResponse(response);
111
114
  }
112
- function makeService(baseURL, baseHeaders) {
113
- const service = (method) => {
114
- return async (path, requestInit = {}) => {
115
- var _a;
116
- const url = makeGetApiUrl(baseURL)(path);
117
- const response = await enhancedFetch(url, {
118
- ...requestInit,
119
- method,
120
- headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
121
- });
122
- return response;
123
- };
115
+ function makeFetcher(baseURL, baseHeaders) {
116
+ return async (path, requestInit = {}) => {
117
+ var _a;
118
+ const url = makeGetApiURL(baseURL)(path);
119
+ const response = await enhancedFetch(url, {
120
+ ...requestInit,
121
+ headers: mergeHeaders(
122
+ typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
123
+ (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
124
+ )
125
+ });
126
+ return response;
124
127
  };
125
- let api = {};
128
+ }
129
+ function makeService(baseURL, baseHeaders) {
130
+ const fetcher = makeFetcher(baseURL, baseHeaders);
131
+ function appliedService(method) {
132
+ return async (path, requestInit = {}) => fetcher(path, { ...requestInit, method });
133
+ }
134
+ let service = {};
126
135
  for (const method of HTTP_METHODS) {
127
136
  const lowerMethod = method.toLowerCase();
128
- api[lowerMethod] = service(method);
137
+ service[lowerMethod] = appliedService(method);
129
138
  }
130
- return api;
139
+ return service;
131
140
  }
132
141
  export {
133
- addQueryToInput,
134
- addQueryToUrl,
142
+ addQueryToURL,
135
143
  enhancedFetch,
136
144
  ensureStringBody,
137
- makeGetApiUrl,
145
+ makeFetcher,
146
+ makeGetApiURL,
138
147
  makeService,
139
148
  mergeHeaders,
149
+ replaceURLParams,
140
150
  typedResponse
141
151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "0.2.0",
3
+ "version": "1.0.0-next.2",
4
4
  "description": "Some utilities to extend the 'fetch' API to better interact with external APIs.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",