@objectstack/objectql 4.0.3 → 4.0.5

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.
@@ -1,245 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi, beforeEach } from 'vitest';
4
- import { ObjectStackProtocolImplementation } from './protocol.js';
5
-
6
- /**
7
- * Tests for the Protocol Implementation's data methods (findData, getData).
8
- * Validates that expand/populate/select parameters are correctly normalized
9
- * and forwarded to the underlying engine.
10
- */
11
- describe('ObjectStackProtocolImplementation - Data Operations', () => {
12
- let protocol: ObjectStackProtocolImplementation;
13
- let mockEngine: any;
14
-
15
- beforeEach(() => {
16
- mockEngine = {
17
- find: vi.fn().mockResolvedValue([]),
18
- findOne: vi.fn().mockResolvedValue(null),
19
- };
20
- protocol = new ObjectStackProtocolImplementation(mockEngine);
21
- });
22
-
23
- // ═══════════════════════════════════════════════════════════════
24
- // findData — expand/populate normalization
25
- // ═══════════════════════════════════════════════════════════════
26
-
27
- describe('findData', () => {
28
- it('should normalize $expand (OData) string to expand Record', async () => {
29
- await protocol.findData({ object: 'order_item', query: { $expand: 'order,product' } });
30
-
31
- expect(mockEngine.find).toHaveBeenCalledWith(
32
- 'order_item',
33
- expect.objectContaining({
34
- expand: { order: { object: 'order' }, product: { object: 'product' } },
35
- }),
36
- );
37
- // $expand should be deleted from options
38
- const callArgs = mockEngine.find.mock.calls[0][1];
39
- expect(callArgs.$expand).toBeUndefined();
40
- });
41
-
42
- it('should normalize $expand (OData) with different fields to expand Record', async () => {
43
- await protocol.findData({ object: 'task', query: { $expand: 'assignee,project' } });
44
-
45
- expect(mockEngine.find).toHaveBeenCalledWith(
46
- 'task',
47
- expect.objectContaining({
48
- expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
49
- }),
50
- );
51
- });
52
-
53
- it('should normalize populate array to expand Record', async () => {
54
- await protocol.findData({ object: 'task', query: { populate: ['assignee'] } });
55
-
56
- expect(mockEngine.find).toHaveBeenCalledWith(
57
- 'task',
58
- expect.objectContaining({
59
- expand: { assignee: { object: 'assignee' } },
60
- }),
61
- );
62
- });
63
-
64
- it('should normalize populate string to expand Record', async () => {
65
- await protocol.findData({ object: 'task', query: { populate: 'assignee,project' } });
66
-
67
- expect(mockEngine.find).toHaveBeenCalledWith(
68
- 'task',
69
- expect.objectContaining({
70
- expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
71
- }),
72
- );
73
- });
74
-
75
- it('should prefer populate names over expand string when both provided', async () => {
76
- await protocol.findData({
77
- object: 'task',
78
- query: { populate: ['assignee'], expand: 'project' },
79
- });
80
-
81
- // populate names take precedence; the non-object expand string is
82
- // cleaned up first, then populate-derived names create the Record.
83
- const callArgs = mockEngine.find.mock.calls[0][1];
84
- expect(callArgs.populate).toBeUndefined();
85
- expect(callArgs.$expand).toBeUndefined();
86
- expect(callArgs.expand).toEqual({ assignee: { object: 'assignee' } });
87
- });
88
-
89
- it('should pass expand Record object through as-is', async () => {
90
- await protocol.findData({
91
- object: 'task',
92
- query: { expand: { owner: { object: 'owner' }, team: { object: 'team' } } },
93
- });
94
-
95
- expect(mockEngine.find).toHaveBeenCalledWith(
96
- 'task',
97
- expect.objectContaining({
98
- expand: { owner: { object: 'owner' }, team: { object: 'team' } },
99
- }),
100
- );
101
- });
102
-
103
- it('should normalize select string to fields array', async () => {
104
- await protocol.findData({ object: 'task', query: { select: 'name,status,assignee' } });
105
-
106
- expect(mockEngine.find).toHaveBeenCalledWith(
107
- 'task',
108
- expect.objectContaining({
109
- fields: ['name', 'status', 'assignee'],
110
- }),
111
- );
112
- });
113
-
114
- it('should pass numeric pagination params correctly', async () => {
115
- await protocol.findData({ object: 'task', query: { top: '10', skip: '20' } });
116
-
117
- expect(mockEngine.find).toHaveBeenCalledWith(
118
- 'task',
119
- expect.objectContaining({
120
- limit: 10,
121
- offset: 20,
122
- }),
123
- );
124
- });
125
-
126
- it('should work with no query options', async () => {
127
- await protocol.findData({ object: 'task' });
128
-
129
- expect(mockEngine.find).toHaveBeenCalledWith('task', {});
130
- });
131
-
132
- it('should return records and standard response shape', async () => {
133
- mockEngine.find.mockResolvedValue([{ id: 't1', name: 'Task 1' }]);
134
-
135
- const result = await protocol.findData({ object: 'task', query: {} });
136
-
137
- expect(result).toEqual(
138
- expect.objectContaining({
139
- object: 'task',
140
- records: [{ id: 't1', name: 'Task 1' }],
141
- total: 1,
142
- }),
143
- );
144
- });
145
- });
146
-
147
- // ═══════════════════════════════════════════════════════════════
148
- // getData — expand/select normalization
149
- // ═══════════════════════════════════════════════════════════════
150
-
151
- describe('getData', () => {
152
- it('should convert expand string to expand Record', async () => {
153
- mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
154
-
155
- await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' });
156
-
157
- expect(mockEngine.findOne).toHaveBeenCalledWith(
158
- 'order_item',
159
- expect.objectContaining({
160
- where: { id: 'oi_1' },
161
- expand: { order: { object: 'order' }, product: { object: 'product' } },
162
- }),
163
- );
164
- });
165
-
166
- it('should convert expand array to expand Record', async () => {
167
- mockEngine.findOne.mockResolvedValue({ id: 't1' });
168
-
169
- await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] });
170
-
171
- expect(mockEngine.findOne).toHaveBeenCalledWith(
172
- 'task',
173
- expect.objectContaining({
174
- where: { id: 't1' },
175
- expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
176
- }),
177
- );
178
- });
179
-
180
- it('should convert select string to fields array', async () => {
181
- mockEngine.findOne.mockResolvedValue({ id: 't1', name: 'Test' });
182
-
183
- await protocol.getData({ object: 'task', id: 't1', select: 'name,status' });
184
-
185
- expect(mockEngine.findOne).toHaveBeenCalledWith(
186
- 'task',
187
- expect.objectContaining({
188
- where: { id: 't1' },
189
- fields: ['name', 'status'],
190
- }),
191
- );
192
- });
193
-
194
- it('should pass both expand and fields together', async () => {
195
- mockEngine.findOne.mockResolvedValue({ id: 'oi_1' });
196
-
197
- await protocol.getData({
198
- object: 'order_item',
199
- id: 'oi_1',
200
- expand: 'order',
201
- select: ['name', 'total'],
202
- });
203
-
204
- expect(mockEngine.findOne).toHaveBeenCalledWith(
205
- 'order_item',
206
- expect.objectContaining({
207
- where: { id: 'oi_1' },
208
- expand: { order: { object: 'order' } },
209
- fields: ['name', 'total'],
210
- }),
211
- );
212
- });
213
-
214
- it('should work without expand or select', async () => {
215
- mockEngine.findOne.mockResolvedValue({ id: 't1' });
216
-
217
- await protocol.getData({ object: 'task', id: 't1' });
218
-
219
- expect(mockEngine.findOne).toHaveBeenCalledWith(
220
- 'task',
221
- { where: { id: 't1' } },
222
- );
223
- });
224
-
225
- it('should return standard GetDataResponse shape', async () => {
226
- mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
227
-
228
- const result = await protocol.getData({ object: 'order_item', id: 'oi_1' });
229
-
230
- expect(result).toEqual({
231
- object: 'order_item',
232
- id: 'oi_1',
233
- record: { id: 'oi_1', name: 'Item 1' },
234
- });
235
- });
236
-
237
- it('should throw when record not found', async () => {
238
- mockEngine.findOne.mockResolvedValue(null);
239
-
240
- await expect(
241
- protocol.getData({ object: 'task', id: 'missing_id' })
242
- ).rejects.toThrow('not found');
243
- });
244
- });
245
- });
@@ -1,213 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach } from 'vitest';
4
- import { ObjectStackProtocolImplementation } from './protocol.js';
5
- import { ObjectQL } from './engine.js';
6
-
7
- describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => {
8
- let protocol: ObjectStackProtocolImplementation;
9
- let engine: ObjectQL;
10
-
11
- beforeEach(() => {
12
- engine = new ObjectQL();
13
- });
14
-
15
- it('should return unavailable auth service when no services registered', async () => {
16
- // Create protocol without service registry
17
- protocol = new ObjectStackProtocolImplementation(engine);
18
-
19
- const discovery = await protocol.getDiscovery();
20
-
21
- expect(discovery.services.auth).toBeDefined();
22
- expect(discovery.services.auth.enabled).toBe(false);
23
- expect(discovery.services.auth.status).toBe('unavailable');
24
- expect(discovery.services.auth.message).toContain('plugin-auth');
25
- // capabilities removed — derive from services
26
- expect(discovery.services.workflow).toBeDefined();
27
- expect(discovery.services.workflow.enabled).toBe(false);
28
- });
29
-
30
- it('should return available auth service when auth is registered', async () => {
31
- // Mock service registry with auth service
32
- const mockServices = new Map<string, any>();
33
- mockServices.set('auth', { /* mock auth service */ });
34
-
35
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
36
-
37
- const discovery = await protocol.getDiscovery();
38
-
39
- expect(discovery.services.auth).toBeDefined();
40
- expect(discovery.services.auth.enabled).toBe(true);
41
- expect(discovery.services.auth.status).toBe('available');
42
- expect(discovery.services.auth.route).toBe('/api/v1/auth');
43
- expect(discovery.services.auth.provider).toBe('plugin-auth');
44
- expect(discovery.routes.auth).toBe('/api/v1/auth');
45
- });
46
-
47
- it('should return available automation service when registered', async () => {
48
- const mockServices = new Map<string, any>();
49
- mockServices.set('automation', { /* mock automation service */ });
50
-
51
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
52
-
53
- const discovery = await protocol.getDiscovery();
54
-
55
- expect(discovery.services.automation).toBeDefined();
56
- expect(discovery.services.automation.enabled).toBe(true);
57
- expect(discovery.services.automation.status).toBe('available');
58
- });
59
-
60
- it('should return multiple available services when registered', async () => {
61
- const mockServices = new Map<string, any>();
62
- mockServices.set('auth', {});
63
- mockServices.set('realtime', {});
64
- mockServices.set('ai', {});
65
-
66
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
67
-
68
- const discovery = await protocol.getDiscovery();
69
-
70
- // Check auth
71
- expect(discovery.services.auth.enabled).toBe(true);
72
- expect(discovery.services.auth.status).toBe('available');
73
-
74
- // Check realtime
75
- expect(discovery.services.realtime.enabled).toBe(true);
76
- expect(discovery.services.realtime.status).toBe('available');
77
-
78
- // Check AI
79
- expect(discovery.services.ai.enabled).toBe(true);
80
- expect(discovery.services.ai.status).toBe('available');
81
-
82
- // Routes should include available services
83
- expect(discovery.routes.auth).toBe('/api/v1/auth');
84
- expect(discovery.routes.realtime).toBe('/api/v1/realtime');
85
- expect(discovery.routes.ai).toBe('/api/v1/ai');
86
- });
87
-
88
- it('should always show core services as available', async () => {
89
- protocol = new ObjectStackProtocolImplementation(engine);
90
-
91
- const discovery = await protocol.getDiscovery();
92
-
93
- // Core services should always be available
94
- expect(discovery.services.metadata.enabled).toBe(true);
95
- expect(discovery.services.metadata.status).toBe('available');
96
- expect(discovery.services.data.enabled).toBe(true);
97
- expect(discovery.services.data.status).toBe('available');
98
- expect(discovery.services.analytics.enabled).toBe(true);
99
- expect(discovery.services.analytics.status).toBe('available');
100
- });
101
-
102
- it('should map file-storage service to storage route', async () => {
103
- const mockServices = new Map<string, any>();
104
- mockServices.set('file-storage', {});
105
-
106
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
107
-
108
- const discovery = await protocol.getDiscovery();
109
-
110
- expect(discovery.services['file-storage'].enabled).toBe(true);
111
- expect(discovery.services['file-storage'].status).toBe('available');
112
- expect(discovery.routes.storage).toBe('/api/v1/storage');
113
- });
114
-
115
- it('should use consistent /api/v1/ route prefix for all services', async () => {
116
- const mockServices = new Map<string, any>();
117
- mockServices.set('auth', {});
118
- mockServices.set('automation', {});
119
- mockServices.set('ai', {});
120
-
121
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
122
-
123
- const discovery = await protocol.getDiscovery();
124
-
125
- // All routes should use consistent /api/v1/ prefix
126
- expect(discovery.routes.data).toBe('/api/v1/data');
127
- expect(discovery.routes.metadata).toBe('/api/v1/meta');
128
- expect(discovery.routes.auth).toBe('/api/v1/auth');
129
- expect(discovery.routes.automation).toBe('/api/v1/automation');
130
- expect(discovery.routes.ai).toBe('/api/v1/ai');
131
- expect(discovery.routes.analytics).toBe('/api/v1/analytics');
132
-
133
- // Service routes should match the routes map
134
- expect(discovery.services.data.route).toBe('/api/v1/data');
135
- expect(discovery.services.metadata.route).toBe('/api/v1/meta');
136
- expect(discovery.services.auth.route).toBe('/api/v1/auth');
137
- expect(discovery.services.analytics.route).toBe('/api/v1/analytics');
138
- });
139
-
140
- it('should return capabilities field populated from registered services', async () => {
141
- const mockServices = new Map<string, any>();
142
- mockServices.set('workflow', {});
143
-
144
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
145
- const discovery = await protocol.getDiscovery();
146
-
147
- // capabilities field should now exist in the response
148
- expect(discovery.capabilities).toBeDefined();
149
- // workflow is registered but doesn't map to a well-known capability directly
150
- expect(discovery.services.workflow.enabled).toBe(true);
151
- // All well-known capabilities should be disabled since workflow doesn't map to any
152
- expect(discovery.capabilities!.feed).toEqual({ enabled: false });
153
- expect(discovery.capabilities!.comments).toEqual({ enabled: false });
154
- expect(discovery.capabilities!.automation).toEqual({ enabled: false });
155
- expect(discovery.capabilities!.cron).toEqual({ enabled: false });
156
- expect(discovery.capabilities!.search).toEqual({ enabled: false });
157
- expect(discovery.capabilities!.export).toEqual({ enabled: false });
158
- expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: false });
159
- });
160
-
161
- it('should set all capabilities to false when no services are registered', async () => {
162
- protocol = new ObjectStackProtocolImplementation(engine);
163
- const discovery = await protocol.getDiscovery();
164
-
165
- expect(discovery.capabilities).toBeDefined();
166
- expect(discovery.capabilities!.feed).toEqual({ enabled: false });
167
- expect(discovery.capabilities!.comments).toEqual({ enabled: false });
168
- expect(discovery.capabilities!.automation).toEqual({ enabled: false });
169
- expect(discovery.capabilities!.cron).toEqual({ enabled: false });
170
- expect(discovery.capabilities!.search).toEqual({ enabled: false });
171
- expect(discovery.capabilities!.export).toEqual({ enabled: false });
172
- expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: false });
173
- });
174
-
175
- it('should dynamically set capabilities based on registered services', async () => {
176
- const mockServices = new Map<string, any>();
177
- mockServices.set('feed', {});
178
- mockServices.set('automation', {});
179
- mockServices.set('search', {});
180
- mockServices.set('file-storage', {});
181
-
182
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
183
- const discovery = await protocol.getDiscovery();
184
-
185
- expect(discovery.capabilities!.feed).toEqual({ enabled: true });
186
- expect(discovery.capabilities!.comments).toEqual({ enabled: true });
187
- expect(discovery.capabilities!.automation).toEqual({ enabled: true });
188
- expect(discovery.capabilities!.cron).toEqual({ enabled: false });
189
- expect(discovery.capabilities!.search).toEqual({ enabled: true });
190
- expect(discovery.capabilities!.export).toEqual({ enabled: true });
191
- expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: true });
192
- });
193
-
194
- it('should enable cron capability when job service is registered', async () => {
195
- const mockServices = new Map<string, any>();
196
- mockServices.set('job', {});
197
-
198
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
199
- const discovery = await protocol.getDiscovery();
200
-
201
- expect(discovery.capabilities!.cron).toEqual({ enabled: true });
202
- });
203
-
204
- it('should enable export capability when queue service is registered', async () => {
205
- const mockServices = new Map<string, any>();
206
- mockServices.set('queue', {});
207
-
208
- protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
209
- const discovery = await protocol.getDiscovery();
210
-
211
- expect(discovery.capabilities!.export).toEqual({ enabled: true });
212
- });
213
- });