@objectql/server 1.7.2 → 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.
package/test/rest.test.ts CHANGED
@@ -17,7 +17,37 @@ class MockDriver implements Driver {
17
17
  async init() {}
18
18
 
19
19
  async find(objectName: string, query: any) {
20
- return this.data[objectName] || [];
20
+ let items = this.data[objectName] || [];
21
+
22
+ // Apply filters if provided
23
+ if (query && query.filters) {
24
+ const filters = query.filters;
25
+ if (typeof filters === 'object') {
26
+ const filterKeys = Object.keys(filters);
27
+ if (filterKeys.length > 0) {
28
+ items = items.filter(item => {
29
+ for (const [key, value] of Object.entries(filters)) {
30
+ if (item[key] !== value) {
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ });
36
+ }
37
+ }
38
+ }
39
+
40
+ // Apply skip and limit if provided
41
+ if (query) {
42
+ if (query.skip) {
43
+ items = items.slice(query.skip);
44
+ }
45
+ if (query.limit) {
46
+ items = items.slice(0, query.limit);
47
+ }
48
+ }
49
+
50
+ return items;
21
51
  }
22
52
 
23
53
  async findOne(objectName: string, id: string | number, query?: any, options?: any) {
@@ -62,6 +92,80 @@ class MockDriver implements Driver {
62
92
  return (this.data[objectName] || []).length;
63
93
  }
64
94
 
95
+ async createMany(objectName: string, data: any[]) {
96
+ const newItems = data.map(item => ({
97
+ _id: String(this.nextId++),
98
+ ...item
99
+ }));
100
+ if (!this.data[objectName]) {
101
+ this.data[objectName] = [];
102
+ }
103
+ this.data[objectName].push(...newItems);
104
+ return newItems;
105
+ }
106
+
107
+ async updateMany(objectName: string, filters: any, data: any) {
108
+ const items = this.data[objectName] || [];
109
+ let count = 0;
110
+
111
+ // NOTE: Simplified filter implementation for testing purposes only.
112
+ // Production drivers should implement full ObjectQL filter evaluation
113
+ // with support for operators like ["<", value], [">=", value], etc.
114
+ // This mock only supports exact-match comparison on object properties.
115
+ items.forEach((item, index) => {
116
+ let matches = true;
117
+ if (filters && typeof filters === 'object') {
118
+ const filterKeys = Object.keys(filters);
119
+ // If no filter keys, match nothing (not everything)
120
+ if (filterKeys.length === 0) {
121
+ matches = false;
122
+ } else {
123
+ for (const [key, value] of Object.entries(filters)) {
124
+ if (item[key] !== value) {
125
+ matches = false;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ if (matches) {
132
+ this.data[objectName][index] = { ...item, ...data };
133
+ count++;
134
+ }
135
+ });
136
+
137
+ return count;
138
+ }
139
+
140
+ async deleteMany(objectName: string, filters: any) {
141
+ const items = this.data[objectName] || [];
142
+ const initialLength = items.length;
143
+
144
+ // NOTE: Simplified filter implementation for testing purposes only.
145
+ // Production drivers should implement full ObjectQL filter evaluation.
146
+ // This mock only supports exact-match comparison on object properties.
147
+ this.data[objectName] = items.filter(item => {
148
+ let matches = true;
149
+ if (filters && typeof filters === 'object') {
150
+ const filterKeys = Object.keys(filters);
151
+ // If no filter keys, match nothing (not everything)
152
+ if (filterKeys.length === 0) {
153
+ matches = false;
154
+ } else {
155
+ for (const [key, value] of Object.entries(filters)) {
156
+ if (item[key] !== value) {
157
+ matches = false;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return !matches; // Keep items that don't match
164
+ });
165
+
166
+ return initialLength - this.data[objectName].length;
167
+ }
168
+
65
169
  async execute(sql: string) {}
66
170
  }
67
171
 
@@ -101,8 +205,8 @@ describe('REST API Adapter', () => {
101
205
  .set('Accept', 'application/json');
102
206
 
103
207
  expect(response.status).toBe(200);
104
- expect(response.body.data).toHaveLength(2);
105
- expect(response.body.data[0].name).toBe('Alice');
208
+ expect(response.body.items).toHaveLength(2);
209
+ expect(response.body.items[0].name).toBe('Alice');
106
210
  });
107
211
 
108
212
  it('should handle GET /api/data/:object/:id - Get single record', async () => {
@@ -111,7 +215,8 @@ describe('REST API Adapter', () => {
111
215
  .set('Accept', 'application/json');
112
216
 
113
217
  expect(response.status).toBe(200);
114
- expect(response.body.data.name).toBe('Alice');
218
+ expect(response.body.name).toBe('Alice');
219
+ expect(response.body['@type']).toBe('user');
115
220
  });
116
221
 
117
222
  it('should handle POST /api/data/:object - Create record', async () => {
@@ -121,8 +226,9 @@ describe('REST API Adapter', () => {
121
226
  .set('Accept', 'application/json');
122
227
 
123
228
  expect(response.status).toBe(201);
124
- expect(response.body.data.name).toBe('Charlie');
125
- expect(response.body.data._id).toBeDefined();
229
+ expect(response.body.name).toBe('Charlie');
230
+ expect(response.body._id).toBeDefined();
231
+ expect(response.body['@type']).toBe('user');
126
232
  });
127
233
 
128
234
  it('should handle PUT /api/data/:object/:id - Update record', async () => {
@@ -140,7 +246,8 @@ describe('REST API Adapter', () => {
140
246
  .set('Accept', 'application/json');
141
247
 
142
248
  expect(response.status).toBe(200);
143
- expect(response.body.data.deleted).toBe(true);
249
+ expect(response.body.deleted).toBe(true);
250
+ expect(response.body['@type']).toBe('user');
144
251
  });
145
252
 
146
253
  it('should return 404 for non-existent object', async () => {
@@ -161,4 +268,173 @@ describe('REST API Adapter', () => {
161
268
  expect(response.status).toBe(400);
162
269
  expect(response.body.error.code).toBe('INVALID_REQUEST');
163
270
  });
271
+
272
+ it('should include pagination metadata with limit and skip', async () => {
273
+ const response = await request(server)
274
+ .get('/api/data/user?limit=1&skip=0')
275
+ .set('Accept', 'application/json');
276
+
277
+ expect(response.status).toBe(200);
278
+ expect(response.body.items).toBeDefined();
279
+ expect(response.body.meta).toBeDefined();
280
+ expect(response.body.meta.total).toBe(2);
281
+ expect(response.body.meta.page).toBe(1);
282
+ expect(response.body.meta.size).toBe(1);
283
+ expect(response.body.meta.pages).toBe(2);
284
+ expect(response.body.meta.has_next).toBe(true);
285
+ });
286
+
287
+ it('should calculate pagination metadata correctly for second page', async () => {
288
+ const response = await request(server)
289
+ .get('/api/data/user?limit=1&skip=1')
290
+ .set('Accept', 'application/json');
291
+
292
+ expect(response.status).toBe(200);
293
+ expect(response.body.meta.page).toBe(2);
294
+ expect(response.body.meta.has_next).toBe(false);
295
+ });
296
+
297
+ // Bulk operations tests
298
+ describe('Bulk Operations', () => {
299
+ it('should handle POST /api/data/:object with array - Create many records', async () => {
300
+ const response = await request(server)
301
+ .post('/api/data/user')
302
+ .send([
303
+ { name: 'User1', email: 'user1@example.com' },
304
+ { name: 'User2', email: 'user2@example.com' },
305
+ { name: 'User3', email: 'user3@example.com' }
306
+ ])
307
+ .set('Accept', 'application/json');
308
+
309
+ expect(response.status).toBe(201);
310
+ expect(response.body.items).toBeDefined();
311
+ expect(response.body.items).toHaveLength(3);
312
+ expect(response.body.count).toBe(3);
313
+ expect(response.body['@type']).toBe('user');
314
+ expect(response.body.items[0].name).toBe('User1');
315
+ expect(response.body.items[0]._id).toBeDefined();
316
+ });
317
+
318
+ it('should handle POST /api/data/:object/bulk-update - Update many records', async () => {
319
+ // First create some users
320
+ await request(server)
321
+ .post('/api/data/user')
322
+ .send([
323
+ { name: 'TestUser1', email: 'test1@example.com', role: 'user' },
324
+ { name: 'TestUser2', email: 'test2@example.com', role: 'user' }
325
+ ]);
326
+
327
+ // Now update all users with role 'user'
328
+ const response = await request(server)
329
+ .post('/api/data/user/bulk-update')
330
+ .send({
331
+ filters: { role: 'user' },
332
+ data: { role: 'admin' }
333
+ })
334
+ .set('Accept', 'application/json');
335
+
336
+ expect(response.status).toBe(200);
337
+ expect(response.body.count).toBeGreaterThan(0);
338
+ expect(response.body['@type']).toBe('user');
339
+
340
+ // Verify the records were actually updated
341
+ const verifyResponse = await request(server)
342
+ .get('/api/data/user')
343
+ .set('Accept', 'application/json');
344
+
345
+ const adminUsers = verifyResponse.body.items.filter((u: any) => u.role === 'admin');
346
+ expect(adminUsers.length).toBeGreaterThan(0);
347
+ });
348
+
349
+ it('should handle POST /api/data/:object/bulk-delete - Delete many records', async () => {
350
+ // First create some users
351
+ await request(server)
352
+ .post('/api/data/user')
353
+ .send([
354
+ { name: 'ToDelete1', email: 'delete1@example.com', status: 'inactive' },
355
+ { name: 'ToDelete2', email: 'delete2@example.com', status: 'inactive' }
356
+ ]);
357
+
358
+ // Now delete all inactive users
359
+ const response = await request(server)
360
+ .post('/api/data/user/bulk-delete')
361
+ .send({
362
+ filters: { status: 'inactive' }
363
+ })
364
+ .set('Accept', 'application/json');
365
+
366
+ expect(response.status).toBe(200);
367
+ expect(response.body.count).toBeGreaterThan(0);
368
+ expect(response.body['@type']).toBe('user');
369
+
370
+ // Verify the records were actually deleted
371
+ const verifyResponse = await request(server)
372
+ .get('/api/data/user')
373
+ .set('Accept', 'application/json');
374
+
375
+ const inactiveUsers = verifyResponse.body.items.filter((u: any) => u.status === 'inactive');
376
+ expect(inactiveUsers.length).toBe(0);
377
+ });
378
+
379
+ // Edge case tests
380
+ it('should handle createMany with empty array', async () => {
381
+ const response = await request(server)
382
+ .post('/api/data/user')
383
+ .send([])
384
+ .set('Accept', 'application/json');
385
+
386
+ expect(response.status).toBe(201);
387
+ expect(response.body.items).toHaveLength(0);
388
+ expect(response.body.count).toBe(0);
389
+ });
390
+
391
+ it('should handle updateMany with no matching records', async () => {
392
+ const response = await request(server)
393
+ .post('/api/data/user/bulk-update')
394
+ .send({
395
+ filters: { role: 'nonexistent' },
396
+ data: { role: 'admin' }
397
+ })
398
+ .set('Accept', 'application/json');
399
+
400
+ expect(response.status).toBe(200);
401
+ expect(response.body.count).toBe(0);
402
+ });
403
+
404
+ it('should handle deleteMany with no matching records', async () => {
405
+ const response = await request(server)
406
+ .post('/api/data/user/bulk-delete')
407
+ .send({
408
+ filters: { status: 'nonexistent' }
409
+ })
410
+ .set('Accept', 'application/json');
411
+
412
+ expect(response.status).toBe(200);
413
+ expect(response.body.count).toBe(0);
414
+ });
415
+
416
+ it('should return error for updateMany without data field', async () => {
417
+ const response = await request(server)
418
+ .post('/api/data/user/bulk-update')
419
+ .send({
420
+ filters: { role: 'user' }
421
+ // Missing data field
422
+ })
423
+ .set('Accept', 'application/json');
424
+
425
+ expect(response.status).toBe(400);
426
+ expect(response.body.error.code).toBe('INVALID_REQUEST');
427
+ });
428
+
429
+ it('should return error for createMany with non-array', async () => {
430
+ const response = await request(server)
431
+ .post('/api/data/user')
432
+ .send({ name: 'NotAnArray' }) // Send object instead of array when bulk create expected
433
+ .set('Accept', 'application/json');
434
+
435
+ // Should still work as single create
436
+ expect(response.status).toBe(201);
437
+ expect(response.body._id).toBeDefined();
438
+ });
439
+ });
164
440
  });