DynamicsWebApi for Microsoft Dynamics 365 CE (CRM) / Microsoft Dataverse Web API (formerly known as Microsoft Common Data Service Web API)
DynamicsWebApi is a Microsoft Dataverse Web API helper library written in Typescript.
It is compatible with: Microsoft Dataverse, Microsoft Dynamics 365: Customer Service, Field Service, Marketing, Project Operations, Talents, Sales and any 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+. If you are working with version 1.x, please check this instead.
Please check DynamicsWebApi Wiki where you will find documentation to DynamicsWebApi API and more.
Browser-compiled script and type definitions can be found in a dist folder.
Main Features
- Microsoft Dataverse Search API. Access full power of its Search, Suggestion and Autocomplete capabilities.
- Batch Requests. Convert all requests into a Batch operation with a single line 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.
Terminology
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.
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.
Table of Contents
Getting Started
Dynamics 365 Web Resource
To use DynamicsWebApi inside Dynamics 365 you need to download a browser version of the library, it can be found in dist folder.
Upload a script as a JavaScript Web Resource, add it to a table form or reference it in your HTML Web Resource and then initialize the main object:
const dynamicsWebApi = new DynamicsWebApi();
const response = await dynamicsWebApi.callFunction("WhoAmI");
Xrm.Navigation.openAlertDialog({ text: `Hello Dynamics 365! My id is: ${response.UserId}` });
Node.js
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:
const DynamicsWebApi = require("dynamics-web-api");
import { DynamicsWebApi } from "dynamics-web-api";
DynamicsWebApi does not fetch authorization tokens, so 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
:
import { Config } from './config.ts';
import { DynamicsWebApi } from 'dynamics-web-api';
import * as MSAL from '@azure/msal-node';
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';
const acquireToken = async () => {
try {
return cca.acquireTokenByClientCredential({
scopes: [`${serverUrl}/.default`],
});
}
catch (error) {
return null;
}
}
const dynamicsWebApi = new DynamicsWebApi({
serverUrl: serverUrl,
dataApi: {
version: "9.2"
},
onTokenRefresh: acquireToken
});
try{
const response = await dynamicsWebApi.callFunction("WhoAmI");
console.log(`Hello from Dynamics 365! My id is: ${response.UserId}`);
}
catch (error){
console.log(error);
}
Configuration
To initialize a new instance of DynamicsWebApi with a configuration object, please use the following code:
Dynamics 365 Web Resource
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:
Node.js
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" } });
Configuration Parameters
Property Name | Type | Description |
---|
dataApi | ApiConfig | Configuration object for Dataverse Web API. The name is based on the url path data . |
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/". |
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 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 seatchApi
can be omitted from a configuration. Their default values are:
{
path: "data",
version: "9.2"
}
{
path: "search",
version: "1.0"
}
dataApi properties:
Property Name | Type | Description |
---|
path | String | Optional. A path to API, for example: "data". |
version | String | Optional. API Version, for example: "9.1" or "9.2". |
Request Examples
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 request
object.
Please note! Not all operaions 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 |
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 , deleteRequest | D365 Web API v9.1+ 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 filefilter |
functionName | string | callFunction | Name of a D365 Web Api function. |
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 |
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 |
proxy | Object | Proxy configuration object. More Info | |
queryParams | string[] | retrieveMultiple , retrieveAll | Additional query parameters that either have not been implemented yet or they are 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 | 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.
Create a table row
interface Lead {
leadid?: string,
subject?: string,
firstname?: string,
lastname?: string,
jobtitle?: string
}
const lead: Lead = {
subject: "Test WebAPI",
firstname: "Test",
lastname: "WebAPI",
jobtitle: "Title"
};
const request: DynamicsWebApi.CreateRequest = {
collection: "leads",
data: lead,
returnRepresentation: true
}
const result = await dynamicsWebApi.create<Lead>(request);
const leadId = result.leadid;
Update a table row
interface Lead {
leadid?: string,
subject?: string,
fullname?: string,
jobtitle?: string
}
const request: DynamicsWebApi.UpdateRequest = {
key: "7d577253-3ef0-4a0a-bb7f-8335c2596e70",
collection: "leads",
data: {
subject: "Test update",
jobtitle: "Developer"
},
returnRepresentation: true,
select: ["fullname"]
};
const result = await dynamicsWebApi.update<Lead>(request);
const fullname = result.fullname;
Update a value in a single column
const request: DynamicsWebApi.UpdateSinglePropertyRequest = {
collection: "leads",
key: "7d577253-3ef0-4a0a-bb7f-8335c2596e70",
fieldValuePair: { subject: "Update Single" }
};
await dynamicsWebApi.updateSingleProperty(request);
Upsert a table row
const leadId = "7d577253-3ef0-4a0a-bb7f-8335c2596e70";
const request: DynamicsWebApi.UpsertRequest = {
key: leadId,
collection: "leads",
returnRepresentation: true,
select: ["fullname"],
data: {
subject: "Test upsert"
},
ifnonematch: "*"
};
const result = dynamicsWebApi.upsert<Lead>(request);
if (result != null) {
}
else {
}
Delete a table row
const request: DynamicsWebApi.DeleteRequest = {
key: leadId,
collection: "leads",
ifmatch: 'W/"470867"'
}
const isDeleted = await dynamicsWebApi.deleteRecord(request);
if (isDeleted){
}
else{
}
Delete a value in a single column
const request: DynamicsWebApi.DeleteRequest = {
key: leadId,
collection: "leads",
fieldName: "subject"
}
await dynamicsWebApi.deleteRecord(request);
Retrieve a table row
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"],
ifnonematch: 'W/"468026"',
includeAnnotations: "OData.Community.Display.V1.FormattedValue"
};
const result = await dynamicsWebApi.retrieve<Lead>(request);
Retrieve a reference to a related table row using a single-valued navigation property
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"]
}
const reference = await dynamicsWebApi.retrieve(request);
const ownerId = reference.id;
const collectionName = reference.collection;
Retrieve a related table row using a single-valued navigation property
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"]
}
const parentLead = await dynamicsWebApi.retrieve<Lead>(request);
const fullname = parentLead.fullname;
Same result can be achieved by setting request.navigationProperty
:
const request: DynamicsWebApi.RetrieveRequest = {
key: recordId,
collection: "new_tests",
navigationProperty: "new_ParentLead",
select: ["fullname", "subject"]
}
const parentLead = await dynamicsWebApi.retrieve<Lead>(request);
const fullname = parentLead.fullname;
Retrieve multiple table rows
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;
Change Tracking
const request: DynamicsWebApi.RetrieveMultipleRequest = {
collection: "leads",
select: ["fullname", "subject"],
trackChanges: true
};
const response1 = await dynamicsWebApi.retrieveMultiple<Lead>(request).then(function (response) {
const deltaLink = response1.oDataDeltaLink;
const response2 = await dynamicsWebApi.retrieveMultiple<Lead>(request, deltaLink);
Retrieve All records
The following function retrieves records and goes through all pages automatically.
const response = await dynamicsWebApi.retrieveAll<Lead>({
collection: "leads",
select: ["fullname", "subject"],
filter: "statecode eq 0"
});
const records = response.value;
Count
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"
});
Count limitation workaround
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",
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.
Associate
const accountId = "00000000-0000-0000-0000-000000000001";
const leadId = "00000000-0000-0000-0000-000000000002";
const request: DynamicsWebApi.AssociateRequest = {
collection: "accounts",
primaryKey: accountId,
relationshipName: "lead_parent_account",
relatedCollection: "leads",
relatedKey: leadId
}
await dynamicsWebApi.associate(request);
Associate for a single-valued navigation property
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);
Disassociate
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);
Disassociate for a single-valued navigation property
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);
Fetch XML Request
interface Account {
accountid?: string,
name?: string
}
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);
Paging
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);
request.pageNumber = page1.PagingInfo.nextPage;
request.pagingCookie = page1.PagingInfo.cookie;
const page2 = await dynamicsWebApi.fetch<Account>(request);
request.pageNumber = page2.PagingInfo.nextPage;
request.pagingCookie = page2.PagingInfo.cookie;
const page3 = await dynamicsWebApi.fetch<Account>(request);
Fetch All records
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
});
Execute Web API functions
Bound functions
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",
functionName: "Microsoft.Dynamics.CRM.RetrieveTeamPrivileges"
}
const result = await dynamicsWebApi.callFunction<RetrieveTeamPrivilegesResponse>(request);
Unbound functions
interface GetTimeZoneCodeByLocalizedNameResponse {
TimeZoneCode: number
}
const parameters = {
LocalizedStandardName: "Pacific Standard Time",
LocaleId: 1033
};
const request: DynamicsWebApi.UnboundFunctionRequest = {
parameters: parameters,
functionName: "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");
Execute Web API actions
Bound actions
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;
Unbound actions
interface WinOpportunityAction {
Status: number,
OpportunityClose: {
subject: string,
"opportunityid@odata.bind": string
}
}
const opportunityId = "b3828ac8-917a-e511-80d2-00155d2a68d2";
const actionRequest: DynamicsWebApi.UnboundActionRequest<WinOpportunityAction> = {
actionName: "WinOpportunity",
action: {
Status: 3,
OpportunityClose: {
subject: "Won Opportunity",
"opportunityid@odata.bind": "opportunities(" + opportunityId + ")"
}
}
}
await dynamicsWebApi.callAction(actionRequest);
Execute Batch Operations
Batch requests bundle multiple operations 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.
The following example bundles 2 retrieve multiple operations and an update:
dynamicsWebApi.startBatch();
dynamicsWebApi.retrieveMultiple({ collection: "accounts" });
dynamicsWebApi.update({
key: '00000000-0000-0000-0000-000000000002',
collection: "contacts",
data: { firstname: "Test", lastname: "Batch!" }
});
dynamicsWebApi.retrieveMultiple({ collection: "contacts" });
const responses = await dynamicsWebApi.executeBatch();
const accounts = responses[0];
const isUpdated = responses[1];
const contacts = responses[2];
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.
const order1 = {
name: "1 year membership",
"customerid_contact@odata.bind": "contacts(00000000-0000-0000-0000-000000000001)"
};
const order2 = {
name: "book",
"customerid_contact@odata.bind": "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){
alert("Cannot complete a checkout. Please try again later.");
}
Use Content-ID to reference requests in a Change Set
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];
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.
Use Content-ID inside a request payload
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",
"customerid_contact@odata.bind": "$1"
};
dynamicsWebApi.startBatch();
dynamicsWebApi.create({ data: contact, collection: "contacts", contentId: "1" });
dynamicsWebApi.create({ data: order, collection: "salesorders" });
const responses = await dynamicsWebApi.executeBatch();
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.
Limitations
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.
- The
odata.continue-on-error
preference is not supported by the Web API. Any error that occurs in the batch will stop the processing of the remainder of the batch.
You can find an official documentation that covers Web API batch requests here: Execute batch operations using the Web API.
Work with Table Definitions (Entity Metadata)
Before working with metadata read the following section from Microsoft Documentation.
Create a new Table Definition
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"
};
const entityId = await dynamicsWebApi.createEntity({ data: entityDefinition });
Retrieve Table Definitions
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";
const entityMetadata = await dynamicsWebApi.retrieveEntity({
key: entityKey,
select: ["SchemaName", "LogicalName"]
});
const schemaName = entityMetadata.SchemaName;
Update Table Definitions
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 });
entityMetadata.DispalyName.LocalizedLabels[0].Label = "New Bank Account";
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.
Retrieve Multiple Table Definitions
const response = await dynamicsWebApi.retrieveEntities({
select: ["LogicalName"],
filter: "OwnershipType eq Microsoft.Dynamics.CRM.OwnershipTypes'UserOwned'"
});
const firstLogicalName = response.value[0].LogicalName;
Create Columns
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
};
const attributeId = await dynamicsWebApi.createAttribute({
entityKey: entityKey,
data: attributeDefinition
});
Retrieve Columns
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";
const attributeKey = "00000000-0000-0000-0000-000000000002";
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;
Update Columns
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
});
attributeMetadata.DispalyName.LocalizedLabels[0].Label = "New Balance";
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
});
attributeMetadata.DispalyName.LocalizedLabels[0].Label = "New Balance";
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
Retrieve Multiple Columns
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];
Use requests to query Table and Column definitions
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 default mergeLabels: false
. - To send entity or attribute definition use
data
property.
Examples
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'"
});
entityMetadata.DisplayName.LocalizedLabels[0].Label = "Organization";
const updateRequest = {
collection: "EntityDefinitions",
key: entityMetadata.MetadataId,
mergeLabels: true,
data: entityMetadata
};
await dynamicsWebApi.update(updateRequest);
const entityMetadata2 = await dynamicsWebApi.retrieveEntity({ key: "LogicalName='account'" });
entityMetadata2.DisplayName.LocalizedLabels[0].Label = "Organization";
await dynamicsWebApi.updateEntity({
data: entityMetadata,
mergeLabels: true
});
Create Relationship
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"
}
};
const relationshipId = await dynamicsWebApi.createRelationship({ data: newRelationship });
Update Relationship
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);
Delete Relationship
const metadataId = "10cb680e-b6a7-e811-816a-480fcfe97e21";
const isDeleted = await dynamicsWebApi.deleteRelationship({ key: metadataId });
Retrieve Relationship
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"
});
Retrieve Multiple Relationships
const relationshipType = "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata";
const relationships = await dynamicsWebApi.retrieveRelationships({
castType: relationshipType,
select: ["SchemaName", "MetadataId"],
filter: "ReferencedEntity eq 'account'"
})
Use multi-table lookup columns (Polymorfic Lookup Attributes)
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 => {
});
Create Global Option Set
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"
}
};
const id = await dynamicsWebApi.createGlobalOptionSet({
data: optionSetDefinition
});
Update Global Option Set
Important! Publish your changes after update, otherwise a label won't be modified.
let key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
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);
Delete Global Option Set
let key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
key = "Name='new_customglobaloptionset'";
const isDeleted = await dynamicsWebApi.deleteGlobalOptionSet({ key: key });
Retrieve Global Option Set
const key = "6e133d25-abd1-e811-816e-480fcfeab9c1";
key = "Name='new_customglobaloptionset'";
let optionSet = await dynamicsWebApi.retrieveGlobalOptionSet(key);
optionSet = await dynamicsWebApi.retrieveGlobalOptionSet({
key: key,
select: ["DisplayName"]
});
optionSet = await dynamicsWebApi.retrieveGlobalOptionSet({
key: key,
castType: "Microsoft.Dynamics.CRM.OptionSetMetadata",
select: ["Name", "Options"]
});
Retrieve Multiple Global Option Sets
let optionSetsResponse = await dynamicsWebApi.retrieveGlobalOptionSets();
let optionSet = optionSetsResponse.value[0];
optionSetResponse = await dynamicsWebApi.retrieveGlobalOptionSets({
castType: "Microsoft.Dynamics.CRM.OptionSetMetadata",
select: ["Name", "Options"]
});
optionSet = optionSetResponse.value[0];
Retrieve CSDL $metadata document
To retrieve a CSDL $metadata document use the following:
const request: DynamicsWebApi.CsdlMetadataRequest = {
addAnnotations: false;
}
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.
Formatted Values and Lookup Columns
Starting from version 1.3.0 it became easier to access formatted values for properties and lookup data in response objects.
DynamicsWebApi automatically creates aliases for each property that contains a formatted value or lookup data.
For example:
let doNotPostEmailFormatted = response['donotpostalmail@OData.Community.Display.V1.FormattedValue'];
doNotPostEmailFormatted = response.donotpostalmail_Formatted;
let customerName = response['_customerid_value@OData.Community.Display.V1.FormattedValue'];
let customerEntityLogicalName = response['_customerid_value@Microsoft.Dynamics.CRM.lookuplogicalname'];
let customerNavigationProperty = response['_customerid_value@Microsoft.Dynamics.CRM.associatednavigationproperty'];
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 |
Using Alternate Keys
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);
Making requests using Entity Logical Names
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 });
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",
"primarycontactid@odata.bind": "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:
const collectionName = dynamicsWebApi.Utility.getCollectionName("account");
Please note, everything said above will happen only if you set useEntityNames: true
in the DynamicsWebApi config.
Work with File Fields
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
Upload file
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",
fieldName: "dwa_file",
fileName: fileName,
data: fileData
}).then(function(){
}).catch (function (error) {
});
}
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",
fieldName: "dwa_file",
fileName: filename
data: data,
}).then(function() {
}).catch(function (error) {
});
});
Download file
const donwloadResponse = await dynamicsWebApi.downloadFile({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
fieldName: "dwa_file"
});
const fileBinary = donwloadResponse.data;
const fileName = donwloadResponse.fileName;
const fileSize = donwloadResponse.fileSize;
Delete file
const isDeleted = await dynamicsWebApi.deleteRecord({
collection: "dwa_filestorages",
key: "00000000-0000-0000-0000-000000000001",
fieldName: "dwa_file"
});
Work with Dataverse Search API
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: "2.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.
Search
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>"
}
});
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;
Suggest
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"
}
});
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'"
}
});
Autocomplete
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"
}
});
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'"
}
});
Abort Request
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(() => somethingHappenedMustAbort(), 200);
try {
const result = await dynamicsWebApi.retrieveAll({
collection: "contacts",
select: ["firstname", "lastname"],
signal: controller.signal
});
}
catch(error){
if (error.name === "AbortError") {
}
}
Using Proxy
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
or https_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: {
username: "john",
password: "doe"
}
}
});
Using TypeScript Declaration Files
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
Node.Js
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, Config } from "dynamics-web-api";
Dynamics 365 web resource
If you are developing CRM Web Resources with TypeScript, you will need to download dynamics-web-api.d.ts
file manually from dist folder.
In my web resources project I usually put a declaration file under "./types/" folder. 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/**/*"
]
In Progress / Feature List
Many more features to come!
Thank you for your patience and for using DynamcisWebApi!
Contributions
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!