CRUD operation on Single Page Application for D365 using MSAL with jqGrid

We are re-creating our SPA but using MSAL instead of ADAL. MSAL allow authentication using wider set of identities instead of just Azure AD as for ADAL. The list of differences can be found here.

Below is the sample code for performing CRUD operation in jqGrid for D365 using MSAL.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/css/ui.jqgrid.min.css">
    <a href="https://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.js">https://alcdn.msauth.net/browser/2.0.0-beta.4/js/msal-browser.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js">https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js">https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js</a>
    <script type="text/javascript">
        const msalConfig = {
            auth: {
                clientId: "xxxxxxxxx-f8d4-448b-ba96-e3097cd7c3dd",
                authority: "https://login.microsoftonline.com/common",
                redirectUri: "https://localhost:44308/SinglePageAppMSAL.html"
            },
            cache: {
                cacheLocation: "sessionStorage", // This configures where your cache will be stored
                storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
            },
            system: {
                loggerOptions: {
                    loggerCallback: (level, message, containsPii) => {
                        if (containsPii) {
                            return;
                        }
                        switch (level) {
                            case msal.LogLevel.Error:
                                console.error(message);
                                return;
                            case msal.LogLevel.Info:
                                console.info(message);
                                return;
                            case msal.LogLevel.Verbose:
                                console.debug(message);
                                return;
                            case msal.LogLevel.Warning:
                                console.warn(message);
                                return;
                        }
                    }
                }
            }
        };

        const loginRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"]
        };


        const tokenRequest = {
            scopes: ["https://xxxxxxxxxxxxx.crm.dynamics.com/.default"],
            forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
        };


        const myMSALObj = new msal.PublicClientApplication(msalConfig);
        let accessToken;
        let username = "";

        myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
            console.error(err);
        });

        function handleResponse(resp) {
            if (resp !== null) {
                username = resp.account.username;
                loginButton.style.display = "none";
                logoutButton.style.display = "block";
                getAccountsButton.style.display = "block";

                var helloMessage = document.createElement("p");
                helloMessage.textContent = "Hello " + username;
                message.appendChild(helloMessage);
            }
            else {
                const currentAccounts = myMSALObj.getAllAccounts();
                if (currentAccounts === null) {

                    return;
                } else if (currentAccounts.length > 1) {
                    // Add choose account code here
                    console.warn("Multiple accounts detected.");
                } else if (currentAccounts.length === 1) {
                    username = currentAccounts[0].username;
                }
            }
        }

        function signIn() {
            myMSALObj.loginRedirect(loginRequest);            
        }

        function signOut() {
            const logoutRequest = {
                account: myMSALObj.getAccountByUsername(username)
            };
            myMSALObj.logout(logoutRequest);
        }

        function getTokenRedirect(request) {
            request.account = myMSALObj.getAccountByUsername(username);
            return myMSALObj.acquireTokenSilent(request).catch(error => {
                console.warn("silent token acquisition fails. acquiring token using redirect");
                if (error instanceof msal.InteractionRequiredAuthError) {
                    return myMSALObj.acquireTokenRedirect(request);
                } else {
                    console.warn(error);
                }
            });
        }

        function readAccount() {
            getTokenRedirect(tokenRequest).then(response => {
                retrieveAccounts(response.accessToken);
            }).catch(error => {
                console.error(error);
            });
        }

        function renderAccount(accounts, token) {
            $("#grid").clearGridData();
            var listOfAccounts = [];
            accounts.forEach(function (account) {
                var name = account.name;
                var city = account.address1_city;
                var accountid = account.accountid;
                listOfAccounts.push({
                    name: name,
                    city: city,
                    accountid: accountid
                });
            });
            $("#grid").jqGrid({
                colModel: [
                    { name: "name", width: 300, editable: true },
                    { name: "city", width: 300, editable: true },
                    { name: "accountid", width: 300, editable: true, editrules: { edithidden: false }, hidden: true },
                ],
                pager: '#pager2',
                sortname: 'name',
                viewrecords: true,
                sortorder: "desc",
                caption: "",
                editable: true,
                data: listOfAccounts
            });
            $("#grid").setGridParam({ data: listOfAccounts }).trigger("reloadGrid");
            $("#grid").jqGrid('navGrid', '#pager2',
                {
                    edit: true,
                    add: true,
                    del: true,
                    search: false,
                    refresh: false,
                    view: false
                },
                {
                    editCaption: "Edit Account",
                    edittext: "Edit",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        if (confirm('Are you sure you want to update this row?')) {
                            UpdateAccount(postdata, token);
                            return [true, ''];
                        } else {
                            return [false, 'You can not submit!'];
                        }
                    }
                },
                {
                    addCaption: "Add Account",
                    addtext: "Add Account",
                    closeAfterAdd: true,
                    recreateForm: true,
                    errorTextFormat: commonError,
                    onclickSubmit: function (response, postdata) {
                        postdata.accountid = AddAccount(postdata, token);
                    }
                },
                {
                    deleteCaption: "delete Account",
                    deletetext: "Delete Account",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        var sr = jQuery("#grid").getGridParam('selrow');
                        var rowdata = jQuery("#grid").getRowData(sr);
                        DeleteAccount(rowdata, token);
                    }
                },
                {},
            );
        }

        function retrieveAccounts(token) {
            if (!token) {
                errorMessage.textContent = 'error occurred: ';
                return;
            }
            
            var req = new XMLHttpRequest()
            req.open("GET", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts?$select=name,address1_city,accountid&$top=10"), true);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.onreadystatechange = function () {
                if (this.readyState == 4 /* complete */) {
                    req.onreadystatechange = null;
                    if (this.status == 200) {
                        var account = JSON.parse(this.response).value;
                        renderAccount(account, token);
                    }
                    else {
                        var error = JSON.parse(this.response).error;
                        console.log(error.message);
                        errorMessage.textContent = error.message;
                    }
                }
            };
            req.send();
        }


        function UpdateAccount(params, token) {
            var req = new XMLHttpRequest();
            req.open("PATCH", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            var body = JSON.stringify({
                "name": params.name,
                "address1_city": params.city
            });
            req.send(body);
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Updated");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function AddAccount(params, token) {
            var account = {};
            account["name"] = params.name;
            account["address1_city"] = params.city;
            var req = new XMLHttpRequest();
            req.open("POST", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.send(JSON.stringify(account));
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Created");
                    return accountid;
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function DeleteAccount(params, token) {
            var req = new XMLHttpRequest();
            req.open("DELETE", encodeURI("https://xxxxxxxxxxxxx.crm.dynamics.com/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.send();
            if (req.readyState === 4) {
                if (req.status === 204) {
                    alert(params.name + " Deleted");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function commonError(data) {
            return "Error Occured during Operation. Please try again";
        }
        document.onreadystatechange = function () {
            if (document.readyState == "complete") {

                //Set DOM elements referenced by scripts
                message = document.getElementById("message");
                errorMessage = document.getElementById("errorMessage");
                loginButton = document.getElementById("login");
                logoutButton = document.getElementById("logout");
                getAccountsButton = document.getElementById("getAccounts");

                //Event handlers on DOM elements
                loginButton.addEventListener("click", signIn);
                logoutButton.addEventListener("click", signOut);
                getAccountsButton.addEventListener("click", readAccount);

                if (username) {
                    loginButton.style.display = "none";
                    logoutButton.style.display = "block";
                    getAccountsButton.style.display = "block";

                    var helloMessage = document.createElement("p");
                    helloMessage.textContent = "Hello " + username;
                    message.appendChild(helloMessage)

                }
                else {
                    loginButton.style.display = "block";
                    logoutButton.style.display = "none";
                    getAccountsButton.style.display = "none";
                }
            }
        }
    </script>
    <style>
        body {
            font-family: 'Segoe UI';
        }

        table {
            border-collapse: collapse;
        }

        td, th {
            border: 1px solid black;
        }

        #errorMessage {
            color: red;
        }

        #message {
            color: green;
        }
    </style>
</head>
<body>
    <button id="login">Login</button>
    <button id="logout" style="display:none;">Logout</button>
    <button id="getAccounts" style="display:none;">Get Accounts</button>
    <div id="errorMessage"></div>
    <div id="message"></div>
    <table id="grid"></table>
    <div id="pager2"></div>
</body>
</html>

Single Page Application(SPA) – CRUD operation in D365 using jqGrid

Below is the sample code to create a SPA to perform CRUD operation on D365 using WebAPI and ADAL for authentication. The code demonstrate use of GET, POST, PATCH and DELETE for Retrieve, Create, Update and Delete respectively. The sample code provided from Microsoft is used here as base and additional feature are added on top of it.

<!DOCTYPE html>
<html>
<head>
    <title>D365 Single Page Application for D365 using jqGrid</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/css/ui.jqgrid.min.css">
    <a href="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js">https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js">https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js</a>
    <a href="https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js">https://cdnjs.cloudflare.com/ajax/libs/free-jqgrid/4.15.4/jquery.jqgrid.min.js</a>

    <script type="text/javascript">
        "use strict";
        var organizationURI = "https://xxxx.crm.dynamics.com"; //The URL of your Dataverse organization
        var tenant = "xxxx.onmicrosoft.com"; //The name of the Azure AD organization you use
        var clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; //The ClientId you got when you registered the application
        var pageUrl = "https://localhost:44347/SHtmlPage.html"; //The URL of this page in your development environment when debugging.

        var user, authContext, message, errorMessage, loginButton, logoutButton, getAccountsButton;

        //Configuration data for AuthenticationContext
        var endpoints = {
            orgUri: organizationURI
        };

        window.config = {
            tenant: tenant,
            clientId: clientId,
            postLogoutRedirectUri: pageUrl,
            endpoints: endpoints,
            cacheLocation: 'localStorage',
        };

        document.onreadystatechange = function () {
            if (document.readyState == "complete") {

                //Set DOM elements referenced by scripts
                message = document.getElementById("message");
                errorMessage = document.getElementById("errorMessage");
                loginButton = document.getElementById("login");
                logoutButton = document.getElementById("logout");
                getAccountsButton = document.getElementById("getAccounts");

                //Event handlers on DOM elements
                loginButton.addEventListener("click", login);
                logoutButton.addEventListener("click", logout);
                getAccountsButton.addEventListener("click", getAccounts);

                //call authentication function
                authenticate();

                if (user) {
                    loginButton.style.display = "none";
                    logoutButton.style.display = "block";
                    getAccountsButton.style.display = "block";

                    var helloMessage = document.createElement("p");
                    helloMessage.textContent = "Hello " + user.profile.name;
                    message.appendChild(helloMessage)

                }
                else {
                    loginButton.style.display = "block";
                    logoutButton.style.display = "none";
                    getAccountsButton.style.display = "none";
                }

            }
        }

        // Function that manages authentication
        function authenticate() {
            //OAuth context
            authContext = new AuthenticationContext(config);

            // Check For & Handle Redirect From AAD After Login
            var isCallback = authContext.isCallback(window.location.hash);
            if (isCallback) {
                authContext.handleWindowCallback();
            }
            var loginError = authContext.getLoginError();

            if (isCallback && !loginError) {
                window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
            }
            else {
                errorMessage.textContent = loginError;
            }
            user = authContext.getCachedUser();

        }

        //function that logs in the user
        function login() {
            authContext.login();
        }
        //function that logs out the user
        function logout() {
            authContext.logOut();
        }

        //function that initiates retrieval of accounts
        function getAccounts() {
            getAccountsButton.disabled = true;
            var retrievingAccountsMessage = document.createElement("p");
            retrievingAccountsMessage.textContent = "Retrieving 10 accounts from " + organizationURI + "/api/data/v9.1/accounts";
            message.appendChild(retrievingAccountsMessage)

            // Function to perform operation is passed as a parameter to the acquireToken method
            authContext.acquireToken(organizationURI, retrieveAccounts)
        }

        //Function that actually retrieves the accounts
        

        function renderAccounts(accounts, token) {
            var listOfAccounts = [];
            accounts.forEach(function (account) {
                var name = account.name;
                var city = account.address1_city;
                var accountid = account.accountid;
                listOfAccounts.push({
                    name: name,
                    city: city,
                    accountid: accountid
                });
            });

            $("#grid").jqGrid({
                colModel: [
                    { name: "name", width: 300, editable: true},
                    { name: "city", width: 300, editable: true },
                    { name: "accountid", width: 300, editable: true, editrules: { edithidden: false }, hidden: true },
                ],
                pager: '#pager2',
                sortname: 'name',
                viewrecords: true,
                sortorder: "desc",
                caption: "",
                editable: true,
                data: listOfAccounts
            });
            $("#grid").jqGrid('navGrid', '#pager2',
                {
                    edit: true,
                    add: true,
                    del: true,
                    search: false,
                    refresh: false,
                    view: false
                },
                {
                    editCaption: "Edit Account",
                    edittext: "Edit",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        if (confirm('Are you sure you want to update this row?')) {
                            UpdateAccount(postdata, token);
                            return [true, ''];
                        } else {
                            return [false, 'You can not submit!'];
                        }
                    }
                },
                //add Options. save key parameter will keybind the Enter key to submit.
                {
                    addCaption: "Add Account",
                    addtext: "Add Account",
                    closeAfterAdd: true,
                    recreateForm: true,
                    errorTextFormat: commonError,
                    onclickSubmit: function (response, postdata) {
                        postdata.accountid = AddAccount(postdata, token);
                    }
                },
                {
                    deleteCaption: "delete Account",
                    deletetext: "Delete Account",
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    savekey: [true, 13],
                    errorTextFormat: commonError,
                    reloadAfterSubmit: true,
                    onclickSubmit: function (response, postdata) {
                        var sr = jQuery("#grid").getGridParam('selrow');
                        var rowdata = jQuery("#grid").getRowData(sr);
                        DeleteAccount(rowdata, token);
                    }
                },
                {},
            );
        }
        
        function retrieveAccounts(error, token) {
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }

            var req = new XMLHttpRequest()
            req.open("GET", encodeURI(organizationURI + "/api/data/v9.1/accounts?$select=name,address1_city,accountid&$top=10"), true);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.onreadystatechange = function () {
                if (this.readyState == 4 /* complete */) {
                    req.onreadystatechange = null;
                    if (this.status == 200) {
                        var accounts = JSON.parse(this.response).value;
                        renderAccounts(accounts, token);
                    }
                    else {
                        var error = JSON.parse(this.response).error;
                        console.log(error.message);
                        errorMessage.textContent = error.message;
                    }
                }
            };
            req.send();
        }

        function UpdateAccount(params, token) {    
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var req = new XMLHttpRequest();
            req.open("PATCH", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            var body = JSON.stringify({
                "name": params.name,
                "address1_city": params.city
            });
            req.send(body);
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Updated");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function AddAccount(params, token) {
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var account = {};
            account["name"] = params.name;
            account["address1_city"] = params.city;
            var req = new XMLHttpRequest();
            req.open("POST", encodeURI(organizationURI + "/api/data/v9.1/accounts"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.send(JSON.stringify(account));
            if (req.readyState === 4) {
                if (req.status === 204) {
                    var uri = req.getResponseHeader("OData-EntityId");
                    var regExp = /\(([^)]+)\)/;
                    var matches = regExp.exec(uri);
                    var accountid = matches[1];
                    alert(params.name + " Created");
                    return accountid;
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function DeleteAccount(params, token) {
            // Handle ADAL Errors.
            if (error || !token) {
                errorMessage.textContent = 'ADAL error occurred: ' + error;
                return;
            }
            var req = new XMLHttpRequest();
            req.open("DELETE", encodeURI(organizationURI + "/api/data/v9.1/accounts(" + params.accountid + ")"), false);
            req.setRequestHeader("Authorization", "Bearer " + token);
            req.setRequestHeader("Accept", "application/json");
            req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            req.setRequestHeader("OData-MaxVersion", "4.0");
            req.setRequestHeader("OData-Version", "4.0");
            req.send();
            if (req.readyState === 4) {
                if (req.status === 204) {
                    alert(params.name + " Deleted");
                }
                else {
                    var error = JSON.parse(req.response).error;
                    alert(error.message);
                }
            }
        }

        function commonError(data) {
            return "Error Occured during Operation. Please try again";
        }

    </script>
    <style>
        body {
            font-family: 'Segoe UI';
        }

        table {
            border-collapse: collapse;
        }

        td, th {
            border: 1px solid black;
        }

        #errorMessage {
            color: red;
        }

        #message {
            color: green;
        }
    </style>
</head>
<body>
    <button id="login">Login</button>
    <button id="logout" style="display:none;">Logout</button>
    <button id="getAccounts" style="display:none;">Get Accounts</button>
    <div id="errorMessage"></div>
    <div id="message"></div>   
    <table id="grid"></table>
    <div id="pager2"></div>
 </body>
</html>

%d bloggers like this: