make-service 0.0.3 → 0.2.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
@@ -76,7 +76,7 @@ Its [`typedResponse`](#typedresponse) can also be parsed with a zod schema. Here
76
76
  ```ts
77
77
  const response = await api.get("/users", {
78
78
  query: { search: "John" },
79
- trace: (input, requestInit) => console.log(input, requestInit),
79
+ trace: (url, requestInit) => console.log(url, requestInit),
80
80
  })
81
81
  const json = await response.json(
82
82
  z.object({
@@ -104,6 +104,38 @@ await api.head("/users")
104
104
  await api.options("/users")
105
105
  ```
106
106
 
107
+ This function can also correctly merge any sort of `URL`, `URLSearchParams`, and `Headers`.
108
+
109
+ ```ts
110
+ import { makeService } from 'make-service'
111
+
112
+ const api = makeService(new URL("https://example.com/api"), new Headers({
113
+ authorization: "Bearer 123"
114
+ }))
115
+
116
+ const response = await api.get("/users?admin=true", {
117
+ headers: [['accept', 'application/json']],
118
+ query: { page: "2" },
119
+ })
120
+
121
+ // It will call "https://example.com/api/users?admin=true&page=2"
122
+ // with headers: { authorization: "Bearer 123", accept: "application/json" }
123
+ ```
124
+
125
+ In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value:
126
+ ```ts
127
+ const api = makeService("https://example.com/api", { authorization: "Bearer 123" })
128
+ const response = await api.get("/users", {
129
+ headers: new Headers({ authorization: 'undefined', "Content-Type": undefined }),
130
+ })
131
+ // headers will be empty.
132
+ ```
133
+ Note: Don't forget headers are case insensitive.
134
+ ```ts
135
+ const headers = new Headers({ 'Content-Type': 'application/json' })
136
+ Object.fromEntries(headers) // equals to: { 'content-type': 'application/json' }
137
+ ```
138
+
107
139
  ## enhancedFetch
108
140
 
109
141
  A wrapper around the `fetch` API.
@@ -121,7 +153,7 @@ const json = await response.json()
121
153
  // You can pass it a generic or schema to type the result
122
154
  ```
123
155
 
124
- 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 `input` and `requestInit` arguments.
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.
125
157
 
126
158
  This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
127
159
 
@@ -132,7 +164,7 @@ await enhancedFetch("https://example.com/api/users", {
132
164
  method: 'POST',
133
165
  body: { some: { object: { as: { body } } } },
134
166
  query: { page: "1" },
135
- trace: (input, requestInit) => console.log(input, requestInit)
167
+ trace: (url, requestInit) => console.log(url, requestInit)
136
168
  })
137
169
 
138
170
  // The trace function will be called with the following arguments:
@@ -155,21 +187,21 @@ import { typedResponse } from 'make-service'
155
187
  import type { TypedResponse } from 'make-service'
156
188
 
157
189
  // With JSON
158
- const response: TypedResponse = new Response(JSON.stringify({ foo: "bar" }))
159
- const json = await typedResponse(response).json()
190
+ const response: TypedResponse = typedResponse(new Response(JSON.stringify({ foo: "bar" })))
191
+ const json = await response.json()
160
192
  // ^? unknown
161
- const json = await typedResponse(response).json<{ foo: string }>()
193
+ const json = await response.json<{ foo: string }>()
162
194
  // ^? { foo: string }
163
- const json = await typedResponse(response).json(z.object({ foo: z.string() }))
195
+ const json = await response.json(z.object({ foo: z.string() }))
164
196
  // ^? { foo: string }
165
197
 
166
198
  // With text
167
- const response: TypedResponse = new Response("foo")
168
- const text = await typedResponse(response).text()
199
+ const response: TypedResponse = typedResponse(new Response("foo"))
200
+ const text = await response.text()
169
201
  // ^? string
170
- const text = await typedResponse(response).text<`foo${string}`>()
202
+ const text = await response.text<`foo${string}`>()
171
203
  // ^? `foo${string}`
172
- const text = await typedResponse(response).text(z.string().email())
204
+ const text = await response.text(z.string().email())
173
205
  // ^? string
174
206
  ```
175
207
 
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"];
2
2
 
3
3
  /**
4
4
  * It returns the JSON object or throws an error if the response is not ok.
@@ -26,24 +26,44 @@ type TypedResponse = Omit<Response, 'json' | 'text'> & {
26
26
  type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
27
27
  body?: JSONValue;
28
28
  query?: SearchParams;
29
+ params?: Record<string, string>;
29
30
  trace?: (...args: Parameters<typeof fetch>) => void;
30
31
  };
31
32
  type ServiceRequestInit = Omit<EnhancedRequestInit, 'method'>;
32
33
  type HTTPMethod = (typeof HTTP_METHODS)[number];
33
34
  type TypedResponseJson = ReturnType<typeof getJson>;
34
35
  type TypedResponseText = ReturnType<typeof getText>;
36
+ type Prettify<T> = {
37
+ [K in keyof T]: T[K];
38
+ } & {};
39
+ type NoEmpty<T> = keyof T extends never ? never : T;
40
+ type PathParams<T extends string> = NoEmpty<T extends `${infer _}:${infer Param}/${infer Rest}` ? Prettify<{
41
+ [K in Param]: string;
42
+ } & PathParams<Rest>> : T extends `${infer _}:${infer Param}` ? {
43
+ [K in Param]: string;
44
+ } : {}>;
35
45
 
36
46
  /**
37
- * @param input a string or URL to which the query parameters will be added
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
38
54
  * @param searchParams the query parameters
39
- * @returns the input with the query parameters added with the same type as the input
55
+ * @returns the url with the query parameters added with the same type as the url
40
56
  */
41
- declare function addQueryToInput(input: string | URL, searchParams?: SearchParams): string | URL;
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;
42
62
  /**
43
63
  * @param baseURL the base path to the API
44
64
  * @returns a function that receives a path and an object of query parameters and returns a URL
45
65
  */
46
- declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: SearchParams) => string | URL;
66
+ declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchParams?: SearchParams) => string | URL;
47
67
  /**
48
68
  * It hacks the Response object to add typed json and text methods
49
69
  * @param response the Response to be proxied
@@ -66,7 +86,7 @@ declare function typedResponse(response: Response): TypedResponse;
66
86
  declare function ensureStringBody(body?: JSONValue): string | undefined;
67
87
  /**
68
88
  *
69
- * @param input a string or URL to be fetched
89
+ * @param url a string or URL to be fetched
70
90
  * @param requestInit the requestInit 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.
71
91
  * @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object
72
92
  * @param requestInit.query the query parameters to be added to the URL
@@ -78,7 +98,7 @@ declare function ensureStringBody(body?: JSONValue): string | undefined;
78
98
  * const untyped = await response.json();
79
99
  * // ^? unknown
80
100
  */
81
- declare function enhancedFetch(input: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
101
+ declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
82
102
  /**
83
103
  *
84
104
  * @param baseURL the base URL to the API
@@ -90,14 +110,6 @@ declare function enhancedFetch(input: string | URL, requestInit?: EnhancedReques
90
110
  * const users = await response.json(userSchema);
91
111
  * // ^? User[]
92
112
  */
93
- declare function makeService(baseURL: string, baseHeaders?: HeadersInit): {
94
- get: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
95
- post: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
96
- put: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
97
- delete: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
98
- patch: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
99
- options: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
100
- head: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
101
- };
113
+ declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): Record<"delete" | "get" | "post" | "put" | "patch" | "options" | "head", (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>>;
102
114
 
103
- export { EnhancedRequestInit, HTTPMethod, JSONValue, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, typedResponse };
115
+ export { EnhancedRequestInit, HTTPMethod, JSONValue, PathParams, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, addQueryToUrl, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
package/dist/index.js CHANGED
@@ -21,23 +21,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  addQueryToInput: () => addQueryToInput,
24
+ addQueryToUrl: () => addQueryToUrl,
24
25
  enhancedFetch: () => enhancedFetch,
25
26
  ensureStringBody: () => ensureStringBody,
26
27
  makeGetApiUrl: () => makeGetApiUrl,
27
28
  makeService: () => makeService,
29
+ mergeHeaders: () => mergeHeaders,
28
30
  typedResponse: () => typedResponse
29
31
  });
30
32
  module.exports = __toCommonJS(src_exports);
31
33
 
32
34
  // src/constants.ts
33
35
  var HTTP_METHODS = [
34
- "get",
35
- "post",
36
- "put",
37
- "delete",
38
- "patch",
39
- "options",
40
- "head"
36
+ "GET",
37
+ "POST",
38
+ "PUT",
39
+ "DELETE",
40
+ "PATCH",
41
+ "OPTIONS",
42
+ "HEAD"
41
43
  ];
