make-service 0.0.2 → 0.1.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
@@ -40,9 +40,9 @@ This library exports the `makeService` function and some primitives used to buil
40
40
 
41
41
  # makeService
42
42
 
43
- The main function of this lib is built on top of the primitives described in the following sections. It allows you to create an "API" object with a `baseURL` and common `headers` for every request.
43
+ 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
44
 
45
- This "api" object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse) object as it uses the [`enhancedFetch`](#enhancedfetch) internally.
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.
46
46
 
47
47
  ```ts
48
48
  import { makeService } from 'make-service'
@@ -107,7 +107,7 @@ await api.options("/users")
107
107
  ## enhancedFetch
108
108
 
109
109
  A wrapper around the `fetch` API.
110
- It returns a [`typedResponse`](#typedresponse) instead of a `Response`.
110
+ It returns a [`TypedResponse`](#typedresponse) instead of a `Response`.
111
111
 
112
112
  ```ts
113
113
  import { enhancedFetch } from 'make-service'
@@ -123,6 +123,8 @@ const json = await response.json()
123
123
 
124
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.
125
125
 
126
+ This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
127
+
126
128
  ```ts
127
129
  import { enhancedFetch } from 'make-service'
128
130
 
@@ -150,23 +152,24 @@ A type-safe wrapper around the `Response` object. It adds a `json` and `text` me
150
152
 
151
153
  ```ts
152
154
  import { typedResponse } from 'make-service'
155
+ import type { TypedResponse } from 'make-service'
153
156
 
154
157
  // With JSON
155
- const response = new Response(JSON.stringify({ foo: "bar" }))
156
- const json = await typedResponse(response).json()
158
+ const response: TypedResponse = typedResponse(new Response(JSON.stringify({ foo: "bar" })))
159
+ const json = await response.json()
157
160
  // ^? unknown
158
- const json = await typedResponse(response).json<{ foo: string }>()
161
+ const json = await response.json<{ foo: string }>()
159
162
  // ^? { foo: string }
160
- const json = await typedResponse(response).json(z.object({ foo: z.string() }))
163
+ const json = await response.json(z.object({ foo: z.string() }))
161
164
  // ^? { foo: string }
162
165
 
163
166
  // With text
164
- const response = new Response("foo")
165
- const text = await typedResponse(response).text()
167
+ const response: TypedResponse = typedResponse(new Response("foo"))
168
+ const text = await response.text()
166
169
  // ^? string
167
- const text = await typedResponse(response).text<`foo${string}`>()
170
+ const text = await response.text<`foo${string}`>()
168
171
  // ^? `foo${string}`
169
- const text = await typedResponse(response).text(z.string().email())
172
+ const text = await response.text(z.string().email())
170
173
  // ^? string
171
174
  ```
172
175
 
package/dist/index.d.ts CHANGED
@@ -1,10 +1,4 @@
1
- type Schema<T> = {
2
- parse: (d: unknown) => T;
3
- };
4
- type JSONValue = string | number | boolean | {
5
- [x: string]: JSONValue;
6
- } | Array<JSONValue>;
7
- type SearchParams = ConstructorParameters<typeof URLSearchParams>[0];
1
+ declare const HTTP_METHODS: readonly ["get", "post", "put", "delete", "patch", "options", "head"];
8
2
 
9
3
  /**
10
4
  * It returns the JSON object or throws an error if the response is not ok.
@@ -18,6 +12,33 @@ declare function getJson(response: Response): <T = unknown>(schema?: Schema<T> |
18
12
  */
19
13
  declare function getText(response: Response): <T extends string = string>(schema?: Schema<T> | undefined) => Promise<T>;
20
14
 
15
+ type Schema<T> = {
16
+ parse: (d: unknown) => T;
17
+ };
18
+ type JSONValue = string | number | boolean | {
19
+ [x: string]: JSONValue;
20
+ } | Array<JSONValue>;
21
+ type SearchParams = ConstructorParameters<typeof URLSearchParams>[0];
22
+ type TypedResponse = Omit<Response, 'json' | 'text'> & {
23
+ json: TypedResponseJson;
24
+ text: TypedResponseText;
25
+ };
26
+ type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
27
+ body?: JSONValue;
28
+ query?: SearchParams;
29
+ trace?: (...args: Parameters<typeof fetch>) => void;
30
+ };
31
+ type ServiceRequestInit = Omit<EnhancedRequestInit, 'method'>;
32
+ type HTTPMethod = (typeof HTTP_METHODS)[number];
33
+ type TypedResponseJson = ReturnType<typeof getJson>;
34
+ type TypedResponseText = ReturnType<typeof getText>;
35
+
36
+ /**
37
+ * It merges multiple HeadersInit objects into a single Headers object
38
+ * @param entries Any number of HeadersInit objects
39
+ * @returns a new Headers object with the merged headers
40
+ */
41
+ declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
21
42
  /**
22
43
  * @param input a string or URL to which the query parameters will be added
23
44
  * @param searchParams the query parameters
@@ -28,7 +49,7 @@ declare function addQueryToInput(input: string | URL, searchParams?: SearchParam
28
49
  * @param baseURL the base path to the API
29
50
  * @returns a function that receives a path and an object of query parameters and returns a URL
30
51
  */
31
- declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: SearchParams) => string | URL;
52
+ declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchParams?: SearchParams) => string | URL;
32
53
  /**
33
54
  * It hacks the Response object to add typed json and text methods
34
55
  * @param response the Response to be proxied
@@ -43,27 +64,19 @@ declare function makeGetApiUrl(baseURL: string): (path: string, searchParams?: S
43
64
  * const typedJson = await response.json<User[]>();
44
65
  * // ^? User[]
45
66
  */
46
- declare function typedResponse(response: Response): Omit<Response, "json" | "text"> & {
47
- json: ReturnType<typeof getJson>;
48
- text: ReturnType<typeof getText>;
49
- };
67
+ declare function typedResponse(response: Response): TypedResponse;
50
68
  /**
51
69
  * @param body the JSON-like body of the request
52
70
  * @returns the body stringified if it is not a string
53
71
  */
54
72
  declare function ensureStringBody(body?: JSONValue): string | undefined;
55
- type Options = Omit<RequestInit, 'body'> & {
56
- body?: JSONValue;
57
- query?: SearchParams;
58
- trace?: (...args: Parameters<typeof fetch>) => void;
59
- };
60
73
  /**
61
74
  *
62
75
  * @param input a string or URL to be fetched
63
- * @param options the options to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
64
- * @param options.body the body of the request. It will be automatically stringified so you can send a JSON-like object
65
- * @param options.query the query parameters to be added to the URL
66
- * @param options.trace a function that receives the URL and the requestInit and can be used to log the request
76
+ * @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
+ * @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object
78
+ * @param requestInit.query the query parameters to be added to the URL
79
+ * @param requestInit.trace a function that receives the URL and the requestInit and can be used to log the request
67
80
  * @returns a Response with typed json and text methods
68
81
  * @example const response = await fetch("https://example.com/api/users");
69
82
  * const users = await response.json(userSchema);
@@ -71,24 +84,26 @@ type Options = Omit<RequestInit, 'body'> & {
71
84
  * const untyped = await response.json();
72
85
  * // ^? unknown
73
86
  */
74
- declare function enhancedFetch(input: string | URL, options?: Options): Promise<Omit<Response, "json" | "text"> & {
75
- json: <T = unknown>(schema?: Schema<T> | undefined) => Promise<T>;
76
- text: <T_1 extends string = string>(schema?: Schema<T_1> | undefined) => Promise<T_1>;
77
- }>;
87
+ declare function enhancedFetch(input: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
78
88
  /**
79
89
  *
80
90
  * @param baseURL the base URL to the API
81
91
  * @param baseHeaders any headers that should be sent with every request
82
- * @returns an API object with HTTP methods that are functions that receive a path and options and return a serialized json response that can be typed or not.
92
+ * @returns a service object with HTTP methods that are functions that receive a path and requestInit and return a serialized json response that can be typed or not.
83
93
  * @example const headers = { Authorization: "Bearer 123" }
84
94
  * const api = makeService("https://example.com/api", headers);
85
95
  * const response = await api.get("/users")
86
96
  * const users = await response.json(userSchema);
87
97
  * // ^? User[]
88
98
  */
89
- declare function makeService(baseURL: string, baseHeaders?: HeadersInit): Record<"get" | "post" | "put" | "delete" | "patch" | "options" | "head", (path: string, options?: Omit<Options, 'method'>) => Promise<Omit<Response, "json" | "text"> & {
90
- json: <T = unknown>(schema?: Schema<T> | undefined) => Promise<T>;
91
- text: <T_1 extends string = string>(schema?: Schema<T_1> | undefined) => Promise<T_1>;
92
- }>>;
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
+ };
93
108
 
94
- export { addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, typedResponse };
109
+ export { EnhancedRequestInit, HTTPMethod, JSONValue, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(src_exports, {
25
25
  ensureStringBody: () => ensureStringBody,
26
26
  makeGetApiUrl: () => makeGetApiUrl,
27
27
  makeService: () => makeService,
28
+ mergeHeaders: () => mergeHeaders,
28
29
  typedResponse: () => typedResponse
29
30
  });
30
31
  module.exports = __toCommonJS(src_exports);
@@ -61,20 +62,42 @@ function isHTTPMethod(method) {
61
62
  }
62
63
 
63
64
  // src/make-service.ts
65
+ function mergeHeaders(...entries) {
66
+ const result = /* @__PURE__ */ new Map();
67
+ for (const entry of entries) {
68
+ const headers = new Headers(entry);
69
+ for (const [key, value] of headers.entries()) {
70
+ if (value === void 0 || value === "undefined") {
71
+ result.delete(key);
72
+ } else {
73
+ result.set(key, value);
74
+ }
75
+ }
76
+ }
77
+ return new Headers(Array.from(result.entries()));
78
+ }
64
79
  function addQueryToInput(input, searchParams) {
65
80
  if (!searchParams)
66
81
  return input;
67
- if (searchParams && typeof input === "string") {
82
+ if (typeof input === "string") {
68
83
  const separator = input.includes("?") ? "&" : "?";
69
84
  return `${input}${separator}${new URLSearchParams(searchParams)}`;
70
85
  }
71
86
  if (searchParams && input instanceof URL) {
72
- input.search = new URLSearchParams(searchParams).toString();
87
+ for (const [key, value] of Object.entries(
88
+ new URLSearchParams(searchParams)
89
+ )) {
90
+ input.searchParams.set(key, value);
91
+ }
73
92
  }
74
93
  return input;
75
94
  }
76
95
  function makeGetApiUrl(baseURL) {
77
- return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
96
+ const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
97
+ return (path, searchParams) => {
98
+ const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
99
+ return addQueryToInput(url, searchParams);
100
+ };
78
101
  }
79
102
  function typedResponse(response) {
80
103
  return new Proxy(response, {
@@ -94,24 +117,31 @@ function ensureStringBody(body) {
94
117
  return body;
95
118
  return JSON.stringify(body);
96
119
  }
97
- async function enhancedFetch(input, options) {
98
- const { query, trace, ...reqInit } = options != null ? options : {};
99
- const headers = { "content-type": "application/json", ...reqInit.headers };
120
+ async function enhancedFetch(input, requestInit) {
121
+ var _a;
122
+ const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
123
+ const headers = mergeHeaders(
124
+ {
125
+ "content-type": "application/json"
126
+ },
127
+ (_a = reqInit.headers) != null ? _a : {}
128
+ );
100
129
  const url = addQueryToInput(input, query);
101
130
  const body = ensureStringBody(reqInit.body);
102
- const requestInit = { ...reqInit, headers, body };
103
- trace == null ? void 0 : trace(url, requestInit);
104
- const request = new Request(url, requestInit);
105
- const response = await fetch(request);
131
+ const enhancedReqInit = { ...reqInit, headers, body };
132
+ trace == null ? void 0 : trace(url, enhancedReqInit);
133
+ const response = await fetch(url, enhancedReqInit);
106
134
  return typedResponse(response);
107
135
  }
108
136
  function makeService(baseURL, baseHeaders) {
109
- const api = (method) => {
110
- return async (path, options = {}) => {
111
- const response = await enhancedFetch(`${baseURL}${path}`, {
112
- ...options,
137
+ const service = (method) => {
138
+ return async (path, requestInit = {}) => {
139
+ var _a;
140
+ const url = makeGetApiUrl(baseURL)(path);
141
+ const response = await enhancedFetch(url, {
142
+ ...requestInit,
113
143
  method,
114
- headers: { ...baseHeaders, ...options == null ? void 0 : options.headers }
144
+ headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
115
145
  });
116
146
  return response;
117
147
  };
@@ -119,7 +149,7 @@ function makeService(baseURL, baseHeaders) {
119
149
  return new Proxy({}, {
120
150
  get(_target, prop) {
121
151
  if (isHTTPMethod(prop))
122
- return api(prop);
152
+ return service(prop.toUpperCase());
123
153
  throw new Error(`Invalid HTTP method: ${prop.toString()}`);
124
154
  }
125
155
  });
@@ -131,5 +161,6 @@ function makeService(baseURL, baseHeaders) {
131
161
  ensureStringBody,
132
162
  makeGetApiUrl,
133
163
  makeService,
164
+ mergeHeaders,
134
165
  typedResponse
135
166
  });
package/dist/index.mjs CHANGED
@@ -30,20 +30,42 @@ function isHTTPMethod(method) {
30
30
  }
31
31
 
32
32
  // src/make-service.ts
33
+ function mergeHeaders(...entries) {
34
+ const result = /* @__PURE__ */ new Map();
35
+ for (const entry of entries) {
36
+ const headers = new Headers(entry);
37
+ for (const [key, value] of headers.entries()) {
38
+ if (value === void 0 || value === "undefined") {
39
+ result.delete(key);
40
+ } else {
41
+ result.set(key, value);
42
+ }
43
+ }
44
+ }
45
+ return new Headers(Array.from(result.entries()));
46
+ }
33
47
  function addQueryToInput(input, searchParams) {
34
48
  if (!searchParams)
35
49
  return input;
36
- if (searchParams && typeof input === "string") {
50
+ if (typeof input === "string") {
37
51
  const separator = input.includes("?") ? "&" : "?";
38
52
  return `${input}${separator}${new URLSearchParams(searchParams)}`;
39
53
  }
40
54
  if (searchParams && input instanceof URL) {
41
- input.search = new URLSearchParams(searchParams).toString();
55
+ for (const [key, value] of Object.entries(
56
+ new URLSearchParams(searchParams)
57
+ )) {
58
+ input.searchParams.set(key, value);
59
+ }
42
60
  }
43
61
  return input;
44
62
  }
45
63
  function makeGetApiUrl(baseURL) {
46
- return (path, searchParams) => addQueryToInput(`${baseURL}${path}`, searchParams);
64
+ const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
65
+ return (path, searchParams) => {
66
+ const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
67
+ return addQueryToInput(url, searchParams);
68
+ };
47
69
  }
48
70
  function typedResponse(response) {
49
71
  return new Proxy(response, {
@@ -63,24 +85,31 @@ function ensureStringBody(body) {
63
85
  return body;
64
86
  return JSON.stringify(body);
65
87
  }
66
- async function enhancedFetch(input, options) {
67
- const { query, trace, ...reqInit } = options != null ? options : {};
68
- const headers = { "content-type": "application/json", ...reqInit.headers };
88
+ async function enhancedFetch(input, requestInit) {
89
+ var _a;
90
+ const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
91
+ const headers = mergeHeaders(
92
+ {
93
+ "content-type": "application/json"
94
+ },
95
+ (_a = reqInit.headers) != null ? _a : {}
96
+ );
69
97
  const url = addQueryToInput(input, query);
70
98
  const body = ensureStringBody(reqInit.body);
71
- const requestInit = { ...reqInit, headers, body };
72
- trace == null ? void 0 : trace(url, requestInit);
73
- const request = new Request(url, requestInit);
74
- const response = await fetch(request);
99
+ const enhancedReqInit = { ...reqInit, headers, body };
100
+ trace == null ? void 0 : trace(url, enhancedReqInit);
101
+ const response = await fetch(url, enhancedReqInit);
75
102
  return typedResponse(response);
76
103
  }
77
104
  function makeService(baseURL, baseHeaders) {
78
- const api = (method) => {
79
- return async (path, options = {}) => {
80
- const response = await enhancedFetch(`${baseURL}${path}`, {
81
- ...options,
105
+ const service = (method) => {
106
+ return async (path, requestInit = {}) => {
107
+ var _a;
108
+ const url = makeGetApiUrl(baseURL)(path);
109
+ const response = await enhancedFetch(url, {
110
+ ...requestInit,
82
111
  method,
83
- headers: { ...baseHeaders, ...options == null ? void 0 : options.headers }
112
+ headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
84
113
  });
85
114
  return response;
86
115
  };
@@ -88,7 +117,7 @@ function makeService(baseURL, baseHeaders) {
88
117
  return new Proxy({}, {
89
118
  get(_target, prop) {
90
119
  if (isHTTPMethod(prop))
91
- return api(prop);
120
+ return service(prop.toUpperCase());
92
121
  throw new Error(`Invalid HTTP method: ${prop.toString()}`);
93
122
  }
94
123
  });
@@ -99,5 +128,6 @@ export {
99
128
  ensureStringBody,
100
129
  makeGetApiUrl,
101
130
  makeService,
131
+ mergeHeaders,
102
132
  typedResponse
103
133
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "0.0.2",
3
+ "version": "0.1.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",