@centrali-io/centrali-sdk 2.0.5 → 2.0.8
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/index.ts
CHANGED
|
@@ -1,12 +1,123 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Centrali TypeScript SDK
|
|
3
3
|
* ----------------------
|
|
4
|
-
* A lightweight SDK for interacting with Centrali's Data and
|
|
4
|
+
* A lightweight SDK for interacting with Centrali's Data, Compute, and Realtime APIs,
|
|
5
5
|
* with support for user-provided tokens or client credentials (Client ID/Secret).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import axios, {AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, Method} from 'axios';
|
|
9
9
|
import qs from 'qs';
|
|
10
|
+
import {EventSource as EventSourcePolyfill} from 'eventsource';
|
|
11
|
+
|
|
12
|
+
// Use native EventSource in browser, polyfill in Node.js
|
|
13
|
+
const EventSourceImpl: typeof EventSource = typeof EventSource !== 'undefined'
|
|
14
|
+
? EventSource
|
|
15
|
+
: EventSourcePolyfill as unknown as typeof EventSource;
|
|
16
|
+
|
|
17
|
+
// =====================================================
|
|
18
|
+
// Realtime Types
|
|
19
|
+
// =====================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Event types emitted by the realtime service.
|
|
23
|
+
* Matches: services/backend/realtime/internal/redis/message.go
|
|
24
|
+
*/
|
|
25
|
+
export type RealtimeEventType = 'record_created' | 'record_updated' | 'record_deleted';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Record event payload from the realtime service.
|
|
29
|
+
* Matches: services/backend/realtime/internal/redis/message.go RecordEvent
|
|
30
|
+
*/
|
|
31
|
+
export interface RealtimeRecordEvent {
|
|
32
|
+
/** Event type */
|
|
33
|
+
event: RealtimeEventType;
|
|
34
|
+
/** Workspace slug where the event occurred */
|
|
35
|
+
workspaceSlug: string;
|
|
36
|
+
/** Structure's record slug (e.g., "order") */
|
|
37
|
+
recordSlug: string;
|
|
38
|
+
/** Record ID */
|
|
39
|
+
recordId: string;
|
|
40
|
+
/** Record data. For updates, contains "before" and "after" fields */
|
|
41
|
+
data?: Record<string, unknown>;
|
|
42
|
+
/** ISO timestamp when the event occurred */
|
|
43
|
+
timestamp: string;
|
|
44
|
+
/** User who created the record (for create events) */
|
|
45
|
+
createdBy?: string;
|
|
46
|
+
/** User who updated the record (for update events) */
|
|
47
|
+
updatedBy?: string;
|
|
48
|
+
/** User who deleted the record (for delete events) */
|
|
49
|
+
deletedBy?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Close event payload from the realtime service.
|
|
54
|
+
*/
|
|
55
|
+
export interface RealtimeCloseEvent {
|
|
56
|
+
/** Reason for the close */
|
|
57
|
+
reason: string;
|
|
58
|
+
/** Whether the client should attempt to reconnect */
|
|
59
|
+
reconnect: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Union type of all realtime events.
|
|
64
|
+
*/
|
|
65
|
+
export type RealtimeEvent = RealtimeRecordEvent;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Error object for realtime connection errors.
|
|
69
|
+
*/
|
|
70
|
+
export interface RealtimeError {
|
|
71
|
+
/** Error code from the server */
|
|
72
|
+
code: 'MISSING_TOKEN' | 'TOKEN_EXPIRED' | 'WORKSPACE_MISMATCH' | 'INVALID_TOKEN' | 'FORBIDDEN' | 'AUTH_ERROR' | 'RATE_LIMIT_EXCEEDED' | 'CONNECTION_ERROR' | 'PARSE_ERROR';
|
|
73
|
+
/** Human-readable error message */
|
|
74
|
+
message: string;
|
|
75
|
+
/** Whether the error is recoverable (client should retry) */
|
|
76
|
+
recoverable: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Options for subscribing to realtime events.
|
|
81
|
+
*/
|
|
82
|
+
export interface RealtimeSubscribeOptions {
|
|
83
|
+
/** Structure recordSlugs to filter events (e.g., ["order", "customer"]). Empty = all structures */
|
|
84
|
+
structures?: string[];
|
|
85
|
+
/** Event types to filter (e.g., ["record_created"]). Empty = all events */
|
|
86
|
+
events?: RealtimeEventType[];
|
|
87
|
+
/** CFL (Centrali Filter Language) expression for data filtering (e.g., "status = 'active'") */
|
|
88
|
+
filter?: string;
|
|
89
|
+
/** Callback for record events */
|
|
90
|
+
onEvent: (event: RealtimeEvent) => void;
|
|
91
|
+
/** Callback for errors */
|
|
92
|
+
onError?: (error: RealtimeError) => void;
|
|
93
|
+
/** Callback when connected */
|
|
94
|
+
onConnected?: () => void;
|
|
95
|
+
/** Callback when disconnected */
|
|
96
|
+
onDisconnected?: (reason?: string) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Realtime subscription handle returned by subscribe().
|
|
101
|
+
*/
|
|
102
|
+
export interface RealtimeSubscription {
|
|
103
|
+
/** Unsubscribe and close the connection */
|
|
104
|
+
unsubscribe: () => void;
|
|
105
|
+
/** Whether the connection is currently open */
|
|
106
|
+
readonly connected: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Internal configuration for realtime connections.
|
|
111
|
+
*/
|
|
112
|
+
interface RealtimeConfig {
|
|
113
|
+
/** Maximum reconnection attempts (default: 10) */
|
|
114
|
+
maxReconnectAttempts: number;
|
|
115
|
+
/** Initial reconnect delay in ms (default: 1000) */
|
|
116
|
+
initialReconnectDelayMs: number;
|
|
117
|
+
/** Maximum reconnect delay in ms (default: 30000) */
|
|
118
|
+
maxReconnectDelayMs: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
10
121
|
// Helper to encode form data
|
|
11
122
|
function encodeFormData(data: Record<string, string>): string {
|
|
12
123
|
return new URLSearchParams(data).toString();
|
|
@@ -40,6 +151,47 @@ export interface ApiResponse<T> {
|
|
|
40
151
|
updatedAt?: string;
|
|
41
152
|
}
|
|
42
153
|
|
|
154
|
+
// =====================================================
|
|
155
|
+
// Trigger Types
|
|
156
|
+
// =====================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Trigger execution types supported by Centrali.
|
|
160
|
+
*/
|
|
161
|
+
export type TriggerExecutionType = 'on-demand' | 'event-driven' | 'scheduled' | 'webhook';
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Function trigger definition.
|
|
165
|
+
*/
|
|
166
|
+
export interface FunctionTrigger {
|
|
167
|
+
id: string;
|
|
168
|
+
name: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
workspaceSlug: string;
|
|
171
|
+
functionId: string;
|
|
172
|
+
executionType: TriggerExecutionType;
|
|
173
|
+
triggerMetadata: Record<string, any>;
|
|
174
|
+
schedulerJobId?: string;
|
|
175
|
+
createdBy: string;
|
|
176
|
+
updatedBy: string;
|
|
177
|
+
createdAt?: string;
|
|
178
|
+
updatedAt?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Options for invoking an on-demand trigger.
|
|
183
|
+
*/
|
|
184
|
+
export interface InvokeTriggerOptions {
|
|
185
|
+
/** Custom payload/parameters to pass to the trigger execution */
|
|
186
|
+
payload?: Record<string, any>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Response from invoking a trigger.
|
|
191
|
+
* Currently the API returns the queued job ID as a string.
|
|
192
|
+
*/
|
|
193
|
+
export type TriggerInvokeResponse = string;
|
|
194
|
+
|
|
43
195
|
|
|
44
196
|
|
|
45
197
|
/**
|
|
@@ -68,6 +220,258 @@ export function getAuthUrl(baseUrl: string): string {
|
|
|
68
220
|
return `${url.protocol}//auth.${hostname}`;
|
|
69
221
|
}
|
|
70
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Generate the realtime service URL from the base URL.
|
|
225
|
+
* E.g., https://centrali.io -> https://api.centrali.io/realtime
|
|
226
|
+
*/
|
|
227
|
+
export function getRealtimeUrl(baseUrl: string): string {
|
|
228
|
+
return `${getApiUrl(baseUrl)}/realtime`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generate the SSE endpoint path for a workspace.
|
|
233
|
+
* Matches: services/backend/realtime/internal/sse/handler.go ServeHTTP route
|
|
234
|
+
*/
|
|
235
|
+
function getRealtimeEventPath(workspaceSlug: string): string {
|
|
236
|
+
return `/workspace/${workspaceSlug}/events`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Default realtime configuration.
|
|
241
|
+
*/
|
|
242
|
+
const DEFAULT_REALTIME_CONFIG: RealtimeConfig = {
|
|
243
|
+
maxReconnectAttempts: 10,
|
|
244
|
+
initialReconnectDelayMs: 1000,
|
|
245
|
+
maxReconnectDelayMs: 30000,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* RealtimeManager handles SSE connections to the Centrali Realtime Service.
|
|
250
|
+
* Provides automatic reconnection with exponential backoff.
|
|
251
|
+
*
|
|
252
|
+
* Usage:
|
|
253
|
+
* ```ts
|
|
254
|
+
* const realtime = new RealtimeManager(baseUrl, workspaceSlug, () => client.getToken());
|
|
255
|
+
* const sub = realtime.subscribe({
|
|
256
|
+
* structures: ['order'],
|
|
257
|
+
* events: ['record_created', 'record_updated'],
|
|
258
|
+
* onEvent: (event) => console.log(event),
|
|
259
|
+
* onError: (error) => console.error(error),
|
|
260
|
+
* });
|
|
261
|
+
* // Later: sub.unsubscribe();
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export class RealtimeManager {
|
|
265
|
+
private baseUrl: string;
|
|
266
|
+
private workspaceSlug: string;
|
|
267
|
+
private getToken: () => string | null | Promise<string | null>;
|
|
268
|
+
private config: RealtimeConfig;
|
|
269
|
+
|
|
270
|
+
constructor(
|
|
271
|
+
baseUrl: string,
|
|
272
|
+
workspaceSlug: string,
|
|
273
|
+
getToken: () => string | null | Promise<string | null>,
|
|
274
|
+
config?: Partial<RealtimeConfig>
|
|
275
|
+
) {
|
|
276
|
+
this.baseUrl = baseUrl;
|
|
277
|
+
this.workspaceSlug = workspaceSlug;
|
|
278
|
+
this.getToken = getToken;
|
|
279
|
+
this.config = { ...DEFAULT_REALTIME_CONFIG, ...config };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Subscribe to realtime events for the workspace.
|
|
284
|
+
*
|
|
285
|
+
* IMPORTANT: Initial Sync Pattern
|
|
286
|
+
* Realtime delivers only new events after connection. For dashboards and lists:
|
|
287
|
+
* 1. Fetch current records first
|
|
288
|
+
* 2. Subscribe to realtime
|
|
289
|
+
* 3. Apply diffs while UI shows the snapshot
|
|
290
|
+
*
|
|
291
|
+
* @param options - Subscription options
|
|
292
|
+
* @returns Subscription handle with unsubscribe() method
|
|
293
|
+
*/
|
|
294
|
+
public subscribe(options: RealtimeSubscribeOptions): RealtimeSubscription {
|
|
295
|
+
let eventSource: EventSource | null = null;
|
|
296
|
+
let unsubscribed = false;
|
|
297
|
+
let connected = false;
|
|
298
|
+
let reconnectAttempt = 0;
|
|
299
|
+
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
300
|
+
|
|
301
|
+
const connect = async () => {
|
|
302
|
+
// Zombie loop prevention: don't reconnect if unsubscribed
|
|
303
|
+
if (unsubscribed) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Get token (may be async for client credentials flow)
|
|
309
|
+
const token = await Promise.resolve(this.getToken());
|
|
310
|
+
if (!token) {
|
|
311
|
+
options.onError?.({
|
|
312
|
+
code: 'MISSING_TOKEN',
|
|
313
|
+
message: 'No authentication token available',
|
|
314
|
+
recoverable: false,
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Build SSE URL with query params
|
|
320
|
+
const realtimeBaseUrl = getRealtimeUrl(this.baseUrl);
|
|
321
|
+
const path = getRealtimeEventPath(this.workspaceSlug);
|
|
322
|
+
const url = new URL(`${realtimeBaseUrl}${path}`);
|
|
323
|
+
|
|
324
|
+
// Add access token
|
|
325
|
+
url.searchParams.set('access_token', token);
|
|
326
|
+
|
|
327
|
+
// Add structure filter
|
|
328
|
+
if (options.structures?.length) {
|
|
329
|
+
url.searchParams.set('structures', options.structures.join(','));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Add event type filter
|
|
333
|
+
if (options.events?.length) {
|
|
334
|
+
url.searchParams.set('events', options.events.join(','));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Add CFL filter
|
|
338
|
+
if (options.filter) {
|
|
339
|
+
url.searchParams.set('filter', options.filter);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create EventSource (uses polyfill in Node.js)
|
|
343
|
+
eventSource = new EventSourceImpl(url.toString());
|
|
344
|
+
|
|
345
|
+
// Handle connection open
|
|
346
|
+
eventSource.onopen = () => {
|
|
347
|
+
if (unsubscribed) {
|
|
348
|
+
eventSource?.close();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
connected = true;
|
|
352
|
+
reconnectAttempt = 0;
|
|
353
|
+
options.onConnected?.();
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Handle record events - server sends all record events as 'message' type
|
|
357
|
+
// The event.event field inside the payload contains the actual type
|
|
358
|
+
// (record_created, record_updated, record_deleted)
|
|
359
|
+
eventSource.addEventListener('message', (e: MessageEvent) => {
|
|
360
|
+
if (unsubscribed) return;
|
|
361
|
+
try {
|
|
362
|
+
const event = JSON.parse(e.data) as RealtimeRecordEvent;
|
|
363
|
+
options.onEvent(event);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
options.onError?.({
|
|
366
|
+
code: 'PARSE_ERROR',
|
|
367
|
+
message: `Failed to parse event: ${err}`,
|
|
368
|
+
recoverable: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Handle close event from server
|
|
374
|
+
eventSource.addEventListener('close', (e: MessageEvent) => {
|
|
375
|
+
if (unsubscribed) return;
|
|
376
|
+
try {
|
|
377
|
+
const closeEvent = JSON.parse(e.data) as RealtimeCloseEvent;
|
|
378
|
+
connected = false;
|
|
379
|
+
options.onDisconnected?.(closeEvent.reason);
|
|
380
|
+
|
|
381
|
+
// Reconnect if server says to
|
|
382
|
+
if (closeEvent.reconnect && !unsubscribed) {
|
|
383
|
+
scheduleReconnect();
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// Ignore parse errors for close events
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Handle errors
|
|
391
|
+
eventSource.onerror = () => {
|
|
392
|
+
if (unsubscribed) {
|
|
393
|
+
eventSource?.close();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
connected = false;
|
|
398
|
+
eventSource?.close();
|
|
399
|
+
eventSource = null;
|
|
400
|
+
|
|
401
|
+
// EventSource error events don't provide much detail
|
|
402
|
+
// The connection will be closed, so we notify and potentially reconnect
|
|
403
|
+
options.onDisconnected?.('connection_error');
|
|
404
|
+
options.onError?.({
|
|
405
|
+
code: 'CONNECTION_ERROR',
|
|
406
|
+
message: 'Connection to realtime service failed',
|
|
407
|
+
recoverable: true,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
scheduleReconnect();
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
} catch (err) {
|
|
414
|
+
options.onError?.({
|
|
415
|
+
code: 'CONNECTION_ERROR',
|
|
416
|
+
message: `Failed to connect: ${err}`,
|
|
417
|
+
recoverable: true,
|
|
418
|
+
});
|
|
419
|
+
scheduleReconnect();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const scheduleReconnect = () => {
|
|
424
|
+
// Zombie loop prevention
|
|
425
|
+
if (unsubscribed) return;
|
|
426
|
+
|
|
427
|
+
reconnectAttempt++;
|
|
428
|
+
if (reconnectAttempt > this.config.maxReconnectAttempts) {
|
|
429
|
+
options.onError?.({
|
|
430
|
+
code: 'CONNECTION_ERROR',
|
|
431
|
+
message: `Max reconnection attempts (${this.config.maxReconnectAttempts}) exceeded`,
|
|
432
|
+
recoverable: false,
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Exponential backoff with jitter
|
|
438
|
+
const delay = Math.min(
|
|
439
|
+
this.config.initialReconnectDelayMs * Math.pow(2, reconnectAttempt - 1),
|
|
440
|
+
this.config.maxReconnectDelayMs
|
|
441
|
+
);
|
|
442
|
+
const jitter = Math.random() * 0.3 * delay; // 0-30% jitter
|
|
443
|
+
|
|
444
|
+
reconnectTimeout = setTimeout(() => {
|
|
445
|
+
if (!unsubscribed) {
|
|
446
|
+
connect();
|
|
447
|
+
}
|
|
448
|
+
}, delay + jitter);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// Start connection
|
|
452
|
+
connect();
|
|
453
|
+
|
|
454
|
+
// Return subscription handle
|
|
455
|
+
return {
|
|
456
|
+
unsubscribe: () => {
|
|
457
|
+
unsubscribed = true;
|
|
458
|
+
connected = false;
|
|
459
|
+
if (reconnectTimeout) {
|
|
460
|
+
clearTimeout(reconnectTimeout);
|
|
461
|
+
reconnectTimeout = null;
|
|
462
|
+
}
|
|
463
|
+
if (eventSource) {
|
|
464
|
+
eventSource.close();
|
|
465
|
+
eventSource = null;
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
get connected() {
|
|
469
|
+
return connected;
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
71
475
|
/**
|
|
72
476
|
* Retrieve an access token using the Client Credentials flow.
|
|
73
477
|
*/
|
|
@@ -113,11 +517,146 @@ export function getFileUploadApiPath(workspaceId: string): string {
|
|
|
113
517
|
return `storage/ws/${workspaceId}/api/v1/files`;
|
|
114
518
|
}
|
|
115
519
|
|
|
116
|
-
|
|
117
|
-
* Generate
|
|
520
|
+
/**
|
|
521
|
+
* Generate Function Triggers base API URL PATH.
|
|
522
|
+
*/
|
|
523
|
+
export function getFunctionTriggersApiPath(workspaceId: string, triggerId?: string): string {
|
|
524
|
+
const basePath = `data/workspace/${workspaceId}/api/v1/function-triggers`;
|
|
525
|
+
return triggerId ? `${basePath}/${triggerId}` : basePath;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Generate Function Trigger execute API URL PATH.
|
|
530
|
+
*/
|
|
531
|
+
export function getFunctionTriggerExecuteApiPath(workspaceId: string, triggerId: string): string {
|
|
532
|
+
return `data/workspace/${workspaceId}/api/v1/function-triggers/${triggerId}/execute`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// =====================================================
|
|
536
|
+
// Triggers Manager
|
|
537
|
+
// =====================================================
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* TriggersManager provides methods for working with on-demand function triggers.
|
|
541
|
+
* Access via `client.triggers`.
|
|
542
|
+
*
|
|
543
|
+
* Note: This manager only works with on-demand triggers. Scheduled, event-driven,
|
|
544
|
+
* and webhook triggers are managed through other mechanisms.
|
|
545
|
+
*
|
|
546
|
+
* Usage:
|
|
547
|
+
* ```ts
|
|
548
|
+
* // Invoke an on-demand trigger
|
|
549
|
+
* const result = await client.triggers.invoke('trigger-id');
|
|
550
|
+
*
|
|
551
|
+
* // Invoke with custom payload
|
|
552
|
+
* const result = await client.triggers.invoke('trigger-id', {
|
|
553
|
+
* payload: { customData: 'value' }
|
|
554
|
+
* });
|
|
555
|
+
*
|
|
556
|
+
* // Get an on-demand trigger by ID
|
|
557
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
558
|
+
*
|
|
559
|
+
* // List all on-demand triggers
|
|
560
|
+
* const triggers = await client.triggers.list();
|
|
561
|
+
* ```
|
|
118
562
|
*/
|
|
119
|
-
export
|
|
120
|
-
|
|
563
|
+
export class TriggersManager {
|
|
564
|
+
private requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>;
|
|
565
|
+
private workspaceId: string;
|
|
566
|
+
|
|
567
|
+
constructor(
|
|
568
|
+
workspaceId: string,
|
|
569
|
+
requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>
|
|
570
|
+
) {
|
|
571
|
+
this.workspaceId = workspaceId;
|
|
572
|
+
this.requestFn = requestFn;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Invoke an on-demand trigger by ID.
|
|
577
|
+
*
|
|
578
|
+
* @param triggerId - The ID of the trigger to invoke
|
|
579
|
+
* @param options - Optional invoke options including custom payload
|
|
580
|
+
* @returns The queued job ID for tracking the execution
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* // Simple invocation
|
|
585
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
586
|
+
* console.log('Job queued:', job.data);
|
|
587
|
+
*
|
|
588
|
+
* // With custom payload
|
|
589
|
+
* const job = await client.triggers.invoke('trigger-id', {
|
|
590
|
+
* payload: { orderId: '12345', action: 'process' }
|
|
591
|
+
* });
|
|
592
|
+
* ```
|
|
593
|
+
*/
|
|
594
|
+
public invoke(
|
|
595
|
+
triggerId: string,
|
|
596
|
+
options?: InvokeTriggerOptions
|
|
597
|
+
): Promise<ApiResponse<TriggerInvokeResponse>> {
|
|
598
|
+
const path = getFunctionTriggerExecuteApiPath(this.workspaceId, triggerId);
|
|
599
|
+
const data = options?.payload ?? {};
|
|
600
|
+
return this.requestFn<TriggerInvokeResponse>('POST', path, data);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get an on-demand trigger by ID.
|
|
605
|
+
*
|
|
606
|
+
* Note: This method validates that the trigger is an on-demand trigger.
|
|
607
|
+
* If the trigger exists but is not on-demand, an error will be thrown.
|
|
608
|
+
*
|
|
609
|
+
* @param triggerId - The ID of the on-demand trigger to retrieve
|
|
610
|
+
* @returns The trigger details
|
|
611
|
+
* @throws Error if the trigger is not an on-demand trigger
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
616
|
+
* console.log('Trigger name:', trigger.data.name);
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
public async get(triggerId: string): Promise<ApiResponse<FunctionTrigger>> {
|
|
620
|
+
const path = getFunctionTriggersApiPath(this.workspaceId, triggerId);
|
|
621
|
+
const response = await this.requestFn<FunctionTrigger>('GET', path);
|
|
622
|
+
|
|
623
|
+
// Validate that the trigger is on-demand
|
|
624
|
+
if (response.data && response.data.executionType !== 'on-demand') {
|
|
625
|
+
throw new Error(`Trigger '${triggerId}' is not an on-demand trigger. Only on-demand triggers can be invoked via the SDK.`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return response;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* List all on-demand triggers in the workspace.
|
|
633
|
+
*
|
|
634
|
+
* This method automatically filters to only return triggers with executionType 'on-demand'.
|
|
635
|
+
*
|
|
636
|
+
* @param queryParams - Optional query parameters for pagination, search, etc.
|
|
637
|
+
* @returns List of on-demand triggers with pagination metadata
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```ts
|
|
641
|
+
* // List all on-demand triggers
|
|
642
|
+
* const triggers = await client.triggers.list();
|
|
643
|
+
*
|
|
644
|
+
* // With pagination
|
|
645
|
+
* const triggers = await client.triggers.list({ limit: 10, page: 1 });
|
|
646
|
+
*
|
|
647
|
+
* // With search
|
|
648
|
+
* const triggers = await client.triggers.list({ search: 'process-order' });
|
|
649
|
+
* ```
|
|
650
|
+
*/
|
|
651
|
+
public list(queryParams?: Record<string, any>): Promise<ApiResponse<FunctionTrigger[]>> {
|
|
652
|
+
const path = getFunctionTriggersApiPath(this.workspaceId);
|
|
653
|
+
// Always filter for on-demand triggers only
|
|
654
|
+
const params = {
|
|
655
|
+
...queryParams,
|
|
656
|
+
executionType: 'on-demand'
|
|
657
|
+
};
|
|
658
|
+
return this.requestFn<FunctionTrigger[]>('GET', path, null, params);
|
|
659
|
+
}
|
|
121
660
|
}
|
|
122
661
|
|
|
123
662
|
/**
|
|
@@ -127,6 +666,8 @@ export class CentraliSDK {
|
|
|
127
666
|
private axios: AxiosInstance;
|
|
128
667
|
private token: string | null = null;
|
|
129
668
|
private options: CentraliSDKOptions;
|
|
669
|
+
private _realtime: RealtimeManager | null = null;
|
|
670
|
+
private _triggers: TriggersManager | null = null;
|
|
130
671
|
|
|
131
672
|
constructor(options: CentraliSDKOptions) {
|
|
132
673
|
this.options = options;
|
|
@@ -159,6 +700,82 @@ export class CentraliSDK {
|
|
|
159
700
|
);
|
|
160
701
|
}
|
|
161
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Realtime namespace for subscribing to SSE events.
|
|
705
|
+
*
|
|
706
|
+
* Usage:
|
|
707
|
+
* ```ts
|
|
708
|
+
* const sub = client.realtime.subscribe({
|
|
709
|
+
* structures: ['order'],
|
|
710
|
+
* events: ['record_created', 'record_updated'],
|
|
711
|
+
* onEvent: (event) => console.log(event),
|
|
712
|
+
* });
|
|
713
|
+
* // Later: sub.unsubscribe();
|
|
714
|
+
* ```
|
|
715
|
+
*
|
|
716
|
+
* IMPORTANT: Initial Sync Pattern
|
|
717
|
+
* Realtime delivers only new events after connection. For dashboards and lists:
|
|
718
|
+
* 1. Fetch current records first
|
|
719
|
+
* 2. Subscribe to realtime
|
|
720
|
+
* 3. Apply diffs while UI shows the snapshot
|
|
721
|
+
*/
|
|
722
|
+
public get realtime(): RealtimeManager {
|
|
723
|
+
if (!this._realtime) {
|
|
724
|
+
this._realtime = new RealtimeManager(
|
|
725
|
+
this.options.baseUrl,
|
|
726
|
+
this.options.workspaceId,
|
|
727
|
+
async () => {
|
|
728
|
+
// If token exists, return it
|
|
729
|
+
if (this.token) {
|
|
730
|
+
return this.token;
|
|
731
|
+
}
|
|
732
|
+
// For client-credentials flow, fetch token if not available
|
|
733
|
+
if (this.options.clientId && this.options.clientSecret) {
|
|
734
|
+
this.token = await fetchClientToken(
|
|
735
|
+
this.options.clientId,
|
|
736
|
+
this.options.clientSecret,
|
|
737
|
+
this.options.baseUrl
|
|
738
|
+
);
|
|
739
|
+
return this.token;
|
|
740
|
+
}
|
|
741
|
+
// No token and no credentials
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return this._realtime;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Triggers namespace for invoking and managing function triggers.
|
|
751
|
+
*
|
|
752
|
+
* Usage:
|
|
753
|
+
* ```ts
|
|
754
|
+
* // Invoke an on-demand trigger
|
|
755
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
756
|
+
*
|
|
757
|
+
* // Invoke with custom payload
|
|
758
|
+
* const job = await client.triggers.invoke('trigger-id', {
|
|
759
|
+
* payload: { orderId: '12345' }
|
|
760
|
+
* });
|
|
761
|
+
*
|
|
762
|
+
* // Get trigger details
|
|
763
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
764
|
+
*
|
|
765
|
+
* // List all triggers
|
|
766
|
+
* const triggers = await client.triggers.list();
|
|
767
|
+
* ```
|
|
768
|
+
*/
|
|
769
|
+
public get triggers(): TriggersManager {
|
|
770
|
+
if (!this._triggers) {
|
|
771
|
+
this._triggers = new TriggersManager(
|
|
772
|
+
this.options.workspaceId,
|
|
773
|
+
this.request.bind(this)
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return this._triggers;
|
|
777
|
+
}
|
|
778
|
+
|
|
162
779
|
/**
|
|
163
780
|
* Manually set or update the bearer token for subsequent requests.
|
|
164
781
|
*/
|
|
@@ -271,18 +888,6 @@ export class CentraliSDK {
|
|
|
271
888
|
return this.request('DELETE', path);
|
|
272
889
|
}
|
|
273
890
|
|
|
274
|
-
// ------------------ Compute API Methods ------------------
|
|
275
|
-
|
|
276
|
-
/** Invoke a compute function by name with given payload. */
|
|
277
|
-
public invokeFunction<T = any>(
|
|
278
|
-
functionId: string,
|
|
279
|
-
payload: Record<string, any>
|
|
280
|
-
): Promise<ApiResponse<T>> {
|
|
281
|
-
const path = getComputeFunctionTriggerApiPath(this.options.workspaceId, functionId);
|
|
282
|
-
return this.request('POST', path, { data: payload });
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
891
|
|
|
287
892
|
|
|
288
893
|
// ------------------ Storage API Methods ------------------
|
|
@@ -319,6 +924,7 @@ export class CentraliSDK {
|
|
|
319
924
|
*
|
|
320
925
|
* const options: CentraliSDKOptions = {
|
|
321
926
|
* baseUrl: 'https://centrali.io',
|
|
927
|
+
* workspaceId: 'my-workspace',
|
|
322
928
|
* clientId: process.env.CLIENT_ID,
|
|
323
929
|
* clientSecret: process.env.CLIENT_SECRET,
|
|
324
930
|
* };
|
|
@@ -330,5 +936,43 @@ export class CentraliSDK {
|
|
|
330
936
|
* // Or set a user token:
|
|
331
937
|
* client.setToken('<JWT_TOKEN>');
|
|
332
938
|
* await client.queryRecords('Product', { limit: 10 });
|
|
939
|
+
*
|
|
940
|
+
* // Subscribe to realtime events (Initial Sync Pattern):
|
|
941
|
+
* // 1. First fetch initial data
|
|
942
|
+
* const orders = await client.queryRecords('Order', { filter: 'status = "pending"' });
|
|
943
|
+
* setOrders(orders.data);
|
|
944
|
+
*
|
|
945
|
+
* // 2. Then subscribe to realtime updates
|
|
946
|
+
* const subscription = client.realtime.subscribe({
|
|
947
|
+
* structures: ['Order'],
|
|
948
|
+
* events: ['record_created', 'record_updated', 'record_deleted'],
|
|
949
|
+
* onEvent: (event) => {
|
|
950
|
+
* // 3. Apply updates to UI
|
|
951
|
+
* console.log('Event:', event.event, event.recordSlug, event.recordId);
|
|
952
|
+
* },
|
|
953
|
+
* onError: (error) => console.error('Realtime error:', error),
|
|
954
|
+
* onConnected: () => console.log('Connected'),
|
|
955
|
+
* onDisconnected: (reason) => console.log('Disconnected:', reason),
|
|
956
|
+
* });
|
|
957
|
+
*
|
|
958
|
+
* // Cleanup when done
|
|
959
|
+
* subscription.unsubscribe();
|
|
960
|
+
*
|
|
961
|
+
* // Invoke an on-demand trigger:
|
|
962
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
963
|
+
* console.log('Job queued:', job.data);
|
|
964
|
+
*
|
|
965
|
+
* // Invoke trigger with custom payload:
|
|
966
|
+
* const job2 = await client.triggers.invoke('trigger-id', {
|
|
967
|
+
* payload: { orderId: '12345', action: 'process' }
|
|
968
|
+
* });
|
|
969
|
+
*
|
|
970
|
+
* // Get trigger details:
|
|
971
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
972
|
+
* console.log('Trigger:', trigger.data.name, trigger.data.executionType);
|
|
973
|
+
*
|
|
974
|
+
* // List all triggers:
|
|
975
|
+
* const triggers = await client.triggers.list();
|
|
976
|
+
* triggers.data.forEach(t => console.log(t.name));
|
|
333
977
|
*```
|
|
334
978
|
*/
|