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.2.2
to
0.2.3
+78
dist/bin/cubes-9rklhdAJ.mjs
import { i as getProjectPaths } from "./bon.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 { n as resolveEnvVarsInCredentials, r as post, t as getLocalDatasource } from "./bon.mjs";
import pc from "picocolors";
import { confirm } from "@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 {
return false;
}
}
//#endregion
export { pushDatasource };
import { i as getProjectPaths } from "./bon.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 validateFiles(files) {
const errors = [];
const cubes = [];
const views = [];
const allNames = /* @__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;
}
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);
}
}
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);
}
}
}
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 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;
}
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: []
};
const result = validateFiles(files);
if (result.errors.length > 0) return {
valid: false,
errors: result.errors,
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: []
};
const missingDescriptions = checkMissingDescriptions(files);
const cubesMissingDataSource = checkMissingDataSource(files);
return {
valid: true,
errors: [],
cubes: result.cubes,
views: result.views,
missingDescriptions,
cubesMissingDataSource
};
}
//#endregion
export { validate };
+1
-1

@@ -151,3 +151,3 @@ # Deploy

- Verify network access to database
- Run: bon datasource test analytics
- Run: bon datasource add (to reconfigure)
```

@@ -154,0 +154,0 @@

@@ -149,3 +149,2 @@ # Workflow

| `bon datasource list` | List configured sources |
| `bon datasource test <name>` | Test connection (requires login) |
| `bon validate` | Check cube and view syntax |

@@ -152,0 +151,0 @@ | `bon deploy -m "message"` | Deploy to Bonnard (message required) |

@@ -119,3 +119,3 @@ # Validate

3. **Fix errors first** — don't deploy with validation errors
4. **Test connections** — use `bon datasource test <name>` to check connectivity
4. **Test connections** — connections are tested automatically during `bon deploy`

@@ -122,0 +122,0 @@ ## See Also

@@ -18,26 +18,24 @@ ---

```bash
# Option A: Import from dbt (if they use it)
# Option A: Use demo data (no warehouse needed)
bon datasource add --demo
# Option B: Import from dbt (if they use it)
bon datasource add --from-dbt
# Option B: Add manually (interactive)
# Option C: Add manually, non-interactive (preferred for agents)
bon datasource add --name my_warehouse --type postgres \
--host db.example.com --port 5432 --database mydb --schema public \
--user myuser --password mypassword
# Option D: Add manually, interactive (in user's terminal)
bon datasource add
# Option C: Use demo data (no warehouse needed)
bon datasource add --demo
```
Supported types: `postgres` (also works for Redshift), `snowflake`, `bigquery`, `databricks`.
The demo option adds a read-only Contoso retail dataset with tables like
`fact_sales`, `dim_product`, `dim_store`, and `dim_customer`.
Then verify the connection works:
The connection will be tested automatically during `bon deploy`.
```bash
bon datasource test <name>
```
If the test fails, common issues:
- Wrong credentials — re-run `bon datasource add`
- Network/firewall — check warehouse allows connections from this machine
- SSL issues (Postgres) — may need `sslmode` in connection config
## Phase 2: Explore the Data

@@ -44,0 +42,0 @@

@@ -51,21 +51,23 @@ ---

Add a datasource pointing to the same database that Metabase queries:
Add a datasource pointing to the same database that Metabase queries.
The database connection details can often be found in Metabase under
Admin > Databases, or in the analysis report header.
```bash
# Interactive setup
bon datasource add
# Non-interactive (preferred for agents)
bon datasource add --name my_warehouse --type postgres \
--host db.example.com --port 5432 --database mydb --schema public \
--user myuser --password mypassword
# Or import from dbt if available
# Import from dbt if available
bon datasource add --from-dbt
# Interactive setup (in user's terminal)
bon datasource add
```
Then verify the connection:
Supported types: `postgres` (also works for Redshift), `snowflake`, `bigquery`, `databricks`.
```bash
bon datasource test <name>
```
The connection will be tested automatically during `bon deploy`.
The database connection details can often be found in Metabase under
Admin > Databases, or in the analysis report header.
## Phase 4: Explore Key Tables

@@ -72,0 +74,0 @@

@@ -66,3 +66,2 @@ # Bonnard Semantic Layer

| `bon datasource add --from-dbt` | Import from dbt profiles |
| `bon datasource test <name>` | Test connection (requires login) |
| `bon validate` | Validate YAML syntax, warn on missing descriptions and `data_source` |

@@ -69,0 +68,0 @@ | `bon deploy -m "message"` | Deploy to Bonnard (requires login, message required) |

{
"name": "@bonnard/cli",
"version": "0.2.2",
"version": "0.2.3",
"type": "module",

@@ -5,0 +5,0 @@ "bin": {

import { t as getProjectPaths } from "./bon.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 { t as getProjectPaths } from "./bon.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 validateFiles(files) {
const errors = [];
const cubes = [];
const views = [];
const allNames = /* @__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;
}
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);
}
}
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);
}
}
}
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 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;
}
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: []
};
const result = validateFiles(files);
if (result.errors.length > 0) return {
valid: false,
errors: result.errors,
cubes: [],
views: [],
missingDescriptions: [],
cubesMissingDataSource: []
};
const missingDescriptions = checkMissingDescriptions(files);
const cubesMissingDataSource = checkMissingDataSource(files);
return {
valid: true,
errors: [],
cubes: result.cubes,
views: result.views,
missingDescriptions,
cubesMissingDataSource
};
}
//#endregion
export { validate };

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

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet