@objectstack/client 3.0.8 → 3.0.10

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/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
- import { QueryAST, SortNode, AggregationNode } from '@objectstack/spec/data';
3
+ import { QueryAST, SortNode, AggregationNode, isFilterAST } from '@objectstack/spec/data';
4
4
  import {
5
5
  BatchUpdateRequest,
6
6
  BatchUpdateResponse,
@@ -20,6 +20,12 @@ import {
20
20
  PresignedUrlResponse,
21
21
  CompleteUploadRequest,
22
22
  FileUploadResponse,
23
+ InitiateChunkedUploadRequest,
24
+ InitiateChunkedUploadResponse,
25
+ UploadChunkResponse,
26
+ CompleteChunkedUploadRequest,
27
+ CompleteChunkedUploadResponse,
28
+ UploadProgress,
23
29
  CheckPermissionRequest,
24
30
  CheckPermissionResponse,
25
31
  GetObjectPermissionsResponse,
@@ -65,7 +71,22 @@ import {
65
71
  GetLocalesResponse,
66
72
  GetTranslationsResponse,
67
73
  GetFieldLabelsResponse,
68
- RegisterRequest
74
+ RegisterRequest,
75
+ GetFeedResponse,
76
+ CreateFeedItemResponse,
77
+ UpdateFeedItemResponse,
78
+ DeleteFeedItemResponse,
79
+ AddReactionResponse,
80
+ RemoveReactionResponse,
81
+ PinFeedItemResponse,
82
+ UnpinFeedItemResponse,
83
+ StarFeedItemResponse,
84
+ UnstarFeedItemResponse,
85
+ SearchFeedResponse,
86
+ GetChangelogResponse,
87
+ SubscribeResponse,
88
+ UnsubscribeResponse,
89
+ WellKnownCapabilities,
69
90
  } from '@objectstack/spec/api';
70
91
  import { Logger, createLogger } from '@objectstack/core';
71
92
 
@@ -94,7 +115,10 @@ export type DiscoveryResult = GetDiscoveryResponse;
94
115
 
95
116
  export interface QueryOptions {
96
117
  select?: string[]; // Simplified Selection
97
- filters?: Record<string, any>; // Map or AST
118
+ /** @canonical Preferred filter parameter (singular). */
119
+ filter?: Record<string, any> | unknown[]; // Map or AST
120
+ /** @deprecated Use `filter` (singular). Kept for backward compatibility. */
121
+ filters?: Record<string, any> | unknown[]; // Map or AST
98
122
  sort?: string | string[] | SortNode[]; // 'name' or ['-created_at'] or AST
99
123
  top?: number;
100
124
  skip?: number;
@@ -235,6 +259,15 @@ export class ObjectStackClient {
235
259
  }
236
260
  }
237
261
 
262
+ /**
263
+ * Well-known capability flags discovered from the server.
264
+ * Returns undefined if the client has not yet connected or the server
265
+ * did not include capabilities in its discovery response.
266
+ */
267
+ get capabilities(): WellKnownCapabilities | undefined {
268
+ return this.discoveryInfo?.capabilities;
269
+ }
270
+
238
271
  /**
239
272
  * Metadata Operations
240
273
  */
@@ -588,13 +621,94 @@ export class ObjectStackClient {
588
621
  const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
589
622
  const data = await res.json();
590
623
  return data.url;
591
- }
624
+ },
625
+
626
+ /**
627
+ * Get a presigned URL for direct-to-cloud upload
628
+ */
629
+ getPresignedUrl: async (req: GetPresignedUrlRequest): Promise<PresignedUrlResponse> => {
630
+ const route = this.getRoute('storage');
631
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
632
+ method: 'POST',
633
+ body: JSON.stringify(req)
634
+ });
635
+ return res.json();
636
+ },
637
+
638
+ /**
639
+ * Initiate a chunked (multipart) upload session
640
+ */
641
+ initChunkedUpload: async (req: InitiateChunkedUploadRequest): Promise<InitiateChunkedUploadResponse> => {
642
+ const route = this.getRoute('storage');
643
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked`, {
644
+ method: 'POST',
645
+ body: JSON.stringify(req)
646
+ });
647
+ return res.json();
648
+ },
649
+
650
+ /**
651
+ * Upload a single chunk/part of a multipart upload
652
+ */
653
+ uploadPart: async (uploadId: string, chunkIndex: number, resumeToken: string, data: Blob | Buffer): Promise<UploadChunkResponse> => {
654
+ const route = this.getRoute('storage');
655
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/chunk/${chunkIndex}`, {
656
+ method: 'PUT',
657
+ headers: { 'x-resume-token': resumeToken },
658
+ body: data as any
659
+ });
660
+ return res.json();
661
+ },
662
+
663
+ /**
664
+ * Complete a chunked upload by assembling all parts
665
+ */
666
+ completeChunkedUpload: async (req: CompleteChunkedUploadRequest): Promise<CompleteChunkedUploadResponse> => {
667
+ const route = this.getRoute('storage');
668
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${req.uploadId}/complete`, {
669
+ method: 'POST',
670
+ body: JSON.stringify(req)
671
+ });
672
+ return res.json();
673
+ },
674
+
675
+ /**
676
+ * Resume an interrupted chunked upload.
677
+ * Fetches current progress, then uploads remaining chunks and completes.
678
+ */
679
+ resumeUpload: async (uploadId: string, file: Blob | ArrayBuffer, chunkSize: number, resumeToken: string): Promise<CompleteChunkedUploadResponse> => {
680
+ const route = this.getRoute('storage');
681
+
682
+ // 1. Get current progress
683
+ const progressRes = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/progress`);
684
+ const progress = await progressRes.json() as UploadProgress;
685
+
686
+ const { totalChunks, uploadedChunks } = progress.data;
687
+ const parts: Array<{ chunkIndex: number; eTag: string }> = [];
688
+
689
+ // 2. Upload remaining chunks
690
+ const fileBuffer = file instanceof ArrayBuffer ? file : await file.arrayBuffer();
691
+ for (let i = uploadedChunks; i < totalChunks; i++) {
692
+ const start = i * chunkSize;
693
+ const end = Math.min(start + chunkSize, fileBuffer.byteLength);
694
+ const chunk = new Blob([fileBuffer.slice(start, end)]);
695
+
696
+ const chunkRes = await this.storage.uploadPart(uploadId, i, resumeToken, chunk);
697
+ parts.push({ chunkIndex: i, eTag: chunkRes.data.eTag });
698
+ }
699
+
700
+ // 3. Complete
701
+ return this.storage.completeChunkedUpload({ uploadId, parts });
702
+ },
592
703
  };
