Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects

August 8, 2009:  Added new version of JavaScript web service helper objects

 

When doing web service calls from JavaScript you are required to write code that concatenates XML strings and manually posts an HTTP request to CRM. The code looks something like this (courtesy of CRM 4.0 SDK).

Traditional Web Service Soap Request
var xml = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
authenticationHeader+
"<soap:Body>"+
"<RetrieveMultiple xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<query xmlns:q1='http://schemas.microsoft.com/crm/2006/Query'"+
" xsi:type='q1:QueryExpression'>"+
"<q1:EntityName>contact</q1:EntityName>"+
"<q1:ColumnSet xsi:type='q1:ColumnSet'>"+
"<q1:Attributes>"+
"<q1:Attribute>fullname</q1:Attribute>"+
"<q1:Attribute>contactid</q1:Attribute>"+
"</q1:Attributes>"+
"</q1:ColumnSet>"+
"<q1:Distinct>false</q1:Distinct>"+
"<q1:Criteria>"+
"<q1:FilterOperator>And</q1:FilterOperator>"+
"<q1:Conditions>"+
"<q1:Condition>"+
"<q1:AttributeName>address1_city</q1:AttributeName>"+
"<q1:Operator>Like</q1:Operator>"+
"<q1:Values>"+
"<q1:Value xsi:type='xsd:string'>"+searchCity+"</q1:Value>"+
"</q1:Values>"+
"</q1:Condition>"+
"</q1:Conditions>"+
"</q1:Criteria>"+
"</query>"+
"</RetrieveMultiple>"+
"</soap:Body>"+
"</soap:Envelope>";

This code is only the SOAP body generation.  It doesn't even include the HTTP request or extracting the data.  You can imagine what a maintenance nightmare you have when your form requires multiple web service calls.  Wouldn't it be nice if the API was closer to the .NET SDK API?

Helper Objects
I have written a couple of helper classes to help make life easier. Here are two examples of what you can do with the helper classes. It doesn't currently support everything (grouping multiple levels of filter conditions), but it handles 90% of the cases you run into. As I add more functionality to the clases, I will repost them. 

Note:  The blog likes to reformat the code.  You can download a code file below. 

Simple query on one entity
This query does a simple select of leads where the city is either Bloomington or Minneapolis.   Include this code as well as the helper objects from the bottom of the post.
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_EQUAL = "Equal";

// Create object passing in the entity you are selecting from     
var crmService = new CrmService("lead", LOGICAL_OPERATOR_OR);
crmService.AddColumn("fullname");
crmService.AddColumn("leadid");

// Add filter conditions  (note:  the "OR" logical operator was specified in constructor)
crmService.AddFilterCondition("address1_city", "Bloomington", CONDITION_OPERATOR_EQUAL);
crmService.AddFilterCondition("address1_city", "Minneapolis", CONDITION_OPERATOR_EQUAL);

// Retrieve the result object
var result = crmService.RetrieveMultiple();

// Loop through rows and select values (they return strings)
for (rowsNumber in result.Rows) {
   var row = result.Rows[rowsNumber];
   // Get Column By Name
   alert(row.GetValue("fullname"));
   alert(row.GetValue("leadid"));
}

Query that Links in Multiple Tables
This selects accountnumber, accountid, and name from the account entity by specifying the ID of the contact.  Include this code as well as the helper objects from the bottom of the post.
var LOGICAL_OPERATOR_AND = "And";
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_EQUAL = "Equal";
var JOINOPERATOR_INNER = "Inner";

// Create object passing in the entity you are selecting from      
var crmService = new CrmService("account", LOGICAL_OPERATOR_OR);

// Specify select columns
crmService.AddColumn("accountnumber");
crmService.AddColumn("accountid");
crmService.AddColumn("name");

// Define linked entity - similar to SDK overload
var entityLinked = crmService.AddLinkedEntityCondition("account", "contact", "accountid", "parentcustomerid", JOINOPERATOR_INNER)

// Set filter operator (AND, OR, Ect)
entityLinked.FilterOperator = LOGICAL_OPERATOR_AND;

// Add filter condition (can add as multiple)
entityLinked.AddFilterCondition("contactid", "{BB1F590A-37D0-DC11-AA32-0003FF33509E}", CONDITION_OPERATOR_EQUAL);

// Retrieve the result object
var result = crmService.RetrieveMultiple();

