-
Notifications
You must be signed in to change notification settings - Fork 15
Deep Dive | Templating
Open Data Exporter uses doT to compile and render templates. doT is similar to handlebars or mustache in that it uses double curly braces ({{ }}
) for expressions, but doT is much more powerful. doT allows not only variable evaluation, but also array iteration, variable assignment, and inline JavaScript function calls, all from within an expression.
Before covering the ins and outs of templating, it's important to first understand the variables and data used in templates.
The primary entry point for accessing any variables, data, or functions is the def
object (def is for "definitions"). It contains the following standard properties:
- now A moment.js object initialized to the local environment's current time and timezone when the job is started
-
data An object containing the responses from requests. The data from a request will generally be available at
def.data.request_key
where request_key is the key string as defined in the configuration's executionPlan property. - job The currently executing dereferenced job object from the config file
- configuration The currently executing dereferenced configuration object from the config file
- vars An object of key/value pairs that contains standard variables as well as any variables loaded from customData properties on config objects
The def.vars
object is initialized with several useful variables:
-
args An object of key/value pairs representing the command line parameters passed to node, in lower case. Flags are set as a boolean (e.g. using
/debugApi
results in"debugapi":true
). Parameters are set as the expected key/value (e.g. using/logLevel=info
results in"loglevel":"info"
). -
date A moment.js object cloned from
def.now
, but converted to the timezone specified in the custom data variable timezoneOverride if a value was provided -
interval The ISO-8601 duration used for calculating intervals. Defaults to 30 minutes with the value
PT30M
.
When a job's configuration is executed, def.vars is populated with custom data. The custom data is loaded from customData properties on objects in the config file in the order below. Any duplicate keys will overwrite the previously set value.
- config.customData Custom data on the root of the config
- job.customData Custom data on the job
- configuration.customData Custom data on the configuration being processed
- configuration.executionPlan Custom data from each object is loaded in the order it is defined
- configuration.templates Custom data from each object is loaded in the order it is defined
- configuration.exports Custom data from each object is loaded in the order it is defined
After def.vars is initialized with default properties and custom data is loaded, the following properties are derived from properties set in def.vars:
- Variables derived from defs.vars.date
- startOfHour A moment.js object at the beginning of the current hour
- previousStartOfHour A moment.js object one hour before the beginning of the current hour
- startOfDay A moment.js object at the beginning of the current day
- previousStartOfDay A moment.js object one day before the beginning of the current day
- startOfWeek A moment.js object at the beginning of the current week
- previousStartOfWeek A moment.js object one week before the beginning of the current week
- startOfMonth A moment.js object at the beginning of the current month
- previousStartOfMonth A moment.js object one month before the beginning of the current month
- startOfQuarter A moment.js object at the beginning of the current quarter
- previousStartOfQuarter A moment.js object one quarter before the beginning of the current quarter
- startOfYear A moment.js object at the beginning of the current year
- previousStartOfYear A moment.js object one year before the beginning of the current year
- Variables derived from defs.vars.date and def.vars.interval
- currentIntervalStart A moment.js object at the beginning of the current interval, calculated based on intervals always starting at the most recent midnight
- previousIntervalStart A moment.js object one interval before the beginning of the current interval
- currentInterval An ISO-8601 interval spanning the defined interval duration (_def.vars.interval) that encompasses the current time (def.vars.date)
- previousInterval An ISO-8601 interval spanning the interval prior to the current interval
The following custom data properties are explicitly used by the application:
- interval Overrides the default interval (PT30M). Must be a valid ISO-8601 duration.
-
timezoneOverride Overrides the default timezone (environment context) for all date objects. Timezones are parsed using moment-timezone and should be in a valid tz database format (e.g
America/Indianapolis
). Numeric offsets are not allowed because they do not reflect DST observance. -
request Do not attempt to set this. When executing transforms during the request templating process,
def.vars.request
is set to the request body object and is cleared afterwards.
For templates to use functions, they must be loaded into the def object. This is done by loading the files listed in a configuration's externalModules object as node modules into the def object. They are loaded with the variable name matching the key in the configuration.
Given this configuration:
"externalModules": {
"exampleFunctions": "./extensions/examples/examples.js"
}
The functions exported by the module in examples.js can now be accessed via def.exampleFunctions
. If the module defines a function named helloWorld, it can be invoked as def.exampleFunctions.helloWorld()
.
Additionally, all .js files found in ./extensions/standard
are automatically loaded as modules with names matching the filenames.
Tips
- The module must export an anonymous object (
module.exports = new MyModule();
) as opposed to an anonymous prototype (module.exports = MyModule;
). - The location of the module must have access to all required node dependencies. The recommended pattern is to place them in the
src/extensions/<yourmodule>/
directory and add your dependencies tosrc/package.json
. This will give your modules access to any modules defined in package.json and will automatically install them when runningnpm install
. - Don't include your module with a name that will conflict with anything else. For example, vars, data, job, and configuration are all bad choices and will break the application.
The template execution process is done in two steps.
First, the template is compiled by parsing the template into a JavaScript function that can be executed. During the compilation, the only valid root variable is the def
object.
Second, the resulting JavaScript function from the compilation process is executed. During the execution stage, the only valid root variable is it
.
The standard expectation is that def
and it
can be used interchangeably and will be the exact same object. The templates {{#def.data.name}}
and {{#it.data.name}}
will result in the exact same output.
There are some exceptions, however:
- Collection iteration in templates can only be done on the
it
variable. Attempts to iterate a collection using thedev
variable will cause a compilation error to be thrown when compiling the template. - When executing repeating requests,
def
will contain the standard object, butit
will contain the current collection member that is in context for the request.
The following sections will assume that the def
object is defined as:
{
"vars": {
"myInt": 5,
"myString": "a value"
},
"data": {
"name": "Steve",
"age": 25,
"address": {
"street": "123 Strasse St.",
"city": "Denver",
"state": "CO",
"zip": "80123"
},
"cars": [
{
"make": "Ford",
"model": "Mustang"
},
{
"make": "Subaru",
"model": "WRX"
}
]
},
"helpers": {
"addNumbers": function(num1, num2) { return num1 + num2; },
"flattenAddress": function(data) { data.fullAddress = data.address.street + ', ' + data.address.city + ', ' + data.address.state + ' ' + data.address.zip; }
}
}
The most basic operation is to evaluate a variable. To do this, use the syntax {{# varname }}
where varname is the name of the variable to evaluate. This can include subobjects like def.vars.myString
.
This template:
{{#def.data.name}} is {{#def.data.age}} years old and lives on {{#def.data.address.street}}.
will evaluate to:
Steve is 25 years old and lives on 123 Strasse St..
Functions that have been loaded from external modules can be executed in a template. The value returned from the function will be inserted into the template. The value should be a string or other primitive type. The value will have .toString() invoked on it and will insert "[object Object]" into the template if the value isn't a primitive type.
This template:
{{#def.data.name}} is {{#def.data.age}} years old now. In {{#def.vars.myInt}} years, he will be {{#def.helpers.addNumbers(def.data.age, def.vars.myInt)}}.
will evaluate to:
Steve is 25 years old now. In 5 years he will be 30.
In some cases it may be useful to assign the output of a function to a variable. When doing so, the function may return any type and the value returned will be stored in the variable, even if an object is returned.
This template:
{{#(def.data.futureAge=def.helpers.addNumbers(def.data.age, def.vars.myInt)) && ""}}
will evaluate to an empty string and will not display any data. However, after executing this expression, this template:
{{#def.data.name}} will be {{#def.data.futureAge}} years old in {{#def.vars.myInt}} years.
will evaluate to:
Steve will be 30 years old in 5 years.
The syntax for variable assignment can be genericised as {{#(expression) && ""}}
.
All objects passed into functions are passed by reference and can be manipulated without a need to return any value. This use of functions is particularly useful for creating transforms.
This template:
{{#def.helpers.flattenAddress(def.data)}}
will evaluate to an empty string since no value is returned. However, def.data.fullAddress
will now have a value. After executing the template above, this template:
{{#def.data.name}} lives at {{#def.data.fullAddress}}.
will evaluate to:
Steve lives at 123 Strasse St., Denver, CO 80123.
Collections can be iterated to execute a template for each member of the collection. The template for iteration is everything that is contained between the open and closing expressions for an array.
To mark the beginning of an iteration template, use the format: {{~collection :varname}}
. To mark the end of the iteration template, use the format: {{~}}
. Within the template, the current item in the collection can be accessed with the format: {{=varname}}
.
This template:
{{#def.data.name}} owns these cars: {{~it.data.cars :car}} a {{=car.make}} {{=car.model}}, {{~}}.
will evaluate to:
Steve owns these cars: a Ford Mustang, a Subaru WRX, .
Tips
- The output has a trailing ", " at the end. Iterating data sets like this may be better handled by a function because the function can trim trailing characters or execute different logic per array member. Template collection iteration is well suited for use cases where each iteration ends in a newline or there are no trailing characters used.
- Iterating collections is only possible using the
it
variable. For more information, see the section Compile-time vs Evaluation-time vars above.