593
704
 
594
705
  /**
595
706
  * Automation Services
596
707
  */
597
708
  automation = {
709
+ /**
710
+ * Trigger a named automation flow (legacy endpoint)
711
+ */
598
712
  trigger: async (triggerName: string, payload: any) => {
599
713
  const route = this.getRoute('automation');
600
714
  const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
@@ -602,7 +716,99 @@ export class ObjectStackClient {
602
716
  body: JSON.stringify(payload)
603
717
  });
604
718
  return res.json();
605
- }
719
+ },
720
+
721
+ /**
722
+ * List all registered automation flows
723
+ */
724
+ list: async (): Promise<{ flows: string[]; total: number; hasMore: boolean }> => {
725
+ const route = this.getRoute('automation');
726
+ const res = await this.fetch(`${this.baseUrl}${route}`);
727
+ return this.unwrapResponse(res);
728
+ },
729
+
730
+ /**
731
+ * Get a flow definition by name
732
+ */
733
+ get: async (name: string): Promise<any> => {
734
+ const route = this.getRoute('automation');
735
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`);
736
+ return this.unwrapResponse(res);
737
+ },
738
+
739
+ /**
740
+ * Create (register) a new flow
741
+ */
742
+ create: async (name: string, definition: any): Promise<any> => {
743
+ const route = this.getRoute('automation');
744
+ const res = await this.fetch(`${this.baseUrl}${route}`, {
745
+ method: 'POST',
746
+ body: JSON.stringify({ name, ...definition }),
747
+ });
748
+ return this.unwrapResponse(res);
749
+ },
750
+
751
+ /**
752
+ * Update an existing flow
753
+ */
754
+ update: async (name: string, definition: any): Promise<any> => {
755
+ const route = this.getRoute('automation');
756
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
757
+ method: 'PUT',
758
+ body: JSON.stringify({ definition }),
759
+ });
760
+ return this.unwrapResponse(res);
761
+ },
762
+
763
+ /**
764
+ * Delete (unregister) a flow
765
+ */
766
+ delete: async (name: string): Promise<{ name: string; deleted: boolean }> => {
767
+ const route = this.getRoute('automation');
768
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
769
+ method: 'DELETE',
770
+ });
771
+ return this.unwrapResponse(res);
772
+ },
773
+
774
+ /**
775
+ * Enable or disable a flow
776
+ */
777
+ toggle: async (name: string, enabled: boolean): Promise<{ name: string; enabled: boolean }> => {
778
+ const route = this.getRoute('automation');
779
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}/toggle`, {
780
+ method: 'POST',
781
+ body: JSON.stringify({ enabled }),
782
+ });
783
+ return this.unwrapResponse(res);
784
+ },
785
+
786
+ /**
787
+ * Execution run history
788
+ */
789
+ runs: {
790
+ /**
791
+ * List execution runs for a flow
792
+ */
793
+ list: async (flowName: string, options?: { limit?: number; cursor?: string }): Promise<{ runs: any[]; hasMore: boolean }> => {
794
+ const route = this.getRoute('automation');
795
+ const params = new URLSearchParams();
796
+ if (options?.limit) params.set('limit', String(options.limit));
797
+ if (options?.cursor) params.set('cursor', options.cursor);
798
+ const qs = params.toString();
799
+ const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs${qs ? `?${qs}` : ''}`);
800
+ return this.unwrapResponse(res);
801
+ },
802
+
803
+ /**
804
+ * Get a single execution run
805
+ */
806
+ get: async (flowName: string, runId: string): Promise<any> => {
807
+ const route = this.getRoute('automation');
808
+ const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
809
+ return this.unwrapResponse(res);
810
+ },
811
+ },
606
812
  };
607
813
 
608
814
  /**
@@ -1016,6 +1222,188 @@ export class ObjectStackClient {
1016
1222
  }
1017
1223
  };
1018
1224
 
1225
+ /**
1226
+ * Feed / Chatter Services
1227
+ *
1228
+ * Provides access to the activity timeline (comments, field changes, tasks),
1229
+ * emoji reactions, pin/star, search, changelog, and record subscriptions.
1230
+ * Base path: /api/data/{object}/{recordId}/feed
1231
+ */
1232
+ feed = {
1233
+ /**
1234
+ * List feed items for a record
1235
+ */
1236
+ list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
1237
+ const route = this.getRoute('feed');
1238
+ const params = new URLSearchParams();
1239
+ if (options?.type) params.set('type', options.type);
1240
+ if (options?.limit) params.set('limit', String(options.limit));
1241
+ if (options?.cursor) params.set('cursor', options.cursor);
1242
+ const qs = params.toString();
1243
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed${qs ? `?${qs}` : ''}`);
1244
+ return this.unwrapResponse<GetFeedResponse>(res);
1245
+ },
1246
+
1247
+ /**
1248
+ * Create a new feed item (comment, note, task, etc.)
1249
+ */
1250
+ create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
1251
+ const route = this.getRoute('feed');
1252
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
1253
+ method: 'POST',
1254
+ body: JSON.stringify(data)
1255
+ });
1256
+ return this.unwrapResponse<CreateFeedItemResponse>(res);
1257
+ },
1258
+
1259
+ /**
1260
+ * Update an existing feed item
1261
+ */
1262
+ update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
1263
+ const route = this.getRoute('feed');
1264
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1265
+ method: 'PUT',
1266
+ body: JSON.stringify(data)
1267
+ });
1268
+ return this.unwrapResponse<UpdateFeedItemResponse>(res);
1269
+ },
1270
+
1271
+ /**
1272
+ * Delete a feed item
1273
+ */
1274
+ delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
1275
+ const route = this.getRoute('feed');
1276
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1277
+ method: 'DELETE'
1278
+ });
1279
+ return this.unwrapResponse<DeleteFeedItemResponse>(res);
1280
+ },
1281
+
1282
+ /**
1283
+ * Add an emoji reaction to a feed item
1284
+ */
1285
+ addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
1286
+ const route = this.getRoute('feed');
1287
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
1288
+ method: 'POST',
1289
+ body: JSON.stringify({ emoji })
1290
+ });
1291
+ return this.unwrapResponse<AddReactionResponse>(res);
1292
+ },
1293
+
1294
+ /**
1295
+ * Remove an emoji reaction from a feed item
1296
+ */
1297
+ removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
1298
+ const route = this.getRoute('feed');
1299
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
1300
+ method: 'DELETE'
1301
+ });
1302
+ return this.unwrapResponse<RemoveReactionResponse>(res);
1303
+ },
1304
+
1305
+ /**
1306
+ * Pin a feed item to the top of the timeline
1307
+ */
1308
+ pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
1309
+ const route = this.getRoute('feed');
1310
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1311
+ method: 'POST'
1312
+ });
1313
+ return this.unwrapResponse<PinFeedItemResponse>(res);
1314
+ },
1315
+
1316
+ /**
1317
+ * Unpin a feed item
1318
+ */
1319
+ unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
1320
+ const route = this.getRoute('feed');
1321
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1322
+ method: 'DELETE'
1323
+ });
1324
+ return this.unwrapResponse<UnpinFeedItemResponse>(res);
1325
+ },
1326
+
1327
+ /**
1328
+ * Star (bookmark) a feed item
1329
+ */
1330
+ star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
1331
+ const route = this.getRoute('feed');
1332
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1333
+ method: 'POST'
1334
+ });
1335
+ return this.unwrapResponse<StarFeedItemResponse>(res);
1336
+ },
1337
+
1338
+ /**
1339
+ * Unstar a feed item
1340
+ */
1341
+ unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
1342
+ const route = this.getRoute('feed');
1343
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1344
+ method: 'DELETE'
1345
+ });
1346
+ return this.unwrapResponse<UnstarFeedItemResponse>(res);
1347
+ },
1348
+
1349
+ /**
1350
+ * Search feed items
1351
+ */
1352
+ search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
1353
+ const route = this.getRoute('feed');
1354
+ const params = new URLSearchParams();
1355
+ params.set('query', query);
1356
+ if (options?.type) params.set('type', options.type);
1357
+ if (options?.actorId) params.set('actorId', options.actorId);
1358
+ if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1359
+ if (options?.dateTo) params.set('dateTo', options.dateTo);
1360
+ if (options?.limit) params.set('limit', String(options.limit));
1361
+ if (options?.cursor) params.set('cursor', options.cursor);
1362
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/search?${params.toString()}`);
1363
+ return this.unwrapResponse<SearchFeedResponse>(res);
1364
+ },
1365
+
1366
+ /**
1367
+ * Get field-level changelog for a record
1368
+ */
1369
+ getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
1370
+ const route = this.getRoute('feed');
1371
+ const params = new URLSearchParams();
1372
+ if (options?.field) params.set('field', options.field);
1373
+ if (options?.actorId) params.set('actorId', options.actorId);
1374
+ if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1375
+ if (options?.dateTo) params.set('dateTo', options.dateTo);
1376
+ if (options?.limit) params.set('limit', String(options.limit));
1377
+ if (options?.cursor) params.set('cursor', options.cursor);
1378
+ const qs = params.toString();
1379
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/changelog${qs ? `?${qs}` : ''}`);
1380
+ return this.unwrapResponse<GetChangelogResponse>(res);
1381
+ },
1382
+
1383
+ /**
1384
+ * Subscribe to record notifications
1385
+ */
1386
+ subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
1387
+ const route = this.getRoute('feed');
1388
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1389
+ method: 'POST',
1390
+ body: JSON.stringify(options || {})
1391
+ });
1392
+ return this.unwrapResponse<SubscribeResponse>(res);
1393
+ },
1394
+
1395
+ /**
1396
+ * Unsubscribe from record notifications
1397
+ */
1398
+ unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
1399
+ const route = this.getRoute('feed');
1400
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1401
+ method: 'DELETE'
1402
+ });
1403
+ return this.unwrapResponse<UnsubscribeResponse>(res);
1404
+ },
1405
+ };
1406
+
1019
1407
  /**
1020
1408
  * Data Operations
1021
1409
  */
