How to turn synchronous API_DoQuery calls into asynchronous calls

  • 0
  • 1
  • Question
  • Updated 3 years ago
  • Answered

I have the following HTML which runs successfully, allowing me to build a dataset that represents all "System Lines" included in a "Quote".

FYI, the relationship is:

Quotes -< Quote Lines >- Systems -< System Lines

I am currently outputting a simple HTML table (which I will tidy up later).

My issue is that "Synchronous XMLHttpRequest ... is deprecated" and I worry my code will not survive into the future if I leave both the "XMLHttpRequest" calls and the $.ajax request as synchronous...

What would be the thought process of changing this to asynchronous calls?

////////////////// CODE BELOW //////////////////

<!DOCTYPE html>

<html>


<head>

    https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js">


    <script>

        // ---------------------------------------------------------------------------------------------

        // This script will allow you to look through a many-to-many relationship to build a table from child records of a different related Master

        // In particular, this will produce a list of records from the following database relationship chain

        // Master1 ==> Child1 <== Master2 ==> Child2

        // By providing the Record ID of "Master1" it is possible to find all related "Child2" records and consolidate them into a single dataset

        // The data ignores duplicates and returns multiple records of Child2 even if the same Master2 record is used multiple times in different Child1 records

        // ---------------------------------------------------------------------------------------------


        // PARAMETERS


        //Master1 parameters

        var master1dbid = ""; // holds the dbID of Master1.This is obtained from the URL

        var master1rid = ""; // holds the ID of the Master1 record being used for this query


        //Child1 parameters

        var child1dbid = "xxxxxxxxx"; // enter the dbID of Child1

        var child1fidrid = "3"; // enter the ID of the field in Child1 that contains the RecordID for Child1 records

        var child1master1fid = "18"; // enter the ID of the field in Child1 that contains the related Master1

        var child1master2fid = "8"; // enter the ID of the field in Child1 that contains the related Master2


        //Child2 parameters

        var child2dbid = "yyyyyyyyy"; // enter the dbID of Child2

        var child2master2fid = "6"; // enter the ID of the field in Child2 that contains the related Master2

        var child2fidrid = "3"; // enter the ID of the field in Child2 that contains the RecordID for Child2 records

        var child2clist = "7.14.54.13.44.31.30.33.32.18.34.36.35.37.59.77.78.80"; // used for final GenResultsTable/s but could use QID instead

        var child2slist = "27.14.54"; // used for final GenResultsTable but could use QID instead


        var master2rids = [];


        function dhsGETINFO() // some of this is copied from QBEF JS file but could just use a parameter here instead of this long search

        {

            var thisURL = document.location.href;

            thisURL.match(/\/db\/([^\?]+)\?/);

            master1dbid = RegExp.$1;

            var queryString = window.location.search;

            queryString = queryString.substring(1);

            var queryNameValuePairs = queryString.split("&");

            for (var i = 0; i < queryNameValuePairs.length; i++) {

                var queryNameValuePair = queryNameValuePairs[i].split("=");

                var pairName = queryNameValuePair[0];

                var pairValue = queryNameValuePair[1];

                if (pairName.match(/DBID/i)) {

                    master1dbid = pairValue;

                } else if (pairName.match(/master1rid/i)) {

                    master1rid = pairValue;

                } else if (pairName.match(/qid/i)) {

                    qid = pairValue;

                } else if (pairName.match(/pagename/i)) {

                    pagename = pairValue;

                }

            }

        }


        dhsGETINFO();


        // ---------------------------------------------------------------------------------------------

        // run a query on the Child1 table to get all records that are related to Master1

        // ---------------------------------------------------------------------------------------------


        xmlhttp1 = new XMLHttpRequest();

        xmlhttp1.open("GET", "https://mycompany.quickbase.com/db/" + child1dbid + "?a=API_DoQuery&includeRids=1&query={" + child1master1fid + ".CT." + master1rid + "}&clist=" + child1fidrid + "&slist=" + child1fidrid, false);

        xmlhttp1.send();

        xmlDoc1 = xmlhttp1.responseXML;

        x = xmlDoc1.getElementsByTagName("record"); //


        var i;

        var n;

        var y;

        var z;


        var qdb_numcols1 = 0;

        var qdb_numrows1 = 0;

        var qdb_heading1 = [];

        var qdb_data1 = [];

        var qdb_data2 = [];


        for (i = 0; i < x.length; i++) {

            // ---------------------------------------------------------------------------------------------

            // gets the RecordIDs of the Child1 records related to Master1

            // ---------------------------------------------------------------------------------------------


            currentChild1 = x[i].getElementsByTagName("record_id_")[0].childNodes[0].nodeValue; // sets currentchild1 to the value of recordID


            // ---------------------------------------------------------------------------------------------

            // runs a new query on the Child1 table to pull out all record info for the record found above (currentChild1)

            // ---------------------------------------------------------------------------------------------


            xmlhttp2 = new XMLHttpRequest();

            xmlhttp2.open("GET", "https://mycompany.quickbase.com/db/" + child1dbid + "?a=API_GetRecordInfo&rid=" + currentChild1, false);

            xmlhttp2.send();

            xmlDoc2 = xmlhttp2.responseXML;

            y = xmlDoc2.getElementsByTagName("field");


            // ---------------------------------------------------------------------------------------------

            // runs through every field in the current child record to find the field that holds the related Master2. Then finds the value of the related Master2

            // ---------------------------------------------------------------------------------------------


            for (n = 0; n < y.length; n++) {

                // --- get the field ID of the current field ---

                currentChildfid = y[n].getElementsByTagName("fid")[0].childNodes[0].nodeValue;


                // --- check if the field ID matches the field ID provided by the user ---

                if (currentChildfid == child1master2fid) {

                    // --- get the value of the related Master2 from the field matched above. This is the Master2 ---

                    currentMaster2rid = y[n].getElementsByTagName("value")[0].childNodes[0].nodeValue;


                    // --- get a report (in Javascript Array) of all Child2's that are related to the current Master2 (This command is JQuery). This returns four variables (two numbers and two arrays) ---


                    $.ajax({

                        async: false,

                        url: "https://mycompany.quickbase.com/db/" + child2dbid + "?a=API_GenResultsTable&query={" + child2master2fid + ".CT." + currentMaster2rid + "}&clist=" + child2clist + "&slist=" + child2slist + "&options=sortorder-AAA&jsa=1",

                        dataType: "script"

                    });


                    qdb_numcols1 = qdb_numcols; // Since the report is the same each time (with different records) this will always be the same

                    qdb_numrows1 += qdb_numrows; // Add the number of rows to any previous number of rows to 

                    qdb_heading1 = qdb_heading; // Since the report is the same each time (with different records) this will always be the same

                    qdb_data1 = qdb_data1.concat(qdb_data); // Add the data to any previous data returned


                }

            }


        }

    </script>


