@objectql/server 4.0.0 → 4.0.2

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
@@ -27,37 +27,97 @@ class MockDriver implements Driver {
27
27
  async find(objectName: string, query: any) {
28
28
  let items = this.data[objectName] || [];
29
29
 
30
- // Apply filters if provided
30
+ // Apply filters if provided (supports FilterNode array format)
31
31
  if (query && query.filters) {
32
- const filters = query.filters;
33
- if (typeof filters === 'object') {
34
- const filterKeys = Object.keys(filters);
35
- if (filterKeys.length > 0) {
36
- items = items.filter(item => {
37
- for (const [key, value] of Object.entries(filters)) {
38
- if (item[key] !== value) {
39
- return false;
40
- }
41
- }
42
- return true;
43
- });
44
- }
45
- }
32
+ items = items.filter(item => this.matchesFilter(item, query.filters));
46
33
  }
47
34
 
48
- // Apply skip and limit if provided
35
+ // Apply skip and top/limit if provided (QueryAST uses 'top' for limit)
49
36
  if (query) {
50
37
  if (query.skip) {
51
38
  items = items.slice(query.skip);
52
39
  }
53
- if (query.limit) {
54
- items = items.slice(0, query.limit);
40
+ if (query.top || query.limit) {
41
+ items = items.slice(0, query.top || query.limit);
55
42
  }
56
43
  }
57
44
 
58
45
  return items;
59
46
  }
60
-
47
+
48
+ /**
49
+ * Matches an item against a filter condition
50
+ * @param item - The data item to test
51
+ * @param filter - The filter in FilterNode array format or simple object format
52
+ * FilterNode format: [field, op, value] for single condition
53
+ * Complex filters: [[field, op, value], 'and', [field2, op2, value2]]
54
+ * Simple format: { field: value }
55
+ * @returns true if item matches the filter
56
+ */
57
+ private matchesFilter(item: any, filter: any): boolean {
58
+ if (!filter) return true;
59
+
60
+ // Handle FilterNode array format: [[field, op, value]] or [[field, op, value], 'and', [field2, op2, value2]]
61
+ if (Array.isArray(filter)) {
62
+ // Single condition: [field, op, value]
63
+ if (filter.length === 3 && typeof filter[0] === 'string') {
64
+ const [field, op, value] = filter;
65
+ return this.evaluateCondition(item, field, op, value);
66
+ }
67
+
68
+ // Multiple conditions with logical operators
69
+ let result = true;
70
+ let currentOp = 'and';
71
+ for (let i = 0; i < filter.length; i++) {
72
+ const element = filter[i];
73
+ if (typeof element === 'string') {
74
+ currentOp = element; // 'and' or 'or'
75
+ } else if (Array.isArray(element)) {
76
+ const conditionResult = this.matchesFilter(item, element);
77
+ if (currentOp === 'and') {
78
+ result = result && conditionResult;
79
+ } else if (currentOp === 'or') {
80
+ result = result || conditionResult;
81
+ }
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ // Handle simple object format: { field: value }
88
+ if (typeof filter === 'object') {
89
+ for (const [key, value] of Object.entries(filter)) {
90
+ if (item[key] !== value) {
91
+ return false;
92
+ }
93
+ }
94
+ return true;
95
+ }
96
+
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Evaluates a single filter condition
102
+ * @param item - The data item to test
103
+ * @param field - The field name
104
+ * @param op - The operator: '=', '!=', '>', '>=', '<', '<=', 'in'
105
+ * @param value - The value to compare against
106
+ * @returns true if the condition is satisfied
107
+ */
108
+ private evaluateCondition(item: any, field: string, op: string, value: any): boolean {
109
+ const fieldValue = item[field];
110
+ switch (op) {
111
+ case '=': return fieldValue === value;
112
+ case '!=': return fieldValue !== value;
113
+ case '>': return fieldValue > value;
114
+ case '>=': return fieldValue >= value;
115
+ case '<': return fieldValue < value;
116
+ case '<=': return fieldValue <= value;
117
+ case 'in': return Array.isArray(value) ? value.includes(fieldValue) : false;
118
+ default: return true;
119
+ }
120
+ }
61
121
  async findOne(objectName: string, id: string | number, query?: any, options?: any) {
62
122
  const items = this.data[objectName] || [];
63
123
  if (id !== undefined && id !== null) {
@@ -97,7 +157,13 @@ class MockDriver implements Driver {
97
157
  }
98
158
 
99
159
  async count(objectName: string, query: any) {
100
- return (this.data[objectName] || []).length;
160
+ // Count should apply filters but not skip/limit
161
+ const countQuery = { ...query };
162
+ delete countQuery.skip;
163
+ delete countQuery.top;
164
+ delete countQuery.limit;
165
+ const items = await this.find(objectName, countQuery);
166
+ return items.length;
101
167
  }
102
168
 
103
169
  async createMany(objectName: string, data: any[]) {
@@ -201,6 +267,8 @@ describe('REST API Adapter', () => {
201
267
  }
202
268
  }
203
269
  });
270
+
271
+ await app.init();
204
272
 
205
273
  // Create handler and server once for all tests
206
274
  handler = createRESTHandler(app);
@@ -223,8 +291,9 @@ describe('REST API Adapter', () => {
223
291
  .set('Accept', 'application/json');
224
292
 
225
293
  expect(response.status).toBe(200);
226
- expect(response.body.name).toBe('Alice');
227
- expect(response.body['@type']).toBe('user');
294
+ expect(response.body.data).toBeDefined();
295
+ expect(response.body.data.name).toBe('Alice');
296
+ expect(response.body.data['@type']).toBe('user');
228
297
  });
229
298
 
230
299
  it('should handle POST /api/data/:object - Create record', async () => {
@@ -234,9 +303,10 @@ describe('REST API Adapter', () => {
234
303
  .set('Accept', 'application/json');
235
304
 
236
305
  expect(response.status).toBe(201);
237
- expect(response.body.name).toBe('Charlie');
238
- expect(response.body._id).toBeDefined();
239
- expect(response.body['@type']).toBe('user');
306
+ expect(response.body.data).toBeDefined();
307
+ expect(response.body.data.name).toBe('Charlie');
308
+ expect(response.body.data._id).toBeDefined();
309
+ expect(response.body.data['@type']).toBe('user');
240
310
  });
241
311
 
242
312
  it('should handle PUT /api/data/:object/:id - Update record', async () => {
@@ -254,8 +324,9 @@ describe('REST API Adapter', () => {
254
324
  .set('Accept', 'application/json');
255
325
 
256
326
  expect(response.status).toBe(200);
257
- expect(response.body.deleted).toBe(true);
258
- expect(response.body['@type']).toBe('user');
327
+ expect(response.body.data).toBeDefined();
328
+ expect(response.body.data.deleted).toBe(true);
329
+ expect(response.body.data['@type']).toBe('user');
259
330
  });
260
331
 
261
332
  it('should return 404 for non-existent object', async () => {
@@ -442,7 +513,8 @@ describe('REST API Adapter', () => {
442
513
 
443
514
  // Should still work as single create
444
515
  expect(response.status).toBe(201);
445
- expect(response.body._id).toBeDefined();
516
+ expect(response.body.data).toBeDefined();
517
+ expect(response.body.data._id).toBeDefined();
446
518
  });
447
519
  });
448
520
  });