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.10
to
0.3.11
+78
dist/bin/cubes-8O9HNxuD.mjs
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 { 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 };
+1
-1
{
"name": "@bonnard/cli",
"version": "0.3.10",
"version": "0.3.11",
"type": "module",

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

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 { 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 };
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