@bonnard/cli 0.2.15 → 0.2.16

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.
@@ -0,0 +1,297 @@
1
+ # ECharts + Bonnard SDK
2
+
3
+ > Build HTML dashboards with Apache ECharts and the Bonnard SDK. No build step required.
4
+
5
+ ECharts offers rich interactivity (tooltips, zoom, legend toggling), built-in dark theme, and extensive chart types. Larger payload than Chart.js (~160KB gzip) but more powerful.
6
+
7
+ ## Starter template
8
+
9
+ Copy this complete HTML file as a starting point. Replace `bon_pk_YOUR_KEY_HERE` with your publishable API key, and update the view/measure/dimension names to match your schema.
10
+
11
+ Use `explore()` to discover available views and fields — see [sdk.query-reference](sdk.query-reference).
12
+
13
+ ```html
14
+ <!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ <title>Dashboard</title>
20
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
21
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
22
+ <style>
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+ body {
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
26
+ background: #09090b; color: #fafafa; padding: 24px;
27
+ }
28
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; }
29
+ .error { color: #ef4444; background: #1c0a0a; padding: 12px; border-radius: 8px; margin-bottom: 16px; display: none; }
30
+ .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }
31
+ .kpi { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
32
+ .kpi-label { font-size: 14px; color: #a1a1aa; margin-bottom: 8px; }
33
+ .kpi-value { font-size: 32px; font-weight: 600; }
34
+ .kpi-value.loading { color: #3f3f46; }
35
+ .charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 24px; }
36
+ .chart-card { background: #18181b; border: 1px solid #27272a; border-radius: 12px; padding: 20px; }
37
+ .chart-title { font-size: 16px; font-weight: 500; margin-bottom: 16px; }
38
+ .chart-container { height: 300px; }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <h1>Dashboard</h1>
43
+ <div id="error" class="error"></div>
44
+
45
+ <div class="kpis">
46
+ <div class="kpi">
47
+ <div class="kpi-label">Revenue</div>
48
+ <div class="kpi-value loading" id="kpi-revenue">--</div>
49
+ </div>
50
+ <div class="kpi">
51
+ <div class="kpi-label">Orders</div>
52
+ <div class="kpi-value loading" id="kpi-orders">--</div>
53
+ </div>
54
+ <div class="kpi">
55
+ <div class="kpi-label">Avg Value</div>
56
+ <div class="kpi-value loading" id="kpi-avg">--</div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="charts">
61
+ <div class="chart-card">
62
+ <div class="chart-title">Revenue by City</div>
63
+ <div class="chart-container" id="bar-chart"></div>
64
+ </div>
65
+ <div class="chart-card">
66
+ <div class="chart-title">Revenue Trend</div>
67
+ <div class="chart-container" id="line-chart"></div>
68
+ </div>
69
+ </div>
70
+
71
+ <script>
72
+ const bon = Bonnard.createClient({
73
+ apiKey: 'bon_pk_YOUR_KEY_HERE',
74
+ });
75
+
76
+ // --- Helpers ---
77
+ function showError(msg) {
78
+ const el = document.getElementById('error');
79
+ el.textContent = msg;
80
+ el.style.display = 'block';
81
+ }
82
+
83
+ function formatNumber(v) {
84
+ return new Intl.NumberFormat().format(v);
85
+ }
86
+
87
+ function formatCurrency(v) {
88
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(v);
89
+ }
90
+
91
+ // Resize all charts on window resize
92
+ const charts = [];
93
+ window.addEventListener('resize', () => charts.forEach(c => c.resize()));
94
+
95
+ // --- Load data ---
96
+ (async () => {
97
+ try {
98
+ // KPIs
99
+ const kpis = await bon.query({
100
+ measures: ['orders.revenue', 'orders.count', 'orders.avg_value'],
101
+ });
102
+ if (kpis.data.length > 0) {
103
+ const row = kpis.data[0];
104
+ document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']);
105
+ document.getElementById('kpi-revenue').classList.remove('loading');
106
+ document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']);
107
+ document.getElementById('kpi-orders').classList.remove('loading');
108
+ document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']);
109
+ document.getElementById('kpi-avg').classList.remove('loading');
110
+ }
111
+
112
+ // Bar chart — revenue by city
113
+ const byCity = await bon.query({
114
+ measures: ['orders.revenue'],
115
+ dimensions: ['orders.city'],
116
+ orderBy: { 'orders.revenue': 'desc' },
117
+ limit: 10,
118
+ });
119
+
120
+ const barChart = echarts.init(document.getElementById('bar-chart'), 'dark');
121
+ charts.push(barChart);
122
+ barChart.setOption({
123
+ tooltip: { trigger: 'axis', formatter: p => `${p[0].name}: ${formatCurrency(p[0].value)}` },
124
+ xAxis: { type: 'category', data: byCity.data.map(d => d['orders.city']) },
125
+ yAxis: { type: 'value', axisLabel: { formatter: v => formatCurrency(v) } },
126
+ series: [{
127
+ type: 'bar',
128
+ data: byCity.data.map(d => d['orders.revenue']),
129
+ itemStyle: { color: '#3b82f6', borderRadius: [4, 4, 0, 0] },
130
+ }],
131
+ grid: { left: 80, right: 20, top: 20, bottom: 40 },
132
+ });
133
+
134
+ // Line chart — revenue trend
135
+ const trend = await bon.query({
136
+ measures: ['orders.revenue'],
137
+ timeDimension: {
138
+ dimension: 'orders.created_at',
139
+ granularity: 'month',
140
+ dateRange: 'last 12 months',
141
+ },
142
+ });
143
+
144
+ const lineChart = echarts.init(document.getElementById('line-chart'), 'dark');
145
+ charts.push(lineChart);
146
+ lineChart.setOption({
147
+ tooltip: { trigger: 'axis', formatter: p => `${p[0].name}: ${formatCurrency(p[0].value)}` },
148
+ xAxis: {
149
+ type: 'category',
150
+ data: trend.data.map(d => {
151
+ const date = new Date(d['orders.created_at']);
152
+ return date.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
153
+ }),
154
+ },
155
+ yAxis: { type: 'value', axisLabel: { formatter: v => formatCurrency(v) } },
156
+ series: [{
157
+ type: 'line',
158
+ data: trend.data.map(d => d['orders.revenue']),
159
+ smooth: true,
160
+ itemStyle: { color: '#3b82f6' },
161
+ areaStyle: { color: 'rgba(59, 130, 246, 0.15)' },
162
+ }],
163
+ grid: { left: 80, right: 20, top: 20, bottom: 40 },
164
+ });
165
+ } catch (err) {
166
+ showError('Failed to load dashboard: ' + err.message);
167
+ }
168
+ })();
169
+ </script>
170
+ </body>
171
+ </html>
172
+ ```
173
+
174
+ ## Chart types
175
+
176
+ ### Bar chart
177
+
178
+ ```javascript
179
+ const chart = echarts.init(el, 'dark');
180
+ chart.setOption({
181
+ xAxis: { type: 'category', data: data.map(d => d['view.dimension']) },
182
+ yAxis: { type: 'value' },
183
+ series: [{ type: 'bar', data: data.map(d => d['view.measure']) }],
184
+ });
185
+ ```
186
+
187
+ ### Horizontal bar chart
188
+
189
+ ```javascript
190
+ chart.setOption({
191
+ xAxis: { type: 'value' },
192
+ yAxis: { type: 'category', data: data.map(d => d['view.dimension']) },
193
+ series: [{ type: 'bar', data: data.map(d => d['view.measure']) }],
194
+ });
195
+ ```
196
+
197
+ ### Line chart
198
+
199
+ ```javascript
200
+ chart.setOption({
201
+ xAxis: { type: 'category', data: labels },
202
+ yAxis: { type: 'value' },
203
+ series: [{ type: 'line', data: values, smooth: true }],
204
+ });
205
+ ```
206
+
207
+ ### Area chart
208
+
209
+ ```javascript
210
+ series: [{
211
+ type: 'line',
212
+ data: values,
213
+ smooth: true,
214
+ areaStyle: { color: 'rgba(59, 130, 246, 0.15)' },
215
+ }]
216
+ ```
217
+
218
+ ### Pie chart
219
+
220
+ ```javascript
221
+ chart.setOption({
222
+ tooltip: { trigger: 'item' },
223
+ series: [{
224
+ type: 'pie',
225
+ radius: ['40%', '70%'], // doughnut — use '60%' for full pie
226
+ data: data.map(d => ({
227
+ name: d['view.dimension'],
228
+ value: d['view.measure'],
229
+ })),
230
+ }],
231
+ });
232
+ ```
233
+
234
+ ### Multi-series line chart
235
+
236
+ ```javascript
237
+ const cities = [...new Set(data.map(d => d['orders.city']))];
238
+ const dates = [...new Set(data.map(d => d['orders.created_at']))];
239
+
240
+ chart.setOption({
241
+ tooltip: { trigger: 'axis' },
242
+ legend: { data: cities },
243
+ xAxis: { type: 'category', data: dates.map(d => new Date(d).toLocaleDateString()) },
244
+ yAxis: { type: 'value' },
245
+ series: cities.map(city => ({
246
+ name: city,
247
+ type: 'line',
248
+ smooth: true,
249
+ data: dates.map(date =>
250
+ data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0
251
+ ),
252
+ })),
253
+ });
254
+ ```
255
+
256
+ ## Dark mode
257
+
258
+ ECharts has a built-in dark theme — pass `'dark'` as the second argument to `echarts.init()`:
259
+
260
+ ```javascript
261
+ const chart = echarts.init(document.getElementById('chart'), 'dark');
262
+ ```
263
+
264
+ The dark theme sets appropriate background, text, axis, and tooltip colors automatically.
265
+
266
+ ## Resize handling
267
+
268
+ ECharts charts don't auto-resize. Add a resize listener:
269
+
270
+ ```javascript
271
+ const charts = [];
272
+
273
+ // After creating each chart:
274
+ charts.push(chart);
275
+
276
+ // Global resize handler:
277
+ window.addEventListener('resize', () => charts.forEach(c => c.resize()));
278
+ ```
279
+
280
+ ## Color palette
281
+
282
+ ```javascript
283
+ const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
284
+
285
+ // Apply to a series:
286
+ series: data.map((d, i) => ({
287
+ // ...
288
+ itemStyle: { color: COLORS[i % COLORS.length] },
289
+ }))
290
+ ```
291
+
292
+ ## See also
293
+
294
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
295
+ - [sdk.query-reference](sdk.query-reference) — Full query API
296
+ - [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smaller payload)
297
+ - [sdk.apexcharts](sdk.apexcharts) — ApexCharts alternative
@@ -0,0 +1,95 @@
1
+ # SDK
2
+
3
+ > Build data apps and dashboards on top of your semantic layer.
4
+
5
+ The Bonnard SDK (`@bonnard/sdk`) is a lightweight TypeScript/JavaScript client for querying your deployed semantic layer. Zero dependencies, ~3KB minified — works in Node.js, browsers, and edge runtimes.
6
+
7
+ ## Two ways to use it
8
+
9
+ ### npm (TypeScript / Node.js / React)
10
+
11
+ ```bash
12
+ npm install @bonnard/sdk
13
+ ```
14
+
15
+ ```typescript
16
+ import { createClient } from '@bonnard/sdk';
17
+
18
+ const bon = createClient({ apiKey: 'bon_pk_...' });
19
+ const { data } = await bon.query({
20
+ measures: ['orders.revenue'],
21
+ dimensions: ['orders.city'],
22
+ });
23
+ ```
24
+
25
+ ### CDN (browser / HTML dashboards)
26
+
27
+ ```html
28
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
29
+ <script>
30
+ const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' });
31
+ </script>
32
+ ```
33
+
34
+ Exposes `window.Bonnard` with `createClient` and `toCubeQuery`. See [sdk.browser](sdk.browser) for the full browser quickstart.
35
+
36
+ ## Authentication
37
+
38
+ Two modes depending on your use case:
39
+
40
+ | Mode | Key type | Use case |
41
+ |------|----------|----------|
42
+ | **Publishable key** | `bon_pk_...` | Public dashboards, client-side apps — safe to expose in HTML |
43
+ | **Token exchange** | `bon_sk_...` → JWT | Multi-tenant / embedded analytics — server exchanges secret key for scoped token |
44
+
45
+ ```typescript
46
+ // Simple: publishable key
47
+ const bon = createClient({ apiKey: 'bon_pk_...' });
48
+
49
+ // Multi-tenant: token exchange
50
+ const bon = createClient({
51
+ fetchToken: async () => {
52
+ const res = await fetch('/my-backend/bonnard-token');
53
+ const { token } = await res.json();
54
+ return token;
55
+ },
56
+ });
57
+ ```
58
+
59
+ The SDK automatically caches tokens and refreshes them 60 seconds before expiry.
60
+
61
+ See [sdk.authentication](sdk.authentication) for the full auth guide.
62
+
63
+ ## What you can query
64
+
65
+ | Method | Purpose |
66
+ |--------|---------|
67
+ | `query()` | JSON query — measures, dimensions, filters, time dimensions |
68
+ | `rawQuery()` | Pass a Cube-native query object directly |
69
+ | `sql()` | SQL with `MEASURE()` syntax |
70
+ | `explore()` | Discover available views, measures, and dimensions |
71
+
72
+ See [sdk.query-reference](sdk.query-reference) for the full API reference.
73
+
74
+ ## Building HTML dashboards
75
+
76
+ The SDK pairs with any chart library for single-file HTML dashboards. No build step required — load both from CDN and start querying.
77
+
78
+ | Library | CDN size (gzip) | Best for |
79
+ |---------|----------------|----------|
80
+ | **Chart.js** | ~65 KB | Default choice — most LLM training data, smallest payload |
81
+ | **ECharts** | ~160 KB | Rich interactivity, built-in dark theme |
82
+ | **ApexCharts** | ~130 KB | Best defaults out of box, SVG rendering |
83
+
84
+ Each guide includes a complete, copy-pasteable HTML starter template:
85
+
86
+ - [sdk.chartjs](sdk.chartjs) — Chart.js + SDK
87
+ - [sdk.echarts](sdk.echarts) — ECharts + SDK
88
+ - [sdk.apexcharts](sdk.apexcharts) — ApexCharts + SDK
89
+
90
+ ## See also
91
+
92
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
93
+ - [sdk.query-reference](sdk.query-reference) — Full query API reference
94
+ - [sdk.authentication](sdk.authentication) — Auth patterns
95
+ - [security-context](security-context) — Row-level security for multi-tenant apps
@@ -0,0 +1,307 @@
1
+ # Query API Reference
2
+
3
+ > Complete reference for querying the Bonnard semantic layer via the SDK.
4
+
5
+ ## query()
6
+
7
+ Execute a JSON query against the semantic layer. All field names must be fully qualified (e.g. `orders.revenue`, not `revenue`).
8
+
9
+ ```javascript
10
+ const { data } = await bon.query({
11
+ measures: ['orders.revenue', 'orders.count'],
12
+ dimensions: ['orders.city'],
13
+ filters: [
14
+ { dimension: 'orders.status', operator: 'equals', values: ['completed'] },
15
+ ],
16
+ timeDimension: {
17
+ dimension: 'orders.created_at',
18
+ granularity: 'month',
19
+ dateRange: ['2025-01-01', '2025-12-31'],
20
+ },
21
+ orderBy: { 'orders.revenue': 'desc' },
22
+ limit: 10,
23
+ });
24
+ ```
25
+
26
+ ### Parameters
27
+
28
+ | Parameter | Type | Required | Description |
29
+ |-----------|------|----------|-------------|
30
+ | `measures` | `string[]` | No | Numeric aggregations to compute (e.g. `['orders.revenue']`) |
31
+ | `dimensions` | `string[]` | No | Group-by columns (e.g. `['orders.city']`) |
32
+ | `filters` | `Filter[]` | No | Row-level filters |
33
+ | `timeDimension` | `TimeDimension` | No | Time-based grouping and date range |
34
+ | `orderBy` | `Record<string, 'asc' \| 'desc'>` | No | Sort order |
35
+ | `limit` | `number` | No | Max rows to return |
36
+
37
+ At least one of `measures` or `dimensions` is required.
38
+
39
+ ### Response
40
+
41
+ ```javascript
42
+ {
43
+ data: [
44
+ { "orders.revenue": 125000, "orders.count": 340, "orders.city": "Berlin" },
45
+ { "orders.revenue": 98000, "orders.count": 280, "orders.city": "Munich" },
46
+ ],
47
+ annotation: {
48
+ measures: {
49
+ "orders.revenue": { title: "Revenue", type: "number" },
50
+ "orders.count": { title: "Count", type: "number" },
51
+ },
52
+ dimensions: {
53
+ "orders.city": { title: "City", type: "string" },
54
+ },
55
+ }
56
+ }
57
+ ```
58
+
59
+ - `data` — array of result rows, keyed by fully qualified field names
60
+ - `annotation` — optional metadata with field titles and types
61
+
62
+ ## Filters
63
+
64
+ ```javascript
65
+ filters: [
66
+ { dimension: 'orders.status', operator: 'equals', values: ['completed'] },
67
+ { dimension: 'orders.revenue', operator: 'gt', values: [1000] },
68
+ ]
69
+ ```
70
+
71
+ ### Filter operators
72
+
73
+ | Operator | Description | Example values |
74
+ |----------|-------------|---------------|
75
+ | `equals` | Exact match (any of values) | `['completed', 'shipped']` |
76
+ | `notEquals` | Exclude matches | `['cancelled']` |
77
+ | `contains` | Substring match | `['berlin']` |
78
+ | `gt` | Greater than | `[1000]` |
79
+ | `gte` | Greater than or equal | `[1000]` |
80
+ | `lt` | Less than | `[100]` |
81
+ | `lte` | Less than or equal | `[100]` |
82
+
83
+ ## Time dimensions
84
+
85
+ Group data by time periods and filter by date range.
86
+
87
+ ```javascript
88
+ timeDimension: {
89
+ dimension: 'orders.created_at',
90
+ granularity: 'month',
91
+ dateRange: ['2025-01-01', '2025-06-30'],
92
+ }
93
+ ```
94
+
95
+ ### Granularities
96
+
97
+ | Granularity | Groups by |
98
+ |-------------|-----------|
99
+ | `day` | Calendar day |
100
+ | `week` | ISO week (Monday start) |
101
+ | `month` | Calendar month |
102
+ | `quarter` | Calendar quarter |
103
+ | `year` | Calendar year |
104
+
105
+ Omit `granularity` to filter by date range without time grouping.
106
+
107
+ ### Date range formats
108
+
109
+ ```javascript
110
+ // ISO date strings (tuple)
111
+ dateRange: ['2025-01-01', '2025-12-31']
112
+
113
+ // Single string (relative)
114
+ dateRange: 'last 30 days'
115
+ dateRange: 'last 6 months'
116
+ dateRange: 'this year'
117
+ dateRange: 'last year'
118
+ dateRange: 'today'
119
+ dateRange: 'yesterday'
120
+ dateRange: 'last week'
121
+ dateRange: 'last month'
122
+ dateRange: 'last quarter'
123
+ ```
124
+
125
+ ## Ordering and pagination
126
+
127
+ ```javascript
128
+ // Sort by revenue descending
129
+ orderBy: { 'orders.revenue': 'desc' }
130
+
131
+ // Multiple sort keys
132
+ orderBy: { 'orders.city': 'asc', 'orders.revenue': 'desc' }
133
+
134
+ // Limit results
135
+ limit: 10
136
+ ```
137
+
138
+ ## rawQuery()
139
+
140
+ Pass a Cube-native query object directly, bypassing the SDK's query format conversion. Use when you need features not exposed by `query()` (e.g. segments, offset).
141
+
142
+ ```javascript
143
+ const { data } = await bon.rawQuery({
144
+ measures: ['orders.revenue'],
145
+ dimensions: ['orders.city'],
146
+ order: [['orders.revenue', 'desc']],
147
+ limit: 10,
148
+ offset: 20,
149
+ });
150
+ ```
151
+
152
+ ## sql()
153
+
154
+ Execute a SQL query using Cube's SQL API. Use `MEASURE()` to reference semantic layer measures.
155
+
156
+ ```javascript
157
+ const { data } = await bon.sql(
158
+ `SELECT city, MEASURE(revenue), MEASURE(count)
159
+ FROM orders
160
+ WHERE status = 'completed'
161
+ GROUP BY 1
162
+ ORDER BY 2 DESC
163
+ LIMIT 10`
164
+ );
165
+ ```
166
+
167
+ Response includes an optional schema:
168
+
169
+ ```javascript
170
+ {
171
+ data: [
172
+ { city: "Berlin", revenue: 125000, count: 340 },
173
+ ],
174
+ schema: [
175
+ { name: "city", type: "string" },
176
+ { name: "revenue", type: "number" },
177
+ { name: "count", type: "number" },
178
+ ]
179
+ }
180
+ ```
181
+
182
+ ## explore()
183
+
184
+ Discover available views, measures, dimensions, and segments.
185
+
186
+ ```javascript
187
+ const meta = await bon.explore();
188
+
189
+ for (const view of meta.cubes) {
190
+ console.log(`${view.name}: ${view.description || ''}`);
191
+ for (const m of view.measures) {
192
+ console.log(` measure: ${m.name} (${m.type})`);
193
+ }
194
+ for (const d of view.dimensions) {
195
+ console.log(` dimension: ${d.name} (${d.type})`);
196
+ }
197
+ }
198
+ ```
199
+
200
+ By default returns only views (`viewsOnly: true`). To include underlying cubes:
201
+
202
+ ```javascript
203
+ const meta = await bon.explore({ viewsOnly: false });
204
+ ```
205
+
206
+ ### Response shape
207
+
208
+ ```javascript
209
+ {
210
+ cubes: [
211
+ {
212
+ name: "orders",
213
+ title: "Orders",
214
+ description: "Customer orders",
215
+ type: "view",
216
+ measures: [
217
+ { name: "orders.revenue", title: "Revenue", type: "number" },
218
+ { name: "orders.count", title: "Count", type: "number" },
219
+ ],
220
+ dimensions: [
221
+ { name: "orders.city", title: "City", type: "string" },
222
+ { name: "orders.created_at", title: "Created At", type: "time" },
223
+ ],
224
+ segments: [],
225
+ }
226
+ ]
227
+ }
228
+ ```
229
+
230
+ ## toCubeQuery()
231
+
232
+ Utility function that converts SDK `QueryOptions` into a Cube-native query object. Useful for debugging or when you need to inspect the query before sending.
233
+
234
+ ```javascript
235
+ import { toCubeQuery } from '@bonnard/sdk';
236
+ // or in browser: Bonnard.toCubeQuery(...)
237
+
238
+ const cubeQuery = Bonnard.toCubeQuery({
239
+ measures: ['orders.revenue'],
240
+ dimensions: ['orders.city'],
241
+ orderBy: { 'orders.revenue': 'desc' },
242
+ });
243
+
244
+ console.log(JSON.stringify(cubeQuery, null, 2));
245
+ // {
246
+ // "measures": ["orders.revenue"],
247
+ // "dimensions": ["orders.city"],
248
+ // "order": [["orders.revenue", "desc"]]
249
+ // }
250
+ ```
251
+
252
+ ## Common patterns
253
+
254
+ ### KPI query (single row, no dimensions)
255
+
256
+ ```javascript
257
+ const { data } = await bon.query({
258
+ measures: ['orders.revenue', 'orders.count', 'orders.avg_value'],
259
+ });
260
+ const kpis = data[0];
261
+ // { "orders.revenue": 1250000, "orders.count": 3400, "orders.avg_value": 367.6 }
262
+ ```
263
+
264
+ ### Top N with dimension
265
+
266
+ ```javascript
267
+ const { data } = await bon.query({
268
+ measures: ['orders.revenue'],
269
+ dimensions: ['orders.city'],
270
+ orderBy: { 'orders.revenue': 'desc' },
271
+ limit: 10,
272
+ });
273
+ ```
274
+
275
+ ### Time series
276
+
277
+ ```javascript
278
+ const { data } = await bon.query({
279
+ measures: ['orders.revenue'],
280
+ timeDimension: {
281
+ dimension: 'orders.created_at',
282
+ granularity: 'month',
283
+ dateRange: 'last 12 months',
284
+ },
285
+ });
286
+ // data[0]["orders.created_at"] is an ISO date string like "2025-01-01T00:00:00.000"
287
+ ```
288
+
289
+ ### Filtered breakdown
290
+
291
+ ```javascript
292
+ const { data } = await bon.query({
293
+ measures: ['orders.revenue'],
294
+ dimensions: ['orders.product_category'],
295
+ filters: [
296
+ { dimension: 'orders.city', operator: 'equals', values: ['Berlin'] },
297
+ { dimension: 'orders.revenue', operator: 'gt', values: [100] },
298
+ ],
299
+ orderBy: { 'orders.revenue': 'desc' },
300
+ });
301
+ ```
302
+
303
+ ## See also
304
+
305
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
306
+ - [sdk.authentication](sdk.authentication) — Auth patterns
307
+ - [sdk.chartjs](sdk.chartjs) — Building dashboards with Chart.js