</head>


<body>


    <table id="bom"></table>


    <script>

        // ---------------------------------------------------------------------------------------------

        // Build the table header

        // ---------------------------------------------------------------------------------------------


        var i;

        var table = "<tr>";

        for (i = 0; i < qdb_heading1.length; i++) {

            table += "<th>" + qdb_heading1[i] + "</th>";

        }

        table += "</tr>";


        // ---------------------------------------------------------------------------------------------

        // Build the table body 

        // ---------------------------------------------------------------------------------------------


        var j;

        for (i = 0; i < qdb_data1.length; i++) {

            table += "<tr>";

            for (j = 0; j < qdb_data1[i].length; j++) {

                table += "<td>" + qdb_data1[i][j] + "</td>";

            }

            table += "</tr>";

        }

        document.getElementById("bom").innerHTML = table;

    </script>


</body>

Photo of David

David

  • 20 Points

Posted 3 years ago

  • 0
  • 1
Photo of Ⲇanom the ultimate (Dan Diebolt)

Ⲇanom the ultimate (Dan Diebolt), Champion

  • 30,044 Points 20k badge 2x thumb
TL;DR

You are including jQuery so you should use jQuery's AJAX methods rather than raw XMLHttpRequest(). Here is some example jQuery AJAX code (which is async by default) with a few parameters:

var dbid = "your table dbid";
var qid = "your qid";
var apptoken = "your application token";

$.ajaxSetup({data: {apptoken: apptoken}});


$.get(dbid, {
  qid: qid
}).then(function(xml) {
  console.dirxml(xml);
});

FWIW, note that XMLHttpRequest has been replace with the new fetch API which is implemented in all evergreen browsers (and is used by Service Workers)

Fetch API
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
Photo of David

David

  • 20 Points
Thanks for the response, Dan.

I am running one Ajax call in this already (which I got from another of your posts to someone elses question but have subsequently changed a bit) and I could probably convert the XMLHttpRequests to Ajax calls fairly easily (i.e. I understand the syntax). My issue is with the whole concept of "asynchronous" calls.

My script is designed specifically to get a full Bill of Materials (from System Lines) for a Quote. The problem is that it is linear, with everything happening one after the other, which doesn't work (as written) with ansynchronous calls.

Relationship.............
Quotes -< Quote Lines >- Systems -< System Lines

Essentially it does this...

1) Process URL for some basic parameters (get's the record ID and table name of the Master "Quotes" table).
2) API_DoQuery to find records in the Detail "Quote Lines" table that are related to the "Quotes" table.
3) For each Quote Line:
   a) API_GetRecordInfo on that "Quote Line" to find its Related "System" (the Master in the middle)
   b) Ajax getScript to get a Javascript array of all "System Lines" related to that "System"
   c) Concatenate the results for each iteration to add this iteration's data to any previous iterations' results
