@objectstack/client 2.0.0 → 2.0.1

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
@@ -19,7 +19,53 @@ import {
19
19
  GetPresignedUrlRequest,
20
20
  PresignedUrlResponse,
21
21
  CompleteUploadRequest,
22
- FileUploadResponse
22
+ FileUploadResponse,
23
+ CheckPermissionRequest,
24
+ CheckPermissionResponse,
25
+ GetObjectPermissionsResponse,
26
+ GetEffectivePermissionsResponse,
27
+ RealtimeConnectRequest,
28
+ RealtimeConnectResponse,
29
+ RealtimeSubscribeRequest,
30
+ RealtimeSubscribeResponse,
31
+ SetPresenceRequest,
32
+ GetPresenceResponse,
33
+ GetWorkflowConfigResponse,
34
+ GetWorkflowStateResponse,
35
+ WorkflowTransitionRequest,
36
+ WorkflowTransitionResponse,
37
+ WorkflowApproveRequest,
38
+ WorkflowApproveResponse,
39
+ WorkflowRejectRequest,
40
+ WorkflowRejectResponse,
41
+ ListViewsResponse,
42
+ GetViewResponse,
43
+ CreateViewRequest,
44
+ CreateViewResponse,
45
+ UpdateViewRequest,
46
+ UpdateViewResponse,
47
+ DeleteViewResponse,
48
+ RegisterDeviceRequest,
49
+ RegisterDeviceResponse,
50
+ UnregisterDeviceResponse,
51
+ GetNotificationPreferencesResponse,
52
+ UpdateNotificationPreferencesRequest,
53
+ UpdateNotificationPreferencesResponse,
54
+ ListNotificationsResponse,
55
+ MarkNotificationsReadResponse,
56
+ MarkAllNotificationsReadResponse,
57
+ AiNlqRequest,
58
+ AiNlqResponse,
59
+ AiChatRequest,
60
+ AiChatResponse,
61
+ AiSuggestRequest,
62
+ AiSuggestResponse,
63
+ AiInsightsRequest,
64
+ AiInsightsResponse,
65
+ GetLocalesResponse,
66
+ GetTranslationsResponse,
67
+ GetFieldLabelsResponse,
68
+ RegisterRequest
23
69
  } from '@objectstack/spec/api';
24
70
  import { Logger, createLogger } from '@objectstack/core';
25
71
 
@@ -338,37 +384,6 @@ export class ObjectStackClient {
338
384
  }
339
385
  };
340
386
 
341
- /**
342
- * Hub Management Services
343
- */
344
- hub = {
345
- spaces: {
346
- list: async () => {
347
- const route = this.getRoute('hub');
348
- const res = await this.fetch(`${this.baseUrl}${route}/spaces`);
349
- return res.json();
350
- },
351
- create: async (payload: any) => {
352
- const route = this.getRoute('hub');
353
- const res = await this.fetch(`${this.baseUrl}${route}/spaces`, {
354
- method: 'POST',
355
- body: JSON.stringify(payload)
356
- });
357
- return res.json();
358
- }
359
- },
360
- plugins: {
361
- install: async (pkg: string, version?: string) => {
362
- const route = this.getRoute('hub');
363
- const res = await this.fetch(`${this.baseUrl}${route}/plugins/install`, {
364
- method: 'POST',
365
- body: JSON.stringify({ pkg, version })
366
- });
367
- return res.json();
368
- }
369
- }
370
- };
371
-
372
387
  /**
373
388
  * Package Management Services
374
389
  *
@@ -488,6 +503,38 @@ export class ObjectStackClient {
488
503
  const route = this.getRoute('auth');
489
504
  const res = await this.fetch(`${this.baseUrl}${route}/me`);
490
505
  return res.json();
506
+ },
507
+
508
+ /**
509
+ * Register a new user account
510
+ */
511
+ register: async (request: RegisterRequest): Promise<SessionResponse> => {
512
+ const route = this.getRoute('auth');
513
+ const res = await this.fetch(`${this.baseUrl}${route}/register`, {
514
+ method: 'POST',
515
+ body: JSON.stringify(request)
516
+ });
517
+ const data = await res.json();
518
+ if (data.data?.token) {
519
+ this.token = data.data.token;
520
+ }
521
+ return data;
522
+ },
523
+
524
+ /**
525
+ * Refresh an authentication token
526
+ */
527
+ refreshToken: async (refreshToken: string): Promise<SessionResponse> => {
528
+ const route = this.getRoute('auth');
529
+ const res = await this.fetch(`${this.baseUrl}${route}/refresh`, {
530
+ method: 'POST',
531
+ body: JSON.stringify({ refreshToken })
532
+ });
533
+ const data = await res.json();
534
+ if (data.data?.token) {
535
+ this.token = data.data.token;
536
+ }
537
+ return data;
491
538
  }
