yamchart 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generate-RD3LCS73.js +315 -0
- package/dist/generate-RD3LCS73.js.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -1
- package/dist/sync-dbt-IDDD4X2Z.js +466 -0
- package/dist/sync-dbt-IDDD4X2Z.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// src/commands/generate.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { join as join2 } from "path";
|
|
4
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
|
|
6
|
+
// src/generate/detector.ts
|
|
7
|
+
var DATE_TYPES = ["date", "timestamp", "datetime", "timestamptz", "timestamp_ntz"];
|
|
8
|
+
var NUMERIC_TYPES = ["int", "integer", "bigint", "smallint", "numeric", "decimal", "float", "double", "real", "number"];
|
|
9
|
+
function isDateColumn(col) {
|
|
10
|
+
const typeLower = (col.data_type || "").toLowerCase();
|
|
11
|
+
if (DATE_TYPES.some((t) => typeLower.includes(t))) return true;
|
|
12
|
+
const nameLower = col.name.toLowerCase();
|
|
13
|
+
return nameLower.endsWith("_at") || nameLower.endsWith("_date") || nameLower.endsWith("_time");
|
|
14
|
+
}
|
|
15
|
+
function isNumericColumn(col) {
|
|
16
|
+
const typeLower = (col.data_type || "").toLowerCase();
|
|
17
|
+
return NUMERIC_TYPES.some((t) => typeLower.includes(t));
|
|
18
|
+
}
|
|
19
|
+
function isPrimaryKey(col) {
|
|
20
|
+
if (col.hints.includes("primary_key") || col.hints.includes("unique")) return true;
|
|
21
|
+
return col.name === "id";
|
|
22
|
+
}
|
|
23
|
+
function isForeignKey(col) {
|
|
24
|
+
return col.hints.some((h) => h.startsWith("fk:"));
|
|
25
|
+
}
|
|
26
|
+
function detectColumnTypes(columns) {
|
|
27
|
+
const dateColumns = [];
|
|
28
|
+
const metricColumns = [];
|
|
29
|
+
const dimensionColumns = [];
|
|
30
|
+
const primaryKeys = [];
|
|
31
|
+
const foreignKeys = [];
|
|
32
|
+
for (const col of columns) {
|
|
33
|
+
if (isPrimaryKey(col)) {
|
|
34
|
+
primaryKeys.push(col.name);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (isForeignKey(col)) {
|
|
38
|
+
foreignKeys.push(col.name);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (isDateColumn(col)) {
|
|
42
|
+
dateColumns.push(col.name);
|
|
43
|
+
} else if (isNumericColumn(col)) {
|
|
44
|
+
metricColumns.push(col.name);
|
|
45
|
+
} else {
|
|
46
|
+
dimensionColumns.push(col.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { dateColumns, metricColumns, dimensionColumns, primaryKeys, foreignKeys };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/generate/variants.ts
|
|
53
|
+
function formatDate() {
|
|
54
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
55
|
+
}
|
|
56
|
+
function quoteIdentifier(name) {
|
|
57
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
58
|
+
}
|
|
59
|
+
function generateHeader(modelName, variantName, description, tableName) {
|
|
60
|
+
return `-- @generated: from dbt model '${modelName}' on ${formatDate()}
|
|
61
|
+
-- @name: ${variantName}
|
|
62
|
+
-- @description: ${description}
|
|
63
|
+
-- @source: ${tableName}
|
|
64
|
+
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function generateMetricSelects(metrics) {
|
|
68
|
+
return metrics.map((m) => ` ${m.aggregation.toUpperCase()}(${quoteIdentifier(m.name)}) AS ${quoteIdentifier(m.aggregation + "_" + m.name)}`).join(",\n");
|
|
69
|
+
}
|
|
70
|
+
function generateVariants(config) {
|
|
71
|
+
const variants = [];
|
|
72
|
+
const { modelName, tableName, dateColumn, metricColumns, dimensionColumns } = config;
|
|
73
|
+
if (dateColumn && metricColumns.length > 0) {
|
|
74
|
+
const name = `${modelName}_over_time`;
|
|
75
|
+
const description = `${modelName} aggregated over time`;
|
|
76
|
+
const sql = `${generateHeader(modelName, name, description, tableName)}SELECT
|
|
77
|
+
date_trunc('{{ granularity }}', ${quoteIdentifier(dateColumn)}) AS period,
|
|
78
|
+
${generateMetricSelects(metricColumns)}
|
|
79
|
+
FROM ${tableName}
|
|
80
|
+
WHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'
|
|
81
|
+
AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'
|
|
82
|
+
GROUP BY 1
|
|
83
|
+
ORDER BY 1
|
|
84
|
+
`;
|
|
85
|
+
variants.push({ name, filename: `${name}.sql`, description, sql });
|
|
86
|
+
}
|
|
87
|
+
for (const dim of dimensionColumns) {
|
|
88
|
+
const name = `${modelName}_by_${dim}`;
|
|
89
|
+
const description = `${modelName} grouped by ${dim}`;
|
|
90
|
+
let sql = generateHeader(modelName, name, description, tableName);
|
|
91
|
+
sql += `SELECT
|
|
92
|
+
${quoteIdentifier(dim)},
|
|
93
|
+
${generateMetricSelects(metricColumns)}
|
|
94
|
+
FROM ${tableName}
|
|
95
|
+
`;
|
|
96
|
+
if (dateColumn) {
|
|
97
|
+
sql += `WHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'
|
|
98
|
+
AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
sql += `GROUP BY 1
|
|
102
|
+
ORDER BY 2 DESC
|
|
103
|
+
`;
|
|
104
|
+
variants.push({ name, filename: `${name}.sql`, description, sql });
|
|
105
|
+
}
|
|
106
|
+
if (metricColumns.length > 0) {
|
|
107
|
+
const name = `${modelName}_kpi`;
|
|
108
|
+
const description = `${modelName} summary metrics`;
|
|
109
|
+
let sql = generateHeader(modelName, name, description, tableName);
|
|
110
|
+
sql += `SELECT
|
|
111
|
+
${generateMetricSelects(metricColumns)}
|
|
112
|
+
FROM ${tableName}
|
|
113
|
+
`;
|
|
114
|
+
if (dateColumn) {
|
|
115
|
+
sql += `WHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'
|
|
116
|
+
AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
variants.push({ name, filename: `${name}.sql`, description, sql });
|
|
120
|
+
}
|
|
121
|
+
return variants;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/generate/writer.ts
|
|
125
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
126
|
+
import { join } from "path";
|
|
127
|
+
import { existsSync } from "fs";
|
|
128
|
+
async function writeStub(projectDir, filename, content) {
|
|
129
|
+
const modelsDir = join(projectDir, "models");
|
|
130
|
+
await mkdir(modelsDir, { recursive: true });
|
|
131
|
+
await writeFile(join(modelsDir, filename), content, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
async function stubExists(projectDir, filename) {
|
|
134
|
+
return existsSync(join(projectDir, "models", filename));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/generate/prompts.ts
|
|
138
|
+
import { confirm, select, checkbox } from "@inquirer/prompts";
|
|
139
|
+
function buildDateColumnChoices(detected) {
|
|
140
|
+
const choices = detected.dateColumns.map((col, i) => ({
|
|
141
|
+
name: col,
|
|
142
|
+
value: col,
|
|
143
|
+
checked: i === 0
|
|
144
|
+
}));
|
|
145
|
+
choices.push({ name: "Skip time series", value: null, checked: false });
|
|
146
|
+
return choices;
|
|
147
|
+
}
|
|
148
|
+
function buildMetricChoices(detected) {
|
|
149
|
+
return detected.metricColumns.map((col) => ({
|
|
150
|
+
name: `${col} (sum)`,
|
|
151
|
+
value: { name: col, aggregation: "sum" },
|
|
152
|
+
checked: true
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
function buildDimensionChoices(detected) {
|
|
156
|
+
return detected.dimensionColumns.map((col) => ({
|
|
157
|
+
name: col,
|
|
158
|
+
value: col,
|
|
159
|
+
checked: true
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
async function promptDateColumn(modelName, detected) {
|
|
163
|
+
if (detected.dateColumns.length === 0) return null;
|
|
164
|
+
const defaultCol = detected.dateColumns[0];
|
|
165
|
+
const confirmed = await confirm({
|
|
166
|
+
message: `${modelName}: Use '${defaultCol}' for time series?`,
|
|
167
|
+
default: true
|
|
168
|
+
});
|
|
169
|
+
if (confirmed) return defaultCol;
|
|
170
|
+
if (detected.dateColumns.length === 1) return null;
|
|
171
|
+
return select({
|
|
172
|
+
message: "Select date column:",
|
|
173
|
+
choices: buildDateColumnChoices(detected).map((c) => ({ name: c.name, value: c.value }))
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function promptMetrics(modelName, detected) {
|
|
177
|
+
if (detected.metricColumns.length === 0) return [];
|
|
178
|
+
const choices = buildMetricChoices(detected);
|
|
179
|
+
const selected = await checkbox({
|
|
180
|
+
message: `${modelName}: Select metrics to aggregate:`,
|
|
181
|
+
choices: choices.map((c) => ({ name: c.name, value: c.value, checked: c.checked }))
|
|
182
|
+
});
|
|
183
|
+
return selected;
|
|
184
|
+
}
|
|
185
|
+
async function promptDimensions(modelName, detected) {
|
|
186
|
+
if (detected.dimensionColumns.length === 0) return [];
|
|
187
|
+
const choices = buildDimensionChoices(detected);
|
|
188
|
+
const selected = await checkbox({
|
|
189
|
+
message: `${modelName}: Select dimensions for grouping:`,
|
|
190
|
+
choices: choices.map((c) => ({ name: c.name, value: c.value, checked: c.checked }))
|
|
191
|
+
});
|
|
192
|
+
return selected;
|
|
193
|
+
}
|
|
194
|
+
async function promptConfirmVariants(modelName, variantNames) {
|
|
195
|
+
const choices = variantNames.map((name) => ({ name, value: name, checked: true }));
|
|
196
|
+
return checkbox({
|
|
197
|
+
message: `${modelName}: Generate these stubs?`,
|
|
198
|
+
choices
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function promptOverwrite(filename) {
|
|
202
|
+
return select({
|
|
203
|
+
message: `${filename} already exists:`,
|
|
204
|
+
choices: [
|
|
205
|
+
{ name: "Overwrite", value: "overwrite" },
|
|
206
|
+
{ name: "Skip", value: "skip" },
|
|
207
|
+
{ name: "Rename (add suffix)", value: "rename" }
|
|
208
|
+
]
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/commands/generate.ts
|
|
213
|
+
async function loadCatalog(projectDir) {
|
|
214
|
+
const catalogPath = join2(projectDir, ".yamchart", "catalog.json");
|
|
215
|
+
if (!existsSync2(catalogPath)) return null;
|
|
216
|
+
const content = await readFile(catalogPath, "utf-8");
|
|
217
|
+
return JSON.parse(content);
|
|
218
|
+
}
|
|
219
|
+
async function processModel(projectDir, model, yolo) {
|
|
220
|
+
const detected = detectColumnTypes(model.columns);
|
|
221
|
+
let dateColumn;
|
|
222
|
+
let metrics;
|
|
223
|
+
let dimensions;
|
|
224
|
+
if (yolo) {
|
|
225
|
+
dateColumn = detected.dateColumns[0] || null;
|
|
226
|
+
metrics = detected.metricColumns.map((name) => ({ name, aggregation: "sum" }));
|
|
227
|
+
dimensions = detected.dimensionColumns;
|
|
228
|
+
} else {
|
|
229
|
+
dateColumn = await promptDateColumn(model.name, detected);
|
|
230
|
+
metrics = await promptMetrics(model.name, detected);
|
|
231
|
+
dimensions = await promptDimensions(model.name, detected);
|
|
232
|
+
}
|
|
233
|
+
if (metrics.length === 0) {
|
|
234
|
+
return { created: 0, skipped: 0 };
|
|
235
|
+
}
|
|
236
|
+
const variants = generateVariants({
|
|
237
|
+
modelName: model.name,
|
|
238
|
+
tableName: model.table || model.name,
|
|
239
|
+
dateColumn,
|
|
240
|
+
metricColumns: metrics,
|
|
241
|
+
dimensionColumns: dimensions
|
|
242
|
+
});
|
|
243
|
+
let confirmedVariants = variants;
|
|
244
|
+
if (!yolo && variants.length > 0) {
|
|
245
|
+
const selected = await promptConfirmVariants(
|
|
246
|
+
model.name,
|
|
247
|
+
variants.map((v) => v.name)
|
|
248
|
+
);
|
|
249
|
+
confirmedVariants = variants.filter((v) => selected.includes(v.name));
|
|
250
|
+
}
|
|
251
|
+
let created = 0;
|
|
252
|
+
let skipped = 0;
|
|
253
|
+
for (const variant of confirmedVariants) {
|
|
254
|
+
const exists = await stubExists(projectDir, variant.filename);
|
|
255
|
+
if (exists && !yolo) {
|
|
256
|
+
const action = await promptOverwrite(variant.filename);
|
|
257
|
+
if (action === "skip") {
|
|
258
|
+
skipped++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (action === "rename") {
|
|
262
|
+
variant.filename = variant.filename.replace(".sql", "_new.sql");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
await writeStub(projectDir, variant.filename, variant.sql);
|
|
266
|
+
created++;
|
|
267
|
+
}
|
|
268
|
+
return { created, skipped };
|
|
269
|
+
}
|
|
270
|
+
async function generate(projectDir, options) {
|
|
271
|
+
const catalog = await loadCatalog(projectDir);
|
|
272
|
+
if (!catalog) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: "catalog.json not found. Run `yamchart sync-dbt` first.",
|
|
276
|
+
modelsProcessed: 0,
|
|
277
|
+
filesCreated: 0,
|
|
278
|
+
filesSkipped: 0
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
let models = catalog.models;
|
|
282
|
+
if (options.model) {
|
|
283
|
+
models = models.filter((m) => m.name === options.model);
|
|
284
|
+
if (models.length === 0) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
error: `Model '${options.model}' not found in catalog`,
|
|
288
|
+
modelsProcessed: 0,
|
|
289
|
+
filesCreated: 0,
|
|
290
|
+
filesSkipped: 0
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let totalCreated = 0;
|
|
295
|
+
let totalSkipped = 0;
|
|
296
|
+
for (const model of models) {
|
|
297
|
+
const { created, skipped } = await processModel(
|
|
298
|
+
projectDir,
|
|
299
|
+
model,
|
|
300
|
+
options.yolo ?? false
|
|
301
|
+
);
|
|
302
|
+
totalCreated += created;
|
|
303
|
+
totalSkipped += skipped;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
modelsProcessed: models.length,
|
|
308
|
+
filesCreated: totalCreated,
|
|
309
|
+
filesSkipped: totalSkipped
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
export {
|
|
313
|
+
generate
|
|
314
|
+
};
|
|
315
|
+
//# sourceMappingURL=generate-RD3LCS73.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/generate.ts","../src/generate/detector.ts","../src/generate/variants.ts","../src/generate/writer.ts","../src/generate/prompts.ts"],"sourcesContent":["// apps/cli/src/commands/generate.ts\nimport { readFile } from 'fs/promises';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport type { CatalogData, CatalogModel } from '../dbt/catalog.js';\nimport { detectColumnTypes } from '../generate/detector.js';\nimport { generateVariants, type MetricColumn } from '../generate/variants.js';\nimport { writeStub, stubExists } from '../generate/writer.js';\nimport {\n promptDateColumn,\n promptMetrics,\n promptDimensions,\n promptConfirmVariants,\n promptOverwrite,\n} from '../generate/prompts.js';\n\nexport interface GenerateOptions {\n model?: string;\n yolo?: boolean;\n}\n\nexport interface GenerateResult {\n success: boolean;\n error?: string;\n modelsProcessed: number;\n filesCreated: number;\n filesSkipped: number;\n}\n\nasync function loadCatalog(projectDir: string): Promise<CatalogData | null> {\n const catalogPath = join(projectDir, '.yamchart', 'catalog.json');\n if (!existsSync(catalogPath)) return null;\n\n const content = await readFile(catalogPath, 'utf-8');\n return JSON.parse(content);\n}\n\nasync function processModel(\n projectDir: string,\n model: CatalogModel,\n yolo: boolean\n): Promise<{ created: number; skipped: number }> {\n const detected = detectColumnTypes(model.columns);\n\n let dateColumn: string | null;\n let metrics: MetricColumn[];\n let dimensions: string[];\n\n if (yolo) {\n // Use all defaults\n dateColumn = detected.dateColumns[0] || null;\n metrics = detected.metricColumns.map(name => ({ name, aggregation: 'sum' as const }));\n dimensions = detected.dimensionColumns;\n } else {\n // Interactive prompts\n dateColumn = await promptDateColumn(model.name, detected);\n metrics = await promptMetrics(model.name, detected);\n dimensions = await promptDimensions(model.name, detected);\n }\n\n if (metrics.length === 0) {\n return { created: 0, skipped: 0 };\n }\n\n const variants = generateVariants({\n modelName: model.name,\n tableName: model.table || model.name,\n dateColumn,\n metricColumns: metrics,\n dimensionColumns: dimensions,\n });\n\n let confirmedVariants = variants;\n if (!yolo && variants.length > 0) {\n const selected = await promptConfirmVariants(\n model.name,\n variants.map(v => v.name)\n );\n confirmedVariants = variants.filter(v => selected.includes(v.name));\n }\n\n let created = 0;\n let skipped = 0;\n\n for (const variant of confirmedVariants) {\n const exists = await stubExists(projectDir, variant.filename);\n\n if (exists && !yolo) {\n const action = await promptOverwrite(variant.filename);\n if (action === 'skip') {\n skipped++;\n continue;\n }\n if (action === 'rename') {\n variant.filename = variant.filename.replace('.sql', '_new.sql');\n }\n }\n\n await writeStub(projectDir, variant.filename, variant.sql);\n created++;\n }\n\n return { created, skipped };\n}\n\nexport async function generate(\n projectDir: string,\n options: GenerateOptions\n): Promise<GenerateResult> {\n const catalog = await loadCatalog(projectDir);\n\n if (!catalog) {\n return {\n success: false,\n error: 'catalog.json not found. Run `yamchart sync-dbt` first.',\n modelsProcessed: 0,\n filesCreated: 0,\n filesSkipped: 0,\n };\n }\n\n let models = catalog.models;\n\n if (options.model) {\n models = models.filter(m => m.name === options.model);\n if (models.length === 0) {\n return {\n success: false,\n error: `Model '${options.model}' not found in catalog`,\n modelsProcessed: 0,\n filesCreated: 0,\n filesSkipped: 0,\n };\n }\n }\n\n let totalCreated = 0;\n let totalSkipped = 0;\n\n for (const model of models) {\n const { created, skipped } = await processModel(\n projectDir,\n model,\n options.yolo ?? false\n );\n totalCreated += created;\n totalSkipped += skipped;\n }\n\n return {\n success: true,\n modelsProcessed: models.length,\n filesCreated: totalCreated,\n filesSkipped: totalSkipped,\n };\n}\n","import type { DbtColumn } from '../dbt/types.js';\n\nexport interface DetectedColumns {\n dateColumns: string[];\n metricColumns: string[];\n dimensionColumns: string[];\n primaryKeys: string[];\n foreignKeys: string[];\n}\n\nconst DATE_TYPES = ['date', 'timestamp', 'datetime', 'timestamptz', 'timestamp_ntz'];\nconst NUMERIC_TYPES = ['int', 'integer', 'bigint', 'smallint', 'numeric', 'decimal', 'float', 'double', 'real', 'number'];\nconst STRING_TYPES = ['string', 'varchar', 'char', 'text'];\nconst BOOLEAN_TYPES = ['boolean', 'bool', 'bit'];\n\nfunction isDateColumn(col: DbtColumn): boolean {\n const typeLower = (col.data_type || '').toLowerCase();\n if (DATE_TYPES.some(t => typeLower.includes(t))) return true;\n\n const nameLower = col.name.toLowerCase();\n return nameLower.endsWith('_at') || nameLower.endsWith('_date') || nameLower.endsWith('_time');\n}\n\nfunction isNumericColumn(col: DbtColumn): boolean {\n const typeLower = (col.data_type || '').toLowerCase();\n return NUMERIC_TYPES.some(t => typeLower.includes(t));\n}\n\nfunction isStringColumn(col: DbtColumn): boolean {\n const typeLower = (col.data_type || '').toLowerCase();\n return STRING_TYPES.some(t => typeLower.includes(t));\n}\n\nfunction isBooleanColumn(col: DbtColumn): boolean {\n const typeLower = (col.data_type || '').toLowerCase();\n return BOOLEAN_TYPES.some(t => typeLower.includes(t));\n}\n\nfunction isPrimaryKey(col: DbtColumn): boolean {\n if (col.hints.includes('primary_key') || col.hints.includes('unique')) return true;\n return col.name === 'id';\n}\n\nfunction isForeignKey(col: DbtColumn): boolean {\n return col.hints.some(h => h.startsWith('fk:'));\n}\n\nexport function detectColumnTypes(columns: DbtColumn[]): DetectedColumns {\n const dateColumns: string[] = [];\n const metricColumns: string[] = [];\n const dimensionColumns: string[] = [];\n const primaryKeys: string[] = [];\n const foreignKeys: string[] = [];\n\n for (const col of columns) {\n if (isPrimaryKey(col)) {\n primaryKeys.push(col.name);\n continue;\n }\n\n if (isForeignKey(col)) {\n foreignKeys.push(col.name);\n continue;\n }\n\n if (isDateColumn(col)) {\n dateColumns.push(col.name);\n } else if (isNumericColumn(col)) {\n metricColumns.push(col.name);\n } else {\n // String, boolean, or unknown types all become dimensions\n dimensionColumns.push(col.name);\n }\n }\n\n return { dateColumns, metricColumns, dimensionColumns, primaryKeys, foreignKeys };\n}\n","// apps/cli/src/generate/variants.ts\n\nexport interface MetricColumn {\n name: string;\n aggregation: 'sum' | 'avg' | 'count' | 'min' | 'max';\n}\n\nexport interface VariantConfig {\n modelName: string;\n tableName: string;\n dateColumn: string | null;\n metricColumns: MetricColumn[];\n dimensionColumns: string[];\n}\n\nexport interface GeneratedVariant {\n name: string;\n filename: string;\n description: string;\n sql: string;\n}\n\nfunction formatDate(): string {\n return new Date().toISOString().split('T')[0];\n}\n\nfunction quoteIdentifier(name: string): string {\n // Double quotes for ANSI SQL standard\n // Escape any existing double quotes by doubling them\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction generateHeader(modelName: string, variantName: string, description: string, tableName: string): string {\n return `-- @generated: from dbt model '${modelName}' on ${formatDate()}\n-- @name: ${variantName}\n-- @description: ${description}\n-- @source: ${tableName}\n\n`;\n}\n\nfunction generateMetricSelects(metrics: MetricColumn[]): string {\n return metrics\n .map(m => ` ${m.aggregation.toUpperCase()}(${quoteIdentifier(m.name)}) AS ${quoteIdentifier(m.aggregation + '_' + m.name)}`)\n .join(',\\n');\n}\n\nexport function generateVariants(config: VariantConfig): GeneratedVariant[] {\n const variants: GeneratedVariant[] = [];\n const { modelName, tableName, dateColumn, metricColumns, dimensionColumns } = config;\n\n // Time series variant\n if (dateColumn && metricColumns.length > 0) {\n const name = `${modelName}_over_time`;\n const description = `${modelName} aggregated over time`;\n const sql = `${generateHeader(modelName, name, description, tableName)}SELECT\n date_trunc('{{ granularity }}', ${quoteIdentifier(dateColumn)}) AS period,\n${generateMetricSelects(metricColumns)}\nFROM ${tableName}\nWHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'\n AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'\nGROUP BY 1\nORDER BY 1\n`;\n variants.push({ name, filename: `${name}.sql`, description, sql });\n }\n\n // Dimension variants\n for (const dim of dimensionColumns) {\n const name = `${modelName}_by_${dim}`;\n const description = `${modelName} grouped by ${dim}`;\n let sql = generateHeader(modelName, name, description, tableName);\n sql += `SELECT\n ${quoteIdentifier(dim)},\n${generateMetricSelects(metricColumns)}\nFROM ${tableName}\n`;\n if (dateColumn) {\n sql += `WHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'\n AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'\n`;\n }\n sql += `GROUP BY 1\nORDER BY 2 DESC\n`;\n variants.push({ name, filename: `${name}.sql`, description, sql });\n }\n\n // KPI variant\n if (metricColumns.length > 0) {\n const name = `${modelName}_kpi`;\n const description = `${modelName} summary metrics`;\n let sql = generateHeader(modelName, name, description, tableName);\n sql += `SELECT\n${generateMetricSelects(metricColumns)}\nFROM ${tableName}\n`;\n if (dateColumn) {\n sql += `WHERE ${quoteIdentifier(dateColumn)} >= '{{ start_date }}'\n AND ${quoteIdentifier(dateColumn)} <= '{{ end_date }}'\n`;\n }\n variants.push({ name, filename: `${name}.sql`, description, sql });\n }\n\n return variants;\n}\n","// apps/cli/src/generate/writer.ts\nimport { writeFile, mkdir } from 'fs/promises';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\n\nexport async function writeStub(projectDir: string, filename: string, content: string): Promise<void> {\n const modelsDir = join(projectDir, 'models');\n await mkdir(modelsDir, { recursive: true });\n await writeFile(join(modelsDir, filename), content, 'utf-8');\n}\n\nexport async function stubExists(projectDir: string, filename: string): Promise<boolean> {\n return existsSync(join(projectDir, 'models', filename));\n}\n","// apps/cli/src/generate/prompts.ts\nimport { confirm, select, checkbox } from '@inquirer/prompts';\nimport type { DetectedColumns } from './detector.js';\nimport type { MetricColumn } from './variants.js';\n\nexport interface DateColumnChoice {\n name: string;\n value: string | null;\n checked: boolean;\n}\n\nexport interface MetricChoice {\n name: string;\n value: MetricColumn;\n checked: boolean;\n}\n\nexport interface DimensionChoice {\n name: string;\n value: string;\n checked: boolean;\n}\n\nexport function buildDateColumnChoices(detected: DetectedColumns): DateColumnChoice[] {\n const choices: DateColumnChoice[] = detected.dateColumns.map((col, i) => ({\n name: col,\n value: col,\n checked: i === 0,\n }));\n choices.push({ name: 'Skip time series', value: null, checked: false });\n return choices;\n}\n\nexport function buildMetricChoices(detected: DetectedColumns): MetricChoice[] {\n return detected.metricColumns.map(col => ({\n name: `${col} (sum)`,\n value: { name: col, aggregation: 'sum' as const },\n checked: true,\n }));\n}\n\nexport function buildDimensionChoices(detected: DetectedColumns): DimensionChoice[] {\n return detected.dimensionColumns.map(col => ({\n name: col,\n value: col,\n checked: true,\n }));\n}\n\nexport async function promptDateColumn(modelName: string, detected: DetectedColumns): Promise<string | null> {\n if (detected.dateColumns.length === 0) return null;\n\n const defaultCol = detected.dateColumns[0];\n const confirmed = await confirm({\n message: `${modelName}: Use '${defaultCol}' for time series?`,\n default: true,\n });\n\n if (confirmed) return defaultCol;\n\n if (detected.dateColumns.length === 1) return null;\n\n return select({\n message: 'Select date column:',\n choices: buildDateColumnChoices(detected).map(c => ({ name: c.name, value: c.value })),\n });\n}\n\nexport async function promptMetrics(modelName: string, detected: DetectedColumns): Promise<MetricColumn[]> {\n if (detected.metricColumns.length === 0) return [];\n\n const choices = buildMetricChoices(detected);\n const selected = await checkbox({\n message: `${modelName}: Select metrics to aggregate:`,\n choices: choices.map(c => ({ name: c.name, value: c.value, checked: c.checked })),\n });\n\n return selected;\n}\n\nexport async function promptDimensions(modelName: string, detected: DetectedColumns): Promise<string[]> {\n if (detected.dimensionColumns.length === 0) return [];\n\n const choices = buildDimensionChoices(detected);\n const selected = await checkbox({\n message: `${modelName}: Select dimensions for grouping:`,\n choices: choices.map(c => ({ name: c.name, value: c.value, checked: c.checked })),\n });\n\n return selected;\n}\n\nexport async function promptConfirmVariants(modelName: string, variantNames: string[]): Promise<string[]> {\n const choices = variantNames.map(name => ({ name, value: name, checked: true }));\n\n return checkbox({\n message: `${modelName}: Generate these stubs?`,\n choices,\n });\n}\n\nexport async function promptOverwrite(filename: string): Promise<'overwrite' | 'skip' | 'rename'> {\n return select({\n message: `${filename} already exists:`,\n choices: [\n { name: 'Overwrite', value: 'overwrite' as const },\n { name: 'Skip', value: 'skip' as const },\n { name: 'Rename (add suffix)', value: 'rename' as const },\n ],\n });\n}\n"],"mappings":"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","names":["join","existsSync","join","existsSync"]}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
info,
|
|
8
8
|
loadEnvFile,
|
|
9
9
|
newline,
|
|
10
|
+
spinner,
|
|
10
11
|
success,
|
|
11
12
|
validateProject,
|
|
12
13
|
warning
|
|
@@ -108,5 +109,64 @@ program.command("init").description("Create a new yamchart project").argument("[
|
|
|
108
109
|
newline();
|
|
109
110
|
info(`Run \`cd ${directory === "." ? basename(targetDir) : directory} && yamchart dev\` to start.`);
|
|
110
111
|
});
|
|
112
|
+
program.command("sync-dbt").description("Sync dbt project metadata into AI-readable catalog").option("-s, --source <type>", "Source type: local, github, dbt-cloud", "local").option("-p, --path <dir>", "Path to dbt project (for local source)").option("--repo <repo>", "GitHub repository (for github source)").option("--branch <branch>", "Git branch (for github source)", "main").option("-i, --include <patterns...>", "Include glob patterns").option("-e, --exclude <patterns...>", "Exclude glob patterns").option("-t, --tag <tags...>", "Filter by dbt tags").option("--refresh", "Re-sync using saved configuration").action(async (options) => {
|
|
113
|
+
const { syncDbt, loadSyncConfig } = await import("./sync-dbt-IDDD4X2Z.js");
|
|
114
|
+
const projectDir = await findProjectRoot(process.cwd());
|
|
115
|
+
if (!projectDir) {
|
|
116
|
+
error("yamchart.yaml not found");
|
|
117
|
+
detail("Run this command from a yamchart project directory");
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
if (options.refresh) {
|
|
121
|
+
const savedConfig = await loadSyncConfig(projectDir);
|
|
122
|
+
if (!savedConfig) {
|
|
123
|
+
error("No saved sync config found");
|
|
124
|
+
detail("Run sync-dbt without --refresh first");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
info(`Re-syncing from ${savedConfig.source}:${savedConfig.path || savedConfig.repo}`);
|
|
128
|
+
}
|
|
129
|
+
const spin = spinner("Syncing dbt metadata...");
|
|
130
|
+
const result = await syncDbt(projectDir, {
|
|
131
|
+
source: options.source,
|
|
132
|
+
path: options.path,
|
|
133
|
+
repo: options.repo,
|
|
134
|
+
branch: options.branch,
|
|
135
|
+
include: options.include || [],
|
|
136
|
+
exclude: options.exclude || [],
|
|
137
|
+
tags: options.tag || [],
|
|
138
|
+
refresh: options.refresh
|
|
139
|
+
});
|
|
140
|
+
spin.stop();
|
|
141
|
+
if (!result.success) {
|
|
142
|
+
error(result.error || "Sync failed");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
success(`Synced ${result.modelsIncluded} models to .yamchart/catalog.md`);
|
|
146
|
+
if (result.modelsExcluded > 0) {
|
|
147
|
+
detail(`${result.modelsExcluded} models filtered out`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
program.command("generate").description("Generate SQL model stubs from dbt catalog").argument("[model]", "Specific model to generate (optional)").option("--yolo", "Skip all prompts, use defaults for everything").action(async (model, options) => {
|
|
151
|
+
const { generate } = await import("./generate-RD3LCS73.js");
|
|
152
|
+
const projectDir = await findProjectRoot(process.cwd());
|
|
153
|
+
if (!projectDir) {
|
|
154
|
+
error("yamchart.yaml not found");
|
|
155
|
+
detail("Run this command from a yamchart project directory");
|
|
156
|
+
process.exit(2);
|
|
157
|
+
}
|
|
158
|
+
const result = await generate(projectDir, {
|
|
159
|
+
model,
|
|
160
|
+
yolo: options.yolo
|
|
161
|
+
});
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
error(result.error || "Generate failed");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
success(`Generated ${result.filesCreated} model stubs`);
|
|
167
|
+
if (result.filesSkipped > 0) {
|
|
168
|
+
detail(`${result.filesSkipped} files skipped`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
111
171
|
program.parse();
|
|
112
172
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport { resolve, basename } from 'path';\nimport { validateProject } from './commands/validate.js';\nimport { findProjectRoot, loadEnvFile } from './utils/config.js';\nimport * as output from './utils/output.js';\n\nconst program = new Command();\n\nprogram\n .name('yamchart')\n .description('Git-native business intelligence dashboards')\n .version('0.1.0');\n\nprogram\n .command('validate')\n .description('Validate configuration files')\n .argument('[path]', 'Path to yamchart project', '.')\n .option('--dry-run', 'Connect to database and test queries with EXPLAIN')\n .option('-c, --connection <name>', 'Connection to use for dry-run')\n .option('--json', 'Output as JSON')\n .action(async (path: string, options: { dryRun?: boolean; connection?: string; json?: boolean }) => {\n const startPath = resolve(path);\n const projectDir = await findProjectRoot(startPath);\n\n if (!projectDir) {\n if (options.json) {\n console.log(JSON.stringify({ success: false, error: 'yamchart.yaml not found' }));\n } else {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n }\n process.exit(2);\n }\n\n // Load .env file\n loadEnvFile(projectDir);\n\n if (!options.json) {\n output.header('Validating yamchart project...');\n }\n\n const result = await validateProject(projectDir, {\n dryRun: options.dryRun ?? false,\n connection: options.connection,\n });\n\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n // Print results\n for (const error of result.errors) {\n output.error(error.file);\n output.detail(error.message);\n if (error.suggestion) {\n output.detail(error.suggestion);\n }\n }\n\n for (const warning of result.warnings) {\n output.warning(warning.file);\n output.detail(warning.message);\n }\n\n output.newline();\n\n if (result.success) {\n output.success(`Schema: ${result.stats.passed} passed`);\n } else {\n output.error(`Schema: ${result.stats.passed} passed, ${result.stats.failed} failed`);\n }\n\n if (result.dryRunStats) {\n output.newline();\n if (result.dryRunStats.failed === 0) {\n output.success(`Queries: ${result.dryRunStats.passed} passed (EXPLAIN OK)`);\n } else {\n output.error(`Queries: ${result.dryRunStats.passed} passed, ${result.dryRunStats.failed} failed`);\n }\n }\n\n output.newline();\n\n if (result.success) {\n output.success('Validation passed');\n } else {\n output.error(`Validation failed with ${result.errors.length} error(s)`);\n }\n }\n\n process.exit(result.success ? 0 : 1);\n });\n\nprogram\n .command('dev')\n .description('Start development server with hot reload')\n .argument('[path]', 'Path to yamchart project', '.')\n .option('-p, --port <number>', 'Port to listen on', '3001')\n .option('--api-only', 'Only serve API, no web UI')\n .option('--no-open', 'Do not open browser automatically')\n .action(async (path: string, options: { port: string; apiOnly?: boolean; open: boolean }) => {\n const startPath = resolve(path);\n const projectDir = await findProjectRoot(startPath);\n\n if (!projectDir) {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n process.exit(2);\n }\n\n const { runDevServer } = await import('./commands/dev.js');\n\n await runDevServer(projectDir, {\n port: parseInt(options.port, 10),\n apiOnly: options.apiOnly ?? false,\n open: options.open,\n });\n });\n\nprogram\n .command('init')\n .description('Create a new yamchart project')\n .argument('[directory]', 'Target directory', '.')\n .option('--example', 'Create full example project with sample database')\n .option('--empty', 'Create only yamchart.yaml (no connections, models, or charts)')\n .option('--force', 'Overwrite existing files')\n .action(async (directory: string, options: { example?: boolean; empty?: boolean; force?: boolean }) => {\n const { initProject } = await import('./commands/init.js');\n const targetDir = resolve(directory);\n\n const result = await initProject(targetDir, options);\n\n if (!result.success) {\n output.error(result.error || 'Failed to create project');\n process.exit(1);\n }\n\n output.newline();\n output.success(`Created ${directory === '.' ? basename(targetDir) : directory}/`);\n for (const file of result.files.slice(0, 10)) {\n output.detail(file);\n }\n if (result.files.length > 10) {\n output.detail(`... and ${result.files.length - 10} more files`);\n }\n output.newline();\n output.info(`Run \\`cd ${directory === '.' ? basename(targetDir) : directory} && yamchart dev\\` to start.`);\n });\n\nprogram.parse();\n"],"mappings":"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","names":["error","warning"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from 'commander';\nimport { resolve, basename } from 'path';\nimport { validateProject } from './commands/validate.js';\nimport { findProjectRoot, loadEnvFile } from './utils/config.js';\nimport * as output from './utils/output.js';\n\nconst program = new Command();\n\nprogram\n .name('yamchart')\n .description('Git-native business intelligence dashboards')\n .version('0.1.0');\n\nprogram\n .command('validate')\n .description('Validate configuration files')\n .argument('[path]', 'Path to yamchart project', '.')\n .option('--dry-run', 'Connect to database and test queries with EXPLAIN')\n .option('-c, --connection <name>', 'Connection to use for dry-run')\n .option('--json', 'Output as JSON')\n .action(async (path: string, options: { dryRun?: boolean; connection?: string; json?: boolean }) => {\n const startPath = resolve(path);\n const projectDir = await findProjectRoot(startPath);\n\n if (!projectDir) {\n if (options.json) {\n console.log(JSON.stringify({ success: false, error: 'yamchart.yaml not found' }));\n } else {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n }\n process.exit(2);\n }\n\n // Load .env file\n loadEnvFile(projectDir);\n\n if (!options.json) {\n output.header('Validating yamchart project...');\n }\n\n const result = await validateProject(projectDir, {\n dryRun: options.dryRun ?? false,\n connection: options.connection,\n });\n\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n // Print results\n for (const error of result.errors) {\n output.error(error.file);\n output.detail(error.message);\n if (error.suggestion) {\n output.detail(error.suggestion);\n }\n }\n\n for (const warning of result.warnings) {\n output.warning(warning.file);\n output.detail(warning.message);\n }\n\n output.newline();\n\n if (result.success) {\n output.success(`Schema: ${result.stats.passed} passed`);\n } else {\n output.error(`Schema: ${result.stats.passed} passed, ${result.stats.failed} failed`);\n }\n\n if (result.dryRunStats) {\n output.newline();\n if (result.dryRunStats.failed === 0) {\n output.success(`Queries: ${result.dryRunStats.passed} passed (EXPLAIN OK)`);\n } else {\n output.error(`Queries: ${result.dryRunStats.passed} passed, ${result.dryRunStats.failed} failed`);\n }\n }\n\n output.newline();\n\n if (result.success) {\n output.success('Validation passed');\n } else {\n output.error(`Validation failed with ${result.errors.length} error(s)`);\n }\n }\n\n process.exit(result.success ? 0 : 1);\n });\n\nprogram\n .command('dev')\n .description('Start development server with hot reload')\n .argument('[path]', 'Path to yamchart project', '.')\n .option('-p, --port <number>', 'Port to listen on', '3001')\n .option('--api-only', 'Only serve API, no web UI')\n .option('--no-open', 'Do not open browser automatically')\n .action(async (path: string, options: { port: string; apiOnly?: boolean; open: boolean }) => {\n const startPath = resolve(path);\n const projectDir = await findProjectRoot(startPath);\n\n if (!projectDir) {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n process.exit(2);\n }\n\n const { runDevServer } = await import('./commands/dev.js');\n\n await runDevServer(projectDir, {\n port: parseInt(options.port, 10),\n apiOnly: options.apiOnly ?? false,\n open: options.open,\n });\n });\n\nprogram\n .command('init')\n .description('Create a new yamchart project')\n .argument('[directory]', 'Target directory', '.')\n .option('--example', 'Create full example project with sample database')\n .option('--empty', 'Create only yamchart.yaml (no connections, models, or charts)')\n .option('--force', 'Overwrite existing files')\n .action(async (directory: string, options: { example?: boolean; empty?: boolean; force?: boolean }) => {\n const { initProject } = await import('./commands/init.js');\n const targetDir = resolve(directory);\n\n const result = await initProject(targetDir, options);\n\n if (!result.success) {\n output.error(result.error || 'Failed to create project');\n process.exit(1);\n }\n\n output.newline();\n output.success(`Created ${directory === '.' ? basename(targetDir) : directory}/`);\n for (const file of result.files.slice(0, 10)) {\n output.detail(file);\n }\n if (result.files.length > 10) {\n output.detail(`... and ${result.files.length - 10} more files`);\n }\n output.newline();\n output.info(`Run \\`cd ${directory === '.' ? basename(targetDir) : directory} && yamchart dev\\` to start.`);\n });\n\nprogram\n .command('sync-dbt')\n .description('Sync dbt project metadata into AI-readable catalog')\n .option('-s, --source <type>', 'Source type: local, github, dbt-cloud', 'local')\n .option('-p, --path <dir>', 'Path to dbt project (for local source)')\n .option('--repo <repo>', 'GitHub repository (for github source)')\n .option('--branch <branch>', 'Git branch (for github source)', 'main')\n .option('-i, --include <patterns...>', 'Include glob patterns')\n .option('-e, --exclude <patterns...>', 'Exclude glob patterns')\n .option('-t, --tag <tags...>', 'Filter by dbt tags')\n .option('--refresh', 'Re-sync using saved configuration')\n .action(async (options: {\n source: 'local' | 'github' | 'dbt-cloud';\n path?: string;\n repo?: string;\n branch?: string;\n include?: string[];\n exclude?: string[];\n tag?: string[];\n refresh?: boolean;\n }) => {\n const { syncDbt, loadSyncConfig } = await import('./commands/sync-dbt.js');\n\n // Find project root\n const projectDir = await findProjectRoot(process.cwd());\n\n if (!projectDir) {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n process.exit(2);\n }\n\n // Handle refresh mode\n if (options.refresh) {\n const savedConfig = await loadSyncConfig(projectDir);\n if (!savedConfig) {\n output.error('No saved sync config found');\n output.detail('Run sync-dbt without --refresh first');\n process.exit(1);\n }\n output.info(`Re-syncing from ${savedConfig.source}:${savedConfig.path || savedConfig.repo}`);\n }\n\n const spin = output.spinner('Syncing dbt metadata...');\n\n const result = await syncDbt(projectDir, {\n source: options.source,\n path: options.path,\n repo: options.repo,\n branch: options.branch,\n include: options.include || [],\n exclude: options.exclude || [],\n tags: options.tag || [],\n refresh: options.refresh,\n });\n\n spin.stop();\n\n if (!result.success) {\n output.error(result.error || 'Sync failed');\n process.exit(1);\n }\n\n output.success(`Synced ${result.modelsIncluded} models to .yamchart/catalog.md`);\n if (result.modelsExcluded > 0) {\n output.detail(`${result.modelsExcluded} models filtered out`);\n }\n });\n\nprogram\n .command('generate')\n .description('Generate SQL model stubs from dbt catalog')\n .argument('[model]', 'Specific model to generate (optional)')\n .option('--yolo', 'Skip all prompts, use defaults for everything')\n .action(async (model: string | undefined, options: { yolo?: boolean }) => {\n const { generate } = await import('./commands/generate.js');\n\n const projectDir = await findProjectRoot(process.cwd());\n\n if (!projectDir) {\n output.error('yamchart.yaml not found');\n output.detail('Run this command from a yamchart project directory');\n process.exit(2);\n }\n\n const result = await generate(projectDir, {\n model,\n yolo: options.yolo,\n });\n\n if (!result.success) {\n output.error(result.error || 'Generate failed');\n process.exit(1);\n }\n\n output.success(`Generated ${result.filesCreated} model stubs`);\n if (result.filesSkipped > 0) {\n output.detail(`${result.filesSkipped} files skipped`);\n }\n });\n\nprogram.parse();\n"],"mappings":"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","names":["error","warning"]}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
// src/commands/sync-dbt.ts
|
|
2
|
+
import { mkdir, writeFile, readFile as readFile3, access as access2 } from "fs/promises";
|
|
3
|
+
import { join as join4 } from "path";
|
|
4
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
5
|
+
|
|
6
|
+
// src/dbt/local-source.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { join as join2 } from "path";
|
|
9
|
+
import fg from "fast-glob";
|
|
10
|
+
import { minimatch } from "minimatch";
|
|
11
|
+
|
|
12
|
+
// src/dbt/parser.ts
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import { dirname, join } from "path";
|
|
15
|
+
function extractHintsFromTests(tests) {
|
|
16
|
+
const hints = [];
|
|
17
|
+
for (const test of tests) {
|
|
18
|
+
if (typeof test === "string") {
|
|
19
|
+
if (test === "unique") {
|
|
20
|
+
hints.push("unique");
|
|
21
|
+
} else if (test === "not_null") {
|
|
22
|
+
hints.push("required");
|
|
23
|
+
} else if (test === "primary_key") {
|
|
24
|
+
hints.push("primary_key");
|
|
25
|
+
}
|
|
26
|
+
} else if (typeof test === "object" && test !== null) {
|
|
27
|
+
if ("relationships" in test) {
|
|
28
|
+
const rel = test.relationships;
|
|
29
|
+
if (rel.to) {
|
|
30
|
+
const match = rel.to.match(/ref\(['"]([^'"]+)['"]\)/);
|
|
31
|
+
if (match) {
|
|
32
|
+
hints.push(`fk:${match[1]}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if ("dbt_constraints" in test) {
|
|
37
|
+
const constraint = test.dbt_constraints;
|
|
38
|
+
if (constraint.type === "primary_key") {
|
|
39
|
+
hints.push("primary_key");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return hints;
|
|
45
|
+
}
|
|
46
|
+
function parseSchemaYml(content, schemaPath) {
|
|
47
|
+
const parsed = parseYaml(content);
|
|
48
|
+
if (!parsed?.models || !Array.isArray(parsed.models)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const schemaDir = dirname(schemaPath);
|
|
52
|
+
return parsed.models.map((model) => {
|
|
53
|
+
const columns = (model.columns || []).map((col) => ({
|
|
54
|
+
name: col.name,
|
|
55
|
+
description: col.description || "",
|
|
56
|
+
data_type: col.data_type,
|
|
57
|
+
hints: col.tests ? extractHintsFromTests(col.tests) : []
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
name: model.name,
|
|
61
|
+
path: join(schemaDir, `${model.name}.sql`),
|
|
62
|
+
description: model.description || "No description",
|
|
63
|
+
tags: model.tags || [],
|
|
64
|
+
meta: model.meta || {},
|
|
65
|
+
columns
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function parseProjectYml(content) {
|
|
70
|
+
const parsed = parseYaml(content);
|
|
71
|
+
return {
|
|
72
|
+
name: parsed.name || "unknown",
|
|
73
|
+
version: parsed.version,
|
|
74
|
+
profile: parsed.profile,
|
|
75
|
+
modelPaths: parsed["model-paths"] || parsed.model_paths || ["models"],
|
|
76
|
+
vars: parsed.vars || {}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/dbt/local-source.ts
|
|
81
|
+
var LocalDbtSource = class {
|
|
82
|
+
type = "local";
|
|
83
|
+
projectPath;
|
|
84
|
+
modelsCache = null;
|
|
85
|
+
configCache = null;
|
|
86
|
+
constructor(projectPath) {
|
|
87
|
+
this.projectPath = projectPath;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read and parse dbt_project.yml
|
|
91
|
+
*/
|
|
92
|
+
async getProjectConfig() {
|
|
93
|
+
if (this.configCache) {
|
|
94
|
+
return this.configCache;
|
|
95
|
+
}
|
|
96
|
+
const configPath = join2(this.projectPath, "dbt_project.yml");
|
|
97
|
+
const content = await readFile(configPath, "utf-8");
|
|
98
|
+
const parsed = parseProjectYml(content);
|
|
99
|
+
this.configCache = {
|
|
100
|
+
name: parsed.name,
|
|
101
|
+
version: parsed.version,
|
|
102
|
+
profile: parsed.profile,
|
|
103
|
+
model_paths: parsed.modelPaths,
|
|
104
|
+
target_path: "target",
|
|
105
|
+
vars: parsed.vars
|
|
106
|
+
};
|
|
107
|
+
return this.configCache;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Find all schema.yml files and parse models from them.
|
|
111
|
+
* Returns summaries for model selection UI.
|
|
112
|
+
*/
|
|
113
|
+
async listModels() {
|
|
114
|
+
await this.loadModels();
|
|
115
|
+
const summaries = [];
|
|
116
|
+
for (const model of this.modelsCache.values()) {
|
|
117
|
+
summaries.push({
|
|
118
|
+
name: model.name,
|
|
119
|
+
path: model.path,
|
|
120
|
+
description: model.description || "No description",
|
|
121
|
+
tags: model.tags || []
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return summaries;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get full model details by name.
|
|
128
|
+
* @throws Error if model not found
|
|
129
|
+
*/
|
|
130
|
+
async getModel(name) {
|
|
131
|
+
await this.loadModels();
|
|
132
|
+
const model = this.modelsCache.get(name);
|
|
133
|
+
if (!model) {
|
|
134
|
+
throw new Error(`Model not found: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
return model;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get multiple models by name.
|
|
140
|
+
* @throws Error if any model not found
|
|
141
|
+
*/
|
|
142
|
+
async getModels(names) {
|
|
143
|
+
if (names.length === 0) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const models = [];
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
models.push(await this.getModel(name));
|
|
149
|
+
}
|
|
150
|
+
return models;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Load all models from schema.yml files into cache.
|
|
154
|
+
*/
|
|
155
|
+
async loadModels() {
|
|
156
|
+
if (this.modelsCache) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const config = await this.getProjectConfig();
|
|
160
|
+
this.modelsCache = /* @__PURE__ */ new Map();
|
|
161
|
+
for (const modelPath of config.model_paths) {
|
|
162
|
+
const pattern = join2(this.projectPath, modelPath, "**/*.yml");
|
|
163
|
+
const files = await fg(pattern, {
|
|
164
|
+
ignore: ["**/node_modules/**"]
|
|
165
|
+
});
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
const content = await readFile(file, "utf-8");
|
|
168
|
+
const relativePath = file.slice(this.projectPath.length + 1);
|
|
169
|
+
const models = parseSchemaYml(content, relativePath);
|
|
170
|
+
for (const model of models) {
|
|
171
|
+
this.modelsCache.set(model.name, model);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Filter models by include/exclude glob patterns and tags.
|
|
178
|
+
* Patterns match against model paths.
|
|
179
|
+
*/
|
|
180
|
+
static filterModels(models, filters) {
|
|
181
|
+
let filtered = [...models];
|
|
182
|
+
if (filters.include && filters.include.length > 0) {
|
|
183
|
+
filtered = filtered.filter(
|
|
184
|
+
(model) => filters.include.some((pattern) => minimatch(model.path, pattern))
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (filters.exclude && filters.exclude.length > 0) {
|
|
188
|
+
filtered = filtered.filter(
|
|
189
|
+
(model) => !filters.exclude.some((pattern) => minimatch(model.path, pattern))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
193
|
+
filtered = filtered.filter(
|
|
194
|
+
(model) => filters.tags.some((tag) => model.tags.includes(tag))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return filtered;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get default filters that focus on marts/reporting models.
|
|
201
|
+
* These are typically the models most useful for BI dashboards.
|
|
202
|
+
*/
|
|
203
|
+
static getDefaultFilters() {
|
|
204
|
+
return {
|
|
205
|
+
include: ["**/marts/**", "**/reporting/**"],
|
|
206
|
+
exclude: ["**/staging/**", "**/intermediate/**"],
|
|
207
|
+
tags: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/dbt/scanner.ts
|
|
213
|
+
import { readFile as readFile2, access, readdir } from "fs/promises";
|
|
214
|
+
import { join as join3, extname, relative } from "path";
|
|
215
|
+
async function scanYamchartModels(projectDir) {
|
|
216
|
+
const modelsDir = join3(projectDir, "models");
|
|
217
|
+
const models = [];
|
|
218
|
+
try {
|
|
219
|
+
await access(modelsDir);
|
|
220
|
+
} catch {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
await scanModelsRecursive(modelsDir, projectDir, models);
|
|
224
|
+
return models;
|
|
225
|
+
}
|
|
226
|
+
async function scanModelsRecursive(dir, projectDir, models) {
|
|
227
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const fullPath = join3(dir, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
await scanModelsRecursive(fullPath, projectDir, models);
|
|
232
|
+
} else if (extname(entry.name) === ".sql") {
|
|
233
|
+
const content = await readFile2(fullPath, "utf-8");
|
|
234
|
+
const metadata = parseModelMetadata(content);
|
|
235
|
+
if (metadata.name) {
|
|
236
|
+
models.push({
|
|
237
|
+
name: metadata.name,
|
|
238
|
+
description: metadata.description || "",
|
|
239
|
+
path: relative(projectDir, fullPath),
|
|
240
|
+
source: metadata.source || extractSourceFromSql(content)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function parseModelMetadata(content) {
|
|
247
|
+
const metadata = {};
|
|
248
|
+
const nameMatch = content.match(/--\s*@name:\s*(.+)/);
|
|
249
|
+
if (nameMatch) {
|
|
250
|
+
metadata.name = nameMatch[1].trim();
|
|
251
|
+
}
|
|
252
|
+
const descMatch = content.match(/--\s*@description:\s*(.+)/);
|
|
253
|
+
if (descMatch) {
|
|
254
|
+
metadata.description = descMatch[1].trim();
|
|
255
|
+
}
|
|
256
|
+
const sourceMatch = content.match(/--\s*@source:\s*(.+)/);
|
|
257
|
+
if (sourceMatch) {
|
|
258
|
+
metadata.source = sourceMatch[1].trim();
|
|
259
|
+
}
|
|
260
|
+
return metadata;
|
|
261
|
+
}
|
|
262
|
+
function extractSourceFromSql(sql) {
|
|
263
|
+
const noComments = sql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
264
|
+
const fromMatch = noComments.match(/\bFROM\s+(\{\{\s*ref\(['"]([^'"]+)['"]\)\s*\}\}|[\w.]+)/i);
|
|
265
|
+
if (fromMatch) {
|
|
266
|
+
if (fromMatch[2]) {
|
|
267
|
+
return fromMatch[2];
|
|
268
|
+
}
|
|
269
|
+
return fromMatch[1];
|
|
270
|
+
}
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/dbt/catalog.ts
|
|
275
|
+
function generateCatalogMd(data) {
|
|
276
|
+
const lines = [];
|
|
277
|
+
lines.push("# Data Catalog");
|
|
278
|
+
lines.push("");
|
|
279
|
+
lines.push(`> Source: ${data.source.type}:${data.source.path || data.source.repo || "unknown"}`);
|
|
280
|
+
lines.push(`> Last synced: ${data.syncedAt.split("T")[0]}`);
|
|
281
|
+
lines.push(`> Models: ${data.stats.modelsIncluded} included, ${data.stats.modelsExcluded} filtered out`);
|
|
282
|
+
lines.push("");
|
|
283
|
+
lines.push("---");
|
|
284
|
+
lines.push("");
|
|
285
|
+
lines.push("## Models");
|
|
286
|
+
lines.push("");
|
|
287
|
+
for (const model of data.models) {
|
|
288
|
+
lines.push(`### ${model.name}`);
|
|
289
|
+
lines.push("");
|
|
290
|
+
lines.push(model.description);
|
|
291
|
+
lines.push("");
|
|
292
|
+
if (model.table) {
|
|
293
|
+
lines.push(`**Table:** \`${model.table}\``);
|
|
294
|
+
}
|
|
295
|
+
if (model.tags.length > 0) {
|
|
296
|
+
lines.push(`**Tags:** ${model.tags.map((t) => `\`${t}\``).join(", ")}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push("");
|
|
299
|
+
if (model.columns.length > 0) {
|
|
300
|
+
lines.push("| Column | Type | Description | Hints |");
|
|
301
|
+
lines.push("|--------|------|-------------|-------|");
|
|
302
|
+
for (const col of model.columns) {
|
|
303
|
+
const type = col.data_type || "";
|
|
304
|
+
const hints = col.hints.join(", ");
|
|
305
|
+
lines.push(`| ${col.name} | ${type} | ${col.description} | ${hints} |`);
|
|
306
|
+
}
|
|
307
|
+
lines.push("");
|
|
308
|
+
}
|
|
309
|
+
lines.push("**Yamchart models:**");
|
|
310
|
+
if (model.yamchartModels.length > 0) {
|
|
311
|
+
for (const ym of model.yamchartModels) {
|
|
312
|
+
lines.push(`- [\`${ym.name}\`](../${ym.path}) - ${ym.description || "No description"}`);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
lines.push("None yet");
|
|
316
|
+
}
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push("---");
|
|
319
|
+
lines.push("");
|
|
320
|
+
}
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
function generateCatalogJson(data) {
|
|
324
|
+
return JSON.stringify(data, null, 2);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/commands/sync-dbt.ts
|
|
328
|
+
async function loadSyncConfig(projectDir) {
|
|
329
|
+
const configPath = join4(projectDir, ".yamchart", "dbt-source.yaml");
|
|
330
|
+
try {
|
|
331
|
+
await access2(configPath);
|
|
332
|
+
const content = await readFile3(configPath, "utf-8");
|
|
333
|
+
return parseYaml2(content);
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function syncDbt(projectDir, options) {
|
|
339
|
+
const yamchartDir = join4(projectDir, ".yamchart");
|
|
340
|
+
const catalogPath = join4(yamchartDir, "catalog.md");
|
|
341
|
+
try {
|
|
342
|
+
await mkdir(yamchartDir, { recursive: true });
|
|
343
|
+
let effectiveOptions = { ...options };
|
|
344
|
+
if (options.refresh) {
|
|
345
|
+
const savedConfig = await loadSyncConfig(projectDir);
|
|
346
|
+
if (!savedConfig) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
modelsIncluded: 0,
|
|
350
|
+
modelsExcluded: 0,
|
|
351
|
+
catalogPath,
|
|
352
|
+
error: "No saved sync config found. Run sync-dbt with --path first."
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
effectiveOptions = {
|
|
356
|
+
source: savedConfig.source,
|
|
357
|
+
path: savedConfig.path,
|
|
358
|
+
repo: savedConfig.repo,
|
|
359
|
+
branch: savedConfig.branch,
|
|
360
|
+
include: savedConfig.filters.include,
|
|
361
|
+
exclude: savedConfig.filters.exclude,
|
|
362
|
+
tags: savedConfig.filters.tags
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (effectiveOptions.source !== "local") {
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
modelsIncluded: 0,
|
|
369
|
+
modelsExcluded: 0,
|
|
370
|
+
catalogPath,
|
|
371
|
+
error: `Source type "${effectiveOptions.source}" not yet supported. Only "local" is available.`
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (!effectiveOptions.path) {
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
modelsIncluded: 0,
|
|
378
|
+
modelsExcluded: 0,
|
|
379
|
+
catalogPath,
|
|
380
|
+
error: "Path to dbt project is required for local source."
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const dbtSource = new LocalDbtSource(effectiveOptions.path);
|
|
384
|
+
const allModels = await dbtSource.listModels();
|
|
385
|
+
let hasFilters = effectiveOptions.include.length > 0 || effectiveOptions.exclude.length > 0 || effectiveOptions.tags.length > 0;
|
|
386
|
+
let filteredModels;
|
|
387
|
+
if (hasFilters) {
|
|
388
|
+
filteredModels = LocalDbtSource.filterModels(allModels, {
|
|
389
|
+
include: effectiveOptions.include,
|
|
390
|
+
exclude: effectiveOptions.exclude,
|
|
391
|
+
tags: effectiveOptions.tags
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
const defaults = LocalDbtSource.getDefaultFilters();
|
|
395
|
+
const withDefaults = LocalDbtSource.filterModels(allModels, defaults);
|
|
396
|
+
filteredModels = withDefaults.length > 0 ? withDefaults : allModels;
|
|
397
|
+
}
|
|
398
|
+
const modelsExcluded = allModels.length - filteredModels.length;
|
|
399
|
+
const modelNames = filteredModels.map((m) => m.name);
|
|
400
|
+
const fullModels = await dbtSource.getModels(modelNames);
|
|
401
|
+
const yamchartModels = await scanYamchartModels(projectDir);
|
|
402
|
+
const catalogModels = fullModels.map((model) => {
|
|
403
|
+
const referencingModels = yamchartModels.filter(
|
|
404
|
+
(ym) => ym.source === model.name || ym.source === model.table
|
|
405
|
+
);
|
|
406
|
+
return {
|
|
407
|
+
...model,
|
|
408
|
+
yamchartModels: referencingModels
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
const catalogData = {
|
|
412
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
413
|
+
source: {
|
|
414
|
+
type: effectiveOptions.source,
|
|
415
|
+
path: effectiveOptions.path,
|
|
416
|
+
repo: effectiveOptions.repo
|
|
417
|
+
},
|
|
418
|
+
stats: {
|
|
419
|
+
modelsIncluded: filteredModels.length,
|
|
420
|
+
modelsExcluded
|
|
421
|
+
},
|
|
422
|
+
models: catalogModels
|
|
423
|
+
};
|
|
424
|
+
const catalogMd = generateCatalogMd(catalogData);
|
|
425
|
+
const catalogJson = generateCatalogJson(catalogData);
|
|
426
|
+
await writeFile(catalogPath, catalogMd, "utf-8");
|
|
427
|
+
await writeFile(join4(yamchartDir, "catalog.json"), catalogJson, "utf-8");
|
|
428
|
+
const syncConfig = {
|
|
429
|
+
source: effectiveOptions.source,
|
|
430
|
+
path: effectiveOptions.path,
|
|
431
|
+
repo: effectiveOptions.repo,
|
|
432
|
+
branch: effectiveOptions.branch,
|
|
433
|
+
lastSync: catalogData.syncedAt,
|
|
434
|
+
filters: {
|
|
435
|
+
include: effectiveOptions.include,
|
|
436
|
+
exclude: effectiveOptions.exclude,
|
|
437
|
+
tags: effectiveOptions.tags
|
|
438
|
+
},
|
|
439
|
+
stats: {
|
|
440
|
+
modelsIncluded: filteredModels.length,
|
|
441
|
+
modelsExcluded
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const configYaml = stringifyYaml(syncConfig);
|
|
445
|
+
await writeFile(join4(yamchartDir, "dbt-source.yaml"), configYaml, "utf-8");
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
modelsIncluded: filteredModels.length,
|
|
449
|
+
modelsExcluded,
|
|
450
|
+
catalogPath
|
|
451
|
+
};
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
modelsIncluded: 0,
|
|
456
|
+
modelsExcluded: 0,
|
|
457
|
+
catalogPath,
|
|
458
|
+
error: err instanceof Error ? err.message : "Unknown error during sync"
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
export {
|
|
463
|
+
loadSyncConfig,
|
|
464
|
+
syncDbt
|
|
465
|
+
};
|
|
466
|
+
//# sourceMappingURL=sync-dbt-IDDD4X2Z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/sync-dbt.ts","../src/dbt/local-source.ts","../src/dbt/parser.ts","../src/dbt/scanner.ts","../src/dbt/catalog.ts"],"sourcesContent":["import { mkdir, writeFile, readFile, access } from 'fs/promises';\nimport { join } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { LocalDbtSource } from '../dbt/local-source.js';\nimport { scanYamchartModels } from '../dbt/scanner.js';\nimport { generateCatalogMd, generateCatalogJson, type CatalogData, type CatalogModel } from '../dbt/catalog.js';\nimport type { DbtSourceConfig } from '../dbt/types.js';\n\nexport interface SyncDbtOptions {\n source: 'local' | 'github' | 'dbt-cloud';\n path?: string;\n repo?: string;\n branch?: string;\n include: string[];\n exclude: string[];\n tags: string[];\n refresh?: boolean;\n}\n\nexport interface SyncDbtResult {\n success: boolean;\n modelsIncluded: number;\n modelsExcluded: number;\n catalogPath: string;\n error?: string;\n}\n\n/**\n * Load saved sync config from .yamchart/dbt-source.yaml\n */\nexport async function loadSyncConfig(projectDir: string): Promise<DbtSourceConfig | null> {\n const configPath = join(projectDir, '.yamchart', 'dbt-source.yaml');\n\n try {\n await access(configPath);\n const content = await readFile(configPath, 'utf-8');\n return parseYaml(content) as DbtSourceConfig;\n } catch {\n return null;\n }\n}\n\n/**\n * Sync dbt project metadata to yamchart catalog.\n *\n * This function:\n * 1. Ensures .yamchart directory exists\n * 2. Handles refresh mode (load saved config)\n * 3. Validates source type (only 'local' supported for v1)\n * 4. Creates LocalDbtSource, lists models, applies filters\n * 5. Uses smart defaults if no filters specified\n * 6. Scans yamchart models for cross-references\n * 7. Generates catalog.md and catalog.json\n * 8. Saves sync config to dbt-source.yaml\n */\nexport async function syncDbt(\n projectDir: string,\n options: SyncDbtOptions\n): Promise<SyncDbtResult> {\n const yamchartDir = join(projectDir, '.yamchart');\n const catalogPath = join(yamchartDir, 'catalog.md');\n\n try {\n // Step 1: Ensure .yamchart directory exists\n await mkdir(yamchartDir, { recursive: true });\n\n // Step 2: Handle refresh mode - load saved config\n let effectiveOptions = { ...options };\n if (options.refresh) {\n const savedConfig = await loadSyncConfig(projectDir);\n if (!savedConfig) {\n return {\n success: false,\n modelsIncluded: 0,\n modelsExcluded: 0,\n catalogPath,\n error: 'No saved sync config found. Run sync-dbt with --path first.',\n };\n }\n\n effectiveOptions = {\n source: savedConfig.source,\n path: savedConfig.path,\n repo: savedConfig.repo,\n branch: savedConfig.branch,\n include: savedConfig.filters.include,\n exclude: savedConfig.filters.exclude,\n tags: savedConfig.filters.tags,\n };\n }\n\n // Step 3: Validate source type (only 'local' supported for v1)\n if (effectiveOptions.source !== 'local') {\n return {\n success: false,\n modelsIncluded: 0,\n modelsExcluded: 0,\n catalogPath,\n error: `Source type \"${effectiveOptions.source}\" not yet supported. Only \"local\" is available.`,\n };\n }\n\n if (!effectiveOptions.path) {\n return {\n success: false,\n modelsIncluded: 0,\n modelsExcluded: 0,\n catalogPath,\n error: 'Path to dbt project is required for local source.',\n };\n }\n\n // Step 4: Create LocalDbtSource, list models\n const dbtSource = new LocalDbtSource(effectiveOptions.path);\n const allModels = await dbtSource.listModels();\n\n // Step 5: Apply filters or use smart defaults\n let hasFilters =\n effectiveOptions.include.length > 0 ||\n effectiveOptions.exclude.length > 0 ||\n effectiveOptions.tags.length > 0;\n\n let filteredModels;\n if (hasFilters) {\n filteredModels = LocalDbtSource.filterModels(allModels, {\n include: effectiveOptions.include,\n exclude: effectiveOptions.exclude,\n tags: effectiveOptions.tags,\n });\n } else {\n // Use smart defaults - prefer marts/reporting, exclude staging/intermediate\n const defaults = LocalDbtSource.getDefaultFilters();\n const withDefaults = LocalDbtSource.filterModels(allModels, defaults);\n\n // If defaults filter out everything, include all models\n filteredModels = withDefaults.length > 0 ? withDefaults : allModels;\n }\n\n const modelsExcluded = allModels.length - filteredModels.length;\n\n // Get full model details for included models\n const modelNames = filteredModels.map(m => m.name);\n const fullModels = await dbtSource.getModels(modelNames);\n\n // Step 6: Scan yamchart models for cross-references\n const yamchartModels = await scanYamchartModels(projectDir);\n\n // Build catalog models with cross-references\n const catalogModels: CatalogModel[] = fullModels.map(model => {\n // Find yamchart models that reference this dbt model\n const referencingModels = yamchartModels.filter(ym =>\n ym.source === model.name || ym.source === model.table\n );\n\n return {\n ...model,\n yamchartModels: referencingModels,\n };\n });\n\n // Step 7: Generate catalog files\n const catalogData: CatalogData = {\n syncedAt: new Date().toISOString(),\n source: {\n type: effectiveOptions.source,\n path: effectiveOptions.path,\n repo: effectiveOptions.repo,\n },\n stats: {\n modelsIncluded: filteredModels.length,\n modelsExcluded,\n },\n models: catalogModels,\n };\n\n const catalogMd = generateCatalogMd(catalogData);\n const catalogJson = generateCatalogJson(catalogData);\n\n await writeFile(catalogPath, catalogMd, 'utf-8');\n await writeFile(join(yamchartDir, 'catalog.json'), catalogJson, 'utf-8');\n\n // Step 8: Save sync config for re-sync\n const syncConfig: DbtSourceConfig = {\n source: effectiveOptions.source,\n path: effectiveOptions.path,\n repo: effectiveOptions.repo,\n branch: effectiveOptions.branch,\n lastSync: catalogData.syncedAt,\n filters: {\n include: effectiveOptions.include,\n exclude: effectiveOptions.exclude,\n tags: effectiveOptions.tags,\n },\n stats: {\n modelsIncluded: filteredModels.length,\n modelsExcluded,\n },\n };\n\n const configYaml = stringifyYaml(syncConfig);\n await writeFile(join(yamchartDir, 'dbt-source.yaml'), configYaml, 'utf-8');\n\n return {\n success: true,\n modelsIncluded: filteredModels.length,\n modelsExcluded,\n catalogPath,\n };\n } catch (err) {\n return {\n success: false,\n modelsIncluded: 0,\n modelsExcluded: 0,\n catalogPath,\n error: err instanceof Error ? err.message : 'Unknown error during sync',\n };\n }\n}\n","import { readFile } from 'fs/promises';\nimport { join } from 'path';\nimport fg from 'fast-glob';\nimport { minimatch } from 'minimatch';\nimport { parseSchemaYml, parseProjectYml } from './parser.js';\nimport type {\n DbtSource,\n DbtProjectConfig,\n DbtModel,\n DbtModelSummary,\n} from './types.js';\n\nexport interface ModelFilters {\n include?: string[];\n exclude?: string[];\n tags?: string[];\n}\n\n/**\n * LocalDbtSource reads dbt project metadata from a local filesystem.\n * It parses schema.yml files to extract model definitions, columns, and hints.\n */\nexport class LocalDbtSource implements DbtSource {\n readonly type = 'local' as const;\n private projectPath: string;\n private modelsCache: Map<string, DbtModel> | null = null;\n private configCache: DbtProjectConfig | null = null;\n\n constructor(projectPath: string) {\n this.projectPath = projectPath;\n }\n\n /**\n * Read and parse dbt_project.yml\n */\n async getProjectConfig(): Promise<DbtProjectConfig> {\n if (this.configCache) {\n return this.configCache;\n }\n\n const configPath = join(this.projectPath, 'dbt_project.yml');\n const content = await readFile(configPath, 'utf-8');\n const parsed = parseProjectYml(content);\n\n this.configCache = {\n name: parsed.name,\n version: parsed.version,\n profile: parsed.profile,\n model_paths: parsed.modelPaths,\n target_path: 'target',\n vars: parsed.vars,\n };\n\n return this.configCache;\n }\n\n /**\n * Find all schema.yml files and parse models from them.\n * Returns summaries for model selection UI.\n */\n async listModels(): Promise<DbtModelSummary[]> {\n await this.loadModels();\n\n const summaries: DbtModelSummary[] = [];\n for (const model of this.modelsCache!.values()) {\n summaries.push({\n name: model.name,\n path: model.path,\n description: model.description || 'No description',\n tags: model.tags || [],\n });\n }\n\n return summaries;\n }\n\n /**\n * Get full model details by name.\n * @throws Error if model not found\n */\n async getModel(name: string): Promise<DbtModel> {\n await this.loadModels();\n\n const model = this.modelsCache!.get(name);\n if (!model) {\n throw new Error(`Model not found: ${name}`);\n }\n\n return model;\n }\n\n /**\n * Get multiple models by name.\n * @throws Error if any model not found\n */\n async getModels(names: string[]): Promise<DbtModel[]> {\n if (names.length === 0) {\n return [];\n }\n\n const models: DbtModel[] = [];\n for (const name of names) {\n models.push(await this.getModel(name));\n }\n\n return models;\n }\n\n /**\n * Load all models from schema.yml files into cache.\n */\n private async loadModels(): Promise<void> {\n if (this.modelsCache) {\n return;\n }\n\n const config = await this.getProjectConfig();\n this.modelsCache = new Map();\n\n // Find all schema.yml files in model paths\n for (const modelPath of config.model_paths) {\n const pattern = join(this.projectPath, modelPath, '**/*.yml');\n const files = await fg(pattern, {\n ignore: ['**/node_modules/**'],\n });\n\n for (const file of files) {\n const content = await readFile(file, 'utf-8');\n // Get relative path from project root\n const relativePath = file.slice(this.projectPath.length + 1);\n const models = parseSchemaYml(content, relativePath);\n\n for (const model of models) {\n this.modelsCache.set(model.name, model);\n }\n }\n }\n }\n\n /**\n * Filter models by include/exclude glob patterns and tags.\n * Patterns match against model paths.\n */\n static filterModels(\n models: DbtModelSummary[],\n filters: ModelFilters\n ): DbtModelSummary[] {\n let filtered = [...models];\n\n // Apply include patterns (match any)\n if (filters.include && filters.include.length > 0) {\n filtered = filtered.filter((model) =>\n filters.include!.some((pattern) => minimatch(model.path, pattern))\n );\n }\n\n // Apply exclude patterns (exclude any matches)\n if (filters.exclude && filters.exclude.length > 0) {\n filtered = filtered.filter(\n (model) =>\n !filters.exclude!.some((pattern) => minimatch(model.path, pattern))\n );\n }\n\n // Apply tag filters (match any)\n if (filters.tags && filters.tags.length > 0) {\n filtered = filtered.filter((model) =>\n filters.tags!.some((tag) => model.tags.includes(tag))\n );\n }\n\n return filtered;\n }\n\n /**\n * Get default filters that focus on marts/reporting models.\n * These are typically the models most useful for BI dashboards.\n */\n static getDefaultFilters(): Required<ModelFilters> {\n return {\n include: ['**/marts/**', '**/reporting/**'],\n exclude: ['**/staging/**', '**/intermediate/**'],\n tags: [],\n };\n }\n}\n","import { parse as parseYaml } from 'yaml';\nimport { dirname, join } from 'path';\nimport type { DbtModel, DbtColumn } from './types.js';\n\ninterface RawSchemaYml {\n version: number;\n models?: RawModel[];\n}\n\ninterface RawModel {\n name: string;\n description?: string;\n meta?: Record<string, unknown>;\n tags?: string[];\n columns?: RawColumn[];\n}\n\ninterface RawColumn {\n name: string;\n description?: string;\n data_type?: string;\n tests?: (string | Record<string, unknown>)[];\n}\n\n/**\n * Extract hints from dbt column tests.\n * - unique → \"unique\"\n * - not_null → \"required\"\n * - relationships: { to: ref('X') } → \"fk:X\"\n */\nexport function extractHintsFromTests(tests: (string | Record<string, unknown>)[]): string[] {\n const hints: string[] = [];\n\n for (const test of tests) {\n if (typeof test === 'string') {\n if (test === 'unique') {\n hints.push('unique');\n } else if (test === 'not_null') {\n hints.push('required');\n } else if (test === 'primary_key') {\n hints.push('primary_key');\n }\n } else if (typeof test === 'object' && test !== null) {\n // Handle relationships test\n if ('relationships' in test) {\n const rel = test.relationships as { to?: string; field?: string };\n if (rel.to) {\n // Extract table name from ref('table_name')\n const match = rel.to.match(/ref\\(['\"]([^'\"]+)['\"]\\)/);\n if (match) {\n hints.push(`fk:${match[1]}`);\n }\n }\n }\n // Handle dbt_constraints for primary key\n if ('dbt_constraints' in test) {\n const constraint = test.dbt_constraints as { type?: string };\n if (constraint.type === 'primary_key') {\n hints.push('primary_key');\n }\n }\n }\n }\n\n return hints;\n}\n\n/**\n * Parse a dbt schema.yml file and extract model definitions.\n * @param content - Raw YAML content\n * @param schemaPath - Path to the schema file (e.g., \"models/marts/_schema.yml\")\n * @returns Array of parsed models\n */\nexport function parseSchemaYml(content: string, schemaPath: string): DbtModel[] {\n const parsed = parseYaml(content) as RawSchemaYml;\n\n if (!parsed?.models || !Array.isArray(parsed.models)) {\n return [];\n }\n\n const schemaDir = dirname(schemaPath);\n\n return parsed.models.map((model): DbtModel => {\n const columns: DbtColumn[] = (model.columns || []).map((col) => ({\n name: col.name,\n description: col.description || '',\n data_type: col.data_type,\n hints: col.tests ? extractHintsFromTests(col.tests) : [],\n }));\n\n return {\n name: model.name,\n path: join(schemaDir, `${model.name}.sql`),\n description: model.description || 'No description',\n tags: model.tags || [],\n meta: model.meta || {},\n columns,\n };\n });\n}\n\n/**\n * Parse dbt_project.yml to get project-level config.\n */\nexport function parseProjectYml(content: string): {\n name: string;\n version?: string;\n profile?: string;\n modelPaths: string[];\n vars: Record<string, unknown>;\n} {\n const parsed = parseYaml(content) as Record<string, unknown>;\n\n return {\n name: (parsed.name as string) || 'unknown',\n version: parsed.version as string | undefined,\n profile: parsed.profile as string | undefined,\n modelPaths: (parsed['model-paths'] as string[]) || (parsed.model_paths as string[]) || ['models'],\n vars: (parsed.vars as Record<string, unknown>) || {},\n };\n}\n","import { readFile, access, readdir } from 'fs/promises';\nimport { join, extname, relative } from 'path';\n\nexport interface YamchartModel {\n name: string;\n description: string;\n path: string; // relative to project\n source?: string; // dbt table this queries\n}\n\n/**\n * Scan yamchart models directory and extract metadata.\n * Used to cross-reference which yamchart models use which dbt tables.\n */\nexport async function scanYamchartModels(projectDir: string): Promise<YamchartModel[]> {\n const modelsDir = join(projectDir, 'models');\n const models: YamchartModel[] = [];\n\n try {\n await access(modelsDir);\n } catch {\n return [];\n }\n\n await scanModelsRecursive(modelsDir, projectDir, models);\n return models;\n}\n\nasync function scanModelsRecursive(\n dir: string,\n projectDir: string,\n models: YamchartModel[]\n): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n\n if (entry.isDirectory()) {\n await scanModelsRecursive(fullPath, projectDir, models);\n } else if (extname(entry.name) === '.sql') {\n const content = await readFile(fullPath, 'utf-8');\n const metadata = parseModelMetadata(content);\n\n if (metadata.name) {\n models.push({\n name: metadata.name,\n description: metadata.description || '',\n path: relative(projectDir, fullPath),\n source: metadata.source || extractSourceFromSql(content),\n });\n }\n }\n }\n}\n\ninterface ModelMetadata {\n name?: string;\n description?: string;\n source?: string;\n}\n\n/**\n * Parse yamchart model metadata from SQL comments.\n */\nfunction parseModelMetadata(content: string): ModelMetadata {\n const metadata: ModelMetadata = {};\n\n // Match @name: value\n const nameMatch = content.match(/--\\s*@name:\\s*(.+)/);\n if (nameMatch) {\n metadata.name = nameMatch[1].trim();\n }\n\n // Match @description: value\n const descMatch = content.match(/--\\s*@description:\\s*(.+)/);\n if (descMatch) {\n metadata.description = descMatch[1].trim();\n }\n\n // Match @source: value (explicit dbt table reference)\n const sourceMatch = content.match(/--\\s*@source:\\s*(.+)/);\n if (sourceMatch) {\n metadata.source = sourceMatch[1].trim();\n }\n\n return metadata;\n}\n\n/**\n * Extract the primary table name from SQL FROM clause.\n * This is a best-effort extraction for cross-referencing.\n */\nfunction extractSourceFromSql(sql: string): string | undefined {\n // Remove comments\n const noComments = sql.replace(/--.*$/gm, '').replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n // Match FROM table_name (handles schema.table and database.schema.table)\n const fromMatch = noComments.match(/\\bFROM\\s+(\\{\\{\\s*ref\\(['\"]([^'\"]+)['\"]\\)\\s*\\}\\}|[\\w.]+)/i);\n\n if (fromMatch) {\n // If it's a Jinja ref(), extract the table name\n if (fromMatch[2]) {\n return fromMatch[2];\n }\n // Otherwise return the raw table name\n return fromMatch[1];\n }\n\n return undefined;\n}\n","import type { DbtModel, DbtColumn } from './types.js';\nimport type { YamchartModel } from './scanner.js';\n\nexport interface CatalogModel extends DbtModel {\n yamchartModels: YamchartModel[];\n}\n\nexport interface CatalogData {\n syncedAt: string;\n source: { type: string; path?: string; repo?: string };\n stats: { modelsIncluded: number; modelsExcluded: number };\n models: CatalogModel[];\n}\n\n/**\n * Generate catalog.md content.\n */\nexport function generateCatalogMd(data: CatalogData): string {\n const lines: string[] = [];\n\n // Header\n lines.push('# Data Catalog');\n lines.push('');\n lines.push(`> Source: ${data.source.type}:${data.source.path || data.source.repo || 'unknown'}`);\n lines.push(`> Last synced: ${data.syncedAt.split('T')[0]}`);\n lines.push(`> Models: ${data.stats.modelsIncluded} included, ${data.stats.modelsExcluded} filtered out`);\n lines.push('');\n lines.push('---');\n lines.push('');\n lines.push('## Models');\n lines.push('');\n\n // Each model\n for (const model of data.models) {\n lines.push(`### ${model.name}`);\n lines.push('');\n lines.push(model.description);\n lines.push('');\n\n if (model.table) {\n lines.push(`**Table:** \\`${model.table}\\``);\n }\n\n if (model.tags.length > 0) {\n lines.push(`**Tags:** ${model.tags.map(t => `\\`${t}\\``).join(', ')}`);\n }\n\n lines.push('');\n\n // Column table\n if (model.columns.length > 0) {\n lines.push('| Column | Type | Description | Hints |');\n lines.push('|--------|------|-------------|-------|');\n\n for (const col of model.columns) {\n const type = col.data_type || '';\n const hints = col.hints.join(', ');\n lines.push(`| ${col.name} | ${type} | ${col.description} | ${hints} |`);\n }\n\n lines.push('');\n }\n\n // Yamchart models using this\n lines.push('**Yamchart models:**');\n if (model.yamchartModels.length > 0) {\n for (const ym of model.yamchartModels) {\n lines.push(`- [\\`${ym.name}\\`](../${ym.path}) - ${ym.description || 'No description'}`);\n }\n } else {\n lines.push('None yet');\n }\n\n lines.push('');\n lines.push('---');\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Generate catalog.json content.\n */\nexport function generateCatalogJson(data: CatalogData): string {\n return JSON.stringify(data, null, 2);\n}\n"],"mappings":"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","names":["readFile","access","join","parseYaml","join","join","readFile","join","join","access","readFile","parseYaml"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yamchart",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Git-native business intelligence dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,11 +33,15 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@inquirer/prompts": "^8.2.0",
|
|
36
37
|
"commander": "^12.1.0",
|
|
37
38
|
"dotenv": "^16.4.0",
|
|
39
|
+
"fast-glob": "^3.3.3",
|
|
40
|
+
"minimatch": "^10.1.2",
|
|
38
41
|
"ora": "^8.0.0",
|
|
39
42
|
"picocolors": "^1.1.0",
|
|
40
43
|
"yaml": "^2.7.0",
|
|
44
|
+
"zod": "^3.24.0",
|
|
41
45
|
"@yamchart/query": "0.1.2",
|
|
42
46
|
"@yamchart/schema": "0.1.2",
|
|
43
47
|
"@yamchart/server": "0.1.2"
|