DevCon, DevCon 2019, FileMaker, FileMaker Data API

FileMaker Data API workshop – activity four

Connecting from the frontend

JavaScript running in a browser is able to perform an XMLHttpRequest from the browser to a web server. The simplest way to implement that is using an AJAX (asynchronous JavaScript and XML) request. You could connect directly to the XMLHttpRequest API yourself, but that makes things way more complicated than they need to be because there are two great ways to take away a lot of the pain – using the jQuery $.ajax() method or the fetch() function.

For a long time jQuery was the ‘standard way’ of writing cross-browser, cross-platform AJAX requests because it really does make things very simple. Since 2017 with the introduction of Fetch this has also become a simple way to implement an AJAX request.

By default your browser won’t be able to communicate with your FileMaker server using AJAX because of something called CORS (Cross-Origin Resource Sharing) but it’s pretty straight forward to resolve that if you follow the instructions in this blog post.

During the workshop everything’s already set up on the dapi.msdev.co.uk server so we’re good to go with making connections. The code we’ll be working with can be found in the Frontend folder, where double clicking on the index.html file should launch a very basic page into your default browser.

AJAX with jQuery and fetch()

In the demonstrations I’m going to use jQuery because it’s still the most commonly used way of performing AJAX requests. If you’re already a ‘jQuery Master’ you may want to consider trying these activities using fetch() instead, but keep in mind that though there are solutions for all of the activities using fetch() all of the demos and explanations will be using jQuery

jQuery has a number of shortcut AJAX methods which you may be familiar with like $.get(), $.post() and $.load(), but 'under the hood' these are all shortcuts to the base method $.ajax(). We need to use $.ajax() directly because we have to be able to do some things, like setting headers, which you can't do with the shortcut methods. We also want to create generic functions which can handle all of the HTTP verbs, not just GET and POST

There's plenty of documentation for $.ajax() but because there are so many possible options which can be set it's pretty confusing if you're coming to this for the first time.

So here's the "Readers' Digest condensed version" of the bits of the $.ajax() method we need to work with the FileMaker Data API

$.ajax({
    type: '', // Which HTTP verb do we want, GET, POST, DELETE etc
    url: '',  // Where should the request be sent
    dataType: 'json', // What type of response to we expect back
    data: JSON.stringify(data), // What data do we want to send, as a string
    beforeSend: function (xhr) {
    	// beforeSend lets us perform any setup we need before making the call, like setting headers
        xhr.setRequestHeader("header", "value");
    },
    success: function(resp) {
        // it went well, what do we do with the response?
    },
    error: function(response) {
        // it went badly, what should we do now?
    }
});

You'll notice that the first four of those are simple string settings which we need to configure. The last three all take functions which allows us to do far more complex things.

fetch() has been built in to modern browsers since 2017 and provides a simple interface for making AJAX requests. It also uses the modern JavaScript paradigm of Promises. (There's a good introduction to Promises in JavaScript in Google Web Fundamentals but if you're not familiar with JavaScript it may be a bit overwhelming to begin with!)

MDN web docs also has good documentation on the fetch() API, but much like the jQuery $.ajax() docs they get pretty complicated pretty quickly, so here's the short version for working with the FileMaker Data API:

let url '';  // Where should the request be sent
let params = {
    method: '', // Which HTTP verb do we want, GET, POST, DELETE etc
    headers: {
        "header": "value", // Set any header we need
    },
    body = JSON.stringify(data), // Set the body to a string version of the data we need to send
};
        
fetch(url, params) // actually perform the call
    .then((response) => {
        // if the request is successful we get a Promise back, we need to extract the JSON body
        return response.json();
    })
    .then((data) => {
        // it went well, what do we do with the response json, now in the data variable      
    })
    .catch((err) => {
        // it went badly, what should we do now?
    });

As you can see there are quite a few similarities to how things are named, but that the mechanics of putting those together are somewhat different to jQuery's $.ajax()

Activity

Now that we have a basic understanding of how $.ajax() (or fetch()) work we need to:

  1. Open the Frontend folder in your IDE (if you don’t have an IDE installed use your preferred text editor, or refer back to the setup details for options to get an IDE installed).
  2. Update the config settings in assets/app.js setting the name of your interface file along with your readonly username and password.
  3. Complete the TODOs in the function fetchToken found in assets/filemaker.js to allow us to retrieve a FileMaker Data API token. Refresh your browser, then select Tests > fetchToken() and confirm in the console that you get a token.
  4. Complete the TODOs in the function performRequest in the same file to use that token and communicate with the FileMaker Data API. Refresh your browser, then select Tests > performRequest() and confirm in the console that you get data returned.
  5. Complete the TODOs in the function fetchData in assets/chart.js to retrieve data from FileMaker and populate our chart! Refresh your browser, then select Tests > performRequest() and confirm in the console that you get data returned. You should also now see a chart displayed on screen.

