@bonnard/cli 0.2.15 → 0.3.0

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,281 @@
1
+ # ApexCharts + Bonnard SDK
2
+
3
+ > Build HTML dashboards with ApexCharts and the Bonnard SDK. No build step required.
4
+
5
+ ApexCharts has the best visual defaults out of the box — no configuration needed for tooltips, responsive behavior, or dark mode. SVG-based rendering produces sharp visuals at any resolution. Moderate payload (~130KB gzip).
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/apexcharts@3/dist/apexcharts.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
+ // ApexCharts dark mode defaults
92
+ const darkTheme = {
93
+ chart: { background: 'transparent', foreColor: '#a1a1aa' },
94
+ theme: { mode: 'dark' },
95
+ grid: { borderColor: '#27272a' },
96
+ tooltip: { theme: 'dark' },
97
+ };
98
+
99
+ // --- Load data ---
100
+ (async () => {
101
+ try {
102
+ // KPIs
103
+ const kpis = await bon.query({
104
+ measures: ['orders.revenue', 'orders.count', 'orders.avg_value'],
105
+ });
106
+ if (kpis.data.length > 0) {
107
+ const row = kpis.data[0];
108
+ document.getElementById('kpi-revenue').textContent = formatCurrency(row['orders.revenue']);
109
+ document.getElementById('kpi-revenue').classList.remove('loading');
110
+ document.getElementById('kpi-orders').textContent = formatNumber(row['orders.count']);
111
+ document.getElementById('kpi-orders').classList.remove('loading');
112
+ document.getElementById('kpi-avg').textContent = formatCurrency(row['orders.avg_value']);
113
+ document.getElementById('kpi-avg').classList.remove('loading');
114
+ }
115
+
116
+ // Bar chart — revenue by city
117
+ const byCity = await bon.query({
118
+ measures: ['orders.revenue'],
119
+ dimensions: ['orders.city'],
120
+ orderBy: { 'orders.revenue': 'desc' },
121
+ limit: 10,
122
+ });
123
+
124
+ new ApexCharts(document.getElementById('bar-chart'), {
125
+ ...darkTheme,
126
+ chart: { ...darkTheme.chart, type: 'bar', height: 300 },
127
+ series: [{ name: 'Revenue', data: byCity.data.map(d => d['orders.revenue']) }],
128
+ xaxis: { categories: byCity.data.map(d => d['orders.city']) },
129
+ yaxis: { labels: { formatter: v => formatCurrency(v) } },
130
+ plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
131
+ colors: ['#3b82f6'],
132
+ dataLabels: { enabled: false },
133
+ }).render();
134
+
135
+ // Line chart — revenue trend
136
+ const trend = await bon.query({
137
+ measures: ['orders.revenue'],
138
+ timeDimension: {
139
+ dimension: 'orders.created_at',
140
+ granularity: 'month',
141
+ dateRange: 'last 12 months',
142
+ },
143
+ });
144
+
145
+ new ApexCharts(document.getElementById('line-chart'), {
146
+ ...darkTheme,
147
+ chart: { ...darkTheme.chart, type: 'area', height: 300 },
148
+ series: [{ name: 'Revenue', data: trend.data.map(d => d['orders.revenue']) }],
149
+ xaxis: {
150
+ categories: 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: { labels: { formatter: v => formatCurrency(v) } },
156
+ colors: ['#3b82f6'],
157
+ fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
158
+ stroke: { curve: 'smooth', width: 2 },
159
+ dataLabels: { enabled: false },
160
+ }).render();
161
+ } catch (err) {
162
+ showError('Failed to load dashboard: ' + err.message);
163
+ }
164
+ })();
165
+ </script>
166
+ </body>
167
+ </html>
168
+ ```
169
+
170
+ ## Chart types
171
+
172
+ ### Bar chart
173
+
174
+ ```javascript
175
+ new ApexCharts(el, {
176
+ chart: { type: 'bar', height: 300 },
177
+ series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
178
+ xaxis: { categories: data.map(d => d['view.dimension']) },
179
+ plotOptions: { bar: { borderRadius: 4 } },
180
+ colors: ['#3b82f6'],
181
+ }).render();
182
+ ```
183
+
184
+ ### Horizontal bar chart
185
+
186
+ ```javascript
187
+ new ApexCharts(el, {
188
+ chart: { type: 'bar', height: 300 },
189
+ series: [{ name: 'Revenue', data: data.map(d => d['view.measure']) }],
190
+ xaxis: { categories: data.map(d => d['view.dimension']) },
191
+ plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
192
+ }).render();
193
+ ```
194
+
195
+ ### Line chart
196
+
197
+ ```javascript
198
+ new ApexCharts(el, {
199
+ chart: { type: 'line', height: 300 },
200
+ series: [{ name: 'Revenue', data: values }],
201
+ xaxis: { categories: labels },
202
+ stroke: { curve: 'smooth', width: 2 },
203
+ }).render();
204
+ ```
205
+
206
+ ### Area chart
207
+
208
+ ```javascript
209
+ new ApexCharts(el, {
210
+ chart: { type: 'area', height: 300 },
211
+ series: [{ name: 'Revenue', data: values }],
212
+ xaxis: { categories: labels },
213
+ fill: { type: 'gradient', gradient: { opacityFrom: 0.3, opacityTo: 0.05 } },
214
+ stroke: { curve: 'smooth', width: 2 },
215
+ }).render();
216
+ ```
217
+
218
+ ### Pie / donut chart
219
+
220
+ ```javascript
221
+ new ApexCharts(el, {
222
+ chart: { type: 'donut', height: 300 }, // or 'pie'
223
+ series: data.map(d => d['view.measure']),
224
+ labels: data.map(d => d['view.dimension']),
225
+ colors: ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6'],
226
+ }).render();
227
+ ```
228
+
229
+ ### Multi-series line chart
230
+
231
+ ```javascript
232
+ const cities = [...new Set(data.map(d => d['orders.city']))];
233
+ const dates = [...new Set(data.map(d => d['orders.created_at']))];
234
+
235
+ new ApexCharts(el, {
236
+ chart: { type: 'line', height: 300 },
237
+ series: cities.map(city => ({
238
+ name: city,
239
+ data: dates.map(date =>
240
+ data.find(d => d['orders.city'] === city && d['orders.created_at'] === date)?.['orders.revenue'] || 0
241
+ ),
242
+ })),
243
+ xaxis: { categories: dates.map(d => new Date(d).toLocaleDateString()) },
244
+ stroke: { curve: 'smooth', width: 2 },
245
+ }).render();
246
+ ```
247
+
248
+ ## Dark mode
249
+
250
+ ApexCharts has built-in dark mode support:
251
+
252
+ ```javascript
253
+ const darkTheme = {
254
+ chart: { background: 'transparent', foreColor: '#a1a1aa' },
255
+ theme: { mode: 'dark' },
256
+ grid: { borderColor: '#27272a' },
257
+ tooltip: { theme: 'dark' },
258
+ };
259
+
260
+ new ApexCharts(el, {
261
+ ...darkTheme,
262
+ chart: { ...darkTheme.chart, type: 'bar', height: 300 },
263
+ // ... rest of config
264
+ }).render();
265
+ ```
266
+
267
+ ## Color palette
268
+
269
+ ```javascript
270
+ const COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
271
+
272
+ // Apply globally:
273
+ colors: COLORS,
274
+ ```
275
+
276
+ ## See also
277
+
278
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
279
+ - [sdk.query-reference](sdk.query-reference) — Full query API
280
+ - [sdk.chartjs](sdk.chartjs) — Chart.js alternative (smallest payload)
281
+ - [sdk.echarts](sdk.echarts) — ECharts alternative (more chart types)
@@ -0,0 +1,130 @@
1
+ # Authentication
2
+
3
+ > How to authenticate SDK requests — publishable keys for public dashboards, token exchange for multi-tenant apps.
4
+
5
+ ## Publishable keys
6
+
7
+ Publishable keys (`bon_pk_...`) are safe to use in client-side code — HTML pages, browser apps, mobile apps. They grant read-only access to your org's semantic layer.
8
+
9
+ ```javascript
10
+ const bon = Bonnard.createClient({
11
+ apiKey: 'bon_pk_...',
12
+ });
13
+ ```
14
+
15
+ Create publishable keys in the Bonnard web app under **Settings > API Keys**.
16
+
17
+ **What publishable keys can do:**
18
+ - Query measures and dimensions
19
+ - Explore schema (views, fields)
20
+
21
+ **What they cannot do:**
22
+ - Modify data or schema
23
+ - Access other orgs' data
24
+ - Bypass governance policies (if configured at org level)
25
+
26
+ ## Token exchange (multi-tenant)
27
+
28
+ For B2B apps where each customer should only see their own data, use **secret key token exchange**. Your server exchanges a secret key for a short-lived JWT with a security context, then your frontend queries with that token.
29
+
30
+ ### Server-side: exchange secret key for scoped token
31
+
32
+ ```javascript
33
+ // Your backend (Node.js, Python, etc.)
34
+ const res = await fetch('https://app.bonnard.dev/api/sdk/token', {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Authorization': `Bearer ${process.env.BONNARD_SECRET_KEY}`, // bon_sk_...
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ body: JSON.stringify({
41
+ security_context: {
42
+ tenant_id: currentCustomer.id, // your tenant identifier
43
+ },
44
+ }),
45
+ });
46
+
47
+ const { token } = await res.json();
48
+ // Pass this token to your frontend
49
+ ```
50
+
51
+ ### Client-side: query with scoped token
52
+
53
+ ```javascript
54
+ const bon = Bonnard.createClient({
55
+ fetchToken: async () => {
56
+ const res = await fetch('/my-backend/bonnard-token');
57
+ const { token } = await res.json();
58
+ return token;
59
+ },
60
+ });
61
+
62
+ const { data } = await bon.query({
63
+ measures: ['orders.revenue'],
64
+ dimensions: ['orders.status'],
65
+ });
66
+ // Only returns rows matching the tenant's security context
67
+ ```
68
+
69
+ ### How token refresh works
70
+
71
+ The SDK automatically:
72
+ 1. Calls `fetchToken()` on the first query
73
+ 2. Caches the returned JWT
74
+ 3. Parses the JWT `exp` claim
75
+ 4. Refreshes 60 seconds before expiry by calling `fetchToken()` again
76
+
77
+ You don't need to manage token lifecycle — just provide the `fetchToken` callback.
78
+
79
+ ### Security context and governance
80
+
81
+ The `security_context` object you pass during token exchange becomes available in your Cube models as `{securityContext.attrs.*}`. Use it in access policies to enforce row-level security:
82
+
83
+ ```yaml
84
+ # In your Cube view definition
85
+ access_policy:
86
+ - role: "*"
87
+ conditions:
88
+ - sql: "{TABLE}.tenant_id = '{securityContext.attrs.tenant_id}'"
89
+ ```
90
+
91
+ See [security-context](security-context) for the full governance setup guide.
92
+
93
+ ## Browser HTML with token exchange
94
+
95
+ For HTML dashboards that need multi-tenant auth, your page fetches a token from your backend:
96
+
97
+ ```html
98
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
99
+ <script>
100
+ const bon = Bonnard.createClient({
101
+ fetchToken: async () => {
102
+ const res = await fetch('/api/bonnard-token');
103
+ const { token } = await res.json();
104
+ return token;
105
+ },
106
+ });
107
+
108
+ (async () => {
109
+ const { data } = await bon.query({
110
+ measures: ['orders.revenue'],
111
+ });
112
+ // Data is scoped to the authenticated tenant
113
+ })();
114
+ </script>
115
+ ```
116
+
117
+ ## When to use which
118
+
119
+ | Scenario | Auth method | Key type |
120
+ |----------|------------|----------|
121
+ | Internal dashboard (your team) | Publishable key | `bon_pk_...` |
122
+ | Public dashboard (anyone can view) | Publishable key | `bon_pk_...` |
123
+ | Embedded analytics (customer sees their data only) | Token exchange | `bon_sk_...` → JWT |
124
+ | Server-side data pipeline | Secret key directly | `bon_sk_...` |
125
+
126
+ ## See also
127
+
128
+ - [sdk.browser](sdk.browser) — Browser / CDN quickstart
129
+ - [sdk.query-reference](sdk.query-reference) — Full query API
130
+ - [security-context](security-context) — Row-level security setup
@@ -0,0 +1,181 @@
1
+ # Browser / CDN Quickstart
2
+
3
+ > Load the Bonnard SDK via a `<script>` tag and query your semantic layer from any HTML page.
4
+
5
+ ## Setup
6
+
7
+ Add the SDK script tag to your HTML. It exposes `window.Bonnard`.
8
+
9
+ ```html
10
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
11
+ ```
12
+
13
+ Alternative CDN:
14
+ ```html
15
+ <script src="https://unpkg.com/@bonnard/sdk/dist/bonnard.iife.js"></script>
16
+ ```
17
+
18
+ Pin a specific version:
19
+ ```html
20
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk@0.4.2/dist/bonnard.iife.js"></script>
21
+ ```
22
+
23
+ ## First query
24
+
25
+ ```html
26
+ <script src="https://cdn.jsdelivr.net/npm/@bonnard/sdk/dist/bonnard.iife.js"></script>
27
+ <script>
28
+ const bon = Bonnard.createClient({
29
+ apiKey: 'bon_pk_YOUR_KEY_HERE',
30
+ });
31
+
32
+ (async () => {
33
+ const { data } = await bon.query({
34
+ measures: ['orders.revenue', 'orders.count'],
35
+ dimensions: ['orders.city'],
36
+ orderBy: { 'orders.revenue': 'desc' },
37
+ limit: 10,
38
+ });
39
+
40
+ console.log(data);
41
+ // [{ "orders.revenue": 125000, "orders.count": 340, "orders.city": "Berlin" }, ...]
42
+ })();
43
+ </script>
44
+ ```
45
+
46
+ Note: the IIFE bundle uses a regular `<script>` tag (not `type="module"`), so top-level `await` is not available. Wrap async code in an IIFE or use `.then()`.
47
+
48
+ ## Async patterns
49
+
50
+ ### IIFE wrapper (recommended)
51
+
52
+ ```html
53
+ <script>
54
+ (async () => {
55
+ const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' });
56
+ const { data } = await bon.query({ measures: ['orders.revenue'] });
57
+ document.getElementById('revenue').textContent = data[0]['orders.revenue'];
58
+ })();
59
+ </script>
60
+ ```
61
+
62
+ ### Promise chain
63
+
64
+ ```html
65
+ <script>
66
+ const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' });
67
+
68
+ bon.query({ measures: ['orders.revenue'] })
69
+ .then(({ data }) => {
70
+ document.getElementById('revenue').textContent = data[0]['orders.revenue'];
71
+ })
72
+ .catch(err => {
73
+ console.error('Query failed:', err.message);
74
+ });
75
+ </script>
76
+ ```
77
+
78
+ ### Parallel queries
79
+
80
+ ```html
81
+ <script>
82
+ (async () => {
83
+ const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' });
84
+
85
+ const [kpis, byCity] = await Promise.all([
86
+ bon.query({ measures: ['orders.revenue', 'orders.count'] }),
87
+ bon.query({
88
+ measures: ['orders.revenue'],
89
+ dimensions: ['orders.city'],
90
+ orderBy: { 'orders.revenue': 'desc' },
91
+ }),
92
+ ]);
93
+
94
+ // Render KPIs
95
+ document.getElementById('revenue').textContent = kpis.data[0]['orders.revenue'];
96
+ document.getElementById('count').textContent = kpis.data[0]['orders.count'];
97
+
98
+ // Render chart with byCity.data...
99
+ })();
100
+ </script>
101
+ ```
102
+
103
+ ## Error handling
104
+
105
+ ```html
106
+ <script>
107
+ (async () => {
108
+ const bon = Bonnard.createClient({ apiKey: 'bon_pk_...' });
109
+
110
+ try {
111
+ const { data } = await bon.query({
112
+ measures: ['orders.revenue'],
113
+ });
114
+ renderDashboard(data);
115
+ } catch (err) {
116
+ document.getElementById('error').textContent = err.message;
117
+ document.getElementById('error').style.display = 'block';
118
+ }
119
+ })();
120
+ </script>
121
+ ```
122
+
123
+ Common errors:
124
+ - `"Unauthorized"` — invalid or expired API key
125
+ - `"Query failed"` — invalid measure/dimension names or query structure
126
+ - Network errors — API unreachable (CORS, connectivity)
127
+
128
+ ## Custom base URL
129
+
130
+ By default the SDK points to `https://app.bonnard.dev`. Override for self-hosted or preview deployments:
131
+
132
+ ```html
133
+ <script>
134
+ const bon = Bonnard.createClient({
135
+ apiKey: 'bon_pk_...',
136
+ baseUrl: 'https://your-deployment.vercel.app',
137
+ });
138
+ </script>
139
+ ```
140
+
141
+ ## What's on `window.Bonnard`
142
+
143
+ The IIFE bundle exposes two exports:
144
+
145
+ | Export | Purpose |
146
+ |--------|---------|
147
+ | `Bonnard.createClient(config)` | Create an SDK client instance |
148
+ | `Bonnard.toCubeQuery(options)` | Convert `QueryOptions` to a Cube-native query object (useful for debugging) |
149
+
150
+ ## Field naming
151
+
152
+ All field names must be fully qualified with the view name:
153
+
154
+ ```javascript
155
+ // Correct
156
+ bon.query({ measures: ['orders.revenue'], dimensions: ['orders.city'] });
157
+
158
+ // Wrong — will fail
159
+ bon.query({ measures: ['revenue'], dimensions: ['city'] });
160
+ ```
161
+
162
+ ## Discovering available fields
163
+
164
+ Use `explore()` to discover what views, measures, and dimensions are available:
165
+
166
+ ```javascript
167
+ const meta = await bon.explore();
168
+ for (const view of meta.cubes) {
169
+ console.log(view.name);
170
+ console.log(' Measures:', view.measures.map(m => m.name));
171
+ console.log(' Dimensions:', view.dimensions.map(d => d.name));
172
+ }
173
+ ```
174
+
175
+ ## Next steps
176
+
177
+ - [sdk.chartjs](sdk.chartjs) — Build a dashboard with Chart.js
178
+ - [sdk.echarts](sdk.echarts) — Build a dashboard with ECharts
179
+ - [sdk.apexcharts](sdk.apexcharts) — Build a dashboard with ApexCharts
180
+ - [sdk.query-reference](sdk.query-reference) — Full query API reference
181
+ - [sdk.authentication](sdk.authentication) — Auth patterns for multi-tenant apps