@objectql/sdk 1.7.3 → 1.8.0

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.
@@ -0,0 +1,441 @@
1
+ import { RemoteDriver } from '../src/index';
2
+
3
+ // Mock fetch globally
4
+ global.fetch = jest.fn();
5
+
6
+ describe('RemoteDriver', () => {
7
+ let driver: RemoteDriver;
8
+ const baseUrl = 'http://localhost:3000';
9
+
10
+ beforeEach(() => {
11
+ driver = new RemoteDriver(baseUrl);
12
+ jest.clearAllMocks();
13
+ });
14
+
15
+ describe('Constructor', () => {
16
+ it('should create instance with base URL', () => {
17
+ expect(driver).toBeDefined();
18
+ expect(driver).toBeInstanceOf(RemoteDriver);
19
+ });
20
+
21
+ it('should handle base URL with trailing slash', () => {
22
+ const driverWithSlash = new RemoteDriver('http://localhost:3000/');
23
+ expect(driverWithSlash).toBeDefined();
24
+ });
25
+ });
26
+
27
+ describe('find', () => {
28
+ it('should fetch multiple records', async () => {
29
+ const mockResponse = {
30
+ data: [
31
+ { _id: '1', name: 'Alice' },
32
+ { _id: '2', name: 'Bob' }
33
+ ]
34
+ };
35
+
36
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
37
+ json: async () => mockResponse
38
+ });
39
+
40
+ const result = await driver.find('user', { filters: [['name', '=', 'Alice']] });
41
+
42
+ expect(global.fetch).toHaveBeenCalledWith(
43
+ 'http://localhost:3000/api/objectql',
44
+ expect.objectContaining({
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({
48
+ op: 'find',
49
+ object: 'user',
50
+ args: { filters: [['name', '=', 'Alice']] }
51
+ })
52
+ })
53
+ );
54
+ expect(result).toEqual(mockResponse.data);
55
+ });
56
+
57
+ it('should handle empty result', async () => {
58
+ const mockResponse = { data: [] };
59
+
60
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
61
+ json: async () => mockResponse
62
+ });
63
+
64
+ const result = await driver.find('user', {});
65
+ expect(result).toEqual([]);
66
+ });
67
+
68
+ it('should throw error when server returns error', async () => {
69
+ const mockResponse = {
70
+ error: { message: 'Object not found' }
71
+ };
72
+
73
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
74
+ json: async () => mockResponse
75
+ });
76
+
77
+ await expect(driver.find('unknown', {}))
78
+ .rejects
79
+ .toThrow('Object not found');
80
+ });
81
+ });
82
+
83
+ describe('findOne', () => {
84
+ it('should fetch single record by id', async () => {
85
+ const mockResponse = {
86
+ data: { _id: '1', name: 'Alice', email: 'alice@example.com' }
87
+ };
88
+
89
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
90
+ json: async () => mockResponse
91
+ });
92
+
93
+ const result = await driver.findOne('user', '1');
94
+
95
+ expect(global.fetch).toHaveBeenCalledWith(
96
+ 'http://localhost:3000/api/objectql',
97
+ expect.objectContaining({
98
+ body: JSON.stringify({
99
+ op: 'findOne',
100
+ object: 'user',
101
+ args: { id: '1', query: undefined }
102
+ })
103
+ })
104
+ );
105
+ expect(result).toEqual(mockResponse.data);
106
+ });
107
+
108
+ it('should return null when record not found', async () => {
109
+ const mockResponse = { data: null };
110
+
111
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
112
+ json: async () => mockResponse
113
+ });
114
+
115
+ const result = await driver.findOne('user', 'non-existent');
116
+ expect(result).toBeNull();
117
+ });
118
+
119
+ it('should handle numeric id', async () => {
120
+ const mockResponse = {
121
+ data: { _id: 123, name: 'Test' }
122
+ };
123
+
124
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
125
+ json: async () => mockResponse
126
+ });
127
+
128
+ const result = await driver.findOne('user', 123);
129
+
130
+ expect(global.fetch).toHaveBeenCalledWith(
131
+ 'http://localhost:3000/api/objectql',
132
+ expect.objectContaining({
133
+ body: JSON.stringify({
134
+ op: 'findOne',
135
+ object: 'user',
136
+ args: { id: 123, query: undefined }
137
+ })
138
+ })
139
+ );
140
+ expect(result).toEqual(mockResponse.data);
141
+ });
142
+ });
143
+
144
+ describe('create', () => {
145
+ it('should create a new record', async () => {
146
+ const newData = { name: 'Charlie', email: 'charlie@example.com' };
147
+ const mockResponse = {
148
+ data: { _id: '3', ...newData }
149
+ };
150
+
151
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
152
+ json: async () => mockResponse
153
+ });
154
+
155
+ const result = await driver.create('user', newData);
156
+
157
+ expect(global.fetch).toHaveBeenCalledWith(
158
+ 'http://localhost:3000/api/objectql',
159
+ expect.objectContaining({
160
+ body: JSON.stringify({
161
+ op: 'create',
162
+ object: 'user',
163
+ args: newData
164
+ })
165
+ })
166
+ );
167
+ expect(result).toEqual(mockResponse.data);
168
+ expect(result._id).toBeDefined();
169
+ });
170
+
171
+ it('should handle validation errors', async () => {
172
+ const mockResponse = {
173
+ error: { message: 'Validation failed: email is required' }
174
+ };
175
+
176
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
177
+ json: async () => mockResponse
178
+ });
179
+
180
+ await expect(driver.create('user', { name: 'Test' }))
181
+ .rejects
182
+ .toThrow('Validation failed: email is required');
183
+ });
184
+ });
185
+
186
+ describe('update', () => {
187
+ it('should update an existing record', async () => {
188
+ const updateData = { name: 'Alice Updated' };
189
+ const mockResponse = {
190
+ data: { _id: '1', name: 'Alice Updated', email: 'alice@example.com' }
191
+ };
192
+
193
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
194
+ json: async () => mockResponse
195
+ });
196
+
197
+ const result = await driver.update('user', '1', updateData);
198
+
199
+ expect(global.fetch).toHaveBeenCalledWith(
200
+ 'http://localhost:3000/api/objectql',
201
+ expect.objectContaining({
202
+ body: JSON.stringify({
203
+ op: 'update',
204
+ object: 'user',
205
+ args: { id: '1', data: updateData }
206
+ })
207
+ })
208
+ );
209
+ expect(result).toEqual(mockResponse.data);
210
+ });
211
+
212
+ it('should handle update of non-existent record', async () => {
213
+ const mockResponse = {
214
+ error: { message: 'Record not found' }
215
+ };
216
+
217
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
218
+ json: async () => mockResponse
219
+ });
220
+
221
+ await expect(driver.update('user', 'non-existent', { name: 'Test' }))
222
+ .rejects
223
+ .toThrow('Record not found');
224
+ });
225
+
226
+ it('should handle numeric id in update', async () => {
227
+ const mockResponse = {
228
+ data: { _id: 123, name: 'Updated' }
229
+ };
230
+
231
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
232
+ json: async () => mockResponse
233
+ });
234
+
235
+ await driver.update('user', 123, { name: 'Updated' });
236
+
237
+ expect(global.fetch).toHaveBeenCalledWith(
238
+ 'http://localhost:3000/api/objectql',
239
+ expect.objectContaining({
240
+ body: JSON.stringify({
241
+ op: 'update',
242
+ object: 'user',
243
+ args: { id: 123, data: { name: 'Updated' } }
244
+ })
245
+ })
246
+ );
247
+ });
248
+ });
249
+
250
+ describe('delete', () => {
251
+ it('should delete a record', async () => {
252
+ const mockResponse = { data: 1 };
253
+
254
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
255
+ json: async () => mockResponse
256
+ });
257
+
258
+ const result = await driver.delete('user', '1');
259
+
260
+ expect(global.fetch).toHaveBeenCalledWith(
261
+ 'http://localhost:3000/api/objectql',
262
+ expect.objectContaining({
263
+ body: JSON.stringify({
264
+ op: 'delete',
265
+ object: 'user',
266
+ args: { id: '1' }
267
+ })
268
+ })
269
+ );
270
+ expect(result).toBe(1);
271
+ });
272
+
273
+ it('should handle delete of non-existent record', async () => {
274
+ const mockResponse = {
275
+ error: { message: 'Record not found' }
276
+ };
277
+
278
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
279
+ json: async () => mockResponse
280
+ });
281
+
282
+ await expect(driver.delete('user', 'non-existent'))
283
+ .rejects
284
+ .toThrow('Record not found');
285
+ });
286
+
287
+ it('should handle numeric id in delete', async () => {
288
+ const mockResponse = { data: 1 };
289
+
290
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
291
+ json: async () => mockResponse
292
+ });
293
+
294
+ await driver.delete('user', 999);
295
+
296
+ expect(global.fetch).toHaveBeenCalledWith(
297
+ 'http://localhost:3000/api/objectql',
298
+ expect.objectContaining({
299
+ body: JSON.stringify({
300
+ op: 'delete',
301
+ object: 'user',
302
+ args: { id: 999 }
303
+ })
304
+ })
305
+ );
306
+ });
307
+ });
308
+
309
+ describe('count', () => {
310
+ it('should count records', async () => {
311
+ const mockResponse = { data: 42 };
312
+
313
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
314
+ json: async () => mockResponse
315
+ });
316
+
317
+ const result = await driver.count('user', { filters: [['active', '=', true]] });
318
+
319
+ expect(global.fetch).toHaveBeenCalledWith(
320
+ 'http://localhost:3000/api/objectql',
321
+ expect.objectContaining({
322
+ body: JSON.stringify({
323
+ op: 'count',
324
+ object: 'user',
325
+ args: { filters: [['active', '=', true]] }
326
+ })
327
+ })
328
+ );
329
+ expect(result).toBe(42);
330
+ });
331
+
332
+ it('should count all records when no filters', async () => {
333
+ const mockResponse = { data: 100 };
334
+
335
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
336
+ json: async () => mockResponse
337
+ });
338
+
339
+ const result = await driver.count('user', {});
340
+ expect(result).toBe(100);
341
+ });
342
+
343
+ it('should return 0 for empty collection', async () => {
344
+ const mockResponse = { data: 0 };
345
+
346
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
347
+ json: async () => mockResponse
348
+ });
349
+
350
+ const result = await driver.count('empty_collection', {});
351
+ expect(result).toBe(0);
352
+ });
353
+ });
354
+
355
+ describe('Error Handling', () => {
356
+ it('should handle network errors', async () => {
357
+ (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
358
+
359
+ await expect(driver.find('user', {}))
360
+ .rejects
361
+ .toThrow('Network error');
362
+ });
363
+
364
+ it('should handle invalid JSON response', async () => {
365
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
366
+ json: async () => { throw new Error('Invalid JSON'); }
367
+ });
368
+
369
+ await expect(driver.find('user', {}))
370
+ .rejects
371
+ .toThrow('Invalid JSON');
372
+ });
373
+
374
+ it('should handle server errors with custom error messages', async () => {
375
+ const mockResponse = {
376
+ error: {
377
+ message: 'Internal server error',
378
+ code: 'SERVER_ERROR'
379
+ }
380
+ };
381
+
382
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
383
+ json: async () => mockResponse
384
+ });
385
+
386
+ await expect(driver.create('user', {}))
387
+ .rejects
388
+ .toThrow('Internal server error');
389
+ });
390
+ });
391
+
392
+ describe('Base URL Handling', () => {
393
+ it('should remove trailing slash from base URL', async () => {
394
+ const driverWithSlash = new RemoteDriver('http://example.com/');
395
+ const mockResponse = { data: [] };
396
+
397
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
398
+ json: async () => mockResponse
399
+ });
400
+
401
+ await driverWithSlash.find('user', {});
402
+
403
+ expect(global.fetch).toHaveBeenCalledWith(
404
+ 'http://example.com/api/objectql',
405
+ expect.any(Object)
406
+ );
407
+ });
408
+
409
+ it('should work with different protocols', async () => {
410
+ const httpsDriver = new RemoteDriver('https://secure.example.com');
411
+ const mockResponse = { data: [] };
412
+
413
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
414
+ json: async () => mockResponse
415
+ });
416
+
417
+ await httpsDriver.find('user', {});
418
+
419
+ expect(global.fetch).toHaveBeenCalledWith(
420
+ 'https://secure.example.com/api/objectql',
421
+ expect.any(Object)
422
+ );
423
+ });
424
+
425
+ it('should work with custom ports', async () => {
426
+ const customPortDriver = new RemoteDriver('http://localhost:8080');
427
+ const mockResponse = { data: [] };
428
+
429
+ (global.fetch as jest.Mock).mockResolvedValueOnce({
430
+ json: async () => mockResponse
431
+ });
432
+
433
+ await customPortDriver.find('user', {});
434
+
435
+ expect(global.fetch).toHaveBeenCalledWith(
436
+ 'http://localhost:8080/api/objectql',
437
+ expect.any(Object)
438
+ );
439
+ });
440
+ });
441
+ });