@objectstack/client 3.0.8 → 3.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -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
 
@@ -235,6 +256,15 @@ export class ObjectStackClient {
235
256
  }
236
257
  }
237
258
 
259
+ /**
260
+ * Well-known capability flags discovered from the server.
261
+ * Returns undefined if the client has not yet connected or the server
262
+ * did not include capabilities in its discovery response.
263
+ */
264
+ get capabilities(): WellKnownCapabilities | undefined {
265
+ return this.discoveryInfo?.capabilities;
266
+ }
267
+
238
268
  /**
239
269
  * Metadata Operations
240
270
  */
@@ -588,13 +618,94 @@ export class ObjectStackClient {
588
618
  const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
589
619
  const data = await res.json();
590
620
  return data.url;
591
- }
621
+ },
622
+
623
+ /**
624
+ * Get a presigned URL for direct-to-cloud upload
625
+ */
626
+ getPresignedUrl: async (req: GetPresignedUrlRequest): Promise<PresignedUrlResponse> => {
627
+ const route = this.getRoute('storage');
628
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
629
+ method: 'POST',
630
+ body: JSON.stringify(req)
631
+ });
632
+ return res.json();
633
+ },
634
+
635
+ /**
636
+ * Initiate a chunked (multipart) upload session
637
+ */
638
+ initChunkedUpload: async (req: InitiateChunkedUploadRequest): Promise<InitiateChunkedUploadResponse> => {
639
+ const route = this.getRoute('storage');
640
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked`, {
641
+ method: 'POST',
642
+ body: JSON.stringify(req)
643
+ });
644
+ return res.json();
645
+ },
646
+
647
+ /**
648
+ * Upload a single chunk/part of a multipart upload
649
+ */
650
+ uploadPart: async (uploadId: string, chunkIndex: number, resumeToken: string, data: Blob | Buffer): Promise<UploadChunkResponse> => {
651
+ const route = this.getRoute('storage');
652
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/chunk/${chunkIndex}`, {
653
+ method: 'PUT',
654
+ headers: { 'x-resume-token': resumeToken },
655
+ body: data as any
656
+ });
657
+ return res.json();
658
+ },
659
+
660
+ /**
661
+ * Complete a chunked upload by assembling all parts
662
+ */
663
+ completeChunkedUpload: async (req: CompleteChunkedUploadRequest): Promise<CompleteChunkedUploadResponse> => {
664
+ const route = this.getRoute('storage');
665
+ const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${req.uploadId}/complete`, {
666
+ method: 'POST',
667
+ body: JSON.stringify(req)
668
+ });
669
+ return res.json();
670
+ },
671
+
672
+ /**
673
+ * Resume an interrupted chunked upload.
674
+ * Fetches current progress, then uploads remaining chunks and completes.
675
+ */
676
+ resumeUpload: async (uploadId: string, file: Blob | ArrayBuffer, chunkSize: number, resumeToken: string): Promise<CompleteChunkedUploadResponse> => {
677
+ const route = this.getRoute('storage');
678
+
679
+ // 1. Get current progress
680
+ const progressRes = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/progress`);
681
+ const progress = await progressRes.json() as UploadProgress;
682
+
683
+ const { totalChunks, uploadedChunks } = progress.data;
684
+ const parts: Array<{ chunkIndex: number; eTag: string }> = [];
685
+
686
+ // 2. Upload remaining chunks
687
+ const fileBuffer = file instanceof ArrayBuffer ? file : await file.arrayBuffer();
688
+ for (let i = uploadedChunks; i < totalChunks; i++) {
689
+ const start = i * chunkSize;
690
+ const end = Math.min(start + chunkSize, fileBuffer.byteLength);
691
+ const chunk = new Blob([fileBuffer.slice(start, end)]);
692
+
693
+ const chunkRes = await this.storage.uploadPart(uploadId, i, resumeToken, chunk);
694
+ parts.push({ chunkIndex: i, eTag: chunkRes.data.eTag });
695
+ }
696
+
697
+ // 3. Complete
698
+ return this.storage.completeChunkedUpload({ uploadId, parts });
699
+ },
592
700
  };
