UNPKG

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
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