UNPKG

23.5 kBPlain TextView Raw
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License. See License.txt in the project root for license information.
3
4import { HttpHeaders, HttpHeadersLike, isHttpHeadersLike } from "./httpHeaders";
5import { OperationSpec } from "./operationSpec";
6import { Mapper, Serializer } from "./serializer";
7import { generateUuid } from "./util/utils";
8import { HttpOperationResponse } from "./httpOperationResponse";
9import { OperationResponse } from "./operationResponse";
10import { AgentSettings, ProxySettings } from "./serviceClient";
11
12export type HttpMethods =
13 | "GET"
14 | "PUT"
15 | "POST"
16 | "DELETE"
17 | "PATCH"
18 | "HEAD"
19 | "OPTIONS"
20 | "TRACE";
21export type HttpRequestBody =
22 | Blob
23 | string
24 | ArrayBuffer
25 | ArrayBufferView
26 | (() => NodeJS.ReadableStream);
27
28/**
29 * Fired in response to upload or download progress.
30 */
31export type TransferProgressEvent = {
32 /**
33 * The number of bytes loaded so far.
34 */
35 loadedBytes: number;
36};
37
38/**
39 * Allows the request to be aborted upon firing of the "abort" event.
40 * Compatible with the browser built-in AbortSignal and common polyfills.
41 */
42export interface AbortSignalLike {
43 readonly aborted: boolean;
44 dispatchEvent: (event: Event) => boolean;
45 onabort: ((this: AbortSignalLike, ev: Event) => any) | null;
46 addEventListener: (
47 type: "abort",
48 listener: (this: AbortSignalLike, ev: Event) => any,
49 options?: any
50 ) => void;
51 removeEventListener: (
52 type: "abort",
53 listener: (this: AbortSignalLike, ev: Event) => any,
54 options?: any
55 ) => void;
56}
57
58/**
59 * An abstraction over a REST call.
60 */
61export interface WebResourceLike {
62 /**
63 * The URL being accessed by the request.
64 */
65 url: string;
66 /**
67 * The HTTP method to use when making the request.
68 */
69 method: HttpMethods;
70 /**
71 * The HTTP body contents of the request.
72 */
73 body?: any;
74 /**
75 * The HTTP headers to use when making the request.
76 */
77 headers: HttpHeadersLike;
78 /**
79 * Whether or not the body of the HttpOperationResponse should be treated as a stream.
80 */
81 streamResponseBody?: boolean;
82 /**
83 * Whether or not the HttpOperationResponse should be deserialized. If this is undefined, then the
84 * HttpOperationResponse should be deserialized.
85 */
86 shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
87 /**
88 * A function that returns the proper OperationResponse for the given OperationSpec and
89 * HttpOperationResponse combination. If this is undefined, then a simple status code lookup will
90 * be used.
91 */
92 operationResponseGetter?: (
93 operationSpec: OperationSpec,
94 response: HttpOperationResponse
95 ) => undefined | OperationResponse;
96 formData?: any;
97 /**
98 * A query string represented as an object.
99 */
100 query?: { [key: string]: any };
101 /**
102 * Used to parse the response.
103 */
104 operationSpec?: OperationSpec;
105 /**
106 * If credentials (cookies) should be sent along during an XHR.
107 */
108 withCredentials: boolean;
109 /**
110 * The number of milliseconds a request can take before automatically being terminated.
111 * If the request is terminated, an `AbortError` is thrown.
112 */
113 timeout: number;
114 /**
115 * Proxy configuration.
116 */
117 proxySettings?: ProxySettings;
118 /**
119 * HTTP(S) agent configuration.
120 */
121 agentSettings?: AgentSettings;
122 /**
123 * If the connection should be reused.
124 */
125 keepAlive?: boolean;
126 /**
127 * Limit the number of redirects followed for this request. If set to 0, redirects will not be followed.
128 * If left undefined the default redirect behaviour of the underlying node_fetch will apply.
129 */
130 redirectLimit?: number;
131
132 /**
133 * Used to abort the request later.
134 */
135 abortSignal?: AbortSignalLike;
136
137 /**
138 * Callback which fires upon upload progress.
139 */
140 onUploadProgress?: (progress: TransferProgressEvent) => void;
141
142 /** Callback which fires upon download progress. */
143 onDownloadProgress?: (progress: TransferProgressEvent) => void;
144
145 /**
146 * Validates that the required properties such as method, url, headers["Content-Type"],
147 * headers["accept-language"] are defined. It will throw an error if one of the above
148 * mentioned properties are not defined.
149 */
150 validateRequestProperties(): void;
151
152 /**
153 * Sets options on the request.
154 */
155 prepare(options: RequestPrepareOptions): WebResourceLike;
156 /**
157 * Clone this request object.
158 */
159 clone(): WebResourceLike;
160}
161
162export function isWebResourceLike(object: any): object is WebResourceLike {
163 if (typeof object !== "object") {
164 return false;
165 }
166 if (
167 typeof object.url === "string" &&
168 typeof object.method === "string" &&
169 typeof object.headers === "object" &&
170 isHttpHeadersLike(object.headers) &&
171 typeof object.validateRequestProperties === "function" &&
172 typeof object.prepare === "function" &&
173 typeof object.clone === "function"
174 ) {
175 return true;
176 }
177 return false;
178}
179
180/**
181 * Creates a new WebResource object.
182 *
183 * This class provides an abstraction over a REST call by being library / implementation agnostic and wrapping the necessary
184 * properties to initiate a request.
185 *
186 * @constructor
187 */
188export class WebResource {
189 url: string;
190 method: HttpMethods;
191 body?: any;
192 headers: HttpHeadersLike;
193 /**
194 * Whether or not the body of the HttpOperationResponse should be treated as a stream.
195 */
196 streamResponseBody?: boolean;
197 /**
198 * Whether or not the HttpOperationResponse should be deserialized. If this is undefined, then the
199 * HttpOperationResponse should be deserialized.
200 */
201 shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
202 /**
203 * A function that returns the proper OperationResponse for the given OperationSpec and
204 * HttpOperationResponse combination. If this is undefined, then a simple status code lookup will
205 * be used.
206 */
207 operationResponseGetter?: (
208 operationSpec: OperationSpec,
209 response: HttpOperationResponse
210 ) => undefined | OperationResponse;
211 formData?: any;
212 query?: { [key: string]: any };
213 operationSpec?: OperationSpec;
214 withCredentials: boolean;
215 timeout: number;
216 proxySettings?: ProxySettings;
217 keepAlive?: boolean;
218 agentSettings?: AgentSettings;
219 redirectLimit?: number;
220
221 abortSignal?: AbortSignalLike;
222
223 /** Callback which fires upon upload progress. */
224 onUploadProgress?: (progress: TransferProgressEvent) => void;
225
226 /** Callback which fires upon download progress. */
227 onDownloadProgress?: (progress: TransferProgressEvent) => void;
228
229 constructor(
230 url?: string,
231 method?: HttpMethods,
232 body?: any,
233 query?: { [key: string]: any },
234 headers?: { [key: string]: any } | HttpHeadersLike,
235 streamResponseBody?: boolean,
236 withCredentials?: boolean,
237 abortSignal?: AbortSignalLike,
238 timeout?: number,
239 onUploadProgress?: (progress: TransferProgressEvent) => void,
240 onDownloadProgress?: (progress: TransferProgressEvent) => void,
241 proxySettings?: ProxySettings,
242 keepAlive?: boolean,
243 agentSettings?: AgentSettings,
244 redirectLimit?: number
245 ) {
246 this.streamResponseBody = streamResponseBody;
247 this.url = url || "";
248 this.method = method || "GET";
249 this.headers = isHttpHeadersLike(headers) ? headers : new HttpHeaders(headers);
250 this.body = body;
251 this.query = query;
252 this.formData = undefined;
253 this.withCredentials = withCredentials || false;
254 this.abortSignal = abortSignal;
255 this.timeout = timeout || 0;
256 this.onUploadProgress = onUploadProgress;
257 this.onDownloadProgress = onDownloadProgress;
258 this.proxySettings = proxySettings;
259 this.keepAlive = keepAlive;
260 this.agentSettings = agentSettings;
261 this.redirectLimit = redirectLimit;
262 }
263
264 /**
265 * Validates that the required properties such as method, url, headers["Content-Type"],
266 * headers["accept-language"] are defined. It will throw an error if one of the above
267 * mentioned properties are not defined.
268 */
269 validateRequestProperties(): void {
270 if (!this.method) {
271 throw new Error("WebResource.method is required.");
272 }
273 if (!this.url) {
274 throw new Error("WebResource.url is required.");
275 }
276 }
277
278 /**
279 * Prepares the request.
280 * @param {RequestPrepareOptions} options Options to provide for preparing the request.
281 * @returns {WebResource} Returns the prepared WebResource (HTTP Request) object that needs to be given to the request pipeline.
282 */
283 prepare(options: RequestPrepareOptions): WebResource {
284 if (!options) {
285 throw new Error("options object is required");
286 }
287
288 if (options.method == undefined || typeof options.method.valueOf() !== "string") {
289 throw new Error("options.method must be a string.");
290 }
291
292 if (options.url && options.pathTemplate) {
293 throw new Error(
294 "options.url and options.pathTemplate are mutually exclusive. Please provide exactly one of them."
295 );
296 }
297
298 if (
299 (options.pathTemplate == undefined || typeof options.pathTemplate.valueOf() !== "string") &&
300 (options.url == undefined || typeof options.url.valueOf() !== "string")
301 ) {
302 throw new Error("Please provide exactly one of options.pathTemplate or options.url.");
303 }
304
305 // set the url if it is provided.
306 if (options.url) {
307 if (typeof options.url !== "string") {
308 throw new Error('options.url must be of type "string".');
309 }
310 this.url = options.url;
311 }
312
313 // set the method
314 if (options.method) {
315 const validMethods = ["GET", "PUT", "HEAD", "DELETE", "OPTIONS", "POST", "PATCH", "TRACE"];
316 if (validMethods.indexOf(options.method.toUpperCase()) === -1) {
317 throw new Error(
318 'The provided method "' +
319 options.method +
320 '" is invalid. Supported HTTP methods are: ' +
321 JSON.stringify(validMethods)
322 );
323 }
324 }
325 this.method = options.method.toUpperCase() as HttpMethods;
326
327 // construct the url if path template is provided
328 if (options.pathTemplate) {
329 const { pathTemplate, pathParameters } = options;
330 if (typeof pathTemplate !== "string") {
331 throw new Error('options.pathTemplate must be of type "string".');
332 }
333 if (!options.baseUrl) {
334 options.baseUrl = "https://management.azure.com";
335 }
336 const baseUrl = options.baseUrl;
337 let url =
338 baseUrl +
339 (baseUrl.endsWith("/") ? "" : "/") +
340 (pathTemplate.startsWith("/") ? pathTemplate.slice(1) : pathTemplate);
341 const segments = url.match(/({\w*\s*\w*})/gi);
342 if (segments && segments.length) {
343 if (!pathParameters) {
344 throw new Error(
345 `pathTemplate: ${pathTemplate} has been provided. Hence, options.pathParameters must also be provided.`
346 );
347 }
348 segments.forEach(function (item) {
349 const pathParamName = item.slice(1, -1);
350 const pathParam = (pathParameters as { [key: string]: any })[pathParamName];
351 if (
352 pathParam === null ||
353 pathParam === undefined ||
354 !(typeof pathParam === "string" || typeof pathParam === "object")
355 ) {
356 throw new Error(
357 `pathTemplate: ${pathTemplate} contains the path parameter ${pathParamName}` +
358 ` however, it is not present in ${pathParameters} - ${JSON.stringify(
359 pathParameters,
360 undefined,
361 2
362 )}.` +
363 `The value of the path parameter can either be a "string" of the form { ${pathParamName}: "some sample value" } or ` +
364 `it can be an "object" of the form { "${pathParamName}": { value: "some sample value", skipUrlEncoding: true } }.`
365 );
366 }
367
368 if (typeof pathParam.valueOf() === "string") {
369 url = url.replace(item, encodeURIComponent(pathParam));
370 }
371
372 if (typeof pathParam.valueOf() === "object") {
373 if (!pathParam.value) {
374 throw new Error(
375 `options.pathParameters[${pathParamName}] is of type "object" but it does not contain a "value" property.`
376 );
377 }
378 if (pathParam.skipUrlEncoding) {
379 url = url.replace(item, pathParam.value);
380 } else {
381 url = url.replace(item, encodeURIComponent(pathParam.value));
382 }
383 }
384 });
385 }
386 this.url = url;
387 }
388
389 // append query parameters to the url if they are provided. They can be provided with pathTemplate or url option.
390 if (options.queryParameters) {
391 const queryParameters = options.queryParameters;
392 if (typeof queryParameters !== "object") {
393 throw new Error(
394 `options.queryParameters must be of type object. It should be a JSON object ` +
395 `of "query-parameter-name" as the key and the "query-parameter-value" as the value. ` +
396 `The "query-parameter-value" may be fo type "string" or an "object" of the form { value: "query-parameter-value", skipUrlEncoding: true }.`
397 );
398 }
399 // append question mark if it is not present in the url
400 if (this.url && this.url.indexOf("?") === -1) {
401 this.url += "?";
402 }
403 // construct queryString
404 const queryParams = [];
405 // We need to populate this.query as a dictionary if the request is being used for Sway's validateRequest().
406 this.query = {};
407 for (const queryParamName in queryParameters) {
408 const queryParam: any = queryParameters[queryParamName];
409 if (queryParam) {
410 if (typeof queryParam === "string") {
411 queryParams.push(queryParamName + "=" + encodeURIComponent(queryParam));
412 this.query[queryParamName] = encodeURIComponent(queryParam);
413 } else if (typeof queryParam === "object") {
414 if (!queryParam.value) {
415 throw new Error(
416 `options.queryParameters[${queryParamName}] is of type "object" but it does not contain a "value" property.`
417 );
418 }
419 if (queryParam.skipUrlEncoding) {
420 queryParams.push(queryParamName + "=" + queryParam.value);
421 this.query[queryParamName] = queryParam.value;
422 } else {
423 queryParams.push(queryParamName + "=" + encodeURIComponent(queryParam.value));
424 this.query[queryParamName] = encodeURIComponent(queryParam.value);
425 }
426 }
427 }
428 } // end-of-for
429 // append the queryString
430 this.url += queryParams.join("&");
431 }
432
433 // add headers to the request if they are provided
434 if (options.headers) {
435 const headers = options.headers;
436 for (const headerName of Object.keys(options.headers)) {
437 this.headers.set(headerName, headers[headerName]);
438 }
439 }
440 // ensure accept-language is set correctly
441 if (!this.headers.get("accept-language")) {
442 this.headers.set("accept-language", "en-US");
443 }
444 // ensure the request-id is set correctly
445 if (!this.headers.get("x-ms-client-request-id") && !options.disableClientRequestId) {
446 this.headers.set("x-ms-client-request-id", generateUuid());
447 }
448
449 // default
450 if (!this.headers.get("Content-Type")) {
451 this.headers.set("Content-Type", "application/json; charset=utf-8");
452 }
453
454 // set the request body. request.js automatically sets the Content-Length request header, so we need not set it explicilty
455 this.body = options.body;
456 if (options.body != undefined) {
457 // body as a stream special case. set the body as-is and check for some special request headers specific to sending a stream.
458 if (options.bodyIsStream) {
459 if (!this.headers.get("Transfer-Encoding")) {
460 this.headers.set("Transfer-Encoding", "chunked");
461 }
462 if (this.headers.get("Content-Type") !== "application/octet-stream") {
463 this.headers.set("Content-Type", "application/octet-stream");
464 }
465 } else {
466 if (options.serializationMapper) {
467 this.body = new Serializer(options.mappers).serialize(
468 options.serializationMapper,
469 options.body,
470 "requestBody"
471 );
472 }
473 if (!options.disableJsonStringifyOnBody) {
474 this.body = JSON.stringify(options.body);
475 }
476 }
477 }
478
479 this.abortSignal = options.abortSignal;
480 this.onDownloadProgress = options.onDownloadProgress;
481 this.onUploadProgress = options.onUploadProgress;
482 this.redirectLimit = options.redirectLimit;
483 this.streamResponseBody = options.streamResponseBody;
484
485 return this;
486 }
487
488 /**
489 * Clone this WebResource HTTP request object.
490 * @returns {WebResource} The clone of this WebResource HTTP request object.
491 */
492 clone(): WebResource {
493 const result = new WebResource(
494 this.url,
495 this.method,
496 this.body,
497 this.query,
498 this.headers && this.headers.clone(),
499 this.streamResponseBody,
500 this.withCredentials,
501 this.abortSignal,
502 this.timeout,
503 this.onUploadProgress,
504 this.onDownloadProgress,
505 this.proxySettings,
506 this.keepAlive,
507 this.agentSettings,
508 this.redirectLimit
509 );
510
511 if (this.formData) {
512 result.formData = this.formData;
513 }
514
515 if (this.operationSpec) {
516 result.operationSpec = this.operationSpec;
517 }
518
519 if (this.shouldDeserialize) {
520 result.shouldDeserialize = this.shouldDeserialize;
521 }
522
523 if (this.operationResponseGetter) {
524 result.operationResponseGetter = this.operationResponseGetter;
525 }
526
527 return result;
528 }
529}
530
531export interface RequestPrepareOptions {
532 /**
533 * The HTTP request method. Valid values are "GET", "PUT", "HEAD", "DELETE", "OPTIONS", "POST",
534 * or "PATCH".
535 */
536 method: HttpMethods;
537 /**
538 * The request url. It may or may not have query parameters in it. Either provide the "url" or
539 * provide the "pathTemplate" in the options object. Both the options are mutually exclusive.
540 */
541 url?: string;
542 /**
543 * A dictionary of query parameters to be appended to the url, where
544 * the "key" is the "query-parameter-name" and the "value" is the "query-parameter-value".
545 * The "query-parameter-value" can be of type "string" or it can be of type "object".
546 * The "object" format should be used when you want to skip url encoding. While using the object format,
547 * the object must have a property named value which provides the "query-parameter-value".
548 * Example:
549 * - query-parameter-value in "object" format: { "query-parameter-name": { value: "query-parameter-value", skipUrlEncoding: true } }
550 * - query-parameter-value in "string" format: { "query-parameter-name": "query-parameter-value"}.
551 * Note: "If options.url already has some query parameters, then the value provided in options.queryParameters will be appended to the url.
552 */
553 queryParameters?: { [key: string]: any | ParameterValue };
554 /**
555 * The path template of the request url. Either provide the "url" or provide the "pathTemplate" in
556 * the options object. Both the options are mutually exclusive.
557 * Example: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}"
558 */
559 pathTemplate?: string;
560 /**
561 * The base url of the request. Default value is: "https://management.azure.com". This is
562 * applicable only with pathTemplate. If you are providing options.url then it is expected that
563 * you provide the complete url.
564 */
565 baseUrl?: string;
566 /**
567 * A dictionary of path parameters that need to be replaced with actual values in the pathTemplate.
568 * Here the key is the "path-parameter-name" and the value is the "path-parameter-value".
569 * The "path-parameter-value" can be of type "string" or it can be of type "object".
570 * The "object" format should be used when you want to skip url encoding. While using the object format,
571 * the object must have a property named value which provides the "path-parameter-value".
572 * Example:
573 * - path-parameter-value in "object" format: { "path-parameter-name": { value: "path-parameter-value", skipUrlEncoding: true } }
574 * - path-parameter-value in "string" format: { "path-parameter-name": "path-parameter-value" }.
575 */
576 pathParameters?: { [key: string]: any | ParameterValue };
577 formData?: { [key: string]: any };
578 /**
579 * A dictionary of request headers that need to be applied to the request.
580 * Here the key is the "header-name" and the value is the "header-value". The header-value MUST be of type string.
581 * - ContentType must be provided with the key name as "Content-Type". Default value "application/json; charset=utf-8".
582 * - "Transfer-Encoding" is set to "chunked" by default if "options.bodyIsStream" is set to true.
583 * - "Content-Type" is set to "application/octet-stream" by default if "options.bodyIsStream" is set to true.
584 * - "accept-language" by default is set to "en-US"
585 * - "x-ms-client-request-id" by default is set to a new Guid. To not generate a guid for the request, please set options.disableClientRequestId to true
586 */
587 headers?: { [key: string]: any };
588 /**
589 * When set to true, instructs the client to not set "x-ms-client-request-id" header to a new Guid().
590 */
591 disableClientRequestId?: boolean;
592 /**
593 * The request body. It can be of any type. This value will be serialized if it is not a stream.
594 */
595 body?: any;
596 /**
597 * Provides information on how to serialize the request body.
598 */
599 serializationMapper?: Mapper;
600 /**
601 * A dictionary of mappers that may be used while [de]serialization.
602 */
603 mappers?: { [x: string]: any };
604 /**
605 * Provides information on how to deserialize the response body.
606 */
607 deserializationMapper?: object;
608 /**
609 * Indicates whether this method should JSON.stringify() the request body. Default value: false.
610 */
611 disableJsonStringifyOnBody?: boolean;
612 /**
613 * Indicates whether the request body is a stream (useful for file upload scenarios).
614 */
615 bodyIsStream?: boolean;
616 abortSignal?: AbortSignalLike;
617 /**
618 * Limit the number of redirects followed for this request. If set to 0, redirects will not be followed.
619 * If left undefined the default redirect behaviour of the underlying node_fetch will apply.
620 */
621 redirectLimit?: number;
622
623 onUploadProgress?: (progress: TransferProgressEvent) => void;
624 onDownloadProgress?: (progress: TransferProgressEvent) => void;
625 streamResponseBody?: boolean;
626}
627
628/**
629 * The Parameter value provided for path or query parameters in RequestPrepareOptions
630 */
631export interface ParameterValue {
632 value: any;
633 skipUrlEncoding: boolean;
634 [key: string]: any;
635}
636
637/**
638 * Describes the base structure of the options object that will be used in every operation.
639 */
640export interface RequestOptionsBase {
641 /**
642 * @property {object} [customHeaders] User defined custom request headers that
643 * will be applied before the request is sent.
644 */
645 customHeaders?: { [key: string]: string };
646
647 /**
648 * The signal which can be used to abort requests.
649 */
650 abortSignal?: AbortSignalLike;
651
652 /**
653 * The number of milliseconds a request can take before automatically being terminated.
654 */
655 timeout?: number;
656
657 /**
658 * Callback which fires upon upload progress.
659 */
660 onUploadProgress?: (progress: TransferProgressEvent) => void;
661
662 /**
663 * Callback which fires upon download progress.
664 */
665 onDownloadProgress?: (progress: TransferProgressEvent) => void;
666
667 [key: string]: any;
668}