Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@bonnard/cli

Package Overview
Dependencies
Maintainers
1
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bonnard/cli - npm Package Compare versions

Comparing version
0.3.11
to
0.3.12
+4
dist/bin/api-B3VngFMP.mjs
import { i as put, n as get, r as post, t as del } from "./api-dRafBjyt.mjs";
import "./config-_EMZ9qeI.mjs";
export { del, get };
import { i as loadConfig, r as isSelfHosted, t as getBaseUrl } from "./config-_EMZ9qeI.mjs";
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import pc from "picocolors";
//#region src/lib/credentials.ts
const CREDENTIALS_DIR = path.join(os.homedir(), ".config", "bon");
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
function saveCredentials(credentials) {
fs.mkdirSync(CREDENTIALS_DIR, {
recursive: true,
mode: 448
});
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
}
function loadCredentials() {
try {
const raw = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
const parsed = JSON.parse(raw);
if (parsed.token && parsed.email) return parsed;
return null;
} catch {
return null;
}
}
function clearCredentials() {
try {
fs.unlinkSync(CREDENTIALS_FILE);
} catch {}
}
//#endregion
//#region src/lib/api.ts
const { version } = createRequire(import.meta.url)("../../package.json");
const USER_AGENT = `bon-cli/${version} node-${process.version} ${os.platform()} (${os.arch()})`;
const VERCEL_BYPASS = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
function getToken() {
const config = loadConfig();
if (isSelfHosted(config)) return config?.admin_token || process.env.ADMIN_TOKEN || null;
const creds = loadCredentials();
if (!creds) {
console.error(pc.red("Not logged in. Run `bon login` first."));
process.exit(1);
}
return creds.token;
}
async function request(method, path, body) {
const config = loadConfig();
const token = getToken();
const url = `${getBaseUrl(config)}${path}`;
const headers = {
"Content-Type": "application/json",
"User-Agent": USER_AGENT
};
if (token) headers.Authorization = `Bearer ${token}`;
if (VERCEL_BYPASS && !isSelfHosted(config)) headers["x-vercel-protection-bypass"] = VERCEL_BYPASS;
const res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : void 0,
signal: AbortSignal.timeout(3e4)
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
data = {};
}
if (!res.ok) {
const message = data.error || `HTTP ${res.status}: ${res.statusText}`;
throw new Error(message);
}
return data;
}
function get(path) {
return request("GET", path);
}
function post(path, body) {
return request("POST", path, body);
}
function put(path, body) {
return request("PUT", path, body);
}
function del(path) {
return request("DELETE", path);
}
//#endregion
export { clearCredentials as a, put as i, get as n, loadCredentials as o, post as r, saveCredentials as s, del as t };
import { n as getProjectPaths } from "./project-Dj085D_B.mjs";
import fs from "node:fs";
import { parse } from "yaml";
//#region src/lib/config.ts
let cachedConfig;
/**
* Parse bon.yaml from the working directory.
* Returns null if the file doesn't exist or can't be parsed.
*/
function loadConfig(cwd) {
if (cachedConfig !== void 0) return cachedConfig;
const paths = getProjectPaths(cwd || process.cwd());
try {
cachedConfig = parse(fs.readFileSync(paths.config, "utf-8")) || null;
} catch {
cachedConfig = null;
}
return cachedConfig;
}
/**
* Check if the project is running in self-hosted mode.
* Checks BON_MODE env var first, then bon.yaml.
*/
function isSelfHosted(config) {
if (process.env.BON_MODE === "self-hosted") return true;
return config?.mode === "self-hosted";
}
/**
* Resolve the base URL for API requests.
* Priority: BON_APP_URL env var > bon.yaml `url` > default.
*/
function getBaseUrl(config) {
if (isSelfHosted(config)) return process.env.BON_APP_URL || config?.url || "http://localhost:3000";
return process.env.BON_APP_URL || "https://app.bonnard.dev";
}
/**
* Resolve the MCP server URL.
* Self-hosted: {baseUrl}/mcp.
* Cloud: https://mcp.bonnard.dev/mcp.
*/
function getMcpUrl(config) {
if (isSelfHosted(config)) return `${getBaseUrl(config)}/mcp`;
return "https://mcp.bonnard.dev/mcp";
}
//#endregion
export { loadConfig as i, getMcpUrl as n, isSelfHosted as r, getBaseUrl as t };
import { i as loadConfig, n as getMcpUrl, r as isSelfHosted, t as getBaseUrl } from "./config-_EMZ9qeI.mjs";
export { getBaseUrl, isSelfHosted, loadConfig };
import { n as getProjectPaths } from "./project-Dj085D_B.mjs";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
//#region src/lib/cubes/datasources.ts
/**
* Extract datasource references from cube and view files
*/
/**
* Collect all YAML files from a directory recursively
*/
function collectYamlFiles(dir) {
if (!fs.existsSync(dir)) return [];
const results = [];
function walk(current) {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) walk(fullPath);
else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push(fullPath);
}
}
walk(dir);
return results;
}
/**
* Parse a single model file and extract datasource references
*/
function extractFromFile(filePath) {
const datasourceToCubes = /* @__PURE__ */ new Map();
try {
const content = fs.readFileSync(filePath, "utf-8");
const parsed = YAML.parse(content);
if (!parsed) return datasourceToCubes;
if (parsed.cubes) for (const cube of parsed.cubes) {
const ds = cube.data_source || "default";
const existing = datasourceToCubes.get(ds) || [];
existing.push(cube.name);
datasourceToCubes.set(ds, existing);
}
if (parsed.views) {
for (const view of parsed.views) if (view.data_source) {
const existing = datasourceToCubes.get(view.data_source) || [];
existing.push(view.name);
datasourceToCubes.set(view.data_source, existing);
}
}
} catch {}
return datasourceToCubes;
}
/**
* Extract all unique datasource references from bonnard/cubes/ and bonnard/views/ directories
* Returns datasource names mapped to the cubes that use them
*/
function extractDatasourcesFromCubes(projectPath) {
const paths = getProjectPaths(projectPath);
const cubesDir = paths.cubes;
const viewsDir = paths.views;
const allFiles = [...collectYamlFiles(cubesDir), ...collectYamlFiles(viewsDir)];
const aggregated = /* @__PURE__ */ new Map();
for (const file of allFiles) {
const fileRefs = extractFromFile(file);
for (const [ds, cubes] of fileRefs) {
const existing = aggregated.get(ds) || [];
existing.push(...cubes);
aggregated.set(ds, existing);
}
}
const results = [];
for (const [name, cubes] of aggregated) if (name !== "default") results.push({
name,
cubes
});
return results;
}
//#endregion
export { extractDatasourcesFromCubes };
import { a as getLocalDatasource, c as resolveEnvVarsInCredentials, i as ensureBonDir, l as saveLocalDatasources, n as addLocalDatasource, o as loadLocalDatasources, r as datasourceExists, s as removeLocalDatasource, t as isDatasourcesTrackedByGit } from "./local-CQt-MJr-.mjs";
export { loadLocalDatasources };
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import { execFileSync } from "node:child_process";
//#region src/lib/local/datasources.ts
/**
* Local datasource storage (.bon/datasources.yaml)
*
* Single file containing both config and credentials.
* Credentials may contain:
* - Plain values: "my_password"
* - dbt env var syntax: "{{ env_var('MY_PASSWORD') }}"
*
* Env vars are resolved at deploy time, not import time.
*/
const BON_DIR$1 = ".bon";
const DATASOURCES_FILE$1 = "datasources.yaml";
function getBonDir(cwd = process.cwd()) {
return path.join(cwd, BON_DIR$1);
}
function getDatasourcesPath$1(cwd = process.cwd()) {
return path.join(getBonDir(cwd), DATASOURCES_FILE$1);
}
/**
* Ensure .bon directory exists
*/
function ensureBonDir(cwd = process.cwd()) {
const bonDir = getBonDir(cwd);
if (!fs.existsSync(bonDir)) fs.mkdirSync(bonDir, { recursive: true });
}
/**
* Load all local datasources
*/
function loadLocalDatasources(cwd = process.cwd()) {
const filePath = getDatasourcesPath$1(cwd);
if (!fs.existsSync(filePath)) return [];
try {
const content = fs.readFileSync(filePath, "utf-8");
return YAML.parse(content)?.datasources ?? [];
} catch {
return [];
}
}
/**
* Save all local datasources (with secure permissions since it contains credentials)
*/
function saveLocalDatasources(datasources, cwd = process.cwd()) {
ensureBonDir(cwd);
const filePath = getDatasourcesPath$1(cwd);
const file = { datasources };
const content = `# Bonnard datasources configuration
# This file contains credentials - add to .gitignore
# Env vars like {{ env_var('PASSWORD') }} are resolved at deploy time
` + YAML.stringify(file, { indent: 2 });
fs.writeFileSync(filePath, content, { mode: 384 });
}
/**
* Add a single datasource (updates existing or appends new)
*/
function addLocalDatasource(datasource, cwd = process.cwd()) {
const existing = loadLocalDatasources(cwd);
const index = existing.findIndex((ds) => ds.name === datasource.name);
if (index >= 0) existing[index] = datasource;
else existing.push(datasource);
saveLocalDatasources(existing, cwd);
}
/**
* Remove a datasource by name
*/
function removeLocalDatasource(name, cwd = process.cwd()) {
const existing = loadLocalDatasources(cwd);
const filtered = existing.filter((ds) => ds.name !== name);
if (filtered.length === existing.length) return false;
saveLocalDatasources(filtered, cwd);
return true;
}
/**
* Get a single datasource by name
*/
function getLocalDatasource(name, cwd = process.cwd()) {
return loadLocalDatasources(cwd).find((ds) => ds.name === name) ?? null;
}
/**
* Check if a datasource name already exists locally
*/
function datasourceExists(name, cwd = process.cwd()) {
return getLocalDatasource(name, cwd) !== null;
}
/**
* Resolve {{ env_var('VAR_NAME') }} patterns in credentials
* Used at deploy time to resolve env vars before uploading
*/
function resolveEnvVarsInCredentials(credentials) {
const resolved = {};
const missing = [];
const envVarPattern = /\{\{\s*env_var\(['"]([\w_]+)['"]\)\s*\}\}/;
for (const [key, value] of Object.entries(credentials)) {
const match = value.match(envVarPattern);
if (match) {
const varName = match[1];
const envValue = process.env[varName];
if (envValue !== void 0) resolved[key] = envValue;
else {
missing.push(varName);
resolved[key] = value;
}
} else resolved[key] = value;
}
return {
resolved,
missing
};
}
//#endregion
//#region src/lib/local/credentials.ts
/**
* Credential utilities (git tracking check)
*/
const BON_DIR = ".bon";
const DATASOURCES_FILE = "datasources.yaml";
function getDatasourcesPath(cwd = process.cwd()) {
return path.join(cwd, BON_DIR, DATASOURCES_FILE);
}
/**
* Check if datasources file is tracked by git (it shouldn't be - contains credentials)
*/
function isDatasourcesTrackedByGit(cwd = process.cwd()) {
const filePath = getDatasourcesPath(cwd);
if (!fs.existsSync(filePath)) return false;
try {
execFileSync("git", [
"ls-files",
"--error-unmatch",
filePath
], {
cwd,
stdio: "pipe"
});
return true;
} catch {
return false;
}
}
//#endregion
export { getLocalDatasource as a, resolveEnvVarsInCredentials as c, ensureBonDir as i, saveLocalDatasources as l, addLocalDatasource as n, loadLocalDatasources as o, datasourceExists as r, removeLocalDatasource as s, isDatasourcesTrackedByGit as t };
import "./api-dRafBjyt.mjs";
import "./config-_EMZ9qeI.mjs";
import "./local-CQt-MJr-.mjs";
import { t as pushDatasource } from "./push-CSaZ-pXY.mjs";
export { pushDatasource };
import { r as post } from "./api-dRafBjyt.mjs";
import { a as getLocalDatasource, c as resolveEnvVarsInCredentials } from "./local-CQt-MJr-.mjs";
import pc from "picocolors";
import "@inquirer/prompts";
//#region src/commands/datasource/push.ts
/**
* Push a datasource programmatically (for use by deploy command)
* Returns true on success, false on failure
*/
async function pushDatasource(name, options = {}) {
const datasource = getLocalDatasource(name);
if (!datasource) {
if (!options.silent) console.error(pc.red(`Datasource "${name}" not found locally`));
return false;
}
const { resolved, missing } = resolveEnvVarsInCredentials(datasource.credentials);
if (missing.length > 0) {
if (!options.silent) console.error(pc.red(`Missing env vars for "${name}": ${missing.join(", ")}`));
return false;
}
try {
await post("/api/datasources", {
name: datasource.name,
warehouse_type: datasource.type,
config: datasource.config,
credentials: resolved
});
return true;
} catch (err) {
if (!options.silent) {
const message = err instanceof Error ? err.message : String(err);
console.error(pc.red(`Failed to push "${name}": ${message}`));
}
return false;
}
}
//#endregion
export { pushDatasource as t };
import { n as getProjectPaths } from "./project-Dj085D_B.mjs";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import { z } from "zod";
//#region src/lib/schema.ts
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)");
const refreshKeySchema = z.object({
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(),
sql: z.string().optional()
});
const measureTypes = [
"count",
"count_distinct",
"count_distinct_approx",
"sum",
"avg",
"min",
"max",
"number",
"string",
"time",
"boolean",
"running_total",
"number_agg"
];
const dimensionTypes = [
"string",
"number",
"boolean",
"time",
"geo",
"switch"
];
const relationshipTypes = [
"many_to_one",
"one_to_many",
"one_to_one"
];
const granularities = [
"second",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"year"
];
const preAggTypes = [
"rollup",
"original_sql",
"rollup_join",
"rollup_lambda"
];
const formats = [
"percent",
"currency",
"number",
"imageUrl",
"link",
"id"
];
const measureSchema = z.object({
name: identifier,
type: z.enum(measureTypes),
sql: z.string().optional(),
description: z.string().optional(),
title: z.string().optional(),
format: z.enum(formats).optional(),
public: z.boolean().optional(),
filters: z.array(z.object({ sql: z.string() })).optional(),
rolling_window: z.object({
trailing: z.string().optional(),
leading: z.string().optional(),
offset: z.string().optional()
}).optional(),
drill_members: z.array(z.string()).optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const dimensionFormatSchema = z.union([z.enum(formats), z.string().startsWith("%")]);
const dimensionSchema = z.object({
name: identifier,
type: z.enum(dimensionTypes),
sql: z.string().optional(),
primary_key: z.boolean().optional(),
sub_query: z.boolean().optional(),
propagate_filters_to_sub_query: z.boolean().optional(),
description: z.string().optional(),
title: z.string().optional(),
format: dimensionFormatSchema.optional(),
public: z.boolean().optional(),
meta: z.record(z.string(), z.unknown()).optional(),
latitude: z.object({ sql: z.string() }).optional(),
longitude: z.object({ sql: z.string() }).optional(),
case: z.object({
when: z.array(z.object({
sql: z.string(),
label: z.string()
})),
else: z.object({ label: z.string() }).optional()
}).optional()
});
const joinSchema = z.object({
name: identifier,
relationship: z.enum(relationshipTypes),
sql: z.string()
});
const segmentSchema = z.object({
name: identifier,
sql: z.string(),
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional()
});
const preAggregationSchema = z.object({
name: identifier,
type: z.enum(preAggTypes).optional(),
measures: z.array(z.string()).optional(),
dimensions: z.array(z.string()).optional(),
time_dimension: z.string().optional(),
granularity: z.enum(granularities).optional(),
partition_granularity: z.enum(granularities).optional(),
refresh_key: refreshKeySchema.optional(),
scheduled_refresh: z.boolean().optional()
});
const hierarchySchema = z.object({
name: identifier,
levels: z.array(z.string()),
title: z.string().optional(),
public: z.boolean().optional()
});
const cubeSchema = z.object({
name: identifier,
sql: z.string().optional(),
sql_table: z.string().optional(),
data_source: z.string().optional(),
extends: z.string().optional(),
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional(),
refresh_key: refreshKeySchema.optional(),
measures: z.array(measureSchema).optional(),
dimensions: z.array(dimensionSchema).optional(),
joins: z.array(joinSchema).optional(),
segments: z.array(segmentSchema).optional(),
pre_aggregations: z.array(preAggregationSchema).optional(),
hierarchies: z.array(hierarchySchema).optional()
}).refine((data) => data.sql != null || data.sql_table != null || data.extends != null, { message: "sql, sql_table, or extends is required" });
const viewIncludeItemSchema = z.union([z.string(), z.object({
name: z.string(),
alias: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
format: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
})]);
const viewCubeRefSchema = z.object({
join_path: z.string(),
includes: z.union([z.literal("*"), z.array(viewIncludeItemSchema)]).optional(),
excludes: z.array(z.string()).optional(),
prefix: z.boolean().optional()
});
const folderSchema = z.lazy(() => z.object({
name: z.string(),
members: z.array(z.string()).optional(),
folders: z.array(folderSchema).optional()
}));
const viewMeasureSchema = z.object({
name: identifier,
sql: z.string(),
type: z.string(),
format: z.string().optional(),
description: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const viewDimensionSchema = z.object({
name: identifier,
sql: z.string(),
type: z.string(),
description: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const viewSegmentSchema = z.object({
name: identifier,
sql: z.string(),
description: z.string().optional()
});
const viewSchema = z.object({
name: identifier,
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional(),
cubes: z.array(viewCubeRefSchema).optional(),
measures: z.array(viewMeasureSchema).optional(),
dimensions: z.array(viewDimensionSchema).optional(),
segments: z.array(viewSegmentSchema).optional(),
folders: z.array(folderSchema).optional()
});
const fileSchema = z.object({
cubes: z.array(cubeSchema).optional(),
views: z.array(viewSchema).optional()
}).refine((data) => data.cubes && data.cubes.length > 0 || data.views && data.views.length > 0, "File must contain at least one cube or view");
function formatZodError(error, fileName, parsed) {
return error.issues.map((issue) => {
const pathParts = issue.path;
let entityContext = "";
if (pathParts.length >= 2) {
const collection = pathParts[0];
const index = pathParts[1];
const entity = parsed?.[collection]?.[index];
if (entity?.name) entityContext = ` (${entity.name})`;
}
const pathStr = pathParts.join(".");
const location = pathStr ? `${pathStr}${entityContext}` : "";
if (issue.code === "invalid_value" && "values" in issue) return `${fileName}: ${location} — invalid value, expected one of: ${issue.values.join(", ")}`;
return `${fileName}: ${location ? `${location} — ` : ""}${issue.message}`;
});
}
function checkViewMemberConflicts(parsedFiles, cubeMap) {
const errors = [];
for (const { fileName, parsed } of parsedFiles) for (const view of parsed.views ?? []) {
if (!view.name || !view.cubes) continue;
const seen = /* @__PURE__ */ new Map();
for (const m of view.measures ?? []) if (m.name) seen.set(m.name, `${view.name} (direct)`);
for (const d of view.dimensions ?? []) if (d.name) seen.set(d.name, `${view.name} (direct)`);
for (const s of view.segments ?? []) if (s.name) seen.set(s.name, `${view.name} (direct)`);
for (const cubeRef of view.cubes) {
const joinPath = cubeRef.join_path;
if (!joinPath) continue;
const segments = joinPath.split(".");
const targetCubeName = segments[segments.length - 1];
let memberNames = [];
if (cubeRef.includes === "*") {
const cube = cubeMap.get(targetCubeName);
if (!cube) continue;
memberNames = [
...cube.measures,
...cube.dimensions,
...cube.segments
];
} else if (Array.isArray(cubeRef.includes)) {
for (const item of cubeRef.includes) if (typeof item === "string") memberNames.push(item);
else if (item && typeof item === "object" && item.name) memberNames.push(item.alias || item.name);
} else continue;
if (Array.isArray(cubeRef.excludes)) {
const excludeSet = new Set(cubeRef.excludes);
memberNames = memberNames.filter((n) => !excludeSet.has(n));
}
for (const rawName of memberNames) {
const finalName = cubeRef.prefix ? `${targetCubeName}_${rawName}` : rawName;
const existingSource = seen.get(finalName);
if (existingSource) errors.push(`${fileName}: view '${view.name}' — member '${finalName}' from '${joinPath}' conflicts with '${existingSource}'. Use prefix: true or an alias.`);
else seen.set(finalName, joinPath);
}
}
}
return errors;
}
function validateFiles(files) {
const errors = [];
const cubes = [];
const views = [];
const allNames = /* @__PURE__ */ new Map();
const parsedFiles = [];
const cubeMap = /* @__PURE__ */ new Map();
for (const file of files) {
let parsed;
try {
parsed = YAML.parse(file.content);
} catch (err) {
errors.push(`${file.fileName}: YAML parse error — ${err.message}`);
continue;
}
if (!parsed || typeof parsed !== "object") {
errors.push(`${file.fileName}: file is empty or not a YAML object`);
continue;
}
const result = fileSchema.safeParse(parsed);
if (!result.success) {
errors.push(...formatZodError(result.error, file.fileName, parsed));
continue;
}
parsedFiles.push({
fileName: file.fileName,
parsed
});
for (const cube of parsed.cubes ?? []) if (cube.name) {
const existing = allNames.get(cube.name);
if (existing) errors.push(`${file.fileName}: duplicate name '${cube.name}' (also defined in ${existing})`);
else {
allNames.set(cube.name, file.fileName);
cubes.push(cube.name);
cubeMap.set(cube.name, {
measures: (cube.measures ?? []).map((m) => m.name).filter(Boolean),
dimensions: (cube.dimensions ?? []).map((d) => d.name).filter(Boolean),
segments: (cube.segments ?? []).map((s) => s.name).filter(Boolean)
});
}
}
for (const view of parsed.views ?? []) if (view.name) {
const existing = allNames.get(view.name);
if (existing) errors.push(`${file.fileName}: duplicate name '${view.name}' (also defined in ${existing})`);
else {
allNames.set(view.name, file.fileName);
views.push(view.name);
}
}
}
if (errors.length === 0) errors.push(...checkViewMemberConflicts(parsedFiles, cubeMap));
return {
errors,
cubes,
views
};
}
//#endregion
//#region src/lib/validate.ts
function collectYamlFiles(dir, rootDir) {
if (!fs.existsSync(dir)) return [];
const results = [];
function walk(current) {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) walk(fullPath);
else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push({
fileName: path.relative(rootDir, fullPath),
content: fs.readFileSync(fullPath, "utf-8")
});
}
}
walk(dir);
return results;
}
function checkMissingDescriptions(files) {
const missing = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
const cubes = parsed.cubes || [];
for (const cube of cubes) {
if (!cube.name) continue;
if (!cube.description) missing.push({
parent: cube.name,
type: "cube",
name: cube.name
});
const measures = cube.measures || [];
for (const measure of measures) if (measure.name && !measure.description) missing.push({
parent: cube.name,
type: "measure",
name: measure.name
});
const dimensions = cube.dimensions || [];
for (const dimension of dimensions) if (dimension.name && !dimension.description) missing.push({
parent: cube.name,
type: "dimension",
name: dimension.name
});
}
const views = parsed.views || [];
for (const view of views) {
if (!view.name) continue;
if (!view.description) missing.push({
parent: view.name,
type: "view",
name: view.name
});
}
} catch {}
return missing;
}
function checkSuspectPrimaryKeys(files) {
const suspects = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const cube of parsed.cubes || []) {
if (!cube.name) continue;
for (const dim of cube.dimensions || []) if (dim.primary_key && dim.type === "time") suspects.push({
cube: cube.name,
dimension: dim.name,
type: dim.type
});
}
} catch {}
return suspects;
}
function checkMissingDataSource(files) {
const missing = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
} catch {}
return missing;
}
function checkUnjoinedViewCubes(files) {
const results = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const view of parsed.views || []) {
if (!view.name || !view.cubes || view.cubes.length < 2) continue;
const roots = /* @__PURE__ */ new Set();
for (const cubeRef of view.cubes) {
if (!cubeRef.join_path) continue;
const firstSegment = cubeRef.join_path.split(".")[0];
roots.add(firstSegment);
}
if (roots.size > 1) results.push({
view: view.name,
roots: [...roots]
});
}
} catch {}
return results;
}
async function validate(projectPath) {
const paths = getProjectPaths(projectPath);
const files = [...collectYamlFiles(paths.cubes, projectPath), ...collectYamlFiles(paths.views, projectPath)];
if (files.length === 0) return {
valid: true,
errors: [],
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: [],
suspectPrimaryKeys: [],
unjoinedViewCubes: []
};
const result = validateFiles(files);
if (result.errors.length > 0) return {
valid: false,
errors: result.errors,
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: [],
suspectPrimaryKeys: [],
unjoinedViewCubes: []
};
const missingDescriptions = checkMissingDescriptions(files);
const cubesMissingDataSource = checkMissingDataSource(files);
const suspectPrimaryKeys = checkSuspectPrimaryKeys(files);
const unjoinedViewCubes = checkUnjoinedViewCubes(files);
return {
valid: true,
errors: [],
cubes: result.cubes,
views: result.views,
missingDescriptions,
cubesMissingDataSource,
suspectPrimaryKeys,
unjoinedViewCubes
};
}
//#endregion
export { validate };
# AI Agent Tools
> Give your AI agents direct access to your semantic layer — pre-built tools for Vercel AI SDK, LangChain, and any framework that accepts Zod schemas.
Instead of writing boilerplate tool definitions for each AI framework, import `createTools` from the SDK and get four production-ready tools that handle schema discovery, querying, SQL execution, and field metadata.
```typescript
import { createClient } from '@bonnard/sdk';
import { createTools } from '@bonnard/sdk/ai/vercel';
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon);
const result = await generateText({
model: anthropic('claude-sonnet-4-5-20250514'),
tools,
prompt: 'What were our top 5 products by revenue last quarter?',
});
```
## Install
The AI tools are included in `@bonnard/sdk` — no extra packages. You only need your AI framework as a peer dependency:
```bash
# Vercel AI SDK
npm install @bonnard/sdk ai @ai-sdk/anthropic
# LangChain / LangGraph
npm install @bonnard/sdk @langchain/core @langchain/anthropic @langchain/langgraph
```
## Framework adapters
### Vercel AI SDK
```typescript
import { createClient } from '@bonnard/sdk';
import { createTools } from '@bonnard/sdk/ai/vercel';
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon);
// tools is Record<string, CoreTool> — spread with your own tools
const result = await generateText({
model: anthropic('claude-sonnet-4-5-20250514'),
tools: { ...tools, ...myOtherTools },
maxSteps: 10,
prompt: 'Show me revenue by region for the last 6 months',
});
```
### LangChain / LangGraph
```typescript
import { createClient } from '@bonnard/sdk';
import { createTools } from '@bonnard/sdk/ai/langchain';
import { ChatAnthropic } from '@langchain/anthropic';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon);
// tools is DynamicStructuredTool[] — spread with your own tools
const agent = createReactAgent({
llm: new ChatAnthropic({ model: 'claude-sonnet-4-5-20250514' }),
tools: [...tools, ...myOtherTools],
});
const result = await agent.invoke({
messages: [{ role: 'user', content: 'What data sources are available?' }],
});
```
### Framework-agnostic
If your framework isn't listed above, import the base tools and wrap them yourself. Each tool has a `name`, `description`, `schema` (Zod), and `execute` function:
```typescript
import { createClient } from '@bonnard/sdk';
import { createTools } from '@bonnard/sdk/ai';
import type { BonnardTool } from '@bonnard/sdk/ai';
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon); // BonnardTool[]
// Wrap for your framework
for (const tool of tools) {
console.log(tool.name); // "explore_schema"
console.log(tool.description); // "Discover available data sources..."
console.log(tool.schema); // Zod schema — convert to JSON Schema with zod-to-json-schema
await tool.execute({ ... }); // Call directly
}
```
This also works for MCP servers — convert the Zod schemas to JSON Schema with `zod-to-json-schema`.
## Tools included
Every `createTools(client)` call returns four tools:
### explore_schema
Discover available data sources, their measures, dimensions, and segments.
| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | `string?` | Source name to get full field listings (e.g. `"orders"`) |
| `search` | `string?` | Keyword to search across all field names and descriptions |
```typescript
// No args — list all sources with summary counts
await tools.explore_schema.execute({});
// → [{ name: "orders", type: "view", measures: 5, dimensions: 8, segments: 1 }]
// By name — full field details for one source
await tools.explore_schema.execute({ name: "orders" });
// → { name: "orders", measures: [...], dimensions: [...], segments: [...] }
// Search — find fields by keyword across all sources
await tools.explore_schema.execute({ search: "revenue" });
// → [{ source: "orders", field: "orders.revenue", kind: "measure", type: "number" }]
```
### query
Run structured queries with measures, dimensions, filters, and time dimensions. All field names must be fully qualified (e.g. `"orders.revenue"`).
| Parameter | Type | Description |
|-----------|------|-------------|
| `measures` | `string[]?` | Measures to aggregate (e.g. `["orders.revenue"]`) |
| `dimensions` | `string[]?` | Dimensions to group by |
| `timeDimensions` | `array?` | Time dimensions with `dimension`, `granularity`, `dateRange` |
| `filters` | `array?` | Filters with `member`, `operator`, `values` |
| `segments` | `string[]?` | Pre-defined filter segments |
| `order` | `object?` | Sort order (e.g. `{ "orders.revenue": "desc" }`) |
| `limit` | `number?` | Max rows (default 250) |
| `offset` | `number?` | Pagination offset |
### sql_query
Execute raw SQL when structured queries can't express what you need — CTEs, UNIONs, custom arithmetic.
| Parameter | Type | Description |
|-----------|------|-------------|
| `sql` | `string` | SQL using Cube syntax with `MEASURE()` for aggregations |
### describe_field
Get metadata for a specific field — its type, description, and which source it belongs to.
| Parameter | Type | Description |
|-----------|------|-------------|
| `field` | `string` | Fully qualified field name (e.g. `"orders.revenue"`) |
## Authentication for agents
The tools use whatever auth you configure on the client. All three patterns work:
### Simple: publishable key
Best for internal tools or prototypes — all queries run with your org's full access.
```typescript
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon);
```
### Multi-tenant: token exchange
For B2B apps where each customer's agent should only see their own data. Your backend exchanges a secret key for a scoped token:
```typescript
// Server-side: create client with token exchange
const bon = createClient({
fetchToken: async () => {
const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
security_context: { tenant_id: currentUser.tenantId },
}),
});
const { token } = await res.json();
return token;
},
});
const tools = createTools(bon);
// Agent queries are automatically scoped to the tenant's data
```
See [sdk.authentication](sdk.authentication) for details on token refresh and security context.
### Frontend: Clerk session
For apps where users authenticate through Clerk:
```typescript
import { useAuth } from '@clerk/nextjs';
const { getToken } = useAuth();
const bon = createClient({
fetchToken: () => getToken(),
});
const tools = createTools(bon);
```
## Tips for better agent responses
### Add a system prompt with context
The tools give the agent the ability to query, but a good system prompt tells it how to approach your data:
```typescript
const result = await generateText({
model: anthropic('claude-sonnet-4-5-20250514'),
tools,
maxSteps: 10,
system: `You are a data analyst. Always start by calling explore_schema to understand
what data is available before querying. Use describe_field to understand metrics
before presenting results. Format numbers with appropriate units.`,
prompt: userMessage,
});
```
### Set maxSteps high enough
Agents typically need 3–5 steps: explore schema → understand fields → query → maybe refine. Set `maxSteps: 10` to give the agent room to work.
### Combine with your own tools
The adapters return spreadable formats so you can mix Bonnard tools with your own:
```typescript
// Vercel AI SDK — Record<string, CoreTool>
const result = await generateText({
tools: { ...bonnardTools, saveReport, sendEmail },
});
// LangChain — StructuredTool[]
const agent = createReactAgent({
tools: [...bonnardTools, saveReport, sendEmail],
});
```
## See also
- [sdk](sdk) — SDK overview and installation
- [sdk.authentication](sdk.authentication) — Auth patterns (publishable keys, token exchange)
- [sdk.query-reference](sdk.query-reference) — Full query API reference
- [mcp](mcp) — MCP server setup (alternative to SDK tools for Claude, ChatGPT, etc.)
- [access-control.security-context](access-control.security-context) — Row-level security for multi-tenant apps
# ============================================================
# DATA SOURCE — configure for your database
# ============================================================
# DuckDB example:
CUBEJS_DB_TYPE=duckdb
CUBEJS_DB_DUCKDB_DATABASE_PATH=/data/my-database.duckdb
# Postgres example (uncomment and replace DuckDB lines above):
# CUBEJS_DB_TYPE=postgres
# CUBEJS_DB_HOST=host.docker.internal
# CUBEJS_DB_PORT=5432
# CUBEJS_DB_NAME=mydb
# CUBEJS_DB_USER=myuser
# CUBEJS_DB_PASS=mypassword
# Multi-datasource example (uncomment):
# If your cubes use `data_source: my_source`, list all sources here
# including "default". Cube maps my_source to CUBEJS_DS_MY_SOURCE_* vars.
# CUBEJS_DATASOURCES=default,my_source
# CUBEJS_DS_MY_SOURCE_DB_TYPE=postgres
# CUBEJS_DS_MY_SOURCE_DB_HOST=host.docker.internal
# CUBEJS_DS_MY_SOURCE_DB_PORT=5432
# CUBEJS_DS_MY_SOURCE_DB_NAME=mydb
# CUBEJS_DS_MY_SOURCE_DB_USER=myuser
# CUBEJS_DS_MY_SOURCE_DB_PASS=mypassword
# ============================================================
# CUBE CONFIGURATION
# ============================================================
# HS256 secret for signing Cube JWTs (auto-generated).
# Both Cube and Bonnard use this for authenticated communication.
CUBEJS_API_SECRET=REPLACE_ME
# ============================================================
# BONNARD CONFIGURATION
# ============================================================
# Optional: protect the admin UI and API with a bearer token.
# ADMIN_TOKEN=
# Optional: custom ports (defaults: Cube=4000, Bonnard=3000).
# CUBE_PORT=4000
# BONNARD_PORT=3000
# ============================================================
# PRODUCTION CONFIGURATION (optional)
# ============================================================
# CORS: allowed origins (* = any, or a specific URL like https://myapp.com).
# CORS_ORIGIN=*
# Pin Cube/Bonnard image versions (defaults: CUBE_VERSION=v1.6, BONNARD_VERSION=latest).
# CUBE_VERSION=v1.6
# BONNARD_VERSION=latest
// Cube configuration for self-hosted Bonnard.
// Model files are loaded from the shared Docker volume.
const fs = require("fs");
const path = require("path");
const MODEL_DIR = process.env.CUBEJS_SCHEMA_PATH || "/cube/conf/model";
const VERSION_FILE = path.join(MODEL_DIR, ".version");
module.exports = {
// Check for model changes every 30 seconds.
// When schemaVersion changes, Cube recompiles all models.
scheduledRefreshTimer: 30,
schemaVersion: () => {
try {
return fs.readFileSync(VERSION_FILE, "utf-8").trim();
} catch {
return "initial";
}
},
};
volumes:
models: {}
services:
cubestore:
image: cubejs/cubestore:${CUBE_VERSION:-v1.6}
restart: unless-stopped
cube:
image: cubejs/cube:${CUBE_VERSION:-v1.6}
restart: unless-stopped
ports:
- "${CUBE_PORT:-4000}:4000"
volumes:
- models:/cube/conf/model:ro
- ./cube.js:/cube/conf/cube.js:ro
- ./data:/data
env_file: .env
environment:
CUBEJS_CUBESTORE_HOST: cubestore
CUBEJS_SQL_PORT: "15432"
depends_on:
cubestore:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:4000/livez').then(()=>process.exit(0)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 15
start_period: 30s
bonnard:
image: ghcr.io/bonnard-data/bonnard:${BONNARD_VERSION:-latest}
restart: unless-stopped
ports:
- "${BONNARD_PORT:-3000}:3000"
volumes:
- models:/app/models
environment:
CUBE_API_URL: http://cube:4000
CUBE_API_SECRET: ${CUBEJS_API_SECRET:-}
ADMIN_TOKEN: ${ADMIN_TOKEN:-}
CORS_ORIGIN: ${CORS_ORIGIN:-*}
MODEL_DIR: /app/models
depends_on:
cube:
condition: service_healthy
# Bonnard (Self-Hosted)
Self-hosted semantic layer for AI agents. Full documentation at [github.com/bonnard-data/bonnard](https://github.com/bonnard-data/bonnard).
## Quick Start
```bash
# 1. Configure your data source
# Edit .env with your database credentials
# 2. Start the server
docker compose up -d
# 3. Define your semantic layer
# Add cube/view YAML files to bonnard/cubes/ and bonnard/views/
# 4. Deploy models to the server
bon deploy
# 5. Verify your semantic layer
bon schema
# 6. Connect AI agents
bon mcp
```
## Connecting AI Agents
Run `bon mcp` to see connection config for your setup. Examples below.
### Claude Desktop / Cursor
```json
{
"mcpServers": {
"bonnard": {
"url": "https://bonnard.example.com/mcp",
"headers": {
"Authorization": "Bearer your-secret-token-here"
}
}
}
}
```
### Claude Code
```json
{
"mcpServers": {
"bonnard": {
"type": "url",
"url": "https://bonnard.example.com/mcp",
"headers": {
"Authorization": "Bearer your-secret-token-here"
}
}
}
}
```
### CrewAI (Python)
```python
from crewai import MCPServerAdapter
mcp = MCPServerAdapter(
url="https://bonnard.example.com/mcp",
transport="streamable-http",
headers={"Authorization": "Bearer your-secret-token-here"}
)
```
## Authentication
Protect your endpoints by setting `ADMIN_TOKEN` in `.env`:
```env
ADMIN_TOKEN=your-secret-token-here
```
All API and MCP endpoints will require `Authorization: Bearer <token>`. The `/health` endpoint remains open for monitoring.
Restart after changing `.env`:
```bash
docker compose up -d
```
## TLS with Caddy
[Caddy](https://caddyserver.com) provides automatic HTTPS via Let's Encrypt.
Create a `Caddyfile` next to your `docker-compose.yml`:
```
bonnard.example.com {
reverse_proxy localhost:3000
}
```
Add Caddy to your `docker-compose.yml`:
```yaml
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
restart: unless-stopped
```
Add the volume at the top level:
```yaml
volumes:
models: {}
caddy_data: {}
```
Then remove the Bonnard port mapping (`ports: - "3000:3000"`) since Caddy handles external traffic.
## Deploy to a VM
```bash
scp -r . user@your-server:~/bonnard/
ssh user@your-server
cd ~/bonnard
docker compose up -d
```
## Configuration
| Variable | Description | Default |
|----------|-------------|---------|
| `CUBEJS_DB_TYPE` | Database driver (`postgres`, `duckdb`, `snowflake`, `bigquery`, `databricks`, `redshift`, `clickhouse`) | `duckdb` |
| `CUBEJS_DB_*` | Database connection settings (host, port, name, user, pass) | — |
| `CUBEJS_API_SECRET` | HS256 secret for Cube JWT auth (auto-generated) | — |
| `ADMIN_TOKEN` | Bearer token for API/MCP authentication | — (open) |
| `CUBE_PORT` | Cube API port | `4000` |
| `BONNARD_PORT` | Bonnard server port | `3000` |
| `CORS_ORIGIN` | Allowed CORS origins | `*` |
| `CUBE_VERSION` | Cube Docker image tag | `v1.6` |
| `BONNARD_VERSION` | Bonnard Docker image tag | `latest` |
## Monitoring
```bash
# Health check
curl http://localhost:3000/health
# View logs
docker compose logs -f
# View active MCP sessions
curl -H "Authorization: Bearer <token>" http://localhost:3000/api/mcp/sessions
```
## Deploying Schema Updates
```bash
bon deploy
```
Pushes cube/view YAML files to the running server. No restart needed — Cube picks up changes automatically.
+46
-0

@@ -7,2 +7,32 @@ # SDK

## Before you start
The SDK queries your **deployed** semantic layer — you need these in place first:
1. **A deployed semantic layer** — at least one cube and view, deployed with `bon deploy`. See [getting-started](getting-started) if you haven't done this yet.
2. **A publishable key** — go to **Settings > API Keys** in the Bonnard dashboard and create one. It starts with `bon_pk_...`.
Once you have a key, use `explore()` to discover what's available before writing your first query:
```typescript
const bon = createClient({ apiKey: 'bon_pk_...' });
// Step 1: see what views exist
const meta = await bon.explore();
console.log(meta.cubes.map(v => v.name)); // ['orders', 'customers', ...]
// Step 2: see what fields a view has
const orders = meta.cubes.find(v => v.name === 'orders');
console.log(orders.measures.map(m => m.name)); // ['orders.revenue', 'orders.count']
console.log(orders.dimensions.map(d => d.name)); // ['orders.city', 'orders.status']
// Step 3: query using those field names
const { data } = await bon.query({
measures: ['orders.revenue'],
dimensions: ['orders.city'],
});
```
**Reading order for new SDK users:** this page → [sdk.authentication](sdk.authentication) → [sdk.query-reference](sdk.query-reference) → then whichever integration guide fits your use case (React, browser, AI agents, chart libraries).
## Two ways to use it

@@ -75,2 +105,17 @@

## Building AI agents
Give your AI agents direct access to your semantic layer. Import `createTools` with a framework adapter and get four production-ready tools — schema discovery, querying, SQL, and field metadata.
```typescript
import { createClient } from '@bonnard/sdk';
import { createTools } from '@bonnard/sdk/ai/vercel';
const bon = createClient({ apiKey: 'bon_pk_...' });
const tools = createTools(bon);
// → { explore_schema, query, sql_query, describe_field }
```
Adapters for Vercel AI SDK and LangChain/LangGraph included. See [sdk.ai-agents](sdk.ai-agents) for the full guide.
## Building with React

@@ -104,2 +149,3 @@

- [sdk.ai-agents](sdk.ai-agents) — AI agent tools (Vercel AI, LangChain, framework-agnostic)
- [sdk.react](sdk.react) — React components (BigValue, charts, DataTable, hooks)

@@ -106,0 +152,0 @@ - [sdk.browser](sdk.browser) — Browser / CDN quickstart

+1
-1
MIT License
Copyright (c) 2025-present Bonnard (meal-inc)
Copyright (c) 2025-present Bonnard (bonnard-data)

@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

{
"name": "@bonnard/cli",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",

@@ -27,3 +27,3 @@ "bin": {

"@bonnard/react": "^0.4.5",
"@bonnard/sdk": "^0.4.2",
"@bonnard/sdk": "^0.4.3",
"@types/node": "^20.0.0",

@@ -43,3 +43,3 @@ "@types/react": "^19.0.0",

"type": "git",
"url": "https://github.com/meal-inc/bonnard-cli.git"
"url": "https://github.com/bonnard-data/bonnard-cli.git"
},

@@ -46,0 +46,0 @@ "license": "MIT",

@@ -17,3 +17,3 @@ <p align="center">

<a href="https://www.npmjs.com/package/@bonnard/cli"><img src="https://img.shields.io/npm/v/@bonnard/cli?style=flat-square&color=0891b2" alt="npm version" /></a>
<a href="https://github.com/meal-inc/bonnard-cli/blob/main/LICENSE"><img src="https://img.shields.io/github/license/meal-inc/bonnard-cli?style=flat-square" alt="MIT License" /></a>
<a href="https://github.com/bonnard-data/bonnard-cli/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bonnard-data/bonnard-cli?style=flat-square" alt="MIT License" /></a>
<a href="https://discord.com/invite/RQuvjGRz"><img src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=flat-square&logo=discord&logoColor=white" alt="Discord" /></a>

@@ -131,3 +131,3 @@ </p>

- [Discord](https://discord.com/invite/RQuvjGRz): ask questions, share feedback, connect with the team
- [GitHub Issues](https://github.com/meal-inc/bonnard-cli/issues): bug reports and feature requests
- [GitHub Issues](https://github.com/bonnard-data/bonnard-cli/issues): bug reports and feature requests
- [LinkedIn](https://www.linkedin.com/company/bonnarddev/): follow for updates

@@ -134,0 +134,0 @@ - [Website](https://www.bonnard.dev): learn more about Bonnard

import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import pc from "picocolors";
//#region src/lib/credentials.ts
const CREDENTIALS_DIR = path.join(os.homedir(), ".config", "bon");
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
function saveCredentials(credentials) {
fs.mkdirSync(CREDENTIALS_DIR, {
recursive: true,
mode: 448
});
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 384 });
}
function loadCredentials() {
try {
const raw = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
const parsed = JSON.parse(raw);
if (parsed.token && parsed.email) return parsed;
return null;
} catch {
return null;
}
}
function clearCredentials() {
try {
fs.unlinkSync(CREDENTIALS_FILE);
} catch {}
}
//#endregion
//#region src/lib/api.ts
const { version } = createRequire(import.meta.url)("../../package.json");
const USER_AGENT = `bon-cli/${version} node-${process.version} ${os.platform()} (${os.arch()})`;
const APP_URL = process.env.BON_APP_URL || "https://app.bonnard.dev";
const VERCEL_BYPASS = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
function getToken() {
const creds = loadCredentials();
if (!creds) {
console.error(pc.red("Not logged in. Run `bon login` first."));
process.exit(1);
}
return creds.token;
}
async function request(method, path, body) {
const token = getToken();
const url = `${APP_URL}${path}`;
const headers = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT
};
if (VERCEL_BYPASS) headers["x-vercel-protection-bypass"] = VERCEL_BYPASS;
const res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : void 0,
signal: AbortSignal.timeout(3e4)
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
data = {};
}
if (!res.ok) {
const message = data.error || `HTTP ${res.status}: ${res.statusText}`;
throw new Error(message);
}
return data;
}
function get(path) {
return request("GET", path);
}
function post(path, body) {
return request("POST", path, body);
}
function put(path, body) {
return request("PUT", path, body);
}
function del(path) {
return request("DELETE", path);
}
//#endregion
export { clearCredentials as a, put as i, get as n, loadCredentials as o, post as r, saveCredentials as s, del as t };
import { i as put, n as get, r as post, t as del } from "./api-BZ9eHZdy.mjs";
export { del, get };
import { n as getProjectPaths } from "./project-Dj085D_B.mjs";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
//#region src/lib/cubes/datasources.ts
/**
* Extract datasource references from cube and view files
*/
/**
* Collect all YAML files from a directory recursively
*/
function collectYamlFiles(dir) {
if (!fs.existsSync(dir)) return [];
const results = [];
function walk(current) {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) walk(fullPath);
else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push(fullPath);
}
}
walk(dir);
return results;
}
/**
* Parse a single model file and extract datasource references
*/
function extractFromFile(filePath) {
const datasourceToCubes = /* @__PURE__ */ new Map();
try {
const content = fs.readFileSync(filePath, "utf-8");
const parsed = YAML.parse(content);
if (!parsed) return datasourceToCubes;
if (parsed.cubes) for (const cube of parsed.cubes) {
const ds = cube.data_source || "default";
const existing = datasourceToCubes.get(ds) || [];
existing.push(cube.name);
datasourceToCubes.set(ds, existing);
}
if (parsed.views) {
for (const view of parsed.views) if (view.data_source) {
const existing = datasourceToCubes.get(view.data_source) || [];
existing.push(view.name);
datasourceToCubes.set(view.data_source, existing);
}
}
} catch {}
return datasourceToCubes;
}
/**
* Extract all unique datasource references from bonnard/cubes/ and bonnard/views/ directories
* Returns datasource names mapped to the cubes that use them
*/
function extractDatasourcesFromCubes(projectPath) {
const paths = getProjectPaths(projectPath);
const cubesDir = paths.cubes;
const viewsDir = paths.views;
const allFiles = [...collectYamlFiles(cubesDir), ...collectYamlFiles(viewsDir)];
const aggregated = /* @__PURE__ */ new Map();
for (const file of allFiles) {
const fileRefs = extractFromFile(file);
for (const [ds, cubes] of fileRefs) {
const existing = aggregated.get(ds) || [];
existing.push(...cubes);
aggregated.set(ds, existing);
}
}
const results = [];
for (const [name, cubes] of aggregated) if (name !== "default") results.push({
name,
cubes
});
return results;
}
//#endregion
export { extractDatasourcesFromCubes };
import { a as getLocalDatasource, c as resolveEnvVarsInCredentials, i as ensureBonDir, l as saveLocalDatasources, n as addLocalDatasource, o as loadLocalDatasources, r as datasourceExists, s as removeLocalDatasource, t as isDatasourcesTrackedByGit } from "./local-ByvuW3eV.mjs";
export { loadLocalDatasources };
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import { execFileSync } from "node:child_process";
//#region src/lib/local/datasources.ts
/**
* Local datasource storage (.bon/datasources.yaml)
*
* Single file containing both config and credentials.
* Credentials may contain:
* - Plain values: "my_password"
* - dbt env var syntax: "{{ env_var('MY_PASSWORD') }}"
*
* Env vars are resolved at deploy time, not import time.
*/
const BON_DIR$1 = ".bon";
const DATASOURCES_FILE$1 = "datasources.yaml";
function getBonDir(cwd = process.cwd()) {
return path.join(cwd, BON_DIR$1);
}
function getDatasourcesPath$1(cwd = process.cwd()) {
return path.join(getBonDir(cwd), DATASOURCES_FILE$1);
}
/**
* Ensure .bon directory exists
*/
function ensureBonDir(cwd = process.cwd()) {
const bonDir = getBonDir(cwd);
if (!fs.existsSync(bonDir)) fs.mkdirSync(bonDir, { recursive: true });
}
/**
* Load all local datasources
*/
function loadLocalDatasources(cwd = process.cwd()) {
const filePath = getDatasourcesPath$1(cwd);
if (!fs.existsSync(filePath)) return [];
try {
const content = fs.readFileSync(filePath, "utf-8");
return YAML.parse(content)?.datasources ?? [];
} catch {
return [];
}
}
/**
* Save all local datasources (with secure permissions since it contains credentials)
*/
function saveLocalDatasources(datasources, cwd = process.cwd()) {
ensureBonDir(cwd);
const filePath = getDatasourcesPath$1(cwd);
const file = { datasources };
const content = `# Bonnard datasources configuration
# This file contains credentials - add to .gitignore
# Env vars like {{ env_var('PASSWORD') }} are resolved at deploy time
` + YAML.stringify(file, { indent: 2 });
fs.writeFileSync(filePath, content, { mode: 384 });
}
/**
* Add a single datasource (updates existing or appends new)
*/
function addLocalDatasource(datasource, cwd = process.cwd()) {
const existing = loadLocalDatasources(cwd);
const index = existing.findIndex((ds) => ds.name === datasource.name);
if (index >= 0) existing[index] = datasource;
else existing.push(datasource);
saveLocalDatasources(existing, cwd);
}
/**
* Remove a datasource by name
*/
function removeLocalDatasource(name, cwd = process.cwd()) {
const existing = loadLocalDatasources(cwd);
const filtered = existing.filter((ds) => ds.name !== name);
if (filtered.length === existing.length) return false;
saveLocalDatasources(filtered, cwd);
return true;
}
/**
* Get a single datasource by name
*/
function getLocalDatasource(name, cwd = process.cwd()) {
return loadLocalDatasources(cwd).find((ds) => ds.name === name) ?? null;
}
/**
* Check if a datasource name already exists locally
*/
function datasourceExists(name, cwd = process.cwd()) {
return getLocalDatasource(name, cwd) !== null;
}
/**
* Resolve {{ env_var('VAR_NAME') }} patterns in credentials
* Used at deploy time to resolve env vars before uploading
*/
function resolveEnvVarsInCredentials(credentials) {
const resolved = {};
const missing = [];
const envVarPattern = /\{\{\s*env_var\(['"]([\w_]+)['"]\)\s*\}\}/;
for (const [key, value] of Object.entries(credentials)) {
const match = value.match(envVarPattern);
if (match) {
const varName = match[1];
const envValue = process.env[varName];
if (envValue !== void 0) resolved[key] = envValue;
else {
missing.push(varName);
resolved[key] = value;
}
} else resolved[key] = value;
}
return {
resolved,
missing
};
}
//#endregion
//#region src/lib/local/credentials.ts
/**
* Credential utilities (git tracking check)
*/
const BON_DIR = ".bon";
const DATASOURCES_FILE = "datasources.yaml";
function getDatasourcesPath(cwd = process.cwd()) {
return path.join(cwd, BON_DIR, DATASOURCES_FILE);
}
/**
* Check if datasources file is tracked by git (it shouldn't be - contains credentials)
*/
function isDatasourcesTrackedByGit(cwd = process.cwd()) {
const filePath = getDatasourcesPath(cwd);
if (!fs.existsSync(filePath)) return false;
try {
execFileSync("git", [
"ls-files",
"--error-unmatch",
filePath
], {
cwd,
stdio: "pipe"
});
return true;
} catch {
return false;
}
}
//#endregion
export { getLocalDatasource as a, resolveEnvVarsInCredentials as c, ensureBonDir as i, saveLocalDatasources as l, addLocalDatasource as n, loadLocalDatasources as o, datasourceExists as r, removeLocalDatasource as s, isDatasourcesTrackedByGit as t };
import { r as post } from "./api-BZ9eHZdy.mjs";
import { a as getLocalDatasource, c as resolveEnvVarsInCredentials } from "./local-ByvuW3eV.mjs";
import pc from "picocolors";
import "@inquirer/prompts";
//#region src/commands/datasource/push.ts
/**
* Push a datasource programmatically (for use by deploy command)
* Returns true on success, false on failure
*/
async function pushDatasource(name, options = {}) {
const datasource = getLocalDatasource(name);
if (!datasource) {
if (!options.silent) console.error(pc.red(`Datasource "${name}" not found locally`));
return false;
}
const { resolved, missing } = resolveEnvVarsInCredentials(datasource.credentials);
if (missing.length > 0) {
if (!options.silent) console.error(pc.red(`Missing env vars for "${name}": ${missing.join(", ")}`));
return false;
}
try {
await post("/api/datasources", {
name: datasource.name,
warehouse_type: datasource.type,
config: datasource.config,
credentials: resolved
});
return true;
} catch (err) {
if (!options.silent) {
const message = err instanceof Error ? err.message : String(err);
console.error(pc.red(`Failed to push "${name}": ${message}`));
}
return false;
}
}
//#endregion
export { pushDatasource as t };
import "./api-BZ9eHZdy.mjs";
import "./local-ByvuW3eV.mjs";
import { t as pushDatasource } from "./push-GInMUUa2.mjs";
export { pushDatasource };
import { n as getProjectPaths } from "./project-Dj085D_B.mjs";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import { z } from "zod";
//#region src/lib/schema.ts
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)");
const refreshKeySchema = z.object({
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(),
sql: z.string().optional()
});
const measureTypes = [
"count",
"count_distinct",
"count_distinct_approx",
"sum",
"avg",
"min",
"max",
"number",
"string",
"time",
"boolean",
"running_total",
"number_agg"
];
const dimensionTypes = [
"string",
"number",
"boolean",
"time",
"geo",
"switch"
];
const relationshipTypes = [
"many_to_one",
"one_to_many",
"one_to_one"
];
const granularities = [
"second",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"year"
];
const preAggTypes = [
"rollup",
"original_sql",
"rollup_join",
"rollup_lambda"
];
const formats = [
"percent",
"currency",
"number",
"imageUrl",
"link",
"id"
];
const measureSchema = z.object({
name: identifier,
type: z.enum(measureTypes),
sql: z.string().optional(),
description: z.string().optional(),
title: z.string().optional(),
format: z.enum(formats).optional(),
public: z.boolean().optional(),
filters: z.array(z.object({ sql: z.string() })).optional(),
rolling_window: z.object({
trailing: z.string().optional(),
leading: z.string().optional(),
offset: z.string().optional()
}).optional(),
drill_members: z.array(z.string()).optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const dimensionFormatSchema = z.union([z.enum(formats), z.string().startsWith("%")]);
const dimensionSchema = z.object({
name: identifier,
type: z.enum(dimensionTypes),
sql: z.string().optional(),
primary_key: z.boolean().optional(),
sub_query: z.boolean().optional(),
propagate_filters_to_sub_query: z.boolean().optional(),
description: z.string().optional(),
title: z.string().optional(),
format: dimensionFormatSchema.optional(),
public: z.boolean().optional(),
meta: z.record(z.string(), z.unknown()).optional(),
latitude: z.object({ sql: z.string() }).optional(),
longitude: z.object({ sql: z.string() }).optional(),
case: z.object({
when: z.array(z.object({
sql: z.string(),
label: z.string()
})),
else: z.object({ label: z.string() }).optional()
}).optional()
});
const joinSchema = z.object({
name: identifier,
relationship: z.enum(relationshipTypes),
sql: z.string()
});
const segmentSchema = z.object({
name: identifier,
sql: z.string(),
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional()
});
const preAggregationSchema = z.object({
name: identifier,
type: z.enum(preAggTypes).optional(),
measures: z.array(z.string()).optional(),
dimensions: z.array(z.string()).optional(),
time_dimension: z.string().optional(),
granularity: z.enum(granularities).optional(),
partition_granularity: z.enum(granularities).optional(),
refresh_key: refreshKeySchema.optional(),
scheduled_refresh: z.boolean().optional()
});
const hierarchySchema = z.object({
name: identifier,
levels: z.array(z.string()),
title: z.string().optional(),
public: z.boolean().optional()
});
const cubeSchema = z.object({
name: identifier,
sql: z.string().optional(),
sql_table: z.string().optional(),
data_source: z.string().optional(),
extends: z.string().optional(),
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional(),
refresh_key: refreshKeySchema.optional(),
measures: z.array(measureSchema).optional(),
dimensions: z.array(dimensionSchema).optional(),
joins: z.array(joinSchema).optional(),
segments: z.array(segmentSchema).optional(),
pre_aggregations: z.array(preAggregationSchema).optional(),
hierarchies: z.array(hierarchySchema).optional()
}).refine((data) => data.sql != null || data.sql_table != null || data.extends != null, { message: "sql, sql_table, or extends is required" });
const viewIncludeItemSchema = z.union([z.string(), z.object({
name: z.string(),
alias: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
format: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
})]);
const viewCubeRefSchema = z.object({
join_path: z.string(),
includes: z.union([z.literal("*"), z.array(viewIncludeItemSchema)]).optional(),
excludes: z.array(z.string()).optional(),
prefix: z.boolean().optional()
});
const folderSchema = z.lazy(() => z.object({
name: z.string(),
members: z.array(z.string()).optional(),
folders: z.array(folderSchema).optional()
}));
const viewMeasureSchema = z.object({
name: identifier,
sql: z.string(),
type: z.string(),
format: z.string().optional(),
description: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const viewDimensionSchema = z.object({
name: identifier,
sql: z.string(),
type: z.string(),
description: z.string().optional(),
meta: z.record(z.string(), z.unknown()).optional()
});
const viewSegmentSchema = z.object({
name: identifier,
sql: z.string(),
description: z.string().optional()
});
const viewSchema = z.object({
name: identifier,
description: z.string().optional(),
title: z.string().optional(),
public: z.boolean().optional(),
cubes: z.array(viewCubeRefSchema).optional(),
measures: z.array(viewMeasureSchema).optional(),
dimensions: z.array(viewDimensionSchema).optional(),
segments: z.array(viewSegmentSchema).optional(),
folders: z.array(folderSchema).optional()
});
const fileSchema = z.object({
cubes: z.array(cubeSchema).optional(),
views: z.array(viewSchema).optional()
}).refine((data) => data.cubes && data.cubes.length > 0 || data.views && data.views.length > 0, "File must contain at least one cube or view");
function formatZodError(error, fileName, parsed) {
return error.issues.map((issue) => {
const pathParts = issue.path;
let entityContext = "";
if (pathParts.length >= 2) {
const collection = pathParts[0];
const index = pathParts[1];
const entity = parsed?.[collection]?.[index];
if (entity?.name) entityContext = ` (${entity.name})`;
}
const pathStr = pathParts.join(".");
const location = pathStr ? `${pathStr}${entityContext}` : "";
if (issue.code === "invalid_value" && "values" in issue) return `${fileName}: ${location} — invalid value, expected one of: ${issue.values.join(", ")}`;
return `${fileName}: ${location ? `${location} — ` : ""}${issue.message}`;
});
}
function checkViewMemberConflicts(parsedFiles, cubeMap) {
const errors = [];
for (const { fileName, parsed } of parsedFiles) for (const view of parsed.views ?? []) {
if (!view.name || !view.cubes) continue;
const seen = /* @__PURE__ */ new Map();
for (const m of view.measures ?? []) if (m.name) seen.set(m.name, `${view.name} (direct)`);
for (const d of view.dimensions ?? []) if (d.name) seen.set(d.name, `${view.name} (direct)`);
for (const s of view.segments ?? []) if (s.name) seen.set(s.name, `${view.name} (direct)`);
for (const cubeRef of view.cubes) {
const joinPath = cubeRef.join_path;
if (!joinPath) continue;
const segments = joinPath.split(".");
const targetCubeName = segments[segments.length - 1];
let memberNames = [];
if (cubeRef.includes === "*") {
const cube = cubeMap.get(targetCubeName);
if (!cube) continue;
memberNames = [
...cube.measures,
...cube.dimensions,
...cube.segments
];
} else if (Array.isArray(cubeRef.includes)) {
for (const item of cubeRef.includes) if (typeof item === "string") memberNames.push(item);
else if (item && typeof item === "object" && item.name) memberNames.push(item.alias || item.name);
} else continue;
if (Array.isArray(cubeRef.excludes)) {
const excludeSet = new Set(cubeRef.excludes);
memberNames = memberNames.filter((n) => !excludeSet.has(n));
}
for (const rawName of memberNames) {
const finalName = cubeRef.prefix ? `${targetCubeName}_${rawName}` : rawName;
const existingSource = seen.get(finalName);
if (existingSource) errors.push(`${fileName}: view '${view.name}' — member '${finalName}' from '${joinPath}' conflicts with '${existingSource}'. Use prefix: true or an alias.`);
else seen.set(finalName, joinPath);
}
}
}
return errors;
}
function validateFiles(files) {
const errors = [];
const cubes = [];
const views = [];
const allNames = /* @__PURE__ */ new Map();
const parsedFiles = [];
const cubeMap = /* @__PURE__ */ new Map();
for (const file of files) {
let parsed;
try {
parsed = YAML.parse(file.content);
} catch (err) {
errors.push(`${file.fileName}: YAML parse error — ${err.message}`);
continue;
}
if (!parsed || typeof parsed !== "object") {
errors.push(`${file.fileName}: file is empty or not a YAML object`);
continue;
}
const result = fileSchema.safeParse(parsed);
if (!result.success) {
errors.push(...formatZodError(result.error, file.fileName, parsed));
continue;
}
parsedFiles.push({
fileName: file.fileName,
parsed
});
for (const cube of parsed.cubes ?? []) if (cube.name) {
const existing = allNames.get(cube.name);
if (existing) errors.push(`${file.fileName}: duplicate name '${cube.name}' (also defined in ${existing})`);
else {
allNames.set(cube.name, file.fileName);
cubes.push(cube.name);
cubeMap.set(cube.name, {
measures: (cube.measures ?? []).map((m) => m.name).filter(Boolean),
dimensions: (cube.dimensions ?? []).map((d) => d.name).filter(Boolean),
segments: (cube.segments ?? []).map((s) => s.name).filter(Boolean)
});
}
}
for (const view of parsed.views ?? []) if (view.name) {
const existing = allNames.get(view.name);
if (existing) errors.push(`${file.fileName}: duplicate name '${view.name}' (also defined in ${existing})`);
else {
allNames.set(view.name, file.fileName);
views.push(view.name);
}
}
}
if (errors.length === 0) errors.push(...checkViewMemberConflicts(parsedFiles, cubeMap));
return {
errors,
cubes,
views
};
}
//#endregion
//#region src/lib/validate.ts
function collectYamlFiles(dir, rootDir) {
if (!fs.existsSync(dir)) return [];
const results = [];
function walk(current) {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) walk(fullPath);
else if (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml")) results.push({
fileName: path.relative(rootDir, fullPath),
content: fs.readFileSync(fullPath, "utf-8")
});
}
}
walk(dir);
return results;
}
function checkMissingDescriptions(files) {
const missing = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
const cubes = parsed.cubes || [];
for (const cube of cubes) {
if (!cube.name) continue;
if (!cube.description) missing.push({
parent: cube.name,
type: "cube",
name: cube.name
});
const measures = cube.measures || [];
for (const measure of measures) if (measure.name && !measure.description) missing.push({
parent: cube.name,
type: "measure",
name: measure.name
});
const dimensions = cube.dimensions || [];
for (const dimension of dimensions) if (dimension.name && !dimension.description) missing.push({
parent: cube.name,
type: "dimension",
name: dimension.name
});
}
const views = parsed.views || [];
for (const view of views) {
if (!view.name) continue;
if (!view.description) missing.push({
parent: view.name,
type: "view",
name: view.name
});
}
} catch {}
return missing;
}
function checkSuspectPrimaryKeys(files) {
const suspects = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const cube of parsed.cubes || []) {
if (!cube.name) continue;
for (const dim of cube.dimensions || []) if (dim.primary_key && dim.type === "time") suspects.push({
cube: cube.name,
dimension: dim.name,
type: dim.type
});
}
} catch {}
return suspects;
}
function checkMissingDataSource(files) {
const missing = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const cube of parsed.cubes || []) if (cube.name && !cube.data_source) missing.push(cube.name);
} catch {}
return missing;
}
function checkUnjoinedViewCubes(files) {
const results = [];
for (const file of files) try {
const parsed = YAML.parse(file.content);
if (!parsed) continue;
for (const view of parsed.views || []) {
if (!view.name || !view.cubes || view.cubes.length < 2) continue;
const roots = /* @__PURE__ */ new Set();
for (const cubeRef of view.cubes) {
if (!cubeRef.join_path) continue;
const firstSegment = cubeRef.join_path.split(".")[0];
roots.add(firstSegment);
}
if (roots.size > 1) results.push({
view: view.name,
roots: [...roots]
});
}
} catch {}
return results;
}
async function validate(projectPath) {
const paths = getProjectPaths(projectPath);
const files = [...collectYamlFiles(paths.cubes, projectPath), ...collectYamlFiles(paths.views, projectPath)];
if (files.length === 0) return {
valid: true,
errors: [],
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: [],
suspectPrimaryKeys: [],
unjoinedViewCubes: []
};
const result = validateFiles(files);
if (result.errors.length > 0) return {
valid: false,
errors: result.errors,
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: [],
suspectPrimaryKeys: [],
unjoinedViewCubes: []
};
const missingDescriptions = checkMissingDescriptions(files);
const cubesMissingDataSource = checkMissingDataSource(files);
const suspectPrimaryKeys = checkSuspectPrimaryKeys(files);
const unjoinedViewCubes = checkUnjoinedViewCubes(files);
return {
valid: true,
errors: [],
cubes: result.cubes,
views: result.views,
missingDescriptions,
cubesMissingDataSource,
suspectPrimaryKeys,
unjoinedViewCubes
};
}
//#endregion
export { validate };

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display