42
44
 
43
45
  // src/internals.ts
@@ -56,25 +58,54 @@ function getText(response) {
56
58
  return schema ? schema.parse(text) : text;
57
59
  };
58
60
  }
59
- function isHTTPMethod(method) {
60
- return HTTP_METHODS.includes(method);
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
69
  }
62
70
 
63
71
  // src/make-service.ts
64
- function addQueryToInput(input, searchParams) {
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
87
  if (!searchParams)
66
- return input;
67
- if (searchParams && typeof input === "string") {
68
- const separator = input.includes("?") ? "&" : "?";
69
- return `${input}${separator}${new URLSearchParams(searchParams)}`;
88
+ return url;
89
+ if (typeof url === "string") {
90
+ const separator = url.includes("?") ? "&" : "?";
91
+ return `${url}${separator}${new URLSearchParams(searchParams)}`;
70
92
  }
71
- if (searchParams && input instanceof URL) {
72
- input.search = new URLSearchParams(searchParams).toString();
93
+ if (searchParams && url instanceof URL) {
94
+ for (const [key, value] of Object.entries(
95
+ new URLSearchParams(searchParams)
96
+ )) {
97
+ url.searchParams.set(key, value);
98
+ }
73
99
  }
74
- return input;
100
+ return url;
75
101
  }
102
+ var addQueryToInput = addQueryToUrl;
76
103
  function makeGetApiUrl(baseURL) {
77
- return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
104
+ const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
105
+ return (path, searchParams) => {
106
+ const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
107
+ return addQueryToUrl(url, searchParams);
108
+ };
78
109
  }
79
110
  function typedResponse(response) {
80
111
  return new Proxy(response, {
@@ -94,42 +125,51 @@ function ensureStringBody(body) {
94
125
  return body;
95
126
  return JSON.stringify(body);
96
127
  }
97
- async function enhancedFetch(input, requestInit) {
128
+ async function enhancedFetch(url, requestInit) {
129
+ var _a, _b;
98
130
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
99
- const headers = { "content-type": "application/json", ...reqInit.headers };
100
- const url = addQueryToInput(input, query);
131
+ const headers = mergeHeaders(
132
+ {
133
+ "content-type": "application/json"
134
+ },
135
+ (_a = reqInit.headers) != null ? _a : {}
136
+ );
137
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
138
+ const fullUrl = addQueryToUrl(withParams, query);
101
139
  const body = ensureStringBody(reqInit.body);
102
140
  const enhancedReqInit = { ...reqInit, headers, body };
103
- trace == null ? void 0 : trace(url, enhancedReqInit);
104
- const request = new Request(url, enhancedReqInit);
105
- const response = await fetch(request);
141
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
142
+ const response = await fetch(fullUrl, enhancedReqInit);
106
143
  return typedResponse(response);
107
144
  }
108
145
  function makeService(baseURL, baseHeaders) {
109
146
  const service = (method) => {
110
147
  return async (path, requestInit = {}) => {
111
- const response = await enhancedFetch(`${baseURL}${path}`, {
148
+ var _a;
149
+ const url = makeGetApiUrl(baseURL)(path);
150
+ const response = await enhancedFetch(url, {
112
151
  ...requestInit,
113
152
  method,
114
- headers: { ...baseHeaders, ...requestInit == null ? void 0 : requestInit.headers }
153
+ headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
115
154
  });
116
155
  return response;
117
156
  };
118
157
  };
119
- return new Proxy({}, {
120
- get(_target, prop) {
121
- if (isHTTPMethod(prop))
122
- return service(prop);
123
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
124
- }
125
- });
158
+ let api = {};
159
+ for (const method of HTTP_METHODS) {
160
+ const lowerMethod = method.toLowerCase();
161
+ api[lowerMethod] = service(method);
162
+ }
163
+ return api;
126
164
  }
127
165
  // Annotate the CommonJS export names for ESM import in node:
128
166
  0 && (module.exports = {
129
167
  addQueryToInput,
168
+ addQueryToUrl,
130
169
  enhancedFetch,
131
170
  ensureStringBody,
132
171
  makeGetApiUrl,
133
172
  makeService,
173
+ mergeHeaders,
134
174
  typedResponse
135
175
  });
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/constants.ts
2
2
  var HTTP_METHODS = [
3
- "get",
4
- "post",
5
- "put",
6
- "delete",
7
- "patch",
8
- "options",
9
- "head"
3
+ "GET",
4
+ "POST",
5
+ "PUT",
6
+ "DELETE",
7
+ "PATCH",
8
+ "OPTIONS",
9
+ "HEAD"
10
10
  ];
11
11
 
12
12
  // src/internals.ts
@@ -25,25 +25,54 @@ function getText(response) {
25
25
  return schema ? schema.parse(text) : text;
26
26
  };
27
27
  }
28
- function isHTTPMethod(method) {
29
- return HTTP_METHODS.includes(method);
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;
30
36
  }
31
37
 
32
38
  // src/make-service.ts
33
- function addQueryToInput(input, searchParams) {
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) {
34
54
  if (!searchParams)
35
- return input;
36
- if (searchParams && typeof input === "string") {
37
- const separator = input.includes("?") ? "&" : "?";
38
- return `${input}${separator}${new URLSearchParams(searchParams)}`;
55
+ return url;
56
+ if (typeof url === "string") {
57
+ const separator = url.includes("?") ? "&" : "?";
58
+ return `${url}${separator}${new URLSearchParams(searchParams)}`;
39
59
  }
40
- if (searchParams && input instanceof URL) {
41
- input.search = new URLSearchParams(searchParams).toString();
60
+ if (searchParams && url instanceof URL) {
61
+ for (const [key, value] of Object.entries(
62
+ new URLSearchParams(searchParams)
63
+ )) {
64
+ url.searchParams.set(key, value);
65
+ }
42
66
  }
43
- return input;
67
+ return url;
44
68
  }
69
+ var addQueryToInput = addQueryToUrl;
45
70
  function makeGetApiUrl(baseURL) {
46
- return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
71
+ const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
72
+ return (path, searchParams) => {
73
+ const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
74
+ return addQueryToUrl(url, searchParams);
75
+ };
47
76
  }
48
77
  function typedResponse(response) {
49
78
  return new Proxy(response, {
@@ -63,41 +92,50 @@ function ensureStringBody(body) {
63
92
  return body;
64
93
  return JSON.stringify(body);
65
94
  }
66
- async function enhancedFetch(input, requestInit) {
95
+ async function enhancedFetch(url, requestInit) {
96
+ var _a, _b;
67
97
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
68
- const headers = { "content-type": "application/json", ...reqInit.headers };
69
- const url = addQueryToInput(input, query);
98
+ const headers = mergeHeaders(
99
+ {
100
+ "content-type": "application/json"
101
+ },
102
+ (_a = reqInit.headers) != null ? _a : {}
103
+ );
104
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
105
+ const fullUrl = addQueryToUrl(withParams, query);
70
106
  const body = ensureStringBody(reqInit.body);
71
107
  const enhancedReqInit = { ...reqInit, headers, body };
72
- trace == null ? void 0 : trace(url, enhancedReqInit);
73
- const request = new Request(url, enhancedReqInit);
74
- const response = await fetch(request);
108
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
109
+ const response = await fetch(fullUrl, enhancedReqInit);
75
110
  return typedResponse(response);
76
111
  }
77
112
  function makeService(baseURL, baseHeaders) {
78
113
  const service = (method) => {
79
114
  return async (path, requestInit = {}) => {
80
- const response = await enhancedFetch(`${baseURL}${path}`, {
115
+ var _a;
116
+ const url = makeGetApiUrl(baseURL)(path);
117
+ const response = await enhancedFetch(url, {
81
118
  ...requestInit,
82
119
  method,
83
- headers: { ...baseHeaders, ...requestInit == null ? void 0 : requestInit.headers }
120
+ headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
84
121
  });
85
122
  return response;
86
123
  };
87
124
  };
88
- return new Proxy({}, {
89
- get(_target, prop) {
90
- if (isHTTPMethod(prop))
91
- return service(prop);
92
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
93
- }
94
- });
125
+ let api = {};
126
+ for (const method of HTTP_METHODS) {
127
+ const lowerMethod = method.toLowerCase();
128
+ api[lowerMethod] = service(method);
129
+ }
130
+ return api;
95
131
  }
96
132
  export {
97
133
  addQueryToInput,
134
+ addQueryToUrl,
98
135
  enhancedFetch,
99
136
  ensureStringBody,
100
137
  makeGetApiUrl,
101
138
  makeService,
139
+ mergeHeaders,
102
140
  typedResponse
103
141
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "0.0.3",
3
+ "version": "0.2.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",