Solutions

Here's one example solution for the fetchToken function from assets/filemaker.js.

fetchToken: function(username, password, success, error) {
    $.ajax({
        type: "POST",
        url: Config.baseURI + '/sessions',
        dataType: 'json',
        beforeSend: function (xhr) {
        	xhr.setRequestHeader("Content-Type", "application/json");
	        xhr.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
        },
        success: function(resp) {
    	    let token = resp.response.token;
        	FileMaker.retried = false;
	        FileMaker.saveToken(token);

    	    success(resp);
        }, error: function(response) {
        	error(response);
        }
    });
}

And one way of implementing performRequest function from assets/filemaker.js.

performRequest: function(method, urlSuffix, data, success, error) {
	let config = {
        type: method,
        url: Config.baseURI + urlSuffix,
        dataType: 'json',
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + FileMaker.retrieveToken());
            xhr.setRequestHeader("Content-Type", "application/json");
        },
        success: function(response) {
    	  // Depending on exactly which endpoint we're calling we either want the data, or the whole response
          // if there's a data set, send that, otherwise the whole response object.
          let data = 'undefined' === typeof response.response.data ? response.response : response.response.data;

          success(data);
        }, error: function(response) {
            let code = parseInt(response.responseJSON.messages[0].code);

            if(401 === code) {
                // no records found
                success([]);
            }

            // if the token has expired or is invalid then in theory 952 will come back
            // but sometimes you get 105 missing layout (go figure), so try a token refresh
            if([105, 952].includes(code) && !FileMaker.retried) {
                // TODO (Activity six) come up with some way to try logging the user back in
            }

            error(response);
        }
    };

    // We should only send data when it's not a GET request, otherwise we end up with additional parameters
    if(method !== 'GET') {
        config.data = JSON.stringify(data);
    }

    $.ajax(config);
}

And finally a completed fetchData from assets/chart.js.

fetchData: function(successCallback, errorCallback) {
    let body = {
         "query": [
             {"Column2": ">0"}
        ],
        "script.prerequest": "SetStates"
    };

    FileMaker.performRequest('POST', '/layouts/VirtualList/_find', body, successCallback, errorCallback);
}

Here's one example solution for the fetchToken function from scripts/filemaker.js.

fetchToken: function (username, password, success, error) {
    fetch(Config.baseURI + '/sessions', {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
             "Authorization": "Basic " + btoa(username + ":" + password)
        }
    })
    .then((response) => response.json())
    .then((response) => {
        if('0' === response.messages[0].code) {
            let token = response.response.token;

            FileMaker.saveToken(token);
            success(token);

            return;
        }
        error(response.messages[0].message);
    })
    .catch((err) => {
        error(err);

    });
}

And one way of implementing performRequest function from scripts/filemaker.js.

performRequest: function(method, urlSuffix, data, success, error) {
	let url = Config.baseURI +  urlSuffix;
    let params = {
        method: method,
        headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + this.retrieveToken()
        },

    };
    if(method !== 'GET') {
        params.body = JSON.stringify(data);
    }

    fetch(url, params)
        .then((response) => {
            // If we get an authorisation error it comes back as a 401 (Unauthorised) or a server error 500
            if([401, 500].includes(response.status) && !FileMaker.retried) {
                // TODO (Activity six) come up with some way to try logging the user back in
            }

            return response.json();
        })
        .then((data) => {
            FileMaker.retried = false;

            // Depending on exactly which endpoint we're calling we either want the data, or the whole response
            // if there's a data set, send that, otherwise the whole response object.
            let respond = 'undefined' === typeof data.response.data ? data.response : data.response.data;
            success(respond);
        })
        .catch((err) => {
            error(err);
        });

}

And finally a completed fetchData from scripts/chart.js.

fetchData: function(successCallback, errorCallback) {
    let body = {
         "query": [
             {"Column2": ">0"}
        ],
        "script.prerequest": "SetStates"
    };

    FileMaker.performRequest('POST', '/layouts/VirtualList/_find', body, successCallback, errorCallback);
}

< Back to activity threeOn to activity five >

Leave A Comment

*
*