492
539
  };
493
540
 
@@ -557,6 +604,417 @@ export class ObjectStackClient {
557
604
  }
558
605
  };
559
606
 
607
+ /**
608
+ * Permissions Services
609
+ */
610
+ permissions = {
611
+ /**
612
+ * Check if current user has permission for an action on an object
613
+ */
614
+ check: async (request: CheckPermissionRequest): Promise<CheckPermissionResponse> => {
615
+ const route = this.getRoute('permissions');
616
+ const res = await this.fetch(`${this.baseUrl}${route}/check`, {
617
+ method: 'POST',
618
+ body: JSON.stringify(request)
619
+ });
620
+ return this.unwrapResponse<CheckPermissionResponse>(res);
621
+ },
622
+
623
+ /**
624
+ * Get all permissions for a specific object
625
+ */
626
+ getObjectPermissions: async (object: string): Promise<GetObjectPermissionsResponse> => {
627
+ const route = this.getRoute('permissions');
628
+ const res = await this.fetch(`${this.baseUrl}${route}/permissions/${encodeURIComponent(object)}`);
629
+ return this.unwrapResponse<GetObjectPermissionsResponse>(res);
630
+ },
631
+
632
+ /**
633
+ * Get effective permissions for the current user
634
+ */
635
+ getEffectivePermissions: async (): Promise<GetEffectivePermissionsResponse> => {
636
+ const route = this.getRoute('permissions');
637
+ const res = await this.fetch(`${this.baseUrl}${route}/permissions/effective`);
638
+ return this.unwrapResponse<GetEffectivePermissionsResponse>(res);
639
+ }
640
+ };
641
+
642
+ /**
643
+ * Realtime Services
644
+ */
645
+ realtime = {
646
+ /**
647
+ * Establish a realtime connection
648
+ */
649
+ connect: async (request?: RealtimeConnectRequest): Promise<RealtimeConnectResponse> => {
650
+ const route = this.getRoute('realtime');
651
+ const res = await this.fetch(`${this.baseUrl}${route}/connect`, {
652
+ method: 'POST',
653
+ body: JSON.stringify(request || {})
654
+ });
655
+ return this.unwrapResponse<RealtimeConnectResponse>(res);
656
+ },
657
+
658
+ /**
659
+ * Disconnect from realtime services
660
+ */
661
+ disconnect: async (): Promise<void> => {
662
+ const route = this.getRoute('realtime');
663
+ await this.fetch(`${this.baseUrl}${route}/disconnect`, {
664
+ method: 'POST'
665
+ });
666
+ },
667
+
668
+ /**
669
+ * Subscribe to a channel
670
+ */
671
+ subscribe: async (request: RealtimeSubscribeRequest): Promise<RealtimeSubscribeResponse> => {
672
+ const route = this.getRoute('realtime');
673
+ const res = await this.fetch(`${this.baseUrl}${route}/subscribe`, {
674
+ method: 'POST',
675
+ body: JSON.stringify(request)
676
+ });
677
+ return this.unwrapResponse<RealtimeSubscribeResponse>(res);
678
+ },
679
+
680
+ /**
681
+ * Unsubscribe from a channel
682
+ */
683
+ unsubscribe: async (subscriptionId: string): Promise<void> => {
684
+ const route = this.getRoute('realtime');
685
+ await this.fetch(`${this.baseUrl}${route}/unsubscribe`, {
686
+ method: 'POST',
687
+ body: JSON.stringify({ subscriptionId })
688
+ });
689
+ },
690
+
691
+ /**
692
+ * Set presence state on a channel
693
+ */
694
+ setPresence: async (channel: string, state: SetPresenceRequest['state']): Promise<void> => {
695
+ const route = this.getRoute('realtime');
696
+ await this.fetch(`${this.baseUrl}${route}/presence`, {
697
+ method: 'PUT',
698
+ body: JSON.stringify({ channel, state })
699
+ });
700
+ },
701
+
702
+ /**
703
+ * Get presence information for a channel
704
+ */
705
+ getPresence: async (channel: string): Promise<GetPresenceResponse> => {
706
+ const route = this.getRoute('realtime');
707
+ const res = await this.fetch(`${this.baseUrl}${route}/presence/${encodeURIComponent(channel)}`);
708
+ return this.unwrapResponse<GetPresenceResponse>(res);
709
+ }
710
+ };
711
+
712
+ /**
713
+ * Workflow Services
714
+ */
715
+ workflow = {
716
+ /**
717
+ * Get workflow configuration for an object
718
+ */
719
+ getConfig: async (object: string): Promise<GetWorkflowConfigResponse> => {
720
+ const route = this.getRoute('workflow');
721
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/config`);
722
+ return this.unwrapResponse<GetWorkflowConfigResponse>(res);
723
+ },
724
+
725
+ /**
726
+ * Get current workflow state for a record
727
+ */
728
+ getState: async (object: string, recordId: string): Promise<GetWorkflowStateResponse> => {
729
+ const route = this.getRoute('workflow');
730
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/state`);
731
+ return this.unwrapResponse<GetWorkflowStateResponse>(res);
732
+ },
733
+
734
+ /**
735
+ * Execute a workflow state transition
736
+ */
737
+ transition: async (request: WorkflowTransitionRequest): Promise<WorkflowTransitionResponse> => {
738
+ const route = this.getRoute('workflow');
739
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/transition`, {
740
+ method: 'POST',
741
+ body: JSON.stringify({
742
+ transition: request.transition,
743
+ comment: request.comment,
744
+ data: request.data
745
+ })
746
+ });
747
+ return this.unwrapResponse<WorkflowTransitionResponse>(res);
748
+ },
749
+
750
+ /**
751
+ * Approve a workflow step
752
+ */
753
+ approve: async (request: WorkflowApproveRequest): Promise<WorkflowApproveResponse> => {
754
+ const route = this.getRoute('workflow');
755
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/approve`, {
756
+ method: 'POST',
757
+ body: JSON.stringify({
758
+ comment: request.comment,
759
+ data: request.data
760
+ })
761
+ });
762
+ return this.unwrapResponse<WorkflowApproveResponse>(res);
763
+ },
764
+
765
+ /**
766
+ * Reject a workflow step
767
+ */
768
+ reject: async (request: WorkflowRejectRequest): Promise<WorkflowRejectResponse> => {
769
+ const route = this.getRoute('workflow');
770
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/reject`, {
771
+ method: 'POST',
772
+ body: JSON.stringify({
773
+ reason: request.reason,
774
+ comment: request.comment
775
+ })
776
+ });
777
+ return this.unwrapResponse<WorkflowRejectResponse>(res);
778
+ }
779
+ };
780
+
781
+ /**
782
+ * Views CRUD Services
783
+ */
784
+ views = {
785
+ /**
786
+ * List views for an object
787
+ */
788
+ list: async (object: string, type?: 'list' | 'form'): Promise<ListViewsResponse> => {
789
+ const route = this.getRoute('views');
790
+ const params = new URLSearchParams();
791
+ if (type) params.set('type', type);
792
+ const qs = params.toString();
793
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}${qs ? `?${qs}` : ''}`);
794
+ return this.unwrapResponse<ListViewsResponse>(res);
795
+ },
796
+
797
+ /**
798
+ * Get a specific view
799
+ */
800
+ get: async (object: string, viewId: string): Promise<GetViewResponse> => {
801
+ const route = this.getRoute('views');
802
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`);
803
+ return this.unwrapResponse<GetViewResponse>(res);
804
+ },
805
+
806
+ /**
807
+ * Create a new view
808
+ */
809
+ create: async (object: string, data: CreateViewRequest['data']): Promise<CreateViewResponse> => {
810
+ const route = this.getRoute('views');
811
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}`, {
812
+ method: 'POST',
813
+ body: JSON.stringify({ object, data })
814
+ });
815
+ return this.unwrapResponse<CreateViewResponse>(res);
816
+ },
817
+
818
+ /**
819
+ * Update an existing view
820
+ */
821
+ update: async (object: string, viewId: string, data: UpdateViewRequest['data']): Promise<UpdateViewResponse> => {
822
+ const route = this.getRoute('views');
823
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
824
+ method: 'PUT',
825
+ body: JSON.stringify({ object, viewId, data })
826
+ });
827
+ return this.unwrapResponse<UpdateViewResponse>(res);
828
+ },
829
+
830
+ /**
831
+ * Delete a view
832
+ */
833
+ delete: async (object: string, viewId: string): Promise<DeleteViewResponse> => {
834
+ const route = this.getRoute('views');
835
+ const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
836
+ method: 'DELETE'
837
+ });
838
+ return this.unwrapResponse<DeleteViewResponse>(res);
839
+ }
840
+ };
841
+
842
+ /**
843
+ * Notification Services
844
+ */
845
+ notifications = {
846
+ /**
847
+ * Register a device for push notifications
848
+ */
849
+ registerDevice: async (request: RegisterDeviceRequest): Promise<RegisterDeviceResponse> => {
850
+ const route = this.getRoute('notifications');
851
+ const res = await this.fetch(`${this.baseUrl}${route}/devices`, {
852
+ method: 'POST',
853
+ body: JSON.stringify(request)
854
+ });
855
+ return this.unwrapResponse<RegisterDeviceResponse>(res);
856
+ },
857
+
858
+ /**
859
+ * Unregister a device from push notifications
860
+ */
861
+ unregisterDevice: async (deviceId: string): Promise<UnregisterDeviceResponse> => {
862
+ const route = this.getRoute('notifications');
863
+ const res = await this.fetch(`${this.baseUrl}${route}/devices/${encodeURIComponent(deviceId)}`, {
864
+ method: 'DELETE'
865
+ });
866
+ return this.unwrapResponse<UnregisterDeviceResponse>(res);
867
+ },
868
+
869
+ /**
870
+ * Get notification preferences for the current user
871
+ */
872
+ getPreferences: async (): Promise<GetNotificationPreferencesResponse> => {
873
+ const route = this.getRoute('notifications');
874
+ const res = await this.fetch(`${this.baseUrl}${route}/preferences`);
875
+ return this.unwrapResponse<GetNotificationPreferencesResponse>(res);
876
+ },
877
+
878
+ /**
879
+ * Update notification preferences
880
+ */
881
+ updatePreferences: async (preferences: UpdateNotificationPreferencesRequest['preferences']): Promise<UpdateNotificationPreferencesResponse> => {
882
+ const route = this.getRoute('notifications');
883
+ const res = await this.fetch(`${this.baseUrl}${route}/preferences`, {
884
+ method: 'PUT',
885
+ body: JSON.stringify({ preferences })
886
+ });
887
+ return this.unwrapResponse<UpdateNotificationPreferencesResponse>(res);
888
+ },
889
+
890
+ /**
891
+ * List notifications for the current user
892
+ */
893
+ list: async (options?: { read?: boolean; type?: string; limit?: number; cursor?: string }): Promise<ListNotificationsResponse> => {
894
+ const route = this.getRoute('notifications');
895
+ const params = new URLSearchParams();
896
+ if (options?.read !== undefined) params.set('read', String(options.read));
897
+ if (options?.type) params.set('type', options.type);
898
+ if (options?.limit) params.set('limit', String(options.limit));
899
+ if (options?.cursor) params.set('cursor', options.cursor);
900
+ const qs = params.toString();
901
+ const res = await this.fetch(`${this.baseUrl}${route}${qs ? `?${qs}` : ''}`);
902
+ return this.unwrapResponse<ListNotificationsResponse>(res);
903
+ },
904
+
905
+ /**
906
+ * Mark specific notifications as read
907
+ */
908
+ markRead: async (ids: string[]): Promise<MarkNotificationsReadResponse> => {
909
+ const route = this.getRoute('notifications');
910
+ const res = await this.fetch(`${this.baseUrl}${route}/read`, {
911
+ method: 'POST',
912
+ body: JSON.stringify({ ids })
913
+ });
914
+ return this.unwrapResponse<MarkNotificationsReadResponse>(res);
915
+ },
916
+
917
+ /**
918
+ * Mark all notifications as read
919
+ */
920
+ markAllRead: async (): Promise<MarkAllNotificationsReadResponse> => {
921
+ const route = this.getRoute('notifications');
922
+ const res = await this.fetch(`${this.baseUrl}${route}/read/all`, {
923
+ method: 'POST'
924
+ });
925
+ return this.unwrapResponse<MarkAllNotificationsReadResponse>(res);
926
+ }
927
+ };
928
+
929
+ /**
930
+ * AI Services
931
+ */
932
+ ai = {
933
+ /**
934
+ * Natural language query — converts natural language to structured query
935
+ */
936
+ nlq: async (request: AiNlqRequest): Promise<AiNlqResponse> => {
937
+ const route = this.getRoute('ai');
938
+ const res = await this.fetch(`${this.baseUrl}${route}/nlq`, {
939
+ method: 'POST',
940
+ body: JSON.stringify(request)
941
+ });
942
+ return this.unwrapResponse<AiNlqResponse>(res);
943
+ },
944
+
945
+ /**
946
+ * Multi-turn AI chat
947
+ */
948
+ chat: async (request: AiChatRequest): Promise<AiChatResponse> => {
949
+ const route = this.getRoute('ai');
950
+ const res = await this.fetch(`${this.baseUrl}${route}/chat`, {
951
+ method: 'POST',
952
+ body: JSON.stringify(request)
953
+ });
954
+ return this.unwrapResponse<AiChatResponse>(res);
955
+ },
956
+
957
+ /**
958
+ * AI-powered field value suggestions
959
+ */
960
+ suggest: async (request: AiSuggestRequest): Promise<AiSuggestResponse> => {
961
+ const route = this.getRoute('ai');
962
+ const res = await this.fetch(`${this.baseUrl}${route}/suggest`, {
963
+ method: 'POST',
964
+ body: JSON.stringify(request)
965
+ });
966
+ return this.unwrapResponse<AiSuggestResponse>(res);
967
+ },
968
+
969
+ /**
970
+ * AI-powered data insights
971
+ */
972
+ insights: async (request: AiInsightsRequest): Promise<AiInsightsResponse> => {
973
+ const route = this.getRoute('ai');
974
+ const res = await this.fetch(`${this.baseUrl}${route}/insights`, {
975
+ method: 'POST',
976
+ body: JSON.stringify(request)
977
+ });
978
+ return this.unwrapResponse<AiInsightsResponse>(res);
979
+ }
980
+ };
981
+
982
+ /**
983
+ * Internationalization Services
984
+ */
985
+ i18n = {
986
+ /**
987
+ * Get available locales
988
+ */
989
+ getLocales: async (): Promise<GetLocalesResponse> => {
990
+ const route = this.getRoute('i18n');
991
+ const res = await this.fetch(`${this.baseUrl}${route}/locales`);
992
+ return this.unwrapResponse<GetLocalesResponse>(res);
993
+ },
994
+
995
+ /**
996
+ * Get translations for a locale
997
+ */
998
+ getTranslations: async (locale: string, options?: { namespace?: string; keys?: string[] }): Promise<GetTranslationsResponse> => {
999
+ const route = this.getRoute('i18n');
1000
+ const params = new URLSearchParams();
1001
+ params.set('locale', locale);
1002
+ if (options?.namespace) params.set('namespace', options.namespace);
1003
+ if (options?.keys) params.set('keys', options.keys.join(','));
1004
+ const res = await this.fetch(`${this.baseUrl}${route}/translations?${params.toString()}`);
1005
+ return this.unwrapResponse<GetTranslationsResponse>(res);
1006
+ },
1007
+
1008
+ /**
1009
+ * Get translated field labels for an object
1010
+ */
1011
+ getFieldLabels: async (object: string, locale: string): Promise<GetFieldLabelsResponse> => {
1012
+ const route = this.getRoute('i18n');
1013
+ const res = await this.fetch(`${this.baseUrl}${route}/labels/${encodeURIComponent(object)}?locale=${encodeURIComponent(locale)}`);
1014
+ return this.unwrapResponse<GetFieldLabelsResponse>(res);
1015
+ }
1016
+ };
1017
+
560
1018
  /**
561
1019
  * Data Operations
562
1020
  */
