make-service 0.1.0 → 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:
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,12 +26,22 @@ 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
47
  * It merges multiple HeadersInit objects into a single Headers object
@@ -40,11 +50,15 @@ type TypedResponseText = ReturnType<typeof getText>;
40
50
  */
41
51
  declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
42
52
  /**
43
- * @param input a string or URL to which the query parameters will be added
53
+ * @param url a string or URL to which the query parameters will be added
44
54
  * @param searchParams the query parameters
45
- * @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
46
56
  */
47
- 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;
48
62
  /**
49
63
  * @param baseURL the base path to the API
50
64
  * @returns a function that receives a path and an object of query parameters and returns a URL
@@ -72,7 +86,7 @@ declare function typedResponse(response: Response): TypedResponse;
72
86
  declare function ensureStringBody(body?: JSONValue): string | undefined;
73
87
  /**
74
88
  *
75
- * @param input a string or URL to be fetched
89
+ * @param url a string or URL to be fetched
76
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.
77
91
  * @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object
78
92
  * @param requestInit.query the query parameters to be added to the URL
@@ -84,7 +98,7 @@ declare function ensureStringBody(body?: JSONValue): string | undefined;
84
98
  * const untyped = await response.json();
85
99
  * // ^? unknown
86
100
  */
87
- declare function enhancedFetch(input: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
101
+ declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
88
102
  /**
89
103
  *
90
104
  * @param baseURL the base URL to the API
@@ -96,14 +110,6 @@ declare function enhancedFetch(input: string | URL, requestInit?: EnhancedReques
96
110
  * const users = await response.json(userSchema);
97
111
  * // ^? User[]
98
112
  */
99
- declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): {
100
- get: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
101
- post: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
102
- put: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
103
- delete: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
104
- patch: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
105
- options: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
106
- head: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
107
- };
113
+ declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): Record<"delete" | "get" | "post" | "put" | "patch" | "options" | "head", (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>>;
108
114
 
109
- export { EnhancedRequestInit, HTTPMethod, JSONValue, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, 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,6 +21,7 @@ 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,
@@ -32,13 +33,13 @@ module.exports = __toCommonJS(src_exports);
32
33
 
33
34
  // src/constants.ts
34
35
  var HTTP_METHODS = [
35
- "get",
36
- "post",
37
- "put",
38
- "delete",
39
- "patch",
40
- "options",
41
- "head"
36
+ "GET",
37
+ "POST",
38
+ "PUT",
39
+ "DELETE",
40
+ "PATCH",
41
+ "OPTIONS",
42
+ "HEAD"
42
43
  ];
43
44
 
44
45
  // src/internals.ts
@@ -57,8 +58,14 @@ function getText(response) {
57
58
  return schema ? schema.parse(text) : text;
58
59
  };
59
60
  }
60
- function isHTTPMethod(method) {
61
- 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;
62
69
  }
63
70
 
64
71
  // src/make-service.ts
@@ -76,27 +83,28 @@ function mergeHeaders(...entries) {
76
83
  }
77
84
  return new Headers(Array.from(result.entries()));
78
85
  }