@@ -1060,14 +1448,19 @@ export class ObjectStackClient {
1060
1448
  }
1061
1449
 
1062
1450
  // 4. Handle Filters (Simple vs AST)
1063
- if (options.filters) {
1451
+ // Canonical HTTP param name: `filter` (singular). `filters` (plural) is accepted
1452
+ // for backward compatibility but `filter` is the standard going forward.
1453
+ const filterValue = options.filter ?? options.filters;
1454
+ if (filterValue) {
1064
1455
  // Detect AST filter format vs simple key-value map. AST filters use an array structure
1065
- // with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST).
1456
+ // with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
1066
1457
  // For complex filter expressions, use .query() which builds a proper QueryAST.
1067
- if (this.isFilterAST(options.filters)) {
1068
- queryParams.set('filters', JSON.stringify(options.filters));
1069
- } else {
1070
- Object.entries(options.filters).forEach(([k, v]) => {
1458
+ if (this.isFilterAST(filterValue) || Array.isArray(filterValue)) {
1459
+ // AST or any array → serialize as JSON in `filter` param
1460
+ queryParams.set('filter', JSON.stringify(filterValue));
1461
+ } else if (typeof filterValue === 'object' && filterValue !== null) {
1462
+ // Plain key-value map → append each as individual query params
1463
+ Object.entries(filterValue as Record<string, unknown>).forEach(([k, v]) => {
1071
1464
  if (v !== undefined && v !== null) {
1072
1465
  queryParams.append(k, String(v));
1073
1466
  }
@@ -1186,10 +1579,9 @@ export class ObjectStackClient {
1186
1579
  */
1187
1580
 
1188
1581
  private isFilterAST(filter: any): boolean {
1189
- // Basic check: if array, it's [field, op, val] or [logic, node, node]
1190
- // If object but not basic KV map... harder to tell without schema
1191
- // For now, assume if it passes Array.isArray it's an AST root
1192
- return Array.isArray(filter);
1582
+ // Delegate to the spec-exported structural validator instead of naive Array.isArray.
1583
+ // This checks for valid AST shapes: [field, op, val], [logic, ...nodes], or [[cond], ...].
1584
+ return isFilterAST(filter);
1193
1585
  }
1194
1586
 
1195
1587
  /**
@@ -1271,7 +1663,7 @@ export class ObjectStackClient {
1271
1663
  * Get the conventional route path for a given API endpoint type
1272
1664
  * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
1273
1665
  */
1274
- private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n'): string {
1666
+ private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n' | 'feed'): string {
1275
1667
  // 1. Use discovered routes if available
1276
1668
  if (this.discoveryInfo?.routes && (this.discoveryInfo.routes as any)[type]) {
1277
1669
  return (this.discoveryInfo.routes as any)[type];
@@ -1294,6 +1686,7 @@ export class ObjectStackClient {
1294
1686
  notifications: '/api/v1/notifications',
1295
1687
  ai: '/api/v1/ai',
1296
1688
  i18n: '/api/v1/i18n',
1689
+ feed: '/api/v1/data',
1297
1690
  };
1298
1691
 
1299
1692
  return routeMap[type] || `/api/v1/${type}`;
@@ -1356,5 +1749,20 @@ export type {
1356
1749
  GetTranslationsResponse,
1357
1750
  GetFieldLabelsResponse,
1358
1751
  RegisterRequest,
1359
- RefreshTokenRequest
1752
+ RefreshTokenRequest,
1753
+ GetFeedResponse,
1754
+ CreateFeedItemResponse,
1755
+ UpdateFeedItemResponse,
1756
+ DeleteFeedItemResponse,
1757
+ AddReactionResponse,
1758
+ RemoveReactionResponse,
1759
+ PinFeedItemResponse,
1760
+ UnpinFeedItemResponse,
1761
+ StarFeedItemResponse,
1762
+ UnstarFeedItemResponse,
1763
+ SearchFeedResponse,
1764
+ GetChangelogResponse,
1765
+ SubscribeResponse,
1766
+ UnsubscribeResponse,
1767
+ WellKnownCapabilities,
1360
1768
  } from '@objectstack/spec/api';