@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/CLIENT_SERVER_INTEGRATION_TESTS.md +939 -0
- package/CLIENT_SPEC_COMPLIANCE.md +361 -0
- package/QUICK_REFERENCE.md +206 -0
- package/README.md +129 -0
- package/dist/index.d.mts +225 -17
- package/dist/index.d.ts +225 -17
- package/dist/index.js +483 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +483 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -10
- package/src/client.test.ts +563 -2
- package/src/index.ts +537 -35
- package/src/query-builder.ts +75 -0
- package/tests/integration/01-discovery.test.ts +68 -0
- package/tests/integration/README.md +72 -0
- package/vitest.integration.config.ts +18 -0
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' | '
|
|
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';
|
package/src/query-builder.ts
CHANGED
|
@@ -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
|
*/
|