UNPKG

16.8 kBPlain TextView Raw
1/**
2 * -------------------------------------------------------------------------------------------
3 * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4 * See License in the project root for license information.
5 * -------------------------------------------------------------------------------------------
6 */
7
8/**
9 * @module BatchRequestContent
10 */
11import { RequestMethod } from "../RequestMethod";
12
13/**
14 * @interface
15 * Signature to represent the buffer request body parsing method
16 * @property {Function} buffer - Returns a promise that resolves to a buffer of the request body
17 */
18interface NodeBody {
19 buffer(): Promise<Buffer>;
20}
21
22/**
23 * @interface
24 * Signature to represent the Request for both Node and browser environments
25 * @extends Request
26 * @extends NodeBody
27 */
28interface IsomorphicRequest extends Request, NodeBody {}
29
30/**
31 * @interface
32 * Signature representing BatchRequestStep data
33 * @property {string} id - Unique identity for the request, Should not be an empty string
34 * @property {string[]} [dependsOn] - Array of dependencies
35 * @property {Request} request - The Request object
36 */
37export interface BatchRequestStep {
38 id: string;
39 dependsOn?: string[];
40 request: Request;
41}
42
43/**
44 * @interface
45 * Signature representing single request in a Batching
46 * @extends RequestInit
47 * @see {@link https://github.com/Microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L1337} and {@link https://fetch.spec.whatwg.org/#requestinit}
48 *
49 * @property {string} url - The url value of the request
50 */
51
52export interface RequestData extends RequestInit {
53 url: string;
54}
55
56/**
57 * @interface
58 * Signature representing batch request data
59 * @property {string} id - Unique identity for the request, Should not be an empty string
60 * @property {string[]} [dependsOn] - Array of dependencies
61 */
62export interface BatchRequestData extends RequestData {
63 id: string;
64 dependsOn?: string[];
65}
66
67/**
68 * @interface
69 * Signature representing batch request body
70 * @property {BatchRequestData[]} requests - Array of request data, a json representation of requests for batch
71 */
72
73export interface BatchRequestBody {
74 requests: BatchRequestData[];
75}
76
77/**
78 * @class
79 * Class for handling BatchRequestContent
80 */
81export class BatchRequestContent {
82 /**
83 * @private
84 * @static
85 * Limit for number of requests {@link - https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
86 */
87 private static requestLimit = 20;
88
89 /**
90 * @public
91 * To keep track of requests, key will be id of the request and value will be the request json
92 */
93 public requests: Map<string, BatchRequestStep>;
94
95 /**
96 * @private
97 * @static
98 * Validates the dependency chain of the requests
99 *
100 * Note:
101 * Individual requests can depend on other individual requests. Currently, requests can only depend on a single other request, and must follow one of these three patterns:
102 * 1. Parallel - no individual request states a dependency in the dependsOn property.
103 * 2. Serial - all individual requests depend on the previous individual request.
104 * 3. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.
105 * As JSON batching matures, these limitations will be removed.
106 * @see {@link https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
107 *
108 * @param {Map<string, BatchRequestStep>} requests - The map of requests.
109 * @returns The boolean indicating the validation status
110 */
111
112 private static validateDependencies(requests: Map<string, BatchRequestStep>): boolean {
113 const isParallel = (reqs: Map<string, BatchRequestStep>): boolean => {
114 const iterator = reqs.entries();
115 let cur = iterator.next();
116 while (!cur.done) {
117 const curReq = cur.value[1];
118 if (curReq.dependsOn !== undefined && curReq.dependsOn.length > 0) {
119 return false;
120 }
121 cur = iterator.next();
122 }
123 return true;
124 };
125 const isSerial = (reqs: Map<string, BatchRequestStep>): boolean => {
126 const iterator = reqs.entries();
127 let cur = iterator.next();
128 const firstRequest: BatchRequestStep = cur.value[1];
129 if (firstRequest.dependsOn !== undefined && firstRequest.dependsOn.length > 0) {
130 return false;
131 }
132 let prev = cur;
133 cur = iterator.next();
134 while (!cur.done) {
135 const curReq: BatchRequestStep = cur.value[1];
136 if (curReq.dependsOn === undefined || curReq.dependsOn.length !== 1 || curReq.dependsOn[0] !== prev.value[1].id) {
137 return false;
138 }
139 prev = cur;
140 cur = iterator.next();
141 }
142 return true;
143 };
144 const isSame = (reqs: Map<string, BatchRequestStep>): boolean => {
145 const iterator = reqs.entries();
146 let cur = iterator.next();
147 const firstRequest: BatchRequestStep = cur.value[1];
148 let dependencyId: string;
149 if (firstRequest.dependsOn === undefined || firstRequest.dependsOn.length === 0) {
150 dependencyId = firstRequest.id;
151 } else {
152 if (firstRequest.dependsOn.length === 1) {
153 const fDependencyId = firstRequest.dependsOn[0];
154 if (fDependencyId !== firstRequest.id && reqs.has(fDependencyId)) {
155 dependencyId = fDependencyId;
156 } else {
157 return false;
158 }
159 } else {
160 return false;
161 }
162 }
163 cur = iterator.next();
164 while (!cur.done) {
165 const curReq = cur.value[1];
166 if ((curReq.dependsOn === undefined || curReq.dependsOn.length === 0) && dependencyId !== curReq.id) {
167 return false;
168 }
169 if (curReq.dependsOn !== undefined && curReq.dependsOn.length !== 0) {
170 if (curReq.dependsOn.length === 1 && (curReq.id === dependencyId || curReq.dependsOn[0] !== dependencyId)) {
171 return false;
172 }
173 if (curReq.dependsOn.length > 1) {
174 return false;
175 }
176 }
177 cur = iterator.next();
178 }
179 return true;
180 };
181 if (requests.size === 0) {
182 const error = new Error("Empty requests map, Please provide at least one request.");
183 error.name = "Empty Requests Error";
184 throw error;
185 }
186 return isParallel(requests) || isSerial(requests) || isSame(requests);
187 }
188
189 /**
190 * @private
191 * @static
192 * @async
193 * Converts Request Object instance to a JSON
194 * @param {IsomorphicRequest} request - The IsomorphicRequest Object instance
195 * @returns A promise that resolves to JSON representation of a request
196 */
197 private static async getRequestData(request: IsomorphicRequest): Promise<RequestData> {
198 const requestData: RequestData = {
199 url: "",
200 };
201 const hasHttpRegex = new RegExp("^https?://");
202 // Stripping off hostname, port and url scheme
203 requestData.url = hasHttpRegex.test(request.url) ? "/" + request.url.split(/.*?\/\/.*?\//)[1] : request.url;
204 requestData.method = request.method;
205 const headers = {};
206 request.headers.forEach((value, key) => {
207 headers[key] = value;
208 });
209 if (Object.keys(headers).length) {
210 requestData.headers = headers;
211 }
212 if (request.method === RequestMethod.PATCH || request.method === RequestMethod.POST || request.method === RequestMethod.PUT) {
213 requestData.body = await BatchRequestContent.getRequestBody(request);
214 }
215 /**
216 * TODO: Check any other property needs to be used from the Request object and add them
217 */
218 return requestData;
219 }
220
221 /**
222 * @private
223 * @static
224 * @async
225 * Gets the body of a Request object instance
226 * @param {IsomorphicRequest} request - The IsomorphicRequest object instance
227 * @returns The Promise that resolves to a body value of a Request
228 */
229 private static async getRequestBody(request: IsomorphicRequest): Promise<any> {
230 let bodyParsed = false;
231 let body;
232 try {
233 const cloneReq = request.clone();
234 body = await cloneReq.json();
235 bodyParsed = true;
236 } catch (e) {
237 //TODO- Handle empty catches
238 }
239 if (!bodyParsed) {
240 try {
241 if (typeof Blob !== "undefined") {
242 const blob = await request.blob();
243 const reader = new FileReader();
244 body = await new Promise((resolve) => {
245 reader.addEventListener(
246 "load",
247 () => {
248 const dataURL = reader.result as string;
249 /**
250 * Some valid dataURL schemes:
251 * 1. data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh
252 * 2. data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678
253 * 3. data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
254 * 4. data:image/png,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
255 * 5. data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
256 * @see Syntax {@link https://en.wikipedia.org/wiki/Data_URI_scheme} for more
257 */
258 const regex = new RegExp("^s*data:(.+?/.+?(;.+?=.+?)*)?(;base64)?,(.*)s*$");
259 const segments = regex.exec(dataURL);
260 resolve(segments[4]);
261 },
262 false,
263 );
264 reader.readAsDataURL(blob);
265 });
266 } else if (typeof Buffer !== "undefined") {
267 const buffer = await request.buffer();
268 body = buffer.toString("base64");
269 }
270 bodyParsed = true;
271 } catch (e) {
272 // TODO-Handle empty catches
273 }
274 }
275 return body;
276 }
277
278 /**
279 * @public
280 * @constructor
281 * Constructs a BatchRequestContent instance
282 * @param {BatchRequestStep[]} [requests] - Array of requests value
283 * @returns An instance of a BatchRequestContent
284 */
285 public constructor(requests?: BatchRequestStep[]) {
286 this.requests = new Map();
287 if (typeof requests !== "undefined") {
288 const limit = BatchRequestContent.requestLimit;
289 if (requests.length > limit) {
290 const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
291 error.name = "Limit Exceeded Error";
292 throw error;
293 }
294 for (const req of requests) {
295 this.addRequest(req);
296 }
297 }
298 }
299
300 /**
301 * @public
302 * Adds a request to the batch request content
303 * @param {BatchRequestStep} request - The request value
304 * @returns The id of the added request
305 */
306 public addRequest(request: BatchRequestStep): string {
307 const limit = BatchRequestContent.requestLimit;
308 if (request.id === "") {
309 const error = new Error(`Id for a request is empty, Please provide an unique id`);
310 error.name = "Empty Id For Request";
311 throw error;
312 }
313 if (this.requests.size === limit) {
314 const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
315 error.name = "Limit Exceeded Error";
316 throw error;
317 }
318 if (this.requests.has(request.id)) {
319 const error = new Error(`Adding request with duplicate id ${request.id}, Make the id of the requests unique`);
320 error.name = "Duplicate RequestId Error";
321 throw error;
322 }
323 this.requests.set(request.id, request);
324 return request.id;
325 }
326
327 /**
328 * @public
329 * Removes request from the batch payload and its dependencies from all dependents
330 * @param {string} requestId - The id of a request that needs to be removed
331 * @returns The boolean indicating removed status
332 */
333 public removeRequest(requestId: string): boolean {
334 const deleteStatus = this.requests.delete(requestId);
335 const iterator = this.requests.entries();
336 let cur = iterator.next();
337 /**
338 * Removing dependencies where this request is present as a dependency
339 */
340 while (!cur.done) {
341 const dependencies = cur.value[1].dependsOn;
342 if (typeof dependencies !== "undefined") {
343 const index = dependencies.indexOf(requestId);
344 if (index !== -1) {
345 dependencies.splice(index, 1);
346 }
347 if (dependencies.length === 0) {
348 delete cur.value[1].dependsOn;
349 }
350 }
351 cur = iterator.next();
352 }
353 return deleteStatus;
354 }
355
356 /**
357 * @public
358 * @async
359 * Serialize content from BatchRequestContent instance
360 * @returns The body content to make batch request
361 */
362 public async getContent(): Promise<BatchRequestBody> {
363 const requests: BatchRequestData[] = [];
364 const requestBody: BatchRequestBody = {
365 requests,
366 };
367 const iterator = this.requests.entries();
368 let cur = iterator.next();
369 if (cur.done) {
370 const error = new Error("No requests added yet, Please add at least one request.");
371 error.name = "Empty Payload";
372 throw error;
373 }
374 if (!BatchRequestContent.validateDependencies(this.requests)) {
375 const error = new Error(`Invalid dependency found, Dependency should be:
3761. Parallel - no individual request states a dependency in the dependsOn property.
3772. Serial - all individual requests depend on the previous individual request.
3783. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.`);
379 error.name = "Invalid Dependency";
380 throw error;
381 }
382 while (!cur.done) {
383 const requestStep: BatchRequestStep = cur.value[1];
384 const batchRequestData: BatchRequestData = (await BatchRequestContent.getRequestData(requestStep.request as IsomorphicRequest)) as BatchRequestData;
385 /**
386 * @see{@https://tools.ietf.org/html/rfc7578#section-4.4}
387 * TODO- Setting/Defaulting of content-type header to the correct value
388 * @see {@link https://developer.microsoft.com/en-us/graph/docs/concepts/json_batching#request-format}
389 */
390 if (batchRequestData.body !== undefined && (batchRequestData.headers === undefined || batchRequestData.headers["content-type"] === undefined)) {
391 const error = new Error(`Content-type header is not mentioned for request #${requestStep.id}, For request having body, Content-type header should be mentioned`);
392 error.name = "Invalid Content-type header";
393 throw error;
394 }
395 batchRequestData.id = requestStep.id;
396 if (requestStep.dependsOn !== undefined && requestStep.dependsOn.length > 0) {
397 batchRequestData.dependsOn = requestStep.dependsOn;
398 }
399 requests.push(batchRequestData);
400 cur = iterator.next();
401 }
402 requestBody.requests = requests;
403 return requestBody;
404 }
405
406 /**
407 * @public
408 * Adds a dependency for a given dependent request
409 * @param {string} dependentId - The id of the dependent request
410 * @param {string} [dependencyId] - The id of the dependency request, if not specified the preceding request will be considered as a dependency
411 * @returns Nothing
412 */
413 public addDependency(dependentId: string, dependencyId?: string): void {
414 if (!this.requests.has(dependentId)) {
415 const error = new Error(`Dependent ${dependentId} does not exists, Please check the id`);
416 error.name = "Invalid Dependent";
417 throw error;
418 }
419 if (typeof dependencyId !== "undefined" && !this.requests.has(dependencyId)) {
420 const error = new Error(`Dependency ${dependencyId} does not exists, Please check the id`);
421 error.name = "Invalid Dependency";
422 throw error;
423 }
424 if (typeof dependencyId !== "undefined") {
425 const dependent = this.requests.get(dependentId);
426 if (dependent.dependsOn === undefined) {
427 dependent.dependsOn = [];
428 }
429 if (dependent.dependsOn.indexOf(dependencyId) !== -1) {
430 const error = new Error(`Dependency ${dependencyId} is already added for the request ${dependentId}`);
431 error.name = "Duplicate Dependency";
432 throw error;
433 }
434 dependent.dependsOn.push(dependencyId);
435 } else {
436 const iterator = this.requests.entries();
437 let prev;
438 let cur = iterator.next();
439 while (!cur.done && cur.value[1].id !== dependentId) {
440 prev = cur;
441 cur = iterator.next();
442 }
443 if (typeof prev !== "undefined") {
444 const dId = prev.value[0];
445 if (cur.value[1].dependsOn === undefined) {
446 cur.value[1].dependsOn = [];
447 }
448 if (cur.value[1].dependsOn.indexOf(dId) !== -1) {
449 const error = new Error(`Dependency ${dId} is already added for the request ${dependentId}`);
450 error.name = "Duplicate Dependency";
451 throw error;
452 }
453 cur.value[1].dependsOn.push(dId);
454 } else {
455 const error = new Error(`Can't add dependency ${dependencyId}, There is only a dependent request in the batch`);
456 error.name = "Invalid Dependency Addition";
457 throw error;
458 }
459 }
460 }
461
462 /**
463 * @public
464 * Removes a dependency for a given dependent request id
465 * @param {string} dependentId - The id of the dependent request
466 * @param {string} [dependencyId] - The id of the dependency request, if not specified will remove all the dependencies of that request
467 * @returns The boolean indicating removed status
468 */
469 public removeDependency(dependentId: string, dependencyId?: string): boolean {
470 const request = this.requests.get(dependentId);
471 if (typeof request === "undefined" || request.dependsOn === undefined || request.dependsOn.length === 0) {
472 return false;
473 }
474 if (typeof dependencyId !== "undefined") {
475 const index = request.dependsOn.indexOf(dependencyId);
476 if (index === -1) {
477 return false;
478 }
479 request.dependsOn.splice(index, 1);
480 return true;
481 } else {
482 delete request.dependsOn;
483 return true;
484 }
485 }
486}