4) Build an HTML table out of the full JS array (which now contains all "System Lines" related to the "Quote"

(NB. The reason why I run the Ajax script multiple times is so that I get all records for each "System Line". It would be tempting to write the results of step 3(a) to an array and then simply build multiple criteria (e.g. query={'[Record ID]'.CT.'8'} OR {'[Record ID]'.CT.'10'}) but this would not be a true Bill of Materials since it would only return each System Line once, even if it matched multiple criteria.)

When I have changed the calls to "asynchronous" (which I can also do with the XMLHttpRequests) my script causes errors because the XMLHttpRequest/s or Ajax call have not finished yet and the data is not available to the next part of the script.

It's really a structural thing but I was wondering how I would structure this logically using "async" calls but  ensure I DON'T try to use the results of an API call BEFORE the results have arrive back.

Ultimately, I want to do things in the right way and ensure my script doesn't fall foul to Synchronous calls being disabled completely (either by the browser or if I go to a new version of Jquery). I presume a warning message saying "Synchronous XMLHttpRequest ... is deprecated" means it will disappear at some stage?
Photo of Ⲇanom the ultimate (Dan Diebolt)

Ⲇanom the ultimate (Dan Diebolt), Champion

  • 30,044 Points 20k badge 2x thumb
>My issue is with the whole concept of "asynchronous" calls.

You are correctly focusing on the central issue - the asynchronous nature of the internet. Any time you make a network request for a resource the response will come back in a variable amount of time. You can't know ahead of time how long it will take and in exceptional circumstances the response may never arrive (eg the network goes down). This differs from other non-network computations where one instruction follows another without any delay as fast as the machine can run.

Libraries like jQuery have created an abstraction where all AJAX requests (eg $.get(). $.post(), $.ajax(). $.getScript()) immediately return what is called a "promise"  (also called deferred or futures in other languages). A promise can be thought of as a container which is initially empty but will become full (with the AJAX response) when the response eventually arrives. The initial state of a promise is called pending; the arrival state is called resolved and an error state is called rejected.

So this code:

var promise = $.get(dbid, {
  qid: qid
});
//more code here

will immediately return a promise that is in the pending state.

After some period of time - 1 millisecond or 1 minute later - the promise will enter the resolved state when the AJAX response arrives. However, the JavaScript after the $.get() does not wait for the promise to resolve - it runs immediately after the promise is assigned and is still in the pending state.

To get the response of an jQuery AJAX call you use the then() method like this:

var promise = $.get(dbid, {
  qid: qid
});
promise.then(function(xml) {
  console.dirxml(xml);
});

Once you get the hang of using promises it is conventional practice to not use intermediate variables like promise above and to write the code like this:
$.get(dbid, {
  qid: qid
}).then(function(xml) {   console.dirxml(xml); });
If you need to make two AJAX requests one after the other and to use some piece of information from the first response to form the second request you structure your code like this:
$.get(dbid1, {   qid: qid1 }).then(function(xml1) {   console.dirxml(xml1);   //extract some info from xml1 to form a second request   $.get(dbid2, {     qid: qid2   }).then(function(xml2) {     console.dirxml(xml2); });
If you only have two levels of AJAX calls you can leave the code as written above. If you have a third or more level you write the code like this:
$.get(dbid1, {   qid: qid1 }).then(function(xml1) {   console.dirxml(xml1);   return $.get(dbid2, {qid: qid2}); }).then(function(xml2) {   console.dirxml(xml2);   return $.get(dbid3, {qid: qid3}); }).then(function(xml3) {   console.dirxml(xml3); });
The essential difference is that the second fragment of code makes a chain of AJAX calls one after the other by always returning promise to that the response can be process by the succeeding then(). This property of promises is called "thenable".
There are other idioms you can use. If you need to make two AJAX calls and don't care which completes first but bother are necessary you use $.when() like this:
var promise1 = $.get(dbid1, {   qid: qid1 });
var promise2 = $.get(dbid2, {   qid: qid2 });
$.when(promise1, promise2).then(function() {   // more code here });
I hope this helps. There are a lot of other details but you first need a understanding of what a promise is - an initially empty container that will eventually hold the result of an AJAX call.
Photo of David

David

  • 20 Points
Wow, that was incredibly helpful! Thanks so much!

It's now after hours here in Blighty but I will give it a go in the morning.

One question... in my code I split the "build the table" bit (the script that takes the JS array, puts it all into a variable with all the right table tags, and inserts it into the <table> tag) from the "get the data" bit. Basically, I did this only so I could put the "build the table" bit after the HTML page had loaded (not sure if it really is doing this TBH). Assuming I use this "async" method, how would I ensure the page had loaded before I do the "build the table" bit? I guess I need to make sure that (a) I don't try to create a table on the page when it hasn't loaded fully and (b) I don't try to call the function to populate the table before all the data is available.

Do you think I should I just use <body onload="myFunction()"> for the whole thing?