make-service 1.0.0-next.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,16 +15,42 @@ It adds a set of little features and allows you to parse responses with [zod](ht
15
15
  ## Example
16
16
 
17
17
  ```ts
18
- const api = makeService("https://example.com/api", {
18
+ const service = makeService("https://example.com/api", {
19
19
  Authorization: "Bearer 123",
20
20
  });
21
21
 
22
- const response = await api.get("/users")
22
+ const response = await service.get("/users")
23
23
  const users = await response.json(usersSchema);
24
24
  // ^? User[]
25
25
  ```
26
26
 
27
- ## 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
28
54
 
29
55
  ```sh
30
56
  npm install make-service
@@ -39,25 +65,24 @@ import { makeService } from "https://deno.land/x/make_service/mod.ts";
39
65
 
40
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.
41
67
 
42
- # makeService
43
-
68
+ ## makeService
44
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.
45
70
 
46
- 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).
47
72
 
48
73
  ```ts
49
74
  import { makeService } from 'make-service'
50
75
 
51
- const api = makeService("https://example.com/api", {
76
+ const service = makeService("https://example.com/api", {
52
77
  authorization: "Bearer 123"
53
78
  })
54
79
 
55
- const response = await api.get("/users")
80
+ const response = await service.get("/users")
56
81
  const json = await response.json()
57
82
  // ^? unknown
58
83
  ```
59
84
 
60
- 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:
61
86
 
62
87
  ```ts
63
88
  // "https://example.com/api/users"
@@ -70,15 +95,22 @@ On the example above, the `api.get` will call the [`enhancedFetch`](#enhancedfet
70
95
  // }
71
96
  ```
72
97
 
73
- 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
+ ```
74
108
 
109
+ ### Runtime type-checking and parsing the response body
75
110
  Its [`typedResponse`](#typedresponse) can also be parsed with a zod schema. Here follows a little more complex example:
76
111
 
77
112
  ```ts
78
- const response = await api.get("/users", {
79
- query: { search: "John" },
80
- trace: (url, requestInit) => console.log(url, requestInit),
81
- })
113
+ const response = await service.get("/users")
82
114
  const json = await response.json(
83
115
  z.object({
84
116
  data: z.object({
@@ -92,41 +124,64 @@ const json = await response.json(
92
124
  .catch([])
93
125
  )
94
126
  // type of json will be { name: string }[]
95
- // 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
96
130
  ```
131
+ You can transform any `Response` in a `TypedResponse` like that by using the [`typedResponse`](#typedresponse) function.
97
132
 
98
- It accepts more HTTP verbs:
133
+ ### Supported HTTP Verbs
134
+ Other than the `get` it also accepts more HTTP verbs:
99
135
  ```ts
100
- await api.post("/users", { body: { name: "John" } })
101
- await api.put("/users/1", { body: { name: "John" } })
102
- await api.patch("/users/1", { body: { name: "John" } })
103
- await api.delete("/users/1")
104
- await api.head("/users")
105
- 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")
106
143
  ```
107
144
 
108
- 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.
109
148
 
110
149
  ```ts
111
150
  import { makeService } from 'make-service'
112
151
 
113
- const api = makeService(new URL("https://example.com/api"), new Headers({
114
- authorization: "Bearer 123"
152
+ const service = makeService("https://example.com/api", new Headers({
153
+ authorization: "Bearer 123",
154
+ accept: "*/*",
115
155
  }))
116
156
 
117
- const response = await api.get("/users?admin=true", {
157
+ const response = await service.get("/users", {
118
158
  headers: [['accept', 'application/json']],
119
- query: { page: "2" },
120
159
  })
121
160
 
122
- // It will call "https://example.com/api/users?admin=true&page=2"
161
+ // It will call "https://example.com/api/users"
123
162
  // with headers: { authorization: "Bearer 123", accept: "application/json" }
124
163
  ```
125
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
126
181
  In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value:
127
182
  ```ts
128
- const api = makeService("https://example.com/api", { authorization: "Bearer 123" })
129
- const response = await api.get("/users", {
183
+ const service = makeService("https://example.com/api", { authorization: "Bearer 123" })
184
+ const response = await service.get("/users", {
130
185
  headers: new Headers({ authorization: 'undefined', "Content-Type": undefined }),
131
186
  })
132
187
  // headers will be empty.
@@ -136,10 +191,132 @@ Note: Don't forget headers are case insensitive.
136
191
  const headers = new Headers({ 'Content-Type': 'application/json' })
137
192
  Object.fromEntries(headers) // equals to: { 'content-type': 'application/json' }
138
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).
139
316
 
140
317
  ## enhancedFetch
141
318
 
142
- A wrapper around the `fetch` API.
319
+ A wrapper around the `fetch` service.
143
320
  It returns a [`TypedResponse`](#typedresponse) instead of a `Response`.
144
321
 
145
322
  ```ts
@@ -154,7 +331,7 @@ const json = await response.json()
154
331
  // You can pass it a generic or schema to type the result
155
332
  ```
156
333
 
157
- 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 of `params` to replace URL wildcards, 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).
158
335
 
159
336
  This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
160
337
 
@@ -166,7 +343,7 @@ await enhancedFetch("https://example.com/api/users/:role", {
166
343
  body: { some: { object: { as: { body } } } },
167
344
  query: { page: "1" },
168
345
  params: { role: "admin" },
169
- trace: (url, requestInit) => console.log(url, requestInit)
346
+ trace: console.log,
170
347
  })
171
348
 
172
349
  // The trace function will be called with the following arguments:
@@ -207,5 +384,124 @@ const text = await response.text(z.string().email())
207
384
  // ^? string
208
385
  ```
209
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
+
210
506
  ## Thank you
211
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,7 +23,8 @@ type TypedResponse = Omit<Response, 'json' | 'text'> & {
23
23
  json: TypedResponseJson;
24
24
  text: TypedResponseText;
25
25
  };
26
- type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
26
+ type EnhancedRequestInit = Omit<RequestInit, 'body' | 'method'> & {
27
+ method?: HTTPMethod | Lowercase<HTTPMethod>;
27
28
  body?: JSONValue | BodyInit | null;
28
29
  query?: SearchParams;
29
30
  params?: Record<string, string>;
@@ -43,23 +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
- * @param baseURL the base path to the API
60
- * @returns a function that receives a path and an object of query parameters and returns a URL
61
- */
62
- declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchParams?: SearchParams) => string | URL;
63
47
  /**
64
48
  * It hacks the Response object to add typed json and text methods
65
49
  * @param response the Response to be proxied
@@ -75,11 +59,6 @@ declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchPara
75
59
  * // ^? User[]
76
60
  */
77
61
  declare function typedResponse(response: Response): TypedResponse;
78
- /**
79
- * @param body the JSON-like body of the request
80
- * @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.
81
- */
82
- declare function ensureStringBody<B extends JSONValue | BodyInit | null>(body?: B): B extends JSONValue ? string : B;
83
62
  /**
84
63
  *
85
64
  * @param url a string or URL to be fetched
@@ -95,6 +74,18 @@ declare function ensureStringBody<B extends JSONValue | BodyInit | null>(body?:
95
74
  * // ^? unknown
96
75
  */
97
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>;
98
89
  /**
99
90
  *
100
91
  * @param baseURL the base URL to the API
@@ -106,6 +97,36 @@ declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestI
106
97
  * const users = await response.json(userSchema);
107
98
  * // ^? User[]
108
99
  */
109
- declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<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;
110
131
 
111
- export { EnhancedRequestInit, HTTPMethod, JSONValue, PathParams, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, 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,12 +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
- addQueryToUrl: () => addQueryToUrl,
23
+ addQueryToURL: () => addQueryToURL,
24
24
  enhancedFetch: () => enhancedFetch,
25
25
  ensureStringBody: () => ensureStringBody,
26
- makeGetApiUrl: () => makeGetApiUrl,
26
+ makeFetcher: () => makeFetcher,
27
+ makeGetApiURL: () => makeGetApiURL,
27
28
  makeService: () => makeService,
28
29
  mergeHeaders: () => mergeHeaders,
30
+ replaceURLParams: () => replaceURLParams,
29
31
  typedResponse: () => typedResponse
30
32
  });
31
33
  module.exports = __toCommonJS(src_exports);
@@ -38,7 +40,9 @@ var HTTP_METHODS = [
38
40
  "DELETE",
39
41
  "PATCH",
40
42
  "OPTIONS",
41
- "HEAD"
43
+ "HEAD",
44
+ "CONNECT"
45
+ // 'TRACE', it has no support in most browsers yet
42
46
  ];
43
47
 
44
48
  // src/internals.ts
@@ -54,35 +58,12 @@ function getText(response) {
54
58
  return schema ? schema.parse(text) : text;
55
59
  };
56
60
  }
57
- function replaceUrlParams(url, params) {
58
- if (!params)
59
- return url;
60
- let urlString = String(url);
61
- Object.entries(params).forEach(([key, value]) => {
62
- urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
63
- });
64
- return url instanceof URL ? new URL(urlString) : urlString;
65
- }
66
61
  function typeOf(t) {
67
62
  return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
68
63
  }
69
64
 
70
- // src/make-service.ts
71
- function mergeHeaders(...entries) {
72
- const result = /* @__PURE__ */ new Map();
73
- for (const entry of entries) {
74
- const headers = new Headers(entry);
75
- for (const [key, value] of headers.entries()) {
76
- if (value === void 0 || value === "undefined") {
77
- result.delete(key);
78
- } else {
79
- result.set(key, value);
80
- }
81
- }
82
- }
83
- return new Headers(Array.from(result.entries()));
84
- }
85
- function addQueryToUrl(url, searchParams) {
65
+ // src/primitives.ts
66
+ function addQueryToURL(url, searchParams) {
86
67
  if (!searchParams)
87
68
  return url;
88
69
  if (typeof url === "string") {
@@ -98,13 +79,45 @@ function addQueryToUrl(url, searchParams) {
98
79
  }
99
80
  return url;
100
81
  }
101
- 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) {
102
90
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
103
91
  return (path, searchParams) => {
104
- const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
105
- return addQueryToUrl(url, searchParams);
92
+ const url = `${base}/${path}`.replace(/([^https?:]\/)\/+/g, "$1");
93
+ return addQueryToURL(url, searchParams);
106
94
  };
107
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
108
121
  function typedResponse(response) {
109
122
  return new Proxy(response, {
110
123
  get(target, prop) {
@@ -116,13 +129,6 @@ function typedResponse(response) {
116
129
  }
117
130
  });
118
131
  }
119
- function ensureStringBody(body) {
120
- if (typeof body === "undefined")
121
- return body;
122
- if (typeof body === "string")
123
- return body;
124
- return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
125
- }
126
132
  async function enhancedFetch(url, requestInit) {
127
133
  var _a, _b;
128
134
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
@@ -132,44 +138,49 @@ async function enhancedFetch(url, requestInit) {
132
138
  },
133
139
  (_a = reqInit.headers) != null ? _a : {}
134
140
  );
135
- const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
136
- const fullUrl = addQueryToUrl(withParams, query);
141
+ const withParams = replaceURLParams(url, (_b = reqInit.params) != null ? _b : {});
142
+ const fullURL = addQueryToURL(withParams, query);
137
143
  const body = ensureStringBody(reqInit.body);
138
144
  const enhancedReqInit = { ...reqInit, headers, body };
139
- trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
140
- const response = await fetch(fullUrl, enhancedReqInit);
145
+ trace == null ? void 0 : trace(fullURL, enhancedReqInit);
146
+ const response = await fetch(fullURL, enhancedReqInit);
141
147
  return typedResponse(response);
142
148
  }
143
- function makeService(baseURL, baseHeaders) {
144
- const service = (method) => {
145
- return async (path, requestInit = {}) => {
146
- var _a;
147
- const url = makeGetApiUrl(baseURL)(path);
148
- const response = await enhancedFetch(url, {
149
- ...requestInit,
150
- method,
151
- headers: mergeHeaders(
152
- typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
153
- (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
154
- )
155
- });
156
- return response;
157
- };
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;
158
161
  };
159
- 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 = {};
160
169
  for (const method of HTTP_METHODS) {
161
170
  const lowerMethod = method.toLowerCase();
162
- api[lowerMethod] = service(method);
171
+ service[lowerMethod] = appliedService(method);
163
172
  }
164
- return api;
173
+ return service;
165
174
  }
166
175
  // Annotate the CommonJS export names for ESM import in node:
167
176
  0 && (module.exports = {
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,7 +6,9 @@ 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
@@ -22,35 +24,12 @@ function getText(response) {
22
24
  return schema ? schema.parse(text) : text;
23
25
  };
24
26
  }
25
- function replaceUrlParams(url, params) {
26
- if (!params)
27
- return url;
28
- let urlString = String(url);
29
- Object.entries(params).forEach(([key, value]) => {
30
- urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
31
- });
32
- return url instanceof URL ? new URL(urlString) : urlString;
33
- }
34
27
  function typeOf(t) {
35
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,13 +45,45 @@ function addQueryToUrl(url, searchParams) {
66
45
  }
67
46
  return url;
68
47
  }
69
- 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) {
70
56
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
71
57
  return (path, searchParams) => {
72
- const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
73
- return addQueryToUrl(url, searchParams);
58
+ const url = `${base}/${path}`.replace(/([^https?:]\/)\/+/g, "$1");
59
+ return addQueryToURL(url, searchParams);
74
60
  };
75
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
76
87
  function typedResponse(response) {
77
88
  return new Proxy(response, {
78
89
  get(target, prop) {
@@ -84,13 +95,6 @@ function typedResponse(response) {
84
95
  }
85
96
  });
86
97
  }
87
- function ensureStringBody(body) {
88
- if (typeof body === "undefined")
89
- return body;
90
- if (typeof body === "string")
91
- return body;
92
- return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
93
- }
94
98
  async function enhancedFetch(url, requestInit) {
95
99
  var _a, _b;
96
100
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
@@ -100,43 +104,48 @@ async function enhancedFetch(url, requestInit) {
100
104
  },
101
105
  (_a = reqInit.headers) != null ? _a : {}
102
106
  );
103
- const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
104
- const fullUrl = addQueryToUrl(withParams, query);
107
+ const withParams = replaceURLParams(url, (_b = reqInit.params) != null ? _b : {});
108
+ const fullURL = addQueryToURL(withParams, query);
105
109
  const body = ensureStringBody(reqInit.body);
106
110
  const enhancedReqInit = { ...reqInit, headers, body };
107
- trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
108
- const response = await fetch(fullUrl, enhancedReqInit);
111
+ trace == null ? void 0 : trace(fullURL, enhancedReqInit);
112
+ const response = await fetch(fullURL, enhancedReqInit);
109
113
  return typedResponse(response);
110
114
  }
111
- function makeService(baseURL, baseHeaders) {
112
- const service = (method) => {
113
- return async (path, requestInit = {}) => {
114
- var _a;
115
- const url = makeGetApiUrl(baseURL)(path);
116
- const response = await enhancedFetch(url, {
117
- ...requestInit,
118
- method,
119
- headers: mergeHeaders(
120
- typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
121
- (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
122
- )
123
- });
124
- return response;
125
- };
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;
126
127
  };
127
- 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 = {};
128
135
  for (const method of HTTP_METHODS) {
129
136
  const lowerMethod = method.toLowerCase();
130
- api[lowerMethod] = service(method);
137
+ service[lowerMethod] = appliedService(method);
131
138
  }
132
- return api;
139
+ return service;
133
140
  }
134
141
  export {
135
- addQueryToUrl,
142
+ addQueryToURL,
136
143
  enhancedFetch,
137
144
  ensureStringBody,
138
- makeGetApiUrl,
145
+ makeFetcher,
146
+ makeGetApiURL,
139
147
  makeService,
140
148
  mergeHeaders,
149
+ replaceURLParams,
141
150
  typedResponse
142
151
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "1.0.0-next.0",
3
+ "version": "1.0.0",
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",