Skip to content

Commit 258ebe3

Browse files
add: use Content-ID to reference requests in a Change Set
1 parent 3ef9e2a commit 258ebe3

File tree

10 files changed

+203
-32
lines changed

10 files changed

+203
-32
lines changed

README.md

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,19 +229,20 @@ Property Name | Type | Operation(s) Supported | Description
229229
------------ | ------------- | ------------- | -------------
230230
async | Boolean | All | **Important! XHR requests only!** Indicates whether the requests should be made synchronously or asynchronously. Default value is `true` (asynchronously).
231231
collection | String | All | The name of the Entity Collection (or Entity Logical name in `v1.4.0+`).
232+
contentId | String | `createRequest`, `updateRequest`, `upsertRequest`, `deleteRequest` | `v1.5.6+` **BATCH REQUESTS ONLY!** Sets Content-ID header or references request in a Change Set. [More Info](https://www.odata.org/documentation/odata-version-3-0/batch-processing/)
232233
count | Boolean | `retrieveMultipleRequest`, `retrieveAllRequest` | Boolean that sets the $count system query option with a value of true to include a count of entities that match the filter criteria up to 5000 (per page). Do not use $top with $count!
233-
duplicateDetection | Boolean | `createRequest`, `updateRequest`, `upsertRequest` | `v.1.3.4+` **Web API v9+ only!** Boolean that enables duplicate detection. [More info](https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/update-delete-entities-using-web-api#check-for-duplicate-records)
234+
duplicateDetection | Boolean | `createRequest`, `updateRequest`, `upsertRequest` | `v.1.3.4+` **Web API v9+ only!** Boolean that enables duplicate detection. [More Info](https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/update-delete-entities-using-web-api#check-for-duplicate-records)
234235
entity | Object | `createRequest`, `updateRequest`, `upsertRequest` | A JavaScript object with properties corresponding to the logical name of entity attributes (exceptions are lookups and single-valued navigation properties).
235236
expand | Array | `retrieveRequest`, `createRequest`, `updateRequest`, `upsertRequest` | An array of Expand Objects (described below the table) representing the $expand OData System Query Option value to control which related records are also returned.
236237
filter | String | `retrieveRequest`, `retrieveMultipleRequest`, `retrieveAllRequest` | Use the $filter system query option to set criteria for which entities will be returned.
237238
id | String | `retrieveRequest`, `createRequest`, `updateRequest`, `upsertRequest`, `deleteRequest` | `deprecated in v.1.3.4` Use `key` field, instead of `id`. A String representing the Primary Key (GUID) of the record.
238-
ifmatch | String | `retrieveRequest`, `updateRequest`, `upsertRequest`, `deleteRequest` | Sets If-Match header value that enables to use conditional retrieval or optimistic concurrency in applicable requests. [More info](https://msdn.microsoft.com/en-us/library/mt607711.aspx)
239-
ifnonematch | String | `retrieveRequest`, `upsertRequest` | 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).
239+
ifmatch | String | `retrieveRequest`, `updateRequest`, `upsertRequest`, `deleteRequest` | Sets If-Match header value that enables to use conditional retrieval or optimistic concurrency in applicable requests. [More Info](https://msdn.microsoft.com/en-us/library/mt607711.aspx)
240+
ifnonematch | String | `retrieveRequest`, `upsertRequest` | 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).
240241
impersonate | String | All | A String representing the GUID value for the Dynamics 365 system user id. Impersonates the user.
241242
includeAnnotations | String | `retrieveRequest`, `retrieveMultipleRequest`, `retrieveAllRequest`, `createRequest`, `updateRequest`, `upsertRequest` | 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.
242243
key | String | `retrieveRequest`, `createRequest`, `updateRequest`, `upsertRequest`, `deleteRequest` | `v.1.3.4+` A String representing collection record's Primary Key (GUID) or Alternate Key(s).
243244
maxPageSize | Number | `retrieveMultipleRequest`, `retrieveAllRequest` | Sets the odata.maxpagesize preference value to request the number of entities returned in the response.
244-
mergeLabels | Boolean | `updateRequest` | `v.1.4.2+` **Metadata Update only!** Sets `MSCRM.MergeLabels` header that controls whether to overwrite the existing labels or merge your new label with any existing language labels. Default value is `false`. [More info](https://msdn.microsoft.com/en-us/library/mt593078.aspx#bkmk_updateEntities)
245+
mergeLabels | Boolean | `updateRequest` | `v.1.4.2+` **Metadata Update only!** Sets `MSCRM.MergeLabels` header that controls whether to overwrite the existing labels or merge your new label with any existing language labels. Default value is `false`. [More Info](https://msdn.microsoft.com/en-us/library/mt593078.aspx#bkmk_updateEntities)
245246
metadataAttributeType | String | `retrieveRequest`, `updateRequest` | `v.1.4.3+` Casts the Attributes to a specific type. (Used in requests to Attribute Metadata) [More Info](https://msdn.microsoft.com/en-us/library/mt607522.aspx#Anchor_4)
246247
navigationProperty | String | `retrieveRequest`, `createRequest`, `updateRequest` | A String representing the name of a single-valued navigation property. Useful when needed to retrieve information about a related record in a single request.
247248
navigationPropertyKey | String | `retrieveRequest`, `createRequest`, `updateRequest` | `v.1.4.3+` A String representing navigation property's Primary Key (GUID) or Alternate Key(s). (For example, to retrieve Attribute Metadata)
@@ -1024,11 +1025,41 @@ dynamicsWebApi.executeBatch().then(function (responses) {
10241025
just pass `null` if you need to add additional parameters in the request,
10251026
for example: `dynamicsWebApi.deleteRecord('00000000-0000-0000-0000-000000000001', 'contacts', null, null, 'firstname')`.
10261027

1028+
## Use Content-ID to reference requests in a Change Set
1029+
1030+
Starting from v.1.5.6 you can reference a request from a Change Set. For example, if you want to create related entities in a single batch request:
1031+
1032+
```js
1033+
var order = {
1034+
name: '1 year membership'
1035+
};
1036+
1037+
var contact = {
1038+
firstname: "test content",
1039+
lastname: "id"
1040+
};
1041+
1042+
dynamicsWebApi.startBatch();
1043+
dynamicsWebApi.createRequest({ entity: order, collection: 'salesorders', contentId: '1' });
1044+
dynamicsWebApi.createRequest({ entity: contact, collection: 'customerid_contact', contentId: "$1" });
1045+
1046+
dynamicsWebApi.executeBatch()
1047+
.then(function (responses) {
1048+
var salesorderId = responses[0];
1049+
//responses[1]; is undefined <- CRM Web API limitation
1050+
}).catch(function (error) {
1051+
//catch error here
1052+
});
1053+
1054+
```
1055+
1056+
Note that the second response does not have a returned value, it is a CRM Web API limitation.
1057+
**Important!** DynamicsWebApi automatically assigns value to a `Content-ID` if it is not provided, therefore, please set your `Content-ID` value less than 100000.
1058+
10271059
#### Limitations
10281060

10291061
Currently, there are some limitations in DynamicsWebApi Batch Operations:
10301062

1031-
* `Content-ID` header cannot be used to reference the Uri of any entity created in a single operation. **This is an upcoming feature**.
10321063
* Operations that use pagination to recursively retrieve all records cannot be used in a 'batch mode'. These include: `retrieveAll`, `retrieveAllRequest`, `countAll`, `fetchAll`, `executeFetchXmlAll`.
10331064
You will get an error saying that the operation is incompatible with a 'batch mode'.
10341065
* The following limitation is for external applications (working outside D365 CE forms). `useEntityNames` may not work in a 'batch mode' if it is set to `true`.
@@ -1913,8 +1944,8 @@ the config option "formatted" will enable developers to retrieve all information
19131944
- [X] Entity Relationships and Global Option Sets helpers. `Implemented in v.1.4.6`
19141945
- [X] Batch requests. `Implemented in v.1.5.0`
19151946
- [X] TypeScript declaration files `d.ts` `Added in v.1.5.3`.
1947+
- [X] Implement `Content-ID` header to reference a request in a Change Set in a batch operation `Added in v.1.5.6`.
19161948
- [ ] Upload DynamicsWebApi declaration files to DefinitelyTyped repository.
1917-
- [ ] Implement `Content-ID` header to reference a created entity in a batch operation.
19181949

19191950
Many more features to come!
19201951

lib/dynamics-web-api-callbacks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ var dwaRequest = function () {
8585
* @property {string} userQuery - A String representing the GUID value of the user query.
8686
* @property {boolean} mergeLabels - If set to 'true', DynamicsWebApi adds a request header 'MSCRM.MergeLabels: true'. Default value is 'false'
8787
* @property {boolean} isBatch - If set to 'true', DynamicsWebApi treats a request as a part of a batch request. Call ExecuteBatch to execute all requests in a batch. Default value is 'false'.
88+
* @property {string} contentId - BATCH REQUESTS ONLY! Sets Content-ID header or references request in a Change Set.
8889
*/
8990

9091
/**

lib/dynamics-web-api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ var dwaRequest = function () {
8888
* @property {string} userQuery - A String representing the GUID value of the user query.
8989
* @property {boolean} mergeLabels - If set to 'true', DynamicsWebApi adds a request header 'MSCRM.MergeLabels: true'. Default value is 'false'.
9090
* @property {boolean} isBatch - If set to 'true', DynamicsWebApi treats a request as a part of a batch request. Call ExecuteBatch to execute all requests in a batch. Default value is 'false'.
91+
* @property {string} contentId - BATCH REQUESTS ONLY! Sets Content-ID header or references request in a Change Set.
9192
*/
9293

9394
/**

lib/requests/http.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ var httpRequest = function (options) {
105105
}
106106
var error = new Error();
107107
Object.keys(crmError).forEach(k => {
108-
error[k] = crmError[k];
109-
})
108+
error[k] = crmError[k];
109+
});
110110
error.status = res.statusCode;
111111
error.statusText = request.statusText;
112112
errorCallback(error);
@@ -123,14 +123,7 @@ var httpRequest = function (options) {
123123
});
124124
}
125125

126-
//request.on('timeout', function () {
127-
// request.abort();
128-
//});
129-
130126
request.on('error', function (error) {
131-
//if (request.aborted) {
132-
// error = new Error('Request timed out: ' + error);
133-
//}
134127
responseParams.length = 0;
135128
errorCallback(error);
136129
});

lib/utilities/BatchConverter.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var convertToBatch = function (requests) {
1010

1111
var batchBody = [];
1212
var currentChangeSet = null;
13-
var contentId = 0;
13+
var contentId = 100000;
1414

1515
for (var i = 0; i < requests.length; i++) {
1616
var request = requests[i];
@@ -21,7 +21,7 @@ var convertToBatch = function (requests) {
2121
batchBody.push('\n--' + currentChangeSet + '--');
2222

2323
currentChangeSet = null;
24-
contentId = 0;
24+
contentId = 100000;
2525
}
2626

2727
if (!currentChangeSet) {
@@ -41,10 +41,19 @@ var convertToBatch = function (requests) {
4141
batchBody.push('Content-Transfer-Encoding: binary');
4242

4343
if (!isGet) {
44-
batchBody.push('Content-ID: ' + ++contentId);
44+
var contentIdValue = request.headers.hasOwnProperty('Content-ID')
45+
? request.headers['Content-ID']
46+
: ++contentId;
47+
48+
batchBody.push('Content-ID: ' + contentIdValue);
4549
}
4650

47-
batchBody.push('\n' + request.method + ' ' + request.config.webApiUrl + request.path + ' HTTP/1.1');
51+
if (!request.path.startsWith("$")) {
52+
batchBody.push('\n' + request.method + ' ' + request.config.webApiUrl + request.path + ' HTTP/1.1');
53+
}
54+
else {
55+
batchBody.push('\n' + request.method + ' ' + request.path + ' HTTP/1.1');
56+
}
4857

4958
if (isGet) {
5059
batchBody.push('Accept: application/json');
@@ -54,7 +63,7 @@ var convertToBatch = function (requests) {
5463
}
5564

5665
for (var key in request.headers) {
57-
if (key === 'Authorization')
66+
if (key === 'Authorization' || key === 'Content-ID')
5867
continue;
5968

6069
batchBody.push(key + ': ' + request.headers[key]);

lib/utilities/RequestConverter.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,13 @@ function convertRequestOptions(request, functionName, url, joinSymbol, config) {
166166
headers['MSCRM.MergeLabels'] = 'true';
167167
}
168168

169+
if (request.contentId) {
170+
ErrorHelper.stringParameterCheck(request.contentId, 'DynamicsWebApi.' + functionName, 'request.contentId');
171+
if (!request.contentId.startsWith('$')) {
172+
headers['Content-ID'] = request.contentId;
173+
}
174+
}
175+
169176
if (request.isBatch) {
170177
ErrorHelper.boolParameterCheck(request.isBatch, 'DynamicsWebApi.' + functionName, 'request.isBatch');
171178
}
@@ -216,6 +223,13 @@ function convertRequest(request, functionName, config) {
216223
ErrorHelper.stringParameterCheck(request.collection, 'DynamicsWebApi.' + functionName, "request.collection");
217224
url = request.collection;
218225

226+
if (request.contentId) {
227+
ErrorHelper.stringParameterCheck(request.contentId, 'DynamicsWebApi.' + functionName, 'request.contentId');
228+
if (request.contentId.startsWith('$')) {
229+
url = request.contentId + '/' + url;
230+
}
231+
}
232+
219233
//add alternate key feature
220234
if (request.key) {
221235
request.key = ErrorHelper.keyParameterCheck(request.key, 'DynamicsWebApi.' + functionName, "request.key");

tests/main-tests.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4731,6 +4731,60 @@ describe("promises -", function () {
47314731
expect(scope.isDone()).to.be.true;
47324732
});
47334733
});
4734+
4735+
describe("create / create with Content-ID", function () {
4736+
var scope;
4737+
var rBody = mocks.data.batchCreateContentID;
4738+
var rBodys = rBody.split('\n');
4739+
var checkBody = '';
4740+
for (var i = 0; i < rBodys.length; i++) {
4741+
checkBody += rBodys[i];
4742+
}
4743+
before(function () {
4744+
var response = mocks.responses.batchUpdateDelete;
4745+
scope = nock(mocks.webApiUrl + '$batch')
4746+
.filteringRequestBody(function (body) {
4747+
body = body.replace(/dwa_batch_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, 'dwa_batch_XXX');
4748+
body = body.replace(/changeset_[\d\w]{8}-[\d\w]{4}-[\d\w]{4}-[\d\w]{4}-[\d\w]{12}/g, 'changeset_XXX');
4749+
var bodys = body.split('\n');
4750+
4751+
var resultBody = '';
4752+
for (var i = 0; i < bodys.length; i++) {
4753+
resultBody += bodys[i];
4754+
}
4755+
return resultBody;
4756+
})
4757+
.post("", checkBody)
4758+
.reply(response.status, response.responseText, response.responseHeaders);
4759+
});
4760+
4761+
after(function () {
4762+
nock.cleanAll();
4763+
});
4764+
4765+
it("returns a correct response", function (done) {
4766+
dynamicsWebApiTest.startBatch();
4767+
4768+
dynamicsWebApiTest.createRequest({ collection: 'records', entity: { firstname: "Test", lastname: "Batch!" }, contentId: '1' });
4769+
dynamicsWebApiTest.createRequest({ collection: 'test_property', entity: { firstname: "Test1", lastname: "Batch!" }, contentId: '$1' });
4770+
4771+
dynamicsWebApiTest.executeBatch()
4772+
.then(function (object) {
4773+
expect(object.length).to.be.eq(2);
4774+
4775+
expect(object[0]).to.be.eq(mocks.data.testEntityId);
4776+
expect(object[1]).to.be.undefined;
4777+
4778+
done();
4779+
}).catch(function (object) {
4780+
done(object);
4781+
});
4782+
});
4783+
4784+
it("all requests have been made", function () {
4785+
expect(scope.isDone()).to.be.true;
4786+
});
4787+
});
47344788
});
47354789

47364790
describe("dynamicsWebApi.constructor -", function () {

0 commit comments

Comments
 (0)