/
builder.ts
255 lines (228 loc) · 5.98 KB
/
builder.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
import FormData from 'form-data'
import { BodyInit, RequestInit } from 'node-fetch'
import { defaultQueryParser, QueryParser } from './query'
/**
* Enum of http method names
*/
enum HttpMethod {
Head = 'HEAD',
Options = 'OPTIONS',
Get = 'GET',
Patch = 'PATCH',
Put = 'PUT',
Post = 'POST',
Delete = 'DELETE',
}
interface BuilderConfig {
baseURL: string
path: string
queryParams?: Record<string, unknown>
}
/**
* The configuration object that the RequestBuilder is designed to produce.
*/
interface RequestOptions {
url: string
options: RequestInit
}
/**
* Fluent-style API for building up HTTP requests.
*/
export class RequestBuilder {
private _options: RequestInit = {}
private config: BuilderConfig
constructor(
baseURL: string,
private readonly queryParser: QueryParser = defaultQueryParser
) {
this.config = { baseURL, path: '' }
}
/**
* Serializes all the current options into an object which can be used to
* make a request using the `fetch` API.
*
* @private
* @returns options for making a fetch request
*/
public toRequestOptions(): RequestOptions {
const path = this.config.path.startsWith('/')
? this.config.path
: `/${this.config.path}`
return {
url: `${this.config.baseURL}${path}${this.parseQueryParams()}`,
options: this._options,
}
}
/**
* Make a HEAD request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public head(path = '') {
return this.setMethod(HttpMethod.Head, path)
}
/**
* Make an OPTIONS request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public options(path = '') {
return this.setMethod(HttpMethod.Options, path)
}
/**
* Make a GET request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public get(path = '') {
return this.setMethod(HttpMethod.Get, path)
}
/**
* Make a PATCH request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public patch(path = '') {
return this.setMethod(HttpMethod.Patch, path)
}
/**
* Make a PUT request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public put(path = '') {
return this.setMethod(HttpMethod.Put, path)
}
/**
* Make a POST request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public post(path = '') {
return this.setMethod(HttpMethod.Post, path)
}
/**
* Make a DELETE request
*
* @param path a url pathname
* @returns the builder object, for chaining
*/
public delete(path = '') {
return this.setMethod(HttpMethod.Delete, path)
}
/**
* Set the raw body on the request. This could be as simple as a string, or
* as complex as a stream.
*
* @param body the raw body to be set on the request
* @returns the builder object, for chaining
*/
public body(body: BodyInit) {
this._options.body = body
return this
}
/**
* Encodes an object as form data and sets it as the body of the request, along
* with setting the Content-Type header to multipart/form-data
*
* @param params a JSON-like object to be encoded as form data on the request
* @returns the builder object, for chaining
*/
public formData(params: Record<string, unknown>) {
const formData = Object.entries(params).reduce<FormData>(
(form, [key, value]) => {
form.append(key, value)
return form
},
new FormData()
)
return this.body(formData).header('Content-Type', 'multipart/form-data')
}
/**
* Encodes a value as JSON and sets it as the body of the request, along
* with setting the Content-Type header to application/json
*
* @param value a JSON-stringifiable value to be encoded as JSON on the request
* @returns the builder object, for chaining
*/
public json(value: any) {
return this.body(JSON.stringify(value)).header(
'Content-Type',
'application/json'
)
}
/**
* Encodes and sets query parameters on the URL. Uses the query parser that
* the RequestBuilder was intiialized with. To customize how query
* parameters are parsed, you can pass in a function into the microtest
* runner configuration.
*
* @example
* const runner = microtest('http://localhost:3000', {
* queryParser: (paramsObject) => 'custom parser logic'
* })
*
* @param params an object representing the query params to be stringified and set on the request
* @returns the builder object, for chaining
*/
public query(params: Record<string, unknown>) {
this.config.queryParams = params
return this
}
/**
* Sets a key/value pair as a header on the request.
*
* @param key the header key, e.g. 'Content-Type'
* @param value the header value, e.g. 'text/plain'
* @returns the builder object, for chaining
*/
public header(key: string, value: string) {
if (!this._options.headers) {
this._options.headers = {}
}
this._options.headers = {
...this._options.headers,
[key]: value,
}
return this
}
/**
* Override fetch options directly. This is useful if you want to provide
* highly customized logic to the underlying request, such as providing an
* AbortController.
*
* Note that this merges your options directly with the builder's underlying
* options, so if, for example, you previously set a header, but now
* override `headers`, your previous changes will not be reflected in the
* request. For this reason, it's strongly recommended to use this method
* _only_ for options that are _not_ provided elsewhere on the builder.
*
* @param options options which will be passed directly to `fetch`
* @returns the builder object, for chaining
*/
public fetchOptions(options: Partial<RequestInit>) {
this._options = {
...this._options,
...options,
}
return this
}
private setMethod(method: HttpMethod, path: string) {
this._options.method = method
this.config.path = path
return this
}
private parseQueryParams() {
const params = this.config.queryParams
if (!params) {
return ''
}
return `?${this.queryParser(params)}`
}
}