make-service 1.1.0 → 2.0.0-next.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
@@ -6,7 +6,7 @@ It adds a set of little features and allows you to parse responses with [zod](ht
6
6
 
7
7
  ## Features
8
8
  - 🤩 Type-safe return of `response.json()` and `response.text()`. Defaults to `unknown` instead of `any`.
9
- - 🚦 Easily setup an API with a `baseURL` and common `headers` for every request.
9
+ - 🚦 Easily setup an API with a `baseURL` and common options like `headers` for every request.
10
10
  - 🏗️ Compose URL from the base by just calling the endpoints and an object-like `query`.
11
11
  - 🐾 Replaces URL wildcards with a **strongly-typed** object of `params`.
12
12
  - 🧙‍♀️ Automatically stringifies the `body` of a request so you can give it a JSON-like structure.
@@ -17,7 +17,9 @@ It adds a set of little features and allows you to parse responses with [zod](ht
17
17
 
18
18
  ```ts
19
19
  const service = makeService("https://example.com/api", {
20
- Authorization: "Bearer 123",
20
+ headers: {
21
+ Authorization: "Bearer 123",
22
+ },
21
23
  });
22
24
 
23
25
  const response = await service.get("/users")
@@ -33,9 +35,12 @@ const users = await response.json(usersSchema);
33
35
  - [Runtime type-checking and parsing the response body](#runtime-type-checking-and-parsing-the-response-body)
34
36
  - [Supported HTTP Verbs](#supported-http-verbs)
35
37
  - [Headers](#headers)
36
- - [Passing a function as `baseHeaders`](#passing-a-function-as-baseheaders)
38
+ - [Passing a function as `headers`](#passing-a-function-as-headers)
37
39
  - [Deleting a previously set header](#deleting-a-previously-set-header)
38
40
  - [Base URL](#base-url)
41
+ - [Transformers](#transformers)
42
+ - [Request transformers](#request-transformers)
43
+ - [Response transformers](#response-transformers)
39
44
  - [Body](#body)
40
45
  - [Query](#query)
41
46
  - [Params](#params)
@@ -68,7 +73,7 @@ import { makeService } from "https://deno.land/x/make_service/mod.ts";
68
73
  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.
69
74
 
70
75
  ## makeService
71
- 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.
76
+ 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 options like `headers` for every request.
72
77
 
73
78
  This service object can be called with every HTTP method and it will return a [`typedResponse`](#typedresponse).
74
79
 
@@ -76,7 +81,9 @@ This service object can be called with every HTTP method and it will return a [`
76
81
  import { makeService } from 'make-service'
77
82
 
78
83
  const service = makeService("https://example.com/api", {
79
- authorization: "Bearer 123"
84
+ headers :{
85
+ authorization: "Bearer 123",
86
+ },
80
87
  })
81
88
 
82
89
  const response = await service.get("/users")
@@ -146,15 +153,17 @@ await service.options("/users")
146
153
 
147
154
  ### Headers
148
155
  The `headers` argument can be a `Headers` object, a `Record<string, string>`, or an array of `[key, value]` tuples (entries).
149
- The `baseHeaders` and the `headers` will be merged together, with the `headers` taking precedence.
156
+ The `headers` option on `baseOptions` and the `headers` argument will be merged together, with the `headers` argument taking precedence.
150
157
 
151
158
  ```ts
152
159
  import { makeService } from 'make-service'
153
160
 
154
- const service = makeService("https://example.com/api", new Headers({
155
- authorization: "Bearer 123",
156
- accept: "*/*",
157
- }))
161
+ const service = makeService("https://example.com/api", {
162
+ headers: new Headers({
163
+ authorization: "Bearer 123",
164
+ accept: "*/*",
165
+ }),
166
+ })
158
167
 
159
168
  const response = await service.get("/users", {
160
169
  headers: [['accept', 'application/json']],
@@ -164,8 +173,8 @@ const response = await service.get("/users", {
164
173
  // with headers: { authorization: "Bearer 123", accept: "application/json" }
165
174
  ```
166
175
 
167
- #### Passing a function as `baseHeaders`
168
- The given `baseHeaders` can be a sync or async function that will run in every request before it gets merged with the other headers.
176
+ #### Passing a function as `headers`
177
+ The `headers` option on `baseOptions` can be a sync or async function that will run in every request before it gets merged with the other headers.
169
178
  This is particularly useful when you need to send a refreshed token or add a timestamp to the request.
170
179
 
171
180
  ```ts
@@ -173,16 +182,21 @@ import { makeService } from 'make-service'
173
182
 
174
183
  declare getAuthorizationToken: () => Promise<HeadersInit>
175
184
 
176
- const service = makeService("https://example.com/api", async () => ({
177
- authorization: await getAuthorizationToken(),
178
- }))
185
+ const service = makeService("https://example.com/api", {
186
+ headers: async () => ({
187
+ authorization: await getAuthorizationToken(),
188
+ }),
189
+ })
179
190
 
180
191
  ```
181
192
 
182
193
  #### Deleting a previously set header
183
194
  In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value:
184
195
  ```ts
185
- const service = makeService("https://example.com/api", { authorization: "Bearer 123" })
196
+ const service = makeService("https://example.com/api", {
197
+ headers: { authorization: "Bearer 123" },
198
+ })
199
+
186
200
  const response = await service.get("/users", {
187
201
  headers: new Headers({ authorization: 'undefined', "Content-Type": undefined }),
188
202
  })
@@ -210,6 +224,37 @@ const response = await service.get("/users?admin=true")
210
224
  ```
211
225
  You can use the [`makeGetApiUrl`](#makegetapiurl) method to do that kind of URL composition.
212
226
 
227
+ ### Transformers
228
+ `makeService` can also receive `requestTransformer` and `responseTransformer` as options that will be applied to all requests.
229
+
230
+ #### Request transformers
231
+ You can transform the request in any way you want, like:
232
+
233
+ ```ts
234
+ const service = makeService('https://example.com/api', {
235
+ requestTransformer: (request) => ({ ...request, query: { admin: 'true' } }),
236
+ })
237
+
238
+ const response = await service.get("/users")
239
+
240
+ // It will call "https://example.com/api/users?admin=true"
241
+ ```
242
+
243
+ Please note that the `headers` option will be applied _after_ the request transformer runs. If you're using a request transformer, we recommend adding custom headers inside your transformer instead of using both options.
244
+
245
+ #### Response transformers
246
+ You can also transform the response in any way you want, like:
247
+
248
+ ```ts
249
+ const service = makeService('https://example.com/api', {
250
+ responseTransformer: (response) => ({ ...response, statusText: 'It worked!' }),
251
+ })
252
+
253
+ const response = await service.get("/users")
254
+
255
+ // response.statusText will be 'It worked!'
256
+ ```
257
+
213
258
  ### Body
214
259
  The function can also receive a `body` object that will be stringified and sent as the request body:
215
260
 
package/dist/index.d.ts CHANGED
@@ -1,17 +1,5 @@
1
1
  declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT"];
2
2
 
3
- /**
4
- * It returns the JSON object or throws an error if the response is not ok.
5
- * @param response the Response to be parsed
6
- * @returns the response.json method that accepts a type or Zod schema for a typed json response
7
- */
8
- declare function getJson(response: Response): <T = unknown>(schema?: Schema<T> | undefined) => Promise<T>;
9
- /**
10
- * @param response the Response to be parsed
11
- * @returns the response.text method that accepts a type or Zod schema for a typed response
12
- */
13
- declare function getText(response: Response): <T extends string = string>(schema?: Schema<T> | undefined) => Promise<T>;
14
-
15
3
  type Schema<T> = {
16
4
  parse: (d: unknown) => T;
17
5
  };
@@ -32,9 +20,18 @@ type EnhancedRequestInit<T = string> = Omit<RequestInit, 'body' | 'method'> & {
32
20
  trace?: (...args: Parameters<typeof fetch>) => void;
33
21
  };
34
22
  type ServiceRequestInit<T = string> = Omit<EnhancedRequestInit<T>, 'method'>;
23
+ type RequestTransformer = (request: EnhancedRequestInit) => EnhancedRequestInit | Promise<EnhancedRequestInit>;
24
+ type ResponseTransformer = (response: TypedResponse) => TypedResponse | Promise<TypedResponse>;
25
+ type BaseOptions = {
26
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
27
+ requestTransformer?: RequestTransformer;
28
+ responseTransformer?: ResponseTransformer;
29
+ };
35
30
  type HTTPMethod = (typeof HTTP_METHODS)[number];
36
- type TypedResponseJson = ReturnType<typeof getJson>;
37
- type TypedResponseText = ReturnType<typeof getText>;
31
+ type TypedResponseJson = <T = unknown>(schema?: Schema<T>) => Promise<T>;
32
+ type TypedResponseText = <T extends string = string>(schema?: Schema<T>) => Promise<T>;
33
+ type GetJson = (response: Response) => TypedResponseJson;
34
+ type GetText = (response: Response) => TypedResponseText;
38
35
  type Prettify<T> = {
39
36
  [K in keyof T]: T[K];
40
37
  } & {};
@@ -58,7 +55,10 @@ type ExtractPathParams<T extends string> = T extends `${infer _}:${infer Param}/
58
55
  * const typedJson = await response.json<User[]>();
59
56
  * // ^? User[]
60
57
  */
61
- declare function typedResponse(response: Response): TypedResponse;
58
+ declare function typedResponse(response: Response, options?: {
59
+ getJson?: GetJson;
60
+ getText?: GetText;
61
+ }): TypedResponse;
62
62
  /**
63
63
  *
64
64
  * @param url a string or URL to be fetched
@@ -77,7 +77,10 @@ declare function enhancedFetch<T extends string | URL>(url: T, requestInit?: Enh
77
77
  /**
78
78
  *
79
79
  * @param baseURL the base URL to be fetched in every request
80
- * @param baseHeaders any headers that should be sent with every request
80
+ * @param baseOptions options that will be applied to all requests
81
+ * @param baseOptions.headers any headers that should be sent with every request
82
+ * @param baseOptions.requestTransformer a function that will transform the enhanced request init of every request
83
+ * @param baseOptions.responseTransformer a function that will transform the typed response of every request
81
84
  * @returns a function that receive a path and requestInit and return a serialized json response that can be typed or not.
82
85
  * @example const headers = { Authorization: "Bearer 123" }
83
86
  * const fetcher = makeFetcher("https://example.com/api", headers);
@@ -85,11 +88,14 @@ declare function enhancedFetch<T extends string | URL>(url: T, requestInit?: Enh
85
88
  * const users = await response.json(userSchema);
86
89
  * // ^? User[]
87
90
  */
88
- declare function makeFetcher(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)): <T extends string>(path: T, requestInit?: EnhancedRequestInit<T>) => Promise<TypedResponse>;
91
+ declare function makeFetcher(baseURL: string | URL, baseOptions?: BaseOptions): <T extends string>(path: T, requestInit?: EnhancedRequestInit<T>) => Promise<TypedResponse>;
89
92
  /**
90
93
  *
91
94
  * @param baseURL the base URL to the API
92
- * @param baseHeaders any headers that should be sent with every request
95
+ * @param baseOptions options that will be applied to all requests
96
+ * @param baseOptions.headers any headers that should be sent with every request
97
+ * @param baseOptions.requestTransformer a function that will transform the enhanced request init of every request
98
+ * @param baseOptions.responseTransformer a function that will transform the typed response of every request
93
99
  * @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.
94
100
  * @example const headers = { Authorization: "Bearer 123" }
95
101
  * const api = makeService("https://example.com/api", headers);
@@ -97,7 +103,7 @@ declare function makeFetcher(baseURL: string | URL, baseHeaders?: HeadersInit |
97
103
  * const users = await response.json(userSchema);
98
104
  * // ^? User[]
99
105
  */
100
- declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)): Record<"get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "connect", <T extends string>(path: T, requestInit?: ServiceRequestInit<T>) => Promise<TypedResponse>>;
106
+ declare function makeService(baseURL: string | URL, baseOptions?: BaseOptions): Record<"get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "connect", <T extends string>(path: T, requestInit?: ServiceRequestInit<T>) => Promise<TypedResponse>>;
101
107
 
102
108
  /**
103
109
  * @param url a string or URL to which the query parameters will be added
@@ -175,4 +181,4 @@ type DeepKebabToSnake<T> = T extends [any, ...any] ? {
175
181
  };
176
182
  declare function kebabToSnake<T>(obj: T): DeepKebabToSnake<T>;
177
183
 
178
- export { CamelToKebab, CamelToSnake, DeepCamelToKebab, DeepCamelToSnake, DeepKebabToCamel, DeepKebabToSnake, DeepSnakeToCamel, DeepSnakeToKebab, EnhancedRequestInit, HTTPMethod, JSONValue, KebabToCamel, KebabToSnake, PathParams, Schema, SearchParams, ServiceRequestInit, SnakeToCamel, SnakeToKebab, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToURL, camelToKebab, camelToSnake, enhancedFetch, ensureStringBody, kebabToCamel, kebabToSnake, makeFetcher, makeGetApiURL, makeService, mergeHeaders, replaceURLParams, snakeToCamel, snakeToKebab, typedResponse };
184
+ export { BaseOptions, CamelToKebab, CamelToSnake, DeepCamelToKebab, DeepCamelToSnake, DeepKebabToCamel, DeepKebabToSnake, DeepSnakeToCamel, DeepSnakeToKebab, EnhancedRequestInit, GetJson, GetText, HTTPMethod, JSONValue, KebabToCamel, KebabToSnake, PathParams, RequestTransformer, ResponseTransformer, Schema, SearchParams, ServiceRequestInit, SnakeToCamel, SnakeToKebab, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToURL, camelToKebab, camelToSnake, enhancedFetch, ensureStringBody, kebabToCamel, kebabToSnake, makeFetcher, makeGetApiURL, makeService, mergeHeaders, replaceURLParams, snakeToCamel, snakeToKebab, typedResponse };
package/dist/index.js CHANGED
@@ -52,18 +52,14 @@ var HTTP_METHODS = [
52
52
  ];
53
53
 
54
54
  // src/internals.ts
55
- function getJson(response) {
56
- return async (schema) => {
57
- const json = await response.json();
58
- return schema ? schema.parse(json) : json;
59
- };
60
- }
61
- function getText(response) {
62
- return async (schema) => {
63
- const text = await response.text();
64
- return schema ? schema.parse(text) : text;
65
- };
66
- }
55
+ var getJson = (response) => async (schema) => {
56
+ const json = await response.json();
57
+ return schema ? schema.parse(json) : json;
58
+ };
59
+ var getText = (response) => async (schema) => {
60
+ const text = await response.text();
61
+ return schema ? schema.parse(text) : text;
62
+ };
67
63
  function typeOf(t) {
68
64
  return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
69
65
  }
@@ -124,13 +120,17 @@ function replaceURLParams(url, params) {
124
120
  }
125
121
 
126
122
  // src/api.ts
127
- function typedResponse(response) {
123
+ var identity = (value) => value;
124
+ function typedResponse(response, options) {
125
+ var _a, _b;
126
+ const getJsonFn = (_a = options == null ? void 0 : options.getJson) != null ? _a : getJson;
127
+ const getTextFn = (_b = options == null ? void 0 : options.getText) != null ? _b : getText;
128
128
  return new Proxy(response, {
129
129
  get(target, prop) {
130
130
  if (prop === "json")
131
- return getJson(target);
131
+ return getJsonFn(target);
132
132
  if (prop === "text")
133
- return getText(target);
133
+ return getTextFn(target);
134
134
  return target[prop];
135
135
  }
136
136
  });
@@ -152,22 +152,32 @@ async function enhancedFetch(url, requestInit) {
152
152
  const response = await fetch(fullURL, enhancedReqInit);
153
153
  return typedResponse(response);
154
154
  }
155
- function makeFetcher(baseURL, baseHeaders) {
155
+ function makeFetcher(baseURL, baseOptions = {}) {
156
156
  return async (path, requestInit = {}) => {
157
- var _a;
157
+ var _a, _b;
158
+ const { headers } = baseOptions;
159
+ const requestTransformer = (_a = baseOptions.requestTransformer) != null ? _a : identity;
160
+ const responseTransformer = (_b = baseOptions.responseTransformer) != null ? _b : identity;
161
+ const headerTransformer = async (ri) => {
162
+ var _a2;
163
+ return {
164
+ ...ri,
165
+ headers: mergeHeaders(
166
+ typeof headers === "function" ? await headers() : headers != null ? headers : {},
167
+ (_a2 = requestInit == null ? void 0 : requestInit.headers) != null ? _a2 : {}
168
+ )
169
+ };
170
+ };
158
171
  const url = makeGetApiURL(baseURL)(path);
159
- const response = await enhancedFetch(url, {
160
- ...requestInit,
161
- headers: mergeHeaders(
162
- typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
163
- (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
164
- )
165
- });
166
- return response;
172
+ const response = await enhancedFetch(
173
+ url,
174
+ await headerTransformer(await requestTransformer(requestInit))
175
+ );
176
+ return responseTransformer(response);
167
177
  };
168
178
  }
169
- function makeService(baseURL, baseHeaders) {
170
- const fetcher = makeFetcher(baseURL, baseHeaders);
179
+ function makeService(baseURL, baseOptions) {
180
+ const fetcher = makeFetcher(baseURL, baseOptions);
171
181
  function appliedService(method) {
172
182
  return async (path, requestInit = {}) => fetcher(path, { ...requestInit, method });
173
183
  }
package/dist/index.mjs CHANGED
@@ -12,18 +12,14 @@ var HTTP_METHODS = [
12
12
  ];
13
13
 
14
14
  // src/internals.ts
15
- function getJson(response) {
16
- return async (schema) => {
17
- const json = await response.json();
18
- return schema ? schema.parse(json) : json;
19
- };
20
- }
21
- function getText(response) {
22
- return async (schema) => {
23
- const text = await response.text();
24
- return schema ? schema.parse(text) : text;
25
- };
26
- }
15
+ var getJson = (response) => async (schema) => {
16
+ const json = await response.json();
17
+ return schema ? schema.parse(json) : json;
18
+ };
19
+ var getText = (response) => async (schema) => {
20
+ const text = await response.text();
21
+ return schema ? schema.parse(text) : text;
22
+ };
27
23
  function typeOf(t) {
28
24
  return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
29
25
  }
@@ -84,13 +80,17 @@ function replaceURLParams(url, params) {
84
80
  }
85
81
 
86
82
  // src/api.ts
87
- function typedResponse(response) {
83
+ var identity = (value) => value;
84
+ function typedResponse(response, options) {
85
+ var _a, _b;
86
+ const getJsonFn = (_a = options == null ? void 0 : options.getJson) != null ? _a : getJson;
87
+ const getTextFn = (_b = options == null ? void 0 : options.getText) != null ? _b : getText;
88
88
  return new Proxy(response, {
89
89
  get(target, prop) {
90
90
  if (prop === "json")
91
- return getJson(target);
91
+ return getJsonFn(target);
92
92
  if (prop === "text")
93
- return getText(target);
93
+ return getTextFn(target);
94
94
  return target[prop];
95
95
  }
96
96
  });
@@ -112,22 +112,32 @@ async function enhancedFetch(url, requestInit) {
112
112
  const response = await fetch(fullURL, enhancedReqInit);
113
113
  return typedResponse(response);
114
114
  }
115
- function makeFetcher(baseURL, baseHeaders) {
115
+ function makeFetcher(baseURL, baseOptions = {}) {
116
116
  return async (path, requestInit = {}) => {
117
- var _a;
117
+ var _a, _b;
118
+ const { headers } = baseOptions;
119
+ const requestTransformer = (_a = baseOptions.requestTransformer) != null ? _a : identity;
120
+ const responseTransformer = (_b = baseOptions.responseTransformer) != null ? _b : identity;
121
+ const headerTransformer = async (ri) => {
122
+ var _a2;
123
+ return {
124
+ ...ri,
125
+ headers: mergeHeaders(
126
+ typeof headers === "function" ? await headers() : headers != null ? headers : {},
127
+ (_a2 = requestInit == null ? void 0 : requestInit.headers) != null ? _a2 : {}
128
+ )
129
+ };
130
+ };
118
131
  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;
132
+ const response = await enhancedFetch(
133
+ url,
134
+ await headerTransformer(await requestTransformer(requestInit))
135
+ );
136
+ return responseTransformer(response);
127
137
  };
128
138
  }
129
- function makeService(baseURL, baseHeaders) {
130
- const fetcher = makeFetcher(baseURL, baseHeaders);
139
+ function makeService(baseURL, baseOptions) {
140
+ const fetcher = makeFetcher(baseURL, baseOptions);
131
141
  function appliedService(method) {
132
142
  return async (path, requestInit = {}) => fetcher(path, { ...requestInit, method });
133
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "1.1.0",
3
+ "version": "2.0.0-next.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",
@@ -16,10 +16,10 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "eslint": "latest",
19
- "jsdom": "^21.1.1",
19
+ "jsdom": "^22.1.0",
20
20
  "prettier": "latest",
21
21
  "tsup": "^6.7.0",
22
- "typescript": "^5.0.4",
22
+ "typescript": "^5.1.3",
23
23
  "vitest": "latest",
24
24
  "zod": "latest"
25
25
  },