miniml
Version:
A minimal, embeddable semantic data modeling language for generating SQL queries from YAML model definitions. Inspired by LookML.
83 lines (81 loc) • 3.03 kB
JavaScript
import { loadYamlFile, loadYamlFileSync, parseYAML } from "./yaml.js";
import { renderJinjaTemplate } from "./jinja.js";
export function createModel(obj, file) {
const model = typeof obj === "string" ? parseYAML(obj) : obj;
if (!model.join)
model.join = {};
validateModel(model);
expandDimensions(model.dimensions);
expandMeasures(model.measures);
expandModelInfo(model, file);
return model;
}
export async function loadModel(file) {
const obj = await loadYamlFile(file);
return createModel(obj, file);
}
export function loadModelSync(file) {
const obj = loadYamlFileSync(file);
return createModel(obj, file);
}
function expandDimensions(dimensions) {
expandMetadataDefs(dimensions);
for (const obj of Object.values(dimensions))
if (!obj.sql)
obj.sql = obj.key;
else if (!/\s+AS\s+[a-z0-9_]+$/i.test(obj.sql))
obj.sql = `${obj.sql} AS ${obj.key}`;
}
function expandMeasures(measures) {
expandMetadataDefs(measures);
for (const obj of Object.values(measures))
if (!obj.sql)
obj.sql = `SUM(${obj.key}) AS ${obj.key}`;
else if (!/\s+AS\s+[a-z0-9_]+$/i.test(obj.sql))
obj.sql = `${obj.sql} AS ${obj.key}`;
}
function expandMetadataDefs(dictionary) {
for (const key of Object.keys(dictionary)) {
const obj = dictionary[key];
if (typeof obj === "string")
dictionary[key] = { key, description: obj };
else if (Array.isArray(obj))
dictionary[key] = { key, description: obj[0], sql: obj[1], join: obj[2] };
}
}
function expandModelInfo(model, file) {
model.info = `
## DIMENSIONS
{%- for dimension in dimensions %}
- \`{{ dimension.key }}\` {{ dimension.description }}
{%- endfor %}
## MEASURES
{%- for measure in measures %}
- \`{{ measure.key }}\` {{ measure.description }}
{%- endfor %}
${model.info || ""}`.trim();
model.info = renderJinjaTemplate(model.info, {
dimensions: Object.keys(model.dimensions).map(key => ({ key, description: model.dimensions[key].description })),
measures: Object.keys(model.measures).map(key => ({ key, description: model.measures[key].description }))
});
if (!model.dialect && file)
inferModelDialect(file);
if (model.dialect)
model.info += `\n\nUse ${model.dialect.toUpperCase()} syntax for generating SQL filter expressions.`;
}
function inferModelDialect(file) {
if (file.includes("bigquery"))
return "bigquery";
else if (file.includes("snowflake"))
return "snowflake";
else
throw new Error(`Unable to determine dialect for model file: ${file}`);
}
function validateModel(model) {
if (model.default_date_range_days !== undefined) {
if (!Number.isInteger(model.default_date_range_days) || model.default_date_range_days <= 0) {
throw new Error(`default_date_range_days must be a positive integer, got: ${model.default_date_range_days}`);
}
}
}
//# sourceMappingURL=load.js.map