@centrali-io/centrali-sdk 2.0.6 → 2.0.9
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 +99 -0
- package/dist/index.js +461 -13
- package/index.ts +661 -17
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ const centrali = new CentraliSDK({
|
|
|
73
73
|
- ✅ **Automatic authentication** and token management
|
|
74
74
|
- ✅ **Records management** - Create, read, update, delete records
|
|
75
75
|
- ✅ **Query operations** - Powerful data querying with filters and sorting
|
|
76
|
+
- ✅ **Realtime events** - Subscribe to record changes via SSE
|
|
76
77
|
- ✅ **Compute functions** - Execute serverless functions
|
|
77
78
|
- ✅ **File uploads** - Upload files to Centrali storage
|
|
78
79
|
- ✅ **Service accounts** - Automatic token refresh for server-to-server auth
|
|
@@ -107,6 +108,104 @@ const products = await centrali.queryRecords('Product', {
|
|
|
107
108
|
});
|
|
108
109
|
```
|
|
109
110
|
|
|
111
|
+
### Realtime Events
|
|
112
|
+
|
|
113
|
+
Subscribe to record changes in real-time using Server-Sent Events (SSE):
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Subscribe to realtime events
|
|
117
|
+
const subscription = centrali.realtime.subscribe({
|
|
118
|
+
structures: ['Order'], // Filter by structure (optional)
|
|
119
|
+
events: ['record_created', 'record_updated'], // Filter by event type (optional)
|
|
120
|
+
filter: 'status = "pending"', // CFL filter expression (optional)
|
|
121
|
+
|
|
122
|
+
onEvent: (event) => {
|
|
123
|
+
console.log('Event received:', event.event);
|
|
124
|
+
console.log('Record:', event.recordSlug, event.recordId);
|
|
125
|
+
console.log('Data:', event.data);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
onConnected: () => {
|
|
129
|
+
console.log('Connected to realtime service');
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
onDisconnected: (reason) => {
|
|
133
|
+
console.log('Disconnected:', reason);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
onError: (error) => {
|
|
137
|
+
console.error('Realtime error:', error.code, error.message);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Later: cleanup subscription
|
|
142
|
+
subscription.unsubscribe();
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Initial Sync Pattern
|
|
146
|
+
|
|
147
|
+
Realtime delivers only **new events** after connection. For dashboards and lists, always:
|
|
148
|
+
|
|
149
|
+
1. Fetch current records first
|
|
150
|
+
2. Subscribe to realtime
|
|
151
|
+
3. Apply diffs while UI shows the snapshot
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// 1. Fetch initial data
|
|
155
|
+
const orders = await centrali.queryRecords('Order', {
|
|
156
|
+
filter: 'status = "pending"'
|
|
157
|
+
});
|
|
158
|
+
renderOrders(orders.data);
|
|
159
|
+
|
|
160
|
+
// 2. Subscribe to realtime updates
|
|
161
|
+
const subscription = centrali.realtime.subscribe({
|
|
162
|
+
structures: ['Order'],
|
|
163
|
+
events: ['record_created', 'record_updated', 'record_deleted'],
|
|
164
|
+
filter: 'status = "pending"',
|
|
165
|
+
onEvent: (event) => {
|
|
166
|
+
// 3. Apply updates to UI
|
|
167
|
+
switch (event.event) {
|
|
168
|
+
case 'record_created':
|
|
169
|
+
addOrderToUI(event.data);
|
|
170
|
+
break;
|
|
171
|
+
case 'record_updated':
|
|
172
|
+
updateOrderInUI(event.recordId, event.data);
|
|
173
|
+
break;
|
|
174
|
+
case 'record_deleted':
|
|
175
|
+
removeOrderFromUI(event.recordId);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### Event Types
|
|
183
|
+
|
|
184
|
+
| Event | Description |
|
|
185
|
+
|-------|-------------|
|
|
186
|
+
| `record_created` | A new record was created |
|
|
187
|
+
| `record_updated` | An existing record was updated |
|
|
188
|
+
| `record_deleted` | A record was deleted |
|
|
189
|
+
|
|
190
|
+
#### Reconnection
|
|
191
|
+
|
|
192
|
+
The SDK automatically reconnects with exponential backoff when connections drop. Configure reconnection behavior:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { RealtimeManager } from '@centrali-io/centrali-sdk';
|
|
196
|
+
|
|
197
|
+
const realtime = new RealtimeManager(
|
|
198
|
+
'https://centrali.io',
|
|
199
|
+
'your-workspace',
|
|
200
|
+
() => centrali.getToken(),
|
|
201
|
+
{
|
|
202
|
+
maxReconnectAttempts: 10, // Default: 10
|
|
203
|
+
initialReconnectDelayMs: 1000, // Default: 1000ms
|
|
204
|
+
maxReconnectDelayMs: 30000 // Default: 30000ms
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
```
|
|
208
|
+
|
|
110
209
|
### Compute Functions
|
|
111
210
|
|
|
112
211
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* Centrali TypeScript SDK
|
|
4
4
|
* ----------------------
|
|
5
|
-
* A lightweight SDK for interacting with Centrali's Data and
|
|
5
|
+
* A lightweight SDK for interacting with Centrali's Data, Compute, and Realtime APIs,
|
|
6
6
|
* with support for user-provided tokens or client credentials (Client ID/Secret).
|
|
7
7
|
*/
|
|
8
8
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
@@ -18,15 +18,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.CentraliSDK = void 0;
|
|
21
|
+
exports.CentraliSDK = exports.TriggersManager = exports.RealtimeManager = void 0;
|
|
22
22
|
exports.getApiUrl = getApiUrl;
|
|
23
23
|
exports.getAuthUrl = getAuthUrl;
|
|
24
|
+
exports.getRealtimeUrl = getRealtimeUrl;
|
|
24
25
|
exports.fetchClientToken = fetchClientToken;
|
|
25
26
|
exports.getRecordApiPath = getRecordApiPath;
|
|
26
27
|
exports.getFileUploadApiPath = getFileUploadApiPath;
|
|
27
|
-
exports.
|
|
28
|
+
exports.getFunctionTriggersApiPath = getFunctionTriggersApiPath;
|
|
29
|
+
exports.getFunctionTriggerExecuteApiPath = getFunctionTriggerExecuteApiPath;
|
|
28
30
|
const axios_1 = __importDefault(require("axios"));
|
|
29
31
|
const qs_1 = __importDefault(require("qs"));
|
|
32
|
+
const eventsource_1 = require("eventsource");
|
|
33
|
+
// Use native EventSource in browser, polyfill in Node.js
|
|
34
|
+
const EventSourceImpl = typeof EventSource !== 'undefined'
|
|
35
|
+
? EventSource
|
|
36
|
+
: eventsource_1.EventSource;
|
|
30
37
|
// Helper to encode form data
|
|
31
38
|
function encodeFormData(data) {
|
|
32
39
|
return new URLSearchParams(data).toString();
|
|
@@ -55,6 +62,230 @@ function getAuthUrl(baseUrl) {
|
|
|
55
62
|
: url.hostname;
|
|
56
63
|
return `${url.protocol}//auth.${hostname}`;
|
|
57
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate the realtime service URL from the base URL.
|
|
67
|
+
* E.g., https://centrali.io -> https://api.centrali.io/realtime
|
|
68
|
+
*/
|
|
69
|
+
function getRealtimeUrl(baseUrl) {
|
|
70
|
+
return `${getApiUrl(baseUrl)}/realtime`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate the SSE endpoint path for a workspace.
|
|
74
|
+
* Matches: services/backend/realtime/internal/sse/handler.go ServeHTTP route
|
|
75
|
+
*/
|
|
76
|
+
function getRealtimeEventPath(workspaceSlug) {
|
|
77
|
+
return `/workspace/${workspaceSlug}/events`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Default realtime configuration.
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_REALTIME_CONFIG = {
|
|
83
|
+
maxReconnectAttempts: 10,
|
|
84
|
+
initialReconnectDelayMs: 1000,
|
|
85
|
+
maxReconnectDelayMs: 30000,
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* RealtimeManager handles SSE connections to the Centrali Realtime Service.
|
|
89
|
+
* Provides automatic reconnection with exponential backoff.
|
|
90
|
+
*
|
|
91
|
+
* Usage:
|
|
92
|
+
* ```ts
|
|
93
|
+
* const realtime = new RealtimeManager(baseUrl, workspaceSlug, () => client.getToken());
|
|
94
|
+
* const sub = realtime.subscribe({
|
|
95
|
+
* structures: ['order'],
|
|
96
|
+
* events: ['record_created', 'record_updated'],
|
|
97
|
+
* onEvent: (event) => console.log(event),
|
|
98
|
+
* onError: (error) => console.error(error),
|
|
99
|
+
* });
|
|
100
|
+
* // Later: sub.unsubscribe();
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
class RealtimeManager {
|
|
104
|
+
constructor(baseUrl, workspaceSlug, getToken, config) {
|
|
105
|
+
this.baseUrl = baseUrl;
|
|
106
|
+
this.workspaceSlug = workspaceSlug;
|
|
107
|
+
this.getToken = getToken;
|
|
108
|
+
this.config = Object.assign(Object.assign({}, DEFAULT_REALTIME_CONFIG), config);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Subscribe to realtime events for the workspace.
|
|
112
|
+
*
|
|
113
|
+
* IMPORTANT: Initial Sync Pattern
|
|
114
|
+
* Realtime delivers only new events after connection. For dashboards and lists:
|
|
115
|
+
* 1. Fetch current records first
|
|
116
|
+
* 2. Subscribe to realtime
|
|
117
|
+
* 3. Apply diffs while UI shows the snapshot
|
|
118
|
+
*
|
|
119
|
+
* @param options - Subscription options
|
|
120
|
+
* @returns Subscription handle with unsubscribe() method
|
|
121
|
+
*/
|
|
122
|
+
subscribe(options) {
|
|
123
|
+
let eventSource = null;
|
|
124
|
+
let unsubscribed = false;
|
|
125
|
+
let connected = false;
|
|
126
|
+
let reconnectAttempt = 0;
|
|
127
|
+
let reconnectTimeout = null;
|
|
128
|
+
const connect = () => __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
var _a, _b, _c, _d;
|
|
130
|
+
// Zombie loop prevention: don't reconnect if unsubscribed
|
|
131
|
+
if (unsubscribed) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Get token (may be async for client credentials flow)
|
|
136
|
+
const token = yield Promise.resolve(this.getToken());
|
|
137
|
+
if (!token) {
|
|
138
|
+
(_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, {
|
|
139
|
+
code: 'MISSING_TOKEN',
|
|
140
|
+
message: 'No authentication token available',
|
|
141
|
+
recoverable: false,
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Build SSE URL with query params
|
|
146
|
+
const realtimeBaseUrl = getRealtimeUrl(this.baseUrl);
|
|
147
|
+
const path = getRealtimeEventPath(this.workspaceSlug);
|
|
148
|
+
const url = new URL(`${realtimeBaseUrl}${path}`);
|
|
149
|
+
// Add access token
|
|
150
|
+
url.searchParams.set('access_token', token);
|
|
151
|
+
// Add structure filter
|
|
152
|
+
if ((_b = options.structures) === null || _b === void 0 ? void 0 : _b.length) {
|
|
153
|
+
url.searchParams.set('structures', options.structures.join(','));
|
|
154
|
+
}
|
|
155
|
+
// Add event type filter
|
|
156
|
+
if ((_c = options.events) === null || _c === void 0 ? void 0 : _c.length) {
|
|
157
|
+
url.searchParams.set('events', options.events.join(','));
|
|
158
|
+
}
|
|
159
|
+
// Add CFL filter
|
|
160
|
+
if (options.filter) {
|
|
161
|
+
url.searchParams.set('filter', options.filter);
|
|
162
|
+
}
|
|
163
|
+
// Create EventSource (uses polyfill in Node.js)
|
|
164
|
+
eventSource = new EventSourceImpl(url.toString());
|
|
165
|
+
// Handle connection open
|
|
166
|
+
eventSource.onopen = () => {
|
|
167
|
+
var _a;
|
|
168
|
+
if (unsubscribed) {
|
|
169
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
connected = true;
|
|
173
|
+
reconnectAttempt = 0;
|
|
174
|
+
(_a = options.onConnected) === null || _a === void 0 ? void 0 : _a.call(options);
|
|
175
|
+
};
|
|
176
|
+
// Handle record events - server sends all record events as 'message' type
|
|
177
|
+
// The event.event field inside the payload contains the actual type
|
|
178
|
+
// (record_created, record_updated, record_deleted)
|
|
179
|
+
eventSource.addEventListener('message', (e) => {
|
|
180
|
+
var _a;
|
|
181
|
+
if (unsubscribed)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
const event = JSON.parse(e.data);
|
|
185
|
+
options.onEvent(event);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
(_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, {
|
|
189
|
+
code: 'PARSE_ERROR',
|
|
190
|
+
message: `Failed to parse event: ${err}`,
|
|
191
|
+
recoverable: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// Handle close event from server
|
|
196
|
+
eventSource.addEventListener('close', (e) => {
|
|
197
|
+
var _a;
|
|
198
|
+
if (unsubscribed)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
const closeEvent = JSON.parse(e.data);
|
|
202
|
+
connected = false;
|
|
203
|
+
(_a = options.onDisconnected) === null || _a === void 0 ? void 0 : _a.call(options, closeEvent.reason);
|
|
204
|
+
// Reconnect if server says to
|
|
205
|
+
if (closeEvent.reconnect && !unsubscribed) {
|
|
206
|
+
scheduleReconnect();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (_b) {
|
|
210
|
+
// Ignore parse errors for close events
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Handle errors
|
|
214
|
+
eventSource.onerror = () => {
|
|
215
|
+
var _a, _b;
|
|
216
|
+
if (unsubscribed) {
|
|
217
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
connected = false;
|
|
221
|
+
eventSource === null || eventSource === void 0 ? void 0 : eventSource.close();
|
|
222
|
+
eventSource = null;
|
|
223
|
+
// EventSource error events don't provide much detail
|
|
224
|
+
// The connection will be closed, so we notify and potentially reconnect
|
|
225
|
+
(_a = options.onDisconnected) === null || _a === void 0 ? void 0 : _a.call(options, 'connection_error');
|
|
226
|
+
(_b = options.onError) === null || _b === void 0 ? void 0 : _b.call(options, {
|
|
227
|
+
code: 'CONNECTION_ERROR',
|
|
228
|
+
message: 'Connection to realtime service failed',
|
|
229
|
+
recoverable: true,
|
|
230
|
+
});
|
|
231
|
+
scheduleReconnect();
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
(_d = options.onError) === null || _d === void 0 ? void 0 : _d.call(options, {
|
|
236
|
+
code: 'CONNECTION_ERROR',
|
|
237
|
+
message: `Failed to connect: ${err}`,
|
|
238
|
+
recoverable: true,
|
|
239
|
+
});
|
|
240
|
+
scheduleReconnect();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const scheduleReconnect = () => {
|
|
244
|
+
var _a;
|
|
245
|
+
// Zombie loop prevention
|
|
246
|
+
if (unsubscribed)
|
|
247
|
+
return;
|
|
248
|
+
reconnectAttempt++;
|
|
249
|
+
if (reconnectAttempt > this.config.maxReconnectAttempts) {
|
|
250
|
+
(_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, {
|
|
251
|
+
code: 'CONNECTION_ERROR',
|
|
252
|
+
message: `Max reconnection attempts (${this.config.maxReconnectAttempts}) exceeded`,
|
|
253
|
+
recoverable: false,
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Exponential backoff with jitter
|
|
258
|
+
const delay = Math.min(this.config.initialReconnectDelayMs * Math.pow(2, reconnectAttempt - 1), this.config.maxReconnectDelayMs);
|
|
259
|
+
const jitter = Math.random() * 0.3 * delay; // 0-30% jitter
|
|
260
|
+
reconnectTimeout = setTimeout(() => {
|
|
261
|
+
if (!unsubscribed) {
|
|
262
|
+
connect();
|
|
263
|
+
}
|
|
264
|
+
}, delay + jitter);
|
|
265
|
+
};
|
|
266
|
+
// Start connection
|
|
267
|
+
connect();
|
|
268
|
+
// Return subscription handle
|
|
269
|
+
return {
|
|
270
|
+
unsubscribe: () => {
|
|
271
|
+
unsubscribed = true;
|
|
272
|
+
connected = false;
|
|
273
|
+
if (reconnectTimeout) {
|
|
274
|
+
clearTimeout(reconnectTimeout);
|
|
275
|
+
reconnectTimeout = null;
|
|
276
|
+
}
|
|
277
|
+
if (eventSource) {
|
|
278
|
+
eventSource.close();
|
|
279
|
+
eventSource = null;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
get connected() {
|
|
283
|
+
return connected;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
exports.RealtimeManager = RealtimeManager;
|
|
58
289
|
/**
|
|
59
290
|
* Retrieve an access token using the Client Credentials flow.
|
|
60
291
|
*/
|
|
@@ -88,18 +319,139 @@ function getRecordApiPath(workspaceId, recordSlug, id) {
|
|
|
88
319
|
function getFileUploadApiPath(workspaceId) {
|
|
89
320
|
return `storage/ws/${workspaceId}/api/v1/files`;
|
|
90
321
|
}
|
|
91
|
-
|
|
92
|
-
* Generate
|
|
322
|
+
/**
|
|
323
|
+
* Generate Function Triggers base API URL PATH.
|
|
324
|
+
*/
|
|
325
|
+
function getFunctionTriggersApiPath(workspaceId, triggerId) {
|
|
326
|
+
const basePath = `data/workspace/${workspaceId}/api/v1/function-triggers`;
|
|
327
|
+
return triggerId ? `${basePath}/${triggerId}` : basePath;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Generate Function Trigger execute API URL PATH.
|
|
331
|
+
*/
|
|
332
|
+
function getFunctionTriggerExecuteApiPath(workspaceId, triggerId) {
|
|
333
|
+
return `data/workspace/${workspaceId}/api/v1/function-triggers/${triggerId}/execute`;
|
|
334
|
+
}
|
|
335
|
+
// =====================================================
|
|
336
|
+
// Triggers Manager
|
|
337
|
+
// =====================================================
|
|
338
|
+
/**
|
|
339
|
+
* TriggersManager provides methods for working with on-demand function triggers.
|
|
340
|
+
* Access via `client.triggers`.
|
|
341
|
+
*
|
|
342
|
+
* Note: This manager only works with on-demand triggers. Scheduled, event-driven,
|
|
343
|
+
* and webhook triggers are managed through other mechanisms.
|
|
344
|
+
*
|
|
345
|
+
* Usage:
|
|
346
|
+
* ```ts
|
|
347
|
+
* // Invoke an on-demand trigger
|
|
348
|
+
* const result = await client.triggers.invoke('trigger-id');
|
|
349
|
+
*
|
|
350
|
+
* // Invoke with custom payload
|
|
351
|
+
* const result = await client.triggers.invoke('trigger-id', {
|
|
352
|
+
* payload: { customData: 'value' }
|
|
353
|
+
* });
|
|
354
|
+
*
|
|
355
|
+
* // Get an on-demand trigger by ID
|
|
356
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
357
|
+
*
|
|
358
|
+
* // List all on-demand triggers
|
|
359
|
+
* const triggers = await client.triggers.list();
|
|
360
|
+
* ```
|
|
93
361
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
362
|
+
class TriggersManager {
|
|
363
|
+
constructor(workspaceId, requestFn) {
|
|
364
|
+
this.workspaceId = workspaceId;
|
|
365
|
+
this.requestFn = requestFn;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Invoke an on-demand trigger by ID.
|
|
369
|
+
*
|
|
370
|
+
* @param triggerId - The ID of the trigger to invoke
|
|
371
|
+
* @param options - Optional invoke options including custom payload
|
|
372
|
+
* @returns The queued job ID for tracking the execution
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* // Simple invocation
|
|
377
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
378
|
+
* console.log('Job queued:', job.data);
|
|
379
|
+
*
|
|
380
|
+
* // With custom payload
|
|
381
|
+
* const job = await client.triggers.invoke('trigger-id', {
|
|
382
|
+
* payload: { orderId: '12345', action: 'process' }
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
invoke(triggerId, options) {
|
|
387
|
+
var _a;
|
|
388
|
+
const path = getFunctionTriggerExecuteApiPath(this.workspaceId, triggerId);
|
|
389
|
+
const data = (_a = options === null || options === void 0 ? void 0 : options.payload) !== null && _a !== void 0 ? _a : {};
|
|
390
|
+
return this.requestFn('POST', path, data);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get an on-demand trigger by ID.
|
|
394
|
+
*
|
|
395
|
+
* Note: This method validates that the trigger is an on-demand trigger.
|
|
396
|
+
* If the trigger exists but is not on-demand, an error will be thrown.
|
|
397
|
+
*
|
|
398
|
+
* @param triggerId - The ID of the on-demand trigger to retrieve
|
|
399
|
+
* @returns The trigger details
|
|
400
|
+
* @throws Error if the trigger is not an on-demand trigger
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```ts
|
|
404
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
405
|
+
* console.log('Trigger name:', trigger.data.name);
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
get(triggerId) {
|
|
409
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
410
|
+
const path = getFunctionTriggersApiPath(this.workspaceId, triggerId);
|
|
411
|
+
const response = yield this.requestFn('GET', path);
|
|
412
|
+
// Validate that the trigger is on-demand
|
|
413
|
+
if (response.data && response.data.executionType !== 'on-demand') {
|
|
414
|
+
throw new Error(`Trigger '${triggerId}' is not an on-demand trigger. Only on-demand triggers can be invoked via the SDK.`);
|
|
415
|
+
}
|
|
416
|
+
return response;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* List all on-demand triggers in the workspace.
|
|
421
|
+
*
|
|
422
|
+
* This method automatically filters to only return triggers with executionType 'on-demand'.
|
|
423
|
+
*
|
|
424
|
+
* @param queryParams - Optional query parameters for pagination, search, etc.
|
|
425
|
+
* @returns List of on-demand triggers with pagination metadata
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* ```ts
|
|
429
|
+
* // List all on-demand triggers
|
|
430
|
+
* const triggers = await client.triggers.list();
|
|
431
|
+
*
|
|
432
|
+
* // With pagination
|
|
433
|
+
* const triggers = await client.triggers.list({ limit: 10, page: 1 });
|
|
434
|
+
*
|
|
435
|
+
* // With search
|
|
436
|
+
* const triggers = await client.triggers.list({ search: 'process-order' });
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
list(queryParams) {
|
|
440
|
+
const path = getFunctionTriggersApiPath(this.workspaceId);
|
|
441
|
+
// Always filter for on-demand triggers only
|
|
442
|
+
const params = Object.assign(Object.assign({}, queryParams), { executionType: 'on-demand' });
|
|
443
|
+
return this.requestFn('GET', path, null, params);
|
|
444
|
+
}
|
|
96
445
|
}
|
|
446
|
+
exports.TriggersManager = TriggersManager;
|
|
97
447
|
/**
|
|
98
448
|
* Main Centrali SDK client.
|
|
99
449
|
*/
|
|
100
450
|
class CentraliSDK {
|
|
101
451
|
constructor(options) {
|
|
102
452
|
this.token = null;
|
|
453
|
+
this._realtime = null;
|
|
454
|
+
this._triggers = null;
|
|
103
455
|
this.options = options;
|
|
104
456
|
this.token = options.token || null;
|
|
105
457
|
const apiUrl = getApiUrl(options.baseUrl);
|
|
@@ -115,6 +467,69 @@ class CentraliSDK {
|
|
|
115
467
|
return config;
|
|
116
468
|
}), (error) => Promise.reject(error));
|
|
117
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Realtime namespace for subscribing to SSE events.
|
|
472
|
+
*
|
|
473
|
+
* Usage:
|
|
474
|
+
* ```ts
|
|
475
|
+
* const sub = client.realtime.subscribe({
|
|
476
|
+
* structures: ['order'],
|
|
477
|
+
* events: ['record_created', 'record_updated'],
|
|
478
|
+
* onEvent: (event) => console.log(event),
|
|
479
|
+
* });
|
|
480
|
+
* // Later: sub.unsubscribe();
|
|
481
|
+
* ```
|
|
482
|
+
*
|
|
483
|
+
* IMPORTANT: Initial Sync Pattern
|
|
484
|
+
* Realtime delivers only new events after connection. For dashboards and lists:
|
|
485
|
+
* 1. Fetch current records first
|
|
486
|
+
* 2. Subscribe to realtime
|
|
487
|
+
* 3. Apply diffs while UI shows the snapshot
|
|
488
|
+
*/
|
|
489
|
+
get realtime() {
|
|
490
|
+
if (!this._realtime) {
|
|
491
|
+
this._realtime = new RealtimeManager(this.options.baseUrl, this.options.workspaceId, () => __awaiter(this, void 0, void 0, function* () {
|
|
492
|
+
// If token exists, return it
|
|
493
|
+
if (this.token) {
|
|
494
|
+
return this.token;
|
|
495
|
+
}
|
|
496
|
+
// For client-credentials flow, fetch token if not available
|
|
497
|
+
if (this.options.clientId && this.options.clientSecret) {
|
|
498
|
+
this.token = yield fetchClientToken(this.options.clientId, this.options.clientSecret, this.options.baseUrl);
|
|
499
|
+
return this.token;
|
|
500
|
+
}
|
|
501
|
+
// No token and no credentials
|
|
502
|
+
return null;
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
return this._realtime;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Triggers namespace for invoking and managing function triggers.
|
|
509
|
+
*
|
|
510
|
+
* Usage:
|
|
511
|
+
* ```ts
|
|
512
|
+
* // Invoke an on-demand trigger
|
|
513
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
514
|
+
*
|
|
515
|
+
* // Invoke with custom payload
|
|
516
|
+
* const job = await client.triggers.invoke('trigger-id', {
|
|
517
|
+
* payload: { orderId: '12345' }
|
|
518
|
+
* });
|
|
519
|
+
*
|
|
520
|
+
* // Get trigger details
|
|
521
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
522
|
+
*
|
|
523
|
+
* // List all triggers
|
|
524
|
+
* const triggers = await client.triggers.list();
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
get triggers() {
|
|
528
|
+
if (!this._triggers) {
|
|
529
|
+
this._triggers = new TriggersManager(this.options.workspaceId, this.request.bind(this));
|
|
530
|
+
}
|
|
531
|
+
return this._triggers;
|
|
532
|
+
}
|
|
118
533
|
/**
|
|
119
534
|
* Manually set or update the bearer token for subsequent requests.
|
|
120
535
|
*/
|
|
@@ -181,12 +596,6 @@ class CentraliSDK {
|
|
|
181
596
|
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id);
|
|
182
597
|
return this.request('DELETE', path);
|
|
183
598
|
}
|
|
184
|
-
// ------------------ Compute API Methods ------------------
|
|
185
|
-
/** Invoke a compute function by name with given payload. */
|
|
186
|
-
invokeFunction(functionId, payload) {
|
|
187
|
-
const path = getComputeFunctionTriggerApiPath(this.options.workspaceId, functionId);
|
|
188
|
-
return this.request('POST', path, { data: payload });
|
|
189
|
-
}
|
|
190
599
|
// ------------------ Storage API Methods ------------------
|
|
191
600
|
/** Upload a file to the storage service. */
|
|
192
601
|
uploadFile(file_1, location_1) {
|
|
@@ -215,6 +624,7 @@ exports.CentraliSDK = CentraliSDK;
|
|
|
215
624
|
*
|
|
216
625
|
* const options: CentraliSDKOptions = {
|
|
217
626
|
* baseUrl: 'https://centrali.io',
|
|
627
|
+
* workspaceId: 'my-workspace',
|
|
218
628
|
* clientId: process.env.CLIENT_ID,
|
|
219
629
|
* clientSecret: process.env.CLIENT_SECRET,
|
|
220
630
|
* };
|
|
@@ -226,5 +636,43 @@ exports.CentraliSDK = CentraliSDK;
|
|
|
226
636
|
* // Or set a user token:
|
|
227
637
|
* client.setToken('<JWT_TOKEN>');
|
|
228
638
|
* await client.queryRecords('Product', { limit: 10 });
|
|
639
|
+
*
|
|
640
|
+
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
641
|
+
* // 1. First fetch initial data
|
|
642
|
+
* const orders = await client.queryRecords('Order', { filter: 'status = "pending"' });
|
|
643
|
+
* setOrders(orders.data);
|
|
644
|
+
*
|
|
645
|
+
* // 2. Then subscribe to realtime updates
|
|
646
|
+
* const subscription = client.realtime.subscribe({
|
|
647
|
+
* structures: ['Order'],
|
|
648
|
+
* events: ['record_created', 'record_updated', 'record_deleted'],
|
|
649
|
+
* onEvent: (event) => {
|
|
650
|
+
* // 3. Apply updates to UI
|
|
651
|
+
* console.log('Event:', event.event, event.recordSlug, event.recordId);
|
|
652
|
+
* },
|
|
653
|
+
* onError: (error) => console.error('Realtime error:', error),
|
|
654
|
+
* onConnected: () => console.log('Connected'),
|
|
655
|
+
* onDisconnected: (reason) => console.log('Disconnected:', reason),
|
|
656
|
+
* });
|
|
657
|
+
*
|
|
658
|
+
* // Cleanup when done
|
|
659
|
+
* subscription.unsubscribe();
|
|
660
|
+
*
|
|
661
|
+
* // Invoke an on-demand trigger:
|
|
662
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
663
|
+
* console.log('Job queued:', job.data);
|
|
664
|
+
*
|
|
665
|
+
* // Invoke trigger with custom payload:
|
|
666
|
+
* const job2 = await client.triggers.invoke('trigger-id', {
|
|
667
|
+
* payload: { orderId: '12345', action: 'process' }
|
|
668
|
+
* });
|
|
669
|
+
*
|
|
670
|
+
* // Get trigger details:
|
|
671
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
672
|
+
* console.log('Trigger:', trigger.data.name, trigger.data.executionType);
|
|
673
|
+
*
|
|
674
|
+
* // List all triggers:
|
|
675
|
+
* const triggers = await client.triggers.list();
|
|
676
|
+
* triggers.data.forEach(t => console.log(t.name));
|
|
229
677
|
*```
|
|
230
678
|
*/
|