@nivaro/sdk 0.1.2
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 +2505 -0
- package/dist/index.d.ts +5167 -0
- package/dist/index.js +2563 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,2505 @@
|
|
|
1
|
+
# @nivaro/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [Nivaro CMS](https://nivaro.dev) — typed REST client, GraphQL, realtime subscriptions, and presence.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nivaro/sdk
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @nivaro/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
All API calls use `nivaro.request(command)` where `command` is a typed descriptor built by one of the helper functions below.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## SDK — Setup
|
|
18
|
+
|
|
19
|
+
The `@nivaro/sdk` package is a fully-typed TypeScript client for the Nivaro REST, GraphQL, and realtime APIs. Works in Node.js, browsers, and edge runtimes.
|
|
20
|
+
|
|
21
|
+
#### Installation
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
pnpm add @nivaro/sdk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### Create a client
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createNivaro } from '@nivaro/sdk'
|
|
31
|
+
|
|
32
|
+
// Minimal — use session cookie (browser) or set token later
|
|
33
|
+
const nivaro = createNivaro('https://nivaro.example.com')
|
|
34
|
+
|
|
35
|
+
// With static token
|
|
36
|
+
const nivaro = createNivaro('https://nivaro.example.com', {
|
|
37
|
+
token: 'nvk_abc123...', // Bearer token
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Full options
|
|
41
|
+
const nivaro = createNivaro('https://nivaro.example.com', {
|
|
42
|
+
token: 'nvk_abc123...',
|
|
43
|
+
workspace: 'workspace-uuid', // optional: workspace to target
|
|
44
|
+
headers: { 'X-Custom': 'value' }, // optional: extra headers
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Client methods
|
|
49
|
+
|
|
50
|
+
| Method | Purpose | Example |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| request(command) | REST operations | await nivaro.request(readItems("articles")) |
|
|
53
|
+
| graphql(query, vars?) | GraphQL queries + mutations | await nivaro.graphql(query, variables) |
|
|
54
|
+
| upload(file) | File upload | await nivaro.upload(file) |
|
|
55
|
+
| fileUrl(fileId) | Get download URL | nivaro.fileUrl("file-uuid") |
|
|
56
|
+
| setToken(token) | Set/clear auth token | nivaro.setToken("nvk_...") |
|
|
57
|
+
| getToken() | Read current token | const t = nivaro.getToken() |
|
|
58
|
+
|
|
59
|
+
#### TypeScript setup
|
|
60
|
+
|
|
61
|
+
All SDK functions are fully typed. For collection-specific item operations, define your data shape and pass it as a generic:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { readItems, createItem } from '@nivaro/sdk'
|
|
65
|
+
|
|
66
|
+
interface Article {
|
|
67
|
+
id: string
|
|
68
|
+
name: string
|
|
69
|
+
status: 'draft' | 'published'
|
|
70
|
+
author_id: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const items = await nivaro.request(
|
|
74
|
+
readItems<Article>('articles', { filter: { status: { _eq: 'published' } } })
|
|
75
|
+
)
|
|
76
|
+
// items.data → Article[]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## SDK — Authentication
|
|
82
|
+
|
|
83
|
+
The SDK supports two authentication methods: static tokens (for backends/scripts) and session cookies (for browser SPAs).
|
|
84
|
+
|
|
85
|
+
#### Static tokens (server-side / CLI)
|
|
86
|
+
|
|
87
|
+
Use a static API token for automated scripts, cron jobs, and server-to-server communication:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { createNivaro, readItems } from '@nivaro/sdk'
|
|
91
|
+
|
|
92
|
+
// Set token at creation
|
|
93
|
+
const nivaro = createNivaro('https://nivaro.example.com', {
|
|
94
|
+
token: process.env.NIVARO_TOKEN, // nvk_...
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Or set at runtime
|
|
98
|
+
nivaro.setToken(process.env.NIVARO_TOKEN)
|
|
99
|
+
|
|
100
|
+
// Check current token
|
|
101
|
+
const hasToken = nivaro.getToken() !== undefined
|
|
102
|
+
|
|
103
|
+
// Clear token (revert to unauth or session-cookie mode)
|
|
104
|
+
nivaro.setToken(null)
|
|
105
|
+
|
|
106
|
+
// Use the client
|
|
107
|
+
const { data: items } = await nivaro.request(readItems('articles'))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Session cookies (browser SPA)
|
|
111
|
+
|
|
112
|
+
In a browser, after a user logs in via the OIDC flow (`/login`), the session cookie is set automatically. The SDK will send it with every request:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { createNivaro } from '@nivaro/sdk'
|
|
116
|
+
|
|
117
|
+
const nivaro = createNivaro('https://nivaro.example.com')
|
|
118
|
+
// No token needed — session cookie is sent automatically
|
|
119
|
+
|
|
120
|
+
// After login, you can also set a static token if desired
|
|
121
|
+
nivaro.setToken(localStorage.getItem('api_token'))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### Token priority
|
|
125
|
+
|
|
126
|
+
- If a static token is set, it takes priority (Authorization: Bearer header).
|
|
127
|
+
- Otherwise, the session cookie is sent if available.
|
|
128
|
+
- If neither exists, requests are made as unauthenticated (limits depend on public routes).
|
|
129
|
+
|
|
130
|
+
#### Generate tokens via the API
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { generateToken } from '@nivaro/sdk'
|
|
134
|
+
|
|
135
|
+
// Generate a new token for yourself
|
|
136
|
+
const { data: result } = await nivaro.request(generateToken())
|
|
137
|
+
console.log(result.token) // Show once — never retrievable again
|
|
138
|
+
|
|
139
|
+
// Store in env var or secure storage
|
|
140
|
+
process.env.NIVARO_TOKEN = result.token
|
|
141
|
+
nivaro.setToken(result.token)
|
|
142
|
+
|
|
143
|
+
// Admin: generate token for another user
|
|
144
|
+
const { data: other } = await nivaro.request(generateUserToken('user-uuid'))
|
|
145
|
+
|
|
146
|
+
// Revoke your own token
|
|
147
|
+
await nivaro.request(revokeToken())
|
|
148
|
+
nivaro.setToken(null)
|
|
149
|
+
|
|
150
|
+
// Admin: revoke another user's token
|
|
151
|
+
await nivaro.request(revokeUserToken('user-uuid'))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## SDK — REST Commands
|
|
157
|
+
|
|
158
|
+
All REST operations use `await nivaro.request(command)`. Command functions are factories that return a descriptor — no network call happens until `request()` executes it.
|
|
159
|
+
|
|
160
|
+
#### Read items
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { readItems, readItem } from '@nivaro/sdk'
|
|
164
|
+
|
|
165
|
+
// List with filtering, sorting, and pagination
|
|
166
|
+
const { data, total, limit, offset } = await nivaro.request(
|
|
167
|
+
readItems('articles', {
|
|
168
|
+
filter: {
|
|
169
|
+
status: { _eq: 'published' },
|
|
170
|
+
created_at: { _gte: '2024-01-01' },
|
|
171
|
+
},
|
|
172
|
+
sort: ['-created_at', 'title'], // descending created_at, then ascending title
|
|
173
|
+
limit: 25,
|
|
174
|
+
offset: 0,
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
// data → T[]
|
|
178
|
+
|
|
179
|
+
// Single item
|
|
180
|
+
const { data: article } = await nivaro.request(readItem('articles', '123'))
|
|
181
|
+
// data → T
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Create items
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createItem } from '@nivaro/sdk'
|
|
188
|
+
|
|
189
|
+
const { data: created } = await nivaro.request(
|
|
190
|
+
createItem('articles', {
|
|
191
|
+
title: 'New Article',
|
|
192
|
+
body: 'Lorem ipsum...',
|
|
193
|
+
status: 'draft',
|
|
194
|
+
author_id: 'user-uuid',
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
// data → T (with id, timestamps, and default values filled in)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Update items
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { updateItem } from '@nivaro/sdk'
|
|
204
|
+
|
|
205
|
+
// Partial update — only changed fields
|
|
206
|
+
const { data: updated } = await nivaro.request(
|
|
207
|
+
updateItem('articles', '123', {
|
|
208
|
+
status: 'published',
|
|
209
|
+
published_at: new Date().toISOString(),
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
// data → T (full record with updates applied)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Delete items
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { deleteItem } from '@nivaro/sdk'
|
|
219
|
+
|
|
220
|
+
await nivaro.request(deleteItem('articles', '123'))
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Bulk operations
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { bulkCreateItems, bulkUpdateItems, bulkDeleteItems } from '@nivaro/sdk'
|
|
227
|
+
|
|
228
|
+
// Bulk create
|
|
229
|
+
const { data: created } = await nivaro.request(
|
|
230
|
+
bulkCreateItems('articles', [
|
|
231
|
+
{ title: 'Article 1', status: 'draft' },
|
|
232
|
+
{ title: 'Article 2', status: 'draft' },
|
|
233
|
+
])
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// Bulk update
|
|
237
|
+
const { data: updated } = await nivaro.request(
|
|
238
|
+
bulkUpdateItems('articles', [
|
|
239
|
+
{ id: '123', status: 'published' },
|
|
240
|
+
{ id: '124', status: 'published' },
|
|
241
|
+
])
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Bulk delete
|
|
245
|
+
await nivaro.request(bulkDeleteItems('articles', ['123', '124']))
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Singletons
|
|
249
|
+
|
|
250
|
+
For single-record collections (e.g., site settings), use singleton commands:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { readSingleton, updateSingleton } from '@nivaro/sdk'
|
|
254
|
+
|
|
255
|
+
// Read the singleton record
|
|
256
|
+
const { data: settings } = await nivaro.request(readSingleton('site_settings'))
|
|
257
|
+
|
|
258
|
+
// Update it
|
|
259
|
+
const { data: updated } = await nivaro.request(
|
|
260
|
+
updateSingleton('site_settings', { site_name: 'My App', theme: 'dark' })
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### Current user
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { readMe, updateMe } from '@nivaro/sdk'
|
|
268
|
+
|
|
269
|
+
// Get your own profile
|
|
270
|
+
const { data: me } = await nivaro.request(readMe())
|
|
271
|
+
// me → { id, email, first_name, last_name, role, current_workspace, ... }
|
|
272
|
+
|
|
273
|
+
// Update your profile
|
|
274
|
+
const { data: updated } = await nivaro.request(
|
|
275
|
+
updateMe({ first_name: 'Jane', last_name: 'Doe' })
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Revisions (audit trail)
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { readRevisions, readRevision } from '@nivaro/sdk'
|
|
283
|
+
|
|
284
|
+
// All changes to an item (newest first)
|
|
285
|
+
const { data: revisions } = await nivaro.request(readRevisions('articles', '123'))
|
|
286
|
+
// Each revision: { id, action ('create'|'update'|'delete'), data, delta, timestamp, user_id, first_name, last_name }
|
|
287
|
+
|
|
288
|
+
// Single revision detail
|
|
289
|
+
const { data: rev } = await nivaro.request(readRevision('rev-uuid'))
|
|
290
|
+
// rev.data → full snapshot at that point in time
|
|
291
|
+
// rev.delta → only the fields that changed (for updates)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## SDK — Workflow State Machine
|
|
297
|
+
|
|
298
|
+
Workflows are state machines that control the lifecycle of items. Each transition can be conditional, role-gated, and can trigger automations. Use these commands to read and drive workflow states.
|
|
299
|
+
|
|
300
|
+
#### Read workflow state
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { readWorkflowInstance, readWorkflowInstances } from '@nivaro/sdk'
|
|
304
|
+
|
|
305
|
+
// Get full workflow context for an item
|
|
306
|
+
const { data: wf } = await nivaro.request(
|
|
307
|
+
readWorkflowInstance('inventory_requests', itemId)
|
|
308
|
+
)
|
|
309
|
+
// wf === null → no workflow bound to this collection
|
|
310
|
+
|
|
311
|
+
// If bound:
|
|
312
|
+
// wf.instance → { current_state, started_at, completed_at, transitioned_at }
|
|
313
|
+
// wf.states → all states with { id, key, label, color, is_initial, is_terminal, lock_record }
|
|
314
|
+
// wf.available_transitions → transitions the user's role can execute from current state
|
|
315
|
+
// wf.history → immutable log: [{ transitioned_at, from_state, to_state, comment, user }]
|
|
316
|
+
|
|
317
|
+
// Find current state label
|
|
318
|
+
const currentState = wf.states.find(s => s.id === wf.instance.current_state)
|
|
319
|
+
console.log(currentState.label) // e.g., "In Progress"
|
|
320
|
+
|
|
321
|
+
// List all workflow instances for a collection (admin)
|
|
322
|
+
const { data: instances } = await nivaro.request(
|
|
323
|
+
readWorkflowInstances('inventory_requests', { limit: 100 })
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
#### Start and transition workflows
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { startWorkflow, transitionWorkflow } from '@nivaro/sdk'
|
|
331
|
+
|
|
332
|
+
// Start a workflow on an item (moves to initial state)
|
|
333
|
+
await nivaro.request(startWorkflow('inventory_requests', itemId))
|
|
334
|
+
|
|
335
|
+
// Execute a transition with optional comment
|
|
336
|
+
const { data: updated } = await nivaro.request(
|
|
337
|
+
transitionWorkflow('inventory_requests', itemId, transitionId, {
|
|
338
|
+
comment: 'Approved — ready to ship',
|
|
339
|
+
})
|
|
340
|
+
)
|
|
341
|
+
// updated → full item with updated workflow state
|
|
342
|
+
|
|
343
|
+
// Check available transitions before showing UI
|
|
344
|
+
const { data: wf } = await nivaro.request(
|
|
345
|
+
readWorkflowInstance('inventory_requests', itemId)
|
|
346
|
+
)
|
|
347
|
+
wf.available_transitions.forEach(tx => {
|
|
348
|
+
// Render a button per transition
|
|
349
|
+
console.log(tx.label, tx.id)
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Conditional transitions
|
|
354
|
+
|
|
355
|
+
Some transitions have conditions (field values that must match) before they can execute:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// Before transitioning, check if conditions are met
|
|
359
|
+
const { data: wf } = await nivaro.request(
|
|
360
|
+
readWorkflowInstance('orders', orderId)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
const transition = wf.available_transitions.find(t => t.id === selectedTxId)
|
|
364
|
+
|
|
365
|
+
// Attempt transition — if conditions not met, API returns 409
|
|
366
|
+
try {
|
|
367
|
+
await nivaro.request(
|
|
368
|
+
transitionWorkflow('orders', orderId, transition.id, { comment: 'Approved' })
|
|
369
|
+
)
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (err.status === 409) {
|
|
372
|
+
console.error('Transition conditions no longer met:', err.message)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
| Function | Purpose | Auth |
|
|
378
|
+
| --- | --- | --- |
|
|
379
|
+
| readWorkflowInstance(collection, itemId) | Get current state + available transitions | Authenticated |
|
|
380
|
+
| startWorkflow(collection, itemId) | Initialize workflow on an item | Authenticated |
|
|
381
|
+
| transitionWorkflow(col, itemId, txId, opts?) | Execute a state transition | Role-gated per transition |
|
|
382
|
+
| readWorkflowInstances(collection) | List all workflow instances | Authenticated |
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## SDK — Pipeline & Ownership Matrix
|
|
387
|
+
|
|
388
|
+
The Pipeline Owner Matrix extends workflows with multi-dimensional ownership. It resolves which users own each workflow state based on dimensional rules (e.g., "If region=North and status=urgent, then assign to @jane").
|
|
389
|
+
|
|
390
|
+
#### Read ownership
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
import {
|
|
394
|
+
readInstanceOwners, readStateOwners, readAllStateOwners
|
|
395
|
+
} from '@nivaro/sdk'
|
|
396
|
+
|
|
397
|
+
// Get owners for the CURRENT state (primary API)
|
|
398
|
+
const { data: owners } = await nivaro.request(
|
|
399
|
+
readInstanceOwners('inventory_requests', itemId)
|
|
400
|
+
)
|
|
401
|
+
// owners → User[] — { id, email, first_name, last_name, is_inherited }
|
|
402
|
+
|
|
403
|
+
// Get owners for a SPECIFIC state (non-current)
|
|
404
|
+
const { data: result } = await nivaro.request(
|
|
405
|
+
readStateOwners('inventory_requests', itemId, stateId)
|
|
406
|
+
)
|
|
407
|
+
// result.state → { id, key, label, color }
|
|
408
|
+
// result.owners → User[] (resolved via matrix rules)
|
|
409
|
+
|
|
410
|
+
// Get owners for ALL states at once (no N+1)
|
|
411
|
+
const { data: allOwners } = await nivaro.request(
|
|
412
|
+
readAllStateOwners('inventory_requests', itemId)
|
|
413
|
+
)
|
|
414
|
+
// allOwners → { [stateId]: { state, owners } } | null
|
|
415
|
+
|
|
416
|
+
// Null means no pipeline bound to collection
|
|
417
|
+
if (allOwners) {
|
|
418
|
+
Object.entries(allOwners).forEach(([stateId, { state, owners }]) => {
|
|
419
|
+
console.log(`${state.label}: ${owners.map(o => o.first_name).join(', ')}`)
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### Manual ownership overrides
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { addInstanceOwner, removeInstanceOwner } from '@nivaro/sdk'
|
|
428
|
+
|
|
429
|
+
// Assign a user as an override owner for this item
|
|
430
|
+
const { data: owner } = await nivaro.request(
|
|
431
|
+
addInstanceOwner('inventory_requests', itemId, 'user-uuid', {
|
|
432
|
+
state_id: stateId, // optional: scope to a specific state
|
|
433
|
+
})
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
// Remove an override
|
|
437
|
+
await nivaro.request(removeInstanceOwner(owner.id))
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### Admin: Pipeline template configuration
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import {
|
|
444
|
+
readPipelineTemplates, readPipelineTemplate,
|
|
445
|
+
readOwnerGroups, readDimensions
|
|
446
|
+
} from '@nivaro/sdk'
|
|
447
|
+
|
|
448
|
+
// List all pipeline templates
|
|
449
|
+
const { data: templates } = await nivaro.request(readPipelineTemplates())
|
|
450
|
+
|
|
451
|
+
// Get one template
|
|
452
|
+
const { data: template } = await nivaro.request(readPipelineTemplate(templateId))
|
|
453
|
+
// template → { id, name, states: [], binding: { collection }, ... }
|
|
454
|
+
|
|
455
|
+
// Owner groups per state (configured in admin UI)
|
|
456
|
+
const { data: groups } = await nivaro.request(readOwnerGroups(templateId))
|
|
457
|
+
// groups[stateId] → OwnerGroup[] with { filters: JSON, priority, users: [] }
|
|
458
|
+
|
|
459
|
+
// Dimensions for the matrix (region, product, etc.)
|
|
460
|
+
const { data: dimensions } = await nivaro.request(readDimensions(templateId))
|
|
461
|
+
// dimensions → Dimension[] — { field, label, is_row_axis, sort }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
#### How ownership resolution works
|
|
465
|
+
|
|
466
|
+
- Owner Groups are evaluated in priority order (lower = higher priority).
|
|
467
|
+
- Each group has filter rules (e.g., "region == North AND status == urgent").
|
|
468
|
+
- First group where ALL filters match assigns its users.
|
|
469
|
+
- If no rules match, the item has no owner.
|
|
470
|
+
- Manual Instance Owner overrides always apply (bypass rules).
|
|
471
|
+
- Delegation via user.delegate_id also applies (temporary out-of-office reassignments).
|
|
472
|
+
|
|
473
|
+
| Function | Purpose | Auth |
|
|
474
|
+
| --- | --- | --- |
|
|
475
|
+
| readInstanceOwners(col, itemId) | Owners for current state | Authenticated |
|
|
476
|
+
| readStateOwners(col, itemId, stateId) | Owners for a specific state | Authenticated |
|
|
477
|
+
| readAllStateOwners(col, itemId) | Owners for all states (no N+1) | Authenticated |
|
|
478
|
+
| addInstanceOwner(col, itemId, userId, opts?) | Add manual override | Authenticated |
|
|
479
|
+
| removeInstanceOwner(ownerId) | Remove override | Authenticated |
|
|
480
|
+
| readPipelineTemplates() | List pipeline templates | Admin |
|
|
481
|
+
| readPipelineTemplate(id) | Get one template | Admin |
|
|
482
|
+
| readOwnerGroups(templateId) | Owner groups by state | Admin |
|
|
483
|
+
| readDimensions(templateId) | Matrix dimensions | Admin |
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## SDK — Form Schema
|
|
488
|
+
|
|
489
|
+
The Form Schema API aggregates collection metadata, fields, groups, layouts, and relations into one normalized response. Use it to power dynamic UIs, form generators, and headless form runtimes.
|
|
490
|
+
|
|
491
|
+
#### Load form schema
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
import { fetchFormSchema } from '@nivaro/sdk'
|
|
495
|
+
|
|
496
|
+
const { data: schema } = await nivaro.request(fetchFormSchema('inventory_requests'))
|
|
497
|
+
// schema → {
|
|
498
|
+
// collection: { id, name, icon, ... },
|
|
499
|
+
// fields: FormField[],
|
|
500
|
+
// groups: FieldGroup[], // section/tab definitions, sorted
|
|
501
|
+
// relations: RelationMeta[], // m2o/o2m/m2m/m2a
|
|
502
|
+
// layout: { id, name, tab_mode, ... },
|
|
503
|
+
// ungroupedSort: 5, // position of Ungrouped zone
|
|
504
|
+
// }
|
|
505
|
+
|
|
506
|
+
// Iterate fields by group
|
|
507
|
+
schema.groups.forEach(group => {
|
|
508
|
+
const fieldsInGroup = schema.fields.filter(f => f.group_key === group.key)
|
|
509
|
+
console.log(group.label, fieldsInGroup.map(f => f.label))
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Evaluate field rules in real-time
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
import { evaluateFieldRules } from '@nivaro/sdk'
|
|
517
|
+
|
|
518
|
+
// As the user types, evaluate inline field rules (no save)
|
|
519
|
+
const values = { category: 'hardware', vendor: null }
|
|
520
|
+
const { data: result } = await nivaro.request(
|
|
521
|
+
evaluateFieldRules('inventory_requests', values)
|
|
522
|
+
)
|
|
523
|
+
// result.updates → { priority: 'high' } (only changed fields)
|
|
524
|
+
|
|
525
|
+
// Apply rule updates to form state
|
|
526
|
+
setValues({ ...values, ...result.updates })
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### Load relation options (picker)
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { readRelationOptions } from '@nivaro/sdk'
|
|
533
|
+
|
|
534
|
+
// Load options for an M2O or M2M field picker
|
|
535
|
+
const { data: options } = await nivaro.request(
|
|
536
|
+
readRelationOptions('inventory_requests', 'assigned_to', {
|
|
537
|
+
search: 'jane', // filter by search term
|
|
538
|
+
limit: 25,
|
|
539
|
+
})
|
|
540
|
+
)
|
|
541
|
+
// options → { value, label }[] (label from display template)
|
|
542
|
+
|
|
543
|
+
// Render picker
|
|
544
|
+
options.forEach(opt => console.log(`${opt.label} (${opt.value})`))
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Submit form item
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { submitFormItem } from '@nivaro/sdk'
|
|
551
|
+
|
|
552
|
+
// Create new item
|
|
553
|
+
const { data: created } = await nivaro.request(
|
|
554
|
+
submitFormItem('inventory_requests', {
|
|
555
|
+
mode: 'create',
|
|
556
|
+
values: {
|
|
557
|
+
title: 'New Request',
|
|
558
|
+
category: 'hardware',
|
|
559
|
+
priority: 'high',
|
|
560
|
+
},
|
|
561
|
+
})
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
// Update existing
|
|
565
|
+
const { data: updated } = await nivaro.request(
|
|
566
|
+
submitFormItem('inventory_requests', {
|
|
567
|
+
mode: 'edit',
|
|
568
|
+
itemId: '123',
|
|
569
|
+
values: { status: 'approved' }, // partial update
|
|
570
|
+
})
|
|
571
|
+
)
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
#### Form field shape
|
|
575
|
+
|
|
576
|
+
| Property | Type | Description |
|
|
577
|
+
| --- | --- | --- |
|
|
578
|
+
| key | string | Field name (used in values/updates). |
|
|
579
|
+
| label | string | Display name. |
|
|
580
|
+
| type | string | text | number | boolean | date | select | etc. |
|
|
581
|
+
| interface | string | UI hint: text-input | textarea | toggle | date-picker. |
|
|
582
|
+
| required | boolean | If true, value must be provided. |
|
|
583
|
+
| sort | number | null | Display order within group. |
|
|
584
|
+
| hidden | boolean | If true, hidden from UI but readable via API. |
|
|
585
|
+
| validation_rules | Rule[] | Constraints: min_length, pattern, unique, etc. |
|
|
586
|
+
| visibility_rules | Rule[] | Show/hide based on other field values. |
|
|
587
|
+
| lock_condition | Rule[] | Make read-only based on field values. |
|
|
588
|
+
| computed_formula | string | null | If set, field is auto-calculated (read-only). |
|
|
589
|
+
|
|
590
|
+
| Command | Purpose |
|
|
591
|
+
| --- | --- |
|
|
592
|
+
| fetchFormSchema(collection) | Load full schema + layout + relations |
|
|
593
|
+
| evaluateFieldRules(collection, values) | Server-evaluate rules against values (no save) |
|
|
594
|
+
| readRelationOptions(collection, field, opts?) | Get picker options for a relation field |
|
|
595
|
+
| submitFormItem(collection, { mode, itemId?, values }) | Create or update via form |
|
|
596
|
+
|
|
597
|
+
> **Note:** Form Schema uses **snake_case** (`validation_rules`, `visibility_rules`, `computed_formula`). The `@nivaro/react` package wraps these in **camelCase** (`validationRules`) for React — do not mix the two APIs.
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## SDK — React (@nivaro/react)
|
|
602
|
+
|
|
603
|
+
`@nivaro/react` is a form runtime built on `@nivaro/sdk`. One hook (`useNivaroForm`) handles schema loading, field rules, visibility/lock evaluation, relation options, validation, and submit. Pair it with your own inputs (headless) or use `<NivaroForm>` for auto-rendering fields.
|
|
604
|
+
|
|
605
|
+
#### Installation
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
pnpm add @nivaro/react @nivaro/sdk react react-dom @tanstack/react-query sonner
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
#### Styling
|
|
612
|
+
|
|
613
|
+
The styled components (`ItemEditForm`, `QueueWorklist`, panels, sheets) are built with Tailwind. Two ways to get their styles — pick one:
|
|
614
|
+
|
|
615
|
+
**Option A — precompiled CSS (simplest, no Tailwind required).** One import, works in any app:
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// app entry
|
|
619
|
+
import '@nivaro/react/full.css'
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Option B — Tailwind v3 preset.** If your app already runs Tailwind 3 and you want one utility pipeline (no duplicated classes):
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
// tailwind.config.js
|
|
626
|
+
module.exports = {
|
|
627
|
+
presets: [require('@nivaro/react/tailwind-preset')],
|
|
628
|
+
content: [
|
|
629
|
+
'./src/**/*.{ts,tsx}',
|
|
630
|
+
'./node_modules/@nivaro/react/dist/**/*.js'
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// app entry
|
|
635
|
+
import '@nivaro/react/styles.css'
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
The preset supplies the theme tokens (`nvr-cyan`, semantic color vars, radius, type scale) and the animate plugin; `styles.css` supplies the CSS variables plus class-based dark-mode overrides. Tailwind v4 configs ignore `tailwind.config.js` presets — use Option A there. Dark mode in both options: `dark` class on `<html>`. The headless `useNivaroForm` API needs neither.
|
|
639
|
+
|
|
640
|
+
#### Setup
|
|
641
|
+
|
|
642
|
+
Wrap your app in `<NivaroProvider>` with a configured SDK client. It provides a TanStack Query `QueryClient` automatically when your app does not already run a `QueryClientProvider` — if you do, yours wins.
|
|
643
|
+
|
|
644
|
+
#### Item links in embedded apps
|
|
645
|
+
|
|
646
|
+
Components like `QueueWorklist` open records at the admin route shape (`/collections/:collection/:id`) by default. When embedding in your own app, override link handling on `NavigationContext`:
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import { NavigationContext } from '@nivaro/react'
|
|
650
|
+
|
|
651
|
+
<NavigationContext.Provider
|
|
652
|
+
value={{
|
|
653
|
+
navigate: (to) => router.push(to),
|
|
654
|
+
// Map record links onto YOUR routes (row clicks, Open buttons, Work Next):
|
|
655
|
+
itemUrl: ({ collection, itemId, layoutSlug }) =>
|
|
656
|
+
`/records/${collection}/${itemId}${layoutSlug ? `?layout=${layoutSlug}` : ''}`,
|
|
657
|
+
// Or intercept opening entirely (return true = handled — e.g. open your own drawer):
|
|
658
|
+
openItem: ({ collection, itemId }) => {
|
|
659
|
+
openMyDetailDrawer(collection, itemId)
|
|
660
|
+
return true
|
|
661
|
+
}
|
|
662
|
+
}}
|
|
663
|
+
>
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`openItem` is checked first; returning `true` skips navigation. Otherwise the component navigates to `itemUrl(target)`, falling back to the admin shape when neither is provided.
|
|
667
|
+
|
|
668
|
+
#### Page renderer
|
|
669
|
+
|
|
670
|
+
`<PageRenderer slug="…" />` renders a page-builder page (widget grid) in your own app — the same surface as the admin's `/p/:slug` viewer: table, KPI, markdown, iframe, recent-activity, query tables (param filters, progress columns, row-click picker/drill/matrix sheets), and matrix editors (inline or drawer-button). Widget data resolves through the client identity, so permissions apply server-side. Requires `NivaroProvider` + a TanStack Query `QueryClientProvider`; supply `NavigationContext` to control where record links go, and an outer `DrilldownContext` if you host your own record sheet (PageRenderer hosts one otherwise).
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
import { NivaroProvider, PageRenderer } from '@nivaro/react'
|
|
674
|
+
|
|
675
|
+
<NivaroProvider client={nivaro}>
|
|
676
|
+
<PageRenderer slug="budget-allocation" />
|
|
677
|
+
</NivaroProvider>
|
|
678
|
+
|
|
679
|
+
// Props: slug (required), hideHeader (skip the page-name row), className
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### Report Studio viewer
|
|
683
|
+
|
|
684
|
+
`<ReportView reportId="…" />` renders a fully-styled, interactive Report Studio report in your own app — same as `QueueWorklist` / `ItemEditForm`: bring the styles via `@nivaro/react/full.css` (or the Tailwind preset). It fetches the definition and resolves every widget (KPIs, KPI groups, bar/line/donut charts via recharts, tables) as the client identity — collection read permissions apply server-side. Interactive: a global filter bar (date-range switcher + live entity-filter chips) and per-widget refresh. Read-only (no drag/resize edit surface).
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
import { NivaroProvider, ReportView } from '@nivaro/react'
|
|
688
|
+
|
|
689
|
+
<ReportView
|
|
690
|
+
reportId="…"
|
|
691
|
+
refetchInterval={60_000} // live-refresh; omit for one-shot
|
|
692
|
+
showFilterBar // date range + entity chips (default true)
|
|
693
|
+
dateRange={{ preset: 'last_3_months' }} // override the saved range (optional)
|
|
694
|
+
initialEntityFilters={[{ field: 'division', values: [1, 2] }]}
|
|
695
|
+
/>
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
For custom rendering, skip the component and drive the data commands directly: `readReport`, `readReportWidgetData` / `previewReportWidget`, plus the full CRUD, subscription (`setReportSubscription`), alert (`createReportAlert`), and AI (`aiBuildReport`, `aiReportFilters`) surface from `@nivaro/sdk`.
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
import { createNivaro } from '@nivaro/sdk'
|
|
702
|
+
import { NivaroProvider } from '@nivaro/react'
|
|
703
|
+
|
|
704
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: '...' })
|
|
705
|
+
|
|
706
|
+
export function App() {
|
|
707
|
+
return (
|
|
708
|
+
<NivaroProvider client={nivaro}>
|
|
709
|
+
<Routes>
|
|
710
|
+
<Route path="/requests/new" element={<CreateRequestForm />} />
|
|
711
|
+
<Route path="/requests/:id/edit" element={<EditRequestForm />} />
|
|
712
|
+
</Routes>
|
|
713
|
+
</NivaroProvider>
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
#### useNivaroForm hook
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
import { useNivaroForm } from '@nivaro/react'
|
|
722
|
+
|
|
723
|
+
function CreateRequestForm() {
|
|
724
|
+
const form = useNivaroForm('inventory_requests', {
|
|
725
|
+
mode: 'create',
|
|
726
|
+
defaultValues: { priority: 'medium', status: 'draft' },
|
|
727
|
+
onSuccess: (item) => navigate(`/requests/${item.id}`),
|
|
728
|
+
onError: (err) => toast.error(err.message),
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<form onSubmit={form.handleSubmit}>
|
|
733
|
+
<input
|
|
734
|
+
type="text"
|
|
735
|
+
value={form.values.title}
|
|
736
|
+
onChange={(e) => form.setValue('title', e.target.value)}
|
|
737
|
+
/>
|
|
738
|
+
{form.errors.title && <p>{form.errors.title}</p>}
|
|
739
|
+
|
|
740
|
+
<select
|
|
741
|
+
value={form.values.priority}
|
|
742
|
+
onChange={(e) => form.setValue('priority', e.target.value)}
|
|
743
|
+
disabled={form.isLocked('priority')}
|
|
744
|
+
>
|
|
745
|
+
<option value="low">Low</option>
|
|
746
|
+
<option value="medium">Medium</option>
|
|
747
|
+
<option value="high">High</option>
|
|
748
|
+
</select>
|
|
749
|
+
|
|
750
|
+
<button type="submit" disabled={!form.isValid || form.isSubmitting}>
|
|
751
|
+
{form.isSubmitting ? 'Saving...' : 'Create'}
|
|
752
|
+
</button>
|
|
753
|
+
</form>
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
#### Hook return shape
|
|
759
|
+
|
|
760
|
+
| Property | Type | Description |
|
|
761
|
+
| --- | --- | --- |
|
|
762
|
+
| values | Record<string, unknown> | Current form values. |
|
|
763
|
+
| errors | Record<string, string> | Validation errors (empty when valid). |
|
|
764
|
+
| isValid | boolean | Whether all fields pass validation. |
|
|
765
|
+
| isDirty | boolean | Whether any field differs from initial values. |
|
|
766
|
+
| isLoading | boolean | Schema loading in progress. |
|
|
767
|
+
| isSubmitting | boolean | Form submission in progress. |
|
|
768
|
+
| setValue(field, value) | void | Update one field; re-runs rules/visibility. |
|
|
769
|
+
| handleSubmit(e?) | void | Validate + submit; fires onSuccess/onError. |
|
|
770
|
+
| reset(values?) | void | Reset to initial or new values. |
|
|
771
|
+
| isVisible(field) | boolean | Whether field passes visibility rules. |
|
|
772
|
+
| isLocked(field) | boolean | Whether field is read-only. |
|
|
773
|
+
| schema | FormSchema | Full schema (camelCase: fieldType, validationRules). |
|
|
774
|
+
| fieldsByGroup | Record<string, Field[]> | Fields bucketed by group key. |
|
|
775
|
+
| visibleGroups | Group[] | Groups with at least one visible field. |
|
|
776
|
+
|
|
777
|
+
#### Auto-render form
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
import { NivaroForm } from '@nivaro/react'
|
|
781
|
+
|
|
782
|
+
function AutoForm() {
|
|
783
|
+
return (
|
|
784
|
+
<NivaroForm
|
|
785
|
+
collection="inventory_requests"
|
|
786
|
+
mode="create"
|
|
787
|
+
onSuccess={(item) => navigate(`/requests/${item.id}`)}
|
|
788
|
+
/>
|
|
789
|
+
)
|
|
790
|
+
}
|
|
791
|
+
// <NivaroForm> auto-renders all fields, groups, and validation.
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
> **Note:** `@nivaro/react` uses **camelCase** (`fieldType`, `validationRules`, `visibilityRules`) — different from the SDK Form Schema snake_case. This is intentional for React conventions. Do not mix the two APIs.
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## SDK — React Layout Hooks (@nivaro/react)
|
|
799
|
+
|
|
800
|
+
These hooks work alongside `useNivaroForm` and require a form returned by that hook. They expose the active collection layout (tabs, sections, col_span grid, ungrouped zone position) plus field-level state, dirty tracking, and repeater management. Import all hooks from `@nivaro/react`.
|
|
801
|
+
|
|
802
|
+
> **Note:** `FormSchema` now includes `ungroupedSort: number | null` — the configured position of the Ungrouped zone relative to named groups. `fetchFormSchema` and `useFormSchema` fetch this automatically from the active layout endpoint; no extra call is needed.
|
|
803
|
+
|
|
804
|
+
| Export | Kind | Purpose |
|
|
805
|
+
| --- | --- | --- |
|
|
806
|
+
| LayoutForm | Component | Full layout-aware auto-renderer (tabs, sections, col_span grid, ungrouped zone). |
|
|
807
|
+
| useOrderedLayout(form) | Hook | Ordered list of groups + `__ungrouped__` sentinel, reflecting `ungroupedSort`. |
|
|
808
|
+
| useTabState(form) | Hook | Active tab + setter + tabs list; `hasTabs` false when layout has no tab groups. |
|
|
809
|
+
| useSectionState(form, defaultCollapsed?) | Hook | Per-section collapse state; `toggle`, `collapseAll`, `expandAll`. |
|
|
810
|
+
| useFieldState(form, field) | Hook | value, error, visible, locked, required, colSpan, descriptor, onChange for one field. |
|
|
811
|
+
| useWatchFields(form, fields[]) | Hook | Reactive Record<string, unknown> slice — re-renders only when watched values change. |
|
|
812
|
+
| useFormDirty(form, initialValues?) | Hook | isDirty, dirtyFields[], isFieldDirty(field) — compares against initial or mounted values. |
|
|
813
|
+
| useFormStatus(form) | Hook | isDirty, isValid, isSubmitting, isLoading, canSubmit — one-stop status object. |
|
|
814
|
+
| useFieldArray(form, field) | Hook | append, remove, move, update, replace for ordered repeater rows. |
|
|
815
|
+
|
|
816
|
+
> **Note:** All layout hooks read the same `form` object returned by `useNivaroForm`. They do not create extra network requests — schema and layout data are fetched once by the hook and shared.
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## SDK — Notifications & Inbox
|
|
821
|
+
|
|
822
|
+
Manage your notification inbox. Notifications are created by rules, workflows, comments (@mentions), and alerts — use these endpoints to read, mark as read, and delete them.
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
import {
|
|
826
|
+
readNotifications, readNotificationCount,
|
|
827
|
+
markNotificationRead, markAllNotificationsRead, deleteNotification
|
|
828
|
+
} from '@nivaro/sdk'
|
|
829
|
+
|
|
830
|
+
// List your inbox notifications (paginated)
|
|
831
|
+
const { data: notifs, total, offset } = await nivaro.request(
|
|
832
|
+
readNotifications({ limit: 50, offset: 0 })
|
|
833
|
+
)
|
|
834
|
+
// notifs → Notification[] — { id, subject, message, status, timestamp, collection, item }
|
|
835
|
+
|
|
836
|
+
// Get unread count (lightweight, for badge)
|
|
837
|
+
const { data: counts } = await nivaro.request(readNotificationCount())
|
|
838
|
+
// counts → { unread: 5, total: 42 }
|
|
839
|
+
|
|
840
|
+
// Mark one as read
|
|
841
|
+
await nivaro.request(markNotificationRead(notif.id))
|
|
842
|
+
|
|
843
|
+
// Mark all as read
|
|
844
|
+
await nivaro.request(markAllNotificationsRead())
|
|
845
|
+
|
|
846
|
+
// Delete one
|
|
847
|
+
await nivaro.request(deleteNotification(notif.id))
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
#### Real-time notifications via Socket.io
|
|
851
|
+
|
|
852
|
+
For live notifications as they arrive, subscribe to the Socket.io event:
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
import { createRealtime } from '@nivaro/sdk'
|
|
856
|
+
|
|
857
|
+
const rt = createRealtime()
|
|
858
|
+
await rt.connect('https://nivaro.example.com')
|
|
859
|
+
|
|
860
|
+
// Subscribe to your notifications room
|
|
861
|
+
rt.subscribe(`user:${userId}`, { event: 'notification:new' }, (notif) => {
|
|
862
|
+
console.log('New notification:', notif.subject)
|
|
863
|
+
// Update badge, toast, etc.
|
|
864
|
+
})
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
| Field | Type | Description |
|
|
868
|
+
| --- | --- | --- |
|
|
869
|
+
| id | string | UUID. |
|
|
870
|
+
| subject | string | Notification title. |
|
|
871
|
+
| message | string | Full message body. |
|
|
872
|
+
| status | string | "inbox" | "read" |
|
|
873
|
+
| timestamp | string | ISO 8601 when created. |
|
|
874
|
+
| collection | string | null | Related collection (if from an item event). |
|
|
875
|
+
| item | string | null | Related item ID. |
|
|
876
|
+
| sender | string | null | User who triggered it (if applicable). |
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## SDK — Activity & Revisions
|
|
881
|
+
|
|
882
|
+
The activity log records all changes and actions in your CMS. Revisions give you full audit trail and rollback capability.
|
|
883
|
+
|
|
884
|
+
#### Activity log
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
import { readActivity } from '@nivaro/sdk'
|
|
888
|
+
|
|
889
|
+
// All activity, newest first (system-wide audit log)
|
|
890
|
+
const { data: entries, total } = await nivaro.request(
|
|
891
|
+
readActivity({ limit: 50, offset: 0 })
|
|
892
|
+
)
|
|
893
|
+
// entries → Activity[] — { id, action, collection, item, user_id, timestamp, ... }
|
|
894
|
+
|
|
895
|
+
// Filter by collection, action, or user
|
|
896
|
+
const { data: creates } = await nivaro.request(
|
|
897
|
+
readActivity({
|
|
898
|
+
collection: 'inventory_requests',
|
|
899
|
+
action: 'create', // 'create' | 'update' | 'delete' | 'schema-*'
|
|
900
|
+
user_id: 'user-uuid', // optional
|
|
901
|
+
limit: 25,
|
|
902
|
+
})
|
|
903
|
+
)
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
#### Revisions (item-level history)
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
import { readRevisions, readRevision } from '@nivaro/sdk'
|
|
910
|
+
|
|
911
|
+
// All revisions for a specific item (newest first)
|
|
912
|
+
const { data: revisions } = await nivaro.request(
|
|
913
|
+
readRevisions('inventory_requests', itemId, { limit: 50 })
|
|
914
|
+
)
|
|
915
|
+
// revisions → Revision[] — each revision includes action, full snapshot, and delta
|
|
916
|
+
|
|
917
|
+
revisions.forEach(rev => {
|
|
918
|
+
console.log(
|
|
919
|
+
`${rev.action.toUpperCase()} by ${rev.first_name} at ${rev.timestamp}`
|
|
920
|
+
)
|
|
921
|
+
if (rev.delta) {
|
|
922
|
+
console.log('Changed:', Object.keys(rev.delta))
|
|
923
|
+
}
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
// Single revision detail with full snapshot and delta
|
|
927
|
+
const { data: rev } = await nivaro.request(readRevision(revisionId))
|
|
928
|
+
console.log('Full snapshot:', rev.data)
|
|
929
|
+
console.log('Changed fields:', rev.delta)
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
#### Revision shape
|
|
933
|
+
|
|
934
|
+
| Field | Type | Description |
|
|
935
|
+
| --- | --- | --- |
|
|
936
|
+
| id | string | Revision UUID. |
|
|
937
|
+
| action | string | "create" | "update" | "delete" |
|
|
938
|
+
| collection | string | Collection name. |
|
|
939
|
+
| item_id | string | Item ID (null for create). |
|
|
940
|
+
| data | Record | Full snapshot of the record at that revision. |
|
|
941
|
+
| delta | Record | null | Only changed fields (for updates). Null for create/delete. |
|
|
942
|
+
| timestamp | string | ISO 8601 datetime. |
|
|
943
|
+
| user_id | string | User who made the change. |
|
|
944
|
+
| first_name / last_name / user_email | string | Display info from nivaro_users. |
|
|
945
|
+
|
|
946
|
+
> **Note:** Revisions are immutable — they form a complete audit trail. You can compare any two revisions to see exactly what changed. For rollback, use the delta as a PATCH to the current item.
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## SDK — External APIs
|
|
951
|
+
|
|
952
|
+
Call configured external APIs without exposing credentials. The SDK handles credential injection server-side. Use these commands to manage API configs, test connections, and call endpoints.
|
|
953
|
+
|
|
954
|
+
#### Calling an external API
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
import { callExternalApi } from '@nivaro/sdk'
|
|
958
|
+
|
|
959
|
+
// Call any endpoint on a configured API
|
|
960
|
+
const { data: result } = await nivaro.request(
|
|
961
|
+
callExternalApi('slack-api', {
|
|
962
|
+
method: 'POST',
|
|
963
|
+
path: '/chat.postMessage',
|
|
964
|
+
body: { channel: 'C1234', text: 'Task completed' },
|
|
965
|
+
})
|
|
966
|
+
)
|
|
967
|
+
// result.status → 200, 400, 500, etc.
|
|
968
|
+
// result.headers → response headers
|
|
969
|
+
// result.body → parsed JSON response (or text if not JSON)
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
#### Admin: API config CRUD
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
import {
|
|
976
|
+
readExternalApis, readExternalApi,
|
|
977
|
+
createExternalApi, updateExternalApi, deleteExternalApi,
|
|
978
|
+
testExternalApi,
|
|
979
|
+
} from '@nivaro/sdk'
|
|
980
|
+
|
|
981
|
+
// List all configured APIs
|
|
982
|
+
const { data: apis } = await nivaro.request(readExternalApis())
|
|
983
|
+
|
|
984
|
+
// Get one
|
|
985
|
+
const { data: api } = await nivaro.request(readExternalApi(apiId))
|
|
986
|
+
|
|
987
|
+
// Create an API config
|
|
988
|
+
const { data: slack } = await nivaro.request(
|
|
989
|
+
createExternalApi({
|
|
990
|
+
name: 'Slack API',
|
|
991
|
+
base_url: 'https://slack.com/api',
|
|
992
|
+
auth_type: 'bearer', // bearer | api_key | basic | oauth2_cc
|
|
993
|
+
auth_config: { token: 'xoxb-...' },
|
|
994
|
+
enabled: true,
|
|
995
|
+
})
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
// Update
|
|
999
|
+
await nivaro.request(updateExternalApi(slack.id, { enabled: false }))
|
|
1000
|
+
|
|
1001
|
+
// Test the connection
|
|
1002
|
+
const { data: testResult } = await nivaro.request(testExternalApi(slack.id, {
|
|
1003
|
+
method: 'GET',
|
|
1004
|
+
path: '/auth.test',
|
|
1005
|
+
}))
|
|
1006
|
+
console.log(testResult.status, testResult.body)
|
|
1007
|
+
|
|
1008
|
+
// Delete
|
|
1009
|
+
await nivaro.request(deleteExternalApi(slack.id))
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
| Auth Type | Config Fields | Example |
|
|
1013
|
+
| --- | --- | --- |
|
|
1014
|
+
| bearer | token | { token: "sk_live_..." } |
|
|
1015
|
+
| api_key | header_name, value | { header_name: "X-API-Key", value: "key123" } |
|
|
1016
|
+
| basic | username, password | { username: "user", password: "pass" } |
|
|
1017
|
+
| oauth2_cc | client_id, client_secret, token_url | { client_id: "...", ... } |
|
|
1018
|
+
|
|
1019
|
+
#### Admin: Endpoint templates
|
|
1020
|
+
|
|
1021
|
+
Pre-define common endpoints for an API so users can call them by slug instead of writing full path/method each time.
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
import {
|
|
1025
|
+
readExternalApiEndpoints, createExternalApiEndpoint,
|
|
1026
|
+
updateExternalApiEndpoint, deleteExternalApiEndpoint,
|
|
1027
|
+
} from '@nivaro/sdk'
|
|
1028
|
+
|
|
1029
|
+
// List templates for an API
|
|
1030
|
+
const { data: endpoints } = await nivaro.request(readExternalApiEndpoints(apiId))
|
|
1031
|
+
|
|
1032
|
+
// Create a template
|
|
1033
|
+
const { data: tpl } = await nivaro.request(
|
|
1034
|
+
createExternalApiEndpoint(apiId, {
|
|
1035
|
+
name: 'Post Message',
|
|
1036
|
+
slug: 'post-message', // users call via this slug
|
|
1037
|
+
method: 'POST',
|
|
1038
|
+
path: '/chat.postMessage',
|
|
1039
|
+
default_body: { channel: 'general' },
|
|
1040
|
+
})
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
// Update
|
|
1044
|
+
await nivaro.request(updateExternalApiEndpoint(tpl.id, {
|
|
1045
|
+
default_body: { channel: 'alerts' },
|
|
1046
|
+
}))
|
|
1047
|
+
|
|
1048
|
+
// Delete
|
|
1049
|
+
await nivaro.request(deleteExternalApiEndpoint(tpl.id))
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
> **Note:** Credentials (tokens, passwords) are never exposed in GET responses — sensitive fields return a masked value like `••••••`. When updating, re-submit the masked value to keep the existing credential; send a plaintext value to change it.
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## SDK — GraphQL Transport
|
|
1057
|
+
|
|
1058
|
+
Use `nivaro.graphql()` to send typed GraphQL queries and mutations. The method throws on errors — no need to check `response.errors` manually. Uses the same auth (token/cookie) as REST.
|
|
1059
|
+
|
|
1060
|
+
#### Queries
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
import { createNivaro } from '@nivaro/sdk'
|
|
1064
|
+
|
|
1065
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: '...' })
|
|
1066
|
+
|
|
1067
|
+
interface ArticlesResult {
|
|
1068
|
+
articles: {
|
|
1069
|
+
data: Array<{ id: string; name: string; status: string; author_id: string }>
|
|
1070
|
+
total: number
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const result = await nivaro.graphql<ArticlesResult>(`
|
|
1075
|
+
query {
|
|
1076
|
+
articles(filter: { status: { _eq: "active" } }, limit: 10) {
|
|
1077
|
+
data { id name status author_id }
|
|
1078
|
+
total
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
`)
|
|
1082
|
+
|
|
1083
|
+
result.articles.data.forEach(article => {
|
|
1084
|
+
console.log(`${article.name} by author ${article.author_id}`)
|
|
1085
|
+
})
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
#### Queries with variables
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
interface ArticlesResult {
|
|
1092
|
+
articles: { data: Article[]; total: number }
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const result = await nivaro.graphql<ArticlesResult>(
|
|
1096
|
+
`query GetArticles($filter: JSON, $limit: Int) {
|
|
1097
|
+
articles(filter: $filter, limit: $limit) {
|
|
1098
|
+
data { id name status }
|
|
1099
|
+
total
|
|
1100
|
+
}
|
|
1101
|
+
}`,
|
|
1102
|
+
{
|
|
1103
|
+
filter: { status: { _eq: 'active' }, created_at: { _gte: '2024-01-01' } },
|
|
1104
|
+
limit: 25,
|
|
1105
|
+
},
|
|
1106
|
+
'GetArticles' // optional operationName (for debugging)
|
|
1107
|
+
)
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
#### Mutations
|
|
1111
|
+
|
|
1112
|
+
```typescript
|
|
1113
|
+
interface CreateArticleResult {
|
|
1114
|
+
createArticle: { id: string; name: string; status: string }
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const result = await nivaro.graphql<CreateArticleResult>(`
|
|
1118
|
+
mutation CreateArticle($name: String!, $status: String) {
|
|
1119
|
+
createArticle(data: { name: $name, status: $status }) {
|
|
1120
|
+
id name status
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
`,
|
|
1124
|
+
{ name: 'New Article', status: 'draft' }
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
console.log('Created:', result.createArticle.id)
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
#### Subscriptions
|
|
1131
|
+
|
|
1132
|
+
```typescript
|
|
1133
|
+
// GraphQL subscriptions require a separate WebSocket transport
|
|
1134
|
+
// Use the Socket.io realtime client instead — it's simpler and handles reconnection
|
|
1135
|
+
|
|
1136
|
+
import { createRealtime } from '@nivaro/sdk'
|
|
1137
|
+
|
|
1138
|
+
const rt = createRealtime()
|
|
1139
|
+
await rt.connect('https://nivaro.example.com')
|
|
1140
|
+
|
|
1141
|
+
rt.subscribe('articles', { event: 'update' }, (article) => {
|
|
1142
|
+
console.log('Article updated:', article)
|
|
1143
|
+
})
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
| Method | Purpose |
|
|
1147
|
+
| --- | --- |
|
|
1148
|
+
| nivaro.graphql(query, variables?, operationName?) | Send a GraphQL query or mutation |
|
|
1149
|
+
| nivaro.setToken(token) | Set auth token (shared with REST) |
|
|
1150
|
+
|
|
1151
|
+
> **Note:** The GraphQL schema is auto-generated from your collections, fields, and relations at startup. View the full schema at `/graphql` in the admin UI.
|
|
1152
|
+
|
|
1153
|
+
---
|
|
1154
|
+
|
|
1155
|
+
## SDK — API Keys & Token Management
|
|
1156
|
+
|
|
1157
|
+
Generate, revoke, and manage static API tokens programmatically. Tokens are prefixed with `nvk_` and can have custom scopes, expiry dates, and IP allowlists.
|
|
1158
|
+
|
|
1159
|
+
#### Generate token for yourself
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
import { generateToken, revokeToken } from '@nivaro/sdk'
|
|
1163
|
+
|
|
1164
|
+
// Generate a new token
|
|
1165
|
+
const { data: result } = await nivaro.request(generateToken())
|
|
1166
|
+
// result.token → "nvk_abc123..." (shown only once!)
|
|
1167
|
+
|
|
1168
|
+
// Use it immediately or store in secure location
|
|
1169
|
+
console.log('Save this token:', result.token)
|
|
1170
|
+
nivaro.setToken(result.token)
|
|
1171
|
+
|
|
1172
|
+
// Later: revoke it
|
|
1173
|
+
await nivaro.request(revokeToken())
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
#### Admin: Generate token for another user
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
import { generateUserToken, revokeUserToken } from '@nivaro/sdk'
|
|
1180
|
+
|
|
1181
|
+
// Generate token for a user (admin only)
|
|
1182
|
+
const { data } = await nivaro.request(generateUserToken('user-uuid'))
|
|
1183
|
+
console.log('Token for user:', data.token)
|
|
1184
|
+
|
|
1185
|
+
// Revoke it
|
|
1186
|
+
await nivaro.request(revokeUserToken('user-uuid'))
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
#### Token configuration (API keys admin page)
|
|
1190
|
+
|
|
1191
|
+
When creating a token in the admin UI, you can configure:
|
|
1192
|
+
|
|
1193
|
+
| Setting | Description | Example |
|
|
1194
|
+
| --- | --- | --- |
|
|
1195
|
+
| Scopes | What the token can do (read, write, admin) | ["read:all", "write:articles"] |
|
|
1196
|
+
| Expires at | Expiry datetime (optional) | 2025-12-31T23:59:59Z |
|
|
1197
|
+
| IP allowlist | Restrict to specific IPs (optional) | ["203.0.113.0", "203.0.113.1"] |
|
|
1198
|
+
| Rate limit | Requests per minute (optional) | 100 |
|
|
1199
|
+
|
|
1200
|
+
> **Warning:** Token values are only returned in the API response once. After you leave the page or close the dialog, the value cannot be retrieved. Copy it to a password manager or secure store immediately.
|
|
1201
|
+
|
|
1202
|
+
> **Note:** All tokens are stored as sha256 hashes in the database — the plaintext is never persisted. Use `setToken()` to set the SDK token at runtime.
|
|
1203
|
+
|
|
1204
|
+
---
|
|
1205
|
+
|
|
1206
|
+
## SDK — Files & Upload
|
|
1207
|
+
|
|
1208
|
+
Upload files to the Nivaro file manager and get URLs for serving them. Files are stored locally by default; configure S3 or other providers in settings.
|
|
1209
|
+
|
|
1210
|
+
#### Upload a file
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
import { createNivaro } from '@nivaro/sdk'
|
|
1214
|
+
|
|
1215
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: '...' })
|
|
1216
|
+
|
|
1217
|
+
// From a file input
|
|
1218
|
+
const fileInput = document.querySelector<HTMLInputElement>('#file-input')!
|
|
1219
|
+
const file = fileInput.files![0]
|
|
1220
|
+
|
|
1221
|
+
const result = await nivaro.upload(file, {
|
|
1222
|
+
title: 'Q2 Report', // optional: display name
|
|
1223
|
+
folder: 'folder-uuid-here', // optional: folder ID
|
|
1224
|
+
})
|
|
1225
|
+
|
|
1226
|
+
// result → FileUploadResult
|
|
1227
|
+
console.log('Uploaded:', result.id, result.filesize, 'bytes')
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
#### Get file URL
|
|
1231
|
+
|
|
1232
|
+
```typescript
|
|
1233
|
+
// Generate a download URL
|
|
1234
|
+
const url = nivaro.fileUrl(fileId)
|
|
1235
|
+
// url → https://nivaro.example.com/api/files/<id>/content
|
|
1236
|
+
|
|
1237
|
+
// Display in an img tag
|
|
1238
|
+
<img src={url} alt="Report" />
|
|
1239
|
+
|
|
1240
|
+
// Or link for download
|
|
1241
|
+
<a href={url} download>Download Report</a>
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
#### FileUploadResult shape
|
|
1245
|
+
|
|
1246
|
+
| Field | Type | Description |
|
|
1247
|
+
| --- | --- | --- |
|
|
1248
|
+
| id | string | UUID primary key (use for fileUrl). |
|
|
1249
|
+
| filename_disk | string | Hashed filename on disk (for deduplication). |
|
|
1250
|
+
| filename_download | string | Original filename from upload. |
|
|
1251
|
+
| title | string | null | Custom display title. |
|
|
1252
|
+
| type | string | MIME type, e.g. "image/png", "application/pdf". |
|
|
1253
|
+
| filesize | number | File size in bytes. |
|
|
1254
|
+
| width | number | null | Image width in pixels (images only). |
|
|
1255
|
+
| height | number | null | Image height in pixels (images only). |
|
|
1256
|
+
| folder | string | null | Folder ID, or null if in root. |
|
|
1257
|
+
| uploaded_on | string | ISO 8601 upload timestamp. |
|
|
1258
|
+
|
|
1259
|
+
#### Usage in forms
|
|
1260
|
+
|
|
1261
|
+
```typescript
|
|
1262
|
+
// Upload and store file ID in a field
|
|
1263
|
+
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
1264
|
+
const file = e.target.files?.[0]
|
|
1265
|
+
if (!file) return
|
|
1266
|
+
|
|
1267
|
+
const uploaded = await nivaro.upload(file)
|
|
1268
|
+
form.setValue('attachment_id', uploaded.id) // store the ID
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// On render, show the file
|
|
1272
|
+
const fileId = form.values.attachment_id
|
|
1273
|
+
if (fileId) {
|
|
1274
|
+
const url = nivaro.fileUrl(fileId)
|
|
1275
|
+
return <a href={url}>{fileId}</a>
|
|
1276
|
+
}
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
---
|
|
1280
|
+
|
|
1281
|
+
## SDK — Realtime (Socket.io)
|
|
1282
|
+
|
|
1283
|
+
Subscribe to live events (item updates, notifications, presence) via Socket.io. The SDK wraps Socket.io with a simple subscribe/unsubscribe API.
|
|
1284
|
+
|
|
1285
|
+
#### Basic setup
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
import { createRealtime } from '@nivaro/sdk'
|
|
1289
|
+
|
|
1290
|
+
// Create and connect
|
|
1291
|
+
const rt = createRealtime()
|
|
1292
|
+
await rt.connect('https://nivaro.example.com', { token: 'nvk_...' })
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
#### Subscribe to events
|
|
1296
|
+
|
|
1297
|
+
```typescript
|
|
1298
|
+
// Subscribe to all updates on a collection
|
|
1299
|
+
const unsubscribe = rt.subscribe(
|
|
1300
|
+
'articles',
|
|
1301
|
+
{ event: 'update' },
|
|
1302
|
+
(article) => {
|
|
1303
|
+
console.log('Article updated:', article)
|
|
1304
|
+
// article → full updated item snapshot
|
|
1305
|
+
}
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
// Subscribe to updates on a specific item
|
|
1309
|
+
rt.subscribe(
|
|
1310
|
+
'articles:123',
|
|
1311
|
+
{ event: 'update' },
|
|
1312
|
+
(article) => console.log('This article changed')
|
|
1313
|
+
)
|
|
1314
|
+
|
|
1315
|
+
// Subscribe to your notifications
|
|
1316
|
+
rt.subscribe(
|
|
1317
|
+
`user:${userId}`,
|
|
1318
|
+
{ event: 'notification:new' },
|
|
1319
|
+
(notif) => console.log('New notification:', notif.subject)
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
// Subscribe to presence changes on an item
|
|
1323
|
+
rt.subscribe(
|
|
1324
|
+
'articles:123',
|
|
1325
|
+
{ event: 'presence' },
|
|
1326
|
+
(users) => console.log(`${users.length} users viewing`)
|
|
1327
|
+
)
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
#### Event types
|
|
1331
|
+
|
|
1332
|
+
| Event | Fires On | Data |
|
|
1333
|
+
| --- | --- | --- |
|
|
1334
|
+
| create | New item added | Full item snapshot |
|
|
1335
|
+
| update | Item field changed | Full item snapshot (with updated values) |
|
|
1336
|
+
| delete | Item deleted | Item ID and deletion timestamp |
|
|
1337
|
+
| notification:new | Notification created | Notification object |
|
|
1338
|
+
| presence | User joins/leaves/edits | Array of currently viewing users |
|
|
1339
|
+
|
|
1340
|
+
#### Unsubscribe and disconnect
|
|
1341
|
+
|
|
1342
|
+
```typescript
|
|
1343
|
+
// Unsubscribe from a room
|
|
1344
|
+
unsubscribe()
|
|
1345
|
+
|
|
1346
|
+
// Disconnect from server (closes all subscriptions)
|
|
1347
|
+
rt.disconnect()
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
> **Note:** Socket.io is multiplexed over the same URL as the API (e.g., https://nivaro.example.com). No separate server configuration needed. The Redis pub/sub adapter means events propagate across all server replicas.
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1354
|
+
## SDK — Filter Helpers
|
|
1355
|
+
|
|
1356
|
+
The SDK exports operator helper functions that make filters type-safe and readable.
|
|
1357
|
+
|
|
1358
|
+
```typescript
|
|
1359
|
+
import {
|
|
1360
|
+
_eq, _neq, _gt, _gte, _lt, _lte,
|
|
1361
|
+
_in, _nin, _null, _nnull,
|
|
1362
|
+
_contains, _ncontains, _starts_with, _ends_with,
|
|
1363
|
+
_and, _or, _some, _none,
|
|
1364
|
+
asc, desc
|
|
1365
|
+
} from '@nivaro/sdk'
|
|
1366
|
+
|
|
1367
|
+
// Scalar field conditions
|
|
1368
|
+
const filter = {
|
|
1369
|
+
status: _in(['active', 'draft']),
|
|
1370
|
+
amount: _gt(1000),
|
|
1371
|
+
deleted_at: _null(),
|
|
1372
|
+
name: _contains('fiber'),
|
|
1373
|
+
email: _ends_with('@nivaro.dev'),
|
|
1374
|
+
title: _ncontains('archived'),
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Logical combinators
|
|
1378
|
+
const combined = _and(
|
|
1379
|
+
{ status: _eq('active') },
|
|
1380
|
+
_or({ region: _eq('East') }, { region: _eq('West') })
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
// Relation filters — O2M / M2M
|
|
1384
|
+
const withTags = {
|
|
1385
|
+
tags: _some({ name: _eq('featured') }), // at least one tag named "featured"
|
|
1386
|
+
approvals: _none({ status: _eq('rejected') }), // no rejected approvals
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Sort helpers
|
|
1390
|
+
const items = await nivaro.request(
|
|
1391
|
+
readItems('projects', {
|
|
1392
|
+
filter: combined,
|
|
1393
|
+
sort: [asc('region.short_name'), desc('created_at')],
|
|
1394
|
+
})
|
|
1395
|
+
)
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
#### Scalar operators
|
|
1399
|
+
|
|
1400
|
+
| Helper | SQL | Notes |
|
|
1401
|
+
| --- | --- | --- |
|
|
1402
|
+
| _eq(v) | = v | Exact equality. |
|
|
1403
|
+
| _neq(v) | != v | Not equal. |
|
|
1404
|
+
| _gt(v) | > v | |
|
|
1405
|
+
| _gte(v) | >= v | |
|
|
1406
|
+
| _lt(v) | < v | |
|
|
1407
|
+
| _lte(v) | <= v | |
|
|
1408
|
+
| _in(arr) | IN (...) | Array of values. |
|
|
1409
|
+
| _nin(arr) | NOT IN (...) | |
|
|
1410
|
+
| _null() | IS NULL | |
|
|
1411
|
+
| _nnull() | IS NOT NULL | |
|
|
1412
|
+
| _contains(s) | LIKE %s% | Substring match. |
|
|
1413
|
+
| _ncontains(s) | NOT LIKE %s% | Substring exclusion. |
|
|
1414
|
+
| _starts_with(s) | LIKE s% | Prefix match. |
|
|
1415
|
+
| _ends_with(s) | LIKE %s | Suffix match. |
|
|
1416
|
+
|
|
1417
|
+
#### Logical & relation operators
|
|
1418
|
+
|
|
1419
|
+
| Helper | Type | Notes |
|
|
1420
|
+
| --- | --- | --- |
|
|
1421
|
+
| _and(...clauses) | Logical | All clauses must match. |
|
|
1422
|
+
| _or(...clauses) | Logical | At least one clause must match. |
|
|
1423
|
+
| _some(filter) | Relation | At least one related record matches filter. |
|
|
1424
|
+
| _none(filter) | Relation | No related records match filter. |
|
|
1425
|
+
|
|
1426
|
+
#### Sort helpers
|
|
1427
|
+
|
|
1428
|
+
| Helper | Example | Notes |
|
|
1429
|
+
| --- | --- | --- |
|
|
1430
|
+
| asc(field) | asc('created_at') | Ascending. Dotted paths for M2O: asc('region.short_name'). |
|
|
1431
|
+
| desc(field) | desc('amount') | Descending. |
|
|
1432
|
+
|
|
1433
|
+
---
|
|
1434
|
+
|
|
1435
|
+
## SDK — Comments & Mentions
|
|
1436
|
+
|
|
1437
|
+
Threaded comments on any record. Users can mention other users via @username syntax — mentioned users receive in-app notifications automatically.
|
|
1438
|
+
|
|
1439
|
+
```typescript
|
|
1440
|
+
import { readComments, createComment, updateComment, deleteComment } from '@nivaro/sdk'
|
|
1441
|
+
|
|
1442
|
+
// All comments on an item (oldest first)
|
|
1443
|
+
const { data: comments } = await nivaro.request(
|
|
1444
|
+
readComments('projects', itemId)
|
|
1445
|
+
)
|
|
1446
|
+
// comments → Comment[] — { id, text, user_id, first_name, last_name, created_at, mentions: [{ user_id, first_name }] }
|
|
1447
|
+
|
|
1448
|
+
// Create a comment with @mentions
|
|
1449
|
+
const { data: comment } = await nivaro.request(
|
|
1450
|
+
createComment({
|
|
1451
|
+
collection: 'projects',
|
|
1452
|
+
item: itemId,
|
|
1453
|
+
text: '@jane Please review when you get a chance. Thanks @bob!',
|
|
1454
|
+
})
|
|
1455
|
+
)
|
|
1456
|
+
// @mentions are parsed from text; mentioned users get notifications
|
|
1457
|
+
|
|
1458
|
+
// Update own comment
|
|
1459
|
+
await nivaro.request(
|
|
1460
|
+
updateComment(comment.id, { text: 'Updated — needs urgent review' })
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
// Delete own comment (or admin delete any)
|
|
1464
|
+
await nivaro.request(deleteComment(comment.id))
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
| Function | Route | Auth |
|
|
1468
|
+
| --- | --- | --- |
|
|
1469
|
+
| readComments(collection, item) | GET /comments | Authenticated |
|
|
1470
|
+
| createComment(body) | POST /comments | Authenticated |
|
|
1471
|
+
| updateComment(id, body) | PATCH /comments/:id | Authenticated (own) |
|
|
1472
|
+
| deleteComment(id) | DELETE /comments/:id | Authenticated (own or admin) |
|
|
1473
|
+
|
|
1474
|
+
> **Note:** Mention syntax: `@firstname-lastname` (from the user directory). The server parses mentions from the text and sends notifications to all mentioned users. Mention list is also returned in the response for UI highlighting.
|
|
1475
|
+
|
|
1476
|
+
---
|
|
1477
|
+
|
|
1478
|
+
## SDK — Webhooks
|
|
1479
|
+
|
|
1480
|
+
HTTP webhooks fire when collection items are created, updated, or deleted. Payloads include the full item snapshot + delta for updates. Deliveries are retried automatically with exponential backoff.
|
|
1481
|
+
|
|
1482
|
+
```typescript
|
|
1483
|
+
import {
|
|
1484
|
+
readWebhooks, readWebhook,
|
|
1485
|
+
createWebhook, updateWebhook, deleteWebhook, testWebhook,
|
|
1486
|
+
} from '@nivaro/sdk'
|
|
1487
|
+
|
|
1488
|
+
// List all webhooks
|
|
1489
|
+
const { data: webhooks } = await nivaro.request(readWebhooks())
|
|
1490
|
+
|
|
1491
|
+
// Create a webhook
|
|
1492
|
+
const { data: wh } = await nivaro.request(
|
|
1493
|
+
createWebhook({
|
|
1494
|
+
name: 'Slack notifications',
|
|
1495
|
+
url: 'https://hooks.slack.com/services/xxx/yyy/zzz',
|
|
1496
|
+
collection: 'projects',
|
|
1497
|
+
events: ['create', 'update'], // or 'delete'
|
|
1498
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
1499
|
+
enabled: true,
|
|
1500
|
+
})
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
// Update
|
|
1504
|
+
await nivaro.request(updateWebhook(wh.id, { enabled: false }))
|
|
1505
|
+
|
|
1506
|
+
// Test delivery (fire one immediately to verify endpoint is working)
|
|
1507
|
+
const { data: testResult } = await nivaro.request(testWebhook(wh.id))
|
|
1508
|
+
console.log(testResult.status, testResult.response_time)
|
|
1509
|
+
|
|
1510
|
+
// Delete
|
|
1511
|
+
await nivaro.request(deleteWebhook(wh.id))
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
#### Payload shape
|
|
1515
|
+
|
|
1516
|
+
```typescript
|
|
1517
|
+
{
|
|
1518
|
+
"event": "item.create",
|
|
1519
|
+
"collection": "projects",
|
|
1520
|
+
"item": {
|
|
1521
|
+
"id": "123",
|
|
1522
|
+
"name": "New Project",
|
|
1523
|
+
"status": "draft",
|
|
1524
|
+
...
|
|
1525
|
+
},
|
|
1526
|
+
"delta": {
|
|
1527
|
+
// For updates only — fields that changed
|
|
1528
|
+
"status": "draft"
|
|
1529
|
+
},
|
|
1530
|
+
"timestamp": "2024-06-14T10:30:00Z",
|
|
1531
|
+
"user_id": "user-uuid",
|
|
1532
|
+
"delivery_id": "delivery-uuid"
|
|
1533
|
+
}
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
| Function | Route | Auth |
|
|
1537
|
+
| --- | --- | --- |
|
|
1538
|
+
| readWebhooks() | GET /webhooks | Admin |
|
|
1539
|
+
| readWebhook(id) | GET /webhooks/:id | Admin |
|
|
1540
|
+
| createWebhook(data) | POST /webhooks | Admin |
|
|
1541
|
+
| updateWebhook(id, data) | PATCH /webhooks/:id | Admin |
|
|
1542
|
+
| deleteWebhook(id) | DELETE /webhooks/:id | Admin |
|
|
1543
|
+
| testWebhook(id) | POST /webhooks/:id/test | Admin |
|
|
1544
|
+
|
|
1545
|
+
> **Note:** Webhooks are fired asynchronously after the item is committed to the database. Failures are retried up to 5 times with exponential backoff (1s, 2s, 4s, 8s, 16s). See the Webhooks admin page to view delivery history and retry failed deliveries.
|
|
1546
|
+
|
|
1547
|
+
---
|
|
1548
|
+
|
|
1549
|
+
## SDK — Rules & Automation
|
|
1550
|
+
|
|
1551
|
+
Automations (rules) run server-side on create/update/delete. Define conditions (field values that must match) and actions (notify, set field, reject, trigger external system). All actions run transactionally — if any action fails, the entire operation is rolled back.
|
|
1552
|
+
|
|
1553
|
+
```typescript
|
|
1554
|
+
import { readRules, createRule, updateRule, deleteRule } from '@nivaro/sdk'
|
|
1555
|
+
|
|
1556
|
+
// List rules for a collection
|
|
1557
|
+
const { data: rules } = await nivaro.request(readRules('orders'))
|
|
1558
|
+
|
|
1559
|
+
// Create a rule
|
|
1560
|
+
const { data: rule } = await nivaro.request(
|
|
1561
|
+
createRule({
|
|
1562
|
+
name: 'Auto-escalate urgent orders',
|
|
1563
|
+
collection: 'orders',
|
|
1564
|
+
trigger: 'create', // or 'update' or 'delete'
|
|
1565
|
+
conditions: [
|
|
1566
|
+
{ field: 'priority', operator: '_eq', value: 'urgent' }
|
|
1567
|
+
],
|
|
1568
|
+
actions: [
|
|
1569
|
+
{ type: 'notify', recipient_user_id: 'manager-uuid', subject: 'Urgent order' },
|
|
1570
|
+
{ type: 'set_field', field: 'escalated', value: true },
|
|
1571
|
+
],
|
|
1572
|
+
enabled: true,
|
|
1573
|
+
})
|
|
1574
|
+
)
|
|
1575
|
+
|
|
1576
|
+
// Disable a rule temporarily
|
|
1577
|
+
await nivaro.request(updateRule(rule.id, { enabled: false }))
|
|
1578
|
+
|
|
1579
|
+
// Delete
|
|
1580
|
+
await nivaro.request(deleteRule(rule.id))
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
#### Condition operators
|
|
1584
|
+
|
|
1585
|
+
| Operator | Meaning | Example |
|
|
1586
|
+
| --- | --- | --- |
|
|
1587
|
+
| _eq | Equal | status _eq "urgent" |
|
|
1588
|
+
| _neq | Not equal | status _neq "draft" |
|
|
1589
|
+
| _gt | Greater than | amount _gt 1000 |
|
|
1590
|
+
| _gte | Greater or equal | amount _gte 500 |
|
|
1591
|
+
| _lt | Less than | age _lt 18 |
|
|
1592
|
+
| _lte | Less or equal | age _lte 65 |
|
|
1593
|
+
| _in | In set | status _in ["urgent", "high"] |
|
|
1594
|
+
| _empty | Is null/empty | description _empty null |
|
|
1595
|
+
| _contains | Substring | name _contains "test" |
|
|
1596
|
+
| _starts_with | Prefix | email _starts_with "admin" |
|
|
1597
|
+
|
|
1598
|
+
#### Action types
|
|
1599
|
+
|
|
1600
|
+
| Action | Fields | Description |
|
|
1601
|
+
| --- | --- | --- |
|
|
1602
|
+
| notify | recipient_user_id, subject, body | Send in-app notification. |
|
|
1603
|
+
| set_field | field, value | Auto-set a field (no auth bypass — must be writable). |
|
|
1604
|
+
| reject | error_message | Block the save and return an error to the user. |
|
|
1605
|
+
| trigger_external | external_api_id, path, method, body | Call an external API (credentials stay server-side). |
|
|
1606
|
+
|
|
1607
|
+
| Function | Route | Auth |
|
|
1608
|
+
| --- | --- | --- |
|
|
1609
|
+
| readRules(collection?) | GET /rules | Admin |
|
|
1610
|
+
| createRule(data) | POST /rules | Admin |
|
|
1611
|
+
| updateRule(id, data) | PATCH /rules/:id | Admin |
|
|
1612
|
+
| deleteRule(id) | DELETE /rules/:id | Admin |
|
|
1613
|
+
|
|
1614
|
+
> **Note:** All conditions in a rule must be true (AND logic) for actions to fire. Actions execute in order. If a "reject" action runs, no subsequent actions run and the item is not saved.
|
|
1615
|
+
|
|
1616
|
+
---
|
|
1617
|
+
|
|
1618
|
+
## SDK — Flow Runs
|
|
1619
|
+
|
|
1620
|
+
Flows are Inngest-backed automations (schedules, webhooks, manual triggers). View execution history, logs, and errors via the SDK.
|
|
1621
|
+
|
|
1622
|
+
```typescript
|
|
1623
|
+
import { readFlowRuns, readFlowRun, triggerFlowRun } from '@nivaro/sdk'
|
|
1624
|
+
|
|
1625
|
+
// List all runs for a flow (newest first)
|
|
1626
|
+
const { data: runs } = await nivaro.request(
|
|
1627
|
+
readFlowRuns(flowId, { limit: 50, status: 'error' })
|
|
1628
|
+
)
|
|
1629
|
+
// runs → Run[] — { id, status, started_at, completed_at, duration_ms, output, error_message }
|
|
1630
|
+
|
|
1631
|
+
// Single run detail with step logs
|
|
1632
|
+
const { data: run } = await nivaro.request(readFlowRun(runId))
|
|
1633
|
+
console.log(run.status) // 'running' | 'success' | 'error'
|
|
1634
|
+
console.log(run.error_message) // if status === 'error'
|
|
1635
|
+
|
|
1636
|
+
// Trigger a flow immediately (bypasses schedule)
|
|
1637
|
+
const { data: run } = await nivaro.request(
|
|
1638
|
+
triggerFlowRun(flowId, { payload: { action: 'review', user_id: '123' } })
|
|
1639
|
+
)
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
| Function | Route | Auth |
|
|
1643
|
+
| --- | --- | --- |
|
|
1644
|
+
| readFlowRuns(flowId, opts?) | GET /flows/:id/runs | Admin |
|
|
1645
|
+
| readFlowRun(runId) | GET /flows/runs/:id | Admin |
|
|
1646
|
+
| triggerFlowRun(flowId, payload?) | POST /flows/:id/trigger | Admin |
|
|
1647
|
+
|
|
1648
|
+
> **Note:** Flows are created and configured in the admin UI at `/flows`. They support cron schedules, manual triggers, webhook triggers, and event-driven execution. Each flow run produces logs for every step — view them in the admin UI or via the GraphQL API.
|
|
1649
|
+
|
|
1650
|
+
---
|
|
1651
|
+
|
|
1652
|
+
## SDK — Custom Queries
|
|
1653
|
+
|
|
1654
|
+
Custom queries are parameterized SQL endpoints defined by admins. Use them for complex analytics, cross-collection joins, and aggregations that the standard filter DSL cannot express.
|
|
1655
|
+
|
|
1656
|
+
```typescript
|
|
1657
|
+
import { readCustomQueries, executeCustomQuery } from '@nivaro/sdk'
|
|
1658
|
+
|
|
1659
|
+
// List all queries visible to the current user
|
|
1660
|
+
const { data: queries } = await nivaro.request(readCustomQueries())
|
|
1661
|
+
// queries → Query[] — { id, name, slug, description, access, cache_ttl, params }
|
|
1662
|
+
|
|
1663
|
+
// Execute a query by slug with parameters
|
|
1664
|
+
const { data: rows, cached, executed_at } = await nivaro.request(
|
|
1665
|
+
executeCustomQuery('high-value-deals', { region: 'West', min_amount: 50000 })
|
|
1666
|
+
)
|
|
1667
|
+
// rows → Record[] — raw query results (any shape, depends on the query)
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
#### Example: Defining a custom query
|
|
1671
|
+
|
|
1672
|
+
In the admin UI at `/custom-queries`, create a query:
|
|
1673
|
+
|
|
1674
|
+
```typescript
|
|
1675
|
+
-- Name: High-value deals
|
|
1676
|
+
-- Access: authenticated
|
|
1677
|
+
|
|
1678
|
+
SELECT
|
|
1679
|
+
d.id,
|
|
1680
|
+
d.name,
|
|
1681
|
+
d.amount,
|
|
1682
|
+
d.region,
|
|
1683
|
+
u.first_name,
|
|
1684
|
+
u.last_name,
|
|
1685
|
+
COUNT(i.id) as item_count
|
|
1686
|
+
FROM deals d
|
|
1687
|
+
LEFT JOIN users u ON u.id = d.owner_id
|
|
1688
|
+
LEFT JOIN items i ON i.deal_id = d.id
|
|
1689
|
+
WHERE d.region = @region
|
|
1690
|
+
AND d.amount >= @min_amount
|
|
1691
|
+
GROUP BY d.id, d.name, d.amount, d.region, u.first_name, u.last_name
|
|
1692
|
+
ORDER BY d.amount DESC
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
#### Parameters
|
|
1696
|
+
|
|
1697
|
+
| Name | Type | Required | Default |
|
|
1698
|
+
| --- | --- | --- | --- |
|
|
1699
|
+
| region | string | true | N/A |
|
|
1700
|
+
| min_amount | number | false | 0 |
|
|
1701
|
+
|
|
1702
|
+
| Function | Route | Auth |
|
|
1703
|
+
| --- | --- | --- |
|
|
1704
|
+
| readCustomQueries() | GET /custom-queries | Authenticated |
|
|
1705
|
+
| executeCustomQuery(slug, params?) | POST /custom-queries/:slug/execute | Per-query setting |
|
|
1706
|
+
|
|
1707
|
+
> **Note:** All parameters are bound safely via MSSQL parameterized queries (@name syntax). Never string-interpolate user input into custom queries. Results can be cached via the TTL setting.
|
|
1708
|
+
|
|
1709
|
+
---
|
|
1710
|
+
|
|
1711
|
+
## SDK — Collections & Schema
|
|
1712
|
+
|
|
1713
|
+
Read collection metadata and field definitions from the registry. Useful for building dynamic UIs, form generators, and schema explorers.
|
|
1714
|
+
|
|
1715
|
+
```typescript
|
|
1716
|
+
import { readCollections, readCollection, readFields } from '@nivaro/sdk'
|
|
1717
|
+
|
|
1718
|
+
// List all collections visible to the current user
|
|
1719
|
+
const { data: collections } = await nivaro.request(readCollections())
|
|
1720
|
+
// collections → Collection[] — { id, collection, label, icon, sort_field, display_template }
|
|
1721
|
+
|
|
1722
|
+
// Single collection metadata
|
|
1723
|
+
const { data: col } = await nivaro.request(readCollection('orders'))
|
|
1724
|
+
// col → { id, collection: "orders", label: "Orders", primary_key_field: "id", ... }
|
|
1725
|
+
|
|
1726
|
+
// All fields in a collection
|
|
1727
|
+
const { data: fields } = await nivaro.request(readFields('orders'))
|
|
1728
|
+
// fields → Field[] — { key, type, interface, required, sort, hidden, ... }
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
#### Collection metadata
|
|
1732
|
+
|
|
1733
|
+
| Property | Type | Description |
|
|
1734
|
+
| --- | --- | --- |
|
|
1735
|
+
| id | string | UUID. |
|
|
1736
|
+
| collection | string | Slug used in API paths. |
|
|
1737
|
+
| label | string | Display name. |
|
|
1738
|
+
| icon | string | Icon class name. |
|
|
1739
|
+
| primary_key_field | string | Usually "id". |
|
|
1740
|
+
| sort_field | string | null | Default sort column. |
|
|
1741
|
+
| display_template | string | null | Handlebars for rendering rows (e.g., "{{ name }} ({{ status }})"). |
|
|
1742
|
+
|
|
1743
|
+
#### Field metadata
|
|
1744
|
+
|
|
1745
|
+
| Property | Type | Description |
|
|
1746
|
+
| --- | --- | --- |
|
|
1747
|
+
| key | string | Field name. |
|
|
1748
|
+
| label | string | Display label. |
|
|
1749
|
+
| type | string | text | number | boolean | date | select | etc. |
|
|
1750
|
+
| interface | string | UI hint: text-input | textarea | toggle | date-picker | etc. |
|
|
1751
|
+
| required | boolean | If true, must have a value. |
|
|
1752
|
+
| sort | number | null | Display order. |
|
|
1753
|
+
| hidden | boolean | If true, hidden from UI (but readable via API). |
|
|
1754
|
+
| computed_formula | string | null | If set, field is auto-calculated (read-only). |
|
|
1755
|
+
|
|
1756
|
+
| Function | Route | Auth |
|
|
1757
|
+
| --- | --- | --- |
|
|
1758
|
+
| readCollections() | GET /collections | Authenticated |
|
|
1759
|
+
| readCollection(collection) | GET /collections/:collection | Authenticated |
|
|
1760
|
+
| readFields(collection) | GET /fields/:collection | Authenticated |
|
|
1761
|
+
|
|
1762
|
+
---
|
|
1763
|
+
|
|
1764
|
+
## SDK — Blackout Dates
|
|
1765
|
+
|
|
1766
|
+
Blackout date ranges (e.g., holidays, maintenance windows) pause scheduled flows, SLA timers, and other time-based automations. Mark days as blackout and time is not counted.
|
|
1767
|
+
|
|
1768
|
+
```typescript
|
|
1769
|
+
import {
|
|
1770
|
+
readBlackoutDates, checkBlackoutDate,
|
|
1771
|
+
createBlackoutDate, deleteBlackoutDate
|
|
1772
|
+
} from '@nivaro/sdk'
|
|
1773
|
+
|
|
1774
|
+
// List all blackout dates
|
|
1775
|
+
const { data: dates } = await nivaro.request(readBlackoutDates())
|
|
1776
|
+
// dates → Blackout[] — { id, name, start_date, end_date, is_active }
|
|
1777
|
+
|
|
1778
|
+
// Check if a specific date is blacked out
|
|
1779
|
+
const { isBlackout, reason } = await nivaro.request(
|
|
1780
|
+
checkBlackoutDate('2025-12-25')
|
|
1781
|
+
)
|
|
1782
|
+
console.log(isBlackout) // true if in any active blackout range
|
|
1783
|
+
|
|
1784
|
+
// Create a blackout range
|
|
1785
|
+
const { data: bd } = await nivaro.request(
|
|
1786
|
+
createBlackoutDate({
|
|
1787
|
+
name: 'Winter Shutdown',
|
|
1788
|
+
description: 'No deployments',
|
|
1789
|
+
start_date: '2024-12-20',
|
|
1790
|
+
end_date: '2025-01-02',
|
|
1791
|
+
is_active: true,
|
|
1792
|
+
})
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
// Delete
|
|
1796
|
+
await nivaro.request(deleteBlackoutDate(bd.id))
|
|
1797
|
+
```
|
|
1798
|
+
|
|
1799
|
+
#### Usage in time-based systems
|
|
1800
|
+
|
|
1801
|
+
When SLA rules, scheduled flows, or other time-tracking systems are active:
|
|
1802
|
+
|
|
1803
|
+
- Time during blackout windows is not counted.
|
|
1804
|
+
- Timers pause on the first second of a blackout and resume on the first second after.
|
|
1805
|
+
- Business hours SLA settings (e.g., "Mon-Fri 09:00-17:00") are multiplied by the business hours factor within a blackout window.
|
|
1806
|
+
|
|
1807
|
+
| Function | Route | Auth |
|
|
1808
|
+
| --- | --- | --- |
|
|
1809
|
+
| readBlackoutDates() | GET /blackout-dates | Authenticated |
|
|
1810
|
+
| checkBlackoutDate(date) | GET /blackout-dates/check | Authenticated |
|
|
1811
|
+
| createBlackoutDate(body) | POST /blackout-dates | Admin |
|
|
1812
|
+
| deleteBlackoutDate(id) | DELETE /blackout-dates/:id | Admin |
|
|
1813
|
+
|
|
1814
|
+
---
|
|
1815
|
+
|
|
1816
|
+
## SDK — Schema Snapshots
|
|
1817
|
+
|
|
1818
|
+
Capture point-in-time snapshots of your entire metadata registry (collections, fields, relations). Useful for version control, environment promotion, and disaster recovery.
|
|
1819
|
+
|
|
1820
|
+
```typescript
|
|
1821
|
+
import { readSchemaSnapshots, createSchemaSnapshot, restoreSchemaSnapshot } from '@nivaro/sdk'
|
|
1822
|
+
|
|
1823
|
+
// List all snapshots
|
|
1824
|
+
const { data: snapshots } = await nivaro.request(readSchemaSnapshots())
|
|
1825
|
+
// snapshots → Snapshot[] — { id, name, created_at, collection_count, field_count }
|
|
1826
|
+
|
|
1827
|
+
// Capture a snapshot
|
|
1828
|
+
const { data: snap } = await nivaro.request(
|
|
1829
|
+
createSchemaSnapshot({
|
|
1830
|
+
name: 'Before v2.0 migration',
|
|
1831
|
+
description: 'Backup of schema before major refactoring',
|
|
1832
|
+
})
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
// Restore a snapshot (rolls back collections/fields to that point in time)
|
|
1836
|
+
await nivaro.request(restoreSchemaSnapshot(snap.id))
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
#### What gets snapshotted
|
|
1840
|
+
|
|
1841
|
+
- All collections (metadata, settings, display templates)
|
|
1842
|
+
- All fields (types, interfaces, validation rules, computed formulas)
|
|
1843
|
+
- All relations (M2O, O2M, M2M, M2A)
|
|
1844
|
+
- Field groups, layouts, and assignments
|
|
1845
|
+
- Collection-level settings (draft/publish, item locking, etc.)
|
|
1846
|
+
|
|
1847
|
+
#### What does NOT get snapshotted
|
|
1848
|
+
|
|
1849
|
+
- Item data (rows) — only schema
|
|
1850
|
+
- Workflows, pipelines, rules, or other business logic
|
|
1851
|
+
- User permissions or role assignments
|
|
1852
|
+
- Custom queries, external APIs, or webhooks
|
|
1853
|
+
|
|
1854
|
+
| Function | Route | Auth |
|
|
1855
|
+
| --- | --- | --- |
|
|
1856
|
+
| readSchemaSnapshots() | GET /schema-snapshot | Admin |
|
|
1857
|
+
| createSchemaSnapshot(body) | POST /schema-snapshot | Admin |
|
|
1858
|
+
| restoreSchemaSnapshot(id) | POST /schema-snapshot/:id/restore | Admin |
|
|
1859
|
+
|
|
1860
|
+
> **Warning:** Restoring a snapshot overwrites the current schema. It does NOT roll back data. Always back up your database before restoring.
|
|
1861
|
+
|
|
1862
|
+
---
|
|
1863
|
+
|
|
1864
|
+
## SDK — Alerts & Monitoring
|
|
1865
|
+
|
|
1866
|
+
Define threshold-based or anomaly-detection alerts on collection fields. Monitor values in real-time and notify users when conditions are met. All users can subscribe to alerts relevant to their work.
|
|
1867
|
+
|
|
1868
|
+
#### Admin: Create alert definitions
|
|
1869
|
+
|
|
1870
|
+
```typescript
|
|
1871
|
+
import { readAlertDefinitions, createAlertDefinition, updateAlertDefinition, deleteAlertDefinition } from '@nivaro/sdk'
|
|
1872
|
+
|
|
1873
|
+
// List all alert definitions
|
|
1874
|
+
const { data: defs } = await nivaro.request(readAlertDefinitions())
|
|
1875
|
+
|
|
1876
|
+
// Create a threshold alert
|
|
1877
|
+
const { data: alert } = await nivaro.request(
|
|
1878
|
+
createAlertDefinition({
|
|
1879
|
+
name: 'High-value order',
|
|
1880
|
+
category: 'threshold', // or 'anomaly'
|
|
1881
|
+
collection: 'orders',
|
|
1882
|
+
field: 'total_amount',
|
|
1883
|
+
operator: 'gt', // _eq, _neq, _gt, _gte, _lt, _lte, _in, _contains
|
|
1884
|
+
threshold: 100000,
|
|
1885
|
+
unit: 'USD',
|
|
1886
|
+
cooldown_minutes: 60, // prevent alert spam
|
|
1887
|
+
is_active: true,
|
|
1888
|
+
})
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
// Update alert
|
|
1892
|
+
await nivaro.request(updateAlertDefinition(alert.id, { is_active: false }))
|
|
1893
|
+
|
|
1894
|
+
// Delete
|
|
1895
|
+
await nivaro.request(deleteAlertDefinition(alert.id))
|
|
1896
|
+
```
|
|
1897
|
+
|
|
1898
|
+
#### User: Subscribe to alerts
|
|
1899
|
+
|
|
1900
|
+
```typescript
|
|
1901
|
+
import { readAlertSubscriptions, createAlertSubscription, deleteAlertSubscription } from '@nivaro/sdk'
|
|
1902
|
+
|
|
1903
|
+
// List alerts you are subscribed to
|
|
1904
|
+
const { data: subs } = await nivaro.request(readAlertSubscriptions())
|
|
1905
|
+
// subs → Subscription[] — { id, alert_id, notify_inapp, notify_email }
|
|
1906
|
+
|
|
1907
|
+
// Subscribe to an alert
|
|
1908
|
+
const { data: sub } = await nivaro.request(
|
|
1909
|
+
createAlertSubscription({
|
|
1910
|
+
alert_definition_id: alertId,
|
|
1911
|
+
notify_inapp: true, // in-app bell notification
|
|
1912
|
+
notify_email: false, // email digest
|
|
1913
|
+
})
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
// Unsubscribe
|
|
1917
|
+
await nivaro.request(deleteAlertSubscription(sub.id))
|
|
1918
|
+
```
|
|
1919
|
+
|
|
1920
|
+
#### Admin: View alert log and manually evaluate
|
|
1921
|
+
|
|
1922
|
+
```typescript
|
|
1923
|
+
import { readAlertLog, evaluateAlerts } from '@nivaro/sdk'
|
|
1924
|
+
|
|
1925
|
+
// Last 100 alert firings for a definition
|
|
1926
|
+
const { data: log } = await nivaro.request(readAlertLog(alertId))
|
|
1927
|
+
// log → Firing[] — { id, triggered_at, collection, item_id, field_value }
|
|
1928
|
+
|
|
1929
|
+
// Trigger immediate evaluation (normally runs every 5 minutes)
|
|
1930
|
+
await nivaro.request(evaluateAlerts())
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1933
|
+
#### Alert operators
|
|
1934
|
+
|
|
1935
|
+
| Operator | Meaning | Example |
|
|
1936
|
+
| --- | --- | --- |
|
|
1937
|
+
| _eq | Equal | status == "overdue" |
|
|
1938
|
+
| _neq | Not equal | status != "active" |
|
|
1939
|
+
| _gt | Greater than | amount > 100000 |
|
|
1940
|
+
| _gte | Greater or equal | age >= 65 |
|
|
1941
|
+
| _lt | Less than | days_remaining < 0 |
|
|
1942
|
+
| _lte | Less or equal | days_remaining <= 7 |
|
|
1943
|
+
| _in | In set | status in ("failed", "cancelled") |
|
|
1944
|
+
| _contains | Contains substring | email contains "@spam" |
|
|
1945
|
+
|
|
1946
|
+
| Command | Route | Auth |
|
|
1947
|
+
| --- | --- | --- |
|
|
1948
|
+
| readAlertDefinitions(collection?) | GET /alerts/definitions | Admin |
|
|
1949
|
+
| createAlertDefinition(body) | POST /alerts/definitions | Admin |
|
|
1950
|
+
| updateAlertDefinition(id, body) | PATCH /alerts/definitions/:id | Admin |
|
|
1951
|
+
| deleteAlertDefinition(id) | DELETE /alerts/definitions/:id | Admin |
|
|
1952
|
+
| readAlertSubscriptions() | GET /alerts/subscriptions | Authenticated |
|
|
1953
|
+
| createAlertSubscription(body) | POST /alerts/subscriptions | Authenticated |
|
|
1954
|
+
| deleteAlertSubscription(id) | DELETE /alerts/subscriptions/:id | Authenticated |
|
|
1955
|
+
| readAlertLog(alertId?) | GET /alerts/log | Admin |
|
|
1956
|
+
| evaluateAlerts() | POST /alerts/evaluate | Admin |
|
|
1957
|
+
|
|
1958
|
+
> **Note:** Alerts are evaluated every 5 minutes by an Inngest cron job. Anomaly detection uses statistical analysis (standard deviation multipliers) to identify unusual values. Cooldown prevents the same alert from firing multiple times within a time window.
|
|
1959
|
+
|
|
1960
|
+
---
|
|
1961
|
+
|
|
1962
|
+
## SDK — Dynamic Attributes
|
|
1963
|
+
|
|
1964
|
+
Dynamic attributes (EAV — Entity-Attribute-Value) let you attach arbitrary custom fields to any collection without modifying the database schema. Useful for extensibility, multi-tenant customization, and handling one-off custom fields.
|
|
1965
|
+
|
|
1966
|
+
#### Admin: Define attribute types
|
|
1967
|
+
|
|
1968
|
+
```typescript
|
|
1969
|
+
import {
|
|
1970
|
+
readAttributeDefinitions, createAttributeDefinition,
|
|
1971
|
+
updateAttributeDefinition, deleteAttributeDefinition,
|
|
1972
|
+
} from '@nivaro/sdk'
|
|
1973
|
+
|
|
1974
|
+
// List all attribute definitions for a collection
|
|
1975
|
+
const { data: defs } = await nivaro.request(readAttributeDefinitions('projects'))
|
|
1976
|
+
|
|
1977
|
+
// Create a new attribute type
|
|
1978
|
+
const { data: def } = await nivaro.request(
|
|
1979
|
+
createAttributeDefinition('projects', {
|
|
1980
|
+
key: 'risk_level', // slug used in API calls
|
|
1981
|
+
label: 'Risk Level', // display name
|
|
1982
|
+
type: 'select', // text | number | boolean | date | select
|
|
1983
|
+
options: ['low', 'medium', 'high'], // required for type='select'
|
|
1984
|
+
required: false,
|
|
1985
|
+
sort: 1,
|
|
1986
|
+
})
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
// Update definition
|
|
1990
|
+
await nivaro.request(updateAttributeDefinition(def.id, { required: true }))
|
|
1991
|
+
|
|
1992
|
+
// Delete (also cleans up orphaned values)
|
|
1993
|
+
await nivaro.request(deleteAttributeDefinition(def.id))
|
|
1994
|
+
```
|
|
1995
|
+
|
|
1996
|
+
#### User: Read and write values
|
|
1997
|
+
|
|
1998
|
+
```typescript
|
|
1999
|
+
import { readAttributes, updateAttributes } from '@nivaro/sdk'
|
|
2000
|
+
|
|
2001
|
+
// Read all attribute values for an item
|
|
2002
|
+
const { data: attrs } = await nivaro.request(readAttributes('projects', '42'))
|
|
2003
|
+
// attrs → { risk_level: 'medium', budget_code: 'IT-2024-003' }
|
|
2004
|
+
|
|
2005
|
+
// Update attribute values (partial patch)
|
|
2006
|
+
await nivaro.request(
|
|
2007
|
+
updateAttributes('projects', '42', {
|
|
2008
|
+
risk_level: 'high',
|
|
2009
|
+
budget_code: 'IT-2024-099',
|
|
2010
|
+
})
|
|
2011
|
+
)
|
|
2012
|
+
// Omitted keys are left unchanged
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
#### Attribute types
|
|
2016
|
+
|
|
2017
|
+
| Type | Stored As | Parsed As | Example |
|
|
2018
|
+
| --- | --- | --- | --- |
|
|
2019
|
+
| text | string | string | "extended warranty" |
|
|
2020
|
+
| number | string | number | "42" |
|
|
2021
|
+
| boolean | string | boolean | "true" or "false" |
|
|
2022
|
+
| date | string | ISO 8601 date | "2024-12-31" |
|
|
2023
|
+
| select | string | option key | "gold" |
|
|
2024
|
+
|
|
2025
|
+
#### Admin UI
|
|
2026
|
+
|
|
2027
|
+
Attribute definitions are managed in `/data-model` → Table Editor → Attributes tab (admin only). Once definitions exist for a collection, an Attributes card appears on the item editor for users to fill in values.
|
|
2028
|
+
|
|
2029
|
+
| Command | Route | Auth |
|
|
2030
|
+
| --- | --- | --- |
|
|
2031
|
+
| readAttributeDefinitions(collection) | GET /attributes/definitions/:collection | Authenticated |
|
|
2032
|
+
| createAttributeDefinition(collection, body) | POST /attributes/definitions/:collection | Admin |
|
|
2033
|
+
| updateAttributeDefinition(id, body) | PATCH /attributes/definitions/:id | Admin |
|
|
2034
|
+
| deleteAttributeDefinition(id) | DELETE /attributes/definitions/:id | Admin |
|
|
2035
|
+
| readAttributes(collection, itemId) | GET /attributes/:collection/:itemId | Authenticated |
|
|
2036
|
+
| updateAttributes(collection, itemId, body) | PATCH /attributes/:collection/:itemId | Authenticated |
|
|
2037
|
+
|
|
2038
|
+
> **Note:** All attribute values are stored as strings in `nivaro_attribute_values` regardless of the definition type. SDKs and the admin UI handle type conversion on read/write. Deleting a definition cleans up its orphaned values.
|
|
2039
|
+
|
|
2040
|
+
---
|
|
2041
|
+
|
|
2042
|
+
## SDK — Notification Subscriptions
|
|
2043
|
+
|
|
2044
|
+
Users subscribe to collection events (create/update/delete) with optional field filters. Notifications are delivered instantly or as daily/weekly digests. Self-serve — no admin permission needed.
|
|
2045
|
+
|
|
2046
|
+
```typescript
|
|
2047
|
+
import {
|
|
2048
|
+
readNotificationSubscriptions,
|
|
2049
|
+
createNotificationSubscription,
|
|
2050
|
+
updateNotificationSubscription,
|
|
2051
|
+
deleteNotificationSubscription,
|
|
2052
|
+
} from '@nivaro/sdk'
|
|
2053
|
+
|
|
2054
|
+
// List my subscriptions
|
|
2055
|
+
const { data: subs } = await nivaro.request(readNotificationSubscriptions())
|
|
2056
|
+
// subs → Subscription[] — { id, collection, event_type, filter_field, filter_value, label, digest_frequency, is_active }
|
|
2057
|
+
|
|
2058
|
+
// Subscribe to all new "urgent" orders
|
|
2059
|
+
const { data: sub } = await nivaro.request(
|
|
2060
|
+
createNotificationSubscription({
|
|
2061
|
+
collection: 'orders',
|
|
2062
|
+
event_type: 'create', // 'create' | 'update' | 'delete'
|
|
2063
|
+
filter_field: 'priority', // optional: filter by field value
|
|
2064
|
+
filter_value: 'urgent', // only notify if priority == 'urgent'
|
|
2065
|
+
label: 'Urgent orders', // custom label
|
|
2066
|
+
is_active: true,
|
|
2067
|
+
})
|
|
2068
|
+
)
|
|
2069
|
+
|
|
2070
|
+
// Switch to daily digest instead of instant notifications
|
|
2071
|
+
await nivaro.request(
|
|
2072
|
+
updateNotificationSubscription(sub.id, {
|
|
2073
|
+
digest_frequency: 'instant', // or 'daily' or 'weekly'
|
|
2074
|
+
})
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
// Unsubscribe
|
|
2078
|
+
await nivaro.request(deleteNotificationSubscription(sub.id))
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
#### Subscription types
|
|
2082
|
+
|
|
2083
|
+
| Event Type | Triggers | Example |
|
|
2084
|
+
| --- | --- | --- |
|
|
2085
|
+
| create | New item is added to the collection | Notify on new leads |
|
|
2086
|
+
| update | An existing item is modified | Notify when status changes |
|
|
2087
|
+
| delete | An item is removed from the collection | Notify on deleted orders |
|
|
2088
|
+
|
|
2089
|
+
#### Filters (optional)
|
|
2090
|
+
|
|
2091
|
+
If you only care about certain items (e.g., high-priority orders), specify a filter_field and filter_value. Notifications only fire when the field matches the value.
|
|
2092
|
+
|
|
2093
|
+
| No Filter | With Filter | Result |
|
|
2094
|
+
| --- | --- | --- |
|
|
2095
|
+
| collection: "orders" | filter_field: "priority", filter_value: "urgent" | Only notify on urgent orders |
|
|
2096
|
+
| collection: "deals" | filter_field: "amount", filter_value: "100000" | Only notify on deals >= 100k |
|
|
2097
|
+
| collection: "projects" | (no filter) | Notify on ALL project changes |
|
|
2098
|
+
|
|
2099
|
+
#### Digest modes
|
|
2100
|
+
|
|
2101
|
+
| Mode | Delivery | Best For |
|
|
2102
|
+
| --- | --- | --- |
|
|
2103
|
+
| instant | Real-time in-app bell notification | Critical events that need immediate action |
|
|
2104
|
+
| daily | Email digest at 08:00 daily | Summary of events from the last 24 hours |
|
|
2105
|
+
| weekly | Email digest on Monday 08:00 | Lower-priority notifications, trending events |
|
|
2106
|
+
|
|
2107
|
+
| Command | Route | Auth |
|
|
2108
|
+
| --- | --- | --- |
|
|
2109
|
+
| readNotificationSubscriptions() | GET /notification-subscriptions | Authenticated |
|
|
2110
|
+
| createNotificationSubscription(body) | POST /notification-subscriptions | Authenticated |
|
|
2111
|
+
| updateNotificationSubscription(id, body) | PATCH /notification-subscriptions/:id | Authenticated |
|
|
2112
|
+
| deleteNotificationSubscription(id) | DELETE /notification-subscriptions/:id | Authenticated |
|
|
2113
|
+
|
|
2114
|
+
> **Note:** Digest emails are sent daily at 08:00 and weekly on Monday at 08:00 (UTC). Each user has a single watermark (`last_digest_at`) shared across both daily and weekly digests — whichever sends first advances the watermark, so events are never delivered twice.
|
|
2115
|
+
|
|
2116
|
+
---
|
|
2117
|
+
|
|
2118
|
+
## SDK — SLA Rules
|
|
2119
|
+
|
|
2120
|
+
Attach time-based SLA targets (e.g., "resolve within 24 hours") to workflow states. Track elapsed time, warn on approaching breach, and escalate to managers when targets are missed.
|
|
2121
|
+
|
|
2122
|
+
#### Admin: Define SLA rules
|
|
2123
|
+
|
|
2124
|
+
```typescript
|
|
2125
|
+
import {
|
|
2126
|
+
readSlaRules, readSlaRule,
|
|
2127
|
+
createSlaRule, updateSlaRule, deleteSlaRule,
|
|
2128
|
+
} from '@nivaro/sdk'
|
|
2129
|
+
|
|
2130
|
+
// List all SLA rules for a workflow template
|
|
2131
|
+
const { data: rules } = await nivaro.request(readSlaRules(workflowTemplateId))
|
|
2132
|
+
|
|
2133
|
+
// Create an SLA rule
|
|
2134
|
+
const { data: rule } = await nivaro.request(
|
|
2135
|
+
createSlaRule({
|
|
2136
|
+
name: 'Critical bug resolution',
|
|
2137
|
+
workflow_template_id: 'wf-uuid',
|
|
2138
|
+
state_key: 'in_progress', // applies to this workflow state
|
|
2139
|
+
duration_hours: 8, // must resolve within 8 hours
|
|
2140
|
+
warning_threshold_pct: 75, // warn at 75% (6 hours)
|
|
2141
|
+
business_hours_only: true, // count only Mon-Fri 09:00-17:00
|
|
2142
|
+
notify_on_warning: true, // notify owner at 75%
|
|
2143
|
+
notify_on_breach: true, // notify owner at 100%
|
|
2144
|
+
escalation_user_id: 'manager-uuid', // escalate to manager on breach
|
|
2145
|
+
is_active: true,
|
|
2146
|
+
})
|
|
2147
|
+
)
|
|
2148
|
+
|
|
2149
|
+
// Update
|
|
2150
|
+
await nivaro.request(updateSlaRule(rule.id, { duration_hours: 16 }))
|
|
2151
|
+
|
|
2152
|
+
// Delete
|
|
2153
|
+
await nivaro.request(deleteSlaRule(rule.id))
|
|
2154
|
+
```
|
|
2155
|
+
|
|
2156
|
+
#### User: Check SLA status
|
|
2157
|
+
|
|
2158
|
+
```typescript
|
|
2159
|
+
import { readSlaStatus, readSlaStatusBatch } from '@nivaro/sdk'
|
|
2160
|
+
|
|
2161
|
+
// Check SLA for a single item
|
|
2162
|
+
const { data: status } = await nivaro.request(readSlaStatus('orders', '42'))
|
|
2163
|
+
// status → SlaStatus[] — one entry per active SLA rule for the workflow state
|
|
2164
|
+
// {
|
|
2165
|
+
// rule_id,
|
|
2166
|
+
// rule_name,
|
|
2167
|
+
// state_key,
|
|
2168
|
+
// elapsed_hours: 4.5,
|
|
2169
|
+
// remaining_hours: 3.5,
|
|
2170
|
+
// warning_threshold_pct: 75,
|
|
2171
|
+
// is_warning: false, // 75% reached?
|
|
2172
|
+
// is_breached: false, // 100% exceeded?
|
|
2173
|
+
// breached_at: null,
|
|
2174
|
+
// }
|
|
2175
|
+
|
|
2176
|
+
// Batch check multiple items
|
|
2177
|
+
const { data: batch } = await nivaro.request(
|
|
2178
|
+
readSlaStatusBatch('orders', ['42', '43', '44'])
|
|
2179
|
+
)
|
|
2180
|
+
// batch → { [itemId]: SlaStatus[] }
|
|
2181
|
+
```
|
|
2182
|
+
|
|
2183
|
+
#### Business hours calculation
|
|
2184
|
+
|
|
2185
|
+
When `business_hours_only: true`, only Mon-Fri 09:00-17:00 is counted. Blackout dates (holidays, maintenance windows) pause the timer entirely.
|
|
2186
|
+
|
|
2187
|
+
| Scenario | Time Counting | Example |
|
|
2188
|
+
| --- | --- | --- |
|
|
2189
|
+
| Mon 10:00 - Mon 18:00 | 8 business hours counted | 8h of an 8h SLA |
|
|
2190
|
+
| Fri 16:00 - Fri 17:00 + Mon 09:00 - 10:00 | 2 business hours counted | 1h Friday + 1h Monday |
|
|
2191
|
+
| During blackout date | Timer paused | Winter shutdown 12/20-1/2 → no time counted |
|
|
2192
|
+
| 24/7 mode (business_hours_only: false) | All hours counted | Calendar hours only |
|
|
2193
|
+
|
|
2194
|
+
| Command | Route | Auth |
|
|
2195
|
+
| --- | --- | --- |
|
|
2196
|
+
| readSlaRules(workflowTemplateId?) | GET /sla/rules | Admin |
|
|
2197
|
+
| readSlaRule(id) | GET /sla/rules/:id | Admin |
|
|
2198
|
+
| createSlaRule(body) | POST /sla/rules | Admin |
|
|
2199
|
+
| updateSlaRule(id, body) | PATCH /sla/rules/:id | Admin |
|
|
2200
|
+
| deleteSlaRule(id) | DELETE /sla/rules/:id | Admin |
|
|
2201
|
+
| readSlaStatus(collection, itemId) | GET /sla/status/:collection/:item | Authenticated |
|
|
2202
|
+
| readSlaStatusBatch(collection, ids) | POST /sla/status/batch | Authenticated |
|
|
2203
|
+
|
|
2204
|
+
> **Note:** SLA times are calculated from workflow history: elapsed time is how long the item has been in the current state. Transitions reset the clock for the new state (which may have its own SLA rule).
|
|
2205
|
+
|
|
2206
|
+
---
|
|
2207
|
+
|
|
2208
|
+
## SDK — Queues
|
|
2209
|
+
|
|
2210
|
+
Full command coverage for cross-collection worklists: queue CRUD and source configuration, item resolution with scopes/filters/sorting/pagination, claims, saved views (including column snapshots), per-viewer default views, stat trends, per-owner workload, and materialized-cache rebuilds.
|
|
2211
|
+
|
|
2212
|
+
```typescript
|
|
2213
|
+
import {
|
|
2214
|
+
createNivaro, listQueues, readQueueItems, claimQueueItem,
|
|
2215
|
+
listQueueViews, createQueueView, setQueueDefaultView, readQueueTrends
|
|
2216
|
+
} from '@nivaro/sdk'
|
|
2217
|
+
|
|
2218
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: 'nvk_…' })
|
|
2219
|
+
|
|
2220
|
+
const { data: queues } = await nivaro.request(listQueues())
|
|
2221
|
+
|
|
2222
|
+
// Table-style page 1 with per-column filters (omit page/limit for the full set)
|
|
2223
|
+
const result = await nivaro.request(
|
|
2224
|
+
readQueueItems(queues[0].id, {
|
|
2225
|
+
scope: 'mine',
|
|
2226
|
+
sort: '-priority',
|
|
2227
|
+
filters: { state: 'Waiting on Manager Approval' },
|
|
2228
|
+
page: 1,
|
|
2229
|
+
limit: 25
|
|
2230
|
+
})
|
|
2231
|
+
)
|
|
2232
|
+
console.log(result.stats.total, result.data.length)
|
|
2233
|
+
|
|
2234
|
+
// Claim the first unclaimed item
|
|
2235
|
+
const next = result.data.find((i) => !i.claimed_by)
|
|
2236
|
+
if (next) await nivaro.request(claimQueueItem(queues[0].id, next))
|
|
2237
|
+
|
|
2238
|
+
// Save the current table state as a shared view and star it as my default
|
|
2239
|
+
const view = await nivaro.request(
|
|
2240
|
+
createQueueView(queues[0].id, {
|
|
2241
|
+
name: 'My triage',
|
|
2242
|
+
is_shared: true,
|
|
2243
|
+
state: { scope: 'mine', sort: '-priority', view: 'table', columns: null }
|
|
2244
|
+
})
|
|
2245
|
+
)
|
|
2246
|
+
await nivaro.request(setQueueDefaultView(queues[0].id, view.data.id))
|
|
2247
|
+
```
|
|
2248
|
+
|
|
2249
|
+
| Command | Purpose |
|
|
2250
|
+
| --- | --- |
|
|
2251
|
+
| listQueues / readQueue | Queues visible to the caller; one queue with sources + extra-field metadata |
|
|
2252
|
+
| createQueue / updateQueue / deleteQueue | Queue CRUD incl. display_config (views, row_click, default_columns…) |
|
|
2253
|
+
| updateQueueSources | Replace sources wholesale (max 10); cache-affecting edits rebuild materialized queues |
|
|
2254
|
+
| readQueueItems | Resolve items: scope mine/unowned/all/claimed, column filters, sort, optional pagination |
|
|
2255
|
+
| readQueueWorkload | Items grouped per owner with WIP limits |
|
|
2256
|
+
| readQueueTrends | Daily stat snapshots for sparklines (scope "mine" = caller series) |
|
|
2257
|
+
| claimQueueItem / releaseQueueItem | Claims with pipeline instance-owner write-through |
|
|
2258
|
+
| listQueueViews / createQueueView / updateQueueView / deleteQueueView | Saved views — state snapshots incl. visible columns |
|
|
2259
|
+
| readQueueColumnPrefs / setQueueDefaultView | Per-viewer starred default view |
|
|
2260
|
+
| rematerializeQueue | Force a materialized-cache rebuild |
|
|
2261
|
+
| readQueueCollectionStates / suggestQueueLabels | Builder helpers: state pickers, label-template previews |
|
|
2262
|
+
|
|
2263
|
+
> **Note:** Queue reads respect queue-level visibility (owner, shared, role-scoped). Claims return 403 when the queue has claims disabled.
|
|
2264
|
+
|
|
2265
|
+
---
|
|
2266
|
+
|
|
2267
|
+
## SDK — Presence & Awareness
|
|
2268
|
+
|
|
2269
|
+
Real-time presence tracking via Socket.io. See who is currently viewing/editing an item and take coordination actions (lock, merge, notify).
|
|
2270
|
+
|
|
2271
|
+
#### REST: Query presence
|
|
2272
|
+
|
|
2273
|
+
```typescript
|
|
2274
|
+
import { readPresence, readAllPresence } from '@nivaro/sdk'
|
|
2275
|
+
|
|
2276
|
+
// Who is currently viewing/editing a specific item?
|
|
2277
|
+
const { data: viewers } = await nivaro.request(readPresence('contracts', '99'))
|
|
2278
|
+
// viewers → Presence[] — { user_id, first_name, last_name, email, is_editing, last_heartbeat }
|
|
2279
|
+
|
|
2280
|
+
// All active sessions across the instance (admin)
|
|
2281
|
+
const { data: sessions, total } = await nivaro.request(readAllPresence())
|
|
2282
|
+
// sessions → Presence[] (paginated; default 100 per page)
|
|
2283
|
+
```
|
|
2284
|
+
|
|
2285
|
+
#### Socket.io: Real-time subscription
|
|
2286
|
+
|
|
2287
|
+
```typescript
|
|
2288
|
+
import { createRealtime } from '@nivaro/sdk'
|
|
2289
|
+
|
|
2290
|
+
const rt = createRealtime(token)
|
|
2291
|
+
await rt.connect('https://nivaro.example.com')
|
|
2292
|
+
|
|
2293
|
+
// Subscribe to presence updates for an item
|
|
2294
|
+
rt.presence.subscribe('contracts:99', (users) => {
|
|
2295
|
+
console.log(`${users.length} users viewing`)
|
|
2296
|
+
users.forEach(u => {
|
|
2297
|
+
if (u.is_editing) console.log(`${u.first_name} is editing`)
|
|
2298
|
+
})
|
|
2299
|
+
})
|
|
2300
|
+
|
|
2301
|
+
// Announce that you are editing
|
|
2302
|
+
rt.presence.setEditing('contracts:99', true)
|
|
2303
|
+
|
|
2304
|
+
// Stop editing
|
|
2305
|
+
rt.presence.setEditing('contracts:99', false)
|
|
2306
|
+
|
|
2307
|
+
// Leave the presence room
|
|
2308
|
+
rt.presence.leave('contracts:99')
|
|
2309
|
+
```
|
|
2310
|
+
|
|
2311
|
+
#### Soft edit locks (item locking)
|
|
2312
|
+
|
|
2313
|
+
Pair presence with item locking to prevent conflicting edits:
|
|
2314
|
+
|
|
2315
|
+
```typescript
|
|
2316
|
+
// Admin UI example
|
|
2317
|
+
const canEdit = async (collection, itemId, userId) => {
|
|
2318
|
+
// Check if item is locked by another user
|
|
2319
|
+
const { data: locked } = await nivaro.request(
|
|
2320
|
+
isItemLocked(collection, itemId, userId)
|
|
2321
|
+
)
|
|
2322
|
+
|
|
2323
|
+
if (locked && locked.lock.user_id !== userId) {
|
|
2324
|
+
// Item is locked by someone else
|
|
2325
|
+
return false
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Item is free to edit — acquire a lock
|
|
2329
|
+
const { data: lock } = await nivaro.request(
|
|
2330
|
+
acquireItemLock(collection, itemId)
|
|
2331
|
+
)
|
|
2332
|
+
return true
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// Release lock when done editing
|
|
2336
|
+
await nivaro.request(releaseItemLock(collection, itemId))
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
#### Presence events via Socket.io
|
|
2340
|
+
|
|
2341
|
+
| Event | When Fired | Data |
|
|
2342
|
+
| --- | --- | --- |
|
|
2343
|
+
| presence:join | User enters a room | { user_id, first_name, last_name } |
|
|
2344
|
+
| presence:leave | User leaves a room | { user_id } |
|
|
2345
|
+
| presence:editing | User starts/stops editing | { user_id, is_editing } |
|
|
2346
|
+
| presence:heartbeat | Periodic keep-alive (30s) | { user_id, last_seen } |
|
|
2347
|
+
|
|
2348
|
+
| Command | Route | Auth |
|
|
2349
|
+
| --- | --- | --- |
|
|
2350
|
+
| readPresence(collection, itemId) | GET /presence/:collection/:itemId | Authenticated |
|
|
2351
|
+
| readAllPresence(limit?, offset?) | GET /presence | Admin |
|
|
2352
|
+
|
|
2353
|
+
> **Note:** Presence data is ephemeral — it lives only in Redis and is lost on server restart. Perfect for collaboration cues but not for audit/compliance tracking. The admin UI emits heartbeats automatically; custom clients should emit `presence:heartbeat` every 30 seconds to stay visible.
|
|
2354
|
+
|
|
2355
|
+
---
|
|
2356
|
+
|
|
2357
|
+
## Tree & Hierarchy
|
|
2358
|
+
|
|
2359
|
+
The Nivaro SDK provides typed commands for both same-collection trees and multi-collection hierarchies.
|
|
2360
|
+
|
|
2361
|
+
#### Tree commands
|
|
2362
|
+
|
|
2363
|
+
```typescript
|
|
2364
|
+
import { createNivaro, readTreeConfig, readTreeNodes, readTreeNested, readTreeAncestors, readTreeDescendants, readTreeChildren, moveTreeNode, reorderTreeSiblings, rebuildTreePaths } from '@nivaro/sdk'
|
|
2365
|
+
|
|
2366
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: 'my-token' })
|
|
2367
|
+
|
|
2368
|
+
// Check if a collection has a tree config
|
|
2369
|
+
const config = await nivaro.request(readTreeConfig('org_units'))
|
|
2370
|
+
// → { data: { id, collection, parent_field, label_field, order_field } | null }
|
|
2371
|
+
|
|
2372
|
+
// Flat node list (for custom rendering)
|
|
2373
|
+
const nodes = await nivaro.request(readTreeNodes('org_units'))
|
|
2374
|
+
|
|
2375
|
+
// Fully nested tree (recursive children arrays)
|
|
2376
|
+
const tree = await nivaro.request(readTreeNested('org_units'))
|
|
2377
|
+
|
|
2378
|
+
// Ancestors of a node (root-first breadcrumb)
|
|
2379
|
+
const path = await nivaro.request(readTreeAncestors('org_units', 42))
|
|
2380
|
+
|
|
2381
|
+
// Direct children of a node
|
|
2382
|
+
const kids = await nivaro.request(readTreeChildren('org_units', 42))
|
|
2383
|
+
|
|
2384
|
+
// All descendants (any depth)
|
|
2385
|
+
const all = await nivaro.request(readTreeDescendants('org_units', 42))
|
|
2386
|
+
|
|
2387
|
+
// Move a node (null = make root)
|
|
2388
|
+
await nivaro.request(moveTreeNode('org_units', 42, 7))
|
|
2389
|
+
|
|
2390
|
+
// Reorder siblings (requires order_field on the tree config)
|
|
2391
|
+
await nivaro.request(reorderTreeSiblings('org_units', 42, [
|
|
2392
|
+
{ id: 42, sort: 0 },
|
|
2393
|
+
{ id: 43, sort: 1 },
|
|
2394
|
+
]))
|
|
2395
|
+
|
|
2396
|
+
// Rebuild materialized path/depth columns (admin; maintain_path configs)
|
|
2397
|
+
await nivaro.request(rebuildTreePaths(3))
|
|
2398
|
+
```
|
|
2399
|
+
|
|
2400
|
+
#### Tree permission commands (admin)
|
|
2401
|
+
|
|
2402
|
+
```typescript
|
|
2403
|
+
import { listTreePermissions, createTreePermission, updateTreePermission, deleteTreePermission } from '@nivaro/sdk'
|
|
2404
|
+
|
|
2405
|
+
// List rules (optionally for one collection)
|
|
2406
|
+
const rules = await nivaro.request(listTreePermissions('org_units'))
|
|
2407
|
+
|
|
2408
|
+
// Deny the "Contractors" role updates inside node 42's subtree
|
|
2409
|
+
await nivaro.request(createTreePermission({
|
|
2410
|
+
collection: 'org_units',
|
|
2411
|
+
node_id: 42,
|
|
2412
|
+
role: '0a1b2c3d-…', // role UUID
|
|
2413
|
+
action: 'update',
|
|
2414
|
+
allow: false,
|
|
2415
|
+
}))
|
|
2416
|
+
|
|
2417
|
+
await nivaro.request(updateTreePermission(7, { action: '*' }))
|
|
2418
|
+
await nivaro.request(deleteTreePermission(7))
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
> **Note:** Item reads on tree collections may include an `_inherited` sidecar (`{ field: ancestorId }`) when inheritable fields resolved values from an ancestor — see Inherited Field Values.
|
|
2422
|
+
|
|
2423
|
+
#### Hierarchy commands
|
|
2424
|
+
|
|
2425
|
+
```typescript
|
|
2426
|
+
import { createNivaro, listHierarchyConfigs, readHierarchyConfig, readHierarchyTree, readHierarchyNodes, readHierarchyNodeChildren, readHierarchyNodeAncestors, createHierarchyConfig, updateHierarchyConfig, deleteHierarchyConfig } from '@nivaro/sdk'
|
|
2427
|
+
|
|
2428
|
+
// List all hierarchy configs
|
|
2429
|
+
const configs = await nivaro.request(listHierarchyConfigs())
|
|
2430
|
+
|
|
2431
|
+
// Full nested tree for hierarchy #1
|
|
2432
|
+
const tree = await nivaro.request(readHierarchyTree(1))
|
|
2433
|
+
|
|
2434
|
+
// Flat nodes for hierarchy #1
|
|
2435
|
+
const nodes = await nivaro.request(readHierarchyNodes(1))
|
|
2436
|
+
|
|
2437
|
+
// Children of a specific node
|
|
2438
|
+
const children = await nivaro.request(readHierarchyNodeChildren(1, 'divisions', 5))
|
|
2439
|
+
|
|
2440
|
+
// Ancestors (breadcrumb) of a node
|
|
2441
|
+
const ancestors = await nivaro.request(readHierarchyNodeAncestors(1, 'regions', 22))
|
|
2442
|
+
|
|
2443
|
+
// Create a new hierarchy config
|
|
2444
|
+
await nivaro.request(createHierarchyConfig({
|
|
2445
|
+
name: 'Org Structure',
|
|
2446
|
+
levels: [
|
|
2447
|
+
{ collection: 'divisions', label_field: 'name', parent_fk: null },
|
|
2448
|
+
{ collection: 'regions', label_field: 'name', parent_fk: 'division_id' },
|
|
2449
|
+
],
|
|
2450
|
+
}))
|
|
2451
|
+
```
|
|
2452
|
+
|
|
2453
|
+
---
|
|
2454
|
+
|
|
2455
|
+
## SDK Coverage: 300+ Typed Commands
|
|
2456
|
+
|
|
2457
|
+
The @nivaro/sdk command surface now covers every feature area — roughly 175 typed `Command<T>` factories spanning items, files, workflows, pipelines, flows, comments, webhooks, rules, custom queries, trees and hierarchies, submission forms, field watches, notification subscriptions, imports, SLA, alerts, AI endpoints (generate, summarize, validate, check-duplicates), translations, drafts, scheduled changes, record templates, saved views, API keys, widget feeds, sync jobs, ERP submissions, PDF templates, pages, queues (worklists, items, claims, saved views, trends, workload), roles & policies (RBAC/RLS), user management + out-of-office delegation, file management (list/meta/presign/transform URLs), dashboard widgets, extension item/bulk actions, throughput reporting, and more. If a REST route exists, there is a typed command for it.
|
|
2458
|
+
|
|
2459
|
+
#### Discovering commands
|
|
2460
|
+
|
|
2461
|
+
- Everything is exported from the package root — editor autocomplete on `import { … } from "@nivaro/sdk"` is the fastest index.
|
|
2462
|
+
- The Playground at /playground runs snippets against the live instance with your session's permissions, with collection and field comboboxes to scaffold calls.
|
|
2463
|
+
- All commands flow through `nivaro.request(command)`, so auth, workspace headers, and error handling are uniform.
|
|
2464
|
+
|
|
2465
|
+
```typescript
|
|
2466
|
+
import { createNivaro, readItems, aiValidate, listWidgetFeeds } from '@nivaro/sdk';
|
|
2467
|
+
|
|
2468
|
+
const nivaro = createNivaro('https://nivaro.example.com').withToken('nvk_...');
|
|
2469
|
+
|
|
2470
|
+
const articles = await nivaro.request(readItems('articles', { limit: 5 }));
|
|
2471
|
+
const check = await nivaro.request(aiValidate('articles', { title: 'Draft post' }));
|
|
2472
|
+
const feeds = await nivaro.request(listWidgetFeeds());
|
|
2473
|
+
```
|
|
2474
|
+
|
|
2475
|
+
|
|
2476
|
+
---
|
|
2477
|
+
|
|
2478
|
+
## TypeScript
|
|
2479
|
+
|
|
2480
|
+
All commands are fully typed. Pass your collection interface as a generic to get typed responses:
|
|
2481
|
+
|
|
2482
|
+
```typescript
|
|
2483
|
+
interface Project {
|
|
2484
|
+
id: string
|
|
2485
|
+
name: string
|
|
2486
|
+
status: 'active' | 'done' | 'archived'
|
|
2487
|
+
owner: string
|
|
2488
|
+
created_at: string
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
const list = await nivaro.request(readItems<Project>('projects', {
|
|
2492
|
+
filter: { status: _eq('active') },
|
|
2493
|
+
sort: [desc('created_at')],
|
|
2494
|
+
}))
|
|
2495
|
+
// list.data is Project[]
|
|
2496
|
+
|
|
2497
|
+
const { data: project } = await nivaro.request(readItem<Project>('projects', id))
|
|
2498
|
+
// project is Project
|
|
2499
|
+
```
|
|
2500
|
+
|
|
2501
|
+
---
|
|
2502
|
+
|
|
2503
|
+
## License
|
|
2504
|
+
|
|
2505
|
+
MIT — see [LICENSE](https://github.com/nodeworks/nivaro/blob/main/LICENSE).
|