1 | "use strict";
|
---|
2 |
|
---|
3 | var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
|
---|
4 | exports.__esModule = true;
|
---|
5 | exports.default = http;
|
---|
6 | exports.encodeFormOrQuery = encodeFormOrQuery;
|
---|
7 | exports.isFile = isFile;
|
---|
8 | exports.makeHttp = makeHttp;
|
---|
9 | exports.mergeInQueryOrForm = mergeInQueryOrForm;
|
---|
10 | exports.self = void 0;
|
---|
11 | exports.serializeHeaders = serializeHeaders;
|
---|
12 | exports.serializeRes = serializeRes;
|
---|
13 | exports.shouldDownloadAsText = void 0;
|
---|
14 | var _qs = _interopRequireDefault(require("qs"));
|
---|
15 | var _jsYaml = _interopRequireDefault(require("js-yaml"));
|
---|
16 | require("../helpers/fetch-polyfill.node.js");
|
---|
17 | var _styleSerializer = require("../execute/oas3/style-serializer.js");
|
---|
18 | // For testing
|
---|
19 | const self = exports.self = {
|
---|
20 | serializeRes,
|
---|
21 | mergeInQueryOrForm
|
---|
22 | };
|
---|
23 |
|
---|
24 | // Handles fetch-like syntax and the case where there is only one object passed-in
|
---|
25 | // (which will have the URL as a property). Also serializes the response.
|
---|
26 | async function http(url, request = {}) {
|
---|
27 | if (typeof url === 'object') {
|
---|
28 | request = url;
|
---|
29 | url = request.url;
|
---|
30 | }
|
---|
31 | request.headers = request.headers || {};
|
---|
32 |
|
---|
33 | // Serializes query, for convenience
|
---|
34 | // Should be the last thing we do, as its hard to mutate the URL with
|
---|
35 | // the search string, but much easier to manipulate the req.query object
|
---|
36 | self.mergeInQueryOrForm(request);
|
---|
37 |
|
---|
38 | // Newlines in header values cause weird error messages from `window.fetch`,
|
---|
39 | // so let's message them out.
|
---|
40 | // Context: https://stackoverflow.com/a/50709178
|
---|
41 | if (request.headers) {
|
---|
42 | Object.keys(request.headers).forEach(headerName => {
|
---|
43 | const value = request.headers[headerName];
|
---|
44 | if (typeof value === 'string') {
|
---|
45 | request.headers[headerName] = value.replace(/\n+/g, ' ');
|
---|
46 | }
|
---|
47 | });
|
---|
48 | }
|
---|
49 |
|
---|
50 | // Wait for the request interceptor, if it was provided
|
---|
51 | // WARNING: don't put anything between this and the request firing unless
|
---|
52 | // you have a good reason!
|
---|
53 | if (request.requestInterceptor) {
|
---|
54 | request = (await request.requestInterceptor(request)) || request;
|
---|
55 | }
|
---|
56 |
|
---|
57 | /**
|
---|
58 | * For content-type=multipart/form-data remove content-type from request before fetch,
|
---|
59 | * so that correct one with `boundary` is set when request body is different from boundary encoded string.
|
---|
60 | */
|
---|
61 | const contentType = request.headers['content-type'] || request.headers['Content-Type'];
|
---|
62 | if (/multipart\/form-data/i.test(contentType)) {
|
---|
63 | delete request.headers['content-type'];
|
---|
64 | delete request.headers['Content-Type'];
|
---|
65 | }
|
---|
66 |
|
---|
67 | // eslint-disable-next-line no-undef
|
---|
68 | let res;
|
---|
69 | try {
|
---|
70 | res = await (request.userFetch || fetch)(request.url, request);
|
---|
71 | res = await self.serializeRes(res, url, request);
|
---|
72 | if (request.responseInterceptor) {
|
---|
73 | res = (await request.responseInterceptor(res)) || res;
|
---|
74 | }
|
---|
75 | } catch (resError) {
|
---|
76 | if (!res) {
|
---|
77 | // res is completely absent, so we can't construct our own error
|
---|
78 | // so we'll just throw the error we got
|
---|
79 | throw resError;
|
---|
80 | }
|
---|
81 | const error = new Error(res.statusText || `response status is ${res.status}`);
|
---|
82 | error.status = res.status;
|
---|
83 | error.statusCode = res.status;
|
---|
84 | error.responseError = resError;
|
---|
85 | throw error;
|
---|
86 | }
|
---|
87 | if (!res.ok) {
|
---|
88 | const error = new Error(res.statusText || `response status is ${res.status}`);
|
---|
89 | error.status = res.status;
|
---|
90 | error.statusCode = res.status;
|
---|
91 | error.response = res;
|
---|
92 | throw error;
|
---|
93 | }
|
---|
94 | return res;
|
---|
95 | }
|
---|
96 |
|
---|
97 | // exported for testing
|
---|
98 | const shouldDownloadAsText = (contentType = '') => /(json|xml|yaml|text)\b/.test(contentType);
|
---|
99 | exports.shouldDownloadAsText = shouldDownloadAsText;
|
---|
100 | function parseBody(body, contentType) {
|
---|
101 | if (contentType && (contentType.indexOf('application/json') === 0 || contentType.indexOf('+json') > 0)) {
|
---|
102 | return JSON.parse(body);
|
---|
103 | }
|
---|
104 | return _jsYaml.default.load(body);
|
---|
105 | }
|
---|
106 |
|
---|
107 | // Serialize the response, returns a promise with headers and the body part of the hash
|
---|
108 | function serializeRes(oriRes, url, {
|
---|
109 | loadSpec = false
|
---|
110 | } = {}) {
|
---|
111 | const res = {
|
---|
112 | ok: oriRes.ok,
|
---|
113 | url: oriRes.url || url,
|
---|
114 | status: oriRes.status,
|
---|
115 | statusText: oriRes.statusText,
|
---|
116 | headers: serializeHeaders(oriRes.headers)
|
---|
117 | };
|
---|
118 | const contentType = res.headers['content-type'];
|
---|
119 | const useText = loadSpec || shouldDownloadAsText(contentType);
|
---|
120 | const getBody = useText ? oriRes.text : oriRes.blob || oriRes.buffer;
|
---|
121 | return getBody.call(oriRes).then(body => {
|
---|
122 | res.text = body;
|
---|
123 | res.data = body;
|
---|
124 | if (useText) {
|
---|
125 | try {
|
---|
126 | const obj = parseBody(body, contentType);
|
---|
127 | res.body = obj;
|
---|
128 | res.obj = obj;
|
---|
129 | } catch (e) {
|
---|
130 | res.parseError = e;
|
---|
131 | }
|
---|
132 | }
|
---|
133 | return res;
|
---|
134 | });
|
---|
135 | }
|
---|
136 | function serializeHeaderValue(value) {
|
---|
137 | const isMulti = value.includes(', ');
|
---|
138 | return isMulti ? value.split(', ') : value;
|
---|
139 | }
|
---|
140 |
|
---|
141 | // Serialize headers into a hash, where mutliple-headers result in an array.
|
---|
142 | //
|
---|
143 | // eg: Cookie: one
|
---|
144 | // Cookie: two
|
---|
145 | // = { Cookie: [ "one", "two" ]
|
---|
146 | function serializeHeaders(headers = {}) {
|
---|
147 | if (typeof headers.entries !== 'function') return {};
|
---|
148 | return Array.from(headers.entries()).reduce((acc, [header, value]) => {
|
---|
149 | acc[header] = serializeHeaderValue(value);
|
---|
150 | return acc;
|
---|
151 | }, {});
|
---|
152 | }
|
---|
153 | function isFile(obj, navigatorObj) {
|
---|
154 | if (!navigatorObj && typeof navigator !== 'undefined') {
|
---|
155 | // eslint-disable-next-line no-undef
|
---|
156 | navigatorObj = navigator;
|
---|
157 | }
|
---|
158 | if (navigatorObj && navigatorObj.product === 'ReactNative') {
|
---|
159 | if (obj && typeof obj === 'object' && typeof obj.uri === 'string') {
|
---|
160 | return true;
|
---|
161 | }
|
---|
162 | return false;
|
---|
163 | }
|
---|
164 | if (typeof File !== 'undefined' && obj instanceof File) {
|
---|
165 | return true;
|
---|
166 | }
|
---|
167 | if (typeof Blob !== 'undefined' && obj instanceof Blob) {
|
---|
168 | return true;
|
---|
169 | }
|
---|
170 | if (ArrayBuffer.isView(obj)) {
|
---|
171 | return true;
|
---|
172 | }
|
---|
173 | return obj !== null && typeof obj === 'object' && typeof obj.pipe === 'function';
|
---|
174 | }
|
---|
175 | function isArrayOfFile(obj, navigatorObj) {
|
---|
176 | return Array.isArray(obj) && obj.some(v => isFile(v, navigatorObj));
|
---|
177 | }
|
---|
178 | const STYLE_SEPARATORS = {
|
---|
179 | form: ',',
|
---|
180 | spaceDelimited: '%20',
|
---|
181 | pipeDelimited: '|'
|
---|
182 | };
|
---|
183 | const SEPARATORS = {
|
---|
184 | csv: ',',
|
---|
185 | ssv: '%20',
|
---|
186 | tsv: '%09',
|
---|
187 | pipes: '|'
|
---|
188 | };
|
---|
189 |
|
---|
190 | /**
|
---|
191 | * Specialized sub-class of File class, that only
|
---|
192 | * accepts string data and retain this data in `data`
|
---|
193 | * public property throughout the lifecycle of its instances.
|
---|
194 | *
|
---|
195 | * This sub-class is exclusively used only when Encoding Object
|
---|
196 | * is defined within the Media Type Object (OpenAPI 3.x.y).
|
---|
197 | */
|
---|
198 | class FileWithData extends File {
|
---|
199 | constructor(data, name = '', options = {}) {
|
---|
200 | super([data], name, options);
|
---|
201 | this.data = data;
|
---|
202 | }
|
---|
203 | valueOf() {
|
---|
204 | return this.data;
|
---|
205 | }
|
---|
206 | toString() {
|
---|
207 | return this.valueOf();
|
---|
208 | }
|
---|
209 | }
|
---|
210 |
|
---|
211 | // Formats a key-value and returns an array of key-value pairs.
|
---|
212 | //
|
---|
213 | // Return value example 1: [['color', 'blue']]
|
---|
214 | // Return value example 2: [['color', 'blue,black,brown']]
|
---|
215 | // Return value example 3: [['color', ['blue', 'black', 'brown']]]
|
---|
216 | // Return value example 4: [['color', 'R,100,G,200,B,150']]
|
---|
217 | // Return value example 5: [['R', '100'], ['G', '200'], ['B', '150']]
|
---|
218 | // Return value example 6: [['color[R]', '100'], ['color[G]', '200'], ['color[B]', '150']]
|
---|
219 | function formatKeyValue(key, input, skipEncoding = false) {
|
---|
220 | const {
|
---|
221 | collectionFormat,
|
---|
222 | allowEmptyValue,
|
---|
223 | serializationOption,
|
---|
224 | encoding
|
---|
225 | } = input;
|
---|
226 | // `input` can be string
|
---|
227 | const value = typeof input === 'object' && !Array.isArray(input) ? input.value : input;
|
---|
228 | const encodeFn = skipEncoding ? k => k.toString() : k => encodeURIComponent(k);
|
---|
229 | const encodedKey = encodeFn(key);
|
---|
230 | if (typeof value === 'undefined' && allowEmptyValue) {
|
---|
231 | return [[encodedKey, '']];
|
---|
232 | }
|
---|
233 |
|
---|
234 | // file
|
---|
235 | if (isFile(value) || isArrayOfFile(value)) {
|
---|
236 | return [[encodedKey, value]];
|
---|
237 | }
|
---|
238 |
|
---|
239 | // for OAS 3 Parameter Object for serialization
|
---|
240 | if (serializationOption) {
|
---|
241 | return formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption);
|
---|
242 | }
|
---|
243 |
|
---|
244 | // for OAS 3 Encoding Object
|
---|
245 | if (encoding) {
|
---|
246 | if ([typeof encoding.style, typeof encoding.explode, typeof encoding.allowReserved].some(type => type !== 'undefined')) {
|
---|
247 | const {
|
---|
248 | style,
|
---|
249 | explode,
|
---|
250 | allowReserved
|
---|
251 | } = encoding;
|
---|
252 | return formatKeyValueBySerializationOption(key, value, skipEncoding, {
|
---|
253 | style,
|
---|
254 | explode,
|
---|
255 | allowReserved
|
---|
256 | });
|
---|
257 | }
|
---|
258 | if (typeof encoding.contentType === 'string') {
|
---|
259 | if (encoding.contentType.startsWith('application/json')) {
|
---|
260 | // if value is a string, assume value is already a JSON string
|
---|
261 | const json = typeof value === 'string' ? value : JSON.stringify(value);
|
---|
262 | const encodedJson = encodeFn(json);
|
---|
263 | const file = new FileWithData(encodedJson, 'blob', {
|
---|
264 | type: encoding.contentType
|
---|
265 | });
|
---|
266 | return [[encodedKey, file]];
|
---|
267 | }
|
---|
268 | const encodedData = encodeFn(String(value));
|
---|
269 | const blob = new FileWithData(encodedData, 'blob', {
|
---|
270 | type: encoding.contentType
|
---|
271 | });
|
---|
272 | return [[encodedKey, blob]];
|
---|
273 | }
|
---|
274 |
|
---|
275 | // Primitive
|
---|
276 | if (typeof value !== 'object') {
|
---|
277 | return [[encodedKey, encodeFn(value)]];
|
---|
278 | }
|
---|
279 |
|
---|
280 | // Array of primitives
|
---|
281 | if (Array.isArray(value) && value.every(v => typeof v !== 'object')) {
|
---|
282 | return [[encodedKey, value.map(encodeFn).join(',')]];
|
---|
283 | }
|
---|
284 |
|
---|
285 | // Array or object
|
---|
286 | return [[encodedKey, encodeFn(JSON.stringify(value))]];
|
---|
287 | }
|
---|
288 |
|
---|
289 | // for OAS 2 Parameter Object
|
---|
290 | // Primitive
|
---|
291 | if (typeof value !== 'object') {
|
---|
292 | return [[encodedKey, encodeFn(value)]];
|
---|
293 | }
|
---|
294 |
|
---|
295 | // Array
|
---|
296 | if (Array.isArray(value)) {
|
---|
297 | if (collectionFormat === 'multi') {
|
---|
298 | // In case of multipart/formdata, it is used as array.
|
---|
299 | // Otherwise, the caller will convert it to a query by qs.stringify.
|
---|
300 | return [[encodedKey, value.map(encodeFn)]];
|
---|
301 | }
|
---|
302 | return [[encodedKey, value.map(encodeFn).join(SEPARATORS[collectionFormat || 'csv'])]];
|
---|
303 | }
|
---|
304 |
|
---|
305 | // Object
|
---|
306 | return [[encodedKey, '']];
|
---|
307 | }
|
---|
308 | function formatKeyValueBySerializationOption(key, value, skipEncoding, serializationOption) {
|
---|
309 | const style = serializationOption.style || 'form';
|
---|
310 | const explode = typeof serializationOption.explode === 'undefined' ? style === 'form' : serializationOption.explode;
|
---|
311 | // eslint-disable-next-line no-nested-ternary
|
---|
312 | const escape = skipEncoding ? false : serializationOption && serializationOption.allowReserved ? 'unsafe' : 'reserved';
|
---|
313 | const encodeFn = v => (0, _styleSerializer.encodeDisallowedCharacters)(v, {
|
---|
314 | escape
|
---|
315 | });
|
---|
316 | const encodeKeyFn = skipEncoding ? k => k : k => (0, _styleSerializer.encodeDisallowedCharacters)(k, {
|
---|
317 | escape
|
---|
318 | });
|
---|
319 |
|
---|
320 | // Primitive
|
---|
321 | if (typeof value !== 'object') {
|
---|
322 | return [[encodeKeyFn(key), encodeFn(value)]];
|
---|
323 | }
|
---|
324 |
|
---|
325 | // Array
|
---|
326 | if (Array.isArray(value)) {
|
---|
327 | if (explode) {
|
---|
328 | // In case of multipart/formdata, it is used as array.
|
---|
329 | // Otherwise, the caller will convert it to a query by qs.stringify.
|
---|
330 | return [[encodeKeyFn(key), value.map(encodeFn)]];
|
---|
331 | }
|
---|
332 | return [[encodeKeyFn(key), value.map(encodeFn).join(STYLE_SEPARATORS[style])]];
|
---|
333 | }
|
---|
334 |
|
---|
335 | // Object
|
---|
336 | if (style === 'deepObject') {
|
---|
337 | return Object.keys(value).map(valueKey => [encodeKeyFn(`${key}[${valueKey}]`), encodeFn(value[valueKey])]);
|
---|
338 | }
|
---|
339 | if (explode) {
|
---|
340 | return Object.keys(value).map(valueKey => [encodeKeyFn(valueKey), encodeFn(value[valueKey])]);
|
---|
341 | }
|
---|
342 | return [[encodeKeyFn(key), Object.keys(value).map(valueKey => [`${encodeKeyFn(valueKey)},${encodeFn(value[valueKey])}`]).join(',')]];
|
---|
343 | }
|
---|
344 | function buildFormData(reqForm) {
|
---|
345 | /**
|
---|
346 | * Build a new FormData instance, support array as field value
|
---|
347 | * OAS2.0 - when collectionFormat is multi
|
---|
348 | * OAS3.0 - when explode of Encoding Object is true
|
---|
349 | *
|
---|
350 | * This function explicitly handles Buffers (for backward compatibility)
|
---|
351 | * if provided as a values to FormData. FormData can only handle USVString
|
---|
352 | * or Blob.
|
---|
353 | *
|
---|
354 | * @param {Object} reqForm - ori req.form
|
---|
355 | * @return {FormData} - new FormData instance
|
---|
356 | */
|
---|
357 | return Object.entries(reqForm).reduce((formData, [name, input]) => {
|
---|
358 | // eslint-disable-next-line no-restricted-syntax
|
---|
359 | for (const [key, value] of formatKeyValue(name, input, true)) {
|
---|
360 | if (Array.isArray(value)) {
|
---|
361 | // eslint-disable-next-line no-restricted-syntax
|
---|
362 | for (const v of value) {
|
---|
363 | if (ArrayBuffer.isView(v)) {
|
---|
364 | const blob = new Blob([v]);
|
---|
365 | formData.append(key, blob);
|
---|
366 | } else {
|
---|
367 | formData.append(key, v);
|
---|
368 | }
|
---|
369 | }
|
---|
370 | } else if (ArrayBuffer.isView(value)) {
|
---|
371 | const blob = new Blob([value]);
|
---|
372 | formData.append(key, blob);
|
---|
373 | } else {
|
---|
374 | formData.append(key, value);
|
---|
375 | }
|
---|
376 | }
|
---|
377 | return formData;
|
---|
378 | }, new FormData());
|
---|
379 | }
|
---|
380 |
|
---|
381 | // Encodes an object using appropriate serializer.
|
---|
382 | function encodeFormOrQuery(data) {
|
---|
383 | /**
|
---|
384 | * Encode parameter names and values
|
---|
385 | * @param {Object} result - parameter names and values
|
---|
386 | * @param {string} parameterName - Parameter name
|
---|
387 | * @return {object} encoded parameter names and values
|
---|
388 | */
|
---|
389 | const encodedQuery = Object.keys(data).reduce((result, parameterName) => {
|
---|
390 | // eslint-disable-next-line no-restricted-syntax
|
---|
391 | for (const [key, value] of formatKeyValue(parameterName, data[parameterName])) {
|
---|
392 | if (value instanceof FileWithData) {
|
---|
393 | result[key] = value.valueOf();
|
---|
394 | } else {
|
---|
395 | result[key] = value;
|
---|
396 | }
|
---|
397 | }
|
---|
398 | return result;
|
---|
399 | }, {});
|
---|
400 | return _qs.default.stringify(encodedQuery, {
|
---|
401 | encode: false,
|
---|
402 | indices: false
|
---|
403 | }) || '';
|
---|
404 | }
|
---|
405 |
|
---|
406 | // If the request has a `query` object, merge it into the request.url, and delete the object
|
---|
407 | // If file and/or multipart, also create FormData instance
|
---|
408 | function mergeInQueryOrForm(req = {}) {
|
---|
409 | const {
|
---|
410 | url = '',
|
---|
411 | query,
|
---|
412 | form
|
---|
413 | } = req;
|
---|
414 | const joinSearch = (...strs) => {
|
---|
415 | const search = strs.filter(a => a).join('&'); // Only truthy value
|
---|
416 | return search ? `?${search}` : ''; // Only add '?' if there is a str
|
---|
417 | };
|
---|
418 | if (form) {
|
---|
419 | const hasFile = Object.keys(form).some(key => {
|
---|
420 | const {
|
---|
421 | value
|
---|
422 | } = form[key];
|
---|
423 | return isFile(value) || isArrayOfFile(value);
|
---|
424 | });
|
---|
425 | const contentType = req.headers['content-type'] || req.headers['Content-Type'];
|
---|
426 | if (hasFile || /multipart\/form-data/i.test(contentType)) {
|
---|
427 | const formdata = buildFormData(req.form);
|
---|
428 | req.formdata = formdata;
|
---|
429 | req.body = formdata;
|
---|
430 | } else {
|
---|
431 | req.body = encodeFormOrQuery(form);
|
---|
432 | }
|
---|
433 | delete req.form;
|
---|
434 | }
|
---|
435 | if (query) {
|
---|
436 | const [baseUrl, oriSearch] = url.split('?');
|
---|
437 | let newStr = '';
|
---|
438 | if (oriSearch) {
|
---|
439 | const oriQuery = _qs.default.parse(oriSearch);
|
---|
440 | const keysToRemove = Object.keys(query);
|
---|
441 | keysToRemove.forEach(key => delete oriQuery[key]);
|
---|
442 | newStr = _qs.default.stringify(oriQuery, {
|
---|
443 | encode: true
|
---|
444 | });
|
---|
445 | }
|
---|
446 | const finalStr = joinSearch(newStr, encodeFormOrQuery(query));
|
---|
447 | req.url = baseUrl + finalStr;
|
---|
448 | delete req.query;
|
---|
449 | }
|
---|
450 | return req;
|
---|
451 | }
|
---|
452 |
|
---|
453 | // Wrap a http function ( there are otherways to do this, consider this deprecated )
|
---|
454 | function makeHttp(httpFn, preFetch, postFetch) {
|
---|
455 | postFetch = postFetch || (a => a);
|
---|
456 | preFetch = preFetch || (a => a);
|
---|
457 | return req => {
|
---|
458 | if (typeof req === 'string') {
|
---|
459 | req = {
|
---|
460 | url: req
|
---|
461 | };
|
---|
462 | }
|
---|
463 | self.mergeInQueryOrForm(req);
|
---|
464 | req = preFetch(req);
|
---|
465 | return postFetch(httpFn(req));
|
---|
466 | };
|
---|
467 | } |
---|