@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.
- package/README.md +9 -2
- package/dist/bin/bon.mjs +384 -32
- package/dist/docs/topics/dashboards.md +21 -24
- package/dist/docs/topics/sdk.apexcharts.md +281 -0
- package/dist/docs/topics/sdk.authentication.md +130 -0
- package/dist/docs/topics/sdk.browser.md +181 -0
- package/dist/docs/topics/sdk.chartjs.md +327 -0
- package/dist/docs/topics/sdk.echarts.md +297 -0
- package/dist/docs/topics/sdk.md +95 -0
- package/dist/docs/topics/sdk.query-reference.md +307 -0
- package/dist/templates/claude/skills/bonnard-build-dashboard/SKILL.md +145 -0
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +1 -1
- package/dist/templates/claude/skills/bonnard-metabase-migrate/SKILL.md +1 -1
- package/dist/templates/cursor/rules/bonnard-build-dashboard.mdc +144 -0
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +1 -1
- package/dist/templates/cursor/rules/bonnard-metabase-migrate.mdc +1 -1
- package/dist/templates/shared/bonnard.md +7 -1
- package/dist/viewer.html +261 -0
- package/package.json +11 -2
|
@@ -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
|