// Loop through rows and select values (they return strings)
for (rowsNumber in result.Rows) {
   var row = result.Rows[rowsNumber];
   // Get Column By Name
   alert(row.GetValue("accountnumber"));
   alert(row.GetValue("name"));
   alert(row.GetValue("accountid"));
}

Helper Objects - Simply copy into the top of your form load
var LOGICAL_OPERATOR_AND = "And";
var LOGICAL_OPERATOR_OR = "Or";
var CONDITION_OPERATOR_LIKE = "Like";
var CONDITION_OPERATOR_EQUAL = "Equal";
var CONDITION_OPERATORNOT_EQUAL = "NotEqual";
var JOINOPERATOR_INNER = "Inner";
var JOINOPERATOR_LEFTOUTER = "LeftOuter";
var JOINOPERATOR_NATURAL = "Natural";


function CrmService(entityName, logicalOperator) {
    // Double check in case you pass a variable that hasn't been set
    // This error is hard to track down
    if (logicalOperator == null)
        throw new Error("Must specify non-null value for logicalOperator");

    if (entityName == null)
        throw new Error("Must specify non-null value for entityName");
    this.entityName = entityName;
    this.ColumnSet = new Array();
    this.LogicalOperator = logicalOperator;
    this.Conditions = new Array();
    this.LinkedEntities = new Array();
}


CrmService.prototype.getEntityName = function() {
    return this.entityName;
}

function Condition(field, value, operator) {
    this.Field = field;
    this.Value = CrmEncodeDecode.CrmXmlEncode(value);
    // Double check in case you pass a variable that hasn't been set
    // This error is hear to track down
    if (operator == null)
        throw new Error("Must specify non-null value for operator");
    this.Operator = operator;
}

CrmService.prototype.setEntityName = function() {
    return this.entityName;
}

CrmService.prototype.AddColumn = function(columnName) {
    this.ColumnSet[this.ColumnSet.length] = columnName;
}

CrmService.prototype.AddFilterCondition = function(field, value, conditionOperator) {
    this.Conditions[this.Conditions.length] = new Condition(field, value, conditionOperator);
}


function LinkedEntity(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator) {
    this.LinkFromEntityName = linkFromEntityName;
    this.LinkToEntityName = linkToEntityName;
    this.LinkFromAttributeName = linkFromAttributeName;
    this.LinkToAttributeName = linkToAttributeName;
    if (joinOperator == null)
        throw new Error("Must specify non-null value for operator");
    this.JoinOperator = joinOperator;
    this.Conditions = new Array();
    this.FilterOperator = LOGICAL_OPERATOR_AND;
}

LinkedEntity.prototype.AddFilterCondition = function(field, value, conditionOperator) {
    this.Conditions[this.Conditions.length] = new Condition(field, value, conditionOperator);
    return this.Conditions[this.Conditions.length - 1];
}

CrmService.prototype.AddLinkedEntityCondition = function(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator) {
    this.LinkedEntities[this.LinkedEntities.length] = new LinkedEntity(linkFromEntityName, linkToEntityName, linkFromAttributeName, linkToAttributeName, joinOperator);
    return this.LinkedEntities[this.LinkedEntities.length - 1];
}

function RetrieveMultipleResult(crmService) {
    this.Rows = new Array();
    this.CrmService = crmService;
}


RetrieveMultipleResult.prototype.AddRow = function() {
    this.Rows[this.Rows.length] = new Row();
    return this.Rows[this.Rows.length - 1];
}

 

function Row() {
    this.Columns = new Array();
}

function Column(columnName, value, dataType) {
    this.ColumnName = columnName;
    this.Value = value;
    this.DataType = dataType;
}

Row.prototype.AddColumn = function(columnName, value) {
    this.Columns[this.Columns.length] = new Column(columnName, value);
}

Row.prototype.GetColumn = function(columnName) {
    for (columnNumber in this.Columns) {
        var column = this.Columns[columnNumber];
        if (columnName.toLowerCase() == column.ColumnName.toLowerCase())
            return column;
    }
    throw new Error("Column " + columnName + " does not exist");
}

Row.prototype.GetValue = function(columnName) {
    var column = this.GetColumn(columnName);
    return column.Value;
}


