Chapter 1 - Hello World

This tutorial is based on the hello_world.js example, which can be found in the TRAC GitHub Repository under examples/apps/javascript.

Installing the API

The easiest way to build web client applications for TRAC is using the web API package, It is available to install with NPM:

npm install --save tracdap-web-api

Web apps built with this package can run in a browser using gRPC-Web to connect directly to the TRAC platform. There is no need to install anything else, or to deploy intermediate servers such as Node.js or Envoy proxy (although these configurations are supported if required).

Setting up a dev environment

The easiest way to get a local development instance of TRAC is to clone the TRAC d.a.p. GitHub Repository and follow the instructions in the main README file.

To make a browser-based app that talks to TRAC, the platform and the app should be served under the same origin. In production this can be handled by proxy servers that take care of routing and other network concerns. For development, the TRAC Gateway provides the capabilities needed to test against a local TRAC instance.

A sample TRAC Gateway config is provided in the TRAC repository under etc/ and includes an example of a route to proxy a client app. Change the target to point at your normal dev server (e.g. WebPack dev server or an embedded server in your IDE). The path under the “match” section is where your app will appear under gateway.

  - routeName: Local App
    routeType: HTTP

      host: localhost
      path: /local/app

      scheme: http
      host: localhost
      port: 9090
      path: /

In this example, if the gateway is running on port 8080 over http, you would be able to access your app at http://localhost:8080/local/app.

Connecting to TRAC

Start by importing the TRAC API package:

17import {tracdap} from 'tracdap-web-api';

We need two things to create the TRAC connection, an RPC transport and an instance of the TRAC API class. You can use tracdap.setup to create an RPC transport that works in the browser.

19// Use tracdap.setup to create an RPC transport pointed at your TRAC server
20// The browser RPC connector will send all requests to the page origin server
21const metaTransport = tracdap.setup.transportForBrowser(tracdap.api.TracMetadataApi);

This assumes you have set up routing through the TRAC gateway as described in the previous section.

One you have an RPC connector, you can use it to create the a TRAC API object. Note that each API class needs its own RPC connector, the RPCs are specific to the APIs they serve. In this example we only need the metadata API.

23// Create a TRAC API instance for the Metadata API
24const metaApi = new tracdap.api.TracMetadataApi(metaTransport);

Running outside a browser

The web API can also be used to build apps that run on a Node.js server or as standalone JavaScript applications. In this case, you’ll need to create an RPC connector pointed at the address of your TRAC instance:

// Use tracdap.setup to create an RPC transport pointed at your TRAC server
const metaTransport = tracdap.setup.transportForTarget(tracdap.api.TracMetadataApi, "http", "localhost", 8080);

You’ll also need to supply the global XMLHttpRequest object, which is not normally available outside a browser environment. The example code sets this up in the run_examples.js host script, using the ‘xhr2’ package available on NPM:

// To run these examples outside of a browser, XMLHttpRequest and WebSocket are required
import xhr2 from 'xhr2';
import WebSocket from "ws";

Creating and saving objects

Suppose we want to create a schema, that we can use to describe some customer account data. (It is not always necessary to create schemas in this way, but we’ll do it for an example).

First we need to build the SchemaDefinition object. In real-world applications schemas would be created by automated tools (for example TRAC generates schemas during data import jobs), but for this example we can define a simple one in code.

