@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/CHANGELOG.md +21 -0
- package/LICENSE +1 -1
- package/README.md +93 -0
- 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 +52 -15
- 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/metadata.js +7 -7
- package/dist/metadata.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +85 -10
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +15 -8
- package/package.json +6 -3
- package/src/adapters/graphql.ts +523 -0
- package/src/adapters/rest.ts +49 -15
- package/src/index.ts +2 -0
- package/src/metadata.ts +7 -7
- package/src/server.ts +100 -11
- package/src/types.ts +24 -8
- package/test/graphql.test.ts +439 -0
- package/test/integration-example.ts +247 -0
- package/test/node.test.ts +4 -2
- package/test/rest-advanced.test.ts +455 -0
- package/test/rest.test.ts +283 -7
- package/tsconfig.tsbuildinfo +1 -1
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
|
-
|
|
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.
|
|
105
|
-
expect(response.body.
|
|
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.
|
|
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.
|
|
125
|
-
expect(response.body.
|
|
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.
|
|
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
|
});
|