@nivaro/sdk 0.2.7 → 0.3.1
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 +286 -195
- package/dist/index.d.ts +1509 -93
- package/dist/index.js +791 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,12 +48,32 @@ const nivaro = createNivaro('https://nivaro.example.com', {
|
|
|
48
48
|
// Full options
|
|
49
49
|
const nivaro = createNivaro('https://nivaro.example.com', {
|
|
50
50
|
token: 'nvk_abc123...',
|
|
51
|
-
workspace: 'workspace-uuid', //
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
headers: { 'x-workspace': 'workspace-uuid', 'x-nivaro-app': 'my-app' }, // extra headers (or a function)
|
|
52
|
+
timeoutMs: 90_000, // abandon a GET read that has not answered (off by default)
|
|
53
|
+
retry: 2, // retry GETs on 408/429/502/503/504 + network errors, honouring Retry-After (off by default)
|
|
54
|
+
debug: true, // one console.debug line per request: method, path, status, ms, request id
|
|
55
|
+
onRequest: (req) => { req.headers['traceparent'] = currentTrace() },
|
|
56
|
+
onResponse: (res) => metrics.observe(res.path, res.status, res.durationMs, res.requestId),
|
|
57
|
+
onError: (err, where) => logger.warn({ where, err }),
|
|
54
58
|
})
|
|
55
59
|
```
|
|
56
60
|
|
|
61
|
+
#### Errors
|
|
62
|
+
|
|
63
|
+
Every non-2xx answer throws a `NivaroError` (an `Error`): `status`, the API machine `code` (e.g. `QUOTA_EXCEEDED`, `SERVER_BUSY`), `requestId` (the `x-nivaro-request-id` the server stamped — quote it when reporting a problem), the parsed body on `response`, `method`, `path` and `retryAfterMs`. A GET past `timeoutMs` throws a `TimeoutError`; a caller abort (`withSignal`) throws the platform `AbortError`.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { isNivaroError, readItem } from '@nivaro/sdk'
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await nivaro.request(readItem('orders', 42))
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (isNivaroError(err) && err.status === 404) return null
|
|
72
|
+
if (isNivaroError(err)) console.error(err.code, err.message, 'request', err.requestId)
|
|
73
|
+
throw err
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
57
77
|
#### Client methods
|
|
58
78
|
|
|
59
79
|
| Method | Purpose | Example |
|
|
@@ -89,7 +109,15 @@ const items = await nivaro.request(
|
|
|
89
109
|
|
|
90
110
|
## SDK — Authentication
|
|
91
111
|
|
|
92
|
-
The SDK
|
|
112
|
+
The SDK sends whatever `token` you give it as `Authorization: Bearer`; without one it rides the browser session cookie.
|
|
113
|
+
|
|
114
|
+
| Credential | Where it comes from | Notes |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| Named API key `nvk_…` | API Keys page / createApiKey() | Scopes, expiry, IP allowlist, rate limit; sandbox keys never write. Sockets: use a ws-token. |
|
|
117
|
+
| Static user token | generateToken() / generateUserToken() | Acts as that user; also signs a socket in directly. |
|
|
118
|
+
| Session cookie | OIDC / SAML / password sign-in in the browser | No token; `credentials: include`. Sockets: use a ws-token. |
|
|
119
|
+
| View-as token `nvm_…` | An admin’s View-as | Acts as the viewed person; ends with the View-as. |
|
|
120
|
+
| OAuth access token `nvo_…` | Remote MCP clients (OAuth 2.1) | Accepted on /api/mcp and its tool calls only — never on other routes. |
|
|
93
121
|
|
|
94
122
|
#### Static tokens (server-side / CLI)
|
|
95
123
|
|
|
@@ -160,6 +188,18 @@ nivaro.setToken(null)
|
|
|
160
188
|
await nivaro.request(revokeUserToken('user-uuid'))
|
|
161
189
|
```
|
|
162
190
|
|
|
191
|
+
#### Session helpers
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { readMe, logout, logoutAll, readWsToken, readAuthProviders } from '@nivaro/sdk'
|
|
195
|
+
|
|
196
|
+
const { data: me } = await nivaro.request(readMe())
|
|
197
|
+
const { data: providers } = await nivaro.request(readAuthProviders()) // public: which sign-ins exist
|
|
198
|
+
const { token } = await nivaro.request(readWsToken()) // one-time socket token (2 min)
|
|
199
|
+
await nivaro.request(logout()) // end this session (cookie sign-in)
|
|
200
|
+
await nivaro.request(logoutAll()) // end every session of mine
|
|
201
|
+
```
|
|
202
|
+
|
|
163
203
|
---
|
|
164
204
|
|
|
165
205
|
## SDK — REST Commands
|
|
@@ -185,8 +225,23 @@ const { data, total, limit, offset } = await nivaro.request(
|
|
|
185
225
|
)
|
|
186
226
|
// data → T[]
|
|
187
227
|
|
|
188
|
-
//
|
|
189
|
-
const
|
|
228
|
+
// Search a few fields by relevance, a cached total, relation labels flattened in
|
|
229
|
+
const found = await nivaro.request(
|
|
230
|
+
readItems('articles', {
|
|
231
|
+
search: 'pump',
|
|
232
|
+
search_fields: ['title', 'code'],
|
|
233
|
+
search_rank: true,
|
|
234
|
+
count: 'approx', // or false to skip the count (total → null)
|
|
235
|
+
labels: true,
|
|
236
|
+
})
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Keyset paging for walking a large collection
|
|
240
|
+
let res = await nivaro.request(readItems('articles', { after: 'start', limit: 500 }))
|
|
241
|
+
while (res.next_cursor) res = await nivaro.request(readItems('articles', { after: res.next_cursor, limit: 500 }))
|
|
242
|
+
|
|
243
|
+
// Single item (optionally projected)
|
|
244
|
+
const { data: article } = await nivaro.request(readItem('articles', '123', { fields: ['id', 'title'] }))
|
|
190
245
|
// data → T
|
|
191
246
|
```
|
|
192
247
|
|
|
@@ -229,29 +284,59 @@ import { deleteItem } from '@nivaro/sdk'
|
|
|
229
284
|
await nivaro.request(deleteItem('articles', '123'))
|
|
230
285
|
```
|
|
231
286
|
|
|
232
|
-
#### Bulk
|
|
287
|
+
#### Bulk, batch and rehearsed writes
|
|
288
|
+
|
|
289
|
+
Every row goes through the items service as the caller — permissions, row filters, User Scopes, validation, hooks and revisions apply row by row; one refused row never fails the others unless you ask for all-or-nothing.
|
|
233
290
|
|
|
234
291
|
```typescript
|
|
235
|
-
import {
|
|
292
|
+
import {
|
|
293
|
+
bulkWriteItems, queueBulkWriteItems, readBulkRun, batchItems,
|
|
294
|
+
bulkUpdateItems, bulkDeleteItems, bulkTransitionItems,
|
|
295
|
+
rehearseCreateItem, rehearseUpdateItem, checkItemFilter
|
|
296
|
+
} from '@nivaro/sdk'
|
|
236
297
|
|
|
237
|
-
//
|
|
238
|
-
const { data:
|
|
239
|
-
|
|
240
|
-
{ title: 'Article 1', status: 'draft' },
|
|
241
|
-
{ title: 'Article 2', status: 'draft' },
|
|
242
|
-
])
|
|
298
|
+
// Up to 500 rows: a row with an id is an update, without one a create (207, per-row status)
|
|
299
|
+
const { data: out } = await nivaro.request(
|
|
300
|
+
bulkWriteItems('articles', [{ title: 'A' }, { id: '124', status: 'published' }])
|
|
243
301
|
)
|
|
302
|
+
// out.results[i] → { index, op, status, id?, error?, code? }; atomic: true = creates, all or nothing
|
|
244
303
|
|
|
245
|
-
//
|
|
246
|
-
const { data:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
304
|
+
// Bigger runs in the background, results kept 24 h
|
|
305
|
+
const { data: queued } = await nivaro.request(queueBulkWriteItems('articles', rows))
|
|
306
|
+
const { data: results } = await nivaro.request(readBulkRun('articles', queued.run_id))
|
|
307
|
+
|
|
308
|
+
// Up to 100 creates + updates, one result per row
|
|
309
|
+
await nivaro.request(batchItems('articles', { create: [{ title: 'B' }], update: [{ id: '9', title: 'C' }] }))
|
|
252
310
|
|
|
253
|
-
//
|
|
311
|
+
// The same values on several records; a transition on several records; delete several
|
|
312
|
+
await nivaro.request(bulkUpdateItems('articles', ['123', '124'], { status: 'published' }))
|
|
313
|
+
await nivaro.request(bulkTransitionItems('articles', ['123'], 'transition-uuid', 'Approved'))
|
|
254
314
|
await nivaro.request(bulkDeleteItems('articles', ['123', '124']))
|
|
315
|
+
|
|
316
|
+
// Rehearse: everything runs, nothing is saved
|
|
317
|
+
const plan = await nivaro.request(rehearseUpdateItem('articles', '123', { status: 'published' }))
|
|
318
|
+
// plan.changes → [{ field, from, to }]; plan.ok / plan.error / plan.code when it would be refused
|
|
319
|
+
const check = await nivaro.request(checkItemFilter('articles', { status: { _eq: 'live' } }))
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Lookups, trash and history
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import {
|
|
326
|
+
readItem, readItemBySlug, readItemDistinct, listTrash, restoreTrashItem,
|
|
327
|
+
readOwnerHistory, readCommentCounts
|
|
328
|
+
} from '@nivaro/sdk'
|
|
329
|
+
|
|
330
|
+
const { data: a } = await nivaro.request(readItem('articles', '123', { fields: ['id', 'author.email'] }))
|
|
331
|
+
const { data: page } = await nivaro.request(readItemBySlug('pages', 'about-us'))
|
|
332
|
+
const { data: statuses } = await nivaro.request(readItemDistinct('articles', 'status'))
|
|
333
|
+
|
|
334
|
+
// Deleted records (admins: all; everyone else: their own deletions)
|
|
335
|
+
const { data: trash } = await nivaro.request(listTrash({ collection: 'articles', days: 30 }))
|
|
336
|
+
await nivaro.request(restoreTrashItem(trash[0].id))
|
|
337
|
+
|
|
338
|
+
const { data: owners } = await nivaro.request(readOwnerHistory('articles', '123')) // per pipeline state
|
|
339
|
+
const { data: counts } = await nivaro.request(readCommentCounts('articles', ['123', '124']))
|
|
255
340
|
```
|
|
256
341
|
|
|
257
342
|
#### Singletons
|
|
@@ -290,12 +375,12 @@ const { data: updated } = await nivaro.request(
|
|
|
290
375
|
```typescript
|
|
291
376
|
import { readRevisions, readRevision } from '@nivaro/sdk'
|
|
292
377
|
|
|
293
|
-
// All changes to an item (newest first)
|
|
378
|
+
// All changes to an item (newest first, up to 100; no paging)
|
|
294
379
|
const { data: revisions } = await nivaro.request(readRevisions('articles', '123'))
|
|
295
|
-
// Each revision: { id, action
|
|
380
|
+
// Each revision: { id, action, data, delta, timestamp, user_id, first_name, last_name, comment, event? }
|
|
296
381
|
|
|
297
382
|
// Single revision detail
|
|
298
|
-
const { data: rev } = await nivaro.request(readRevision(
|
|
383
|
+
const { data: rev } = await nivaro.request(readRevision(revisions[0].id!))
|
|
299
384
|
// rev.data → full snapshot at that point in time
|
|
300
385
|
// rev.delta → only the fields that changed (for updates)
|
|
301
386
|
```
|
|
@@ -902,131 +987,174 @@ Query keys are `liveRecordKey(collection, id, fields)` and `liveListKey(collecti
|
|
|
902
987
|
|
|
903
988
|
## SDK — Notifications & Inbox
|
|
904
989
|
|
|
905
|
-
|
|
990
|
+
Read and work the caller's own inbox. Notifications are created by rules, pipelines, comments (@mentions), alerts and people. Every command acts on the caller's own rows only.
|
|
906
991
|
|
|
907
992
|
```typescript
|
|
908
993
|
import {
|
|
909
|
-
|
|
910
|
-
markNotificationRead, markAllNotificationsRead, deleteNotification
|
|
994
|
+
listNotifications, readUnreadNotificationCount,
|
|
995
|
+
markNotificationRead, markNotificationsRead, markAllNotificationsRead, deleteNotification
|
|
911
996
|
} from '@nivaro/sdk'
|
|
912
997
|
|
|
913
|
-
//
|
|
914
|
-
const
|
|
915
|
-
|
|
998
|
+
// One page of the inbox (default 50, max 200). Filters match the notification center's own.
|
|
999
|
+
const page = await nivaro.request(
|
|
1000
|
+
listNotifications({ page: 1, limit: 50, lane: 'attention', search: 'invoice', unread: true })
|
|
916
1001
|
)
|
|
917
|
-
//
|
|
1002
|
+
// page.data → NotificationItem[]; page.pinned → acknowledgement-required rows (page 1)
|
|
1003
|
+
// status: 'inbox' | 'read' | 'archived' | 'all'; snoozed: true lists only sleeping rows
|
|
918
1004
|
|
|
919
|
-
//
|
|
920
|
-
const
|
|
921
|
-
//
|
|
1005
|
+
// Badge figures, split by lane
|
|
1006
|
+
const counts = await nivaro.request(readUnreadNotificationCount())
|
|
1007
|
+
// → { unread: 5, attention: 3, lanes: { critical, needs_you, fyi } }
|
|
922
1008
|
|
|
923
|
-
|
|
924
|
-
await nivaro.request(
|
|
925
|
-
|
|
926
|
-
// Mark all as read
|
|
1009
|
+
await nivaro.request(markNotificationRead(id))
|
|
1010
|
+
await nivaro.request(markNotificationsRead([id1, id2]))
|
|
927
1011
|
await nivaro.request(markAllNotificationsRead())
|
|
1012
|
+
await nivaro.request(deleteNotification(id))
|
|
1013
|
+
```
|
|
928
1014
|
|
|
929
|
-
|
|
930
|
-
|
|
1015
|
+
#### Snooze, acknowledge, bulk actions, mutes
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
import {
|
|
1019
|
+
snoozeNotification, acknowledgeNotification, bulkNotificationAction,
|
|
1020
|
+
archiveReadNotifications, listSentNotifications, readRecordMute, toggleRecordMute
|
|
1021
|
+
} from '@nivaro/sdk'
|
|
1022
|
+
|
|
1023
|
+
await nivaro.request(snoozeNotification(id, { until: '2026-11-02T09:00:00Z' }))
|
|
1024
|
+
await nivaro.request(snoozeNotification(id, { untilChange: true })) // wake when the record moves
|
|
1025
|
+
await nivaro.request(acknowledgeNotification(id)) // when the sender asked for an acknowledgement
|
|
1026
|
+
|
|
1027
|
+
// One action over explicit ids (≤1000) or everything matching a filter (≤2000)
|
|
1028
|
+
await nivaro.request(bulkNotificationAction({ action: 'archive', filter: { status: 'read', lane: 'fyi' } }))
|
|
1029
|
+
await nivaro.request(archiveReadNotifications(30)) // read rows older than 30 days
|
|
1030
|
+
|
|
1031
|
+
// What I sent, with each recipient's read / acknowledgement state
|
|
1032
|
+
const { data: sent } = await nivaro.request(listSentNotifications({ lane: 'all' }))
|
|
1033
|
+
|
|
1034
|
+
// Mute one record's notifications for me
|
|
1035
|
+
const { data: mute } = await nivaro.request(toggleRecordMute('orders', 42)) // { muted: true }
|
|
931
1036
|
```
|
|
932
1037
|
|
|
933
|
-
####
|
|
1038
|
+
#### Delivery history and reminders
|
|
1039
|
+
|
|
1040
|
+
```typescript
|
|
1041
|
+
import { readNotificationHistory, listReminders, createReminder, deleteReminder } from '@nivaro/sdk'
|
|
1042
|
+
|
|
1043
|
+
// What actually reached me: in-app rows, alert firings, report alerts, digest-queued emails
|
|
1044
|
+
const { data: history } = await nivaro.request(readNotificationHistory(80))
|
|
1045
|
+
// history[i] → { kind, at, title, detail, collection?, item?, status?, channels: ['in-app', 'push'] }
|
|
1046
|
+
|
|
1047
|
+
const { data: r } = await nivaro.request(createReminder({ note: 'Call vendor', remind_at: '2026-11-03T14:00:00Z' }))
|
|
1048
|
+
const { data: mine } = await nivaro.request(listReminders())
|
|
1049
|
+
await nivaro.request(deleteReminder(r.id))
|
|
1050
|
+
```
|
|
934
1051
|
|
|
935
|
-
|
|
1052
|
+
#### Live notifications
|
|
936
1053
|
|
|
937
1054
|
```typescript
|
|
938
|
-
import { createRealtime } from '@nivaro/sdk'
|
|
1055
|
+
import { createRealtime, NOTIFICATIONS_CHANNEL, readWsToken } from '@nivaro/sdk'
|
|
939
1056
|
|
|
940
1057
|
const rt = createRealtime()
|
|
941
|
-
|
|
1058
|
+
rt.connect('https://nivaro.example.com', () => nivaro.request(readWsToken()).then((r) => r.token))
|
|
942
1059
|
|
|
943
|
-
//
|
|
944
|
-
rt.subscribe(
|
|
945
|
-
console.log('New notification:', notif.subject)
|
|
946
|
-
// Update badge, toast, etc.
|
|
947
|
-
})
|
|
1060
|
+
// notification:new for the signed-in person
|
|
1061
|
+
rt.subscribe(NOTIFICATIONS_CHANNEL, {}, ([notif]) => console.log('New notification', notif))
|
|
948
1062
|
```
|
|
949
1063
|
|
|
950
|
-
| Field | Type | Description |
|
|
951
|
-
| --- | --- | --- |
|
|
952
|
-
| id | string | UUID. |
|
|
953
|
-
| subject | string | Notification title. |
|
|
954
|
-
| message | string | Full message body. |
|
|
955
|
-
| status | string | "inbox" | "read" |
|
|
956
|
-
| timestamp | string | ISO 8601 when created. |
|
|
957
|
-
| collection | string | null | Related collection (if from an item event). |
|
|
958
|
-
| item | string | null | Related item ID. |
|
|
959
|
-
| sender | string | null | User who triggered it (if applicable). |
|
|
960
|
-
|
|
961
1064
|
---
|
|
962
1065
|
|
|
963
|
-
## SDK — Activity &
|
|
1066
|
+
## SDK — Activity, Revisions & Logs
|
|
964
1067
|
|
|
965
|
-
The
|
|
1068
|
+
The audit log records every change and notable action; revisions are a record's own history; the change feed, request log, job runs and mail log answer "what happened" from the API itself. Reads of the system-wide logs are for administrators.
|
|
966
1069
|
|
|
967
|
-
####
|
|
1070
|
+
#### Audit log (administrators)
|
|
968
1071
|
|
|
969
1072
|
```typescript
|
|
970
|
-
import { readActivity } from '@nivaro/sdk'
|
|
1073
|
+
import { readActivity, readActivityEntry, listActivityActions, logExport } from '@nivaro/sdk'
|
|
971
1074
|
|
|
972
|
-
//
|
|
1075
|
+
// Newest first; limit default 25, max 200
|
|
973
1076
|
const { data: entries, total } = await nivaro.request(
|
|
974
|
-
readActivity({
|
|
975
|
-
)
|
|
976
|
-
// entries → Activity[] — { id, action, collection, item, user_id, timestamp, ... }
|
|
977
|
-
|
|
978
|
-
// Filter by collection, action, or user
|
|
979
|
-
const { data: creates } = await nivaro.request(
|
|
980
|
-
readActivity({
|
|
981
|
-
collection: 'inventory_requests',
|
|
982
|
-
action: 'create', // 'create' | 'update' | 'delete' | 'schema-*'
|
|
983
|
-
user_id: 'user-uuid', // optional
|
|
984
|
-
limit: 25,
|
|
985
|
-
})
|
|
1077
|
+
readActivity({ collection: 'orders', action: 'update', user: 'user-uuid', limit: 50 })
|
|
986
1078
|
)
|
|
1079
|
+
// entries[i] → { id, action, collection, item, user, timestamp, ip, comment, origin,
|
|
1080
|
+
// first_name, last_name, user_email, via_agent }
|
|
1081
|
+
|
|
1082
|
+
const { data: entry } = await nivaro.request(readActivityEntry(entries[0].id)) // + user_agent
|
|
1083
|
+
const { data: actions } = await nivaro.request(listActivityActions()) // every action in the log
|
|
1084
|
+
|
|
1085
|
+
// Anyone signed in: report an export made in the browser (an `export` audit row)
|
|
1086
|
+
await nivaro.request(logExport({ collection: 'orders', row_count: 120, format: 'csv' }))
|
|
987
1087
|
```
|
|
988
1088
|
|
|
989
|
-
#### Revisions (
|
|
1089
|
+
#### Revisions (a record’s history)
|
|
990
1090
|
|
|
991
1091
|
```typescript
|
|
992
|
-
import { readRevisions, readRevision } from '@nivaro/sdk'
|
|
1092
|
+
import { readRevisions, readRevision, readLatestRevisionId, rollbackRevision } from '@nivaro/sdk'
|
|
993
1093
|
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
)
|
|
998
|
-
|
|
1094
|
+
// Newest first: up to 100 revisions plus folded pipeline moves and O2M child events.
|
|
1095
|
+
// Same gate as opening the record; snapshots are cut to the fields you may read. No paging.
|
|
1096
|
+
const { data: revisions } = await nivaro.request(readRevisions('orders', 42))
|
|
1097
|
+
for (const rev of revisions) {
|
|
1098
|
+
if (rev.event) console.log('moved', rev.event.from_label, '→', rev.event.to_label)
|
|
1099
|
+
else console.log(rev.action, 'by', rev.first_name, 'changed', Object.keys(rev.delta ?? {}))
|
|
1100
|
+
}
|
|
999
1101
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
})
|
|
1102
|
+
const { data: rev } = await nivaro.request(readRevision(revisions[0].id!))
|
|
1103
|
+
const { data: latest } = await nivaro.request(readLatestRevisionId('orders', 42)) // { latest }
|
|
1104
|
+
await nivaro.request(rollbackRevision(rev.id!)) // through the items service, as you
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
#### Change feed
|
|
1008
1108
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1109
|
+
```typescript
|
|
1110
|
+
import { readChanges } from '@nivaro/sdk'
|
|
1111
|
+
|
|
1112
|
+
// Ids, actions and changed field NAMES only, for records you can see now
|
|
1113
|
+
let page = (await nivaro.request(readChanges({ since: '2026-10-29T12:00:00Z', collections: ['orders'] }))).data
|
|
1114
|
+
while (page.has_more) page = (await nivaro.request(readChanges({ cursor: page.cursor }))).data
|
|
1115
|
+
// keep page.cursor and call again later for what changed since
|
|
1013
1116
|
```
|
|
1014
1117
|
|
|
1015
|
-
####
|
|
1118
|
+
#### API request log, key usage, jobs and mail (administrators)
|
|
1016
1119
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1120
|
+
```typescript
|
|
1121
|
+
import {
|
|
1122
|
+
listApiRequests, readApiAnalyticsByKey, readApiKeyUsage, readApiKeyMonthlyUsage,
|
|
1123
|
+
listJobRuns, readJobRunLog, cancelJobRun, readBusyJobs,
|
|
1124
|
+
listMailLog, readMailLogEntry, resendMail, listRecordMail
|
|
1125
|
+
} from '@nivaro/sdk'
|
|
1126
|
+
|
|
1127
|
+
const { data: failures } = await nivaro.request(listApiRequests({ status: '5xx', hours: 6 }))
|
|
1128
|
+
const { data: byKey } = await nivaro.request(readApiAnalyticsByKey(24))
|
|
1129
|
+
const { data: usage } = await nivaro.request(readApiKeyUsage(keyId)) // last 30 days
|
|
1130
|
+
const csv = await nivaro.request(readApiKeyMonthlyUsage(keyId, '2026-10', 'csv'))
|
|
1131
|
+
|
|
1132
|
+
const { data: runs } = await nivaro.request(listJobRuns({ status: 'error' }))
|
|
1133
|
+
const { data: log } = await nivaro.request(readJobRunLog(runs[0].id)) // { run, lines }
|
|
1134
|
+
const { data: busy } = await nivaro.request(readBusyJobs())
|
|
1135
|
+
|
|
1136
|
+
const { data: mails } = await nivaro.request(listMailLog({ status: 'failed' }))
|
|
1137
|
+
await nivaro.request(resendMail(mails[0].id))
|
|
1138
|
+
// Anyone who can read the record: the mail sent about it (headers only)
|
|
1139
|
+
const { data: recordMail } = await nivaro.request(listRecordMail('orders', 42))
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
#### Your own jobs and background reads
|
|
1143
|
+
|
|
1144
|
+
```typescript
|
|
1145
|
+
import { listMyJobs, startBackgroundRun, readBackgroundRun, readBackgroundRunResult } from '@nivaro/sdk'
|
|
1146
|
+
|
|
1147
|
+
const { data: jobs } = await nivaro.request(listMyJobs()) // running or finished in the last 30 min
|
|
1148
|
+
|
|
1149
|
+
// Hand a slow read to the server; it is replayed as you
|
|
1150
|
+
const { data: run } = await nivaro.request(
|
|
1151
|
+
startBackgroundRun({ method: 'POST', url: '/api/custom-queries/q/execute', body: { params: {} } })
|
|
1152
|
+
)
|
|
1153
|
+
const { data: now } = await nivaro.request(readBackgroundRun(run.id))
|
|
1154
|
+
if (now.status === 'completed') console.log(await nivaro.request(readBackgroundRunResult(run.id)))
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
> **Note:** Every API answer carries an `x-nivaro-request-id` header — the id the slow-trace buffer, the access log and the request log share. The client puts it on `NivaroError.requestId` and on the `onResponse` hook (see Setup). A cross-origin browser call can only read it when the API exposes the header through CORS.
|
|
1030
1158
|
|
|
1031
1159
|
---
|
|
1032
1160
|
|
|
@@ -1216,13 +1344,13 @@ console.log('Created:', result.createArticle.id)
|
|
|
1216
1344
|
// GraphQL subscriptions require a separate WebSocket transport
|
|
1217
1345
|
// Use the Socket.io realtime client instead — it's simpler and handles reconnection
|
|
1218
1346
|
|
|
1219
|
-
import { createRealtime } from '@nivaro/sdk'
|
|
1347
|
+
import { createRealtime, readWsToken } from '@nivaro/sdk'
|
|
1220
1348
|
|
|
1221
1349
|
const rt = createRealtime()
|
|
1222
|
-
|
|
1350
|
+
rt.connect('https://nivaro.example.com', () => nivaro.request(readWsToken()).then((r) => r.token))
|
|
1223
1351
|
|
|
1224
|
-
rt.subscribe('articles', { event: 'update' }, (
|
|
1225
|
-
console.log('
|
|
1352
|
+
rt.subscribe('articles', { event: 'update' }, (changes) => {
|
|
1353
|
+
console.log('Articles changed:', changes.map((c) => c.item))
|
|
1226
1354
|
})
|
|
1227
1355
|
```
|
|
1228
1356
|
|
|
@@ -1363,74 +1491,49 @@ if (fileId) {
|
|
|
1363
1491
|
|
|
1364
1492
|
## SDK — Realtime (Socket.io)
|
|
1365
1493
|
|
|
1366
|
-
|
|
1494
|
+
Hear record changes and notifications as they happen. `createRealtime()` signs the socket in, joins one read-gated room per subscribed collection, and re-joins after every reconnect. Events carry ids, the action and changed field NAMES only — never values; read the record through the REST API when you need it.
|
|
1367
1495
|
|
|
1368
|
-
####
|
|
1496
|
+
#### Connect and sign in
|
|
1369
1497
|
|
|
1370
1498
|
```typescript
|
|
1371
|
-
import { createRealtime } from '@nivaro/sdk'
|
|
1499
|
+
import { createNivaro, createRealtime, readWsToken } from '@nivaro/sdk'
|
|
1372
1500
|
|
|
1373
|
-
|
|
1501
|
+
const nivaro = createNivaro('https://nivaro.example.com', { token: process.env.NIVARO_TOKEN })
|
|
1374
1502
|
const rt = createRealtime()
|
|
1375
|
-
|
|
1503
|
+
|
|
1504
|
+
// A ws-token works for every sign-in (session cookie, API key, static token). It is single-use,
|
|
1505
|
+
// so pass a getter: it is asked again on every reconnect.
|
|
1506
|
+
rt.connect('https://nivaro.example.com', () => nivaro.request(readWsToken()).then((r) => r.token))
|
|
1507
|
+
|
|
1508
|
+
// A user's static token or a View-as token may be passed as a plain string instead.
|
|
1509
|
+
rt.onAuthenticated((userId) => console.log('signed in as', userId))
|
|
1376
1510
|
```
|
|
1377
1511
|
|
|
1378
|
-
#### Subscribe to
|
|
1512
|
+
#### Subscribe to a collection
|
|
1379
1513
|
|
|
1380
1514
|
```typescript
|
|
1381
|
-
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
console.log('Article updated:', article)
|
|
1387
|
-
// article → full updated item snapshot
|
|
1515
|
+
const unsubscribe = rt.subscribe('orders', { event: 'update' }, (changes) => {
|
|
1516
|
+
for (const c of changes) {
|
|
1517
|
+
// c → { collection, item, action, changed_fields?, source?, coalesced?, many? }
|
|
1518
|
+
if (c.many) refetchList() // a burst too large to list ids
|
|
1519
|
+
else refetchRow(c.item)
|
|
1388
1520
|
}
|
|
1389
|
-
)
|
|
1390
|
-
|
|
1391
|
-
// Subscribe to updates on a specific item
|
|
1392
|
-
rt.subscribe(
|
|
1393
|
-
'articles:123',
|
|
1394
|
-
{ event: 'update' },
|
|
1395
|
-
(article) => console.log('This article changed')
|
|
1396
|
-
)
|
|
1521
|
+
})
|
|
1397
1522
|
|
|
1398
|
-
//
|
|
1399
|
-
rt.subscribe(
|
|
1400
|
-
`user:${userId}`,
|
|
1401
|
-
{ event: 'notification:new' },
|
|
1402
|
-
(notif) => console.log('New notification:', notif.subject)
|
|
1403
|
-
)
|
|
1523
|
+
rt.subscribe('orders', { event: '*' }, (changes) => {}) // create + update + delete
|
|
1404
1524
|
|
|
1405
|
-
//
|
|
1406
|
-
rt.
|
|
1407
|
-
'articles:123',
|
|
1408
|
-
{ event: 'presence' },
|
|
1409
|
-
(users) => console.log(`${users.length} users viewing`)
|
|
1410
|
-
)
|
|
1525
|
+
unsubscribe() // leaves the room when no subscription on it is left
|
|
1526
|
+
rt.disconnect()
|
|
1411
1527
|
```
|
|
1412
1528
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
| Event | Fires On | Data |
|
|
1529
|
+
| Event | Fires on | Payload (one RealtimeChange per record) |
|
|
1416
1530
|
| --- | --- | --- |
|
|
1417
|
-
| create |
|
|
1418
|
-
| update |
|
|
1419
|
-
| delete |
|
|
1420
|
-
| notification:new
|
|
1421
|
-
| presence | User joins/leaves/edits | Array of currently viewing users |
|
|
1531
|
+
| create | A record was created | { collection, item, action: "create" } |
|
|
1532
|
+
| update | A record changed | { collection, item, action: "update", changed_fields? } |
|
|
1533
|
+
| delete | A record was deleted | { collection, item, action: "delete" } |
|
|
1534
|
+
| NOTIFICATIONS_CHANNEL | notification:new for the signed-in person | The notification object |
|
|
1422
1535
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
```typescript
|
|
1426
|
-
// Unsubscribe from a room
|
|
1427
|
-
unsubscribe()
|
|
1428
|
-
|
|
1429
|
-
// Disconnect from server (closes all subscriptions)
|
|
1430
|
-
rt.disconnect()
|
|
1431
|
-
```
|
|
1432
|
-
|
|
1433
|
-
> **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.
|
|
1536
|
+
> **Note:** Wire protocol: the client emits `auth {token}` after connecting (the server answers `auth:ok`), then `collection:join {collection}` per room; the server sends `collection:update` (bursts coalesced as `{ items, many, count, coalesced: true }`) and `notification:new`. A collection whose realtime mode is `off` sends nothing — poll it. SDK 0.2.x and earlier emitted `subscribe` and listened for `items:*`, which the server never handled.
|
|
1434
1537
|
|
|
1435
1538
|
---
|
|
1436
1539
|
|
|
@@ -2423,43 +2526,31 @@ Real-time presence tracking via Socket.io. See who is currently viewing/editing
|
|
|
2423
2526
|
#### REST: Query presence
|
|
2424
2527
|
|
|
2425
2528
|
```typescript
|
|
2426
|
-
import {
|
|
2529
|
+
import { getPresence, listActivePresence, readOnlinePresence } from '@nivaro/sdk'
|
|
2530
|
+
|
|
2531
|
+
// Who is viewing a specific record right now (de-duplicated by user/session)
|
|
2532
|
+
const { data: viewers } = await nivaro.request(getPresence('contracts', '99'))
|
|
2427
2533
|
|
|
2428
|
-
// Who is
|
|
2429
|
-
const
|
|
2430
|
-
// viewers → Presence[] — { user_id, first_name, last_name, email, is_editing, last_heartbeat }
|
|
2534
|
+
// Who is online (the Online list, filtered to what you may see)
|
|
2535
|
+
const online = await nivaro.request(readOnlinePresence())
|
|
2431
2536
|
|
|
2432
|
-
//
|
|
2433
|
-
const { data: sessions, total } = await nivaro.request(
|
|
2434
|
-
// sessions → Presence[] (paginated; default 100 per page)
|
|
2537
|
+
// Every active page session across the instance (admin)
|
|
2538
|
+
const { data: sessions, total } = await nivaro.request(listActivePresence())
|
|
2435
2539
|
```
|
|
2436
2540
|
|
|
2437
|
-
####
|
|
2541
|
+
#### Report your own presence
|
|
2438
2542
|
|
|
2439
2543
|
```typescript
|
|
2440
|
-
import {
|
|
2544
|
+
import { createPresence } from '@nivaro/sdk'
|
|
2441
2545
|
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
//
|
|
2446
|
-
rt.presence.subscribe('contracts:99', (users) => {
|
|
2447
|
-
console.log(`${users.length} users viewing`)
|
|
2448
|
-
users.forEach(u => {
|
|
2449
|
-
if (u.is_editing) console.log(`${u.first_name} is editing`)
|
|
2450
|
-
})
|
|
2451
|
-
})
|
|
2452
|
-
|
|
2453
|
-
// Announce that you are editing
|
|
2454
|
-
rt.presence.setEditing('contracts:99', true)
|
|
2455
|
-
|
|
2456
|
-
// Stop editing
|
|
2457
|
-
rt.presence.setEditing('contracts:99', false)
|
|
2458
|
-
|
|
2459
|
-
// Leave the presence room
|
|
2460
|
-
rt.presence.leave('contracts:99')
|
|
2546
|
+
// Starts at once: pings POST /api/presence/ping every 30 s (and when the tab is shown again)
|
|
2547
|
+
const presence = createPresence({ apiUrl: 'https://nivaro.example.com', userId, userName })
|
|
2548
|
+
presence.setUser(newUserId) // after a sign-in change
|
|
2549
|
+
presence.destroy() // stop pinging and send the disconnect beacon
|
|
2461
2550
|
```
|
|
2462
2551
|
|
|
2552
|
+
> **Note:** Record changes and notifications arrive over the realtime client (`createRealtime`, see Realtime). Presence on records is read over REST; poll `getPresence` while a record is open.
|
|
2553
|
+
|
|
2463
2554
|
#### Soft edit locks (item locking)
|
|
2464
2555
|
|
|
2465
2556
|
Pair presence with item locking to prevent conflicting edits:
|