CrmService.prototype.RetrieveMultiple = function() {

    //create SOAP envelope
    var xmlSoapHeader = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">";

    var xmlAuthHeader = GenerateAuthenticationHeader();

    var xmlSoapBody = "<soap:Body>" +
 "      <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">  " +
 "<query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\"q1:QueryExpression\">  " +
 "  <q1:EntityName>" + this.getEntityName() + "</q1:EntityName>  " +
 "  <q1:ColumnSet xsi:type=\"q1:ColumnSet\">  " +
 "    <q1:Attributes>  ";

    for (columnNumber in this.ColumnSet) {
        var column = this.ColumnSet[columnNumber];
        xmlSoapBody = xmlSoapBody + "          <q1:Attribute>" + column + "</q1:Attribute>";
    }

    xmlSoapBody = xmlSoapBody + "        </q1:Attributes>" +
 "      </q1:ColumnSet>" +
 "          <q1:Distinct>false</q1:Distinct>  " +
 "          <q1:PageInfo>  " +
 "            <q1:PageNumber>0</q1:PageNumber>  " +
 "            <q1:Count>0</q1:Count>  " +
 "          </q1:PageInfo>  " +
 "         <q1:LinkEntities>";

    if (this.LinkedEntities.length > 0) {
        for (linkedEntityNumber in this.LinkedEntities) {
            var linkedEntity = this.LinkedEntities[linkedEntityNumber];
            xmlSoapBody += " <q1:LinkEntity> ";
            xmlSoapBody += "                 <q1:LinkFromAttributeName>" + linkedEntity.LinkFromAttributeName + "</q1:LinkFromAttributeName> ";
            xmlSoapBody += "                 <q1:LinkFromEntityName>" + linkedEntity.LinkFromEntityName + "</q1:LinkFromEntityName> ";
            xmlSoapBody += "                 <q1:LinkToEntityName>" + linkedEntity.LinkToEntityName + "</q1:LinkToEntityName> ";
            xmlSoapBody += "<q1:LinkToAttributeName>" + linkedEntity.LinkToAttributeName + "</q1:LinkToAttributeName> ";
            xmlSoapBody += "<q1:JoinOperator>" + linkedEntity.JoinOperator + "</q1:JoinOperator> ";
            xmlSoapBody += "<q1:LinkCriteria> ";

            if (linkedEntity.FilterOperator == null)
                throw new Error("Must specify non-null value for FilterOperator");

            xmlSoapBody += " <q1:FilterOperator>" + linkedEntity.FilterOperator + "</q1:FilterOperator> ";
            xmlSoapBody += " <q1:Conditions> ";

            for (conditionLinkedNumber in linkedEntity.Conditions) {
                var conditionLinked = linkedEntity.Conditions[conditionLinkedNumber];
                xmlSoapBody += "                             <q1:Condition> ";
                xmlSoapBody += "                                             <q1:AttributeName>" + conditionLinked.Field + "</q1:AttributeName> ";
                xmlSoapBody += "                                             <q1:Operator>" + conditionLinked.Operator + "</q1:Operator> ";
                xmlSoapBody += "                                             <q1:Values> ";
                xmlSoapBody += "                                                             <q1:Value xsi:type=\"xsd:string\">" + conditionLinked.Value + "</q1:Value> ";
                xmlSoapBody += "                                             </q1:Values> ";
                xmlSoapBody += "                             </q1:Condition> ";
            }
            xmlSoapBody += " </q1:Conditions> ";
            xmlSoapBody += " <q1:Filters /> ";
            xmlSoapBody += "</q1:LinkCriteria> ";
            xmlSoapBody += "<q1:LinkEntities />";
            xmlSoapBody += "</q1:LinkEntity>";
        }
    }

    if (this.LogicalOperator == null)
        throw new Error("Must specify non-null value for LogicalOperator");


    xmlSoapBody += "</q1:LinkEntities>" +
 "          <q1:Criteria>  " +
 "            <q1:FilterOperator>" + this.LogicalOperator + "</q1:FilterOperator>  " +
 "            <q1:Conditions>  ";

 

    for (conditionNumber in this.Conditions) {
        var condition = this.Conditions[conditionNumber];

        if (condition.Operator == null)
            throw new Error("Must specify non-null value for condition Operator");

        xmlSoapBody += "              <q1:Condition>  " +
                "                <q1:AttributeName>" + condition.Field + "</q1:AttributeName>  " +
                "                <q1:Operator>" + condition.Operator + "</q1:Operator>  " +
                "                <q1:Values>  " +
                "                  <q1:Value xsi:type=\"xsd:string\">" + condition.Value + "</q1:Value>  " +
                "                </q1:Values>  " +
                "              </q1:Condition>  ";

    }

 


    xmlSoapBody += "            </q1:Conditions>  " +
 "            <q1:Filters />  " +
 "          </q1:Criteria>  " +
 "          <q1:Orders />  " +
 "        </query>  " +
 "      </RetrieveMultiple>  " +
 "    </soap:Body> " +
 "   </soap:Envelope>";


    var xmlt = xmlSoapHeader + xmlAuthHeader + xmlSoapBody;
    var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
    xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
    xmlHttpRequest.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");
    xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlHttpRequest.setRequestHeader("Content-Length", xmlt.length);
    xmlHttpRequest.send(xmlt);

    if (xmlHttpRequest.responseXML == null || xmlHttpRequest.responseXML.xml == null || xmlHttpRequest.responseXML.xml == "") {
        if (xmlHttpRequest.responseText != null && xmlHttpRequest.responseText != "")
            throw new Error(xmlHttpRequest.responseText);
        else
            throw new Error("Error returning response");
    }

    var xmlResponse = xmlHttpRequest.responseXML.xml;
    if (xmlHttpRequest.responseXML.documentElement.selectNodes("//error/description").length > 0) {
        throw new Error(xmlResponse);
    }

    var objNodeList = xmlHttpRequest.responseXML.documentElement.selectNodes("//BusinessEntity");


    var totalNodesCount = objNodeList.length;

    var result = new RetrieveMultipleResult(this);

    var nodeIndex = 0;
    var fieldTextTemp = "";
    var fieldText = "";
    if (totalNodesCount > 0) {
        do {

            var row = result.AddRow();
            for (columnNumber in this.ColumnSet) {
                var columnName = this.ColumnSet[columnNumber];
                fieldText = "";
                var valueNode = objNodeList[nodeIndex].getElementsByTagName("q1:" + columnName)[0];
                if (valueNode != null) {
                    fieldTextTemp = valueNode.childNodes[0].nodeValue;
                    if (fieldTextTemp != null && fieldTextTemp != "") {
                        fieldText = fieldText + fieldTextTemp;
                    }
                }
                row.AddColumn(columnName, fieldText);
            }
            nodeIndex = nodeIndex + 1;
        }
        while (totalNodesCount > nodeIndex)
    }
    return result;
}

 


