@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +201 -3
- package/dist/index.d.ts +201 -3
- package/dist/index.js +338 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +338 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/src/client.feed.test.ts +273 -0
- package/src/client.msw.test.ts +4 -2
- package/src/client.test.ts +179 -0
- package/src/index.ts +425 -17
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
|
-
|
|
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
|
-
|
|
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(
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
|
|
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';
|