593
701
 
594
702
  /**
595
703
  * Automation Services
596
704
  */
597
705
  automation = {
706
+ /**
707
+ * Trigger a named automation flow (legacy endpoint)
708
+ */
598
709
  trigger: async (triggerName: string, payload: any) => {
599
710
  const route = this.getRoute('automation');
600
711
  const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
@@ -602,7 +713,99 @@ export class ObjectStackClient {
602
713
  body: JSON.stringify(payload)
603
714
  });
604
715
  return res.json();
605
- }
716
+ },
717
+
718
+ /**
719
+ * List all registered automation flows
720
+ */
721
+ list: async (): Promise<{ flows: string[]; total: number; hasMore: boolean }> => {
722
+ const route = this.getRoute('automation');
723
+ const res = await this.fetch(`${this.baseUrl}${route}`);
724
+ return this.unwrapResponse(res);
725
+ },
726
+
727
+ /**
728
+ * Get a flow definition by name
729
+ */
730
+ get: async (name: string): Promise<any> => {
731
+ const route = this.getRoute('automation');
732
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`);
733
+ return this.unwrapResponse(res);
734
+ },
735
+
736
+ /**
737
+ * Create (register) a new flow
738
+ */
739
+ create: async (name: string, definition: any): Promise<any> => {
740
+ const route = this.getRoute('automation');
741
+ const res = await this.fetch(`${this.baseUrl}${route}`, {
742
+ method: 'POST',
743
+ body: JSON.stringify({ name, ...definition }),
744
+ });
745
+ return this.unwrapResponse(res);
746
+ },
747
+
748
+ /**
749
+ * Update an existing flow
750
+ */
751
+ update: async (name: string, definition: any): Promise<any> => {
752
+ const route = this.getRoute('automation');
753
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
754
+ method: 'PUT',
755
+ body: JSON.stringify({ definition }),
756
+ });
757
+ return this.unwrapResponse(res);
758
+ },
759
+
760
+ /**
761
+ * Delete (unregister) a flow
762
+ */
763
+ delete: async (name: string): Promise<{ name: string; deleted: boolean }> => {
764
+ const route = this.getRoute('automation');
765
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
766
+ method: 'DELETE',
767
+ });
768
+ return this.unwrapResponse(res);
769
+ },
770
+
771
+ /**
772
+ * Enable or disable a flow
773
+ */
774
+ toggle: async (name: string, enabled: boolean): Promise<{ name: string; enabled: boolean }> => {
775
+ const route = this.getRoute('automation');
776
+ const res = await this.fetch(`${this.baseUrl}${route}/${name}/toggle`, {
777
+ method: 'POST',
778
+ body: JSON.stringify({ enabled }),
779
+ });
780
+ return this.unwrapResponse(res);
781
+ },
782
+
783
+ /**
784
+ * Execution run history
785
+ */
786
+ runs: {
787
+ /**
788
+ * List execution runs for a flow
789
+ */
790
+ list: async (flowName: string, options?: { limit?: number; cursor?: string }): Promise<{ runs: any[]; hasMore: boolean }> => {
791
+ const route = this.getRoute('automation');
792
+ const params = new URLSearchParams();
793
+ if (options?.limit) params.set('limit', String(options.limit));
794
+ if (options?.cursor) params.set('cursor', options.cursor);
795
+ const qs = params.toString();
796
+ const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs${qs ? `?${qs}` : ''}`);
797
+ return this.unwrapResponse(res);
798
+ },
799
+
800
+ /**
801
+ * Get a single execution run
802
+ */
803
+ get: async (flowName: string, runId: string): Promise<any> => {
804
+ const route = this.getRoute('automation');
805
+ const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
806
+ return this.unwrapResponse(res);
807
+ },
808
+ },
606
809
  };
607
810
 
