Skip to content

Commit 60ad5c9

Browse files
added non-atomic batch operation tests #145
1 parent c74286e commit 60ad5c9

File tree

11 files changed

+1573
-1346
lines changed

11 files changed

+1573
-1346
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ ifmatch | `string` | `retrieve`, `update`, `upsert`, `deleteRecord` | Sets If-Ma
314314
ifnonematch | `string` | `retrieve`, `upsert` | Sets If-None-Match header value that enables to use conditional retrieval in applicable requests. [More Info](https://msdn.microsoft.com/en-us/library/mt607711.aspx).
315315
impersonate | `string` | All | Impersonates a user based on their systemuserid by adding a "MSCRMCallerID" header. A String representing the GUID value for the Dynamics 365 systemuserid. [More Info](https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/webapi/impersonate-another-user-web-api)
316316
impersonateAAD | `string` | All | Impersonates a user based on their Azure Active Directory (AAD) object id by passing that value along with the header "CallerObjectId". A String should represent a GUID value. [More Info](https://docs.microsoft.com/en-us/powerapps/developer/common-data-service/webapi/impersonate-another-user-web-api)
317+
inChangeSet | `boolean` | All, except `uploadFile`, `downloadFile`, `retrieveAll`, `countAll`, `fetchAll`, `search`, `suggest`, `autocomplete` | Indicates if an operation must be included in a Change Set or not. Works in Batch Operations only. `true` by default, except for GET operations - they are not allowed in Change Sets.
317318
includeAnnotations | `string` | `retrieve`, `retrieveMultiple`, `retrieveAll`, `create`, `update`, `upsert` | Sets Prefer header with value "odata.include-annotations=" and the specified annotation. Annotations provide additional information about lookups, options sets and other complex attribute types.
318319
key | `string` | `retrieve`, `create`, `update`, `upsert`, `deleteRecord`, `uploadFile`, `downloadFile`, `callAction`, `callFunction` | A string representing collection record's Primary Key (GUID) or Alternate Key(s).
319320
maxPageSize | `number` | `retrieveMultiple`, `retrieveAll` | Sets the odata.maxpagesize preference value to request the number of entities returned in the response.

dist/dynamics-web-api.cjs.js

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/dynamics-web-api.cjs.min.js

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/dynamics-web-api.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*! dynamics-web-api v2.0.0-alpha.1 (c) 2023 Aleksandr Rogov */
1+
/*! dynamics-web-api v2.0.0-beta.1 (c) 2023 Aleksandr Rogov */
22
/// <reference types="node" />
33
/**
44
* Microsoft Dynamics CRM Web API helper library written in JavaScript.

dist/dynamics-web-api.js

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/dynamics-web-api.min.js

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dynamics-web-api",
3-
"version": "2.0.0-alpha.1",
3+
"version": "2.0.0-beta.1",
44
"description": "DynamicsWebApi is a Microsoft Dynamics CRM Web API helper library",
55
"keywords": [
66
"crm",

src/client/RequestClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class RequestClient {
6363

6464
if (!batchRequest) throw ErrorHelper.batchIsEmpty();
6565

66-
const batchResult = RequestUtility.convertToBatch(batchRequest, config);
66+
const batchResult = RequestUtility.convertToBatch(batchRequest, config, request);
6767

6868
processedData = batchResult.body;
6969
request.headers = { ...batchResult.headers, ...request.headers };

src/utils/Request.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ export class RequestUtility {
419419
return prefer.join(",");
420420
}
421421

422-
static convertToBatch(requests: Core.InternalRequest[], config: InternalConfig): Core.InternalBatchRequest {
422+
static convertToBatch(requests: Core.InternalRequest[], config: InternalConfig, batchRequest?: Core.InternalRequest): Core.InternalBatchRequest {
423423
const batchBoundary = `dwa_batch_${Utility.generateUUID()}`;
424424

425425
const batchBody: string[] = [];
@@ -428,6 +428,7 @@ export class RequestUtility {
428428

429429
requests.forEach((internalRequest) => {
430430
internalRequest.functionName = "executeBatch";
431+
if (batchRequest?.inChangeSet === false) internalRequest.inChangeSet = false;
431432
const inChangeSet = internalRequest.method === "GET" ? false : !!internalRequest.inChangeSet;
432433

433434
if (!inChangeSet && currentChangeSet) {
@@ -466,7 +467,7 @@ export class RequestUtility {
466467
batchBody.push(`\n${internalRequest.method} ${internalRequest.path} HTTP/1.1`);
467468
}
468469

469-
if (!inChangeSet) {
470+
if (internalRequest.method === "GET") {
470471
batchBody.push("Accept: application/json");
471472
} else {
472473
batchBody.push("Content-Type: application/json");
@@ -478,10 +479,8 @@ export class RequestUtility {
478479
batchBody.push(`${key}: ${internalRequest.headers[key]}`);
479480
}
480481

481-
const data = internalRequest.data;
482-
483-
if (inChangeSet && data) {
484-
batchBody.push(`\n${RequestUtility.processData(data, config)}`);
482+
if (internalRequest.data) {
483+
batchBody.push(`\n${RequestUtility.processData(internalRequest.data, config)}`);
485484
}
486485
});
487486

tests/main.spec.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,174 @@ describe("dynamicsWebApi.retrieveMultiple -", () => {
6969
});
7070
});
7171
});
72+
73+
describe("dynamicsWebApi.executeBatch -", () => {
74+
describe("non-atomic global - create / create (Content-ID in a header gets cleared)", function () {
75+
let scope;
76+
const rBody = mocks.data.batchCreateContentIDPayloadNonAtomic;
77+
const rBodys = rBody.split("\n");
78+
let checkBody = "";
79+
for (let i = 0; i < rBodys.length; i++) {
80+
checkBody += rBodys[i];
81+
}
82+
before(function () {
83+
const response = mocks.responses.batchUpdateDelete;
84+
scope = nock(mocks.webApiUrl)
85+
.filteringRequestBody((body) => {
86+
body = body.replace(/dwa_batch_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "dwa_batch_XXX");
87+
body = body.replace(/changeset_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "changeset_XXX");
88+
const bodys = body.split("\n");
89+
90+
let resultBody = "";
91+
for (let i = 0; i < bodys.length; i++) {
92+
resultBody += bodys[i];
93+
}
94+
95+
return resultBody;
96+
})
97+
.post("/$batch", checkBody)
98+
.reply(response.status, response.responseText, response.responseHeaders);
99+
});
100+
101+
after(function () {
102+
nock.cleanAll();
103+
});
104+
105+
it("returns a correct response", async () => {
106+
dynamicsWebApiTest.startBatch();
107+
108+
dynamicsWebApiTest.create({ collection: "records", data: { firstname: "Test", lastname: "Batch!" }, contentId: "1" });
109+
dynamicsWebApiTest.create({ collection: "tests", data: { firstname: "Test1", lastname: "Batch!", "[email protected]": "$1" } });
110+
111+
try {
112+
const object = await dynamicsWebApiTest.executeBatch({
113+
inChangeSet: false,
114+
});
115+
116+
expect(object.length).to.be.eq(2);
117+
118+
expect(object[0]).to.be.eq(mocks.data.testEntityId);
119+
expect(object[1]).to.be.undefined;
120+
} catch (error) {
121+
console.error(error);
122+
throw error;
123+
}
124+
});
125+
126+
it("all requests have been made", () => {
127+
expect(scope.isDone()).to.be.true;
128+
});
129+
});
130+
131+
describe("non-atomic per request - create / create (Content-ID in a header gets cleared)", function () {
132+
let scope;
133+
const rBody = mocks.data.batchCreateContentIDPayloadNonAtomic;
134+
const rBodys = rBody.split("\n");
135+
let checkBody = "";
136+
for (let i = 0; i < rBodys.length; i++) {
137+
checkBody += rBodys[i];
138+
}
139+
before(function () {
140+
const response = mocks.responses.batchUpdateDelete;
141+
scope = nock(mocks.webApiUrl)
142+
.filteringRequestBody((body) => {
143+
body = body.replace(/dwa_batch_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "dwa_batch_XXX");
144+
body = body.replace(/changeset_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "changeset_XXX");
145+
const bodys = body.split("\n");
146+
147+
let resultBody = "";
148+
for (let i = 0; i < bodys.length; i++) {
149+
resultBody += bodys[i];
150+
}
151+
152+
return resultBody;
153+
})
154+
.post("/$batch", checkBody)
155+
.reply(response.status, response.responseText, response.responseHeaders);
156+
});
157+
158+
after(function () {
159+
nock.cleanAll();
160+
});
161+
162+
it("returns a correct response", async () => {
163+
dynamicsWebApiTest.startBatch();
164+
165+
dynamicsWebApiTest.create({ collection: "records", data: { firstname: "Test", lastname: "Batch!" }, contentId: "1", inChangeSet: false });
166+
dynamicsWebApiTest.create({ collection: "tests", data: { firstname: "Test1", lastname: "Batch!", "[email protected]": "$1" }, inChangeSet: false });
167+
168+
try {
169+
const object = await dynamicsWebApiTest.executeBatch();
170+
171+
expect(object.length).to.be.eq(2);
172+
173+
expect(object[0]).to.be.eq(mocks.data.testEntityId);
174+
expect(object[1]).to.be.undefined;
175+
} catch (error) {
176+
console.error(error);
177+
throw error;
178+
}
179+
});
180+
181+
it("all requests have been made", () => {
182+
expect(scope.isDone()).to.be.true;
183+
});
184+
});
185+
186+
describe("non-atomic & atomic mixed - create / create (Content-ID in a payload)", function () {
187+
let scope;
188+
const rBody = mocks.data.batchCreateContentIDPayloadNonAtomicMixed;
189+
const rBodys = rBody.split("\n");
190+
let checkBody = "";
191+
for (let i = 0; i < rBodys.length; i++) {
192+
checkBody += rBodys[i];
193+
}
194+
before(function () {
195+
const response = mocks.responses.batchUpdateDelete;
196+
scope = nock(mocks.webApiUrl)
197+
.filteringRequestBody((body) => {
198+
body = body.replace(/dwa_batch_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "dwa_batch_XXX");
199+
body = body.replace(/changeset_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, "changeset_XXX");
200+
const bodys = body.split("\n");
201+
202+
let resultBody = "";
203+
for (let i = 0; i < bodys.length; i++) {
204+
resultBody += bodys[i];
205+
}
206+
207+
console.log(checkBody);
208+
console.log(resultBody);
209+
return resultBody;
210+
})
211+
.post("/$batch", checkBody)
212+
.reply(response.status, response.responseText, response.responseHeaders);
213+
});
214+
215+
after(function () {
216+
nock.cleanAll();
217+
});
218+
219+
it("returns a correct response", async () => {
220+
dynamicsWebApiTest.startBatch();
221+
222+
dynamicsWebApiTest.create({ collection: "records", data: { firstname: "Test", lastname: "Batch!" }, contentId: "1", inChangeSet: false });
223+
dynamicsWebApiTest.create({ collection: "tests", data: { firstname: "Test1", lastname: "Batch!", "[email protected]": "$1" } });
224+
225+
try {
226+
const object = await dynamicsWebApiTest.executeBatch();
227+
228+
expect(object.length).to.be.eq(2);
229+
230+
expect(object[0]).to.be.eq(mocks.data.testEntityId);
231+
expect(object[1]).to.be.undefined;
232+
} catch (error) {
233+
console.error(error);
234+
throw error;
235+
}
236+
});
237+
238+
it("all requests have been made", () => {
239+
expect(scope.isDone()).to.be.true;
240+
});
241+
});
242+
});

0 commit comments

Comments
 (0)