@remit/api-http-client 0.0.21 → 0.0.23

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.
@@ -3,12 +3,7 @@
3
3
  import { createSseClient } from '../core/serverSentEvents.gen.js';
4
4
  import type { HttpMethod } from '../core/types.gen.js';
5
5
  import { getValidRequestBody } from '../core/utils.gen.js';
6
- import type {
7
- Client,
8
- Config,
9
- RequestOptions,
10
- ResolvedRequestOptions,
11
- } from './types.gen.js';
6
+ import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen.js';
12
7
  import {
13
8
  buildUrl,
14
9
  createConfig,
@@ -34,27 +29,26 @@ export const createClient = (config: Config = {}): Client => {
34
29
  return getConfig();
35
30
  };
36
31
 
37
- const interceptors = createInterceptors<
38
- Request,
39
- Response,
40
- unknown,
41
- ResolvedRequestOptions
42
- >();
32
+ const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
43
33
 
44
- const beforeRequest = async (options: RequestOptions) => {
34
+ const beforeRequest = async <
35
+ TData = unknown,
36
+ TResponseStyle extends 'data' | 'fields' = 'fields',
37
+ ThrowOnError extends boolean = boolean,
38
+ Url extends string = string,
39
+ >(
40
+ options: RequestOptions<TData, TResponseStyle, ThrowOnError, Url>,
41
+ ) => {
45
42
  const opts = {
46
43
  ..._config,
47
44
  ...options,
48
45
  fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
49
46
  headers: mergeHeaders(_config.headers, options.headers),
50
- serializedBody: undefined,
47
+ serializedBody: undefined as string | undefined,
51
48
  };
52
49
 
53
50
  if (opts.security) {
54
- await setAuthParams({
55
- ...opts,
56
- security: opts.security,
57
- });
51
+ await setAuthParams(opts);
58
52
  }
59
53
 
60
54
  if (opts.requestValidator) {
@@ -62,7 +56,7 @@ export const createClient = (config: Config = {}): Client => {
62
56
  }
63
57
 
64
58
  if (opts.body !== undefined && opts.bodySerializer) {
65
- opts.serializedBody = opts.bodySerializer(opts.body);
59
+ opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined;
66
60
  }
67
61
 
68
62
  // remove Content-Type header if body is empty to avoid sending invalid requests
@@ -70,219 +64,191 @@ export const createClient = (config: Config = {}): Client => {
70
64
  opts.headers.delete('Content-Type');
71
65
  }
72
66
 
73
- const url = buildUrl(opts);
67
+ const resolvedOpts = opts as typeof opts &
68
+ ResolvedRequestOptions<TResponseStyle, ThrowOnError, Url>;
69
+ const url = buildUrl(resolvedOpts);
74
70
 
75
- return { opts, url };
71
+ return { opts: resolvedOpts, url };
76
72
  };
77
73
 
78
74
  const request: Client['request'] = async (options) => {
79
- // @ts-expect-error
80
- const { opts, url } = await beforeRequest(options);
81
- const requestInit: ReqInit = {
82
- redirect: 'follow',
83
- ...opts,
84
- body: getValidRequestBody(opts),
85
- };
75
+ const throwOnError = options.throwOnError ?? _config.throwOnError;
76
+ const responseStyle = options.responseStyle ?? _config.responseStyle;
86
77
 
87
- let request = new Request(url, requestInit);
88
-
89
- for (const fn of interceptors.request.fns) {
90
- if (fn) {
91
- request = await fn(request, opts);
92
- }
93
- }
94
-
95
- // fetch must be assigned here, otherwise it would throw the error:
96
- // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
97
- const _fetch = opts.fetch!;
98
- let response: Response;
78
+ let request: Request | undefined;
79
+ let response: Response | undefined;
99
80
 
100
81
  try {
101
- response = await _fetch(request);
102
- } catch (error) {
103
- // Handle fetch exceptions (AbortError, network errors, etc.)
104
- let finalError = error;
82
+ const { opts, url } = await beforeRequest(options);
83
+ const requestInit: ReqInit = {
84
+ redirect: 'follow',
85
+ ...opts,
86
+ body: getValidRequestBody(opts),
87
+ };
105
88
 
106
- for (const fn of interceptors.error.fns) {
89
+ request = new Request(url, requestInit);
90
+
91
+ for (const fn of interceptors.request.fns) {
107
92
  if (fn) {
108
- finalError = (await fn(
109
- error,
110
- undefined as any,
111
- request,
112
- opts,
113
- )) as unknown;
93
+ request = await fn(request, opts);
114
94
  }
115
95
  }
116
96
 
117
- finalError = finalError || ({} as unknown);
97
+ // fetch must be assigned here, otherwise it would throw the error:
98
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
99
+ const _fetch = opts.fetch!;
118
100
 
119
- if (opts.throwOnError) {
120
- throw finalError;
121
- }
122
-
123
- // Return error response
124
- return opts.responseStyle === 'data'
125
- ? undefined
126
- : {
127
- error: finalError,
128
- request,
129
- response: undefined as any,
130
- };
131
- }
101
+ response = await _fetch(request);
132
102
 
133
- for (const fn of interceptors.response.fns) {
134
- if (fn) {
135
- response = await fn(response, request, opts);
103
+ for (const fn of interceptors.response.fns) {
104
+ if (fn) {
105
+ response = await fn(response, request, opts);
106
+ }
136
107
  }
137
- }
138
108
 
139
- const result = {
140
- request,
141
- response,
142
- };
109
+ const result = {
110
+ request,
111
+ response,
112
+ };
113
+
114
+ if (response.ok) {
115
+ const parseAs =
116
+ (opts.parseAs === 'auto'
117
+ ? getParseAs(response.headers.get('Content-Type'))
118
+ : opts.parseAs) ?? 'json';
119
+
120
+ if (response.status === 204 || response.headers.get('Content-Length') === '0') {
121
+ let emptyData: any;
122
+ switch (parseAs) {
123
+ case 'arrayBuffer':
124
+ case 'blob':
125
+ case 'text':
126
+ emptyData = await response[parseAs]();
127
+ break;
128
+ case 'formData':
129
+ emptyData = new FormData();
130
+ break;
131
+ case 'stream':
132
+ emptyData = response.body;
133
+ break;
134
+ case 'json':
135
+ default:
136
+ emptyData = {};
137
+ break;
138
+ }
139
+ return opts.responseStyle === 'data'
140
+ ? emptyData
141
+ : {
142
+ data: emptyData,
143
+ ...result,
144
+ };
145
+ }
143
146
 
144
- if (response.ok) {
145
- const parseAs =
146
- (opts.parseAs === 'auto'
147
- ? getParseAs(response.headers.get('Content-Type'))
148
- : opts.parseAs) ?? 'json';
149
-
150
- if (
151
- response.status === 204 ||
152
- response.headers.get('Content-Length') === '0'
153
- ) {
154
- let emptyData: any;
147
+ let data: any;
155
148
  switch (parseAs) {
156
149
  case 'arrayBuffer':
157
150
  case 'blob':
151
+ case 'formData':
158
152
  case 'text':
159
- emptyData = await response[parseAs]();
153
+ data = await response[parseAs]();
160
154
  break;
161
- case 'formData':
162
- emptyData = new FormData();
155
+ case 'json': {
156
+ // Some servers return 200 with no Content-Length and empty body.
157
+ // response.json() would throw; read as text and parse if non-empty.
158
+ const text = await response.text();
159
+ data = text ? JSON.parse(text) : {};
163
160
  break;
161
+ }
164
162
  case 'stream':
165
- emptyData = response.body;
166
- break;
167
- case 'json':
168
- default:
169
- emptyData = {};
170
- break;
163
+ return opts.responseStyle === 'data'
164
+ ? response.body
165
+ : {
166
+ data: response.body,
167
+ ...result,
168
+ };
171
169
  }
170
+
171
+ if (parseAs === 'json') {
172
+ if (opts.responseValidator) {
173
+ await opts.responseValidator(data);
174
+ }
175
+
176
+ if (opts.responseTransformer) {
177
+ data = await opts.responseTransformer(data);
178
+ }
179
+ }
180
+
172
181
  return opts.responseStyle === 'data'
173
- ? emptyData
182
+ ? data
174
183
  : {
175
- data: emptyData,
184
+ data,
176
185
  ...result,
177
186
  };
178
187
  }
179
188
 
180
- let data: any;
181
- switch (parseAs) {
182
- case 'arrayBuffer':
183
- case 'blob':
184
- case 'formData':
185
- case 'text':
186
- data = await response[parseAs]();
187
- break;
188
- case 'json': {
189
- // Some servers return 200 with no Content-Length and empty body.
190
- // response.json() would throw; read as text and parse if non-empty.
191
- const text = await response.text();
192
- data = text ? JSON.parse(text) : {};
193
- break;
194
- }
195
- case 'stream':
196
- return opts.responseStyle === 'data'
197
- ? response.body
198
- : {
199
- data: response.body,
200
- ...result,
201
- };
189
+ const textError = await response.text();
190
+ let jsonError: unknown;
191
+
192
+ try {
193
+ jsonError = JSON.parse(textError);
194
+ } catch {
195
+ // noop
202
196
  }
203
197
 
204
- if (parseAs === 'json') {
205
- if (opts.responseValidator) {
206
- await opts.responseValidator(data);
207
- }
198
+ throw jsonError ?? textError;
199
+ } catch (error) {
200
+ let finalError = error;
208
201
 
209
- if (opts.responseTransformer) {
210
- data = await opts.responseTransformer(data);
202
+ for (const fn of interceptors.error.fns) {
203
+ if (fn) {
204
+ finalError = await fn(finalError, response, request, options as ResolvedRequestOptions);
211
205
  }
212
206
  }
213
207
 
214
- return opts.responseStyle === 'data'
215
- ? data
216
- : {
217
- data,
218
- ...result,
219
- };
220
- }
208
+ finalError = finalError || {};
221
209
 
222
- const textError = await response.text();
223
- let jsonError: unknown;
224
-
225
- try {
226
- jsonError = JSON.parse(textError);
227
- } catch {
228
- // noop
229
- }
230
-
231
- const error = jsonError ?? textError;
232
- let finalError = error;
233
-
234
- for (const fn of interceptors.error.fns) {
235
- if (fn) {
236
- finalError = (await fn(error, response, request, opts)) as string;
210
+ if (throwOnError) {
211
+ throw finalError;
237
212
  }
238
- }
239
213
 
240
- finalError = finalError || ({} as string);
241
-
242
- if (opts.throwOnError) {
243
- throw finalError;
214
+ // TODO: we probably want to return error and improve types
215
+ return responseStyle === 'data'
216
+ ? undefined
217
+ : {
218
+ error: finalError,
219
+ request,
220
+ response,
221
+ };
244
222
  }
245
-
246
- // TODO: we probably want to return error and improve types
247
- return opts.responseStyle === 'data'
248
- ? undefined
249
- : {
250
- error: finalError,
251
- ...result,
252
- };
253
223
  };
254
224
 
255
- const makeMethodFn =
256
- (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
257
- request({ ...options, method });
225
+ const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
226
+ request({ ...options, method });
258
227
 
259
- const makeSseFn =
260
- (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
261
- const { opts, url } = await beforeRequest(options);
262
- return createSseClient({
263
- ...opts,
264
- body: opts.body as BodyInit | null | undefined,
265
- headers: opts.headers as unknown as Record<string, string>,
266
- method,
267
- onRequest: async (url, init) => {
268
- let request = new Request(url, init);
269
- for (const fn of interceptors.request.fns) {
270
- if (fn) {
271
- request = await fn(request, opts);
272
- }
228
+ const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
229
+ const { opts, url } = await beforeRequest(options);
230
+ return createSseClient({
231
+ ...opts,
232
+ body: opts.body as BodyInit | null | undefined,
233
+ method,
234
+ onRequest: async (url, init) => {
235
+ let request = new Request(url, init);
236
+ for (const fn of interceptors.request.fns) {
237
+ if (fn) {
238
+ request = await fn(request, opts);
273
239
  }
274
- return request;
275
- },
276
- serializedBody: getValidRequestBody(opts) as
277
- | BodyInit
278
- | null
279
- | undefined,
280
- url,
281
- });
282
- };
240
+ }
241
+ return request;
242
+ },
243
+ serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined,
244
+ url,
245
+ });
246
+ };
247
+
248
+ const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options });
283
249
 
284
250
  return {
285
- buildUrl,
251
+ buildUrl: _buildUrl,
286
252
  connect: makeMethodFn('CONNECT'),
287
253
  delete: makeMethodFn('DELETE'),
288
254
  get: makeMethodFn('GET'),
package/client/index.ts CHANGED
@@ -9,6 +9,8 @@ export {
9
9
  } from '../core/bodySerializer.gen.js';
10
10
  export { buildClientParams } from '../core/params.gen.js';
11
11
  export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen.js';
12
+ export type { ServerSentEventsResult } from '../core/serverSentEvents.gen.js';
13
+ export type { ClientMeta } from '../core/types.gen.js';
12
14
  export { createClient } from './client.gen.js';
13
15
  export type {
14
16
  Client,
@@ -5,17 +5,13 @@ import type {
5
5
  ServerSentEventsOptions,
6
6
  ServerSentEventsResult,
7
7
  } from '../core/serverSentEvents.gen.js';
8
- import type {
9
- Client as CoreClient,
10
- Config as CoreConfig,
11
- } from '../core/types.gen.js';
8
+ import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen.js';
12
9
  import type { Middleware } from './utils.gen.js';
13
10
 
14
11
  export type ResponseStyle = 'data' | 'fields';
15
12
 
16
13
  export interface Config<T extends ClientOptions = ClientOptions>
17
- extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
18
- CoreConfig {
14
+ extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
19
15
  /**
20
16
  * Base URL for all requests made by this client.
21
17
  */
@@ -42,14 +38,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
42
38
  *
43
39
  * @default 'auto'
44
40
  */
45
- parseAs?:
46
- | 'arrayBuffer'
47
- | 'auto'
48
- | 'blob'
49
- | 'formData'
50
- | 'json'
51
- | 'stream'
52
- | 'text';
41
+ parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text';
53
42
  /**
54
43
  * Should we return only data or multiple fields (data, error, response, etc.)?
55
44
  *
@@ -69,12 +58,15 @@ export interface RequestOptions<
69
58
  TResponseStyle extends ResponseStyle = 'fields',
70
59
  ThrowOnError extends boolean = boolean,
71
60
  Url extends string = string,
72
- > extends Config<{
61
+ >
62
+ extends
63
+ Config<{
73
64
  responseStyle: TResponseStyle;
74
65
  throwOnError: ThrowOnError;
75
66
  }>,
76
67
  Pick<
77
68
  ServerSentEventsOptions<TData>,
69
+ | 'onRequest'
78
70
  | 'onSseError'
79
71
  | 'onSseEvent'
80
72
  | 'sseDefaultRetryDelay'
@@ -101,6 +93,7 @@ export interface ResolvedRequestOptions<
101
93
  ThrowOnError extends boolean = boolean,
102
94
  Url extends string = string,
103
95
  > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
96
+ headers: Headers;
104
97
  serializedBody?: string;
105
98
  }
106
99
 
@@ -116,36 +109,28 @@ export type RequestResult<
116
109
  ? TData[keyof TData]
117
110
  : TData
118
111
  : {
119
- data: TData extends Record<string, unknown>
120
- ? TData[keyof TData]
121
- : TData;
112
+ data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
122
113
  request: Request;
123
114
  response: Response;
124
115
  }
125
116
  >
126
117
  : Promise<
127
118
  TResponseStyle extends 'data'
128
- ?
129
- | (TData extends Record<string, unknown>
130
- ? TData[keyof TData]
131
- : TData)
132
- | undefined
119
+ ? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
133
120
  : (
134
121
  | {
135
- data: TData extends Record<string, unknown>
136
- ? TData[keyof TData]
137
- : TData;
122
+ data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
138
123
  error: undefined;
139
124
  }
140
125
  | {
141
126
  data: undefined;
142
- error: TError extends Record<string, unknown>
143
- ? TError[keyof TError]
144
- : TError;
127
+ error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
145
128
  }
146
129
  ) & {
147
- request: Request;
148
- response: Response;
130
+ /** request may be undefined, because error may be from building the request object itself */
131
+ request?: Request;
132
+ /** response may be undefined, because error may be from building the request object itself or from a network error */
133
+ response?: Response;
149
134
  }
150
135
  >;
151
136
 
@@ -166,12 +151,13 @@ type MethodFn = <
166
151
 
167
152
  type SseFn = <
168
153
  TData = unknown,
169
- TError = unknown,
154
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
155
+ _TError = unknown,
170
156
  ThrowOnError extends boolean = false,
171
157
  TResponseStyle extends ResponseStyle = 'fields',
172
158
  >(
173
- options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
174
- ) => Promise<ServerSentEventsResult<TData, TError>>;
159
+ options: Omit<RequestOptions<never, TResponseStyle, ThrowOnError>, 'method'>,
160
+ ) => Promise<ServerSentEventsResult<TData>>;
175
161
 
176
162
  type RequestFn = <
177
163
  TData = unknown,
@@ -180,10 +166,7 @@ type RequestFn = <
180
166
  TResponseStyle extends ResponseStyle = 'fields',
181
167
  >(
182
168
  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
183
- Pick<
184
- Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
185
- 'method'
186
- >,
169
+ Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>,
187
170
  ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
188
171
 
189
172
  type BuildUrlFn = <
@@ -197,13 +180,7 @@ type BuildUrlFn = <
197
180
  options: TData & Options<TData>,
198
181
  ) => string;
199
182
 
200
- export type Client = CoreClient<
201
- RequestFn,
202
- Config,
203
- MethodFn,
204
- BuildUrlFn,
205
- SseFn
206
- > & {
183
+ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
207
184
  interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
208
185
  };
209
186