@codatum/embed 0.1.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 +482 -0
- package/dist/index.cjs +648 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.global.min.js +640 -0
- package/dist/index.global.min.js.map +1 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# @codatum/embed
|
|
2
|
+
|
|
3
|
+
Core TypeScript SDK for embedding Codatum Notebook with [signed embed](https://docs.codatum.com/sharing/signed-embed): create the iframe in the browser, pass a token from your backend, handle parameters and events. Server-side token issuance is out of scope.
|
|
4
|
+
|
|
5
|
+
**Docs**: [Signed embed](https://docs.codatum.com/sharing/signed-embed) · [Integration](https://docs.codatum.com/sharing/signed-embed/integration) · [Use cases](https://docs.codatum.com/sharing/signed-embed/use-case). **Wrappers**: [@codatum/embed-react](https://github.com/codatum/codatum-embed-js/tree/main/packages/embed-react#readme), [@codatum/embed-vue](https://github.com/codatum/codatum-embed-js/tree/main/packages/embed-vue#readme).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @codatum/embed
|
|
11
|
+
# or
|
|
12
|
+
npm install @codatum/embed
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createEmbed } from '@codatum/embed';
|
|
19
|
+
|
|
20
|
+
const embed = createEmbed({
|
|
21
|
+
container: '#dashboard',
|
|
22
|
+
embedUrl: 'https://app.codatum.com/protected/workspace/xxx/notebook/yyy',
|
|
23
|
+
tokenProvider: async () => {
|
|
24
|
+
// Issue a token in your backend and fetch it here
|
|
25
|
+
const res = await fetch('/api/codatum/token', { method: 'POST' });
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
return { token: data.token };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
await embed.init();
|
|
31
|
+
|
|
32
|
+
// cleanup
|
|
33
|
+
embed.destroy();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Embed
|
|
37
|
+
|
|
38
|
+
### `EmbedOptions`
|
|
39
|
+
|
|
40
|
+
| Property | Required | Description |
|
|
41
|
+
|--------|----------|-------------|
|
|
42
|
+
| `container` | Yes | `HTMLElement` or CSS selector where the iframe is inserted |
|
|
43
|
+
| `embedUrl` | Yes | Signed embed URL from Codatum |
|
|
44
|
+
| `tokenProvider` | Yes | See [tokenProvider](#tokenprovider) below |
|
|
45
|
+
| `iframeOptions` | No | See [IframeOptions](#iframeoptions) below |
|
|
46
|
+
| `tokenOptions` | No | See [TokenOptions](#tokenoptions) below |
|
|
47
|
+
| `displayOptions` | No | See [DisplayOptions](#displayoptions) below |
|
|
48
|
+
|
|
49
|
+
#### `tokenProvider`
|
|
50
|
+
|
|
51
|
+
Required callback that issues a token from your backend and returns it (and optionally `params`). Called on `init()`, `reload()`, and when the token is auto-refreshed — `tokenOptions.refreshBuffer` seconds before the token expires.
|
|
52
|
+
|
|
53
|
+
**Signature:** `(context: TokenProviderContext) => Promise<{ token: string; params?: EncodedParam[] }>`
|
|
54
|
+
|
|
55
|
+
- **`context.trigger`** — `'INIT'` | `'RELOAD'` | `'REFRESH'`.
|
|
56
|
+
- **`context.markNonRetryable()`** — Call on failure to skip retries (ignores `tokenOptions.retryCount`).
|
|
57
|
+
- **`params`** — Optional. If returned, sent to the embed with the token; use [ParamMapper](#parammapper) `encode()` to build.
|
|
58
|
+
|
|
59
|
+
**Example:**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
tokenProvider: async (context) => {
|
|
63
|
+
const res = await fetch('/api/codatum/token', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify({ tenant_id: currentUser.tenantId }),
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
if (res.status === 401) context.markNonRetryable();
|
|
69
|
+
throw new Error(`Token failed: ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
const params = paramMapper.encode({
|
|
73
|
+
store_id: currentUser.defaultStoreId,
|
|
74
|
+
date_range: ['2025-01-01', '2025-01-31']
|
|
75
|
+
});
|
|
76
|
+
return { token: data.token, params };
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### `IframeOptions`
|
|
81
|
+
|
|
82
|
+
Options applied to the iframe element and passed to the embed via URL/search params.
|
|
83
|
+
|
|
84
|
+
| Property | Type | Default | Description |
|
|
85
|
+
|----------|------|---------|-------------|
|
|
86
|
+
| `theme` | `'LIGHT'` \| `'DARK'`\| `'SYSTEM'` | `'SYSTEM'` | UI theme of the embedded notebook |
|
|
87
|
+
| `locale` | `string` | Browser's locale | Locale code (e.g. `'en'`, `'ja'`) for the embed UI |
|
|
88
|
+
| `className` | `string` | - | CSS class name(s) applied to the iframe element |
|
|
89
|
+
| `style` | `object` | `{width: '100%', height: '100%', border: 'none'}` | Inline styles for the iframe; overrides the default styles |
|
|
90
|
+
|
|
91
|
+
#### `TokenOptions`
|
|
92
|
+
|
|
93
|
+
Controls token lifetime, refresh behavior, and init timeout.
|
|
94
|
+
|
|
95
|
+
| Property | Type | Default | Description |
|
|
96
|
+
|----------|------|---------|-------------|
|
|
97
|
+
| `refreshBuffer` | `number` | `60` | Number of seconds before the token expires when auto-refresh is triggered |
|
|
98
|
+
| `retryCount` | `number` | `2` | Number of retries on token fetch failure; `0` = no retry |
|
|
99
|
+
| `initTimeout` | `number` | `30` | Max wait in seconds for embed "ready"; `0` = no timeout |
|
|
100
|
+
| `onRefreshError` | `(error: EmbedError) => void` | `undefined` | Callback invoked when token auto-refresh fails (due to `tokenProvider` failure) and does not recover after all retries |
|
|
101
|
+
|
|
102
|
+
#### `DisplayOptions`
|
|
103
|
+
|
|
104
|
+
Sent to the embed with the token.
|
|
105
|
+
|
|
106
|
+
| Property | Type | Default | Description |
|
|
107
|
+
|----------|------|---------|-------------|
|
|
108
|
+
| `sqlDisplay` | `'SHOW'` \| `'RESULT_ONLY'` \| `'HIDE'` | `'SHOW'` | Whether to show SQL Blocks, results only, or hide |
|
|
109
|
+
| `hideParamsForm` | `boolean` | `false` | Hide the parameter form in the embed (e.g. when your app owns the filters) |
|
|
110
|
+
| `expandParamsFormByDefault` | `boolean` | `false` | Whether the parameter form is expanded by default |
|
|
111
|
+
|
|
112
|
+
### Creating an embed instance
|
|
113
|
+
|
|
114
|
+
**`createEmbed(options: EmbedOptions): EmbedInstance`**
|
|
115
|
+
|
|
116
|
+
Creates an embed instance. Throws `EmbedError` if options are invalid. Call `init()` to create the iframe and start the token flow.
|
|
117
|
+
|
|
118
|
+
### Instance methods
|
|
119
|
+
|
|
120
|
+
| Method | Description |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `async init()` | Creates the iframe, waits for it to be ready, calls `tokenProvider`, and sends token (and optional params) to the embed. Resolves when ready. Rejects with `EmbedError` on failure. |
|
|
123
|
+
| `async reload()` | Calls `tokenProvider` again and sends the returned token and params via `SET_TOKEN`. May throw `EmbedError` when run. |
|
|
124
|
+
| `destroy()` | Removes iframe, clears listeners and timers. No-op if already destroyed. |
|
|
125
|
+
|
|
126
|
+
### Instance properties
|
|
127
|
+
|
|
128
|
+
| Property | Type | Description |
|
|
129
|
+
|----------|------|-------------|
|
|
130
|
+
| `iframe` | `HTMLIFrameElement \| null` | The embed iframe element. |
|
|
131
|
+
| `status` | `'CREATED' \| 'INITIALIZING' \| 'READY' \| 'DESTROYED'` | Current instance state. |
|
|
132
|
+
|
|
133
|
+
### Events
|
|
134
|
+
|
|
135
|
+
Subscribe with `on(event, handler)` and `off(event, handler)`.
|
|
136
|
+
|
|
137
|
+
| Event | Description | Payload |
|
|
138
|
+
|-------|-------------|---------|
|
|
139
|
+
| `paramChanged` | User changed parameters in the embed. | `{ type: 'PARAM_CHANGED', params: EncodedParam[] }` |
|
|
140
|
+
| `executeSqlsTriggered` | SQL execution was triggered in the embed. | `{ type: 'EXECUTE_SQLS_TRIGGERED', params: EncodedParam[] }` |
|
|
141
|
+
|
|
142
|
+
Decode with `ParamMapper.decode(payload.params)`. `EncodedParam`: see [ParamMapper](#parammapper).
|
|
143
|
+
|
|
144
|
+
## ParamMapper
|
|
145
|
+
|
|
146
|
+
The embed uses `param_id`s (IDs assigned per notebook parameter). Your app typically works with meaningful keys such as `store_id` or `date_range`. **ParamMapper** maps between your app's key–value pairs and Codatum's `param_id` + `param_value` in both directions.
|
|
147
|
+
|
|
148
|
+
### Basic usage
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { createParamMapper } from '@codatum/embed';
|
|
152
|
+
|
|
153
|
+
const paramMapper = createParamMapper({
|
|
154
|
+
store_id: '67a1b2c3d4e5f6a7b8c9d0e1',
|
|
155
|
+
date_range: '67a1b2c3d4e5f6a7b8c9d0e2',
|
|
156
|
+
product_category: '67a1b2c3d4e5f6a7b8c9d0e3',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const appState = {
|
|
160
|
+
store_id: 'store_001',
|
|
161
|
+
date_range: ['2025-01-01', '2025-01-31'],
|
|
162
|
+
product_category: ['Electronics'],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// encode: app key:value → EncodedParam[] (use in tokenProvider return)
|
|
166
|
+
paramMapper.encode(appState);
|
|
167
|
+
// → [
|
|
168
|
+
// { param_id: '67a1b2c3...', param_value: '"store_001"' },
|
|
169
|
+
// { param_id: '67a1b2c3...', param_value: '["2025-01-01","2025-01-31"]' },
|
|
170
|
+
// { param_id: '67a1b2c3...', param_value: '["Electronics"]' },
|
|
171
|
+
// ]
|
|
172
|
+
|
|
173
|
+
// decode: EncodedParam[] → app key:value (use in paramChanged / executeSqlsTriggered)
|
|
174
|
+
paramMapper.decode(payload.params);
|
|
175
|
+
// → { store_id: 'store_001', date_range: [...], product_category: ['Electronics'] }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Creating a mapper
|
|
179
|
+
|
|
180
|
+
**`createParamMapper(mapping, meta?)`**
|
|
181
|
+
|
|
182
|
+
| Argument | Type | Description |
|
|
183
|
+
|----------|------|-------------|
|
|
184
|
+
| `mapping` | `Record<string, string>` | Your app's key → Codatum `param_id` for each parameter. |
|
|
185
|
+
| `meta` | `Record<key, ParamMeta>` (optional) | Same keys as `mapping`. Per-key options: `hidden`, `required`, `datatype`. |
|
|
186
|
+
|
|
187
|
+
**`ParamMeta`** (optional, per key):
|
|
188
|
+
|
|
189
|
+
| Property | Type | Description |
|
|
190
|
+
|----------|------|-------------|
|
|
191
|
+
| `datatype` | `ParamDatatype` | Enables validation and improves typing. See [Param datatypes](#param-datatypes) below. |
|
|
192
|
+
| `required` | `boolean` | If `true`, `encode` and `decode` throw `MISSING_REQUIRED_PARAM` when the param is missing. |
|
|
193
|
+
| `hidden` | `boolean` | If `true`, encoded params include `is_hidden: true` so the embed can hide them from the params form. |
|
|
194
|
+
|
|
195
|
+
#### Param datatypes
|
|
196
|
+
|
|
197
|
+
When `meta[key].datatype` is set, `encode` and `decode` validate values. Supported values:
|
|
198
|
+
|
|
199
|
+
| `ParamDatatype` | JS/TS type | Notes |
|
|
200
|
+
|-----------------|------------|--------|
|
|
201
|
+
| `'STRING'` | `string` | — |
|
|
202
|
+
| `'NUMBER'` | `number` | Rejects `NaN`. |
|
|
203
|
+
| `'BOOLEAN'` | `boolean` | — |
|
|
204
|
+
| `'DATE'` | `string` | Must be `YYYY-MM-DD`. |
|
|
205
|
+
| `'STRING[]'` | `string[]` | Array of strings. |
|
|
206
|
+
| `'[DATE, DATE]'` | `[string, string]` | Date range; both elements must be `YYYY-MM-DD`. |
|
|
207
|
+
|
|
208
|
+
### Instance methods
|
|
209
|
+
|
|
210
|
+
| Method | Description |
|
|
211
|
+
|--------|-------------|
|
|
212
|
+
| **`encode(values, options?)`** | App key:value → `EncodedParam[]`. JSON-stringifies values. Use `RESET_TO_DEFAULT` as a value to reset that param to the notebook's default.
|
|
213
|
+
| **`decode(params, options?)`** | `EncodedParam[]` → app key:value. Ignores params not in mapping. |
|
|
214
|
+
|
|
215
|
+
#### `encode` / `decode` options
|
|
216
|
+
|
|
217
|
+
Both methods accept an optional second argument:
|
|
218
|
+
|
|
219
|
+
| Option | Type | Description |
|
|
220
|
+
|--------|------|-------------|
|
|
221
|
+
| `only` | `(keyof mapping)[]` | Limit to these keys. Useful when the host sends only a subset (e.g. server sends some params; client sends the rest via `encode` with `only`). |
|
|
222
|
+
| `noValidate` | `boolean` | When `true`, skips required and datatype validation. |
|
|
223
|
+
|
|
224
|
+
### Type-first usage
|
|
225
|
+
|
|
226
|
+
When you define parameter definitions (e.g. with `datatype`) and want typed encode/decode, you can use the helper types.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { createParamMapper, RESET_TO_DEFAULT, type DefineDecodedParams, type EncodedParam } from '@codatum/embed';
|
|
230
|
+
|
|
231
|
+
const paramDefs = {
|
|
232
|
+
store_id: { datatype: 'STRING', required: true },
|
|
233
|
+
date_range: { datatype: '[DATE, DATE]' },
|
|
234
|
+
product_category: { datatype: 'STRING[]' },
|
|
235
|
+
} as const;
|
|
236
|
+
|
|
237
|
+
type ParamValues = DefineDecodedParams<typeof paramDefs>;
|
|
238
|
+
// → { store_id: string, date_range?: [string, string], product_category?: string[] }
|
|
239
|
+
|
|
240
|
+
const paramValues: ParamValues = {
|
|
241
|
+
store_id: 'store_001',
|
|
242
|
+
date_range: RESET_TO_DEFAULT,
|
|
243
|
+
product_category: ['Electronics']
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const paramMapper = createParamMapper({
|
|
247
|
+
store_id: '67a1b2c3d4e5f6a7b8c9d0e1',
|
|
248
|
+
date_range: '67a1b2c3d4e5f6a7b8c9d0e2',
|
|
249
|
+
product_category: '67a1b2c3d4e5f6a7b8c9d0e3',
|
|
250
|
+
}, paramDefs);
|
|
251
|
+
|
|
252
|
+
// encode only the date_range and product_category params for client-side params
|
|
253
|
+
const clientParams = paramMapper.encode(paramValues, { only: ['date_range', 'product_category'] })
|
|
254
|
+
// → [
|
|
255
|
+
// { param_id: '67a1b2c3d4e5f6a7b8c9d0e2', param_value: '["2025-01-01","2025-01-31"]' },
|
|
256
|
+
// { param_id: '67a1b2c3d4e5f6a7b8c9d0e3', param_value: '["Electronics"]' },
|
|
257
|
+
// ]
|
|
258
|
+
|
|
259
|
+
const onParamChanged = (ev: { params: EncodedParam[] }) => {
|
|
260
|
+
const values: ParamValues = paramMapper.decode(ev.params);
|
|
261
|
+
console.log('Changed:', values);
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Errors
|
|
266
|
+
|
|
267
|
+
All errors are thrown/rejected as `EmbedError` with a `code` property.
|
|
268
|
+
|
|
269
|
+
| Code | Thrown by | Description |
|
|
270
|
+
|------|----------|-------------|
|
|
271
|
+
| `INVALID_OPTIONS` | `createEmbed` | Options are invalid |
|
|
272
|
+
| `CONTAINER_NOT_FOUND` | `init` | Container element not found |
|
|
273
|
+
| `INIT_TIMEOUT` | `init` | Ready not received within `tokenOptions.initTimeout` |
|
|
274
|
+
| `TOKEN_PROVIDER_FAILED` | `init` / `reload` | `tokenProvider` threw |
|
|
275
|
+
| `MISSING_REQUIRED_PARAM` | `encode` / `decode` | Required param missing |
|
|
276
|
+
| `INVALID_PARAM_VALUE` | `encode` / `decode` | Value failed validation |
|
|
277
|
+
| `UNEXPECTED_ERROR` | `createEmbed` / `init` / `reload` | Unexpected error |
|
|
278
|
+
|
|
279
|
+
### Error handling
|
|
280
|
+
|
|
281
|
+
`init()` and `reload()` throw on failure — handle with try/catch. Auto-refresh errors are delivered via the `tokenOptions.onRefreshError` callback.
|
|
282
|
+
```ts
|
|
283
|
+
import { createEmbed, EmbedError } from '@codatum/embed';
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const embed = createEmbed({
|
|
287
|
+
container: '#dashboard',
|
|
288
|
+
embedUrl: '...',
|
|
289
|
+
tokenProvider: async () => { /* ... */ },
|
|
290
|
+
tokenOptions: {
|
|
291
|
+
onRefreshError: (error) => {
|
|
292
|
+
// Token auto-refresh failed after all retries.
|
|
293
|
+
// e.g. redirect to login, show a banner, etc.
|
|
294
|
+
console.error('Refresh failed:', error);
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
await embed.init();
|
|
299
|
+
|
|
300
|
+
// reload() also throws on failure
|
|
301
|
+
await embed.reload();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error instanceof EmbedError) {
|
|
304
|
+
// error.cause holds the original error thrown by tokenProvider (if applicable)
|
|
305
|
+
console.error(error.code, error.message);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Usage examples
|
|
311
|
+
|
|
312
|
+
The following patterns demonstrate common integration scenarios as outlined in the [Signed embed use cases](https://docs.codatum.com/sharing/signed-embed/use-case).
|
|
313
|
+
|
|
314
|
+
### Example A: Params form in embed (server validates store, client sends filters)
|
|
315
|
+
|
|
316
|
+
Embed shows the notebook's parameter form. The server validates `store_id` and encodes it in the token; the client sends current `store_id` on each token request and encodes only `date_range` and `product_category` as client-side params so the form and host stay in sync.
|
|
317
|
+
|
|
318
|
+
**Client**
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
import { createEmbed, createParamMapper, RESET_TO_DEFAULT } from '@codatum/embed';
|
|
322
|
+
|
|
323
|
+
const paramMapper = createParamMapper({...}, {
|
|
324
|
+
store_id: { datatype: 'STRING' }, // server-side param
|
|
325
|
+
date_range: { datatype: '[DATE, DATE]' }, // client-side param
|
|
326
|
+
product_category: { datatype: 'STRING[]' }, // client-side param
|
|
327
|
+
});
|
|
328
|
+
let paramValues = { store_id: undefined, date_range: RESET_TO_DEFAULT, product_category: [] };
|
|
329
|
+
|
|
330
|
+
const embed = createEmbed({
|
|
331
|
+
container: '#dashboard',
|
|
332
|
+
embedUrl: '...',
|
|
333
|
+
displayOptions: { expandParamsFormByDefault: true },
|
|
334
|
+
tokenProvider: async () => {
|
|
335
|
+
const res = await fetch('/api/codatum/token', {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
body: JSON.stringify({ tokenUserId: userId, params: { store_id: paramValues.store_id } }),
|
|
338
|
+
});
|
|
339
|
+
const { token } = await res.json();
|
|
340
|
+
const params = paramMapper.encode(paramValues, { only: ['date_range', 'product_category'] });
|
|
341
|
+
return { token, params };
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
await embed.init();
|
|
345
|
+
|
|
346
|
+
embed.on('paramChanged', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
347
|
+
embed.on('executeSqlsTriggered', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Server** — Validate `store_id` against the tenant and encode it in the token:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { createParamMapper } from '@codatum/embed';
|
|
354
|
+
|
|
355
|
+
// POST /token body: { tokenUserId, params?: { store_id } }
|
|
356
|
+
const paramMapper = createParamMapper({...}, {
|
|
357
|
+
tenant_id: { datatype: 'STRING', required: true }, // server-side param
|
|
358
|
+
store_id: { datatype: 'STRING', required: true }, // server-side param
|
|
359
|
+
});
|
|
360
|
+
const tenantId = await getTenantIdByUserId(tokenUserId);
|
|
361
|
+
const storeIdsForTenant = await getStoreIdsByTenantId(tenantId);
|
|
362
|
+
const storeId = params?.store_id ?? storeIdsForTenant[0];
|
|
363
|
+
if (!storeIdsForTenant.includes(storeId)) throw new Error('Invalid storeId');
|
|
364
|
+
const encoded = paramMapper.encode({ tenant_id: tenantId, store_id: storeId });
|
|
365
|
+
// issue token with encoded params
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Example B: Client-side params only (SaaS owns filters, hide params form)
|
|
369
|
+
|
|
370
|
+
All parameters except `tenant_id` are managed by the host; the embed's parameter form is hidden. The token carries only tenant context, so it can be cached and reused. When the user changes filters in the host UI, call `reload()` so the token provider runs again with updated values.
|
|
371
|
+
|
|
372
|
+
**Client**
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { createEmbed, createParamMapper } from '@codatum/embed';
|
|
376
|
+
|
|
377
|
+
const paramMapper = createParamMapper({...}, {
|
|
378
|
+
store_id: { datatype: 'STRING' }, // client-side param
|
|
379
|
+
date_range: { datatype: '[DATE, DATE]' }, // client-side param
|
|
380
|
+
product_category: { datatype: 'STRING[]' }, // client-side param
|
|
381
|
+
});
|
|
382
|
+
let paramValues = { store_id: 'store_001', date_range: ['2025-01-01', '2025-01-31'], product_category: [] };
|
|
383
|
+
|
|
384
|
+
const embed = createEmbed({
|
|
385
|
+
container: '#dashboard',
|
|
386
|
+
embedUrl: '...',
|
|
387
|
+
displayOptions: { hideParamsForm: true },
|
|
388
|
+
tokenProvider: async () => {
|
|
389
|
+
const res = await fetch('/api/codatum/token', { method: 'POST', body: JSON.stringify({ tokenUserId: userId }) });
|
|
390
|
+
const { token } = await res.json();
|
|
391
|
+
const params = paramMapper.encode(paramValues);
|
|
392
|
+
return { token, params };
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
await embed.init();
|
|
396
|
+
|
|
397
|
+
embed.on('paramChanged', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
398
|
+
embed.on('executeSqlsTriggered', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
399
|
+
|
|
400
|
+
// When the user changes filters in the host UI, update paramValues and reload the embed
|
|
401
|
+
async function onDashboardFilterChange(newValues: ParamValues) {
|
|
402
|
+
paramValues = newValues;
|
|
403
|
+
await embed.reload();
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Server** — Token needs only tenant scope (no param in body):
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { createParamMapper } from '@codatum/embed';
|
|
411
|
+
|
|
412
|
+
// POST /token body: { tokenUserId }
|
|
413
|
+
const paramMapper = createParamMapper({...}, {
|
|
414
|
+
tenant_id: { datatype: 'STRING', required: true }, // server-side param
|
|
415
|
+
});
|
|
416
|
+
const tenantId = await getTenantIdByUserId(tokenUserId);
|
|
417
|
+
const encoded = paramMapper.encode({ tenant_id: tenantId });
|
|
418
|
+
// issue token with encoded params
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Example C: Server-side store (token re-issue on store change)
|
|
422
|
+
|
|
423
|
+
`store_id` is fixed in the token and validated server-side; changing the store requires a new token. The params form is visible for `date_range` and `product_category`. Keep latest param values from `paramChanged` and pass `store_id` to the server on each token request; when the user switches store, reload so a new token is issued for the new store.
|
|
424
|
+
|
|
425
|
+
**Client**
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
import { createEmbed, createParamMapper, RESET_TO_DEFAULT } from '@codatum/embed';
|
|
429
|
+
|
|
430
|
+
const paramMapper = createParamMapper({...}, {
|
|
431
|
+
store_id: { datatype: 'STRING' }, // server-side param
|
|
432
|
+
date_range: { datatype: '[DATE, DATE]' }, // client-side param
|
|
433
|
+
product_category: { datatype: 'STRING[]' }, // client-side param
|
|
434
|
+
});
|
|
435
|
+
let paramValues = { store_id: undefined, date_range: RESET_TO_DEFAULT, product_category: ['Electronics'] };
|
|
436
|
+
|
|
437
|
+
const embed = createEmbed({
|
|
438
|
+
container: '#dashboard',
|
|
439
|
+
embedUrl: '...',
|
|
440
|
+
displayOptions: { expandParamsFormByDefault: true },
|
|
441
|
+
tokenProvider: async () => {
|
|
442
|
+
const res = await fetch('/api/codatum/token', {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
body: JSON.stringify({ tokenUserId: userId, params: { store_id: paramValues.store_id } }),
|
|
445
|
+
});
|
|
446
|
+
const { token } = await res.json();
|
|
447
|
+
const params = paramMapper.encode(paramValues, { only: ['date_range', 'product_category'] });
|
|
448
|
+
return { token, params };
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
await embed.init();
|
|
452
|
+
|
|
453
|
+
embed.on('paramChanged', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
454
|
+
embed.on('executeSqlsTriggered', (ev) => { paramValues = paramMapper.decode(ev.params); });
|
|
455
|
+
|
|
456
|
+
async function onStoreSwitch(storeId: string) {
|
|
457
|
+
paramValues.store_id = storeId;
|
|
458
|
+
await embed.reload();
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Server** — Same as Example A: accept `params.store_id`, validate, encode `tenant_id` + `store_id` in the token.
|
|
463
|
+
|
|
464
|
+
## CDN
|
|
465
|
+
|
|
466
|
+
A separate IIFE build exposes a single global `CodatumEmbed`. Load the script and use `CodatumEmbed.createEmbed` then `embed.init()`.
|
|
467
|
+
|
|
468
|
+
```html
|
|
469
|
+
<script src="https://unpkg.com/@codatum/embed/dist/index.global.min.js"></script>
|
|
470
|
+
<script>
|
|
471
|
+
(async function () {
|
|
472
|
+
const embed = CodatumEmbed.createEmbed({ container: '#dashboard', embedUrl: '...', tokenProvider: ... });
|
|
473
|
+
await embed.init();
|
|
474
|
+
})();
|
|
475
|
+
</script>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## See also
|
|
479
|
+
|
|
480
|
+
- **Versioning and changelog**: [Versioning](https://github.com/codatum/codatum-embed-js#versioning)
|
|
481
|
+
- **Security and deployment**: [Security and deployment](https://github.com/codatum/codatum-embed-js#security-and-deployment)
|
|
482
|
+
- **Supported environments**: [Supported environments](https://github.com/codatum/codatum-embed-js#supported-environments)
|