Posted 02-01-2009 6:13 PM by Andrew Zimmer
Filed under: ,

Comments

vbullinger wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 02-02-2009 12:42 AM

This is a really good concept, Andrew, and I think you should see it through to completion.  Make this completely like the .Net SDK.

You should make all of those "var"s into enumerations.  Contrary to popular belief, you CAN create enumerators in BLOCKED SCRIPT

var LogicalOperator = { And:"And", Or:"Or" };

alert(LogicalOperator.And);

Now you can use LogicalOperator, just like in .Net :)

... wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-04-2009 3:28 AM

Gute Arbeit hier! Gute Inhalte.

Andrew Zimmer wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-04-2009 10:43 PM

Thank you for the complement.  I will admit that I had to translate it to English - Good work here! Good content.

kuldeep Shige wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-24-2009 4:27 AM

hey man very helpful content for me

thanks a lot

dpacker122 wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 05-21-2009 3:02 PM

Andrew - Thanks for the post - your method is a LOT more efficient!

I'm wondering: Have you found a way to make this work for a third or fourth linked entity?

Andrew Zimmer wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 08-08-2009 2:59 PM

I haven't added in the ability add more than two levels of linked entities but I did add order-bys and not null checks.  

blogs.inetium.com/.../dynamics-crm-4-0-javascript-web-service-helper-objects-part-ii.aspx

One of these days I'll add the ability to do more linked entities.

virtualme123 wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-26-2010 5:22 AM

Actually didn't know if you could help as I am creating entity records with SOAP quite well but I've hit a hitch with the handling of decimals. I am adding an InvoiceDetail line and when using the priceperunit column I am using a float of up to 5 decimal places, however when using create it rounds everything up to 2 decmial places. Not sure if I am missing something from my attribute tag like type=decimal or something ... any ideas much appreciated.

Thanks

Andrew Zimmer wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-26-2010 7:11 AM

Send me over your code and I'll take a look.  Email me at:  azimmer@inetium.com

Andrew Zimmer wrote re: Microsoft Dynamics CRM 4.0 - JavaScript Web Service Helper Objects
on 03-26-2010 10:23 PM

Let me know if this helps.  If not, I’ll dig in further.

Go to System Settings and change the pricingdecimalprecision to 4.

groups.google.com/.../06e1b0c5fe59bca1

Inetium, LLC. Site Information