DynamicsWebApi for Microsoft Dataverse Web API / Microsoft Power Pages / Microsoft Dynamics 365 (CRM)
DynamicsWebApi is a Microsoft Dataverse Web API helper library written in Typescript.
Compatible with: Microsoft Dataverse; Microsoft Dynamics 365: Customer Service, Field Service, Marketing, Project Operations, Talents, Sales and any model-driven application built on Microsoft Power Apps platform. As well as Microsoft Dynamics 365 CE (online), Microsoft Dynamics 365 CE (on-premises), Microsoft Dynamics CRM 2016, Microsoft Dynamics CRM Online.
This documentation is for version 2.x. If you are working with version 1.x, please check this instead.
If you want to upgrade from v1 - v2 breaking changes are here. List of new features in v2 is here.
Please check DynamicsWebApi Wiki where you will find documentation to DynamicsWebApi API and more.
Changelog can be found here.
Browser-compiled script and type definitions can be found in a v2 dist folder.
- Microsoft Dataverse Search API. Access the full power of its Search, Suggestion and Autocomplete capabilities.
- Batch Requests. Convert all requests into a Batch operation with two lines of code.
- Simplicity and Automation. Such as automated paging, big file downloading/uploading in chunks of data, automated conversion of requests with long URLs into a Batch Request in the background and more!
- CRUD operations. Including Fetch XML, Actions and Functions in Microsoft Dataverse Web API.
- Table Definitions (Entity Metadata). Query and modify Table, Column, Choice (Option Set) and Relationship definitions.
- File Fields. Upload, Download and Delete data stored in the File Fields.
- Abort Signal and Abort Controller (Browser and Node.js 15+). Abort requests when they are no longer need to be completed.
- Node.js and a Browser support.
- Proxy Configuration support.
- Works with Microsoft Power Pages (aka Microsoft Portal).
v2.1.0+
I maintain this project in my free time and it takes a considerable amount of time to make sure that the library has all new features, gets improved and all raised tickets have been answered and fixed in a short amount of time. If you feel that this project has saved your time and you would like to support it, then please feel free to sponsor it through GitHub Sponsors.
Also, please check suggestions and contributions section to learn more about how you can help to improve the library.
Check out Dataverse Terminology. Microsoft has done some changes in the namings of the objects and components of D365 and since DynamicsWebApi has been developing for many years there may be conflicting naming, such as: createEntity
- which right now means "Create a Table Definition". Dataverse SDK terminology is what the library has been based on. I have no plans on changing that (except in documentation), mainly because Microsoft may change the namings again in the future which will lead to naming issues ...again.
Please note! "Dynamics 365" in this readme refers to Microsoft Dataverse (formerly known as Microsoft Common Data Service) / Microsoft Dynamics 365 Customer Engagement / Micorosft Dynamics CRM. NOT Microsoft Dynamics 365 Finance and Operations.
v2 breaking changes are here. List of new features in v2 is here.
- Getting Started
- Request Examples
- Create a Table Row
- Update a Table Row
- Update a value in a single column
- Upsert a Table Row
- Delete a Table Row
- Retrieve a Table Row
- Retrieve Multiple Table Rows
- Count
- Associate
- Associate for a single-valued navigation property
- Disassociate
- Disassociate for a single-valued navigation property
- Fetch XML Request
- Execute Web API functions
- Execute Web API actions
- Batch Operations
- Work with Table Definitions (Entity Metadata)
- Tables
- Create a new Table Definition
- Retrieve Table Definitions
- Update Table Definitions
- Retrieve Multiple Table Definitions
- Columns
- Create Columns
- Retrieve Columns
- Update Columns
- Retrieve Multiple Columns
- Use requests to query Table and Column definitions
- Relationships
- Create Relationship
- Update Relationship
- Delete Relationship
- Retrieve Relationship
- Retrieve Multiple Relationships
- Use multi-table lookup columns (Polymorfic Lookup Attributes)
- Global Option Sets (Choices)
- Create Global Option Set
- Update Global Option Set
- Delete Global Option Set
- Retrieve Global Option Set
- Retrieve Multiple Global Option Sets
- Retrieve CSDL $metadata document
- Formatted Values and Lookup Columns
- Using Alternate Keys
- Making requests using Entity Logical Names
- Work with File Fields/Columns
- Work with Dataverse Search API
- Abort Request
- Using Proxy
- Using TypeScript Declaration Files
- In Progress / Feature List
- Contributions
To use DynamicsWebApi inside Dynamics 365 you need to download a browser version of the library, it can be found in v2 dist folder.
Upload a script as a JavaScript Web Resource, add it to a table form or reference it in the HTML Web Resource and then initialize the main object:
//By default DynamicsWebApi makes calls to
//Web API v9.2 and Search API v1.0
const dynamicsWebApi = new DynamicsWebApi();
const response = await dynamicsWebApi.callFunction("WhoAmI");
Xrm.Navigation.openAlertDialog({ text: `Hello Dynamics 365! My id is: ${response.UserId}` });
v.2.1.0+
There are two ways to include DynamicsWebApi in your portal: upload as a Web File or use CDN, such as unpkg
.
It is possible to upload the library as a Web File in Microsoft Power Pages. Usually, the .js
extensions are forbidden to upload but it is still possible to do, here's a workaround.
Once the web file is uploaded, it can be included in a template, a page or a form the following way:
<script type="text/javascript" src="~/dynamicsWebApi.min.js"></script>
With CDN, it is a bit easier: no need to create and upload a web file - just include the script in your template, page or a form:
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/dynamics-web-api.min.js"></script>
And you are good to go! DynamicsWebApi will automatically detect if the library is running on Power Pages and will supply an anti-forgery token with each request.
Currently, there are some ootb limitations in the Power Pages:
- Batch operations are not supported. Hopefully, Microsoft will add it in the future.
- Long requests (with URL more than 2000 chars) are not supported. This is because of the 1st limitation. DynamicsWebApi will try to convert it to a Batch operation which will ultimately fail because it's not supported.
To use DynamicsWebApi in Node.js install the dynamics-web-api
package from NPM:
npm install dynamics-web-api --save
Then include it in your script:
//CommonJS
const DynamicsWebApi = require("dynamics-web-api").DynamicsWebApi;
//ESM
import { DynamicsWebApi } from "dynamics-web-api";
DynamicsWebApi does not fetch authorization tokens, thus you will need to acquire them in your code and pass them back to the library. Authorization tokens can be acquired using Microsoft Authentication Library for Node or you can write your own logic to retrieve the tokens.
Here is an example using @azure/msal-node
:
//app configuraiton must be stored in a safe place
import { Config } from './config.ts';
import { DynamicsWebApi } from 'dynamics-web-api';
import * as MSAL from '@azure/msal-node';
//OAuth Token Endpoint (from your Azure App Registration)
const authorityUrl = 'https://login.microsoftonline.com/<COPY A GUID HERE>';
const msalConfig = {
auth: {
authority: authorityUrl,
clientId: Config.clientId,
clientSecret: Config.secret,
knownAuthorities: ['login.microsoftonline.com']
}
}
const cca = new MSAL.ConfidentialClientApplication(msalConfig);
const serverUrl = 'https://<YOUR ORG HERE>.api.crm.dynamics.com';
//function that acquires a token and passes it to DynamicsWebApi
const acquireToken = async () => {
try {
return cca.acquireTokenByClientCredential({
scopes: [`${serverUrl}/.default`],
});
}
catch (error) {
//error logging here
//or a fallback authentication
//to abort a request just return null
//or re-throw an error
return null;
}
}
//create DynamicsWebApi;
//By default DynamicsWebApi makes calls to
//Web API v9.2 and Search API v1.0
const dynamicsWebApi = new DynamicsWebApi({
serverUrl: serverUrl,
onTokenRefresh: acquireToken
});
try {
//call any function
const response = await dynamicsWebApi.callFunction("WhoAmI");
console.log(`Hello from Dynamics 365! My id is: ${response.UserId}`);
}
catch (error){
console.log(error);
}
To initialize a new instance of DynamicsWebApi with a configuration object, please use the following code:
const dynamicsWebApi = new DynamicsWebApi({ dataApi: { version: "9.1" } });
The library in Node.js requires a url to the Web API server and a refresh token callback function:
const dynamicsWebApi = new DynamicsWebApi({
serverUrl: "https://myorg.api.crm.dynamics.com",
dataApi: {
version: "9.1"
},
onTokenRefresh: acquireToken
});
You can set the configuration dynamically if needed:
dynamicsWebApi.setConfig({ dataApi: { version: "9.0" } });
Property Name | Type | Description |
---|---|---|
dataApi | ApiConfig |
Configuration object for Dataverse Web API. The name is based on the url path data . |
headers | Object |
v2.1+ Custom default headers to supply with each request. For example: { "my-header": "value", "another-header": "another-value" } . |
impersonate | string |
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 |
impersonateAAD | string |
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 |
includeAnnotations | string |
Defaults Prefer header with value "odata.include-annotations=" and the specified annotation. Annotations provide additional information about lookups, options sets and other complex attribute types. |
maxPageSize | number |
Defaults the odata.maxpagesize preference. Use to set the number of entities returned in the response. |
onTokenRefresh | Function |
A callback function that triggered when DynamicsWebApi requests a new OAuth token. (At this moment it is done before each call to Dynamics 365, as recommended by Microsoft). |
organizationUrl | string |
Dynamics 365 Web Api organization URL. It is required when used in Node.js application (outside web resource). Example: "https://myorg.api.crm.dynamics.com/". |
proxy | Object |
Proxy configuration object. More Info |
returnRepresentation | boolean |
Defaults Prefer header with value "return=representation". Use this property to return just created or updated entity in a single request. |
searchApi | ApiConfig |
Configuration object for Dataverse Search API. The name is based on the url path search . |
serverUrl | string |
The url to Dataverse API server, for example: https://contoso.api.crm.dynamics.com/. It is required when used in Node.js application. |
timeout | number |
Sets a number of milliseconds before a request times out. |
useEntityNames | boolean |
Indicates whether to use entity logical names instead of collection logical names during requests. |
Note!
serverUrl
and onTokenRefresh
are required when DynamicsWebApi is used in a Node.js application.
Important!
If you are using DynamicsWebApi
outside Microsoft Dynamics 365 and set useEntityNames
to true
the first request to Web Api will fetch LogicalCollectionName
and LogicalName
from EntityMetadata
for all entities. It does not happen when DynamicsWebApi
is used in Microsoft Dynamics 365 Web Resources (there is no additional request, no impact on perfomance).
ApiConfig Properties:
Property Name | Type | Description |
---|---|---|
path | string |
A path to API, for example: "data" or "search". Optional. |
version | string |
API Version, for example: "1.0" or "9.2". Optional. |
Both dataApi
and searchApi
can be omitted from a configuration. Their default values are:
//dataApi
{
path: "data",
version: "9.2"
}
//searchApi
{
path: "search",
version: "1.0"
}
dataApi properties:
Property Name | Type | Description |
---|---|---|
path | string |
Optional. A path to API, default: "data". |
version | string |
Optional. API Version, default: "9.2". |
searchApi properties:
Property Name | Type | Description |
---|---|---|
path | string |
Optional. A path to API, default: "search". |
version | string |
Optional. API Version, default: "1.0". |
Please use DynamicsWebApi Wiki for an object reference. It is automatically generated and I could not find a better doc generator, pardon me for that. If you know a good ".d.ts -> .md" doc generator - let me know!
The following table describes all possible properties that can be set in a request
object. Some parameters may still be absent in a table, please refer to DynamicsWebApi Wiki.
Please note! Not all operations accept all properties and if by mistake an invalid property has been specified you will receive either an error saying that the request is invalid or the response will not have expected results.
Property Name | Type | Operation(s) Supported | Description |
---|---|---|---|
action | Object |
callAction |
A JavaScript object that represents a Dynamics 365 Web API action. |
actionName | string |
callAction |
Web API Action name. |
addAnnotations | boolean |
retrieveCsdlMetadata |
If set to true the document will include many different kinds of annotations that can be useful. Most annotations are not included by default because they increase the total size of the document. |
apply | string |
retrieveMultiple , retrieveAll |
Sets the $apply system query option to aggregate and group your data dynamically. More Info |
async | boolean |
All | XHR requests only! Indicates whether the requests should be made synchronously or asynchronously. Default value is true (asynchronously). |
bypassCustomPluginExecution | boolean |
create , update , upsert , delete |
If set to true, the request bypasses custom business logic, all synchronous plug-ins and real-time workflows are disabled. Check for special exceptions in Microsft Docs. More Info |
collection | string |
All | Entity Collection name. |
contentId | string |
create , update , upsert , deleteRecord |
BATCH REQUESTS ONLY! Sets Content-ID header or references request in a Change Set. More Info |
continueOnError | boolean |
executeBatch |
BATCH REQUESTS ONLY! Sets Prefer header to odata.continue-on-error that allows more requests be processed when errors occur. The batch request will return 200 OK and individual response errors will be returned in the batch response body. More Info |
count | boolean |
retrieveMultiple , retrieveAll |
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! |
data | Object or ArrayBuffer / Buffer (for node.js) |
create , update , upsert , uploadFile |
A JavaScript object that represents Dynamics 365 entity, action, metadata and etc. |
duplicateDetection | boolean |
create , update , upsert |
D365 Web API v9+ Boolean that enables duplicate detection. More Info |
expand | Expand[] |
retrieve , retrieveMultiple , create , update , upsert |
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. |
fetchXml | string |
fetch , fetchAll |
Property that sets FetchXML - a proprietary query language that provides capabilities to perform aggregation. |
fieldName | string |
uploadFile , downloadFile , deleteRecord |
D365 Web API v9.1+. Deprecated, use property instead Use this option to specify the name of the file attribute in Dynamics 365. More Info |
fileName | string |
uploadFile |
D365 Web API v9.1+ Specifies the name of the file |
filter | String | retrieve , retrieveMultiple , retrieveAll , callFunction |
Use the $filter system query option to set criteria for which entities will be returned. |
functionName | string |
callFunction |
Deprecated from v2.1.3 Use name instead. Name of a D365 Web Api function. |
headers | Object |
All | v2.1+ Custom headers to supply with a request. These headers will override configuraiton headers if the identical ones were set. For example: { "my-header": "value", "another-header": "another-value" } . |
ifmatch | string |
retrieve , update , upsert , deleteRecord |
Sets If-Match header value that enables to use conditional retrieval or optimistic concurrency in applicable requests. More Info |
ifnonematch | string |
retrieve , upsert |
Sets If-None-Match header value that enables to use conditional retrieval in applicable requests. More Info. |
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 |
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 |
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. |
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. |
key | string |
retrieve , create , update , upsert , deleteRecord , uploadFile , downloadFile , callAction , callFunction |
A string representing collection record's Primary Key (GUID) or Alternate Key(s). |
maxPageSize | number |
retrieveMultiple , retrieveAll |
Sets the odata.maxpagesize preference value to request the number of entities returned in the response. |
mergeLabels | boolean |
update |
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 |
metadataAttributeType | string |
retrieve , update |
Casts the Attributes to a specific type. (Used in requests to Attribute Metadata) More Info |
name | string |
callFunction |
v2.1.3+ The name of a D365 Web API function (replaces functionName ). |
navigationProperty | string |
retrieve , create , update |
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. |
navigationPropertyKey | string |
retrieve , create , update |
A string representing navigation property's Primary Key (GUID) or Alternate Key(s). (For example, to retrieve Attribute Metadata) |
noCache | boolean |
All | If set to true , DynamicsWebApi adds a request header Cache-Control: no-cache . Default value is false . |
orderBy | string[] |
retrieveMultiple , retrieveAll |
An array (of strings) representing the order in which items are returned using the $orderby system query option. Use the asc or desc suffix to specify ascending or descending order respectively. The default is ascending if the suffix isn't applied. |
pageNumber | number |
fetch |
Sets a page number for Fetch XML request ONLY! |
pagingCookie | string |
fetch |
Sets a paging cookie for Fetch XML request ONLY! |
parameters | Object |
callFunction |
Function's input parameters. Example: { param1: "test", param2: 3 } . |
partitionId | string |
create , update , upsert , delete , retrieve , retrieveMultiple |
Sets a unique partition key value of a logical partition for non-relational custom entity data stored in NoSql tables of Azure heterogenous storage. More Info |
property | string |
uploadFile , downloadFile , deleteRecord |
v2.1.6+ D365 Web API v9.1+. Use this option to specify the name of the column or of the file attribute in Dynamics 365. More Info |
queryParams | string[] |
All | Custom query parameters. Can also be used to set the parameter aliases for "$filter" and "$orderBy". Important! These parameters ARE NOT URI encoded! |
returnRepresentation | boolean |
create , update , upsert |
Sets Prefer header request with value "return=representation". Use this property to return just created or updated entity in a single request. |
savedQuery | string |
retrieve |
A String representing the GUID value of the saved query. |
select | string[] |
retrieve , retrieveMultiple , retrieveAll , update , upsert , callFunction |
An array (of Strings) representing the $select OData System Query Option to control which attributes will be returned. |
signal | AbortSignal |
All | Specifies an AbortSignal that can be used to abort a request if required via an AbortController object. More Info |
timeout | number |
All | Sets a number of milliseconds before a request times out. |
token | string |
All | Authorization Token. If set, onTokenRefresh will not be called. |
top | number |
retrieveMultiple , retrieveAll |
Limit the number of results returned by using the $top system query option. Do not use $top with $count! |
trackChanges | boolean |
retrieveMultiple , retrieveAll |
Sets Prefer header with value 'odata.track-changes' to request that a delta link be returned which can subsequently be used to retrieve entity changes. Important! Change Tracking must be enabled for the entity. More Info |
userQuery | string |
retrieve |
A String representing the GUID value of the user query. |
The following table describes Expand
Object properties:
Property Name | Type | Description |
---|---|---|
expand | Expand[] |
An array of Expand Objects representing the $expand OData System Query Option value to control which related records are also returned. |
filter | string |
Use the $filter system query option to set criteria for which related entities will be returned. |
orderBy | string[] |
An Array (of strings) representing the order in which related items are returned using the $orderby system query option. Use the asc or desc suffix to specify ascending or descending order respectively. The default is ascending if the suffix isn't applied. |
property | string |
A name of a single-valued navigation property which needs to be expanded. |
select | string[] |
An Array (of strings) representing the $select OData System Query Option to control which attributes will be returned. |
top | number |
Limit the number of results returned by using the $top system query option. |
All requests to Web API that have long URLs (more than 2000 characters) are automatically converted to a Batch Request. This feature is very convenient when you make a call with big Fetch XMLs. No special parameters needed to do a convertation.
Heads up! This feature may cause an issue in Microsoft Power Pages because Batch Requests are not supported there out of the box. Please keep your requests short :)
//declaring interface for a Lead entity (declaration can be done in d.ts file)
interface Lead {
leadid?: string,
subject?: string,
firstname?: string,
lastname?: string,
jobtitle?: string
}
//init an object representing Dynamics 365 entity
const lead: Lead = {
subject: "Test WebAPI",
firstname: "Test",
lastname: "WebAPI",
jobtitle: "Title"
};
//init DynamicsWebApi request
const request: DynamicsWebApi.CreateRequest = {
collection: "leads",
data: lead,
returnRepresentation: true
}
//call dynamicsWebApi.create function
//let's use Lead here because we set returnRepresentation to `true`
//if the type was ommitted here then the result would be of type `any`
const result = await dynamicsWebApi.create<Lead>(request);
//do something with a record here
const leadId = result.leadid;
//declaring interface for a Lead entity (declaration can be done in d.ts file)
interface Lead {
leadid?: string,
subject?: string,
fullname?: string,
jobtitle?: string
}
//init DynamicsWebApi request
const request: DynamicsWebApi.UpdateRequest = {
key: "7d577253-3ef0-4a0a-bb7f-8335c2596e70",
collection: "leads",
data: {
subject: "Test update",
jobtitle: "Developer"
},
returnRepresentation: true,
select: ["fullname"]
};
//call dynamicsWebApi.update function
//let's use Lead here because we set returnRepresentation to `true`
//if the type was ommitted here then the result would be of type `any`
const result = await dynamicsWebApi.update<Lead>(request);
//do something with a fullname of a recently updated entity record
const fullname = result.fullname;
//initialize key value pair object
const request: DynamicsWebApi.UpdateSinglePropertyRequest = {
collection: "leads",
key: "7d577253-3ef0-4a0a-bb7f-8335c2596e70",
fieldValuePair: { subject: "Update Single" }
};
//perform an update single property operation
await dynamicsWebApi.updateSingleProperty(request);
//do something after a succesful operation
const leadId = "7d577253-3ef0-4a0a-bb7f-8335c2596e70";
const request: DynamicsWebApi.UpsertRequest = {
key: leadId,
collection: "leads",
returnRepresentation: true,
select: ["fullname"],
data: {
subject: "Test upsert"
},
ifnonematch: "*" //to prevent update
};
const result = dynamicsWebApi.upsert<Lead>(request);
if (result != null) {
//record created
}
else {
//update prevented
}
//delete with optimistic concurrency
const request: DynamicsWebApi.DeleteRequest = {
key: leadId,
collection: "leads",
ifmatch: 'W/"470867"'
}
const isDeleted = await dynamicsWebApi.deleteRecord(request);
if (isDeleted){
//record has been deleted
}
else{
//record has not been deleted
}
//delete with optimistic concurrency
const request: DynamicsWebApi.DeleteRequest = {
key: leadId,
collection: "leads",
property: "subject"
}
await dynamicsWebApi.deleteRecord(request);
//declaring interface for a Lead entity (declaration can be done in d.ts file)
interface Lead {
leadid?: string,
subject?: string,
fullname?: string,
jobtitle?: string
}
const request: DynamicsWebApi.RetrieveRequest = {
key: "7d577253-3ef0-4a0a-bb7f-8335c2596e70",
collection: "leads",
select: ["fullname", "subject"],
//ETag value with the If-None-Match header to request data to be retrieved only
//if it has changed since the last time it was retrieved.
ifnonematch: 'W/"468026"',
//Retrieved record will contain formatted values
includeAnnotations: "OData.Community.Display.V1.FormattedValue"
};
//call dynamicsWebApi.retrieve function
//if the Lead type was ommitted here then the result would be of type `any`
const result = await dynamicsWebApi.retrieve<Lead>(request);
//do something with a retrieved record
It is possible to retrieve a reference to the related entity. In order to do that: select
property
must contain only a single value representing a name of a single-valued navigation property
and it must have a suffix /$ref
attached to it. Example:
const leadId = "7d577253-3ef0-4a0a-bb7f-8335c2596e70";
const request: DynamicsWebApi.RetrieveRequest = {
collection: "leads",
key: leadId,
select: ["onwerid/$ref"]
}
//perform a retrieve operaion
const reference = await dynamicsWebApi.retrieve(request);
const ownerId = reference.id;
const collectionName = reference.collection; // systemusers or teams
In order to retrieve a related record by a single-valued navigation property you need to add a prefix "/" to the first element in a select
array:
select: ["/ownerid", "fullname"]
. The first element must be the name of a single-valued navigation property
and it must contain a prefix "/"; all other elements in a select
array will represent attributes of the related entity. Examples:
const recordId = "7d577253-3ef0-4a0a-bb7f-8335c2596e70";
const request: DynamicsWebApi.RetrieveRequest = {
key: recordId,
collection: "new_tests",
select: ["/new_ParentLead", "fullname", "subject"]
}
//perform a retrieve operaion
const parentLead = await dynamicsWebApi.retrieve<Lead>(request);
const fullname = parentLead.fullname;
//... and etc
Same result can be achieved by setting request.navigationProperty
:
const request: DynamicsWebApi.RetrieveRequest = {
key: recordId,
collection: "new_tests",
navigationProperty: "new_ParentLead", //use request.navigationProperty
select: ["fullname", "subject"]
}
//perform a retrieve operaion
const parentLead = await dynamicsWebApi.retrieve<Lead>(request);
const fullname = parentLead.fullname;
//... and etc
//declaring interface for a Lead entity (declaration can be done in d.ts file)
interface Lead {
leadid?: string,
subject?: string,
fullname?: string
}
const request: DynamicsWebApi.RetrieveMultipleRequest = {
collection: "leads",
select: ["fullname", "subject"],
filter: "statecode eq 0",
maxPageSize: 5,
count: true
}
const response = await dynamicsWebApi.retrieveMultiple<Lead>(request);
const count = response.oDataCount;
const nextLink = response.oDataNextLink;
const records = response.value;
//set the request parameters
const request: DynamicsWebApi.RetrieveMultipleRequest = {
collection: "leads",
select: ["fullname", "subject"],
trackChanges: true
};
//perform a multiple records retrieve operation (1)
const response1 = await dynamicsWebApi.retrieveMultiple<Lead>(request).then(function (response) {
const deltaLink = response1.oDataDeltaLink;
//make other requests to Web API
//...
//(2) only retrieve changes:
const response2 = await dynamicsWebApi.retrieveMultiple<Lead>(request, deltaLink);
//response2 contains only changed records between the first retrieveMultiple (1) and the second one (2)
The following function retrieves records and goes through all pages automatically.
//perform a multiple records retrieve operation
const response = await dynamicsWebApi.retrieveAll<Lead>({
collection: "leads",
select: ["fullname", "subject"],
filter: "statecode eq 0"
});
const records = response.value;
//do something else with a records array. Access a record: records[0].subject;
It is possible to count records separately from RetrieveMultiple call. In order to do that use the following snippet:
IMPORTANT! The count value does not represent the total number of entities in the system. It is limited by the maximum number of entities that can be returned.
const count = await dynamicsWebApi.count({
collection: "leads",
filter: "statecode eq 0"
});
//do something with count here
The following function can be used to count all records in a collection. It's a workaround and just counts the number of objects in the array
returned in retrieveAll
.
Downside of this workaround is that it does not only return a count number but also all data for records in a collection. In order to make a small optimisation always provide a column for select paramete, it will reduce a size of the response significantly.
const count = await dynamicsWebApi.countAll({
collection: "leads",
filter: "statecode eq 0",
//if you use this workaround, always provide a column
//to limit a response size
select: ["leadid"]
});
FYI, in the majority of cases it is better to use Fetch XML aggregation, but take into a consideration that it is also limited to 50000 records by default.
const accountId = "00000000-0000-0000-0000-000000000001";
const leadId = "00000000-0000-0000-0000-000000000002";
//associate lead reacord to account
const request: DynamicsWebApi.AssociateRequest = {
collection: "accounts",
primaryKey: accountId,
relationshipName: "lead_parent_account",
relatedCollection: "leads",
relatedKey: leadId
}
await dynamicsWebApi.associate(request);
//does not have any return value
The name of a single-valued navigation property can be retrieved by using a GET
request with a header Prefer:odata.include-annotations=Microsoft.Dynamics.CRM.associatednavigationproperty
,
then individual records in the response will contain the property @Microsoft.Dynamics.CRM.associatednavigationproperty
which is the name of the needed navigation property.
For example, there is an entity with a logical name new_test
, it has a lookup attribute to lead
entity called new_parentlead
and schema name new_ParentLead
which is needed single-valued navigation property.
const new_testid = "00000000-0000-0000-0000-000000000001";
const leadId = "00000000-0000-0000-0000-000000000002";
const request: DynamicsWebApi.AssociateSingleValuedRequest = {
collection: "new_tests",
primaryKey: new_testid,
navigationProperty: "new_ParentLead",
relatedCollection: "leads",
relatedKey: leadId
}
await dynamicsWebApi.associateSingleValued(request);
//does not have any return value
const accountId = "00000000-0000-0000-0000-000000000001";
const leadId = "00000000-0000-0000-0000-000000000002";
const request: DynamicsWebApi.DisassociateRequest = {
collection: "accounts",
primaryKey: accountId,
relationshipName: "lead_parent_account",
relatedKey: leadId
}
await dynamicsWebApi.disassociate(request);
//does not have any return value
Current request removes a reference to an entity for a single-valued navigation property. The following code snippet uses an example shown in Associate for a single-valued navigation property.
const new_testid = "00000000-0000-0000-0000-000000000001";
const request: DynamicsWebApi.AssociateSingleValuedRequest = {
collection: "new_tests",
primaryKey: new_testid,
navigationProperty: "new_ParentLead"
}
await dynamicsWebApi.disassociateSingleValued(request);
//does not have any return value
//declaring interface for an Account entity (declaration can be done in d.ts file)
interface Account {
accountid?: string,
name?: string
}
//build a fetch xml
const fetchXml = '<fetch mapping="logical">' +
'<entity name="account">' +
'<attribute name="accountid"/>' +
'<attribute name="name"/>' +
'</entity>' +
'</fetch>';
const request: DynamicsWebApi.FetchXmlRequest = {
collection: "accounts",
fetchXml: fetchXml
}
const result = await dynamicsWebApi.fetch<Account>(request);
//do something with results here; access records result.value[0].accountid
//build a fetch xml
const fetchXml = '<fetch mapping="logical">' +
'<entity name="account">' +
'<attribute name="accountid"/>' +
'<attribute name="name"/>' +
'</entity>' +
'</fetch>';
const request: DynamicsWebApi.FetchXmlRequest = {
collection: "accounts",
fetchXml: fetchXml
}
const page1 = await dynamicsWebApi.fetch<Account>(request);
//do something with results here; access records page1.value[0].accountid
request.pageNumber = page1.PagingInfo.nextPage;
request.pagingCookie = page1.PagingInfo.cookie;
const page2 = await dynamicsWebApi.fetch<Account>(request);
//do something with results here; access records page2.value[0].accountid
request.pageNumber = page2.PagingInfo.nextPage;
request.pagingCookie = page2.PagingInfo.cookie;
const page3 = await dynamicsWebApi.fetch<Account>(request);
//and so on... or use a recoursive loop.
The following function executes a FetchXml request and goes through all pages automatically:
const fetchXml = '<fetch mapping="logical">' +
'<entity name="account">' +
'<attribute name="accountid"/>' +
'<attribute name="name"/>' +
'</entity>' +
'</fetch>';
const result = await dynamicsWebApi.fetchAll<Account>({
collection: "accounts",
fetchXml: fetchXml
});
//do something with results here; access records result.value[0].accountid
//declaring needed types for the Function (types can be declared in *.d.ts file), if needed
enum UserResponse {
Basic = 0,
Local = 1,
Deep = 2,
Global = 3
}
interface RolePrivilege {
Depth: UserResponse,
PrivilegeId: string,
BusinessUnitId: string
}
interface RetrieveTeamPrivilegesResponse {
RolePrivileges: RolePrivilege[]
}
const teamId = "00000000-0000-0000-0000-000000000001";
const request: DynamicsWebApi.BoundFunctionRequest = {
id: teamId,
collection: "teams",
name: "Microsoft.Dynamics.CRM.RetrieveTeamPrivileges"
}
const result = await dynamicsWebApi.callFunction<RetrieveTeamPrivilegesResponse>(request);
//do something with a result
//declaring needed types for the Function (types can be declared in *.d.ts file), if needed
interface GetTimeZoneCodeByLocalizedNameResponse {
TimeZoneCode: number
}
const parameters = {
LocalizedStandardName: "Pacific Standard Time",
LocaleId: 1033
};
const request: DynamicsWebApi.UnboundFunctionRequest = {
parameters: parameters,
name: "GetTimeZoneCodeByLocalizedName"
}
const result = await dynamicsWebApi.callFunction<GetTimeZoneCodeByLocalizedNameResponse>(request);
const timeZoneCode = result.TimeZoneCode;
Unbound Web API functions can also be called using a short form (in case there are no parameters), for example:
const whoAmIResult = await dynamicsWebApi.callFunction("WhoAmI");
interface LetterAction {
Target: {
activityid: string,
"@data.type": string
}
}
interface LetterActionResponse {
QueueItemId: string
}
const queueId = "00000000-0000-0000-0000-000000000001";
const actionRequest: DynamicsWebApi.BoundActionRequest<LetterAction> = {
key: queueId,
collection: "queues",
actionName: "Microsoft.Dynamics.CRM.AddToQueue",
action: {
Target: {
activityid: "00000000-0000-0000-0000-000000000002",
"@odata.type": "Microsoft.Dynamics.CRM.letter"
}
}
}
const result = await dynamicsWebApi.callAction<LetterActionResponse>(actionRequest);
const queueItemId = result.QueueItemId;
interface WinOpportunityAction {
Status: number,
OpportunityClose: {
subject: string,
"[email protected]": string
}
}
const opportunityId = "b3828ac8-917a-e511-80d2-00155d2a68d2";
const actionRequest: DynamicsWebApi.UnboundActionRequest<WinOpportunityAction> = {
actionName: "WinOpportunity",
action: {
Status: 3,
OpportunityClose: {
subject: "Won Opportunity",
//DynamicsWebApi will add full url if the property contains @odata.bind suffix
//but it is also possible to provide a full url to the entity record manually
"[email protected]": "opportunities(" + opportunityId + ")"
}
}
}
await dynamicsWebApi.callAction(actionRequest);
Batch Operations bundle multiple requests into a single one and have the following advantages:
- Reduces a number of requests sent to the Web API server.
Each user is allowed up to 60,000 API requests, per organization instance, within five minute sliding interval.
More Info - Provides a way to run multiple operations in a single transaction. If any operation that changes data (within a single changeset) fails all completed ones will be rolled back.
- All operations within a batch request run consequently (FIFO).
DynamicsWebApi provides a straightforward way to execute Batch Operations which may not always be simple to compose.
Please Note! By default, all operations that modify data, such as: CREATE, UPDATE, UPSERT, PUT and DELETE - are automatically made atomic (combined in Change Sets). You can change this behaviour by setting inChangeSet
to false
. More Info
The following example bundles 2 retrieve multiple operations and an update:
//when you want to start a batch operation call the following function:
//it is important to call it, otherwise all operations below will be executed right away.
dynamicsWebApi.startBatch();
//call necessary operations just like you would normally do.
//these calls will be converted into a single batch request
dynamicsWebApi.retrieveMultiple({ collection: "accounts" });
dynamicsWebApi.update({
key: '00000000-0000-0000-0000-000000000002',
collection: "contacts",
data: { firstname: "Test", lastname: "Batch!" }
});
dynamicsWebApi.retrieveMultiple({ collection: "contacts" });
//execute a batch request:
const responses = await dynamicsWebApi.executeBatch();
//'responses' is an array of responses of each individual request
//they have the same sequence as the calls between startBatch() and executeBatch()
//in this case responses.length is 3
//dynamicsWebApi.retrieveMultiple response:
const accounts = responses[0];
//dynamicsWebApi.update response
const isUpdated = responses[1]; //should be 'true'
//dynamicsWebApi.retrieveMultiple response:
const contacts = responses[2]; //will contain an updated contact
The next example shows how to run multiple operations in a single transaction which means if at least one operation fails all completed ones will be rolled back which ensures a data consistency.
//for example, a user did a checkout and we need to create two orders
const order1 = {
name: "1 year membership",
"[email protected]": "contacts(00000000-0000-0000-0000-000000000001)"
};
const order2 = {
name: "book",
"[email protected]": "contacts(00000000-0000-0000-0000-000000000001)"
};
dynamicsWebApi.startBatch();
dynamicsWebApi.create({ data: order1, collection: "salesorders" });
dynamicsWebApi.create({ data: order2, collection: "salesorders" });
try {
const responses = await dynamicsWebApi.executeBatch();
const salesorderId1 = responses[0];
const salesorderId2 = responses[1];
}
catch (error){
//catch error here
//all completed operations will be rolled back
alert("Cannot complete a checkout. Please try again later.");
}
You can reference a request in a Change Set. For example, if you want to create related entities in a single batch request:
const order = {
name: "1 year membership"
};
const contact = {
firstname: "John",
lastname: "Doe"
};
dynamicsWebApi.startBatch();
dynamicsWebApi.create({ data: order, collection: "salesorders", contentId: "1" });
dynamicsWebApi.create({ data: contact, collection: "customerid_contact", contentId: "$1" });
const responses = await dynamicsWebApi.executeBatch()
const salesorderId = responses[0];
//responses[1]; is undefined <- Dataverse Web API limitation
Note that if you are making a request to a navigation property (collection: "customerid_contact"
), the request won't have a response, it is an OOTB Web API limitation.
Important! DynamicsWebApi automatically assigns value to a Content-ID
if it is not provided, therefore, please set your Content-ID
value less than 100000.
Another option to make the same request is to use Content-ID
reference inside a request payload as following:
const contact = {
firstname: "John",
lastname: "Doe"
};
const order = {
name: "1 year membership",
//reference a request in a navigation property
"[email protected]": "$1"
};
dynamicsWebApi.startBatch();
dynamicsWebApi.create({ data: contact, collection: "contacts", contentId: "1" });
dynamicsWebApi.create({ data: order, collection: "salesorders" });
const responses = await dynamicsWebApi.executeBatch();
//in this case both ids exist in a response
//which makes it a preferred method
const contactId = responses[0];
const salesorderId = responses[1];
Important! Web API seems to have a limitation (or a bug) where it does not return the response with returnRepresentation
set to true
. It happens only if you are trying to return a representation of an entity that is being
linked to another one in a single request. More Info and examples is in this issue.
As mentioned before, by default, all operations that modify data: CREATE, UPDATE, UPSERT, PUT and DELETE - are automatically made atomic (combined in Change Sets).
In some cases this can be an undesirable behaviour and with v2 there are several ways to make those operations non-atomic: per batch operation and per request. Let's use a code sample above and make all operations non-atomic. It can be done by setting inChangeSet
property to false
.
Important! contentId
can only be used inside the Change Sets. Any contentId
set in a request won't be included in a non-atomic batch operation! If $1
parameter was used outside of Change Set you will get an error similar to the following: Error identified in Payload provided by the user for Entity :'<entity name>'
.
Per batch operation:
const contact = {
firstname: "John",
lastname: "Doe"
};
const order = {
name: "1 year membership",
//reference a request in a navigation property
//"[email protected]": "$1" <--- commented out because we don't want to get an error
};
dynamicsWebApi.startBatch();
dynamicsWebApi.create({
data: contact,
collection: "contacts",
contentId: "1", //<--- will not be used
});
dynamicsWebApi.create({
data: order,
collection: "salesorders",
});
const responses = await dynamicsWebApi.executeBatch({
inChangeSet: false //<--- do not use change sets
});
Important! There seem to be a bug in Dynamics 365 Web Api (Checked: July 16, 2023) where it does not process the last operation in a batch request (Change Sets work fine). As a workaround, you can add any "GET" operation at the end to make it work, like in the following example. Please let me know if this bug was fixed.
Per request:
dynamicsWebApi.startBatch();
dynamicsWebApi.create({
data: contact,
collection: "contacts",
inChangeSet: false //<--- do not include in a change set
});
dynamicsWebApi.create({
data: order,
collection: "salesorders",
inChangeSet: false //<--- do not include in a change set
});
//this is a workaround to a D365 bug (checked on July 16, 2023)
dynamicsWebApi.retrieveMutliple({
collection: "contacts",
top: 1,
select: ["firstname"]
});
const responses = await dynamicsWebApi.executeBatch();
These two samples do the same thing: make all requests non-atomic.
By setting inChangeSet:false
per request gives more control over which operation should be included in a change set and which ones do not, for example:
dynamicsWebApi.startBatch();
dynamicsWebApi.create({
data: contact,
collection: "contacts",
});
dynamicsWebApi.create({
data: order,
collection: "salesorders",
});
dynamicsWebApi.create({
data: email,
collection: "emails",
inChangeSet: false //<--- do not include in a change set
});
const responses = await dynamicsWebApi.executeBatch();
The first two requests will be atomic (included in a Change Set) and the last one will be executed separately. So, if for some reason, there was an error during creation of an Email record, the whole operation won't be rolled back and the Contact and Order records will be created in the system.
Be extra careful with an order of requests in a Batch Operation, especially if it has a combination of atomic and non-atomic operations in it. For example, if in an example above we move the creation of an Email record above Order - it will create 2 separate change sets for the Contact and for the Order records.
dynamicsWebApi.startBatch();
//Change Set A starts
dynamicsWebApi.create({
data: contact,
collection: "contacts",
contentId: "1",
});
//Change Set A ends
dynamicsWebApi.create({
data: email,
collection: "emails",
inChangeSet: false //<--- do not include in a change set
});
//Change Set B starts
dynamicsWebApi.create({
data: order,
collection: "salesorders",
//"$1" parameter cannot be used here because it is defined in a Change Set A
//otherwise, you will get an error
});
//Change Set B ends
const responses = await dynamicsWebApi.executeBatch();
In this case, if creation of an Email fails Sales Order won't be created either. If this is an intended behaviour and if the operation must continue despite the errors set continueOnError
to true
in executeBatch
:
const responses = await dynamicsWebApi.executeBatch({ continueOnError: true });
Currently, there are some limitations in DynamicsWebApi Batch Operations:
- Operations that use pagination to recursively retrieve all records cannot be used in a 'batch mode'. These include:
retrieveAll
,retrieveAllRequest
,countAll
,fetchAll
,executeFetchXmlAll
. You will get an error saying that the operation is incompatible with a 'batch mode'.
There are also out of the box Web API limitations for batch operations:
- Batch requests can contain up to 1000 individual requests and cannot contain other batch requests.
- Not supported in Microsoft Power Pages. (checked June 2024)
You can find an official documentation that covers Web API batch requests here: Execute batch operations using the Web API.
Before working with metadata read the following section from Microsoft Documentation.
const entityDefinition = {
"@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
"Attributes": [
{
"AttributeType": "String",
"AttributeTypeName": {
"Value": "StringType"
},
"Description": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Type the name of the bank account",
"LanguageCode": 1033
}]
},
"DisplayName": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Account Name",
"LanguageCode": 1033
}]
},
"IsPrimaryName": true,
"RequiredLevel": {
"Value": "None",
"CanBeChanged": true,
"ManagedPropertyLogicalName": "canmodifyrequirementlevelsettings"
},
"SchemaName": "new_AccountName",
"@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
"FormatName": {
"Value": "Text"
},
"MaxLength": 100
}],
"Description": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "An entity to store information about customer bank accounts",
"LanguageCode": 1033
}]
},
"DisplayCollectionName": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Bank Accounts",
"LanguageCode": 1033
}]
},
"DisplayName": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Bank Account",
"LanguageCode": 1033
}]
},
"HasActivities": false,
"HasNotes": false,
"IsActivity": false,
"OwnershipType": "UserOwned",
"SchemaName": "new_BankAccount"
};
//entityId is newly created entity id (MetadataId)
const entityId = await dynamicsWebApi.createEntity({ data: entityDefinition });
Entity Metadata can be retrieved by either Primary Key (MetadataId) or by an Alternate Key (LogicalName). More Info
const entityKey = "00000000-0000-0000-0000-000000000001";
// or you can use an alternate key:
//const entityKey = "LogicalName='new_accountname'";
const entityMetadata = await dynamicsWebApi.retrieveEntity({
key: entityKey,
select: ["SchemaName", "LogicalName"]
});
const schemaName = entityMetadata.SchemaName;
Microsoft recommends to make changes in the entity metadata that has been priorly retrieved to avoid any mistake. I would also recommend to read information about MSCRM.MergeLabels header prior updating metadata. More information about the header can be found here.
Important! Make sure you set MetadataId
property when you update the metadata, DynamicsWebApi uses it as a primary key for the EntityDefinition record.
const entityKey = "LogicalName='new_accountname'";
const entityMetadata = await dynamicsWebApi.retrieveEntity({ key: entityKey });
//1. change label
entityMetadata.DispalyName.LocalizedLabels[0].Label = "New Bank Account";
//2. update metadata
await dynamicsWebApi.updateEntity({ data: entityMetadata });
Important! When you update a table definition, you must publish your changes. More Info. In our case we need to do an additional request to publish changes:
await dynamicsWebApi.callAction({
actionName: "PublishXml",
action: {
ParameterXml: "<importexportxml><webresources><webresource>new_accountname</webresource></webresources></importexportxml>"
}
});
You can find examples of ParameterXml
here.
const response = await dynamicsWebApi.retrieveEntities({
select: ["LogicalName"],
filter: "OwnershipType eq Microsoft.Dynamics.CRM.OwnershipTypes'UserOwned'"
});
const firstLogicalName = response.value[0].LogicalName;
const entityKey = '00000000-0000-0000-0000-000000000001';
const attributeDefinition = {
"AttributeType": "Money",
"AttributeTypeName": {
"Value": "MoneyType"
},
"Description": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Enter the balance amount",
"LanguageCode": 1033
}]
},
"DisplayName": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Balance",
"LanguageCode": 1033
}]
},
"RequiredLevel": {
"Value": "None",
"CanBeChanged": true,
"ManagedPropertyLogicalName": "canmodifyrequirementlevelsettings"
},
"SchemaName": "new_Balance",
"@odata.type": "Microsoft.Dynamics.CRM.MoneyAttributeMetadata",
"PrecisionSource": 2
};
//attributeId is a PrimaryKey (MetadataId) for newly created column
const attributeId = await dynamicsWebApi.createAttribute({
entityKey: entityKey,
data: attributeDefinition
});
Column definitions can be retrieved by either Primary Key (MetadataId) or by an Alternate Key (LogicalName). More Info
The following example will retrieve only common properties available in AttributeMetadata entity.
const entityKey = "00000000-0000-0000-0000-000000000001";
//or you can use an alternate key:
//var entityKey = "LogicalName='new_accountname'";
const attributeKey = "00000000-0000-0000-0000-000000000002";
//or you can use an alternate key:
//var attributeKey = "LogicalName='new_balance'";
const attributeMetadata = await dynamicsWebApi.retrieveAttribute({
entityKey: entityKey,
attributeKey: attributeKey,
select: ["SchemaName"]
});
const schemaName = attributeMetadata.SchemaName;
Use castType
property in a request to cast the attribute to a specific type.
const entityKey = '00000000-0000-0000-0000-000000000001';
const attributeKey = '00000000-0000-0000-0000-000000000002';
const attributeMetadata = await dynamicsWebApi.retrieveAttribute({
entityKey: entityKey,
attributeKey: attributeKey,
select: ["SchemaName"],
castType: "Microsoft.Dynamics.CRM.MoneyAttributeMetadata"
})
const schemaName = attributeMetadata.SchemaName;
Important! Make sure you set MetadataId
property when you update the metadata, DynamicsWebApi use it as a primary key for the EntityDefinition record.
The following example will update only common properties availible in AttributeMetadata entity. If you need to update specific properties of Attributes with type that inherit from the AttributeMetadata you will need to cast the attribute to the specific type. More Info
const entityKey = "LogicalName='my_accountname'";
const attributeKey = "LogicalName='my_balance'";
const attributeMetadata = await dynamicsWebApi.retrieveAttribute({
entityKey: entityKey,
attributeKey: attributeKey
});
//1. change label
attributeMetadata.DispalyName.LocalizedLabels[0].Label = "New Balance";
//2. update metadata
await dynamicsWebApi.updateAttribute({
entityKey: entityKey,
data: attributeMetadata
});
To cast a property to a specific type use a parameter in the function.
const entityKey = "LogicalName='my_accountname'";
const attributeKey = "LogicalName='my_balance'";
const attributeType = 'Microsoft.Dynamics.CRM.MoneyAttributeMetadata';
const attributeMetadata = await dynamicsWebApi.retrieveAttribute({
entityKey: entityKey,
attributeKey: attributeKey,
castType: attributeType
});
//1. change label
attributeMetadata.DispalyName.LocalizedLabels[0].Label = "New Balance";
//2. update metadata
await dynamicsWebApi.updateAttribute({
entityKey: entityKey,
data: attributeMetadata,
castType: attributeType
});
Important! Make sure you include the attribute type in the update function as well.
Important! When you update an attribute, you must publish changes in CRM. More Info
The following example will retrieve only common properties available in AttributeMetadata entity.
const entityKey = "LogicalName='my_accountname'";
const response = await dynamicsWebApi.retrieveAttributes({ entityKey: entityKey });
const firstAttribute = response.value[0];
To retrieve columns of only a specific type use a castType
property in a request object:
const entityKey = "LogicalName='my_accountname'";
const response = await dynamicsWebApi.retrieveAttributes({
entityKey: entityKey,
castType: "Microsoft.Dynamics.CRM.MoneyAttributeMetadata"
});
const firstAttribute = response.value[0];
You can also use common request functions to create, retrieve and update entity and attribute metadata. Just use the following rules:
- Always set
collection: "EntityDefinitions"
. - To retrieve a specific entity metadata by a Primary or Alternate Key use
key
property. For example:key: 'LogicalName="account"'
. - To get attributes, set
navigationProperty: "Attributes"
. - To retrieve a specific attribute metadata by Primary or Alternate Key use
navigationPropertyKey
. For example:navigationPropertyKey: "00000000-0000-0000-0000-000000000002"
. - During entity or attribute metadata update you can use
mergeLabels
property to set MSCRM.MergeLabels attribute. By defaultmergeLabels: false
. - To send entity or attribute definition use
data
property.
Retrieve a table definition with columns (with common properties):
const entityMetadata = await dynamicsWebApi.retrieve({
collection: "EntityDefinitions",
key: "00000000-0000-0000-0000-000000000001",
select: ["LogicalName", "SchemaName"],
expand: "Attributes"
});
const attributes = entityMetadata.Attributes;
Retrieve a column definition and cast it to the StringType
:
const request = {
collection: "EntityDefinitions",
key: "LogicalName='account'",
navigationProperty: "Attributes",
navigationPropertyKey: "LogicalName='firstname'",
metadataAttributeType: "Microsoft.Dynamics.CRM.StringAttributeMetadata"
};
const attributeMetadata = await dynamicsWebApi.retrieve(request);
const displayNameDefaultLabel = attributeMetadata.DisplayName.LocalizedLabels[0].Label;
Update entity metadata with MSCRM.MergeLabels header set to true
:
const entityMetadata = await dynamicsWebApi.retrieve({
collection: "EntityDefinitions",
key: "LogicalName='account'"
});
//1. change label
entityMetadata.DisplayName.LocalizedLabels[0].Label = "Organization";
//2. configure update request
const updateRequest = {
collection: "EntityDefinitions",
key: entityMetadata.MetadataId,
mergeLabels: true,
data: entityMetadata
};
//3. call update request
await dynamicsWebApi.update(updateRequest);
//it is the same as:
const entityMetadata2 = await dynamicsWebApi.retrieveEntity({ key: "LogicalName='account'" });
//1. change label
entityMetadata2.DisplayName.LocalizedLabels[0].Label = "Organization";
//2. call update request
await dynamicsWebApi.updateEntity({
data: entityMetadata,
mergeLabels: true
});
const newRelationship = {
"SchemaName": "dwa_contact_dwa_dynamicswebapitest",
"@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
"AssociatedMenuConfiguration": {
"Behavior": "UseCollectionName",
"Group": "Details",
"Label": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [
{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "DWA Test",
"LanguageCode": 1033
}
],
"UserLocalizedLabel": {
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "DWA Test",
"LanguageCode": 1033
}
},
"Order": 10000
},
"CascadeConfiguration": {
"Assign": "Cascade",
"Delete": "Cascade",
"Merge": "Cascade",
"Reparent": "Cascade",
"Share": "Cascade",
"Unshare": "Cascade"
},
"ReferencedAttribute": "contactid",
"ReferencedEntity": "contact",
"ReferencingEntity": "dwa_dynamicswebapitest",
"Lookup": {
"AttributeType": "Lookup",
"AttributeTypeName": {
"Value": "LookupType"
},
"Description": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [
{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "The owner of the test",
"LanguageCode": 1033
}
],
"UserLocalizedLabel": {
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "The owner of the test",
"LanguageCode": 1033
}
},
"DisplayName": {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [
{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "DWA Test Owner",
"LanguageCode": 1033
}
],
"UserLocalizedLabel": {
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "DWA Test Owner",
"LanguageCode": 1033
}
},
"RequiredLevel": {
"Value": "ApplicationRequired",
"CanBeChanged": true,
"ManagedPropertyLogicalName": "canmodifyrequirementlevelsettings"
},
"SchemaName": "dwa_TestOwner",
"@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata"
}
};
//relationshipId is a PrimaryKey (MetadataId) for a newly created relationship
const relationshipId = await dynamicsWebApi.createRelationship({ data: newRelationship });
Important! Make sure you set MetadataId
property when you update the metadata, DynamicsWebApi use it as a primary key for the EntityDefinition record.
const metadataId = "10cb680e-b6a7-e811-816a-480fcfe97e21";
const relationship = await dynamicsWebApi.retrieveRelationship({ key: metadataId });
relationship.AssociatedMenuConfiguration.Label.LocalizedLabels[0].Label = "New Label";
const updateResponse = await dynamicsWebApi.updateRelationship(relationship);
const metadataId = "10cb680e-b6a7-e811-816a-480fcfe97e21";
const isDeleted = await dynamicsWebApi.deleteRelationship({ key: metadataId });
const metadataId = "10cb680e-b6a7-e811-816a-480fcfe97e21";
const relationship = await dynamicsWebApi.retrieveRelationship({ key: metadataId });
You can also cast a relationship into a specific type:
const metadataId = "10cb680e-b6a7-e811-816a-480fcfe97e21";
const relationship = await dynamicsWebApi.retrieveRelationship({
key: metadataId,
castType: "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata"
});
const relationshipType = "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata";
const relationships = await dynamicsWebApi.retrieveRelationships({
castType: relationshipType,
select: ["SchemaName", "MetadataId"],
filter: "ReferencedEntity eq 'account'"
})
Please check an official Microsoft Documentation for more information.
Find an example on how to create a polymorfic lookup attribute below. All other operations can be done with existing functions such as retrieveRelationship
or retrieveAttribute
...
interface CreatePolymorphicLookupAttributeResponse {
"@odata.context": string,
RelationshipIds: string[],
AttributeId: string
}
const response = await dynamicsWebApi.create<CreatePolymorphicLookupAttributeResponse>({
collection: "CreatePolymorphicLookupAttribute",
data: {
OneToManyRelationships: [
{
SchemaName: "new_media_new_book",
ReferencedEntity: "new_book",
ReferencingEntity: "new_media"
},
{
SchemaName: "new_media_new_video",
ReferencedEntity: "new_video",
ReferencingEntity: "new_media"
},
{
SchemaName: "new_media_new_audio",
ReferencedEntity: "new_audio",
ReferencingEntity: "new_media",
CascadeConfiguration: {
Assign: "NoCascade",
Delete: "RemoveLink",
Merge: "NoCascade",
Reparent: "NoCascade",
Share: "NoCascade",
Unshare: "NoCascade"
}
}],
Lookup: {
AttributeType: "Lookup",
AttributeTypeName: {
Value: "LookupType"
},
Description: {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
LocalizedLabels: [
{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
Label: "Media Polymorphic Lookup",
LanguageCode: 1033
}],
UserLocalizedLabel: {
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
Label: " Media Polymorphic Lookup Attribute",
LanguageCode: 1033
}
},
DisplayName: {
"@odata.type": "Microsoft.Dynamics.CRM.Label",
LocalizedLabels: [
{
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
Label: "MediaPolymorphicLookup",
LanguageCode: 1033
}],
UserLocalizedLabel: {
"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
Label: "MediaPolymorphicLookup",
LanguageCode: 1033
}
},
SchemaName: "new_mediaPolymporphicLookup",
"@odata.type": "Microsoft.Dynamics.CRM.ComplexLookupAttributeMetadata"
}
}
});
const attributeId = response.AttributeId;
response.RelationshipIds.forEach(id => {
//do something with the ids
});
const optionSetDefinition = {
"@odata.type": "Microsoft.Dynamics.CRM.OptionSetMetadata",
IsCustomOptionSet: true,
IsGlobal: true,
IsManaged: false,
Name: "new_customglobaloptionset",
OptionSetType: "Picklist",
Options: [{
Value: 0,
Label: {
LocalizedLabels: [{
Label: "Label 1", LanguageCode: 1033
}],
UserLocalizedLabel: {
Label: "Label 1", LanguageCode: 1033
}
},
Description: {
LocalizedLabels: [],
UserLocalizedLabel: null
}
}, {
Value: 1,
Label: {
LocalizedLabels: [{
Label: "Label 2", LanguageCode: 1033
}],
UserLocalizedLabel: {
Label: "Label 2", LanguageCode: 1033
}
},
Description: {
LocalizedLabels: [],
UserLocalizedLabel: null
}
}],
Description: {
LocalizedLabels: [{
Label: "Description to the Global Option Set.", LanguageCode: 1033
}],
UserLocalizedLabel: {
Label: "Description to the Global Option Set.", LanguageCode: 1033
}
},
DisplayName: {
LocalizedLabels: [{
Label: "Display name to the Custom Global Option Set.", LanguageCode: 1033
}],
UserLocalizedLabel: {
Label: "Display name to the Custom Global Option Set.", LanguageCode: 1033
}
},
IsCustomizable: {
Value: true, "CanBeChanged": true, ManagedPropertyLogicalName: "iscustomizable"
}
};
//result is a metadata id
const id = await dynamicsWebApi.createGlobalOptionSet({
data: optionSetDefinition
});
Important! Publish your changes after update, otherwise a label won't be modified.
let key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
//or
key = "Name='new_customglobaloptionset'";
const optionSet = await dynamicsWebApi.retrieveGlobalOptionSet({ key: key });
optionSet.DisplayName.LocalizedLabels[0].Label = "Updated Display name to the Custom Global Option Set.";
const updatedOptionSet = dynamicsWebApi.updateGlobalOptionSet(response);
let key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
//or
key = "Name='new_customglobaloptionset'";
const isDeleted = await dynamicsWebApi.deleteGlobalOptionSet({ key: key });
const key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
//or
key = "Name='new_customglobaloptionset'";
let optionSet = await dynamicsWebApi.retrieveGlobalOptionSet(key);
//OR select specific attributes
optionSet = await dynamicsWebApi.retrieveGlobalOptionSet({
key: key,
select: ["DisplayName"]
});
//OR cast option set to a specific type, for example:
//Options attribute exists only in OptionSetMetadata, therefore we need to cast to it
optionSet = await dynamicsWebApi.retrieveGlobalOptionSet({
key: key,
castType: "Microsoft.Dynamics.CRM.OptionSetMetadata",
select: ["Name", "Options"]
});
let optionSetsResponse = await dynamicsWebApi.retrieveGlobalOptionSets();
let optionSet = optionSetsResponse.value[0]; //first global option set
//OR select specific attributes AND cast to a specific type
optionSetResponse = await dynamicsWebApi.retrieveGlobalOptionSets({
castType: "Microsoft.Dynamics.CRM.OptionSetMetadata",
select: ["Name", "Options"]
});
optionSet = optionSetResponse.value[0]; //first global option set
To retrieve a CSDL $metadata document use the following:
const request: DynamicsWebApi.CsdlMetadataRequest = {
addAnnotations: false; //or true;
}
//the parameter "request" is optional and can be ommited if additional annotations are not necessary
const csdlDocument: string = await dynamicsWebApi.retrieveCsdlMetadata(request);
The csdlDocument
will be the type of string
. DynamicsWebApi does not parse the contents of the document and it should be done by the developer.
With DynamicsWebApi it is easier (less code) to access formatted values for the columns as well as the lookup data in response objects. DynamicsWebApi automatically creates aliases for each property that contains a formatted value or lookup data. For example:
//DynamicsWebApi supports an access to formatted values in both ways
//normally you would access a formatted value for account.donotpostalmail field could as following:
let doNotPostEmailFormatted = response['[email protected]'];
//with DynamicsWebApi it can be access like this:
doNotPostEmailFormatted = response.donotpostalmail_Formatted;
//same for lookup data
//normally
let customerName = response['[email protected]'];
let customerEntityLogicalName = response['[email protected]'];
let customerNavigationProperty = response['_customerid_value@Microsoft.Dynamics.CRM.associatednavigationproperty'];
//with DynamicsWebApi
customerName = response._customerid_value_Formatted;
customerEntityLogicalName = response._customerid_value_LogicalName;
customerNavigationProperty = response._customerid_value_NavigationProperty;
If you still want to use old properties you can do so, they are not removed from the response, so it does not break your existing functionality.
As you have already noticed formatted and lookup data values are accesed by adding a particular suffix to a property name, the following table summarizes it.
OData Annotation | Property Suffix |
---|---|
@OData.Community.Display.V1.FormattedValue |
_Formatted |
@Microsoft.Dynamics.CRM.lookuplogicalname |
_LogicalName |
@Microsoft.Dynamics.CRM.associatednavigationproperty |
_NavigationProperty |
You can use alternate keys to Update, Upsert, Retrieve and Delete records. More Info
const request = {
key: "alternateKey='keyValue'",
collection: 'leads',
select: ['fullname', 'subject']
};
const record = await dynamicsWebApi.retrieveRequest(request);
//do something with a record
It is possible to make requests using Entity Logical Names (for example: account
, instead of accounts
).
There's a small perfomance impact when this feature is used outside CRM/D365 Web Resources: DynamicsWebApi makes a request to
Entity Metadata and retrieves LogicalCollectionName and LogicalName for all entities during the first call to Web Api on the page.
To enable this feature set useEntityNames: true
in DynamicsWebApi config.
interface Lead {
fullname?: string,
subject?: string,
leadid?: string
}
const dynamicsWebApi = new DynamicsWebApi({ useEntityNames: true });
//make request using entity names
const lead = await dynamicsWebApi.retrieve<Lead>({
key: leadId,
collection: "lead",
select: ["fullname", "subject"]
});
This feature also applies when you set a navigation property and provide an entity name in the value:
const account = {
name: "account name",
"[email protected]": "contact(00000000-0000-0000-0000-000000000001)"
}
const accountid = await dynamicsWebApi.create({
collection: "account",
data: account
});
In the example above, entity names will be replaced with collection names: contact
with contacts
, account
with accounts
.
This happens, because DynamicsWebApi automatically checks all properties that end with @odata.bind
or @odata.id
.
Thus, there may be a case when those properties are not used but you still need a collection name instead of an entity name.
Please use the following method to get a collection name from a cached entity metadata:
//IMPORTANT! collectionName will be null if there was no call to Web API prior to that
//this restriction does not apply if DynamicsWebApi used inside CRM/D365
const collectionName = dynamicsWebApi.Utility.getCollectionName("account");
Please note, everything said above will happen only if you set useEntityNames: true
in the DynamicsWebApi config.
Please make sure that you are connected to Dynamics 365 Web API with version 9.1+ to successfully use the functions. More information can be found here
Browser
const fileElement = document.getElementById("upload");
const fileName = fileElement.files[0].name;
const fr = new FileReader();
fr.onload = function(){
const fileData = new Uint8Array(this.result);
dynamicsWebApi.uploadFile({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
property: "dwa_file",
fileName: fileName,
data: fileData
}).then(function(){
//success
}).catch (function (error) {
//catch error here
});
}
fr.readAsArrayBuffer(fileElement.files[0]);
Node.JS
const fs = require("fs");
const filename = "logo.png";
fs.readFile(filename, (err, data) => {
dynamicsWebApi.uploadFile({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
property: "dwa_file",
fileName: filename
data: data,
}).then(function() {
//success
}).catch(function (error) {
//catch error here
});
});
const donwloadResponse = await dynamicsWebApi.downloadFile({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
property: "dwa_file"
});
//Uint8Array for browser and Buffer for Node.js
const fileBinary = donwloadResponse.data;
const fileName = donwloadResponse.fileName;
const fileSize = donwloadResponse.fileSize;
const isDeleted = await dynamicsWebApi.deleteRecord({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
property: "dwa_file"
});
Important! Currently only Search API 1.0 is supported. Search API 2.0 is coming with v.2.2.0
.
If you must use 2.0, you could use corresponding Actions for now. more info
DynamicsWebApi can be used to call Dataverse Search API and utilize its powerful Search, Suggest and Autocomplete capabilities. Before using, I highly recommend to get familiar with it by reading an official documentation.
To set Search API version use: new DynamicsWebApi({ searchApi: { version: "1.0" }})
.
Search, Suggest and Autocomplete requests have a common property query
. This is the main property that configures a relevance search request.
All functions can also be called with a single parameter term
which is of type string
.
Examples below follow Microsoft official documenation.
The following table describes all parameters for a search
request:
Property Name | Type | Description |
---|---|---|
search | string |
Required. The search parameter value contains the term to be searched for and has a 100-character limit. |
entities | string[] |
The default table list searches across all Dataverse search–configured tables and columns. The default list is configured by your administrator when Dataverse search is enabled. |
facets | string[] |
Facets support the ability to drill down into data results after they've been retrieved. |
filter | string |
Filters are applied while searching data and are specified in standard OData syntax. |
orderBy | string[] |
A list of comma-separated clauses where each clause consists of a column name followed by 'asc' (ascending, which is the default) or 'desc' (descending). This list specifies how to order the results in order of precedence. |
returnTotalRecordCount | boolean |
Specify true to return the total record count; otherwise false. The default is false. |
searchMode | string |
Specifies whether any or all the search terms must be matched to count the document as a match. The default is 'any'. |
searchType | string |
The search type specifies the syntax of a search query. Using 'simple' selects simple query syntax and 'full' selects Lucene query syntax. The default is 'simple'. |
skip | number |
Specifies the number of search results to skip. |
top | number |
Specifies the number of search results to retrieve. The default is 50, and the maximum value is 100. |
Examples:
let result = await dynamicsWebApi.search({
query: {
search: "<search term>"
}
});
//the same as:
result = await dynamicsWebApi.search("<search term>");
const result = await dynamicsWebApi.search({
query: {
search: "maria",
facets: [
"@search.entityname,count:100",
"account.primarycontactid,count:100",
"ownerid,count:100",
"modifiedon,values:2019-04-27T00:00:00|2020-03-27T00:00:00|2020-04-20T00:00:00|2020-04-27T00:00:00",
"createdon,values:2019-04-27T00:00:00|2020-03-27T00:00:00|2020-04-20T00:00:00|2020-04-27T00:00:00"
]
}
});
const result = await dynamicsWebApi.search({
query: {
search: "maria",
filter: "account:modifiedon ge 2020-04-27T00:00:00," +
"activities: regardingobjecttypecode eq 'account', annotation:objecttypecode eq 'account'," +
"incident: (prioritycode eq 1 or prioritycode eq 2)"
}
});
const result = await dynamicsWebApi.search({
query: {
search: "maria",
facets: [
"@search.entityname,count:100",
"account.primarycontactid,count:100",
"ownerid,count:100",
"modifiedon,values:2019-04-27T00:00:00|2020-03-27T00:00:00|2020-04-20T00:00:00|2020-04-27T00:00:00",
"createdon,values:2019-04-27T00:00:00|2020-03-27T00:00:00|2020-04-20T00:00:00|2020-04-27T00:00:00"
]
}
});
const firstHit = result.value[0];
const firstHitScore = firstHit["@search.score"];
const firstSearchEntityName = result.facets["@search.entityname"][0].Value;
The following table describes all parameters for a suggest
request:
Property Name | Type | Description |
---|---|---|
search | string |
Required. The search parameter value contains the term to be searched for and has a 3-character min limit and max 100-character limit. |
entities | string[] |
The default table list searches across all Dataverse search–configured tables and columns. The default list is configured by your administrator when Dataverse search is enabled. |
orderBy | string[] |
A list of comma-separated clauses where each clause consists of a column name followed by 'asc' (ascending, which is the default) or 'desc' (descending). This list specifies how to order the results in order of precedence. |
filter | string |
Filters are applied while searching data and are specified in standard OData syntax. |
top | number |
Number of suggestions to retrieve. The default is 5. |
useFuzzy | boolean |
Use fuzzy search to aid with misspellings. The default is false. |
Examples:
let result = await dynamicsWebApi.suggest({
query: {
search: "mar"
}
});
//the same as:
result = await dynamicsWebApi.suggest("mar");
const firstText = result.value[0].text;
const firstDocument = result.value[0].document;
const result = await dynamicsWebApi.suggest({
query: {
search: "mar",
filter: "account:modifiedon ge 2020-04-27T00:00:00," +
"activities:regardingobjecttypecode eq 'account', annotation:objecttypecode eq 'account'"
}
});
The following table describes all parameters for an autocomplete
request:
Property Name | Type | Description |
---|---|---|
search | string |
Required. The search parameter value contains the term to be searched for and has a 100-character limit. |
entities | string[] |
The default table list searches across all Dataverse search–configured tables and columns. The default list is configured by your administrator when Dataverse search is enabled. |
filter | string |
Filters are applied while searching data and are specified in standard OData syntax. |
useFuzzy | boolean |
Use fuzzy search to aid with misspellings. The default is false. |
Examples:
let result = await dynamicsWebApi.autocomplete({
query: {
search: "mar"
}
});
//the same as:
result = await dynamicsWebApi.autocomplete("mar");
const value = result.value;
const result = await dynamicsWebApi.autocomplete({
query: {
search: "mar",
filter: "account:modifiedon ge 2020-04-27T00:00:00," +
"activities:regardingobjecttypecode eq 'account', annotation:objecttypecode eq 'account'"
}
});
If necessary, it is possible to abort a DynamicsWebApi request via the AbortController
object. Request cancellation works in Browsers and Node.js v15.0.0+.
const controller = new AbortController();
const somethingHappenedMustAbort = () => controller.abort();
/*
setTimeout here is used as an example: something happened
and the app must abort the request right away.
...
if you need to set a specific timeout for a request,
use "timeout" setting in the configuration
*/
setTimeout(() => somethingHappenedMustAbort(), 200);
try {
const result = await dynamicsWebApi.retrieveAll({
collection: "contacts",
select: ["firstname", "lastname"],
signal: controller.signal
});
}
catch(error){
if (error.name === "AbortError") {
//request was aborted
}
}
Node.js Only. DynamicsWebApi supports different types of connections through proxy. To make it possible, I added two dependencies in a package.json
:
http-proxy-agent and https-proxy-agent, DynamicsWebApi will use one of those agents based on the type of a protocol.
In order to let DynamicsWebApi know that you are using proxy you have two options:
- add environmental variables
http_proxy
orhttps_proxy
, - or pass parameters in DynamicsWebApi configuration, for example:
const dynamicsWebApi = new DynamicsWebApi({
serverUrl: "https://myorg.api.crm.dynamics.com/",
onTokenRefresh: acquireToken,
proxy: {
url: "http://localhost:12345",
//auth is optional, you can also provide authentication in the url
auth: {
username: "john",
password: "doe"
}
}
});
If you are not familiar with declaration files, these files are used to provide TypeScript type information about an API. You want to consume those from your TypeScript code. Quote
If you are using Node.Js with TypeScript, declarations will be fetched with an NPM package during an installation or an update process.
At the top of a necessary .ts
file add the following:
import { DynamicsWebApi, type Config } from "dynamics-web-api";
//for CommonJS:
//const DynamicsWebApi = require("dynamics-web-api");
If you are developing CRM Web Resources with TypeScript (and are not using NPM), you can download a TypeScript declaration file dynamics-web-api.d.ts
manually from v2 dist folder. I usually put all declarations in the "./types/" folder of my web resources project. For example:
[project root]/
-- src/
-- form_web_resource.ts
-- types/
-- dynamics-web-api/
-- dynamics-web-api.d.ts
-- tsconfig.json
Important! Make sure that you include types
folder in your tsconfig.json
:
"include": [
"./src/**/*",
"./types/**/*"
]
The declaration file is an ESM module, so if you are not using any bundler, you will have to add another d.ts file (let's call it dynamics-web-api.umd.d.ts
and put it in the types
folder) that will make DynamicsWebApi available globally. Here is an example (the same folder structure as mentioned above):
//dynamics-web-api.umd.d.ts
//import a DynamicsWebApi class from dynamics-web-api.d.ts file
import { DynamicsWebApi } from "./dynamics-web-api"
//make the DynamicsWebApi class available globally
export = DynamicsWebApi;
//wrap all other exports in a namespace
export as namespace DynamicsWebApi;
DynamicsWebApi
will now be a global object.
If you need to access other types that DynamicsWebApi
exports, you will have to create an additional d.ts
file, unfortunately. I could not find a way to do this in a single file (mostly because of export =
which cannot be combined with any other exports except for "as namespace"). If anyone knows a way to do that - let me know. Here's an example with a seprate file:
//dynamics-web-api.types.umd.d.ts
export type {
RetrieveMultipleRequest,
CreateRequest
/* Other exports here if needed */
} from "./dynamics-web-api";
export as namespace Dwa;
These types can then be used anywhere in the code the following way:
const createRequest: Dwa.CreateRequest<Account> = {
collection: "accounts",
data: {
name: "Example"
}
}
const id = await dynamicsWebApi.create(createRequest) as string;
DynamicsWebApi as an external library. For those who use bundlers, but want to keep DynamicsWebApi as an external library (meaning that you don't want to bundle the library but reference it separately in a script tag somewhere): 1. you will have to make sure that your bundler supports externals
configuraiton. 2. you will need to set the replacement for the import of dynamics-web-api
with _dynamicsWebApiExports
. For example, in webpack's case, you will have something like this:
{
//...your bundle configuration
"externals": {
"dynamics-web-api" : "_dynamicsWebApiExports"
}
}
This will work only if you import the library like this: import { DynamicsWebApi } from "dynamics-web-api"
. If you don't install the package with npm but just keep the declaration files somewhere and import it with relative paths, you can set the paths option in your tsconfig.json
which will create a consistent alias for all your relative import paths.
{
"compilerOptions": {
//...your compiler options
"paths": {
"dynamics-web-api": ["./types/dynamics-web-api"]
}
}
}
- Overloaded functions with rich request options for all Web API operations.
- Get all pages requests, such as: countAll, retrieveMultipleAll, fetchXmlAll and etc.
Implemented in v.1.2.5
- Web API requests that have long URL (more than 2000 characters) should be automatically converted to batch requests.
Feature is very convenient for big Fetch XMLs.
Implemented in v.1.2.8
- "Formatted" values in responses. For instance: Web API splits information about lookup fields into separate properties, the config option "formatted" will enable developers to retrieve all information about such fields in a single requests and access it through DynamicsWebApi custom response objects.
- Simplified names for "Formatted" properties.
Implemented in v.1.3.0
- RUD operations using Alternate Keys.
Implemented in v.1.3.4
- Duplicate Detection for Web API v.9.0.
Implemented in v.1.3.4
- Ability to use entity names instead of collection names.
Implemented in v.1.4.0
- Entity and Attribute Metadata helpers.
Implemented in v.1.4.3
- Entity Relationships and Global Option Sets helpers.
Implemented in v.1.4.6
- Batch requests.
Implemented in v.1.5.0
- TypeScript declaration files
d.ts
Added in v.1.5.3
- Implement
Content-ID
header to reference a request in a Change Set in a batch operationAdded in v.1.5.6
- Change Tracking
Added in v.1.5.11
- Support for Aggregate and Grouping results
Added in v1.6.4
- Support for Timeout option in the configuration
Added in v1.6.10
- Impersonate a user based on their Azure Active Directory (AAD) object id.
Added in v.1.6.12
- File upload/download/delete for a File Field.
Added in v.1.7.0
- Full proxy support.
Added in v.1.7.2
- Refactoring and conversion to TypeScript.
Added in v.2.0.0
- Implement Dataverse Search API 1.0.
Added in v.2.0.0
- Allow custom headers to be passed to the request #151.
Added in v.2.1.0
- Support Microsoft Power Pages.
Added in v.2.1.0
- Support Search API 2.0 #174.
Coming with v.2.2.0
- Custom requests.
Many more features to come!
Thank you for your patience and for using DynamcisWebApi!
First of all, I would like to thank you for using DynamicsWebApi
library in your Dynamics 365 CE / Common Data Service project, the fact that my project helps someone to achieve their development goals already makes me happy.
And if you would like to contribute to the project you may do it in multiple ways:
- Submit an issue/bug if you have encountered one.
- If you know the root of the issue please feel free to submit a pull request, just make sure that all tests pass and if the fix needs new unit tests, please add one.
- Let me and community know if you have any ideas or suggestions on how to improve the project by submitting an issue on GitHub, I will label it as a 'future enhancement'.
- Feel free to connect with me on LinkedIn and if you wish to let me know how you use
DynamicsWebApi
and what project you are working on, I will be happy to hear about it. - I maintain this project in my free time and, to be honest with you, it takes a considerable amount of time to make sure that the library has all new features, gets improved and all raised tickets have been answered and fixed in a short amount of time. If you feel that this project has saved your time and you would like to support it, then please feel free to use PayPal or GitHub Sponsors. My PayPal button: , GitHub button can be found on the project's page.
All contributions are greatly appreciated!