yamchart 0.1.2 → 0.3.1
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/chunk-3CLMQNNR.js +64 -0
- package/dist/chunk-3CLMQNNR.js.map +1 -0
- package/dist/{chunk-TBILHUB3.js → chunk-6GDL3DH4.js} +49 -60
- package/dist/chunk-6GDL3DH4.js.map +1 -0
- package/dist/chunk-HJVVHYVN.js +59 -0
- package/dist/chunk-HJVVHYVN.js.map +1 -0
- package/dist/{dev-UHYN2RXH.js → dev-HMLMSTA7.js} +27 -6
- package/dist/dev-HMLMSTA7.js.map +1 -0
- package/dist/generate-RD3LCS73.js +315 -0
- package/dist/generate-RD3LCS73.js.map +1 -0
- package/dist/index.js +144 -7
- package/dist/index.js.map +1 -1
- package/dist/reset-password-MJ54ICGP.js +59 -0
- package/dist/reset-password-MJ54ICGP.js.map +1 -0
- package/dist/sync-dbt-IDDD4X2Z.js +466 -0
- package/dist/sync-dbt-IDDD4X2Z.js.map +1 -0
- package/dist/templates/default/CLAUDE.md +9 -0
- package/dist/templates/default/docs/yamchart-reference.md +315 -0
- package/dist/test-N4KIIKQN.js +221 -0
- package/dist/test-N4KIIKQN.js.map +1 -0
- package/dist/update-QHLCWS56.js +75 -0
- package/dist/update-QHLCWS56.js.map +1 -0
- package/package.json +8 -3
- package/dist/chunk-TBILHUB3.js.map +0 -1
- package/dist/dev-UHYN2RXH.js.map +0 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Yamchart Configuration Reference
|
|
2
|
+
|
|
3
|
+
Yamchart is a Git-native BI framework. Dashboards are defined as YAML configs and SQL models — if it's not in Git, it doesn't exist.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
{{name}}/
|
|
9
|
+
├── yamchart.yaml # Project config + default connection
|
|
10
|
+
├── connections/ # Database connection configs (.yaml)
|
|
11
|
+
├── models/ # SQL models with Jinja templating (.sql)
|
|
12
|
+
├── charts/ # Chart definitions (.yaml)
|
|
13
|
+
└── dashboards/ # Dashboard layouts (.yaml)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
yamchart dev # Start dev server with hot reload
|
|
20
|
+
yamchart validate # Validate all config files
|
|
21
|
+
yamchart validate --dry-run # Also test queries with EXPLAIN
|
|
22
|
+
yamchart test # Run model @returns and @tests assertions
|
|
23
|
+
yamchart test <model> # Test specific model
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## SQL Models (`models/*.sql`)
|
|
27
|
+
|
|
28
|
+
Models are SQL files with metadata comments and Jinja templating.
|
|
29
|
+
|
|
30
|
+
```sql
|
|
31
|
+
-- @name: model_name
|
|
32
|
+
-- @description: What this model computes
|
|
33
|
+
-- @returns:
|
|
34
|
+
-- - column1: type
|
|
35
|
+
-- - column2: type
|
|
36
|
+
|
|
37
|
+
SELECT
|
|
38
|
+
date_trunc('{{ granularity }}', order_date) AS period,
|
|
39
|
+
SUM(amount) AS revenue
|
|
40
|
+
FROM {{ ref('orders') }}
|
|
41
|
+
WHERE order_date BETWEEN '{{ start_date }}' AND '{{ end_date }}'
|
|
42
|
+
{% if category and category != 'All' %}
|
|
43
|
+
AND category = '{{ category }}'
|
|
44
|
+
{% endif %}
|
|
45
|
+
GROUP BY 1
|
|
46
|
+
ORDER BY 1
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Metadata tags:** `@name` (required), `@description`, `@param name: type = default`, `@returns` (column: type list), `@tests` (SQL assertions where zero rows = pass)
|
|
50
|
+
|
|
51
|
+
**Templating:** `{{ variable }}` for substitution, `{% if %}` / `{% endif %}` for conditionals, `{{ ref('table') }}` for table references, `{{ user.attribute }}` for row-level security.
|
|
52
|
+
|
|
53
|
+
**Date parameters:** When a chart has `type: date_range`, the model receives `start_date` and `end_date` automatically.
|
|
54
|
+
|
|
55
|
+
## Charts (`charts/*.yaml`)
|
|
56
|
+
|
|
57
|
+
### Common Structure
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
name: my-chart # Unique identifier (required)
|
|
61
|
+
title: My Chart # Display title (required)
|
|
62
|
+
description: Description # Optional
|
|
63
|
+
source:
|
|
64
|
+
model: model_name # SQL model to use
|
|
65
|
+
parameters: [] # Interactive filters (optional)
|
|
66
|
+
chart:
|
|
67
|
+
type: line # Chart type
|
|
68
|
+
# ... type-specific config
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Chart Types
|
|
72
|
+
|
|
73
|
+
**Line / Bar / Area** — time series and categories:
|
|
74
|
+
```yaml
|
|
75
|
+
chart:
|
|
76
|
+
type: line # or bar, area
|
|
77
|
+
x: { field: date, type: temporal }
|
|
78
|
+
y: { field: revenue, format: "$,.0f" }
|
|
79
|
+
series:
|
|
80
|
+
field: category # Multi-series by grouping field (optional)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Pie / Donut** — part-to-whole:
|
|
84
|
+
```yaml
|
|
85
|
+
chart:
|
|
86
|
+
type: pie # or donut
|
|
87
|
+
x: { field: segment, type: nominal }
|
|
88
|
+
y: { field: amount, format: "$,.0f" }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Scatter** — correlations with optional size/group/regression:
|
|
92
|
+
```yaml
|
|
93
|
+
chart:
|
|
94
|
+
type: scatter
|
|
95
|
+
x: { field: revenue, type: quantitative }
|
|
96
|
+
y: { field: orders, type: quantitative }
|
|
97
|
+
size: { field: avg_value, min: 8, max: 40 }
|
|
98
|
+
group: { field: region }
|
|
99
|
+
regression: { type: linear, show_equation: true, show_r_squared: true }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**KPI** — single metric with comparison:
|
|
103
|
+
```yaml
|
|
104
|
+
chart:
|
|
105
|
+
type: kpi
|
|
106
|
+
value: { field: value }
|
|
107
|
+
format: { type: currency, currency: USD, decimals: 0 }
|
|
108
|
+
comparison: { enabled: true, field: previous_value, type: percent_change }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Heatmap** — two-dimensional intensity:
|
|
112
|
+
```yaml
|
|
113
|
+
chart:
|
|
114
|
+
type: heatmap
|
|
115
|
+
x: { field: hour, type: ordinal }
|
|
116
|
+
y: { field: day, type: ordinal }
|
|
117
|
+
value: { field: count }
|
|
118
|
+
color_range: { min: '#EBF5FB', max: '#1A5276' }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Funnel** — conversion stages:
|
|
122
|
+
```yaml
|
|
123
|
+
chart:
|
|
124
|
+
type: funnel
|
|
125
|
+
stage: { field: stage_name }
|
|
126
|
+
value: { field: user_count }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Waterfall** — incremental deltas:
|
|
130
|
+
```yaml
|
|
131
|
+
chart:
|
|
132
|
+
type: waterfall
|
|
133
|
+
category: { field: label }
|
|
134
|
+
value: { field: amount }
|
|
135
|
+
total_field: is_total
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Gauge** — single metric dial:
|
|
139
|
+
```yaml
|
|
140
|
+
chart:
|
|
141
|
+
type: gauge
|
|
142
|
+
value: { field: score }
|
|
143
|
+
min: 0
|
|
144
|
+
max: 100
|
|
145
|
+
thresholds:
|
|
146
|
+
- { value: 60, color: '#EF4444' }
|
|
147
|
+
- { value: 85, color: '#F59E0B' }
|
|
148
|
+
- { value: 100, color: '#22C55E' }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Combo** — mixed types with dual y-axes:
|
|
152
|
+
```yaml
|
|
153
|
+
chart:
|
|
154
|
+
type: combo
|
|
155
|
+
x: { field: date, type: temporal }
|
|
156
|
+
series:
|
|
157
|
+
columns:
|
|
158
|
+
- { field: revenue, type: bar, axis: left, color: "#3B82F6" }
|
|
159
|
+
- { field: margin_pct, type: line, axis: right, color: "#10B981" }
|
|
160
|
+
axes:
|
|
161
|
+
left: { format: "$,.0f", label: Revenue }
|
|
162
|
+
right: { format: ".0%", label: Margin }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Multi-Series
|
|
166
|
+
|
|
167
|
+
**Long format** — group by a field:
|
|
168
|
+
```yaml
|
|
169
|
+
series:
|
|
170
|
+
field: region
|
|
171
|
+
colors:
|
|
172
|
+
North: "#3B82F6"
|
|
173
|
+
South: "#EF4444"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Wide format** — explicit columns:
|
|
177
|
+
```yaml
|
|
178
|
+
series:
|
|
179
|
+
columns:
|
|
180
|
+
- { field: revenue, name: Revenue, color: "#3B82F6" }
|
|
181
|
+
- { field: cost, name: Cost, color: "#EF4444", style: dashed }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Stacking
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
chart:
|
|
188
|
+
type: bar
|
|
189
|
+
stacking: stacked # or "percent" (normalizes to 100%)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Parameters
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
parameters:
|
|
196
|
+
- name: date_range
|
|
197
|
+
type: date_range
|
|
198
|
+
default: last_30_days
|
|
199
|
+
|
|
200
|
+
- name: region
|
|
201
|
+
type: select
|
|
202
|
+
options: [North, South, East, West]
|
|
203
|
+
default: North
|
|
204
|
+
|
|
205
|
+
- name: category
|
|
206
|
+
type: dynamic_select
|
|
207
|
+
source: { model: category_options, value_field: id, label_field: name }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Presets for date_range:** today, yesterday, last_7_days, last_30_days, last_90_days, last_365_days, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year
|
|
211
|
+
|
|
212
|
+
### Drill-Down
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
drillDown:
|
|
216
|
+
chart: detail-chart # Target chart name
|
|
217
|
+
field: category # Field to pass as filter
|
|
218
|
+
columns: # Optional table columns on landing
|
|
219
|
+
- { field: name, label: Name }
|
|
220
|
+
- { field: revenue, label: Revenue, format: "$,.0f" }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Gradients and Colors
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
chart:
|
|
227
|
+
gradient: true # Auto gradient
|
|
228
|
+
color: "#3B82F6" # Single color override
|
|
229
|
+
color: # Conditional coloring
|
|
230
|
+
conditions:
|
|
231
|
+
- { when: "> 0", color: "#10B981" }
|
|
232
|
+
- { when: "< 0", color: "#EF4444" }
|
|
233
|
+
default: "#6B7280"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Dashboards (`dashboards/*.yaml`)
|
|
237
|
+
|
|
238
|
+
12-column grid layout with chart and text widgets.
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
name: overview
|
|
242
|
+
title: Executive Overview
|
|
243
|
+
|
|
244
|
+
filters:
|
|
245
|
+
- { name: date_range, type: date_range, default: last_30_days }
|
|
246
|
+
|
|
247
|
+
layout:
|
|
248
|
+
gap: 16
|
|
249
|
+
rows:
|
|
250
|
+
- height: 180
|
|
251
|
+
widgets:
|
|
252
|
+
- { type: chart, ref: revenue-kpi, cols: 3 }
|
|
253
|
+
- { type: chart, ref: orders-kpi, cols: 3 }
|
|
254
|
+
- { type: chart, ref: customers-kpi, cols: 6 }
|
|
255
|
+
- height: 400
|
|
256
|
+
widgets:
|
|
257
|
+
- { type: chart, ref: revenue-trend, cols: 8 }
|
|
258
|
+
- type: text
|
|
259
|
+
cols: 4
|
|
260
|
+
content: |
|
|
261
|
+
## Summary
|
|
262
|
+
Revenue: **{{revenue-kpi}}**
|
|
263
|
+
vs last month: {{revenue-kpi@previous_month}}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**KPI references in text:** `{{chart}}`, `{{chart.field}}`, `{{chart@preset}}`
|
|
267
|
+
|
|
268
|
+
## Connections (`connections/*.yaml`)
|
|
269
|
+
|
|
270
|
+
```yaml
|
|
271
|
+
# DuckDB (local file)
|
|
272
|
+
name: local
|
|
273
|
+
type: duckdb
|
|
274
|
+
path: ./data/analytics.duckdb
|
|
275
|
+
|
|
276
|
+
# PostgreSQL
|
|
277
|
+
name: warehouse
|
|
278
|
+
type: postgres
|
|
279
|
+
host: ${DB_HOST}
|
|
280
|
+
port: 5432
|
|
281
|
+
database: analytics
|
|
282
|
+
user: ${DB_USER}
|
|
283
|
+
password: ${DB_PASSWORD}
|
|
284
|
+
ssl: true
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Supported:** DuckDB, PostgreSQL, MySQL, SQLite, Snowflake
|
|
288
|
+
|
|
289
|
+
Use `${ENV_VAR}` for credentials. Never commit secrets.
|
|
290
|
+
|
|
291
|
+
## Axis Types
|
|
292
|
+
|
|
293
|
+
| Type | Use | Example |
|
|
294
|
+
|------|-----|---------|
|
|
295
|
+
| `temporal` | Dates/times | `2024-01-15` |
|
|
296
|
+
| `ordinal` | Categories | `Electronics` |
|
|
297
|
+
| `quantitative` | Numbers | `250.5` |
|
|
298
|
+
|
|
299
|
+
## Number Formats (d3-format)
|
|
300
|
+
|
|
301
|
+
| Format | Output | Use |
|
|
302
|
+
|--------|--------|-----|
|
|
303
|
+
| `$,.0f` | $1,234 | Currency |
|
|
304
|
+
| `,.0f` | 1,234 | Numbers |
|
|
305
|
+
| `.2%` | 12.34% | Percentages |
|
|
306
|
+
| `.2s` | 1.2M | SI prefix |
|
|
307
|
+
|
|
308
|
+
## Theme (`yamchart.yaml`)
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
theme:
|
|
312
|
+
palette: ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]
|
|
313
|
+
gradient: false
|
|
314
|
+
opacity: 1.0
|
|
315
|
+
```
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detail,
|
|
3
|
+
error,
|
|
4
|
+
header,
|
|
5
|
+
newline,
|
|
6
|
+
success,
|
|
7
|
+
warning
|
|
8
|
+
} from "./chunk-HJVVHYVN.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/test.ts
|
|
11
|
+
import { readFile, readdir, access } from "fs/promises";
|
|
12
|
+
import { join, extname } from "path";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import {
|
|
15
|
+
parseModelMetadata,
|
|
16
|
+
DuckDBConnector,
|
|
17
|
+
runAll,
|
|
18
|
+
renderTemplate,
|
|
19
|
+
createTemplateContext,
|
|
20
|
+
expandDatePreset,
|
|
21
|
+
isDatePreset
|
|
22
|
+
} from "@yamchart/query";
|
|
23
|
+
async function testProject(projectDir, modelFilter, options) {
|
|
24
|
+
const projectPath = join(projectDir, "yamchart.yaml");
|
|
25
|
+
const projectContent = await readFile(projectPath, "utf-8");
|
|
26
|
+
const projectConfig = parseYaml(projectContent);
|
|
27
|
+
const connName = options.connection || projectConfig.defaults?.connection;
|
|
28
|
+
if (!connName) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"No connection specified. Use --connection or set defaults.connection in yamchart.yaml"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const connConfig = await loadConnectionConfig(projectDir, connName);
|
|
34
|
+
if (connConfig.type !== "duckdb") {
|
|
35
|
+
throw new Error(`Test command currently supports DuckDB only, got "${connConfig.type}"`);
|
|
36
|
+
}
|
|
37
|
+
const dbPathRaw = connConfig.config.path;
|
|
38
|
+
const dbPath = dbPathRaw.startsWith("/") ? dbPathRaw : join(projectDir, dbPathRaw);
|
|
39
|
+
const modelsDir = join(projectDir, "models");
|
|
40
|
+
const allModels = await loadModels(modelsDir);
|
|
41
|
+
let modelsToTest = allModels;
|
|
42
|
+
if (modelFilter) {
|
|
43
|
+
modelsToTest = allModels.filter((m) => m.name === modelFilter);
|
|
44
|
+
if (modelsToTest.length === 0) {
|
|
45
|
+
throw new Error(`Model "${modelFilter}" not found`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const refs = {};
|
|
49
|
+
for (const m of allModels) {
|
|
50
|
+
refs[m.name] = m.name;
|
|
51
|
+
}
|
|
52
|
+
const testInputs = [];
|
|
53
|
+
for (const model of modelsToTest) {
|
|
54
|
+
try {
|
|
55
|
+
const compiledSql = compileWithDefaults(model.sql, model.metadata, refs);
|
|
56
|
+
testInputs.push({ compiledSql, metadata: model.metadata });
|
|
57
|
+
} catch {
|
|
58
|
+
testInputs.push({ compiledSql: model.sql, metadata: model.metadata });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const connector = new DuckDBConnector({ path: dbPath });
|
|
62
|
+
try {
|
|
63
|
+
await connector.connect();
|
|
64
|
+
const suite = await runAll(testInputs, connector);
|
|
65
|
+
return { success: suite.failed === 0, suite, connectionName: connName };
|
|
66
|
+
} finally {
|
|
67
|
+
await connector.disconnect();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function loadConnectionConfig(projectDir, connName) {
|
|
71
|
+
const yamlPath = join(projectDir, "connections", `${connName}.yaml`);
|
|
72
|
+
const ymlPath = join(projectDir, "connections", `${connName}.yml`);
|
|
73
|
+
let connPath = yamlPath;
|
|
74
|
+
try {
|
|
75
|
+
await access(yamlPath);
|
|
76
|
+
} catch {
|
|
77
|
+
try {
|
|
78
|
+
await access(ymlPath);
|
|
79
|
+
connPath = ymlPath;
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`Connection "${connName}" not found at connections/${connName}.yaml`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const connContent = await readFile(connPath, "utf-8");
|
|
85
|
+
return parseYaml(connContent);
|
|
86
|
+
}
|
|
87
|
+
function resolveDynamicDefault(value) {
|
|
88
|
+
const trimmed = value.trim().toLowerCase();
|
|
89
|
+
if (trimmed === "current_date()" || trimmed === "current_date" || trimmed === "now()" || trimmed.includes("current_date")) {
|
|
90
|
+
return formatISODate(/* @__PURE__ */ new Date());
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function formatISODate(date) {
|
|
95
|
+
const y = date.getFullYear();
|
|
96
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
97
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
98
|
+
return `${y}-${m}-${d}`;
|
|
99
|
+
}
|
|
100
|
+
function compileWithDefaults(sql, metadata, refs) {
|
|
101
|
+
const params = {};
|
|
102
|
+
if (metadata.params) {
|
|
103
|
+
for (const p of metadata.params) {
|
|
104
|
+
if (p.default !== void 0) {
|
|
105
|
+
params[p.name] = resolveDynamicDefault(p.default);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (typeof params.date_range === "string" && isDatePreset(params.date_range)) {
|
|
110
|
+
const range = expandDatePreset(params.date_range);
|
|
111
|
+
if (range) {
|
|
112
|
+
params.start_date = range.start_date;
|
|
113
|
+
params.end_date = range.end_date;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const context = createTemplateContext(params, refs);
|
|
117
|
+
return renderTemplate(sql, context);
|
|
118
|
+
}
|
|
119
|
+
async function loadModels(dir) {
|
|
120
|
+
const models = [];
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
124
|
+
} catch {
|
|
125
|
+
return models;
|
|
126
|
+
}
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const fullPath = join(dir, entry.name);
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
const subModels = await loadModels(fullPath);
|
|
131
|
+
models.push(...subModels);
|
|
132
|
+
} else if (extname(entry.name) === ".sql") {
|
|
133
|
+
const content = await readFile(fullPath, "utf-8");
|
|
134
|
+
try {
|
|
135
|
+
const parsed = parseModelMetadata(content);
|
|
136
|
+
models.push({ name: parsed.name, sql: parsed.sql, metadata: parsed });
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return models;
|
|
142
|
+
}
|
|
143
|
+
function formatTestOutput(result, connectionName) {
|
|
144
|
+
const { suite } = result;
|
|
145
|
+
const testedModels = suite.models.filter(
|
|
146
|
+
(m) => m.schemaCheck || m.assertions.length > 0 || m.error
|
|
147
|
+
);
|
|
148
|
+
header(`Testing ${suite.models.length} model(s) against ${connectionName}...`);
|
|
149
|
+
for (const model of suite.models) {
|
|
150
|
+
const hasChecks = model.schemaCheck || model.assertions.length > 0 || model.error;
|
|
151
|
+
if (!hasChecks) continue;
|
|
152
|
+
console.log(` ${model.modelName}`);
|
|
153
|
+
if (model.error) {
|
|
154
|
+
error(` error: ${model.error}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (model.schemaCheck) {
|
|
158
|
+
formatSchemaCheck(model.schemaCheck);
|
|
159
|
+
}
|
|
160
|
+
for (const assertion of model.assertions) {
|
|
161
|
+
formatAssertion(assertion);
|
|
162
|
+
}
|
|
163
|
+
newline();
|
|
164
|
+
}
|
|
165
|
+
const totalChecks = suite.passed + suite.failed;
|
|
166
|
+
const summary = `${testedModels.length} model(s), ${totalChecks} check(s): ${suite.passed} passed, ${suite.failed} failed`;
|
|
167
|
+
const skippedSuffix = suite.skipped > 0 ? ` (${suite.skipped} skipped)` : "";
|
|
168
|
+
const durationSuffix = ` (${suite.durationMs.toFixed(0)}ms)`;
|
|
169
|
+
if (suite.failed === 0) {
|
|
170
|
+
success(`${summary}${skippedSuffix}${durationSuffix}`);
|
|
171
|
+
} else {
|
|
172
|
+
error(`${summary}${skippedSuffix}${durationSuffix}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function formatSchemaCheck(check) {
|
|
176
|
+
if (check.passed) {
|
|
177
|
+
success(` schema: returns expected columns (${check.expectedColumns.join(", ")})`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const issues = [];
|
|
181
|
+
if (check.missingColumns.length > 0) {
|
|
182
|
+
issues.push(`missing: ${check.missingColumns.join(", ")}`);
|
|
183
|
+
}
|
|
184
|
+
for (const m of check.typeMismatches) {
|
|
185
|
+
issues.push(`${m.column}: expected ${m.expected}, got ${m.actual}`);
|
|
186
|
+
}
|
|
187
|
+
error(` schema: ${issues.join("; ")}`);
|
|
188
|
+
}
|
|
189
|
+
function formatAssertion(assertion) {
|
|
190
|
+
const label = extractAssertionLabel(assertion.sql);
|
|
191
|
+
if (assertion.warning) {
|
|
192
|
+
warning(` ${label}`);
|
|
193
|
+
detail(` ${assertion.warning}`);
|
|
194
|
+
}
|
|
195
|
+
if (assertion.error) {
|
|
196
|
+
error(` test: ${label} -- error`);
|
|
197
|
+
detail(` ${assertion.error}`);
|
|
198
|
+
} else if (assertion.passed) {
|
|
199
|
+
success(` test: ${label}`);
|
|
200
|
+
} else {
|
|
201
|
+
error(` test: ${label} -- ${assertion.violationCount} failing row(s)`);
|
|
202
|
+
if (assertion.sampleViolations) {
|
|
203
|
+
for (const row of assertion.sampleViolations.slice(0, 3)) {
|
|
204
|
+
detail(` ${JSON.stringify(row)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function extractAssertionLabel(sql) {
|
|
210
|
+
const whereMatch = sql.match(/WHERE\s+(.+?)$/i);
|
|
211
|
+
if (whereMatch?.[1]) {
|
|
212
|
+
const clause = whereMatch[1].trim();
|
|
213
|
+
return clause.length > 60 ? clause.slice(0, 57) + "..." : clause;
|
|
214
|
+
}
|
|
215
|
+
return sql.length > 60 ? sql.slice(0, 57) + "..." : sql;
|
|
216
|
+
}
|
|
217
|
+
export {
|
|
218
|
+
formatTestOutput,
|
|
219
|
+
testProject
|
|
220
|
+
};
|
|
221
|
+
//# sourceMappingURL=test-N4KIIKQN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/test.ts"],"sourcesContent":["import { readFile, readdir, access } from 'fs/promises';\nimport { join, extname } from 'path';\nimport { parse as parseYaml } from 'yaml';\nimport {\n parseModelMetadata,\n DuckDBConnector,\n runAll,\n renderTemplate,\n createTemplateContext,\n expandDatePreset,\n isDatePreset,\n type TestSuiteResult,\n type TestModelInput,\n} from '@yamchart/query';\nimport type { ModelMetadata } from '@yamchart/schema';\nimport * as output from '../utils/output.js';\n\nexport interface TestOptions {\n connection?: string;\n json?: boolean;\n}\n\nexport interface TestResult {\n success: boolean;\n suite: TestSuiteResult;\n connectionName: string;\n}\n\nexport async function testProject(\n projectDir: string,\n modelFilter: string | undefined,\n options: TestOptions,\n): Promise<TestResult> {\n const projectPath = join(projectDir, 'yamchart.yaml');\n const projectContent = await readFile(projectPath, 'utf-8');\n const projectConfig = parseYaml(projectContent) as {\n name: string;\n defaults?: { connection?: string };\n };\n\n const connName = options.connection || projectConfig.defaults?.connection;\n if (!connName) {\n throw new Error(\n 'No connection specified. Use --connection or set defaults.connection in yamchart.yaml',\n );\n }\n\n const connConfig = await loadConnectionConfig(projectDir, connName);\n\n if (connConfig.type !== 'duckdb') {\n throw new Error(`Test command currently supports DuckDB only, got \"${connConfig.type}\"`);\n }\n\n const dbPathRaw = connConfig.config.path as string;\n const dbPath = dbPathRaw.startsWith('/') ? dbPathRaw : join(projectDir, dbPathRaw);\n\n const modelsDir = join(projectDir, 'models');\n const allModels = await loadModels(modelsDir);\n\n let modelsToTest = allModels;\n if (modelFilter) {\n modelsToTest = allModels.filter((m) => m.name === modelFilter);\n if (modelsToTest.length === 0) {\n throw new Error(`Model \"${modelFilter}\" not found`);\n }\n }\n\n // Build refs map: model name -> model name (identity for standalone execution)\n const refs: Record<string, string> = {};\n for (const m of allModels) {\n refs[m.name] = m.name;\n }\n\n const testInputs: TestModelInput[] = [];\n for (const model of modelsToTest) {\n try {\n const compiledSql = compileWithDefaults(model.sql, model.metadata, refs);\n testInputs.push({ compiledSql, metadata: model.metadata });\n } catch {\n // If compilation fails, include raw SQL so the error is reported during test execution\n testInputs.push({ compiledSql: model.sql, metadata: model.metadata });\n }\n }\n\n const connector = new DuckDBConnector({ path: dbPath });\n try {\n await connector.connect();\n const suite = await runAll(testInputs, connector);\n return { success: suite.failed === 0, suite, connectionName: connName };\n } finally {\n await connector.disconnect();\n }\n}\n\nasync function loadConnectionConfig(\n projectDir: string,\n connName: string,\n): Promise<{ type: string; config: Record<string, unknown> }> {\n const yamlPath = join(projectDir, 'connections', `${connName}.yaml`);\n const ymlPath = join(projectDir, 'connections', `${connName}.yml`);\n\n let connPath = yamlPath;\n try {\n await access(yamlPath);\n } catch {\n try {\n await access(ymlPath);\n connPath = ymlPath;\n } catch {\n throw new Error(`Connection \"${connName}\" not found at connections/${connName}.yaml`);\n }\n }\n\n const connContent = await readFile(connPath, 'utf-8');\n return parseYaml(connContent) as { type: string; config: Record<string, unknown> };\n}\n\nfunction resolveDynamicDefault(value: string): string {\n const trimmed = value.trim().toLowerCase();\n if (\n trimmed === 'current_date()' ||\n trimmed === 'current_date' ||\n trimmed === 'now()' ||\n trimmed.includes('current_date')\n ) {\n return formatISODate(new Date());\n }\n return value;\n}\n\nfunction formatISODate(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction compileWithDefaults(\n sql: string,\n metadata: ModelMetadata,\n refs: Record<string, string>,\n): string {\n const params: Record<string, unknown> = {};\n if (metadata.params) {\n for (const p of metadata.params) {\n if (p.default !== undefined) {\n params[p.name] = resolveDynamicDefault(p.default);\n }\n }\n }\n\n // Expand date presets into start_date/end_date\n if (typeof params.date_range === 'string' && isDatePreset(params.date_range)) {\n const range = expandDatePreset(params.date_range);\n if (range) {\n params.start_date = range.start_date;\n params.end_date = range.end_date;\n }\n }\n\n const context = createTemplateContext(params, refs);\n return renderTemplate(sql, context);\n}\n\ninterface LoadedModel {\n name: string;\n sql: string;\n metadata: ModelMetadata;\n}\n\nasync function loadModels(dir: string): Promise<LoadedModel[]> {\n const models: LoadedModel[] = [];\n\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return models;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n const subModels = await loadModels(fullPath);\n models.push(...subModels);\n } else if (extname(entry.name) === '.sql') {\n const content = await readFile(fullPath, 'utf-8');\n try {\n const parsed = parseModelMetadata(content);\n models.push({ name: parsed.name, sql: parsed.sql, metadata: parsed });\n } catch {\n // Skip unparseable models\n }\n }\n }\n\n return models;\n}\n\nexport function formatTestOutput(result: TestResult, connectionName: string): void {\n const { suite } = result;\n\n const testedModels = suite.models.filter(\n (m) => m.schemaCheck || m.assertions.length > 0 || m.error,\n );\n\n output.header(`Testing ${suite.models.length} model(s) against ${connectionName}...`);\n\n for (const model of suite.models) {\n const hasChecks = model.schemaCheck || model.assertions.length > 0 || model.error;\n if (!hasChecks) continue;\n\n console.log(` ${model.modelName}`);\n\n if (model.error) {\n output.error(` error: ${model.error}`);\n continue;\n }\n\n if (model.schemaCheck) {\n formatSchemaCheck(model.schemaCheck);\n }\n\n for (const assertion of model.assertions) {\n formatAssertion(assertion);\n }\n\n output.newline();\n }\n\n // Summary line\n const totalChecks = suite.passed + suite.failed;\n const summary = `${testedModels.length} model(s), ${totalChecks} check(s): ${suite.passed} passed, ${suite.failed} failed`;\n const skippedSuffix = suite.skipped > 0 ? ` (${suite.skipped} skipped)` : '';\n const durationSuffix = ` (${suite.durationMs.toFixed(0)}ms)`;\n\n if (suite.failed === 0) {\n output.success(`${summary}${skippedSuffix}${durationSuffix}`);\n } else {\n output.error(`${summary}${skippedSuffix}${durationSuffix}`);\n }\n}\n\nfunction formatSchemaCheck(check: NonNullable<TestSuiteResult['models'][0]['schemaCheck']>): void {\n if (check.passed) {\n output.success(` schema: returns expected columns (${check.expectedColumns.join(', ')})`);\n return;\n }\n\n const issues: string[] = [];\n if (check.missingColumns.length > 0) {\n issues.push(`missing: ${check.missingColumns.join(', ')}`);\n }\n for (const m of check.typeMismatches) {\n issues.push(`${m.column}: expected ${m.expected}, got ${m.actual}`);\n }\n output.error(` schema: ${issues.join('; ')}`);\n}\n\nfunction formatAssertion(assertion: TestSuiteResult['models'][0]['assertions'][0]): void {\n const label = extractAssertionLabel(assertion.sql);\n\n if (assertion.warning) {\n output.warning(` ${label}`);\n output.detail(` ${assertion.warning}`);\n }\n\n if (assertion.error) {\n output.error(` test: ${label} -- error`);\n output.detail(` ${assertion.error}`);\n } else if (assertion.passed) {\n output.success(` test: ${label}`);\n } else {\n output.error(` test: ${label} -- ${assertion.violationCount} failing row(s)`);\n if (assertion.sampleViolations) {\n for (const row of assertion.sampleViolations.slice(0, 3)) {\n output.detail(` ${JSON.stringify(row)}`);\n }\n }\n }\n}\n\nfunction extractAssertionLabel(sql: string): string {\n const whereMatch = sql.match(/WHERE\\s+(.+?)$/i);\n if (whereMatch?.[1]) {\n const clause = whereMatch[1].trim();\n return clause.length > 60 ? clause.slice(0, 57) + '...' : clause;\n }\n return sql.length > 60 ? sql.slice(0, 57) + '...' : sql;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkForUpdate
|
|
3
|
+
} from "./chunk-3CLMQNNR.js";
|
|
4
|
+
import {
|
|
5
|
+
detail,
|
|
6
|
+
error,
|
|
7
|
+
info,
|
|
8
|
+
spinner,
|
|
9
|
+
success
|
|
10
|
+
} from "./chunk-HJVVHYVN.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/update.ts
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
async function runUpdate(currentVersion) {
|
|
19
|
+
const spin = spinner("Checking for updates...");
|
|
20
|
+
const update = await checkForUpdate(currentVersion);
|
|
21
|
+
spin.stop();
|
|
22
|
+
if (!update) {
|
|
23
|
+
success(`yamchart ${currentVersion} is the latest version`);
|
|
24
|
+
} else {
|
|
25
|
+
info(`Update available: ${update.current} \u2192 ${update.latest}`);
|
|
26
|
+
const installSpin = spinner("Installing yamchart@latest...");
|
|
27
|
+
try {
|
|
28
|
+
execSync("npm install -g yamchart@latest", { stdio: "pipe" });
|
|
29
|
+
installSpin.stop();
|
|
30
|
+
success(`Updated yamchart ${update.current} \u2192 ${update.latest}`);
|
|
31
|
+
} catch {
|
|
32
|
+
installSpin.stop();
|
|
33
|
+
error("Automatic install failed. Run manually:");
|
|
34
|
+
detail("npm install -g yamchart@latest");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
await refreshDocs();
|
|
39
|
+
}
|
|
40
|
+
async function refreshDocs() {
|
|
41
|
+
const projectFile = join(process.cwd(), "yamchart.yaml");
|
|
42
|
+
try {
|
|
43
|
+
await access(projectFile);
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const bundled = await readBundledReference();
|
|
48
|
+
if (!bundled) return;
|
|
49
|
+
const refPath = join(process.cwd(), "docs", "yamchart-reference.md");
|
|
50
|
+
try {
|
|
51
|
+
const existing = await readFile(refPath, "utf-8");
|
|
52
|
+
if (existing === bundled) return;
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
await mkdir(join(process.cwd(), "docs"), { recursive: true });
|
|
56
|
+
await writeFile(refPath, bundled, "utf-8");
|
|
57
|
+
success("Updated docs/yamchart-reference.md");
|
|
58
|
+
}
|
|
59
|
+
async function readBundledReference() {
|
|
60
|
+
const distPath = join(__dirname, "templates", "default", "docs", "yamchart-reference.md");
|
|
61
|
+
const srcPath = join(__dirname, "..", "templates", "default", "docs", "yamchart-reference.md");
|
|
62
|
+
for (const path of [distPath, srcPath]) {
|
|
63
|
+
try {
|
|
64
|
+
return await readFile(path, "utf-8");
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
refreshDocs,
|
|
73
|
+
runUpdate
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=update-QHLCWS56.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/update.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport { readFile, writeFile, mkdir, access } from 'fs/promises';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport * as output from '../utils/output.js';\nimport { checkForUpdate } from '../utils/update-check.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport async function runUpdate(currentVersion: string): Promise<void> {\n const spin = output.spinner('Checking for updates...');\n\n const update = await checkForUpdate(currentVersion);\n\n spin.stop();\n\n if (!update) {\n output.success(`yamchart ${currentVersion} is the latest version`);\n } else {\n output.info(`Update available: ${update.current} → ${update.latest}`);\n const installSpin = output.spinner('Installing yamchart@latest...');\n try {\n execSync('npm install -g yamchart@latest', { stdio: 'pipe' });\n installSpin.stop();\n output.success(`Updated yamchart ${update.current} → ${update.latest}`);\n } catch {\n installSpin.stop();\n output.error('Automatic install failed. Run manually:');\n output.detail('npm install -g yamchart@latest');\n return;\n }\n }\n\n // Refresh project docs if we're in a yamchart project\n await refreshDocs();\n}\n\n/**\n * Refresh docs/yamchart-reference.md from the bundled template.\n * Only runs when yamchart.yaml exists in the current directory.\n */\nexport async function refreshDocs(): Promise<void> {\n const projectFile = join(process.cwd(), 'yamchart.yaml');\n try {\n await access(projectFile);\n } catch {\n return; // Not in a yamchart project\n }\n\n const bundled = await readBundledReference();\n if (!bundled) return;\n\n const refPath = join(process.cwd(), 'docs', 'yamchart-reference.md');\n\n try {\n const existing = await readFile(refPath, 'utf-8');\n if (existing === bundled) return; // Already up to date\n } catch {\n // File doesn't exist — create it\n }\n\n await mkdir(join(process.cwd(), 'docs'), { recursive: true });\n await writeFile(refPath, bundled, 'utf-8');\n output.success('Updated docs/yamchart-reference.md');\n}\n\nasync function readBundledReference(): Promise<string | null> {\n // Production: dist/templates/default/docs/yamchart-reference.md\n const distPath = join(__dirname, 'templates', 'default', 'docs', 'yamchart-reference.md');\n // Development: src/commands → src/templates\n const srcPath = join(__dirname, '..', 'templates', 'default', 'docs', 'yamchart-reference.md');\n\n for (const path of [distPath, srcPath]) {\n try {\n return await readFile(path, 'utf-8');\n } catch {\n continue;\n }\n }\n return null;\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,UAAU,WAAW,OAAO,cAAc;AACnD,SAAS,MAAM,eAAe;AAC9B,SAAS,qBAAqB;AAI9B,IAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAExD,eAAsB,UAAU,gBAAuC;AACrE,QAAM,OAAc,QAAQ,yBAAyB;AAErD,QAAM,SAAS,MAAM,eAAe,cAAc;AAElD,OAAK,KAAK;AAEV,MAAI,CAAC,QAAQ;AACX,IAAO,QAAQ,YAAY,cAAc,wBAAwB;AAAA,EACnE,OAAO;AACL,IAAO,KAAK,qBAAqB,OAAO,OAAO,WAAM,OAAO,MAAM,EAAE;AACpE,UAAM,cAAqB,QAAQ,+BAA+B;AAClE,QAAI;AACF,eAAS,kCAAkC,EAAE,OAAO,OAAO,CAAC;AAC5D,kBAAY,KAAK;AACjB,MAAO,QAAQ,oBAAoB,OAAO,OAAO,WAAM,OAAO,MAAM,EAAE;AAAA,IACxE,QAAQ;AACN,kBAAY,KAAK;AACjB,MAAO,MAAM,yCAAyC;AACtD,MAAO,OAAO,gCAAgC;AAC9C;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY;AACpB;AAMA,eAAsB,cAA6B;AACjD,QAAM,cAAc,KAAK,QAAQ,IAAI,GAAG,eAAe;AACvD,MAAI;AACF,UAAM,OAAO,WAAW;AAAA,EAC1B,QAAQ;AACN;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,qBAAqB;AAC3C,MAAI,CAAC,QAAS;AAEd,QAAM,UAAU,KAAK,QAAQ,IAAI,GAAG,QAAQ,uBAAuB;AAEnE,MAAI;AACF,UAAM,WAAW,MAAM,SAAS,SAAS,OAAO;AAChD,QAAI,aAAa,QAAS;AAAA,EAC5B,QAAQ;AAAA,EAER;AAEA,QAAM,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5D,QAAM,UAAU,SAAS,SAAS,OAAO;AACzC,EAAO,QAAQ,oCAAoC;AACrD;AAEA,eAAe,uBAA+C;AAE5D,QAAM,WAAW,KAAK,WAAW,aAAa,WAAW,QAAQ,uBAAuB;AAExF,QAAM,UAAU,KAAK,WAAW,MAAM,aAAa,WAAW,QAAQ,uBAAuB;AAE7F,aAAW,QAAQ,CAAC,UAAU,OAAO,GAAG;AACtC,QAAI;AACF,aAAO,MAAM,SAAS,MAAM,OAAO;AAAA,IACrC,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yamchart",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Git-native business intelligence dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,14 +33,19 @@
|
|
|
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",
|
|
45
|
+
"@yamchart/auth-local": "0.1.0",
|
|
41
46
|
"@yamchart/query": "0.1.2",
|
|
42
|
-
"@yamchart/
|
|
43
|
-
"@yamchart/
|
|
47
|
+
"@yamchart/server": "0.1.2",
|
|
48
|
+
"@yamchart/schema": "0.1.2"
|
|
44
49
|
},
|
|
45
50
|
"devDependencies": {
|
|
46
51
|
"@types/node": "^22.0.0",
|