Skip to content

Commit c344031

Browse files
authored
feat: add batch request content (#354)
* feat: add batch request content * update comment * Fetch batch requests and responses * Adds unit tests * fix comment * fix comment * Add comments * Serialize batch item * unwrap node * code cleanup * Return body as bytearray * Fix * More tests * delete unused file * error mappings are required * Adds collection response * Remove unused code * Update batch request * Rename batch item * update index file
1 parent 66645d0 commit c344031

10 files changed

+1134
-0
lines changed

src/content/BatchRequestBuilder.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { ErrorMappings, HttpMethod, RequestAdapter, RequestInformation } from "@microsoft/kiota-abstractions";
2+
import {
3+
BatchResponseBody,
4+
createBatchResponseContentFromDiscriminatorValue,
5+
serializeBatchRequestBody,
6+
} from "./BatchRequestStep";
7+
import { BatchResponseContent } from "./BatchResponseContent";
8+
import { BatchRequestContent } from "./BatchRequestContent";
9+
import { BatchRequestContentCollection } from "./BatchRequestContentCollection";
10+
import { BatchResponseContentCollection } from "./BatchResponseContentCollection";
11+
12+
export class BatchRequestBuilder {
13+
/**
14+
* @private
15+
* @static
16+
* Executes the requests in the batch request content
17+
*/
18+
private readonly requestAdapter: RequestAdapter;
19+
20+
/**
21+
* @private
22+
* @static
23+
* Error mappings to be used while deserializing the response
24+
*/
25+
private readonly errorMappings: ErrorMappings;
26+
27+
/**
28+
* Creates an instance of BatchRequestContent.
29+
* @param {RequestAdapter} requestAdapter - The request adapter to be used for executing the requests.
30+
* @param {ErrorMappings} errorMappings - The error mappings to be used while deserializing the response.
31+
* @throws {Error} If the request adapter is undefined.
32+
* @throws {Error} If the error mappings are undefined.
33+
*/
34+
constructor(requestAdapter: RequestAdapter, errorMappings: ErrorMappings) {
35+
if (!requestAdapter) {
36+
const error = new Error("Request adapter is undefined, Please provide a valid request adapter");
37+
error.name = "Invalid Request Adapter Error";
38+
throw error;
39+
}
40+
this.requestAdapter = requestAdapter;
41+
if (!errorMappings) {
42+
const error = new Error("Error mappings are undefined, Please provide a valid error mappings");
43+
error.name = "Invalid Error Mappings Error";
44+
throw error;
45+
}
46+
this.errorMappings = errorMappings;
47+
}
48+
49+
/**
50+
* @public
51+
* @async
52+
* Executes the batch request
53+
*/
54+
public async postBatchResponseContentAsync(
55+
batchRequestContent: BatchRequestContent,
56+
): Promise<BatchResponseContent | undefined> {
57+
const requestInformation = new RequestInformation();
58+
requestInformation.httpMethod = HttpMethod.POST;
59+
requestInformation.urlTemplate = "{+baseurl}/$batch";
60+
61+
const content = batchRequestContent.getContent();
62+
requestInformation.setContentFromParsable(
63+
this.requestAdapter,
64+
"application/json",
65+
content,
66+
serializeBatchRequestBody,
67+
);
68+
69+
requestInformation.headers.add("Content-Type", "application/json");
70+
71+
const result = await this.requestAdapter.send<BatchResponseBody>(
72+
requestInformation,
73+
createBatchResponseContentFromDiscriminatorValue,
74+
this.errorMappings,
75+
);
76+
77+
if (result === undefined) {
78+
return undefined;
79+
} else {
80+
return new BatchResponseContent(result);
81+
}
82+
}
83+
84+
/**
85+
* Executes the batch requests asynchronously.
86+
*
87+
* @returns {Promise<BatchResponseContent | undefined>} A promise that resolves to the batch response content or undefined.
88+
* @throws {Error} If the batch limit is exceeded.
89+
*/
90+
public async postBatchRequestContentCollectionAsync(
91+
collection: BatchRequestContentCollection,
92+
): Promise<BatchResponseContentCollection | undefined> {
93+
// chuck the batch requests into smaller batches
94+
const batches = collection.getBatchResponseContents();
95+
96+
// loop over batches and create batch request body
97+
const batchResponseBody: BatchResponseContent[] = [];
98+
for (const requestContent of batches) {
99+
const response = await requestContent.postAsync();
100+
if (response) {
101+
batchResponseBody.push(response);
102+
}
103+
}
104+
return new BatchResponseContentCollection(batchResponseBody);
105+
}
106+
}

src/content/BatchRequestContent.ts

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { RequestAdapter, RequestInformation, ErrorMappings } from "@microsoft/kiota-abstractions";
2+
import { BatchRequestStep, BatchRequestBody, convertRequestInformationToBatchItem } from "./BatchRequestStep";
3+
import { BatchResponseContent } from "./BatchResponseContent";
4+
import { BatchRequestBuilder } from "./BatchRequestBuilder";
5+
6+
/**
7+
* -------------------------------------------------------------------------------------------
8+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
9+
* See License in the project root for license information.
10+
* -------------------------------------------------------------------------------------------
11+
*/
12+
13+
/**
14+
* @module BatchRequestContent
15+
*/
16+
17+
/**
18+
* Represents the content of a batch request.
19+
*/
20+
export class BatchRequestContent {
21+
/**
22+
* @private
23+
* @static
24+
* Limit for number of requests {@link - https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
25+
*/
26+
private static get requestLimit() {
27+
return 20;
28+
}
29+
30+
/**
31+
* @public
32+
* To keep track of requests, key will be id of the request and value will be the request json
33+
*/
34+
public requests: Map<string, BatchRequestStep>;
35+
36+
/**
37+
* @private
38+
* @static
39+
* Executes the requests in the batch request content
40+
*/
41+
private readonly requestAdapter: RequestAdapter;
42+
43+
/**
44+
* @private
45+
* @static
46+
* Error mappings to be used while deserializing the response
47+
*/
48+
private readonly errorMappings: ErrorMappings;
49+
50+
/**
51+
* Creates an instance of BatchRequestContent.
52+
* @param {RequestAdapter} requestAdapter - The request adapter to be used for executing the requests.
53+
* @param {ErrorMappings} errorMappings - The error mappings to be used while deserializing the response.
54+
* @throws {Error} If the request adapter is undefined.
55+
* @throws {Error} If the error mappings are undefined.
56+
*/
57+
constructor(requestAdapter: RequestAdapter, errorMappings: ErrorMappings) {
58+
this.requests = new Map<string, BatchRequestStep>();
59+
if (!requestAdapter) {
60+
const error = new Error("Request adapter is undefined, Please provide a valid request adapter");
61+
error.name = "Invalid Request Adapter Error";
62+
throw error;
63+
}
64+
this.requestAdapter = requestAdapter;
65+
if (!errorMappings) {
66+
const error = new Error("Error mappings are undefined, Please provide a valid error mappings");
67+
error.name = "Invalid Error Mappings Error";
68+
throw error;
69+
}
70+
this.errorMappings = errorMappings;
71+
}
72+
73+
/**
74+
* @private
75+
* @static
76+
* Validates the dependency chain of the requests
77+
*
78+
* Note:
79+
* 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:
80+
* 1. Parallel - no individual request states a dependency in the dependsOn property.
81+
* 2. Serial - all individual requests depend on the previous individual request.
82+
* 3. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.
83+
* As JSON batching matures, these limitations will be removed.
84+
* @see {@link https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
85+
*
86+
* @param {Map<string, BatchRequestStep>} requests - The map of requests.
87+
* @returns The boolean indicating the validation status
88+
*/
89+
private static validateDependencies(requests: Map<string, BatchRequestStep>): boolean {
90+
const isParallel = (reqs: Map<string, BatchRequestStep>): boolean => {
91+
const iterator = reqs.entries();
92+
let cur = iterator.next();
93+
while (!cur.done) {
94+
const curReq = cur.value[1];
95+
if (curReq.dependsOn !== undefined && curReq.dependsOn.length > 0) {
96+
return false;
97+
}
98+
cur = iterator.next();
99+
}
100+
return true;
101+
};
102+
const isSerial = (reqs: Map<string, BatchRequestStep>): boolean => {
103+
const iterator = reqs.entries();
104+
let cur = iterator.next();
105+
if (cur.done || cur.value === undefined) return false;
106+
const firstRequest: BatchRequestStep = cur.value[1];
107+
if (firstRequest.dependsOn !== undefined && firstRequest.dependsOn.length > 0) {
108+
return false;
109+
}
110+
let prev = cur;
111+
cur = iterator.next();
112+
while (!cur.done) {
113+
const curReq: BatchRequestStep = cur.value[1];
114+
if (
115+
curReq.dependsOn === undefined ||
116+
curReq.dependsOn.length !== 1 ||
117+
curReq.dependsOn[0] !== prev.value[1].id
118+
) {
119+
return false;
120+
}
121+
prev = cur;
122+
cur = iterator.next();
123+
}
124+
return true;
125+
};
126+
const isSame = (reqs: Map<string, BatchRequestStep>): boolean => {
127+
const iterator = reqs.entries();
128+
let cur = iterator.next();
129+
if (cur.done || cur.value === undefined) return false;
130+
const firstRequest: BatchRequestStep = cur.value[1];
131+
let dependencyId: string;
132+
if (firstRequest.dependsOn === undefined || firstRequest.dependsOn.length === 0) {
133+
dependencyId = firstRequest.id;
134+
} else {
135+
if (firstRequest.dependsOn.length === 1) {
136+
const fDependencyId = firstRequest.dependsOn[0];
137+
if (fDependencyId !== firstRequest.id && reqs.has(fDependencyId)) {
138+
dependencyId = fDependencyId;
139+
} else {
140+
return false;
141+
}
142+
} else {
143+
return false;
144+
}
145+
}
146+
cur = iterator.next();
147+
while (!cur.done) {
148+
const curReq = cur.value[1];
149+
if ((curReq.dependsOn === undefined || curReq.dependsOn.length === 0) && dependencyId !== curReq.id) {
150+
return false;
151+
}
152+
if (curReq.dependsOn !== undefined && curReq.dependsOn.length !== 0) {
153+
if (curReq.dependsOn.length === 1 && (curReq.id === dependencyId || curReq.dependsOn[0] !== dependencyId)) {
154+
return false;
155+
}
156+
if (curReq.dependsOn.length > 1) {
157+
return false;
158+
}
159+
}
160+
cur = iterator.next();
161+
}
162+
return true;
163+
};
164+
if (requests.size === 0) {
165+
const error = new Error("Empty requests map, Please provide at least one request.");
166+
error.name = "Empty Requests Error";
167+
throw error;
168+
}
169+
return isParallel(requests) || isSerial(requests) || isSame(requests);
170+
}
171+
172+
/**
173+
* @public
174+
* Adds a request to the batch request content
175+
* @param {BatchRequestStep} request - The request value
176+
* @returns The id of the added request
177+
*/
178+
private addRequest(request: BatchRequestStep): string {
179+
const limit = BatchRequestContent.requestLimit;
180+
if (request.id === "") {
181+
const error = new Error(`Id for a request is empty, Please provide an unique id`);
182+
error.name = "Empty Id For Request";
183+
throw error;
184+
}
185+
if (this.requests.size === limit) {
186+
const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
187+
error.name = "Limit Exceeded Error";
188+
throw error;
189+
}
190+
if (this.requests.has(request.id)) {
191+
const error = new Error(`Adding request with duplicate id ${request.id}, Make the id of the requests unique`);
192+
error.name = "Duplicate RequestId Error";
193+
throw error;
194+
}
195+
this.requests.set(request.id, request);
196+
return request.id;
197+
}
198+
199+
/**
200+
* @public
201+
* Adds multiple requests to the batch request content
202+
* @param {BatchRequestStep[]} requests - The request value
203+
*/
204+
public addRequests(requests: BatchRequestStep[]) {
205+
// loop and add this request
206+
requests.forEach(request => {
207+
this.addRequest(request);
208+
});
209+
}
210+
211+
/**
212+
* @public
213+
* Receives a request information object, converts it and adds it to the batch request execution chain
214+
* @param requestInformation - The request information object
215+
* @param batchId - The batch id to be used for the request
216+
*/
217+
public addBatchRequest(requestInformation: RequestInformation, batchId?: string): BatchRequestStep {
218+
const batchItem = convertRequestInformationToBatchItem(this.requestAdapter, requestInformation, batchId);
219+
this.addRequest(batchItem);
220+
return batchItem;
221+
}
222+
223+
/**
224+
* @public
225+
* Gets the content of the batch request
226+
* @returns The batch request collection
227+
*/
228+
public readonly getContent = (): BatchRequestBody => {
229+
const content = {
230+
requests: Array.from(this.requests.values()),
231+
};
232+
if (!BatchRequestContent.validateDependencies(this.requests)) {
233+
const error = new Error("Invalid dependency chain found in the requests, Please provide valid dependency chain");
234+
error.name = "Invalid Dependency Chain Error";
235+
throw error;
236+
}
237+
return content;
238+
};
239+
240+
/**
241+
* @public
242+
* @async
243+
* Executes the batch request
244+
*/
245+
public async postAsync(): Promise<BatchResponseContent | undefined> {
246+
const requestBuilder = new BatchRequestBuilder(this.requestAdapter, this.errorMappings);
247+
return await requestBuilder.postBatchResponseContentAsync(this);
248+
}
249+
}

0 commit comments

Comments
 (0)