@centrali-io/centrali-sdk 2.0.6 → 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.
Files changed (4) hide show
  1. package/README.md +99 -0
  2. package/dist/index.js +461 -13
  3. package/index.ts +661 -17
  4. 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 Compute APIs,
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 Compute Function Trigger API URL PATH.
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 function getComputeFunctionTriggerApiPath(workspaceId: string, functionId: string): string {
120
- return `data/workspace/${workspaceId}/api/v1/function-triggers/${functionId}/execute`;
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
  */