27export function createSchema() {
29    // Build the schema definition we want to save
30    const schema = tracdap.metadata.SchemaDefinition.create({
32        schemaType: tracdap.SchemaType.TABLE,
33        table: {
34            fields: [
35                {
36                    fieldName: "customer_id", fieldType: tracdap.STRING, businessKey: true,
37                    label: "Unique customer account number"
38                },
39                {
40                    fieldName: "customer_type", fieldType: tracdap.STRING, categorical: true,
41                    label: "Is the customer an individual, company, govt agency or something else"
42                },
43                {
44                    fieldName: "customer_name", fieldType: tracdap.STRING,
45                    label: "Customer's common name"
46                },
47                {
48                    fieldName: "account_open_date", fieldType: tracdap.DATE,
49                    label: "Date the customer account was opened"
50                },
51                {
52                    fieldName: "credit_limit", fieldType: tracdap.DECIMAL,
53                    label: "Ordinary credit limit on the customer account, in USD"
54                }
55            ]
56        }
57    });

The web API package provides structured classes for every type and enum in the tracdap.metadata package. A .create() method is available for every type, which provides auto-complete and type hints in IDEs that support it. Enums are set using the constants defined in the API package. All enum types are available in the trac namespace, so tracdap.SchemaType is shorthand for tracdap.metadata.SchemaType and so on. The basic types in the TRAC type system are also available, so tracdap.STRING is a shorthand for tracdap.metadata.BasicType.STRING.

Now we want to save the schema into the TRAC metadata store. To do that, we use a MetadataWriteRequest. Request objects from the tracdap.api package can be created the same way metadata objects from tracdap.metadata.

59    // Set up a metadata write request, to save the schema with some informational tags
60    const request = tracdap.api.MetadataWriteRequest.create({
62        tenant: "ACME_CORP",
63        objectType: tracdap.ObjectType.SCHEMA,
65        definition: {
66            objectType: tracdap.ObjectType.SCHEMA,
67            schema: schema
68        },
70        tagUpdates: [
71            { attrName: "schema_type", value: { stringValue: "customer_records" } },
72            { attrName: "business_division", value: { stringValue: "WIDGET_SALES" } },
73            { attrName: "description", value: { stringValue: "A month-end snapshot of customer accounts" } },
74        ]
75    });

There are several things to call out here. TRAC is a multi-tenant platform and every API request includes a tenant code. By default resources are separated between tenants, so tenant A cannot access a resource created in tenant B. For this example we have a single tenant called “ACME_CORP”.

Objects are the basic resources held in the platform, each object is described by an ObjectDefinition which can hold one of a number of types of object. Here we are creating a SCHEMA object, we build the object definition using the schema created earlier.

We also want to tag our new schema with some informational attributes. These attributes describe the schema object and will allow us to find it later using metadata searches. Tags can be applied using TagUpdate instructions when objects are created. Here we are applying three tags to the new object, two are categorical tags and one is descriptive.

The last step is to call createObject(), to send our request to the TRAC metadata service.

77    // Use the metadata API to create the object
78    return metaApi.createObject(request).then(schemaId => {
80        console.log(`Created schema ${schemaId.objectId} version ${schemaId.objectVersion}`);
82        return schemaId;
83    });

The createObject() method returns the ID of the newly created schema as a TagHeader, which includes the object type, ID, version and timestamps.

All the API methods in the web API package are available in both future and callback form. This example uses the future form, which allows chaining of .then(), .catch() and .finally() blocks. The equivalent call using a callback would be:

metaApi.createObject(request, (err, header) => {

    // Handle error or response

Loading objects

Now the schema has been saved into TRAC, at some point we will want to retrieve it. We can do this using a MetadataReadRequest.

86export function loadTag(tagHeader) {
88    const request = tracdap.api.MetadataReadRequest.create({
90        tenant: "ACME_CORP",
91        selector: tagHeader
92    });

All that is needed is the tenant code and a TagSelector. Tag selectors allow different versions of an object or tag to be selected according to various criteria. In this example we already have the tag header, which tells us the exact version that should be loaded. The web API package allows headers to be used as selectors, doing this will create a selector for the version identified in the header.

Finally we use readObject() to send the read request.

94    return metaApi.readObject(request);

The readObject() method returns a Tag, which includes the TagHeader and all the tag attributes, as well as the ObjectDefinition. Since we used the future form of the call, it will return a promise for the tag.

Putting it all together

In a real-world situation these calls would be built into the framework of an application. The example scripts all include main() functions so they can be tried out easily from a console or IDE. In this example we just create the schema and then load it back from TRAC.

 96export async function main() {
 98    console.log("Creating a schema...");
100    const schemaId = await createSchema();
102    console.log("Loading the schema...");
104    const schemaTag = await loadTag(schemaId);
106    console.log(JSON.stringify(schemaTag, null, 2));

The last call to JSON.stringify() will provide a human-readable representation of the TRAC object, that can be useful for debugging.