79
- function addQueryToInput(input, searchParams) {
86
+ function addQueryToUrl(url, searchParams) {
80
87
  if (!searchParams)
81
- return input;
82
- if (typeof input === "string") {
83
- const separator = input.includes("?") ? "&" : "?";
84
- 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)}`;
85
92
  }
86
- if (searchParams && input instanceof URL) {
93
+ if (searchParams && url instanceof URL) {
87
94
  for (const [key, value] of Object.entries(
88
95
  new URLSearchParams(searchParams)
89
96
  )) {
90
- input.searchParams.set(key, value);
97
+ url.searchParams.set(key, value);
91
98
  }
92
99
  }
93
- return input;
100
+ return url;
94
101
  }
102
+ var addQueryToInput = addQueryToUrl;
95
103
  function makeGetApiUrl(baseURL) {
96
104
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
97
105
  return (path, searchParams) => {
98
106
  const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
99
- return addQueryToInput(url, searchParams);
107
+ return addQueryToUrl(url, searchParams);
100
108
  };
101
109
  }
102
110
  function typedResponse(response) {
@@ -117,8 +125,8 @@ function ensureStringBody(body) {
117
125
  return body;
118
126
  return JSON.stringify(body);
119
127
  }
120
- async function enhancedFetch(input, requestInit) {
121
- var _a;
128
+ async function enhancedFetch(url, requestInit) {
129
+ var _a, _b;
122
130
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
123
131
  const headers = mergeHeaders(
124
132
  {
@@ -126,11 +134,12 @@ async function enhancedFetch(input, requestInit) {
126
134
  },
127
135
  (_a = reqInit.headers) != null ? _a : {}
128
136
  );
129
- const url = addQueryToInput(input, query);
137
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
138
+ const fullUrl = addQueryToUrl(withParams, query);
130
139
  const body = ensureStringBody(reqInit.body);
131
140
  const enhancedReqInit = { ...reqInit, headers, body };
132
- trace == null ? void 0 : trace(url, enhancedReqInit);
133
- const response = await fetch(url, enhancedReqInit);
141
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
142
+ const response = await fetch(fullUrl, enhancedReqInit);
134
143
  return typedResponse(response);
135
144
  }
136
145
  function makeService(baseURL, baseHeaders) {
@@ -146,17 +155,17 @@ function makeService(baseURL, baseHeaders) {
146
155
  return response;
147
156
  };
148
157
  };
149
- return new Proxy({}, {
150
- get(_target, prop) {
151
- if (isHTTPMethod(prop))
152
- return service(prop.toUpperCase());
153
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
154
- }
155
- });
158
+ let api = {};
159
+ for (const method of HTTP_METHODS) {
160
+ const lowerMethod = method.toLowerCase();
161
+ api[lowerMethod] = service(method);
162
+ }
163
+ return api;
156
164
  }
157
165
  // Annotate the CommonJS export names for ESM import in node:
158
166
  0 && (module.exports = {
159
167
  addQueryToInput,
168
+ addQueryToUrl,
160
169
  enhancedFetch,
161
170
  ensureStringBody,
162
171
  makeGetApiUrl,
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,8 +25,14 @@ 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
@@ -44,27 +50,28 @@ function mergeHeaders(...entries) {
44
50
  }
45
51
  return new Headers(Array.from(result.entries()));
46
52
  }
47
- function addQueryToInput(input, searchParams) {
53
+ function addQueryToUrl(url, searchParams) {
48
54
  if (!searchParams)
49
- return input;
50
- if (typeof input === "string") {
51
- const separator = input.includes("?") ? "&" : "?";
52
- 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)}`;
53
59
  }
54
- if (searchParams && input instanceof URL) {
60
+ if (searchParams && url instanceof URL) {
55
61
  for (const [key, value] of Object.entries(
56
62
  new URLSearchParams(searchParams)
57
63
  )) {
58
- input.searchParams.set(key, value);
64
+ url.searchParams.set(key, value);
59
65
  }
60
66
  }
61
- return input;
67
+ return url;
62
68
  }
69
+ var addQueryToInput = addQueryToUrl;
63
70
  function makeGetApiUrl(baseURL) {
64
71
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
65
72
  return (path, searchParams) => {
66
73
  const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
67
- return addQueryToInput(url, searchParams);
74
+ return addQueryToUrl(url, searchParams);
68
75
  };
69
76
  }
70
77
  function typedResponse(response) {
@@ -85,8 +92,8 @@ function ensureStringBody(body) {
85
92
  return body;
86
93
  return JSON.stringify(body);
87
94
  }
88
- async function enhancedFetch(input, requestInit) {
89
- var _a;
95
+ async function enhancedFetch(url, requestInit) {
96
+ var _a, _b;
90
97
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
91
98
  const headers = mergeHeaders(
92
99
  {
@@ -94,11 +101,12 @@ async function enhancedFetch(input, requestInit) {
94
101
  },
95
102
  (_a = reqInit.headers) != null ? _a : {}
96
103
  );
97
- const url = addQueryToInput(input, query);
104
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
105
+ const fullUrl = addQueryToUrl(withParams, query);
98
106
  const body = ensureStringBody(reqInit.body);
99
107
  const enhancedReqInit = { ...reqInit, headers, body };
100
- trace == null ? void 0 : trace(url, enhancedReqInit);
101
- const response = await fetch(url, enhancedReqInit);
108
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
109
+ const response = await fetch(fullUrl, enhancedReqInit);
102
110
  return typedResponse(response);
103
111
  }
104
112
  function makeService(baseURL, baseHeaders) {
@@ -114,16 +122,16 @@ function makeService(baseURL, baseHeaders) {
114
122
  return response;
115
123
  };
116
124
  };
117
- return new Proxy({}, {
118
- get(_target, prop) {
119
- if (isHTTPMethod(prop))
120
- return service(prop.toUpperCase());
121
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
122
- }
123
- });
125
+ let api = {};
126
+ for (const method of HTTP_METHODS) {
127
+ const lowerMethod = method.toLowerCase();
128
+ api[lowerMethod] = service(method);
129
+ }
130
+ return api;
124
131
  }
125
132
  export {
126
133
  addQueryToInput,
134
+ addQueryToUrl,
127
135
  enhancedFetch,
128
136
  ensureStringBody,
129
137
  makeGetApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "0.1.0",
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",