Forum Discussion

_anomDiebolt_'s avatar
_anomDiebolt_
Qrew Elite
6 years ago

How To Create ES6 Module For QuickBase

How To Create ES6 Module For QuickBase

In previous postings I showed you how to use ES6 Modules with QuickBase and made reference to two CDN repositories (https://unpkg.com/ and  https://www.jsdelivr.com/) where you could obtain ES6 Modules for popular JavaScript libraries. This post will show you how you can host your own ES6 Modules for use with QuickBase in either (1) an external server you control or (2) a QuickBase Code Pages.

First let's explain the two functions we will be putting in our ES6 Module. The dbid's we use all day long are actually just integers encoded using a scheme called ob32. The ob refers to OneBase - the predecessor product that became QuickBase two decades ago - and the 32 refers to the 32 characters used to encode the integer (the 24 characters a-z excluding o and l along with the 8 digits 2-9 which excludes 0 and 1). The dbid actually represents the creation time of the table in milliseconds since midnight January 1, 1970 (start of the Unix Epoch) encoded using ob32 encoding. Here are the two functions that perform the ob32 encoding and ob32 decoding:
function decode(ob32) {
  var ob32Characters = "abcdefghijkmnpqrstuvwxyz23456789";
  var decode = 0;
  var place = 1;
  for (var counter = ob32.length - 1; counter >= 0; counter--) {
    var oneChar = ob32.charAt(counter);
    var oneDigit = ob32Characters.indexOf(oneChar);
    decode += (oneDigit * place);
    place = place * 32;
  }
  return decode;
}

function encode(strDecimal) {
  var ob32Characters = "abcdefghijkmnpqrstuvwxyz23456789";
  var decimal = parseInt(strDecimal);
  var ob32 = "";
  while (decimal > 0) {
    var remainder = decimal % 32;
    remainder = ob32Characters.substr(remainder, 1);
    ob32 = remainder.concat(ob32);
    decimal = Math.floor(decimal / 32);
  }
  return ob32;
}
These functions will not change so long as QuickBase continues to use dbid's and it would be awkward to constantly paste them into our code every time we might need them. So we will just put them into an ES6 Module named eb32.js that looks like this:
export function decode(ob32) {
  var ob32Characters = "abcdefghijkmnpqrstuvwxyz23456789";
  var decode = 0;
  var place = 1;
  for (var counter = ob32.length - 1; counter >= 0; counter--) {
    var oneChar = ob32.charAt(counter);
    var oneDigit = ob32Characters.indexOf(oneChar);
    decode += (oneDigit * place);
    place = place * 32;
  }
  return decode;
}
export function encode(strDecimal) {
  var ob32Characters = "abcdefghijkmnpqrstuvwxyz23456789";
  var decimal = parseInt(strDecimal);
  var ob32 = "";
  while (decimal > 0) {
    var remainder = decimal % 32;
    remainder = ob32Characters.substr(remainder, 1);
    ob32 = remainder.concat(ob32);
    decimal = Math.floor(decimal / 32);
  }
  return ob32;
}
Now let's take up the first case of hosting our ES6 Module from an external server as it is the easier of the two cases. Here is the above eb32.js module hosted on my website:

ob32.js Module Hosted on External Server
https://dandiebolt.com/qb/modules/ob32.js

In addition to hosting the raw file, you have to configure the server to add a couple of special headers to the file. This is typically done on Shared Hosting Plans by setting the .htaccess file with the following:
Header add Access-Control-Allow-Origin "*"
Header add Access-Control-Allow-Headers "origin, x-requested-with, content-type"
Header add Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS"
These settings insure that the ES6 Module file eb32.js can be used "cross- domain".

If you were to inspect the download of this file in the DevTools Network tab you would see that is has the headers set by the .htaccess file:




That's all you have to do to set up an externally hosted ES6 Module. Now let's look at hosting the same ES6 Module within a QuickBase code page. In this case we will use a slightly different file extension (mjs) so our ES6 Module will be named ob32.mjs:

ES6 Module Hosted Within QuickBase as a Code Page
https://haversineconsulting.quickbase.com/db/bn86s5fc8?a=dbpage&pagename=ob32.mjs

Unfortunately, QuickBase will serve this code page with the MIME Type:
application/x-javascript
when usage as a ES6 Module will require that the code page be served with the MIME Type
application/javascript
That is a problem we can correct with a Service Worker which will intercept all URLs ending in mjs and convert them to have the proper MIME Type:
self.addEventListener("fetch", (event) => {
  if (event.request.url.endsWith(".mjs")) {
    event.respondWith(
      fetch(event.request.url)
      .then((response) => response.text())
      .then((body) => {
        return new Response(body, {
          headers: new Headers({
            "Content-Type": "application/javascript"
          })
        });
      })
    );
  } 
});
Service Worker Hosted Within QuickBase as a Code Page
https://haversineconsulting.quickbase.com/db/bn86s5fc8?a=dbpage&pagename=sw.js

So here is the demo of both techniques:

First visit this page to get the Service Worker to register:

ES6 Module: OneBase Encode/Decode ~ Applicaiton Dashboard
https://haversineconsulting.quickbase.com/db/bn86s5fc8

Next visit each of these tables:

Table1 ~ Using Externally Hosted ES6 Module
https://haversineconsulting.quickbase.com/db/bn86s5un6?a=td

Table2 ~ Using Code Page and Service Worker to Host ES6 Module
https://haversineconsulting.quickbase.com/db/bn87f8t55?a=td

On each of the pages the [OB32] field implements the 3Q&S Technique to do the following three steps:

(1) loads the ES6 Module from (a) the external server or (b) a QuickBase Code page
(2) grabs the current page's dbid and decodes it an re-encodes it
(3) stuff the calculated results into the cell where the [OB32] resides


Here is the Rich Text Formula Field definitions used in the above to tables:

Table 1 Formula for OB32 field:
"<img src onerror='
(async () => {
  let {encode, decode} = await import('https://dandiebolt.com/qb/modules/ob32.js');
 
let dbid = document.location.pathname.split('/')[2];
  let dbidDecoded = decode(dbid);
  let dbidEncoded = encode(dbidDecoded);
 
this.outerHTML = '
    <h1>dbid = ${dbid}</h1>
    <h1>dbidDecoded = ${dbidDecoded}</h1>
    <h1>dbidEncoded = ${dbidEncoded}</h1>
    ';
})();
'>"
Table 2 Formula for OB32 field:
"<img src onerror='
(async () => {
  let {encode, decode} = await import('https://haversineconsulting.quickbase.com/db/bn86s5fc8?a=dbpage&pagename=ob32.mjs');
 
let dbid = document.location.pathname.split('/')[2];
  let dbidDecoded = decode(dbid);
  let dbidEncoded = encode(dbidDecoded);
 
this.outerHTML = '
    <h1>dbid = ${dbid}</h1>
    <h1>dbidDecoded = ${dbidDecoded}</h1>
    <h1>dbidEncoded = ${dbidEncoded}</h1>
    ';
})();
'>"
Note that the only difference between the two formulas is the URL from which the ES6 Module is being loaded!

Notes:

(1) The hardest part of this demo was formatting the forum post in one sitting. I will add more notes in a separate message.

(2) If I didn't explicitly state it, the whole point of using ES6 Modules is to import libraries or user written code in a flexible and simple manner. ES6 Modules are the modern way to import libraries and over time I would expect most web sites to adopt them as it is vastly better approach to managing libraries. 

(3) As to using ES6 Modules with the 3S&S Technique, one sub-goal is to make the script within the Rich Text Formula Field as short as possible. Any helper functions or libraries can simply be imported in one line of code. Additionally, when an ES6 Module is imported multiple times in the same page, only one fetch of the resource is made. This insures that when the Rich Text Formula Field is used in a report only one fetch of the resource is made despite their being an import statement in the Rich Text Formula Field in every record.

(4) Notice how short the code in the Rich Text Formula Field is when using the 3Q&S Technique. You can break the code down into three sections:
"<img src onerror='

(async () => {
  //load some libraries or user defined code
  let {encode, decode} = await import('https://haversineconsulting.quickbase.com/db/bn86s5fc8?a=dbpage&pagename=ob32.mjs');
  
  //calculate something to display
  let dbid = document.location.pathname.split('/')[2];
  let dbidDecoded = decode(dbid);
  let dbidEncoded = encode(dbidDecoded);
  
  //stuff the result into the place on the page where the field is located
  this.outerHTML = '
    <h1>dbid = ${dbid}</h1>
    <h1>dbidDecoded = ${dbidDecoded}</h1>
    <h1>dbidEncoded = ${dbidEncoded}</h1>
    ';
})();
'>"
This pattern is so general and flexible that it can be adopted for a very wide variety of purposes and with a little forethought can implement great new features while still using a short amount of code in the formula.

(5) It is possible to expose custom APIs in the script. For example, the entire database schema (tables, fields, queries, etc) can be packed into an API and made available to the Rich Text Formula Field through a single import of an ES6 Module.

(6) I would be remiss if I did not mention a security issue. If you host an ES6 Module within a code page in your account you will be able to access it throughout your account but you will not be able to access it from another QuickBase account. This is because you can't set the CORS headers for the code page to get around the same origin policy. However, if you host your ES6 Module from an external server you can set the CORS headers and the ES6 Module can be accessed from any QuickBase account. So the security issue is that you should only use externally hosted ES6 Modules from trusted sources. I don't mean to scare you with this advice but whenever you include JavaScript from an external source you have to trust that there is no malicious code in that repository. There is nothing unique to using ES6 Modules in this regard -
No RepliesBe the first to reply