1 |
|
2 |
|
3 |
|
4 | import { HttpHeaders, HttpHeadersLike, isHttpHeadersLike } from "./httpHeaders";
|
5 | import { OperationSpec } from "./operationSpec";
|
6 | import { Mapper, Serializer } from "./serializer";
|
7 | import { generateUuid } from "./util/utils";
|
8 | import { HttpOperationResponse } from "./httpOperationResponse";
|
9 | import { OperationResponse } from "./operationResponse";
|
10 | import { AgentSettings, ProxySettings } from "./serviceClient";
|
11 |
|
12 | export type HttpMethods =
|
13 | | "GET"
|
14 | | "PUT"
|
15 | | "POST"
|
16 | | "DELETE"
|
17 | | "PATCH"
|
18 | | "HEAD"
|
19 | | "OPTIONS"
|
20 | | "TRACE";
|
21 | export 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 | */
|
31 | export 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 | */
|
42 | export 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 |
|
60 |
|
61 | export interface WebResourceLike {
|
62 | |
63 |
|
64 |
|
65 | url: string;
|
66 | |
67 |
|
68 |
|
69 | method: HttpMethods;
|
70 | |
71 |
|
72 |
|
73 | body?: any;
|
74 | |
75 |
|
76 |
|
77 | headers: HttpHeadersLike;
|
78 | |
79 |
|
80 |
|
81 | streamResponseBody?: boolean;
|
82 | |
83 |
|
84 |
|
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 |
|
99 |
|
100 | query?: { [key: string]: any };
|
101 | |
102 |
|
103 |
|
104 | operationSpec?: OperationSpec;
|
105 | |
106 |
|
107 |
|
108 | withCredentials: boolean;
|
109 | |
110 |
|
111 |
|
112 |
|
113 | timeout: number;
|
114 | |
115 |
|
116 |
|
117 | proxySettings?: ProxySettings;
|
118 | |
119 |
|
120 |
|
121 | agentSettings?: AgentSettings;
|
122 | |
123 |
|
124 |
|
125 | keepAlive?: boolean;
|
126 | |
127 |
|
128 |
|
129 |
|
130 | redirectLimit?: number;
|
131 |
|
132 | |
133 |
|
134 |
|
135 | abortSignal?: AbortSignalLike;
|
136 |
|
137 | |
138 |
|
139 |
|
140 | onUploadProgress?: (progress: TransferProgressEvent) => void;
|
141 |
|
142 |
|
143 | onDownloadProgress?: (progress: TransferProgressEvent) => void;
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 | validateRequestProperties(): void;
|
151 |
|
152 | |
153 |
|
154 |
|
155 | prepare(options: RequestPrepareOptions): WebResourceLike;
|
156 | |
157 |
|
158 |
|
159 | clone(): WebResourceLike;
|
160 | }
|
161 |
|
162 | export 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 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | export class WebResource {
|
189 | url: string;
|
190 | method: HttpMethods;
|
191 | body?: any;
|
192 | headers: HttpHeadersLike;
|
193 | |
194 |
|
195 |
|
196 | streamResponseBody?: boolean;
|
197 | |
198 |
|
199 |
|
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 |
|
224 | onUploadProgress?: (progress: TransferProgressEvent) => void;
|
225 |
|
226 |
|
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 |
|
266 |
|
267 |
|
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 |
|
280 |
|
281 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
400 | if (this.url && this.url.indexOf("?") === -1) {
|
401 | this.url += "?";
|
402 | }
|
403 |
|
404 | const queryParams = [];
|
405 |
|
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 | }
|
429 |
|
430 | this.url += queryParams.join("&");
|
431 | }
|
432 |
|
433 |
|
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 |
|
441 | if (!this.headers.get("accept-language")) {
|
442 | this.headers.set("accept-language", "en-US");
|
443 | }
|
444 |
|
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 |
|
450 | if (!this.headers.get("Content-Type")) {
|
451 | this.headers.set("Content-Type", "application/json; charset=utf-8");
|
452 | }
|
453 |
|
454 |
|
455 | this.body = options.body;
|
456 | if (options.body != undefined) {
|
457 |
|
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 |
|
490 |
|
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 |
|
531 | export interface RequestPrepareOptions {
|
532 | |
533 |
|
534 |
|
535 |
|
536 | method: HttpMethods;
|
537 | |
538 |
|
539 |
|
540 |
|
541 | url?: string;
|
542 | |
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 |
|
551 |
|
552 |
|
553 | queryParameters?: { [key: string]: any | ParameterValue };
|
554 | |
555 |
|
556 |
|
557 |
|
558 |
|
559 | pathTemplate?: string;
|
560 | |
561 |
|
562 |
|
563 |
|
564 |
|
565 | baseUrl?: string;
|
566 | |
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 | pathParameters?: { [key: string]: any | ParameterValue };
|
577 | formData?: { [key: string]: any };
|
578 | |
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 |
|
587 | headers?: { [key: string]: any };
|
588 | |
589 |
|
590 |
|
591 | disableClientRequestId?: boolean;
|
592 | |
593 |
|
594 |
|
595 | body?: any;
|
596 | |
597 |
|
598 |
|
599 | serializationMapper?: Mapper;
|
600 | |
601 |
|
602 |
|
603 | mappers?: { [x: string]: any };
|
604 | |
605 |
|
606 |
|
607 | deserializationMapper?: object;
|
608 | |
609 |
|
610 |
|
611 | disableJsonStringifyOnBody?: boolean;
|
612 | |
613 |
|
614 |
|
615 | bodyIsStream?: boolean;
|
616 | abortSignal?: AbortSignalLike;
|
617 | |
618 |
|
619 |
|
620 |
|
621 | redirectLimit?: number;
|
622 |
|
623 | onUploadProgress?: (progress: TransferProgressEvent) => void;
|
624 | onDownloadProgress?: (progress: TransferProgressEvent) => void;
|
625 | streamResponseBody?: boolean;
|
626 | }
|
627 |
|
628 |
|
629 |
|
630 |
|
631 | export interface ParameterValue {
|
632 | value: any;
|
633 | skipUrlEncoding: boolean;
|
634 | [key: string]: any;
|
635 | }
|
636 |
|
637 |
|
638 |
|
639 |
|
640 | export interface RequestOptionsBase {
|
641 | |
642 |
|
643 |
|
644 |
|
645 | customHeaders?: { [key: string]: string };
|
646 |
|
647 | |
648 |
|
649 |
|
650 | abortSignal?: AbortSignalLike;
|
651 |
|
652 | |
653 |
|
654 |
|
655 | timeout?: number;
|
656 |
|
657 | |
658 |
|
659 |
|
660 | onUploadProgress?: (progress: TransferProgressEvent) => void;
|
661 |
|
662 | |
663 |
|
664 |
|
665 | onDownloadProgress?: (progress: TransferProgressEvent) => void;
|
666 |
|
667 | [key: string]: any;
|
668 | }
|