@bonnard/cli 0.1.13 → 0.2.0
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 +249 -5
- package/dist/bin/{validate-DiN3DaTl.mjs → validate-C31hmPk8.mjs} +15 -3
- 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 +2 -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 +60 -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 -5
- package/dist/docs/topics/workflow.query.md +2 -2
- package/dist/docs/topics/workflow.validate.md +2 -2
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +12 -3
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +12 -3
- package/dist/templates/shared/bonnard.md +31 -4
- package/package.json +1 -1
package/dist/bin/bon.mjs
CHANGED
|
@@ -2279,7 +2279,7 @@ async function validateCommand(options = {}) {
|
|
|
2279
2279
|
console.log(pc.red("No bon.yaml found. Are you in a Bonnard project?"));
|
|
2280
2280
|
process.exit(1);
|
|
2281
2281
|
}
|
|
2282
|
-
const { validate } = await import("./validate-
|
|
2282
|
+
const { validate } = await import("./validate-C31hmPk8.mjs");
|
|
2283
2283
|
const result = await validate(cwd);
|
|
2284
2284
|
if (result.cubes.length === 0 && result.views.length === 0 && result.valid) {
|
|
2285
2285
|
console.log(pc.yellow(`No cube or view files found in ${BONNARD_DIR}/cubes/ or ${BONNARD_DIR}/views/.`));
|
|
@@ -2307,6 +2307,13 @@ async function validateCommand(options = {}) {
|
|
|
2307
2307
|
}
|
|
2308
2308
|
for (const [parent, items] of byParent) console.log(pc.dim(` ${parent}: ${items.join(", ")}`));
|
|
2309
2309
|
}
|
|
2310
|
+
if (result.cubesMissingDataSource.length > 0) {
|
|
2311
|
+
console.log();
|
|
2312
|
+
console.log(pc.yellow(`⚠ ${result.cubesMissingDataSource.length} cube(s) missing data_source`));
|
|
2313
|
+
console.log(pc.dim(" Without an explicit data_source, cubes use the default warehouse."));
|
|
2314
|
+
console.log(pc.dim(" This can cause issues when multiple warehouses are configured."));
|
|
2315
|
+
console.log(pc.dim(` ${result.cubesMissingDataSource.join(", ")}`));
|
|
2316
|
+
}
|
|
2310
2317
|
if (options.testConnection) {
|
|
2311
2318
|
console.log();
|
|
2312
2319
|
await testReferencedConnections(cwd);
|
|
@@ -2383,6 +2390,9 @@ function collectFiles(dir, rootDir) {
|
|
|
2383
2390
|
walk(dir);
|
|
2384
2391
|
return files;
|
|
2385
2392
|
}
|
|
2393
|
+
function capitalize$1(s) {
|
|
2394
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2395
|
+
}
|
|
2386
2396
|
async function deployCommand(options = {}) {
|
|
2387
2397
|
const cwd = process.cwd();
|
|
2388
2398
|
const paths = getProjectPaths(cwd);
|
|
@@ -2391,7 +2401,7 @@ async function deployCommand(options = {}) {
|
|
|
2391
2401
|
process.exit(1);
|
|
2392
2402
|
}
|
|
2393
2403
|
console.log(pc.dim("Validating cubes and views..."));
|
|
2394
|
-
const { validate } = await import("./validate-
|
|
2404
|
+
const { validate } = await import("./validate-C31hmPk8.mjs");
|
|
2395
2405
|
const result = await validate(cwd);
|
|
2396
2406
|
if (!result.valid) {
|
|
2397
2407
|
console.log(pc.red("Validation failed:\n"));
|
|
@@ -2412,10 +2422,40 @@ async function deployCommand(options = {}) {
|
|
|
2412
2422
|
console.log(pc.dim(`Deploying ${fileCount} file(s)...`));
|
|
2413
2423
|
console.log();
|
|
2414
2424
|
try {
|
|
2415
|
-
const response = await post("/api/deploy", {
|
|
2425
|
+
const response = await post("/api/deploy", {
|
|
2426
|
+
files,
|
|
2427
|
+
...options.message && { message: options.message }
|
|
2428
|
+
});
|
|
2416
2429
|
console.log(pc.green("Deploy successful!"));
|
|
2417
2430
|
console.log(`Deployment ID: ${pc.cyan(response.deployment.id)}`);
|
|
2418
|
-
console.log(
|
|
2431
|
+
console.log();
|
|
2432
|
+
if (response.deployment.isFirstDeploy) console.log(pc.dim(` First deployment — ${response.deployment.fileCount} files uploaded`));
|
|
2433
|
+
else if (response.deployment.changes && response.deployment.changes.details.length > 0) {
|
|
2434
|
+
const { changes } = response.deployment;
|
|
2435
|
+
console.log(pc.dim("Changes from previous deploy:"));
|
|
2436
|
+
console.log();
|
|
2437
|
+
for (const c of changes.details) {
|
|
2438
|
+
const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
|
|
2439
|
+
const label = `${capitalize$1(c.changeType)} ${c.objectType}: ${c.objectName}`;
|
|
2440
|
+
const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
|
|
2441
|
+
const summaryTag = c.summary ? pc.dim(` — ${c.summary}`) : "";
|
|
2442
|
+
console.log(` ${prefix} ${label}${summaryTag}${breakingTag}`);
|
|
2443
|
+
}
|
|
2444
|
+
if (changes.breaking > 0) {
|
|
2445
|
+
console.log();
|
|
2446
|
+
console.log(pc.red(`${changes.breaking} breaking change${changes.breaking > 1 ? "s" : ""} detected`));
|
|
2447
|
+
}
|
|
2448
|
+
console.log();
|
|
2449
|
+
console.log(pc.bold("Annotate these changes with reasoning:"));
|
|
2450
|
+
console.log();
|
|
2451
|
+
const annotationTemplate = { annotations: changes.details.map((c) => ({
|
|
2452
|
+
objectName: c.objectName,
|
|
2453
|
+
annotation: `Why ${c.objectName} was ${c.changeType}`
|
|
2454
|
+
})) };
|
|
2455
|
+
console.log(` bon annotate ${response.deployment.id} --data '${JSON.stringify(annotationTemplate)}'`);
|
|
2456
|
+
console.log();
|
|
2457
|
+
console.log(pc.dim("Replace each annotation value with the reasoning behind the change."));
|
|
2458
|
+
} else console.log(pc.dim(" No changes detected from previous deployment."));
|
|
2419
2459
|
console.log();
|
|
2420
2460
|
console.log(pc.bold("Connect AI agents via MCP:"));
|
|
2421
2461
|
console.log(` MCP URL: ${pc.cyan("https://mcp.bonnard.dev/mcp")}`);
|
|
@@ -2532,6 +2572,207 @@ async function testAndSyncDatasources(cwd, options = {}) {
|
|
|
2532
2572
|
return false;
|
|
2533
2573
|
}
|
|
2534
2574
|
|
|
2575
|
+
//#endregion
|
|
2576
|
+
//#region src/commands/deployments.ts
|
|
2577
|
+
function relativeTime(dateStr) {
|
|
2578
|
+
const now = Date.now();
|
|
2579
|
+
const then = new Date(dateStr).getTime();
|
|
2580
|
+
const seconds = Math.floor((now - then) / 1e3);
|
|
2581
|
+
if (seconds < 60) return "just now";
|
|
2582
|
+
const minutes = Math.floor(seconds / 60);
|
|
2583
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
2584
|
+
const hours = Math.floor(minutes / 60);
|
|
2585
|
+
if (hours < 24) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
2586
|
+
const days = Math.floor(hours / 24);
|
|
2587
|
+
return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
2588
|
+
}
|
|
2589
|
+
function truncate(str, max) {
|
|
2590
|
+
if (str.length <= max) return str;
|
|
2591
|
+
return str.slice(0, max - 1) + "…";
|
|
2592
|
+
}
|
|
2593
|
+
function statusColor(status) {
|
|
2594
|
+
switch (status) {
|
|
2595
|
+
case "success": return pc.green(status);
|
|
2596
|
+
case "failed": return pc.red(status);
|
|
2597
|
+
case "processing": return pc.yellow(status);
|
|
2598
|
+
default: return pc.dim(status);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async function deploymentsCommand(options = {}) {
|
|
2602
|
+
const limit = options.all ? 100 : 10;
|
|
2603
|
+
let response;
|
|
2604
|
+
try {
|
|
2605
|
+
response = await get(`/api/deploy/history?limit=${limit}`);
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
console.log(pc.red(`Failed to fetch deployments: ${err instanceof Error ? err.message : err}`));
|
|
2608
|
+
process.exit(1);
|
|
2609
|
+
}
|
|
2610
|
+
const { deployments } = response;
|
|
2611
|
+
if (options.format === "json") {
|
|
2612
|
+
console.log(JSON.stringify(deployments, null, 2));
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
console.log();
|
|
2616
|
+
console.log(pc.bold("Deployments for Bonnard"));
|
|
2617
|
+
console.log();
|
|
2618
|
+
if (deployments.length === 0) {
|
|
2619
|
+
console.log(pc.dim(" No deployments found."));
|
|
2620
|
+
console.log();
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const colId = 10;
|
|
2624
|
+
const colStatus = 12;
|
|
2625
|
+
const colFiles = 7;
|
|
2626
|
+
const colMessage = 32;
|
|
2627
|
+
console.log(pc.dim(" " + "ID".padEnd(colId) + "Status".padEnd(colStatus) + "Files".padEnd(colFiles) + "Message".padEnd(colMessage) + "Deployed"));
|
|
2628
|
+
for (const d of deployments) {
|
|
2629
|
+
const id = d.id.slice(0, 8);
|
|
2630
|
+
const status = statusColor(d.status);
|
|
2631
|
+
const statusPad = " ".repeat(Math.max(0, colStatus - d.status.length));
|
|
2632
|
+
const files = String(d.fileCount);
|
|
2633
|
+
const message = d.message ? truncate(d.message, 30) : "—";
|
|
2634
|
+
const time = relativeTime(d.createdAt);
|
|
2635
|
+
console.log(" " + id.padEnd(colId) + status + statusPad + files.padEnd(colFiles) + message.padEnd(colMessage) + pc.dim(time));
|
|
2636
|
+
}
|
|
2637
|
+
console.log();
|
|
2638
|
+
console.log(pc.dim(`Showing ${deployments.length} deployment${deployments.length !== 1 ? "s" : ""}.`));
|
|
2639
|
+
console.log();
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
//#endregion
|
|
2643
|
+
//#region src/commands/annotate.ts
|
|
2644
|
+
function readStdin() {
|
|
2645
|
+
return new Promise((resolve) => {
|
|
2646
|
+
if (process.stdin.isTTY) {
|
|
2647
|
+
resolve(null);
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
let data = "";
|
|
2651
|
+
process.stdin.setEncoding("utf8");
|
|
2652
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
2653
|
+
process.stdin.on("end", () => resolve(data.trim() || null));
|
|
2654
|
+
setTimeout(() => resolve(data.trim() || null), 100);
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
function parseAnnotations(raw) {
|
|
2658
|
+
let parsed;
|
|
2659
|
+
try {
|
|
2660
|
+
parsed = JSON.parse(raw);
|
|
2661
|
+
} catch {
|
|
2662
|
+
throw new Error("Invalid JSON. Expected: {\"annotations\": [{\"objectName\": \"...\", \"annotation\": \"...\"}]}");
|
|
2663
|
+
}
|
|
2664
|
+
const obj = parsed;
|
|
2665
|
+
if (!obj.annotations || !Array.isArray(obj.annotations) || obj.annotations.length === 0) throw new Error("JSON must contain a non-empty \"annotations\" array");
|
|
2666
|
+
for (const entry of obj.annotations) {
|
|
2667
|
+
const e = entry;
|
|
2668
|
+
if (!e.objectName || typeof e.objectName !== "string") throw new Error("Each annotation must have a string \"objectName\"");
|
|
2669
|
+
if (!e.annotation || typeof e.annotation !== "string") throw new Error(`Missing \"annotation\" text for \"${e.objectName}\"`);
|
|
2670
|
+
if (e.annotation.length > 1e3) throw new Error(`Annotation for \"${e.objectName}\" exceeds 1000 chars`);
|
|
2671
|
+
}
|
|
2672
|
+
return { annotations: obj.annotations };
|
|
2673
|
+
}
|
|
2674
|
+
async function annotateCommand(id, options = {}) {
|
|
2675
|
+
const raw = options.data || await readStdin();
|
|
2676
|
+
if (!raw) {
|
|
2677
|
+
try {
|
|
2678
|
+
const { changes } = await get(`/api/deploy/changes/${id}`);
|
|
2679
|
+
if (changes.length === 0) {
|
|
2680
|
+
console.log(pc.dim("No changes recorded for this deployment."));
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
console.log();
|
|
2684
|
+
console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
|
|
2685
|
+
console.log();
|
|
2686
|
+
for (const c of changes) {
|
|
2687
|
+
const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
|
|
2688
|
+
const label = `${c.changeType} ${c.objectType}: ${c.objectName}`;
|
|
2689
|
+
const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
|
|
2690
|
+
console.log(` ${prefix} ${label}${breakingTag}`);
|
|
2691
|
+
if (c.annotation) console.log(pc.dim(` "${c.annotation}"`));
|
|
2692
|
+
}
|
|
2693
|
+
console.log();
|
|
2694
|
+
console.log(pc.dim("To annotate, provide JSON via --data or stdin:"));
|
|
2695
|
+
console.log(pc.dim(` bon annotate ${id.slice(0, 8)} --data '{"annotations":[{"objectName":"...","annotation":"..."}]}'`));
|
|
2696
|
+
console.log();
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
|
|
2699
|
+
process.exit(1);
|
|
2700
|
+
}
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
let payload;
|
|
2704
|
+
try {
|
|
2705
|
+
payload = parseAnnotations(raw);
|
|
2706
|
+
} catch (err) {
|
|
2707
|
+
console.log(pc.red(err instanceof Error ? err.message : String(err)));
|
|
2708
|
+
process.exit(1);
|
|
2709
|
+
}
|
|
2710
|
+
try {
|
|
2711
|
+
const response = await post(`/api/deploy/annotate/${id}`, payload);
|
|
2712
|
+
console.log(pc.green(`Annotated ${response.updated}/${response.total} changes.`));
|
|
2713
|
+
} catch (err) {
|
|
2714
|
+
console.log(pc.red(`Failed to submit annotations: ${err instanceof Error ? err.message : err}`));
|
|
2715
|
+
process.exit(1);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
//#endregion
|
|
2720
|
+
//#region src/commands/diff.ts
|
|
2721
|
+
async function diffCommand(id, options = {}) {
|
|
2722
|
+
let response;
|
|
2723
|
+
try {
|
|
2724
|
+
response = await get(`/api/deploy/changes/${id}`);
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
|
|
2727
|
+
process.exit(1);
|
|
2728
|
+
}
|
|
2729
|
+
let { changes } = response;
|
|
2730
|
+
if (options.breaking) changes = changes.filter((c) => c.breaking);
|
|
2731
|
+
if (options.format === "json") {
|
|
2732
|
+
console.log(JSON.stringify(changes, null, 2));
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
console.log();
|
|
2736
|
+
console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
|
|
2737
|
+
console.log();
|
|
2738
|
+
if (changes.length === 0) {
|
|
2739
|
+
console.log(pc.dim(" No changes recorded."));
|
|
2740
|
+
console.log();
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
for (const c of changes) {
|
|
2744
|
+
const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
|
|
2745
|
+
const label = `${capitalize(c.changeType)} ${c.objectType}: ${c.objectName}`;
|
|
2746
|
+
const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
|
|
2747
|
+
console.log(` ${prefix} ${label}${breakingTag}`);
|
|
2748
|
+
const details = [];
|
|
2749
|
+
if (c.summary) details.push(c.summary);
|
|
2750
|
+
if (details.length > 0) console.log(pc.dim(` ${details.join(" | ")}`));
|
|
2751
|
+
if (c.changeType === "modified" && c.previousDefinition && c.newDefinition) for (const key of Object.keys(c.newDefinition)) {
|
|
2752
|
+
const oldVal = c.previousDefinition[key];
|
|
2753
|
+
const newVal = c.newDefinition[key];
|
|
2754
|
+
if (oldVal !== newVal && oldVal !== void 0 && newVal !== void 0) console.log(pc.dim(` ${key}: ${JSON.stringify(oldVal)} -> ${JSON.stringify(newVal)}`));
|
|
2755
|
+
}
|
|
2756
|
+
if (c.annotation) console.log(` ${pc.cyan("\"" + c.annotation + "\"")}`);
|
|
2757
|
+
console.log();
|
|
2758
|
+
}
|
|
2759
|
+
const added = changes.filter((c) => c.changeType === "added").length;
|
|
2760
|
+
const modified = changes.filter((c) => c.changeType === "modified").length;
|
|
2761
|
+
const removed = changes.filter((c) => c.changeType === "removed").length;
|
|
2762
|
+
const breaking = changes.filter((c) => c.breaking).length;
|
|
2763
|
+
const parts = [
|
|
2764
|
+
`${added} added`,
|
|
2765
|
+
`${modified} modified`,
|
|
2766
|
+
`${removed} removed`
|
|
2767
|
+
];
|
|
2768
|
+
if (breaking > 0) parts.push(pc.red(`${breaking} breaking`));
|
|
2769
|
+
console.log(pc.dim(`Summary: ${parts.join(", ")}`));
|
|
2770
|
+
console.log();
|
|
2771
|
+
}
|
|
2772
|
+
function capitalize(s) {
|
|
2773
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2535
2776
|
//#endregion
|
|
2536
2777
|
//#region src/commands/mcp.ts
|
|
2537
2778
|
const MCP_URL = "https://mcp.bonnard.dev/mcp";
|
|
@@ -2857,7 +3098,10 @@ datasource.command("remove").description("Remove a data source from .bon/datasou
|
|
|
2857
3098
|
datasource.command("push").description("Push a local data source to Bonnard server (requires login)").argument("<name>", "Data source name from .bon/datasources.yaml").option("--force", "Overwrite if already exists on remote").action(datasourcePushCommand);
|
|
2858
3099
|
program.command("preview").description("Preview data from a local warehouse using raw SQL (for development/exploration)").argument("<datasource>", "Data source name from .bon/datasources.yaml").argument("<sql>", "SQL query to execute").option("--schema <schema>", "Override schema").option("--database <database>", "Override database").option("--limit <limit>", "Max rows to return", "1000").option("--format <format>", "Output format: toon or json", "toon").action(previewCommand);
|
|
2859
3100
|
program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").option("--test-connection", "Also test datasource connections (warns on failure, doesn't block)").action(validateCommand);
|
|
2860
|
-
program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, tests connections (fails on error)").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").action(deployCommand);
|
|
3101
|
+
program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, tests connections (fails on error)").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").requiredOption("-m, --message <text>", "Deploy message describing your changes").action(deployCommand);
|
|
3102
|
+
program.command("deployments").description("List deployment history").option("--all", "Show all deployments (default: last 10)").option("--format <format>", "Output format: table or json", "table").action(deploymentsCommand);
|
|
3103
|
+
program.command("diff").description("Show changes in a deployment").argument("<id>", "Deployment ID").option("--format <format>", "Output format: table or json", "table").option("--breaking", "Show only breaking changes").action(diffCommand);
|
|
3104
|
+
program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);
|
|
2861
3105
|
program.command("mcp").description("MCP connection info and setup instructions").action(mcpCommand).command("test").description("Test MCP server connectivity").action(mcpTestCommand);
|
|
2862
3106
|
program.command("query").description("Execute a query against the deployed semantic layer").argument("<query>", "JSON query or SQL (with --sql flag)").option("--sql", "Use SQL API instead of JSON format").option("--limit <limit>", "Max rows to return").option("--format <format>", "Output format: toon or json", "toon").action(cubeQueryCommand);
|
|
2863
3107
|
program.command("docs").description("Browse documentation for building cubes and views").argument("[topic]", "Topic to display (e.g., cubes, cubes.measures)").option("-r, --recursive", "Show topic and all child topics").option("-s, --search <query>", "Search topics for a keyword").option("-f, --format <format>", "Output format: markdown or json", "markdown").action(docsCommand).command("schema").description("Show JSON schema for a type (cube, view, measure, etc.)").argument("<type>", "Schema type to display").action(docsSchemaCommand);
|
|
@@ -59,6 +59,15 @@ function checkMissingDescriptions(files) {
|
|
|
59
59
|
} catch {}
|
|
60
60
|
return missing;
|
|
61
61
|
}
|
|
62
|
+
function checkMissingDataSource(files) {
|
|
63
|
+
const missing = [];
|
|
64
|
+
for (const file of files) try {
|
|
65
|
+
const parsed = YAML.parse(file.content);
|
|
66
|
+
if (!parsed) continue;
|
|
67
|
+
for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
|
|
68
|
+
} catch {}
|
|
69
|
+
return missing;
|
|
70
|
+
}
|
|
62
71
|
function createModelRepository(projectPath) {
|
|
63
72
|
const paths = getProjectPaths(projectPath);
|
|
64
73
|
const cubesDir = paths.cubes;
|
|
@@ -79,7 +88,8 @@ async function validate(projectPath) {
|
|
|
79
88
|
errors: [],
|
|
80
89
|
cubes: [],
|
|
81
90
|
views: [],
|
|
82
|
-
missingDescriptions: []
|
|
91
|
+
missingDescriptions: [],
|
|
92
|
+
cubesMissingDataSource: []
|
|
83
93
|
};
|
|
84
94
|
try {
|
|
85
95
|
const { cubeEvaluator } = await compile(repo, {});
|
|
@@ -92,7 +102,8 @@ async function validate(projectPath) {
|
|
|
92
102
|
errors: [],
|
|
93
103
|
cubes,
|
|
94
104
|
views,
|
|
95
|
-
missingDescriptions: checkMissingDescriptions(files)
|
|
105
|
+
missingDescriptions: checkMissingDescriptions(files),
|
|
106
|
+
cubesMissingDataSource: checkMissingDataSource(files)
|
|
96
107
|
};
|
|
97
108
|
} catch (err) {
|
|
98
109
|
const raw = err.messages ?? err.message ?? String(err);
|
|
@@ -101,7 +112,8 @@ async function validate(projectPath) {
|
|
|
101
112
|
errors: Array.isArray(raw) ? raw : [raw],
|
|
102
113
|
cubes: [],
|
|
103
114
|
views: [],
|
|
104
|
-
missingDescriptions: []
|
|
115
|
+
missingDescriptions: [],
|
|
116
|
+
cubesMissingDataSource: []
|
|
105
117
|
};
|
|
106
118
|
}
|
|
107
119
|
}
|
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
|
|
|
@@ -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
|