Execution Environment (JavaScript)
Cube Data Model Compiler uses Node.js VM (opens in a new tab) to execute data model compiler code. It gives required flexibility allowing transpiling data model files before they get executed, storing data models in external databases and executing untrusted code in a safe manner. Cube data model JavaScript is standard JavaScript supported by Node.js starting in version 8 with the following exceptions.
Require
Being executed in VM, data model JavaScript code doesn't have access to Node.js
require (opens in a new tab) directly. Instead require()
is implemented by Data
Model Compiler to provide access to other data model files and to regular
Node.js modules. Besides that, the data model require()
can resolve Cube
packages such as Funnels
unlike standard Node.js require()
.
Node.js globals (process.env, console.log and others)
Data model JavaScript code doesn't have access to any standard Node.js globals
like process
or console
. In order to access process.env
, utility functions
can be added outside the model/
directory:
tablePrefix.js:
exports.tableSchema = () => process.env.TABLE_SCHEMA;
model/cubes/Users.js:
import { tableSchema } from "../tablePrefix";
cube(`users`, {
sql_table: `${tableSchema()}.users`,
// ...
});
console.log
Data models cannot access console.log
due to a separate VM
instance (opens in a new tab) that runs it. Suppose you find yourself writing complex
logic for SQL generation that depends on a lot of external input. In that case,
you probably want to introduce a helper service outside of schema
directory
that you can debug as usual Node.js code.
Cube globals (cube and others)
Cube defines cube()
, context()
and asyncModule()
global variable functions
in order to provide API for data model configuration which aren't normally
accessible outside of a Cube data model.
Import / Export
Data model JavaScript files are transpiled to convert ES6 import
and export
expressions to corresponding Node.js calls. In fact import
is routed to
Require method.
export
can be used to define named exports as well as default ones:
constants.js:
export const TEST_USER_IDS = [1, 2, 3, 4, 5];
usersSql.js:
export default (usersTable) => `select * from ${usersTable}`;
Later, you can import
into the cube, wherever needed:
Users.js:
// in users.js
import { TEST_USER_IDS } from "./constants";
import usersSql from "./usersSql";
cube(`users`, {
sql: usersSql(`users`),
measures: {
/* ... */
},
dimensions: {
/* ... */
},
segments: {
excludeTestUsers: {
sql: `${CUBE}.id NOT IN (${TEST_USER_IDS.join(", ")})`,
},
},
});
asyncModule
Data models can be externally stored and retrieved through an asynchronous
operation using the asyncModule()
. For more information, consult the Dynamic
Schema Creation page.
Context symbols transpile
Cube uses a custom transpiler to optimize boilerplate code around referencing
cubes and cube members. There are reserved property names inside cube
definition that undergo reference resolve transpiling process:
sql
measures
dimensions
segments
time_dimension
drill_members
context_members
Each of these properties inside cube
and context
definitions are transpiled
to functions with resolved arguments. For example:
cube(`users`, {
// ...
measures: {
count: {
type: `count`,
},
ratio: {
sql: `SUM(${CUBE}.amount) / ${count}`,
type: `number`,
},
},
});
is transpiled to:
cube(`users`, {
// ...
measures: {
count: {
type: `count`,
},
ratio: {
sql: (CUBE, count) => `SUM(${CUBE}.amount) / ${count}`,
type: `number`,
},
},
});
So for example if you want to pass the definition of ratio
outside of the
cube, you would define it as:
const measureRatioDefinition = {
sql: (CUBE, count) => `sum(${CUBE}.amount) / ${count}`,
type: `number`,
};
cube(`users`, {
// ...
measures: {
count: {
type: `count`,
},
ratio: measureRatioDefinition,
},
});