@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/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 Compute APIs,
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.getComputeFunctionTriggerApiPath = getComputeFunctionTriggerApiPath;
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 Compute Function Trigger API URL PATH.
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
- function getComputeFunctionTriggerApiPath(workspaceId, functionId) {
95
- return `data/workspace/${workspaceId}/api/v1/function-triggers/${functionId}/execute`;
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
  */