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 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
match:
host: localhost
path: /local/app
target:
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:
18import {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.
20// Use tracdap.setup to create an RPC transport pointed at your TRAC server
21// The browser RPC connector will send all requests to the page origin server
22const 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.
24// Create a TRAC API instance for the Metadata API
25const 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.
28export function createSchema() {
29
30 // Build the schema definition we want to save
31 const schema = tracdap.metadata.SchemaDefinition.create({
32
33 schemaType: tracdap.SchemaType.TABLE,
34 table: {
35 fields: [
36 {
37 fieldName: "customer_id", fieldType: tracdap.STRING, businessKey: true,
38 label: "Unique customer account number"
39 },
40 {
41 fieldName: "customer_type", fieldType: tracdap.STRING, categorical: true,
42 label: "Is the customer an individual, company, govt agency or something else"
43 },
44 {
45 fieldName: "customer_name", fieldType: tracdap.STRING,
46 label: "Customer's common name"
47 },
48 {
49 fieldName: "account_open_date", fieldType: tracdap.DATE,
50 label: "Date the customer account was opened"
51 },
52 {
53 fieldName: "credit_limit", fieldType: tracdap.DECIMAL,
54 label: "Ordinary credit limit on the customer account, in USD"
55 }
56 ]
57 }
58 });
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
.
60 // Set up a metadata write request, to save the schema with some informational tags
61 const request = tracdap.api.MetadataWriteRequest.create({
62
63 tenant: "ACME_CORP",
64 objectType: tracdap.ObjectType.SCHEMA,
65
66 definition: {
67 objectType: tracdap.ObjectType.SCHEMA,
68 schema: schema
69 },
70
71 tagUpdates: [
72 { attrName: "schema_type", value: { stringValue: "customer_records" } },
73 { attrName: "business_division", value: { stringValue: "WIDGET_SALES" } },
74 { attrName: "description", value: { stringValue: "A month-end snapshot of customer accounts" } },
75 ]
76 });
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.
78 // Use the metadata API to create the object
79 return metaApi.createObject(request).then(schemaId => {
80
81 console.log(`Created schema ${schemaId.objectId} version ${schemaId.objectVersion}`);
82
83 return schemaId;
84 });
85}
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
.
87export function loadTag(tagHeader) {
88
89 const request = tracdap.api.MetadataReadRequest.create({
90
91 tenant: "ACME_CORP",
92 selector: tagHeader
93 });
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.
95 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.
97export async function main() {
98
99 console.log("Creating a schema...");
100
101 const schemaId = await createSchema();
102
103 console.log("Loading the schema...");
104
105 const schemaTag = await loadTag(schemaId);
106
107 console.log(JSON.stringify(schemaTag, null, 2));
108}
The last call to JSON.stringify()
will provide a human-readable representation of the
TRAC object, that can be useful for debugging.