-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
PetitioRequest.ts
319 lines (291 loc) · 10.1 KB
/
PetitioRequest.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/**
* @module PetitioRequest
*/
// @ts-expect-error 7016 - Unusual type exports
import Client from "undici/lib/core/client";
import type ClientType from "undici/types/client"; // eslint-disable-line node/no-missing-import
import type { IncomingHttpHeaders } from "http";
import type { ParsedUrlQueryInput } from "querystring";
import { PetitioResponse } from "./PetitioResponse";
import type { Readable } from "stream";
import { URL } from "url";
import { join } from "path";
import { stringify } from "querystring"; // eslint-disable-line no-duplicate-imports
/**
* Accepted HTTP methods (currently only supports up to HTTP/1.1).
*/
export type HTTPMethod = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
/**
* @see [Undici ClientOptions timeout documentation](https://github.com/nodejs/undici/blob/main/docs/api/Client.md#parameter-clientoptions)
*/
export interface TimeoutOptions {
bodyTimeout?: number;
headersTimeout?: number;
keepAliveTimeout?: number
}
export class PetitioRequest {
/**
* Options to use for Undici under the hood.
* @see [Undici ClientOptions documentation](https://github.com/nodejs/undici/blob/main/docs/api/Client.md#parameter-clientoptions)
*/
public coreOptions: ClientType.Options = {};
/**
* The data to be sent as the request body.
* This will be a buffer or string for normal requests, or a stream.Readable
* if the request is to be sent as a stream.
*/
public data?: Buffer | string | Readable;
/**
* @see [[HTTPMethod]]
*/
public httpMethod: HTTPMethod = "GET";
/**
* @see [[PetitioRequest.client]]
*/
public kClient?: ClientType;
/**
* Whether [[PetitioRequest.kClient]] will persist between [[PetitioRequest.send]]
* calls. It is recommended to enable this for superior performance.
*/
public keepClient?: boolean;
/**
* The headers to attach to the request.
*/
public reqHeaders: IncomingHttpHeaders = {};
/**
* The timeout options for the Undici client.
* @see [[TimeoutOptions]]
*/
public timeoutOptions: TimeoutOptions = {};
/**
* The URL destination for the request, targeted in [[PetitioRequest.send]].
*/
public url: URL;
/**
* @param {(string | URL)} url The URL to start composing a request for.
* @param {HTTPMethod} [httpMethod="GET"] The HTTP method to use.
* @return {PetitioRequest} The Petitio request instance for your URL.
*/
public constructor(url: string | URL, httpMethod: HTTPMethod = "GET") {
this.url = typeof url === "string" ? new URL(url) : url;
this.httpMethod = httpMethod;
if (!["http:", "https:"].includes(this.url.protocol)) throw new Error(`Bad URL protocol: ${this.url.protocol}`);
}
/**
* @param {ClientType} client The Undici client instance you wish to use.
* @param {boolean} keepAlive Whether to persist the client across requests or not.
* @return {*} The request object for further composition.
* @see [Undici Client documentation](https://github.com/nodejs/undici/blob/main/docs/api/Client.md)
*/
public client(client: ClientType, keepAlive?: boolean): this {
this.kClient = client;
if (keepAlive) this.keepClient = true;
return this;
}
/**
* @param {*} key The query key to use for the URL query parameters.
* @param {*} value The value to set the query key to.
* @example
* If you wish to make a query at https://example.com/index?query=parameter
* you can use `.query("query", "parameter")`.
*/
public query(key: string, value: any): this
/**
* @param {*} key An object of query keys and their respective values.
* @example
* If you wish to make multiple queries at once, you can use
* `.query({"keyOne": "hello", "keyTwo": "world!"})`.
*/
public query(key: Record<string, any>): this
public query(key: string | Record<string, any>, value?: any): this {
if (typeof key === "object") for (const qy of Object.keys(key)) this.url.searchParams.append(qy, key[qy]);
else this.url.searchParams.append(key, value);
return this;
}
/**
* @param {string} relativePath A path to resolve relative to the current URL.
* @return {*} The request object for further composition.
* @example `https://example.org/hello/world` with `.path("../petitio")`
* would resolve to `https://example.org/hello/petitio`.
*/
public path(relativePath: string): this {
this.url.pathname = join(this.url.pathname, relativePath);
return this;
}
/**
* @param {*} data The data to be set for the request body.
*/
public body(data: Buffer | string): this
/**
* @param {*} data The data to be set for the request body.
* @param {*} sendAs If data is set to any object type value other than a
* buffer or this is set to `json`, the `Content-Type` header will be set to
* `application/json` and the request data will be set to the stringified
* JSON form of the supplied data.
*/
public body(data: Record<string, any>, sendAs?: "json"): this
/**
* @param {*} data The data to be set for the request body.
* @param {*} sendAs If data is a string or a parsed object of query
* parameters *AND* this is set to `form`, the `Content-Type` header will be
* set to `application/x-www-form-urlencoded` and the request data will be
* set to the URL encoded version of the query string.
*/
public body(data: ParsedUrlQueryInput | string, sendAs: "form"): this
/**
* @param {*} data The data to be set for the request body.
* @param {*} sendAs If data is a stream.Readable *AND* this is set to
* `stream`, the body will be sent as the stream with no modifications to
* it or the headers.
*/
public body(data: Readable, sendAs: "stream"): this
public body(data: any, sendAs?: "json" | "form" | "stream"): this {
switch (sendAs) {
case "json": {
this.data = JSON.stringify(data);
this.header({
"content-type": "application/json",
"content-length": Buffer.byteLength(this.data).toString()
});
break;
}
case "form": {
this.data = stringify(data);
this.header({
"content-type": "application/x-www-form-urlencoded",
"content-length": Buffer.byteLength(this.data).toString()
});
break;
}
case "stream": {
this.data = data;
break;
}
default: {
if (typeof data === "object" && !Buffer.isBuffer(data)) {
this.data = JSON.stringify(data);
this.header({
"content-type": "application/json",
"content-length": Buffer.byteLength(this.data as string).toString()
});
} else {
this.data = data;
this.header("content-length", Buffer.byteLength(this.data as string | Buffer).toString());
}
break;
}
}
return this;
}
/**
* @param {*} header The encoded header name to set.
* @param {*} value The value to set the header to.
*/
public header(header: string, value: string): this
/**
* @param {*} header An object of keys and values to set headers to.
*/
public header(header: Record<string, string>): this
public header(header: string | Record<string, string>, value?: string): this {
// eslint-disable-next-line max-len
if (typeof header === "object") for (const hN of Object.keys(header)) this.reqHeaders[hN.toLowerCase()] = header[hN];
else this.reqHeaders[header.toLowerCase()] = value;
return this;
}
/**
* @param {*} method The HTTP method to change the request to.
* @return {*} The request object for further composition.
*/
public method(method: HTTPMethod): this {
this.httpMethod = method;
return this;
}
/**
* @param {*} timeout The timeout (in milliseconds) to set the `bodyTimeout`
* to.
* @see [[TimeoutOptions.bodyTimeout]]
*/
public timeout(timeout: number): this
/**
* @param {*} timeout The timeout option to change.
* @param {*} time The number of milliseconds to set the timeout to.
* @see [[TimeoutOptions]]
*/
public timeout(timeout: keyof TimeoutOptions, time: number): this
public timeout(timeout: keyof TimeoutOptions | number, time?: number): this {
if (typeof timeout === "string") this.timeoutOptions[timeout] = time;
else this.timeoutOptions.bodyTimeout = timeout;
return this;
}
/**
* @param {*} key An object of key-value options to set for Undici.
* @see [Undici Client documentation](https://github.com/nodejs/undici/blob/main/docs/api/Client.md)
*/
public option(key: ClientType.Options): this
/**
* @template T
* @param {T} key The client options key to set.
* @param {ClientType.Options[T]} value The value to set the client option to (type checked).
* @see [Undici Client documentation](https://github.com/nodejs/undici/blob/main/docs/api/Client.md)
*/
public option<T extends keyof ClientType.Options>(key: T, value: ClientType.Options[T]): this
public option(key: keyof ClientType.Options | ClientType.Options, value?: any) {
if (typeof key === "object") Object.assign(this.coreOptions, key);
else this.coreOptions[key] = value;
return this;
}
/**
* @template T Type casting parameter for the JSON result.
* @return {*} A serialized object result from sending the request.
*/
public async json<T = any>(): Promise<T> {
const res = await this.send();
return res.json<T>();
}
/**
* @return {*} The raw response body as a buffer.
*/
public async raw(): Promise<Buffer> {
const res = await this.send();
return res.body;
}
/**
* @return {*} The raw response body as a string.
*/
public async text(): Promise<string> {
const res = await this.send();
return res.text();
}
/**
* Finalizes and sends the composable request to the target server.
* @return {*} The response object.
*/
public send(): Promise<PetitioResponse> {
return new Promise((resolve, reject) => {
const options: ClientType.RequestOptions = {
path: this.url.pathname + this.url.search,
method: this.httpMethod,
headers: this.reqHeaders,
body: this.data
};
const client = this.kClient ?? new Client(this.url.origin, this.coreOptions);
const res: PetitioResponse = new PetitioResponse();
client.dispatch(options, {
onData: (data: Buffer) => {
return res._addChunk(data);
},
onError: (err: Error) => reject(err),
onComplete: () => {
if (!this.keepClient) client.close();
resolve(res);
},
onConnect: () => null,
onHeaders: (statusCode: number, headers: string[], resume: () => void) => {
res.statusCode = statusCode;
res._parseHeaders(headers);
resume();
}
});
});
}
}