608
811
  /**
@@ -1016,6 +1219,188 @@ export class ObjectStackClient {
1016
1219
  }
1017
1220
  };
1018
1221
 
1222
+ /**
1223
+ * Feed / Chatter Services
1224
+ *
1225
+ * Provides access to the activity timeline (comments, field changes, tasks),
1226
+ * emoji reactions, pin/star, search, changelog, and record subscriptions.
1227
+ * Base path: /api/data/{object}/{recordId}/feed
1228
+ */
1229
+ feed = {
1230
+ /**
1231
+ * List feed items for a record
1232
+ */
1233
+ list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
1234
+ const route = this.getRoute('feed');
1235
+ const params = new URLSearchParams();
1236
+ if (options?.type) params.set('type', options.type);
1237
+ if (options?.limit) params.set('limit', String(options.limit));
1238
+ if (options?.cursor) params.set('cursor', options.cursor);
1239
+ const qs = params.toString();
1240
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed${qs ? `?${qs}` : ''}`);
1241
+ return this.unwrapResponse<GetFeedResponse>(res);
1242
+ },
1243
+
1244
+ /**
1245
+ * Create a new feed item (comment, note, task, etc.)
1246
+ */
1247
+ create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
1248
+ const route = this.getRoute('feed');
1249
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
1250
+ method: 'POST',
1251
+ body: JSON.stringify(data)
1252
+ });
1253
+ return this.unwrapResponse<CreateFeedItemResponse>(res);
1254
+ },
1255
+
1256
+ /**
1257
+ * Update an existing feed item
1258
+ */
1259
+ update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
1260
+ const route = this.getRoute('feed');
1261
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1262
+ method: 'PUT',
1263
+ body: JSON.stringify(data)
1264
+ });
1265
+ return this.unwrapResponse<UpdateFeedItemResponse>(res);
1266
+ },
1267
+
1268
+ /**
1269
+ * Delete a feed item
1270
+ */
1271
+ delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
1272
+ const route = this.getRoute('feed');
1273
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1274
+ method: 'DELETE'
1275
+ });
1276
+ return this.unwrapResponse<DeleteFeedItemResponse>(res);
1277
+ },
1278
+
1279
+ /**
1280
+ * Add an emoji reaction to a feed item
1281
+ */
1282
+ addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
1283
+ const route = this.getRoute('feed');
1284
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
1285
+ method: 'POST',
1286
+ body: JSON.stringify({ emoji })
1287
+ });
1288
+ return this.unwrapResponse<AddReactionResponse>(res);
1289
+ },
1290
+
1291
+ /**
1292
+ * Remove an emoji reaction from a feed item
1293
+ */
1294
+ removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
1295
+ const route = this.getRoute('feed');
1296
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
1297
+ method: 'DELETE'
1298
+ });
1299
+ return this.unwrapResponse<RemoveReactionResponse>(res);
1300
+ },
1301
+
1302
+ /**
1303
+ * Pin a feed item to the top of the timeline
1304
+ */
1305
+ pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
1306
+ const route = this.getRoute('feed');
1307
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1308
+ method: 'POST'
1309
+ });
1310
+ return this.unwrapResponse<PinFeedItemResponse>(res);
1311
+ },
1312
+
1313
+ /**
1314
+ * Unpin a feed item
1315
+ */
1316
+ unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
1317
+ const route = this.getRoute('feed');
1318
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1319
+ method: 'DELETE'
1320
+ });
1321
+ return this.unwrapResponse<UnpinFeedItemResponse>(res);
1322
+ },
1323
+
1324
+ /**
1325
+ * Star (bookmark) a feed item
1326
+ */
1327
+ star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
1328
+ const route = this.getRoute('feed');
1329
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1330
+ method: 'POST'
1331
+ });
1332
+ return this.unwrapResponse<StarFeedItemResponse>(res);
1333
+ },
1334
+
1335
+ /**
1336
+ * Unstar a feed item
1337
+ */
1338
+ unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
1339
+ const route = this.getRoute('feed');
1340
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1341
+ method: 'DELETE'
1342
+ });
1343
+ return this.unwrapResponse<UnstarFeedItemResponse>(res);
1344
+ },
1345
+
1346
+ /**
1347
+ * Search feed items
1348
+ */
1349
+ search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
1350
+ const route = this.getRoute('feed');
1351
+ const params = new URLSearchParams();
1352
+ params.set('query', query);
1353
+ if (options?.type) params.set('type', options.type);
1354
+ if (options?.actorId) params.set('actorId', options.actorId);
1355
+ if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1356
+ if (options?.dateTo) params.set('dateTo', options.dateTo);
1357
+ if (options?.limit) params.set('limit', String(options.limit));
1358
+ if (options?.cursor) params.set('cursor', options.cursor);
1359
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/search?${params.toString()}`);
1360
+ return this.unwrapResponse<SearchFeedResponse>(res);
1361
+ },
1362
+
1363
+ /**
1364
+ * Get field-level changelog for a record
1365
+ */
1366
+ getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
1367
+ const route = this.getRoute('feed');
1368
+ const params = new URLSearchParams();
1369
+ if (options?.field) params.set('field', options.field);
1370
+ if (options?.actorId) params.set('actorId', options.actorId);
1371
+ if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1372
+ if (options?.dateTo) params.set('dateTo', options.dateTo);
1373
+ if (options?.limit) params.set('limit', String(options.limit));
1374
+ if (options?.cursor) params.set('cursor', options.cursor);
1375
+ const qs = params.toString();
1376
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/changelog${qs ? `?${qs}` : ''}`);
1377
+ return this.unwrapResponse<GetChangelogResponse>(res);
1378
+ },
1379
+
1380
+ /**
1381
+ * Subscribe to record notifications
1382
+ */
1383
+ subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
1384
+ const route = this.getRoute('feed');
1385
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1386
+ method: 'POST',
1387
+ body: JSON.stringify(options || {})
1388
+ });
1389
+ return this.unwrapResponse<SubscribeResponse>(res);
1390
+ },
1391
+
1392
+ /**
1393
+ * Unsubscribe from record notifications
1394
+ */
1395
+ unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
1396
+ const route = this.getRoute('feed');
1397
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1398
+ method: 'DELETE'
1399
+ });
1400
+ return this.unwrapResponse<UnsubscribeResponse>(res);
1401
+ },
1402
+ };
1403
+
1019
1404
  /**
1020
1405
  * Data Operations
1021
1406
  */
@@ -1271,7 +1656,7 @@ export class ObjectStackClient {
1271
1656
  * Get the conventional route path for a given API endpoint type
1272
1657
  * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
1273
1658
  */
1274
- private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n'): string {
1659
+ private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n' | 'feed'): string {
1275
1660
  // 1. Use discovered routes if available
1276
1661
  if (this.discoveryInfo?.routes && (this.discoveryInfo.routes as any)[type]) {
1277
1662
  return (this.discoveryInfo.routes as any)[type];
@@ -1294,6 +1679,7 @@ export class ObjectStackClient {
1294
1679
  notifications: '/api/v1/notifications',
1295
1680
  ai: '/api/v1/ai',
1296
1681
  i18n: '/api/v1/i18n',
1682
+ feed: '/api/v1/data',
1297
1683
  };
1298
1684
 
1299
1685
  return routeMap[type] || `/api/v1/${type}`;
@@ -1356,5 +1742,20 @@ export type {
1356
1742
  GetTranslationsResponse,
1357
1743
  GetFieldLabelsResponse,
1358
1744
  RegisterRequest,
1359
- RefreshTokenRequest
1745
+ RefreshTokenRequest,
1746
+ GetFeedResponse,
1747
+ CreateFeedItemResponse,
1748
+ UpdateFeedItemResponse,
1749
+ DeleteFeedItemResponse,
1750
+ AddReactionResponse,
1751
+ RemoveReactionResponse,
1752
+ PinFeedItemResponse,
1753
+ UnpinFeedItemResponse,
1754
+ StarFeedItemResponse,
1755
+ UnstarFeedItemResponse,
1756
+ SearchFeedResponse,
1757
+ GetChangelogResponse,
1758
+ SubscribeResponse,
1759
+ UnsubscribeResponse,
1760
+ WellKnownCapabilities,
1360
1761
  } from '@objectstack/spec/api';