@objectql/server 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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -118
- package/dist/adapters/graphql.d.ts +17 -0
- package/dist/adapters/graphql.js +460 -0
- package/dist/adapters/graphql.js.map +1 -0
- package/dist/adapters/rest.d.ts +7 -5
- package/dist/adapters/rest.js +51 -14
- package/dist/adapters/rest.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server.js +33 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +6 -4
- package/src/adapters/graphql.ts +523 -0
- package/src/adapters/rest.ts +48 -14
- package/src/index.ts +2 -0
- package/src/server.ts +42 -0
- package/src/types.ts +1 -1
- package/test/graphql.test.ts +439 -0
- package/test/rest-advanced.test.ts +455 -0
- package/test/rest.test.ts +236 -0
- package/tsconfig.tsbuildinfo +1 -1
package/test/rest.test.ts
CHANGED
|
@@ -19,6 +19,24 @@ class MockDriver implements Driver {
|
|
|
19
19
|
async find(objectName: string, query: any) {
|
|
20
20
|
let items = this.data[objectName] || [];
|
|
21
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
|
+
|
|
22
40
|
// Apply skip and limit if provided
|
|
23
41
|
if (query) {
|
|
24
42
|
if (query.skip) {
|
|
@@ -74,6 +92,80 @@ class MockDriver implements Driver {
|
|
|
74
92
|
return (this.data[objectName] || []).length;
|
|
75
93
|
}
|
|
76
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
|
+
|
|
77
169
|
async execute(sql: string) {}
|
|
78
170
|
}
|
|
79
171
|
|
|
@@ -201,4 +293,148 @@ describe('REST API Adapter', () => {
|
|
|
201
293
|
expect(response.body.meta.page).toBe(2);
|
|
202
294
|
expect(response.body.meta.has_next).toBe(false);
|
|
203
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
|
+
});
|
|
204
440
|
});
|