1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | import { GraphClientError } from "./GraphClientError";
|
12 | import { GraphError } from "./GraphError";
|
13 | import { GraphErrorHandler } from "./GraphErrorHandler";
|
14 | import { oDataQueryNames, serializeContent, urlJoin } from "./GraphRequestUtil";
|
15 | import { GraphResponseHandler } from "./GraphResponseHandler";
|
16 | import { HTTPClient } from "./HTTPClient";
|
17 | import { ClientOptions } from "./IClientOptions";
|
18 | import { Context } from "./IContext";
|
19 | import { FetchOptions } from "./IFetchOptions";
|
20 | import { GraphRequestCallback } from "./IGraphRequestCallback";
|
21 | import { MiddlewareControl } from "./middleware/MiddlewareControl";
|
22 | import { MiddlewareOptions } from "./middleware/options/IMiddlewareOptions";
|
23 | import { RequestMethod } from "./RequestMethod";
|
24 | import { ResponseType } from "./ResponseType";
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | interface KeyValuePairObjectStringNumber {
|
31 | [key: string]: string | number;
|
32 | }
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | export interface URLComponents {
|
47 | host: string;
|
48 | version: string;
|
49 | path?: string;
|
50 | oDataQueryParams: KeyValuePairObjectStringNumber;
|
51 | otherURLQueryParams: KeyValuePairObjectStringNumber;
|
52 | otherURLQueryOptions?: string[];
|
53 | }
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | export class GraphRequest {
|
60 | |
61 |
|
62 |
|
63 |
|
64 | private httpClient: HTTPClient;
|
65 |
|
66 | |
67 |
|
68 |
|
69 |
|
70 | private config: ClientOptions;
|
71 |
|
72 | |
73 |
|
74 |
|
75 |
|
76 | private urlComponents: URLComponents;
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 | private _headers: HeadersInit;
|
83 |
|
84 | |
85 |
|
86 |
|
87 |
|
88 | private _options: FetchOptions;
|
89 |
|
90 | |
91 |
|
92 |
|
93 |
|
94 | private _middlewareOptions: MiddlewareOptions[];
|
95 |
|
96 | |
97 |
|
98 |
|
99 |
|
100 | private _responseType: ResponseType;
|
101 |
|
102 | |
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | public constructor(httpClient: HTTPClient, config: ClientOptions, path: string) {
|
111 | this.httpClient = httpClient;
|
112 | this.config = config;
|
113 | this.urlComponents = {
|
114 | host: this.config.baseUrl,
|
115 | version: this.config.defaultVersion,
|
116 | oDataQueryParams: {},
|
117 | otherURLQueryParams: {},
|
118 | otherURLQueryOptions: [],
|
119 | };
|
120 | this._headers = {};
|
121 | this._options = {};
|
122 | this._middlewareOptions = [];
|
123 | this.parsePath(path);
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | private parsePath = (path: string): void => {
|
133 |
|
134 | if (path.indexOf("https://") !== -1) {
|
135 | path = path.replace("https://", "");
|
136 |
|
137 |
|
138 | const endOfHostStrPos = path.indexOf("/");
|
139 | if (endOfHostStrPos !== -1) {
|
140 |
|
141 | this.urlComponents.host = "https://" + path.substring(0, endOfHostStrPos);
|
142 |
|
143 | path = path.substring(endOfHostStrPos + 1, path.length);
|
144 | }
|
145 |
|
146 |
|
147 | const endOfVersionStrPos = path.indexOf("/");
|
148 | if (endOfVersionStrPos !== -1) {
|
149 |
|
150 | this.urlComponents.version = path.substring(0, endOfVersionStrPos);
|
151 |
|
152 | path = path.substring(endOfVersionStrPos + 1, path.length);
|
153 | }
|
154 | }
|
155 |
|
156 |
|
157 | if (path.charAt(0) === "/") {
|
158 | path = path.substr(1);
|
159 | }
|
160 |
|
161 | const queryStrPos = path.indexOf("?");
|
162 | if (queryStrPos === -1) {
|
163 |
|
164 | this.urlComponents.path = path;
|
165 | } else {
|
166 | this.urlComponents.path = path.substr(0, queryStrPos);
|
167 |
|
168 |
|
169 | const queryParams = path.substring(queryStrPos + 1, path.length).split("&");
|
170 | for (const queryParam of queryParams) {
|
171 | this.parseQueryParameter(queryParam);
|
172 | }
|
173 | }
|
174 | };
|
175 |
|
176 | |
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | private addCsvQueryParameter(propertyName: string, propertyValue: string | string[], additionalProperties: IArguments): void {
|
185 |
|
186 | this.urlComponents.oDataQueryParams[propertyName] = this.urlComponents.oDataQueryParams[propertyName] ? this.urlComponents.oDataQueryParams[propertyName] + "," : "";
|
187 |
|
188 | let allValues: string[] = [];
|
189 |
|
190 | if (additionalProperties.length > 1 && typeof propertyValue === "string") {
|
191 | allValues = Array.prototype.slice.call(additionalProperties);
|
192 | } else if (typeof propertyValue === "string") {
|
193 | allValues.push(propertyValue);
|
194 | } else {
|
195 | allValues = allValues.concat(propertyValue);
|
196 | }
|
197 |
|
198 | this.urlComponents.oDataQueryParams[propertyName] += allValues.join(",");
|
199 | }
|
200 |
|
201 | |
202 |
|
203 |
|
204 |
|
205 |
|
206 | private buildFullUrl(): string {
|
207 | const url = urlJoin([this.urlComponents.host, this.urlComponents.version, this.urlComponents.path]) + this.createQueryString();
|
208 |
|
209 | if (this.config.debugLogging) {
|
210 | console.log(url);
|
211 | }
|
212 | return url;
|
213 | }
|
214 |
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 | private createQueryString(): string {
|
221 |
|
222 | const urlComponents = this.urlComponents;
|
223 | const query: string[] = [];
|
224 | if (Object.keys(urlComponents.oDataQueryParams).length !== 0) {
|
225 | for (const property in urlComponents.oDataQueryParams) {
|
226 | if (Object.prototype.hasOwnProperty.call(urlComponents.oDataQueryParams, property)) {
|
227 | query.push(property + "=" + urlComponents.oDataQueryParams[property]);
|
228 | }
|
229 | }
|
230 | }
|
231 | if (Object.keys(urlComponents.otherURLQueryParams).length !== 0) {
|
232 | for (const property in urlComponents.otherURLQueryParams) {
|
233 | if (Object.prototype.hasOwnProperty.call(urlComponents.otherURLQueryParams, property)) {
|
234 | query.push(property + "=" + urlComponents.otherURLQueryParams[property]);
|
235 | }
|
236 | }
|
237 | }
|
238 |
|
239 | if (urlComponents.otherURLQueryOptions.length !== 0) {
|
240 | for (const str of urlComponents.otherURLQueryOptions) {
|
241 | query.push(str);
|
242 | }
|
243 | }
|
244 | return query.length > 0 ? "?" + query.join("&") : "";
|
245 | }
|
246 |
|
247 | |
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 | private parseQueryParameter(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest {
|
254 | if (typeof queryDictionaryOrString === "string") {
|
255 | if (queryDictionaryOrString.charAt(0) === "?") {
|
256 | queryDictionaryOrString = queryDictionaryOrString.substring(1);
|
257 | }
|
258 |
|
259 | if (queryDictionaryOrString.indexOf("&") !== -1) {
|
260 | const queryParams = queryDictionaryOrString.split("&");
|
261 | for (const str of queryParams) {
|
262 | this.parseQueryParamenterString(str);
|
263 | }
|
264 | } else {
|
265 | this.parseQueryParamenterString(queryDictionaryOrString);
|
266 | }
|
267 | } else if (queryDictionaryOrString.constructor === Object) {
|
268 | for (const key in queryDictionaryOrString) {
|
269 | if (Object.prototype.hasOwnProperty.call(queryDictionaryOrString, key)) {
|
270 | this.setURLComponentsQueryParamater(key, queryDictionaryOrString[key]);
|
271 | }
|
272 | }
|
273 | }
|
274 |
|
275 | return this;
|
276 | }
|
277 |
|
278 | |
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 | private parseQueryParamenterString(queryParameter: string): void {
|
285 | |
286 |
|
287 | if (this.isValidQueryKeyValuePair(queryParameter)) {
|
288 | const indexOfFirstEquals = queryParameter.indexOf("=");
|
289 | const paramKey = queryParameter.substring(0, indexOfFirstEquals);
|
290 | const paramValue = queryParameter.substring(indexOfFirstEquals + 1);
|
291 | this.setURLComponentsQueryParamater(paramKey, paramValue);
|
292 | } else {
|
293 | |
294 |
|
295 | this.urlComponents.otherURLQueryOptions.push(queryParameter);
|
296 | }
|
297 | }
|
298 |
|
299 | |
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 | private setURLComponentsQueryParamater(paramKey: string, paramValue: string | number): void {
|
307 | if (oDataQueryNames.indexOf(paramKey) !== -1) {
|
308 | const currentValue = this.urlComponents.oDataQueryParams[paramKey];
|
309 | const isValueAppendable = currentValue && (paramKey === "$expand" || paramKey === "$select" || paramKey === "$orderby");
|
310 | this.urlComponents.oDataQueryParams[paramKey] = isValueAppendable ? currentValue + "," + paramValue : paramValue;
|
311 | } else {
|
312 | this.urlComponents.otherURLQueryParams[paramKey] = paramValue;
|
313 | }
|
314 | }
|
315 | |
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 | private isValidQueryKeyValuePair(queryString: string): boolean {
|
322 | const indexofFirstEquals = queryString.indexOf("=");
|
323 | if (indexofFirstEquals === -1) {
|
324 | return false;
|
325 | }
|
326 | const indexofOpeningParanthesis = queryString.indexOf("(");
|
327 | if (indexofOpeningParanthesis !== -1 && queryString.indexOf("(") < indexofFirstEquals) {
|
328 |
|
329 | return false;
|
330 | }
|
331 | return true;
|
332 | }
|
333 |
|
334 | |
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | private updateRequestOptions(options: FetchOptions): void {
|
341 | const optionsHeaders: HeadersInit = { ...options.headers };
|
342 | if (this.config.fetchOptions !== undefined) {
|
343 | const fetchOptions: FetchOptions = { ...this.config.fetchOptions };
|
344 | Object.assign(options, fetchOptions);
|
345 | if (typeof this.config.fetchOptions.headers !== undefined) {
|
346 | options.headers = { ...this.config.fetchOptions.headers };
|
347 | }
|
348 | }
|
349 | Object.assign(options, this._options);
|
350 | if (options.headers !== undefined) {
|
351 | Object.assign(optionsHeaders, options.headers);
|
352 | }
|
353 | Object.assign(optionsHeaders, this._headers);
|
354 | options.headers = optionsHeaders;
|
355 | }
|
356 |
|
357 | |
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 | private async send(request: RequestInfo, options: FetchOptions, callback?: GraphRequestCallback): Promise<any> {
|
367 | let rawResponse: Response;
|
368 | const middlewareControl = new MiddlewareControl(this._middlewareOptions);
|
369 | this.updateRequestOptions(options);
|
370 | const customHosts = this.config?.customHosts;
|
371 | try {
|
372 | const context: Context = await this.httpClient.sendRequest({
|
373 | request,
|
374 | options,
|
375 | middlewareControl,
|
376 | customHosts,
|
377 | });
|
378 |
|
379 | rawResponse = context.response;
|
380 | const response: any = await GraphResponseHandler.getResponse(rawResponse, this._responseType, callback);
|
381 | return response;
|
382 | } catch (error) {
|
383 | if (error instanceof GraphClientError) {
|
384 | throw error;
|
385 | }
|
386 | let statusCode: number;
|
387 |
|
388 | if (rawResponse) {
|
389 | statusCode = rawResponse.status;
|
390 | }
|
391 | const gError: GraphError = await GraphErrorHandler.getError(error, statusCode, callback, rawResponse);
|
392 | throw gError;
|
393 | }
|
394 | }
|
395 |
|
396 | |
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 | private setHeaderContentType(): void {
|
403 | if (!this._headers) {
|
404 | this.header("Content-Type", "application/json");
|
405 | return;
|
406 | }
|
407 | const headerKeys = Object.keys(this._headers);
|
408 | for (const headerKey of headerKeys) {
|
409 | if (headerKey.toLowerCase() === "content-type") {
|
410 | return;
|
411 | }
|
412 | }
|
413 |
|
414 | this.header("Content-Type", "application/json");
|
415 | }
|
416 |
|
417 | |
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 | public header(headerKey: string, headerValue: string): GraphRequest {
|
425 | this._headers[headerKey] = headerValue;
|
426 | return this;
|
427 | }
|
428 |
|
429 | |
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 | public headers(headers: KeyValuePairObjectStringNumber | HeadersInit): GraphRequest {
|
436 | for (const key in headers) {
|
437 | if (Object.prototype.hasOwnProperty.call(headers, key)) {
|
438 | this._headers[key] = headers[key] as string;
|
439 | }
|
440 | }
|
441 | return this;
|
442 | }
|
443 |
|
444 | |
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 | public option(key: string, value: any): GraphRequest {
|
452 | this._options[key] = value;
|
453 | return this;
|
454 | }
|
455 |
|
456 | |
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 | public options(options: { [key: string]: any }): GraphRequest {
|
463 | for (const key in options) {
|
464 | if (Object.prototype.hasOwnProperty.call(options, key)) {
|
465 | this._options[key] = options[key];
|
466 | }
|
467 | }
|
468 | return this;
|
469 | }
|
470 |
|
471 | |
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | public middlewareOptions(options: MiddlewareOptions[]): GraphRequest {
|
478 | this._middlewareOptions = options;
|
479 | return this;
|
480 | }
|
481 |
|
482 | |
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 | public version(version: string): GraphRequest {
|
489 | this.urlComponents.version = version;
|
490 | return this;
|
491 | }
|
492 |
|
493 | |
494 |
|
495 |
|
496 |
|
497 |
|
498 |
|
499 | public responseType(responseType: ResponseType): GraphRequest {
|
500 | this._responseType = responseType;
|
501 | return this;
|
502 | }
|
503 |
|
504 | |
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 | |
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 | public select(properties: string | string[]): GraphRequest {
|
517 | this.addCsvQueryParameter("$select", properties, arguments);
|
518 | return this;
|
519 | }
|
520 |
|
521 | |
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 | public expand(properties: string | string[]): GraphRequest {
|
528 | this.addCsvQueryParameter("$expand", properties, arguments);
|
529 | return this;
|
530 | }
|
531 |
|
532 | |
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 | public orderby(properties: string | string[]): GraphRequest {
|
539 | this.addCsvQueryParameter("$orderby", properties, arguments);
|
540 | return this;
|
541 | }
|
542 |
|
543 | |
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 | public filter(filterStr: string): GraphRequest {
|
550 | this.urlComponents.oDataQueryParams.$filter = filterStr;
|
551 | return this;
|
552 | }
|
553 |
|
554 | |
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 | public search(searchStr: string): GraphRequest {
|
561 | this.urlComponents.oDataQueryParams.$search = searchStr;
|
562 | return this;
|
563 | }
|
564 |
|
565 | |
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 | public top(n: number): GraphRequest {
|
572 | this.urlComponents.oDataQueryParams.$top = n;
|
573 | return this;
|
574 | }
|
575 |
|
576 | |
577 |
|
578 |
|
579 |
|
580 |
|
581 |
|
582 | public skip(n: number): GraphRequest {
|
583 | this.urlComponents.oDataQueryParams.$skip = n;
|
584 | return this;
|
585 | }
|
586 |
|
587 | |
588 |
|
589 |
|
590 |
|
591 |
|
592 |
|
593 | public skipToken(token: string): GraphRequest {
|
594 | this.urlComponents.oDataQueryParams.$skipToken = token;
|
595 | return this;
|
596 | }
|
597 |
|
598 | |
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 | public count(isCount = true): GraphRequest {
|
605 | this.urlComponents.oDataQueryParams.$count = isCount.toString();
|
606 | return this;
|
607 | }
|
608 |
|
609 | |
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 | |
616 |
|
617 |
|
618 |
|
619 | public query(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest {
|
620 | return this.parseQueryParameter(queryDictionaryOrString);
|
621 | }
|
622 |
|
623 | |
624 |
|
625 |
|
626 |
|
627 |
|
628 |
|
629 |
|
630 | public async get(callback?: GraphRequestCallback): Promise<any> {
|
631 | const url = this.buildFullUrl();
|
632 | const options: FetchOptions = {
|
633 | method: RequestMethod.GET,
|
634 | };
|
635 | const response = await this.send(url, options, callback);
|
636 | return response;
|
637 | }
|
638 |
|
639 | |
640 |
|
641 |
|
642 |
|
643 |
|
644 |
|
645 |
|
646 |
|
647 | public async post(content: any, callback?: GraphRequestCallback): Promise<any> {
|
648 | const url = this.buildFullUrl();
|
649 | const options: FetchOptions = {
|
650 | method: RequestMethod.POST,
|
651 | body: serializeContent(content),
|
652 | };
|
653 | const className: string = content && content.constructor && content.constructor.name;
|
654 | if (className === "FormData") {
|
655 |
|
656 | options.headers = {};
|
657 | } else {
|
658 | this.setHeaderContentType();
|
659 | options.headers = this._headers;
|
660 | }
|
661 | return await this.send(url, options, callback);
|
662 | }
|
663 |
|
664 | |
665 |
|
666 |
|
667 |
|
668 |
|
669 |
|
670 |
|
671 |
|
672 | public async create(content: any, callback?: GraphRequestCallback): Promise<any> {
|
673 | return await this.post(content, callback);
|
674 | }
|
675 |
|
676 | |
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 | public async put(content: any, callback?: GraphRequestCallback): Promise<any> {
|
685 | const url = this.buildFullUrl();
|
686 | this.setHeaderContentType();
|
687 | const options: FetchOptions = {
|
688 | method: RequestMethod.PUT,
|
689 | body: serializeContent(content),
|
690 | };
|
691 | return await this.send(url, options, callback);
|
692 | }
|
693 |
|
694 | |
695 |
|
696 |
|
697 |
|
698 |
|
699 |
|
700 |
|
701 |
|
702 | public async patch(content: any, callback?: GraphRequestCallback): Promise<any> {
|
703 | const url = this.buildFullUrl();
|
704 | this.setHeaderContentType();
|
705 | const options: FetchOptions = {
|
706 | method: RequestMethod.PATCH,
|
707 | body: serializeContent(content),
|
708 | };
|
709 | return await this.send(url, options, callback);
|
710 | }
|
711 |
|
712 | |
713 |
|
714 |
|
715 |
|
716 |
|
717 |
|
718 |
|
719 |
|
720 | public async update(content: any, callback?: GraphRequestCallback): Promise<any> {
|
721 | return await this.patch(content, callback);
|
722 | }
|
723 |
|
724 | |
725 |
|
726 |
|
727 |
|
728 |
|
729 |
|
730 |
|
731 | public async delete(callback?: GraphRequestCallback): Promise<any> {
|
732 | const url = this.buildFullUrl();
|
733 | const options: FetchOptions = {
|
734 | method: RequestMethod.DELETE,
|
735 | };
|
736 | return await this.send(url, options, callback);
|
737 | }
|
738 |
|
739 | |
740 |
|
741 |
|
742 |
|
743 |
|
744 |
|
745 |
|
746 | public async del(callback?: GraphRequestCallback): Promise<any> {
|
747 | return await this.delete(callback);
|
748 | }
|
749 |
|
750 | |
751 |
|
752 |
|
753 |
|
754 |
|
755 |
|
756 |
|
757 | public async getStream(callback?: GraphRequestCallback): Promise<any> {
|
758 | const url = this.buildFullUrl();
|
759 | const options = {
|
760 | method: RequestMethod.GET,
|
761 | };
|
762 | this.responseType(ResponseType.STREAM);
|
763 | return await this.send(url, options, callback);
|
764 | }
|
765 |
|
766 | |
767 |
|
768 |
|
769 |
|
770 |
|
771 |
|
772 |
|
773 |
|
774 | public async putStream(stream: any, callback?: GraphRequestCallback): Promise<any> {
|
775 | const url = this.buildFullUrl();
|
776 | const options = {
|
777 | method: RequestMethod.PUT,
|
778 | headers: {
|
779 | "Content-Type": "application/octet-stream",
|
780 | },
|
781 | body: stream,
|
782 | };
|
783 | return await this.send(url, options, callback);
|
784 | }
|
785 | }
|