@zimic/fetch 0.1.0-canary.7 → 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.
@@ -4,35 +4,54 @@ import {
4
4
  HttpSearchParams,
5
5
  LiteralHttpSchemaPathFromNonLiteral,
6
6
  HttpSchema,
7
+ HttpHeaders,
7
8
  } from '@zimic/http';
8
-
9
- import { Default } from '@/types/utils';
10
- import { createRegexFromURL, excludeNonPathParams, joinURL } from '@/utils/urls';
9
+ import createRegexFromURL from '@zimic/utils/url/createRegExpFromURL';
10
+ import excludeURLParams from '@zimic/utils/url/excludeURLParams';
11
+ import joinURL from '@zimic/utils/url/joinURL';
11
12
 
12
13
  import FetchResponseError from './errors/FetchResponseError';
13
- import { FetchInput, FetchOptions, Fetch } from './types/public';
14
+ import {
15
+ FetchInput,
16
+ FetchOptions,
17
+ Fetch,
18
+ FetchClient as PublicFetchClient,
19
+ FetchDefaults,
20
+ FetchFunction,
21
+ } from './types/public';
14
22
  import { FetchRequestConstructor, FetchRequestInit, FetchRequest, FetchResponse } from './types/requests';
15
23
 
16
- class FetchClient<Schema extends HttpSchema> {
24
+ class FetchClient<Schema extends HttpSchema>
25
+ implements Omit<PublicFetchClient<Schema>, 'defaults' | 'loose' | 'Request'>
26
+ {
17
27
  fetch: Fetch<Schema>;
18
28
 
19
29
  constructor({ onRequest, onResponse, ...defaults }: FetchOptions<Schema>) {
20
30
  this.fetch = this.createFetchFunction();
21
- this.fetch.defaults = defaults;
22
- this.fetch.Request = this.createRequestClass(defaults);
31
+
32
+ this.fetch.defaults = {
33
+ ...defaults,
34
+ headers: defaults.headers ?? {},
35
+ searchParams: defaults.searchParams ?? {},
36
+ };
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ this.fetch.loose = this.fetch as Fetch<any> as FetchFunction.Loose;
40
+
41
+ this.fetch.Request = this.createRequestClass(this.fetch.defaults);
23
42
  this.fetch.onRequest = onRequest;
24
43
  this.fetch.onResponse = onResponse;
25
44
  }
26
45
 
27
46
  private createFetchFunction() {
28
47
  const fetch = async <
29
- Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
30
48
  Method extends HttpSchemaMethod<Schema>,
49
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
31
50
  >(
32
- input: FetchInput<Schema, Path, Method>,
33
- init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
51
+ input: FetchInput<Schema, Method, Path>,
52
+ init: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>,
34
53
  ) => {
35
- const request = await this.createFetchRequest<Path, Method>(input, init);
54
+ const request = await this.createFetchRequest<Method, Path>(input, init);
36
55
  const requestClone = request.clone();
37
56
 
38
57
  const rawResponse = await globalThis.fetch(
@@ -40,8 +59,8 @@ class FetchClient<Schema extends HttpSchema> {
40
59
  requestClone as Request,
41
60
  );
42
61
  const response = await this.createFetchResponse<
43
- LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,
44
- Method
62
+ Method,
63
+ LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>
45
64
  >(request, rawResponse);
46
65
 
47
66
  return response;
@@ -53,11 +72,11 @@ class FetchClient<Schema extends HttpSchema> {
53
72
  }
54
73
 
55
74
  private async createFetchRequest<
56
- Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
57
75
  Method extends HttpSchemaMethod<Schema>,
76
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
58
77
  >(
59
- input: FetchInput<Schema, Path, Method>,
60
- init: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
78
+ input: FetchInput<Schema, Method, Path>,
79
+ init: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>,
61
80
  ) {
62
81
  let request = input instanceof Request ? input : new this.fetch.Request(input, init);
63
82
 
@@ -65,7 +84,6 @@ class FetchClient<Schema extends HttpSchema> {
65
84
  const requestAfterInterceptor = await this.fetch.onRequest(
66
85
  // Optimize type checking by narrowing the type of request
67
86
  request as FetchRequest.Loose,
68
- this.fetch,
69
87
  );
70
88
 
71
89
  if (requestAfterInterceptor !== request) {
@@ -73,7 +91,7 @@ class FetchClient<Schema extends HttpSchema> {
73
91
 
74
92
  request = isFetchRequest
75
93
  ? (requestAfterInterceptor as Request as typeof request)
76
- : new this.fetch.Request(requestAfterInterceptor as FetchInput<Schema, Path, Method>, init);
94
+ : new this.fetch.Request(requestAfterInterceptor as FetchInput<Schema, Method, Path>, init);
77
95
  }
78
96
  }
79
97
 
@@ -81,16 +99,15 @@ class FetchClient<Schema extends HttpSchema> {
81
99
  }
82
100
 
83
101
  private async createFetchResponse<
84
- Path extends HttpSchemaPath<Schema, Method>,
85
102
  Method extends HttpSchemaMethod<Schema>,
86
- >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, rawResponse: Response) {
87
- let response = this.defineFetchResponseProperties<Path, Method>(fetchRequest, rawResponse);
103
+ Path extends HttpSchemaPath.Literal<Schema, Method>,
104
+ >(fetchRequest: FetchRequest<Schema, Method, Path>, rawResponse: Response) {
105
+ let response = this.defineFetchResponseProperties<Method, Path>(fetchRequest, rawResponse);
88
106
 
89
107
  if (this.fetch.onResponse) {
90
108
  const responseAfterInterceptor = await this.fetch.onResponse(
91
109
  // Optimize type checking by narrowing the type of response
92
110
  response as FetchResponse.Loose,
93
- this.fetch,
94
111
  );
95
112
 
96
113
  const isFetchResponse =
@@ -100,17 +117,17 @@ class FetchClient<Schema extends HttpSchema> {
100
117
 
101
118
  response = isFetchResponse
102
119
  ? (responseAfterInterceptor as typeof response)
103
- : this.defineFetchResponseProperties<Path, Method>(fetchRequest, responseAfterInterceptor);
120
+ : this.defineFetchResponseProperties<Method, Path>(fetchRequest, responseAfterInterceptor);
104
121
  }
105
122
 
106
123
  return response;
107
124
  }
108
125
 
109
126
  private defineFetchResponseProperties<
110
- Path extends HttpSchemaPath<Schema, Method>,
111
127
  Method extends HttpSchemaMethod<Schema>,
112
- >(fetchRequest: FetchRequest<Path, Method, Default<Schema[Path][Method]>>, response: Response) {
113
- const fetchResponse = response as FetchResponse<Path, Method, Default<Schema[Path][Method]>>;
128
+ Path extends HttpSchemaPath.Literal<Schema, Method>,
129
+ >(fetchRequest: FetchRequest<Schema, Method, Path>, response: Response) {
130
+ const fetchResponse = response as FetchResponse<Schema, Method, Path>;
114
131
 
115
132
  Object.defineProperty(fetchResponse, 'request', {
116
133
  value: fetchRequest satisfies FetchResponse.Loose['request'],
@@ -119,13 +136,20 @@ class FetchClient<Schema extends HttpSchema> {
119
136
  configurable: false,
120
137
  });
121
138
 
122
- const responseError = (
123
- fetchResponse.ok ? null : new FetchResponseError(fetchRequest, fetchResponse)
124
- ) satisfies FetchResponse.Loose['error'];
139
+ let responseError: FetchResponse.Loose['error'] | undefined;
125
140
 
126
141
  Object.defineProperty(fetchResponse, 'error', {
127
- value: responseError,
128
- writable: false,
142
+ get() {
143
+ if (responseError === undefined) {
144
+ responseError = fetchResponse.ok
145
+ ? null
146
+ : new FetchResponseError(
147
+ fetchRequest,
148
+ fetchResponse as FetchResponse<Schema, Method, Path, true, 'manual'>,
149
+ );
150
+ }
151
+ return responseError;
152
+ },
129
153
  enumerable: true,
130
154
  configurable: false,
131
155
  });
@@ -133,53 +157,76 @@ class FetchClient<Schema extends HttpSchema> {
133
157
  return fetchResponse;
134
158
  }
135
159
 
136
- private createRequestClass(defaults: FetchRequestInit.Defaults) {
160
+ private createRequestClass(defaults: FetchDefaults) {
137
161
  class Request<
138
- Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
139
162
  Method extends HttpSchemaMethod<Schema>,
163
+ Path extends HttpSchemaPath.NonLiteral<Schema, Method>,
140
164
  > extends globalThis.Request {
141
165
  path: LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;
142
166
 
143
167
  constructor(
144
- input: FetchInput<Schema, Path, Method>,
145
- rawInit: FetchRequestInit<Schema, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>, Method>,
168
+ input: FetchInput<Schema, Method, Path>,
169
+ init: FetchRequestInit<Schema, Method, LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>>,
146
170
  ) {
147
- const init = { ...defaults, ...rawInit };
171
+ const initWithDefaults = { ...defaults, ...init };
172
+
173
+ const headersFromDefaults = new HttpHeaders(defaults.headers);
174
+ const headersFromInit = new HttpHeaders((init satisfies RequestInit as RequestInit).headers);
148
175
 
149
176
  let url: URL;
177
+ const baseURL = new URL(initWithDefaults.baseURL);
150
178
 
151
179
  if (input instanceof globalThis.Request) {
152
- super(
153
- // Optimize type checking by narrowing the type of input
154
- input as globalThis.Request,
155
- init,
156
- );
180
+ // Optimize type checking by narrowing the type of input
181
+ const request = input as globalThis.Request;
182
+ const headersFromRequest = new HttpHeaders(input.headers);
183
+
184
+ initWithDefaults.headers = {
185
+ ...headersFromDefaults.toObject(),
186
+ ...headersFromRequest.toObject(),
187
+ ...headersFromInit.toObject(),
188
+ };
189
+
190
+ super(request, initWithDefaults);
157
191
 
158
192
  url = new URL(input.url);
159
193
  } else {
160
- url = input instanceof URL ? new URL(input) : new URL(joinURL(init.baseURL, input));
194
+ initWithDefaults.headers = {
195
+ ...headersFromDefaults.toObject(),
196
+ ...headersFromInit.toObject(),
197
+ };
198
+
199
+ url = input instanceof URL ? new URL(input) : new URL(joinURL(baseURL, input));
161
200
 
162
- if (init.searchParams) {
163
- url.search = new HttpSearchParams(init.searchParams).toString();
164
- }
201
+ const searchParamsFromDefaults = new HttpSearchParams(defaults.searchParams);
202
+ const searchParamsFromInit = new HttpSearchParams(initWithDefaults.searchParams);
165
203
 
166
- super(url, init);
204
+ initWithDefaults.searchParams = {
205
+ ...searchParamsFromDefaults.toObject(),
206
+ ...searchParamsFromInit.toObject(),
207
+ };
208
+
209
+ url.search = new HttpSearchParams(initWithDefaults.searchParams).toString();
210
+
211
+ super(url, initWithDefaults);
167
212
  }
168
213
 
169
- this.path = excludeNonPathParams(url)
214
+ const baseURLWithoutTrailingSlash = baseURL.toString().replace(/\/$/, '');
215
+
216
+ this.path = excludeURLParams(url)
170
217
  .toString()
171
- .replace(init.baseURL, '') as LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;
218
+ .replace(baseURLWithoutTrailingSlash, '') as LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>;
172
219
  }
173
220
 
174
- clone(): Request<Path, Method> {
221
+ clone(): Request<Method, Path> {
175
222
  const rawClone = super.clone();
176
223
 
177
- return new Request<Path, Method>(
178
- rawClone as unknown as FetchInput<Schema, Path, Method>,
224
+ return new Request<Method, Path>(
225
+ rawClone as unknown as FetchInput<Schema, Method, Path>,
179
226
  rawClone as unknown as FetchRequestInit<
180
227
  Schema,
181
- LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>,
182
- Method
228
+ Method,
229
+ LiteralHttpSchemaPathFromNonLiteral<Schema, Method, Path>
183
230
  >,
184
231
  );
185
232
  }
@@ -188,11 +235,11 @@ class FetchClient<Schema extends HttpSchema> {
188
235
  return Request as FetchRequestConstructor<Schema>;
189
236
  }
190
237
 
191
- isRequest<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
238
+ isRequest<Path extends HttpSchemaPath.Literal<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
192
239
  request: unknown,
193
- path: Path,
194
240
  method: Method,
195
- ): request is FetchRequest<Path, Method, Default<Schema[Path][Method]>> {
241
+ path: Path,
242
+ ): request is FetchRequest<Schema, Method, Path> {
196
243
  return (
197
244
  request instanceof Request &&
198
245
  request.method === method &&
@@ -202,29 +249,29 @@ class FetchClient<Schema extends HttpSchema> {
202
249
  );
203
250
  }
204
251
 
205
- isResponse<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
252
+ isResponse<Path extends HttpSchemaPath.Literal<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
206
253
  response: unknown,
207
- path: Path,
208
254
  method: Method,
209
- ): response is FetchResponse<Path, Method, Default<Schema[Path][Method]>> {
255
+ path: Path,
256
+ ): response is FetchResponse<Schema, Method, Path> {
210
257
  return (
211
258
  response instanceof Response &&
212
259
  'request' in response &&
260
+ this.isRequest(response.request, method, path) &&
213
261
  'error' in response &&
214
- this.isRequest(response.request, path, method)
262
+ (response.error === null || response.error instanceof FetchResponseError)
215
263
  );
216
264
  }
217
265
 
218
- isResponseError<Path extends HttpSchemaPath<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
266
+ isResponseError<Path extends HttpSchemaPath.Literal<Schema, Method>, Method extends HttpSchemaMethod<Schema>>(
219
267
  error: unknown,
220
- path: Path,
221
268
  method: Method,
222
- ): error is FetchResponseError<Path, Method, Default<Schema[Path][Method]>> {
269
+ path: Path,
270
+ ): error is FetchResponseError<Schema, Method, Path> {
223
271
  return (
224
272
  error instanceof FetchResponseError &&
225
- error.request.method === method &&
226
- typeof error.request.path === 'string' &&
227
- createRegexFromURL(path).test(error.request.path)
273
+ this.isRequest(error.request, method, path) &&
274
+ this.isResponse(error.response, method, path)
228
275
  );
229
276
  }
230
277
  }
@@ -1,15 +1,56 @@
1
- import { HttpMethod, HttpMethodSchema } from '@zimic/http';
1
+ import { HttpSchema, HttpSchemaMethod, HttpSchemaPath } from '@zimic/http';
2
2
 
3
3
  import { FetchRequest, FetchResponse } from '../types/requests';
4
4
 
5
+ /**
6
+ * An error representing a response with a failure status code (4XX or 5XX).
7
+ *
8
+ * @example
9
+ * import { type HttpSchema } from '@zimic/http';
10
+ * import { createFetch } from '@zimic/fetch';
11
+ *
12
+ * interface User {
13
+ * id: string;
14
+ * username: string;
15
+ * }
16
+ *
17
+ * type Schema = HttpSchema<{
18
+ * '/users/:userId': {
19
+ * GET: {
20
+ * response: {
21
+ * 200: { body: User };
22
+ * 404: { body: { message: string } };
23
+ * };
24
+ * };
25
+ * };
26
+ * }>;
27
+ *
28
+ * const fetch = createFetch<Schema>({
29
+ * baseURL: 'http://localhost:3000',
30
+ * });
31
+ *
32
+ * const response = await fetch(`/users/${userId}`, {
33
+ * method: 'GET',
34
+ * });
35
+ *
36
+ * if (!response.ok) {
37
+ * console.log(response.status); // 404
38
+ *
39
+ * console.log(response.error); // FetchResponseError<Schema, 'GET', '/users'>
40
+ * console.log(response.error.request); // FetchRequest<Schema, 'GET', '/users'>
41
+ * console.log(response.error.response); // FetchResponse<Schema, 'GET', '/users'>
42
+ * }
43
+ *
44
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchresponseerror `FetchResponseError` API reference}
45
+ */
5
46
  class FetchResponseError<
6
- Path extends string = string,
7
- Method extends HttpMethod = HttpMethod,
8
- MethodSchema extends HttpMethodSchema = HttpMethodSchema,
47
+ Schema extends HttpSchema,
48
+ Method extends HttpSchemaMethod<Schema>,
49
+ Path extends HttpSchemaPath.Literal<Schema, Method>,
9
50
  > extends Error {
10
51
  constructor(
11
- public request: FetchRequest<Path, Method, MethodSchema>,
12
- public response: FetchResponse<Path, Method, MethodSchema, true>,
52
+ public request: FetchRequest<Schema, Method, Path>,
53
+ public response: FetchResponse<Schema, Method, Path, true, 'manual'>,
13
54
  ) {
14
55
  super(`${request.method} ${request.url} failed with status ${response.status}: ${response.statusText}`);
15
56
  this.name = 'FetchResponseError';
@@ -1,12 +1,63 @@
1
1
  import { HttpSchema } from '@zimic/http';
2
2
 
3
3
  import FetchClient from './FetchClient';
4
- import { FetchOptions, Fetch as PublicFetch } from './types/public';
4
+ import { FetchOptions, Fetch } from './types/public';
5
5
 
6
- function createFetch<Schema extends HttpSchema>(
7
- options: FetchOptions<HttpSchema<Schema>>,
8
- ): PublicFetch<HttpSchema<Schema>> {
9
- const { fetch } = new FetchClient<HttpSchema<Schema>>(options);
6
+ /**
7
+ * Creates a {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch fetch instance} typed with an HTTP
8
+ * schema, closely compatible with the {@link https://developer.mozilla.org/docs/Web/API/Fetch_API native Fetch API}. All
9
+ * requests and responses are typed by default with the schema, including methods, paths, status codes, parameters, and
10
+ * bodies.
11
+ *
12
+ * Requests sent by the fetch instance have their URL automatically prefixed with the base URL of the instance.
13
+ * {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch.defaults Default options} are also applied to the
14
+ * requests, if provided.
15
+ *
16
+ * @example
17
+ * import { type HttpSchema } from '@zimic/http';
18
+ * import { createFetch } from '@zimic/fetch';
19
+ *
20
+ * interface User {
21
+ * id: string;
22
+ * username: string;
23
+ * }
24
+ *
25
+ * type Schema = HttpSchema<{
26
+ * '/users': {
27
+ * POST: {
28
+ * request: {
29
+ * headers: { 'content-type': 'application/json' };
30
+ * body: { username: string };
31
+ * };
32
+ * response: {
33
+ * 201: { body: User };
34
+ * };
35
+ * };
36
+ *
37
+ * GET: {
38
+ * request: {
39
+ * searchParams: {
40
+ * query?: string;
41
+ * page?: number;
42
+ * limit?: number;
43
+ * };
44
+ * };
45
+ * response: {
46
+ * 200: { body: User[] };
47
+ * };
48
+ * };
49
+ * };
50
+ * }>;
51
+ *
52
+ * const fetch = createFetch<Schema>({
53
+ * baseURL: 'http://localhost:3000',
54
+ * });
55
+ *
56
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#createfetch `createFetch(options)` API reference}
57
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference}
58
+ */
59
+ function createFetch<Schema extends HttpSchema>(options: FetchOptions<Schema>): Fetch<Schema> {
60
+ const { fetch } = new FetchClient<Schema>(options);
10
61
  return fetch;
11
62
  }
12
63
 
@@ -1,5 +1,12 @@
1
1
  const value = Symbol.for('JSONStringified.value');
2
2
 
3
+ /**
4
+ * Represents a value stringified by `JSON.stringify`, maintaining a reference to the original type.
5
+ *
6
+ * This type is used to validate that the expected stringified body is passed to `fetch`.
7
+ *
8
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#using-a-json-body}
9
+ */
3
10
  export type JSONStringified<Value> = string & { [value]: Value };
4
11
 
5
12
  declare global {