@bonnard/cli 0.1.13 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/bon.mjs +305 -620
- package/dist/bin/validate-DEh1XQnH.mjs +365 -0
- package/dist/docs/_index.md +1 -1
- package/dist/docs/topics/cubes.data-source.md +2 -2
- package/dist/docs/topics/cubes.dimensions.format.md +2 -2
- package/dist/docs/topics/cubes.dimensions.md +2 -2
- package/dist/docs/topics/cubes.dimensions.primary-key.md +2 -2
- package/dist/docs/topics/cubes.dimensions.sub-query.md +2 -2
- package/dist/docs/topics/cubes.dimensions.time.md +2 -2
- package/dist/docs/topics/cubes.dimensions.types.md +2 -2
- package/dist/docs/topics/cubes.extends.md +2 -2
- package/dist/docs/topics/cubes.hierarchies.md +2 -2
- package/dist/docs/topics/cubes.joins.md +2 -2
- package/dist/docs/topics/cubes.md +2 -2
- package/dist/docs/topics/cubes.measures.calculated.md +2 -2
- package/dist/docs/topics/cubes.measures.drill-members.md +2 -2
- package/dist/docs/topics/cubes.measures.filters.md +2 -2
- package/dist/docs/topics/cubes.measures.format.md +21 -2
- package/dist/docs/topics/cubes.measures.md +2 -2
- package/dist/docs/topics/cubes.measures.rolling.md +2 -2
- package/dist/docs/topics/cubes.measures.types.md +2 -2
- package/dist/docs/topics/cubes.public.md +2 -2
- package/dist/docs/topics/cubes.refresh-key.md +2 -2
- package/dist/docs/topics/cubes.segments.md +2 -2
- package/dist/docs/topics/cubes.sql.md +2 -2
- package/dist/docs/topics/features.catalog.md +31 -0
- package/dist/docs/topics/features.cli.md +59 -0
- package/dist/docs/topics/features.context-graph.md +18 -0
- package/dist/docs/topics/features.governance.md +84 -0
- package/dist/docs/topics/features.mcp.md +48 -0
- package/dist/docs/topics/features.md +15 -0
- package/dist/docs/topics/features.sdk.md +53 -0
- package/dist/docs/topics/features.semantic-layer.md +50 -0
- package/dist/docs/topics/features.slack-teams.md +18 -0
- package/dist/docs/topics/getting-started.md +2 -143
- package/dist/docs/topics/pre-aggregations.md +2 -2
- package/dist/docs/topics/pre-aggregations.rollup.md +2 -2
- package/dist/docs/topics/syntax.context-variables.md +2 -2
- package/dist/docs/topics/syntax.md +2 -2
- package/dist/docs/topics/syntax.references.md +2 -2
- package/dist/docs/topics/views.cubes.md +2 -2
- package/dist/docs/topics/views.folders.md +2 -2
- package/dist/docs/topics/views.includes.md +2 -2
- package/dist/docs/topics/views.md +2 -2
- package/dist/docs/topics/workflow.deploy.md +79 -14
- package/dist/docs/topics/workflow.mcp.md +19 -13
- package/dist/docs/topics/workflow.md +25 -8
- package/dist/docs/topics/workflow.query.md +2 -2
- package/dist/docs/topics/workflow.validate.md +4 -31
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +16 -26
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +16 -26
- package/dist/templates/shared/bonnard.md +31 -6
- package/package.json +4 -8
- package/dist/bin/validate-DiN3DaTl.mjs +0 -110
- /package/dist/bin/{cubes-De1_2_YJ.mjs → cubes-Bf0IPYd7.mjs} +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { t as getProjectPaths } from "./bon.mjs";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/lib/schema.ts
|
|
8
|
+
const identifier = z.string().regex(/^[_a-zA-Z][_a-zA-Z0-9]*$/, "must be a valid identifier (letters, numbers, underscores; cannot start with a number)");
|
|
9
|
+
const refreshKeySchema = z.object({
|
|
10
|
+
every: z.string().regex(/^\d+\s+(second|minute|hour|day|week)s?$/, { message: "must be a time interval like \"1 hour\", \"30 minute\", \"1 day\"" }).optional(),
|
|
11
|
+
sql: z.string().optional()
|
|
12
|
+
});
|
|
13
|
+
const measureTypes = [
|
|
14
|
+
"count",
|
|
15
|
+
"count_distinct",
|
|
16
|
+
"count_distinct_approx",
|
|
17
|
+
"sum",
|
|
18
|
+
"avg",
|
|
19
|
+
"min",
|
|
20
|
+
"max",
|
|
21
|
+
"number",
|
|
22
|
+
"string",
|
|
23
|
+
"time",
|
|
24
|
+
"boolean",
|
|
25
|
+
"running_total",
|
|
26
|
+
"number_agg"
|
|
27
|
+
];
|
|
28
|
+
const dimensionTypes = [
|
|
29
|
+
"string",
|
|
30
|
+
"number",
|
|
31
|
+
"boolean",
|
|
32
|
+
"time",
|
|
33
|
+
"geo",
|
|
34
|
+
"switch"
|
|
35
|
+
];
|
|
36
|
+
const relationshipTypes = [
|
|
37
|
+
"many_to_one",
|
|
38
|
+
"one_to_many",
|
|
39
|
+
"one_to_one"
|
|
40
|
+
];
|
|
41
|
+
const granularities = [
|
|
42
|
+
"second",
|
|
43
|
+
"minute",
|
|
44
|
+
"hour",
|
|
45
|
+
"day",
|
|
46
|
+
"week",
|
|
47
|
+
"month",
|
|
48
|
+
"quarter",
|
|
49
|
+
"year"
|
|
50
|
+
];
|
|
51
|
+
const preAggTypes = [
|
|
52
|
+
"rollup",
|
|
53
|
+
"original_sql",
|
|
54
|
+
"rollup_join",
|
|
55
|
+
"rollup_lambda"
|
|
56
|
+
];
|
|
57
|
+
const formats = [
|
|
58
|
+
"percent",
|
|
59
|
+
"currency",
|
|
60
|
+
"number",
|
|
61
|
+
"imageUrl",
|
|
62
|
+
"link",
|
|
63
|
+
"id"
|
|
64
|
+
];
|
|
65
|
+
const measureSchema = z.object({
|
|
66
|
+
name: identifier,
|
|
67
|
+
type: z.enum(measureTypes),
|
|
68
|
+
sql: z.string().optional(),
|
|
69
|
+
description: z.string().optional(),
|
|
70
|
+
title: z.string().optional(),
|
|
71
|
+
format: z.enum(formats).optional(),
|
|
72
|
+
public: z.boolean().optional(),
|
|
73
|
+
filters: z.array(z.object({ sql: z.string() })).optional(),
|
|
74
|
+
rolling_window: z.object({
|
|
75
|
+
trailing: z.string().optional(),
|
|
76
|
+
leading: z.string().optional(),
|
|
77
|
+
offset: z.string().optional()
|
|
78
|
+
}).optional(),
|
|
79
|
+
drill_members: z.array(z.string()).optional(),
|
|
80
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
81
|
+
});
|
|
82
|
+
const dimensionFormatSchema = z.union([z.enum(formats), z.string().startsWith("%")]);
|
|
83
|
+
const dimensionSchema = z.object({
|
|
84
|
+
name: identifier,
|
|
85
|
+
type: z.enum(dimensionTypes),
|
|
86
|
+
sql: z.string().optional(),
|
|
87
|
+
primary_key: z.boolean().optional(),
|
|
88
|
+
sub_query: z.boolean().optional(),
|
|
89
|
+
propagate_filters_to_sub_query: z.boolean().optional(),
|
|
90
|
+
description: z.string().optional(),
|
|
91
|
+
title: z.string().optional(),
|
|
92
|
+
format: dimensionFormatSchema.optional(),
|
|
93
|
+
public: z.boolean().optional(),
|
|
94
|
+
meta: z.record(z.string(), z.unknown()).optional(),
|
|
95
|
+
latitude: z.object({ sql: z.string() }).optional(),
|
|
96
|
+
longitude: z.object({ sql: z.string() }).optional(),
|
|
97
|
+
case: z.object({
|
|
98
|
+
when: z.array(z.object({
|
|
99
|
+
sql: z.string(),
|
|
100
|
+
label: z.string()
|
|
101
|
+
})),
|
|
102
|
+
else: z.object({ label: z.string() }).optional()
|
|
103
|
+
}).optional()
|
|
104
|
+
});
|
|
105
|
+
const joinSchema = z.object({
|
|
106
|
+
name: identifier,
|
|
107
|
+
relationship: z.enum(relationshipTypes),
|
|
108
|
+
sql: z.string()
|
|
109
|
+
});
|
|
110
|
+
const segmentSchema = z.object({
|
|
111
|
+
name: identifier,
|
|
112
|
+
sql: z.string(),
|
|
113
|
+
description: z.string().optional(),
|
|
114
|
+
title: z.string().optional(),
|
|
115
|
+
public: z.boolean().optional()
|
|
116
|
+
});
|
|
117
|
+
const preAggregationSchema = z.object({
|
|
118
|
+
name: identifier,
|
|
119
|
+
type: z.enum(preAggTypes).optional(),
|
|
120
|
+
measures: z.array(z.string()).optional(),
|
|
121
|
+
dimensions: z.array(z.string()).optional(),
|
|
122
|
+
time_dimension: z.string().optional(),
|
|
123
|
+
granularity: z.enum(granularities).optional(),
|
|
124
|
+
partition_granularity: z.enum(granularities).optional(),
|
|
125
|
+
refresh_key: refreshKeySchema.optional(),
|
|
126
|
+
scheduled_refresh: z.boolean().optional()
|
|
127
|
+
});
|
|
128
|
+
const hierarchySchema = z.object({
|
|
129
|
+
name: identifier,
|
|
130
|
+
levels: z.array(z.string()),
|
|
131
|
+
title: z.string().optional(),
|
|
132
|
+
public: z.boolean().optional()
|
|
133
|
+
});
|
|
134
|
+
const cubeSchema = z.object({
|
|
135
|
+
name: identifier,
|
|
136
|
+
sql: z.string().optional(),
|
|
137
|
+
sql_table: z.string().optional(),
|
|
138
|
+
data_source: z.string().optional(),
|
|
139
|
+
extends: z.string().optional(),
|
|
140
|
+
description: z.string().optional(),
|
|
141
|
+
title: z.string().optional(),
|
|
142
|
+
public: z.boolean().optional(),
|
|
143
|
+
refresh_key: refreshKeySchema.optional(),
|
|
144
|
+
measures: z.array(measureSchema).optional(),
|
|
145
|
+
dimensions: z.array(dimensionSchema).optional(),
|
|
146
|
+
joins: z.array(joinSchema).optional(),
|
|
147
|
+
segments: z.array(segmentSchema).optional(),
|
|
148
|
+
pre_aggregations: z.array(preAggregationSchema).optional(),
|
|
149
|
+
hierarchies: z.array(hierarchySchema).optional()
|
|
150
|
+
}).refine((data) => data.sql != null || data.sql_table != null || data.extends != null, { message: "sql, sql_table, or extends is required" });
|
|
151
|
+
const viewIncludeItemSchema = z.union([z.string(), z.object({
|
|
152
|
+
name: z.string(),
|
|
153
|
+
alias: z.string().optional(),
|
|
154
|
+
title: z.string().optional(),
|
|
155
|
+
description: z.string().optional(),
|
|
156
|
+
format: z.string().optional(),
|
|
157
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
158
|
+
})]);
|
|
159
|
+
const viewCubeRefSchema = z.object({
|
|
160
|
+
join_path: z.string(),
|
|
161
|
+
includes: z.union([z.literal("*"), z.array(viewIncludeItemSchema)]).optional(),
|
|
162
|
+
excludes: z.array(z.string()).optional(),
|
|
163
|
+
prefix: z.boolean().optional()
|
|
164
|
+
});
|
|
165
|
+
const folderSchema = z.lazy(() => z.object({
|
|
166
|
+
name: z.string(),
|
|
167
|
+
members: z.array(z.string()).optional(),
|
|
168
|
+
folders: z.array(folderSchema).optional()
|
|
169
|
+
}));
|
|
170
|
+
const viewMeasureSchema = z.object({
|
|
171
|
+
name: identifier,
|
|
172
|
+
sql: z.string(),
|
|
173
|
+
type: z.string(),
|
|
174
|
+
format: z.string().optional(),
|
|
175
|
+
description: z.string().optional(),
|
|
176
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
177
|
+
});
|
|
178
|
+
const viewDimensionSchema = z.object({
|
|
179
|
+
name: identifier,
|
|
180
|
+
sql: z.string(),
|
|
181
|
+
type: z.string(),
|
|
182
|
+
description: z.string().optional(),
|
|
183
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
184
|
+
});
|
|
185
|
+
const viewSegmentSchema = z.object({
|
|
186
|
+
name: identifier,
|
|
187
|
+
sql: z.string(),
|
|
188
|
+
description: z.string().optional()
|
|
189
|
+
});
|
|
190
|
+
const viewSchema = z.object({
|
|
191
|
+
name: identifier,
|
|
192
|
+
description: z.string().optional(),
|
|
193
|
+
title: z.string().optional(),
|
|
194
|
+
public: z.boolean().optional(),
|
|
195
|
+
cubes: z.array(viewCubeRefSchema).optional(),
|
|
196
|
+
measures: z.array(viewMeasureSchema).optional(),
|
|
197
|
+
dimensions: z.array(viewDimensionSchema).optional(),
|
|
198
|
+
segments: z.array(viewSegmentSchema).optional(),
|
|
199
|
+
folders: z.array(folderSchema).optional()
|
|
200
|
+
});
|
|
201
|
+
const fileSchema = z.object({
|
|
202
|
+
cubes: z.array(cubeSchema).optional(),
|
|
203
|
+
views: z.array(viewSchema).optional()
|
|
204
|
+
}).refine((data) => data.cubes && data.cubes.length > 0 || data.views && data.views.length > 0, "File must contain at least one cube or view");
|
|
205
|
+
function formatZodError(error, fileName, parsed) {
|
|
206
|
+
return error.issues.map((issue) => {
|
|
207
|
+
const pathParts = issue.path;
|
|
208
|
+
let entityContext = "";
|
|
209
|
+
if (pathParts.length >= 2) {
|
|
210
|
+
const collection = pathParts[0];
|
|
211
|
+
const index = pathParts[1];
|
|
212
|
+
const entity = parsed?.[collection]?.[index];
|
|
213
|
+
if (entity?.name) entityContext = ` (${entity.name})`;
|
|
214
|
+
}
|
|
215
|
+
const pathStr = pathParts.join(".");
|
|
216
|
+
const location = pathStr ? `${pathStr}${entityContext}` : "";
|
|
217
|
+
if (issue.code === "invalid_value" && "values" in issue) return `${fileName}: ${location} — invalid value, expected one of: ${issue.values.join(", ")}`;
|
|
218
|
+
return `${fileName}: ${location ? `${location} — ` : ""}${issue.message}`;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function validateFiles(files) {
|
|
222
|
+
const errors = [];
|
|
223
|
+
const cubes = [];
|
|
224
|
+
const views = [];
|
|
225
|
+
const allNames = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const file of files) {
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = YAML.parse(file.content);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
errors.push(`${file.fileName}: YAML parse error — ${err.message}`);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!parsed || typeof parsed !== "object") {
|
|
235
|
+
errors.push(`${file.fileName}: file is empty or not a YAML object`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const result = fileSchema.safeParse(parsed);
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
errors.push(...formatZodError(result.error, file.fileName, parsed));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (const cube of parsed.cubes ?? []) if (cube.name) {
|
|
244
|
+
const existing = allNames.get(cube.name);
|
|
245
|
+
if (existing) errors.push(`${file.fileName}: duplicate name '${cube.name}' (also defined in ${existing})`);
|
|
246
|
+
else {
|
|
247
|
+
allNames.set(cube.name, file.fileName);
|
|
248
|
+
cubes.push(cube.name);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const view of parsed.views ?? []) if (view.name) {
|
|
252
|
+
const existing = allNames.get(view.name);
|
|
253
|
+
if (existing) errors.push(`${file.fileName}: duplicate name '${view.name}' (also defined in ${existing})`);
|
|
254
|
+
else {
|
|
255
|
+
allNames.set(view.name, file.fileName);
|
|
256
|
+
views.push(view.name);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
errors,
|
|
262
|
+
cubes,
|
|
263
|
+
views
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/lib/validate.ts
|
|
269
|
+
function collectYamlFiles(dir, rootDir) {
|
|
270
|
+
if (!fs.existsSync(dir)) return [];
|
|
271
|
+
const results = [];
|
|
272
|
+
function walk(current) {
|
|
273
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
274
|
+
const fullPath = path.join(current, entry.name);
|
|
275
|
+
if (entry.isDirectory()) walk(fullPath);
|
|
276
|
+
else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push({
|
|
277
|
+
fileName: path.relative(rootDir, fullPath),
|
|
278
|
+
content: fs.readFileSync(fullPath, "utf-8")
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
walk(dir);
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
function checkMissingDescriptions(files) {
|
|
286
|
+
const missing = [];
|
|
287
|
+
for (const file of files) try {
|
|
288
|
+
const parsed = YAML.parse(file.content);
|
|
289
|
+
if (!parsed) continue;
|
|
290
|
+
const cubes = parsed.cubes || [];
|
|
291
|
+
for (const cube of cubes) {
|
|
292
|
+
if (!cube.name) continue;
|
|
293
|
+
if (!cube.description) missing.push({
|
|
294
|
+
parent: cube.name,
|
|
295
|
+
type: "cube",
|
|
296
|
+
name: cube.name
|
|
297
|
+
});
|
|
298
|
+
const measures = cube.measures || [];
|
|
299
|
+
for (const measure of measures) if (measure.name && !measure.description) missing.push({
|
|
300
|
+
parent: cube.name,
|
|
301
|
+
type: "measure",
|
|
302
|
+
name: measure.name
|
|
303
|
+
});
|
|
304
|
+
const dimensions = cube.dimensions || [];
|
|
305
|
+
for (const dimension of dimensions) if (dimension.name && !dimension.description) missing.push({
|
|
306
|
+
parent: cube.name,
|
|
307
|
+
type: "dimension",
|
|
308
|
+
name: dimension.name
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const views = parsed.views || [];
|
|
312
|
+
for (const view of views) {
|
|
313
|
+
if (!view.name) continue;
|
|
314
|
+
if (!view.description) missing.push({
|
|
315
|
+
parent: view.name,
|
|
316
|
+
type: "view",
|
|
317
|
+
name: view.name
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
} catch {}
|
|
321
|
+
return missing;
|
|
322
|
+
}
|
|
323
|
+
function checkMissingDataSource(files) {
|
|
324
|
+
const missing = [];
|
|
325
|
+
for (const file of files) try {
|
|
326
|
+
const parsed = YAML.parse(file.content);
|
|
327
|
+
if (!parsed) continue;
|
|
328
|
+
for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
|
|
329
|
+
} catch {}
|
|
330
|
+
return missing;
|
|
331
|
+
}
|
|
332
|
+
async function validate(projectPath) {
|
|
333
|
+
const paths = getProjectPaths(projectPath);
|
|
334
|
+
const files = [...collectYamlFiles(paths.cubes, projectPath), ...collectYamlFiles(paths.views, projectPath)];
|
|
335
|
+
if (files.length === 0) return {
|
|
336
|
+
valid: true,
|
|
337
|
+
errors: [],
|
|
338
|
+
cubes: [],
|
|
339
|
+
views: [],
|
|
340
|
+
missingDescriptions: [],
|
|
341
|
+
cubesMissingDataSource: []
|
|
342
|
+
};
|
|
343
|
+
const result = validateFiles(files);
|
|
344
|
+
if (result.errors.length > 0) return {
|
|
345
|
+
valid: false,
|
|
346
|
+
errors: result.errors,
|
|
347
|
+
cubes: [],
|
|
348
|
+
views: [],
|
|
349
|
+
missingDescriptions: [],
|
|
350
|
+
cubesMissingDataSource: []
|
|
351
|
+
};
|
|
352
|
+
const missingDescriptions = checkMissingDescriptions(files);
|
|
353
|
+
const cubesMissingDataSource = checkMissingDataSource(files);
|
|
354
|
+
return {
|
|
355
|
+
valid: true,
|
|
356
|
+
errors: [],
|
|
357
|
+
cubes: result.cubes,
|
|
358
|
+
views: result.views,
|
|
359
|
+
missingDescriptions,
|
|
360
|
+
cubesMissingDataSource
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
//#endregion
|
|
365
|
+
export { validate };
|
package/dist/docs/_index.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Data Source
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The data_source property connects cubes to specific data warehouse connections. Use it when your semantic layer spans multiple databases like PostgreSQL, Snowflake, or BigQuery.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dimension Format
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The format property controls how dimension values are displayed to consumers. Apply date formatting, number formatting, or custom patterns to make dimension output human-readable.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dimensions
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Dimensions define the attributes used for grouping and filtering data in your semantic layer. They map to table columns and provide the axes for slicing measures in queries.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Primary Key
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The primary_key property marks a dimension as the unique identifier for a cube. Primary keys are required for count_distinct measures and for establishing correct join relationships.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sub-Query Dimensions
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Sub-query dimensions let you bring a measure from another cube into a dimension using a correlated subquery. Useful for denormalizing aggregated values like "lifetime revenue per user."
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Time Dimensions
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Time dimensions enable date and time-based analysis in your semantic layer. They support automatic granularity (day, week, month, year) and power time-series queries and date filters.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Dimension Types
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Reference for all 6 dimension types in Bonnard: string, number, boolean, time, geo, and json. Each type determines how the dimension is stored, indexed, and queried.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Extends
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Extends lets you inherit measures, dimensions, and joins from other cubes to reduce duplication. Build base cubes with shared logic and extend them for specific use cases.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hierarchies
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Hierarchies define drill-down paths for dimensional analysis in your semantic layer. Set up parent-child relationships between dimensions so consumers can explore data at different levels.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Joins
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Joins connect cubes together so you can query measures and dimensions across multiple tables. Define one-to-many, many-to-one, and one-to-one relationships between cubes.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Calculated Measures
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Calculated measures let you build complex metrics from other measures in the same cube. Combine existing aggregations to create ratios, percentages, and derived metrics without raw SQL.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Drill Members
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Drill members define which dimensions are shown when a user drills into a measure. Configure drill-down paths so consumers can explore aggregated numbers down to individual records.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Measure Filters
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Measure filters apply permanent WHERE conditions to measures for conditional aggregations. Create filtered metrics like "revenue from paid plans" or "active users in the last 30 days."
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Measure Format
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The format property controls how measure values are displayed to consumers. Specify currency symbols, decimal places, percentage formatting, and custom number patterns.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -27,6 +27,12 @@ measures:
|
|
|
27
27
|
|
|
28
28
|
## Supported Formats
|
|
29
29
|
|
|
30
|
+
| Format | Description |
|
|
31
|
+
|--------|-------------|
|
|
32
|
+
| `percent` | Percentage display (0.75 → 75%) |
|
|
33
|
+
| `currency` | Monetary display ($1,234.56) |
|
|
34
|
+
| `number` | Plain number with standard formatting |
|
|
35
|
+
|
|
30
36
|
### percent
|
|
31
37
|
|
|
32
38
|
Displays value as a percentage:
|
|
@@ -53,6 +59,19 @@ Displays value as monetary amount:
|
|
|
53
59
|
|
|
54
60
|
Output: `$1,234.56` (formatting depends on BI tool locale)
|
|
55
61
|
|
|
62
|
+
### number
|
|
63
|
+
|
|
64
|
+
Standard number formatting with grouping separators:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- name: total_slots
|
|
68
|
+
type: sum
|
|
69
|
+
sql: slot_count
|
|
70
|
+
format: number
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Output: `1,234` instead of raw `1234`. Use this when the measure represents a count or quantity that benefits from thousand separators.
|
|
74
|
+
|
|
56
75
|
## Usage Notes
|
|
57
76
|
|
|
58
77
|
### Format vs Calculation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Measures
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Measures define the metrics and aggregations in your semantic layer. Use them to calculate sums, counts, averages, and custom expressions that stay consistent across every consumer.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Rolling Measures
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Rolling measures calculate aggregations over sliding time windows like 7-day averages, 30-day sums, and month-to-date totals. Define the window type, trailing period, and offset.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Measure Types
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Reference for all 12 measure types available in Bonnard: count, sum, avg, min, max, count_distinct, running_total, and more. Each type maps to a specific SQL aggregation.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Public
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The public property controls whether cubes, measures, and dimensions are exposed in the API. Set public to false to hide internal implementation details from consumers.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Refresh Key
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The refresh_key property controls when cube data is refreshed in the cache. Define time-based or query-based refresh strategies to balance data freshness with query performance.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Segments
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Segments define reusable row-level filters that can be applied to any query on a cube. Use them for common filters like "active users" or "paid orders" that multiple consumers need.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SQL
|
|
2
2
|
|
|
3
|
-
> Define the SQL table or subquery that powers a cube.
|
|
3
|
+
> Define the SQL table or subquery that powers a cube. Use the sql property to point to a physical table, or write a SELECT statement for derived datasets and transformations.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Catalog
|
|
2
|
+
|
|
3
|
+
> Browse and understand your data model — no code required.
|
|
4
|
+
|
|
5
|
+
The Bonnard catalog gives everyone on your team a live view of your semantic layer. Browse cubes, views, measures, and dimensions from the browser. Understand what data is available before writing a single query.
|
|
6
|
+
|
|
7
|
+
## What you can explore
|
|
8
|
+
|
|
9
|
+
- **Cubes and Views** — See every deployed source with field counts at a glance
|
|
10
|
+
- **Measures** — Aggregation type, SQL expression, format (currency, percentage), and description
|
|
11
|
+
- **Dimensions** — Data type, time granularity options, and custom metadata
|
|
12
|
+
- **Segments** — Pre-defined filters available for queries
|
|
13
|
+
|
|
14
|
+
## Field-level detail
|
|
15
|
+
|
|
16
|
+
Click any field to see exactly how it's calculated:
|
|
17
|
+
|
|
18
|
+
- **SQL expression** — The underlying query logic
|
|
19
|
+
- **Type and format** — How the field is aggregated and displayed
|
|
20
|
+
- **Origin cube** — Which cube a view field traces back to
|
|
21
|
+
- **Referenced fields** — Dependencies this field relies on
|
|
22
|
+
- **Custom metadata** — Tags, labels, and annotations set by your data team
|
|
23
|
+
|
|
24
|
+
## Built for business users
|
|
25
|
+
|
|
26
|
+
The catalog is designed for anyone who needs to understand the data, not just engineers. No YAML, no terminal, no warehouse credentials. Browse the schema, read descriptions, and know exactly what to ask your AI agent for.
|
|
27
|
+
|
|
28
|
+
## See Also
|
|
29
|
+
|
|
30
|
+
- [views](views) — How to create curated views for your team
|
|
31
|
+
- [cubes.public](cubes.public) — Control which cubes are visible
|