Skip to content

Commit 8ee68a7

Browse files
authored
Merge pull request #184 from objectstack-ai/copilot/update-documentation-links
2 parents 92f9fdf + f0939a1 commit 8ee68a7

3 files changed

Lines changed: 100 additions & 26 deletions

File tree

packages/runtime/server/src/server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ export class ObjectQLServer {
182182
const skip = args.skip || 0;
183183
const limit = args.limit || items.length;
184184

185-
// Get total count - use the same arguments as the query to ensure consistency
186-
const total = await repo.count(args || {});
185+
// Get total count - exclude limit/skip to count all matching records
186+
const countArgs: any = {};
187+
if (args.filters) countArgs.filters = args.filters;
188+
if (args.expand) countArgs.expand = args.expand;
189+
const total = await repo.count(countArgs);
187190

188191
const size = limit;
189192
const page = limit > 0 ? Math.floor(skip / limit) + 1 : 1;

packages/runtime/server/test/rest-advanced.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ class MockDriver implements Driver {
4141
}
4242
}
4343

44-
// Apply skip and limit
44+
// Apply skip and top/limit (QueryAST uses 'top' for limit)
4545
if (query?.skip) {
4646
items = items.slice(query.skip);
4747
}
48-
if (query?.limit) {
49-
items = items.slice(0, query.limit);
48+
if (query?.top || query?.limit) {
49+
items = items.slice(0, query.top || query.limit);
5050
}
5151

5252
return items;
@@ -87,7 +87,12 @@ class MockDriver implements Driver {
8787
}
8888

8989
async count(objectName: string, query: any) {
90-
const items = await this.find(objectName, query);
90+
// Count should not apply skip/limit, only filters
91+
const countQuery = { ...query };
92+
delete countQuery.skip;
93+
delete countQuery.top;
94+
delete countQuery.limit;
95+
const items = await this.find(objectName, countQuery);
9196
return items.length;
9297
}
9398

packages/runtime/server/test/rest.test.ts

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,37 +27,97 @@ class MockDriver implements Driver {
2727
async find(objectName: string, query: any) {
2828
let items = this.data[objectName] || [];
2929

30-
// Apply filters if provided
30+
// Apply filters if provided (supports FilterNode array format)
3131
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));
4633
}
4734

48-
// Apply skip and limit if provided
35+
// Apply skip and top/limit if provided (QueryAST uses 'top' for limit)
4936
if (query) {
5037
if (query.skip) {
5138
items = items.slice(query.skip);
5239
}
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);
5542
}
5643
}
5744

5845
return items;
5946
}
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+
}
61121
async findOne(objectName: string, id: string | number, query?: any, options?: any) {
62122
const items = this.data[objectName] || [];
63123
if (id !== undefined && id !== null) {
@@ -97,7 +157,13 @@ class MockDriver implements Driver {
97157
}
98158

99159
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;
101167
}
102168

103169
async createMany(objectName: string, data: any[]) {

0 commit comments

Comments
 (0)