@@ -811,7 +1269,7 @@ export class ObjectStackClient {
811
1269
  * Get the conventional route path for a given API endpoint type
812
1270
  * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
813
1271
  */
814
- private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'hub' | 'storage' | 'automation' | 'packages'): string {
1272
+ private getRoute(type: 'data' | 'metadata' | 'ui' | 'auth' | 'analytics' | 'storage' | 'automation' | 'packages' | 'permissions' | 'realtime' | 'workflow' | 'views' | 'notifications' | 'ai' | 'i18n'): string {
815
1273
  // 1. Use discovered routes if available
816
1274
  // Note: Spec uses 'endpoints', mapped dynamically
817
1275
  if (this.discoveryInfo?.endpoints && (this.discoveryInfo.endpoints as any)[type]) {
@@ -825,10 +1283,16 @@ export class ObjectStackClient {
825
1283
  ui: '/api/v1/ui',
826
1284
  auth: '/api/v1/auth',
827
1285
  analytics: '/api/v1/analytics',
828
- hub: '/api/v1/hub',
829
1286
  storage: '/api/v1/storage',
830
1287
  automation: '/api/v1/automation',
831
1288
  packages: '/api/v1/packages',
1289
+ permissions: '/api/v1/auth', // Permission endpoints are under /api/v1/auth per spec
1290
+ realtime: '/api/v1/realtime',
1291
+ workflow: '/api/v1/workflow',
1292
+ views: '/api/v1/ui/views',
1293
+ notifications: '/api/v1/notifications',
1294
+ ai: '/api/v1/ai',
1295
+ i18n: '/api/v1/i18n',
832
1296
  };
833
1297
 
834
1298
  return routeMap[type] || `/api/v1/${type}`;
@@ -853,5 +1317,43 @@ export type {
853
1317
  ErrorCategory,
854
1318
  GetDiscoveryResponse,
855
1319
  GetMetaTypesResponse,
856
- GetMetaItemsResponse
1320
+ GetMetaItemsResponse,
1321
+ CheckPermissionRequest,
1322
+ CheckPermissionResponse,
1323
+ GetObjectPermissionsResponse,
1324
+ GetEffectivePermissionsResponse,
1325
+ RealtimeConnectRequest,
1326
+ RealtimeConnectResponse,
1327
+ RealtimeSubscribeRequest,
1328
+ RealtimeSubscribeResponse,
1329
+ GetPresenceResponse,
1330
+ GetWorkflowConfigResponse,
1331
+ GetWorkflowStateResponse,
1332
+ WorkflowTransitionRequest,
1333
+ WorkflowTransitionResponse,
1334
+ WorkflowApproveRequest,
1335
+ WorkflowApproveResponse,
1336
+ WorkflowRejectRequest,
1337
+ WorkflowRejectResponse,
1338
+ ListViewsResponse,
1339
+ GetViewResponse,
1340
+ CreateViewResponse,
1341
+ UpdateViewResponse,
1342
+ DeleteViewResponse,
1343
+ RegisterDeviceRequest,
1344
+ RegisterDeviceResponse,
1345
+ ListNotificationsResponse,
1346
+ AiNlqRequest,
1347
+ AiNlqResponse,
1348
+ AiChatRequest,
1349
+ AiChatResponse,
1350
+ AiSuggestRequest,
1351
+ AiSuggestResponse,
1352
+ AiInsightsRequest,
1353
+ AiInsightsResponse,
1354
+ GetLocalesResponse,
1355
+ GetTranslationsResponse,
1356
+ GetFieldLabelsResponse,
1357
+ RegisterRequest,
1358
+ RefreshTokenRequest
857
1359
  } from '@objectstack/spec/api';
@@ -106,6 +106,46 @@ export class FilterBuilder<T = any> {
106
106
  return this;
107
107
  }
108
108
 
109
+ /**
110
+ * BETWEEN filter: field BETWEEN min AND max
111
+ */
112
+ between<K extends keyof T>(field: K, min: T[K], max: T[K]): this {
113
+ this.conditions.push(['and', [field as string, '>=', min], [field as string, '<=', max]] as FilterCondition);
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * CONTAINS filter: field contains value (case-insensitive LIKE %value%)
119
+ */
120
+ contains<K extends keyof T>(field: K, value: string): this {
121
+ this.conditions.push([field as string, 'like', `%${value}%`]);
122
+ return this;
123
+ }
124
+
125
+ /**
126
+ * STARTS WITH filter: field starts with value (LIKE value%)
127
+ */
128
+ startsWith<K extends keyof T>(field: K, value: string): this {
129
+ this.conditions.push([field as string, 'like', `${value}%`]);
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * ENDS WITH filter: field ends with value (LIKE %value)
135
+ */
136
+ endsWith<K extends keyof T>(field: K, value: string): this {
137
+ this.conditions.push([field as string, 'like', `%${value}`]);
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * EXISTS filter: field is not null (alias for isNotNull)
143
+ */
144
+ exists<K extends keyof T>(field: K): this {
145
+ this.conditions.push([field as string, 'is_not_null', null]);
146
+ return this;
147
+ }
148
+
109
149
  /**
110
150
  * Build the filter condition
111
151
  */
@@ -220,6 +260,41 @@ export class QueryBuilder<T = any> {
220
260
  return this;
221
261
  }
222
262
 
263
+ /**
264
+ * Expand (eager-load) a related object with an optional sub-query
265
+ */
266
+ expand(relation: string, subQuery?: Partial<QueryAST>): this {
267
+ if (!this.query.expand) {
268
+ this.query.expand = {};
269
+ }
270
+ (this.query.expand as Record<string, any>)[relation] = subQuery || {};
271
+ return this;
272
+ }
273
+
274
+ /**
275
+ * Add full-text search
276
+ */
277
+ search(query: string, options?: { fields?: string[]; fuzzy?: boolean }): this {
278
+ (this.query as any).search = { query, ...options };
279
+ return this;
280
+ }
281
+
282
+ /**
283
+ * Set cursor for keyset pagination
284
+ */
285
+ cursor(cursor: Record<string, any>): this {
286
+ (this.query as any).cursor = cursor;
287
+ return this;
288
+ }
289
+
290
+ /**
291
+ * Enable SELECT DISTINCT
292
+ */
293
+ distinct(): this {
294
+ (this.query as any).distinct = true;
295
+ return this;
296
+ }
297
+
223
298
  /**
224
299
  * Build the final query AST
225
300
  */