Skip to content

Commit cdd675e

Browse files
Copilothotlong
andcommitted
feat: Migrate to modern FilterCondition syntax in types and core
- Replace FilterExpression array syntax with FilterCondition object syntax - Update UnifiedQuery to use Filter (FilterCondition) type - Implement filter translation from new object syntax to legacy array format - Update all API types to use new Filter type - Add operator mapping: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, etc. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 5139d6b commit cdd675e

4 files changed

Lines changed: 153 additions & 19 deletions

File tree

packages/drivers/sdk/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
MetadataApiResponse,
4949
ObjectQLError,
5050
ApiErrorCode,
51-
FilterExpression
51+
Filter
5252
} from '@objectql/types';
5353

5454
/**
@@ -391,7 +391,7 @@ export class DataApiClient implements IDataApiClient {
391391
);
392392
}
393393

394-
async count(objectName: string, filters?: FilterExpression): Promise<DataApiCountResponse> {
394+
async count(objectName: string, filters?: Filter): Promise<DataApiCountResponse> {
395395
return this.request<DataApiCountResponse>(
396396
'GET',
397397
`${this.dataPath}/${objectName}`,

packages/foundation/core/src/repository.ts

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, FilterExpression } from '@objectql/types';
9+
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, Filter } from '@objectql/types';
1010
import type { ObjectStackKernel } from '@objectstack/runtime';
1111
import type { QueryAST, FilterNode, SortNode } from '@objectstack/spec';
1212
import { Validator } from './validator';
@@ -43,16 +43,119 @@ export class ObjectRepository {
4343
}
4444

4545
/**
46-
* Translates ObjectQL FilterExpression to ObjectStack FilterNode format
46+
* Translates ObjectQL Filter (FilterCondition) to ObjectStack FilterNode format
47+
*
48+
* Converts modern object-based syntax to legacy array-based syntax:
49+
* Input: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] }
50+
* Output: [["age", ">=", 18], "or", [["status", "=", "active"], "or", ["role", "=", "admin"]]]
4751
*/
48-
private translateFilters(filters?: FilterExpression[]): FilterNode | undefined {
49-
if (!filters || filters.length === 0) {
52+
private translateFilters(filters?: Filter): FilterNode | undefined {
53+
if (!filters || Object.keys(filters).length === 0) {
5054
return undefined;
5155
}
5256

53-
// FilterExpression[] is already compatible with FilterNode format
54-
// Just pass through as-is
55-
return filters as FilterNode;
57+
return this.convertFilterToNode(filters);
58+
}
59+
60+
/**
61+
* Recursively converts FilterCondition to FilterNode array format
62+
*/
63+
private convertFilterToNode(filter: Filter): FilterNode {
64+
const nodes: any[] = [];
65+
66+
// Process logical operators first
67+
if (filter.$and) {
68+
const andNodes = filter.$and.map(f => this.convertFilterToNode(f));
69+
nodes.push(...this.interleaveWithOperator(andNodes, 'and'));
70+
}
71+
72+
if (filter.$or) {
73+
const orNodes = filter.$or.map(f => this.convertFilterToNode(f));
74+
if (nodes.length > 0) {
75+
nodes.push('and');
76+
}
77+
nodes.push(...this.interleaveWithOperator(orNodes, 'or'));
78+
}
79+
80+
if (filter.$not) {
81+
// NOT operator: convert to array of negated conditions
82+
// This is a simplification - proper implementation would need driver support
83+
const notNode = this.convertFilterToNode(filter.$not);
84+
if (nodes.length > 0) {
85+
nodes.push('and');
86+
}
87+
// Wrap in array to indicate it's a NOT group
88+
nodes.push(['not', notNode]);
89+
}
90+
91+
// Process field conditions
92+
for (const [field, value] of Object.entries(filter)) {
93+
if (field.startsWith('$')) {
94+
continue; // Skip logical operators (already processed)
95+
}
96+
97+
if (nodes.length > 0) {
98+
nodes.push('and');
99+
}
100+
101+
// Handle field value
102+
if (value === null || value === undefined) {
103+
nodes.push([field, '=', value]);
104+
} else if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
105+
// Explicit operators
106+
for (const [op, opValue] of Object.entries(value)) {
107+
if (nodes.length > 0 && nodes[nodes.length - 1] !== 'and') {
108+
nodes.push('and');
109+
}
110+
111+
const legacyOp = this.mapOperatorToLegacy(op);
112+
nodes.push([field, legacyOp, opValue]);
113+
}
114+
} else {
115+
// Implicit equality
116+
nodes.push([field, '=', value]);
117+
}
118+
}
119+
120+
return nodes.length === 1 ? nodes[0] : nodes;
121+
}
122+
123+
/**
124+
* Interleaves filter nodes with a logical operator
125+
*/
126+
private interleaveWithOperator(nodes: FilterNode[], operator: string): any[] {
127+
if (nodes.length === 0) return [];
128+
if (nodes.length === 1) return [nodes[0]];
129+
130+
const result: any[] = [nodes[0]];
131+
for (let i = 1; i < nodes.length; i++) {
132+
result.push(operator, nodes[i]);
133+
}
134+
return result;
135+
}
136+
137+
/**
138+
* Maps modern $-prefixed operators to legacy format
139+
*/
140+
private mapOperatorToLegacy(operator: string): string {
141+
const mapping: Record<string, string> = {
142+
'$eq': '=',
143+
'$ne': '!=',
144+
'$gt': '>',
145+
'$gte': '>=',
146+
'$lt': '<',
147+
'$lte': '<=',
148+
'$in': 'in',
149+
'$nin': 'nin',
150+
'$contains': 'contains',
151+
'$startsWith': 'startswith',
152+
'$endsWith': 'endswith',
153+
'$null': 'is_null',
154+
'$exist': 'is_not_null',
155+
'$between': 'between',
156+
};
157+
158+
return mapping[operator] || operator.replace('$', '');
56159
}
57160

58161
/**

packages/foundation/types/src/api.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* These types enable frontend applications to make type-safe API calls.
1414
*/
1515

16-
import { UnifiedQuery, FilterExpression } from './query';
16+
import { UnifiedQuery, Filter } from './query';
1717
import { ObjectConfig } from './object';
1818
import { FieldConfig } from './field';
1919
import { ActionConfig } from './action';
@@ -142,8 +142,8 @@ export interface DataApiItemResponse<T = unknown> extends DataApiResponse<T> {
142142
* Query parameters for GET /api/data/:object (list records)
143143
*/
144144
export interface DataApiListParams {
145-
/** Filter expression (can be FilterExpression array or JSON string) */
146-
filter?: FilterExpression | string;
145+
/** Filter expression (can be Filter object or JSON string) */
146+
filter?: Filter | string;
147147
/** Fields to return (array or comma-separated string) */
148148
fields?: string[] | string;
149149
/** Sort criteria - array of [field, direction] tuples */
@@ -184,7 +184,7 @@ export interface DataApiUpdateRequest {
184184
*/
185185
export interface DataApiBulkUpdateRequest {
186186
/** Filter criteria to select records to update */
187-
filters: FilterExpression;
187+
filters: Filter;
188188
/** Data to update */
189189
data: Record<string, unknown>;
190190
}
@@ -194,7 +194,7 @@ export interface DataApiBulkUpdateRequest {
194194
*/
195195
export interface DataApiBulkDeleteRequest {
196196
/** Filter criteria to select records to delete */
197-
filters: FilterExpression;
197+
filters: Filter;
198198
}
199199

200200
/**
@@ -461,7 +461,7 @@ export interface IDataApiClient {
461461
* @param objectName - Name of the object
462462
* @param filters - Filter criteria
463463
*/
464-
count(objectName: string, filters?: FilterExpression): Promise<DataApiCountResponse>;
464+
count(objectName: string, filters?: Filter): Promise<DataApiCountResponse>;
465465
}
466466

467467
/**

packages/foundation/types/src/query.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,21 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
export type FilterCriterion = [string, string, any];
10-
export type FilterExpression = FilterCriterion | 'and' | 'or' | FilterExpression[];
9+
import type { FilterCondition } from '@objectstack/spec';
10+
11+
/**
12+
* Modern Query Filter using @objectstack/spec FilterCondition
13+
*
14+
* Supports MongoDB/Prisma-style object-based syntax:
15+
* - Implicit equality: { field: value }
16+
* - Explicit operators: { field: { $eq: value, $gt: 10 } }
17+
* - Logical operators: { $and: [...], $or: [...], $not: {...} }
18+
* - String operators: { name: { $contains: "text" } }
19+
* - Range operators: { age: { $between: [18, 65] } }
20+
* - Set operators: { status: { $in: ["active", "pending"] } }
21+
* - Null checks: { field: { $null: true } }
22+
*/
23+
export type Filter = FilterCondition;
1124

1225
export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
1326

@@ -17,15 +30,33 @@ export interface AggregateOption {
1730
alias?: string; // Optional: rename the result field
1831
}
1932

33+
/**
34+
* Unified Query Interface
35+
*
36+
* Provides a consistent query API across all ObjectQL drivers.
37+
*/
2038
export interface UnifiedQuery {
39+
/** Field selection - specify which fields to return */
2140
fields?: string[];
22-
filters?: FilterExpression[];
41+
42+
/** Filter conditions using modern FilterCondition syntax */
43+
filters?: Filter;
44+
45+
/** Sort order - array of [field, direction] tuples */
2346
sort?: [string, 'asc' | 'desc'][];
47+
48+
/** Pagination - number of records to skip */
2449
skip?: number;
50+
51+
/** Pagination - maximum number of records to return */
2552
limit?: number;
53+
54+
/** Relation expansion - load related records */
2655
expand?: Record<string, UnifiedQuery>;
2756

28-
// === Aggregation Support ===
57+
/** Aggregation - group by fields */
2958
groupBy?: string[];
59+
60+
/** Aggregation - aggregate functions to apply */
3061
aggregate?: AggregateOption[];
3162
}

0 commit comments

Comments
 (0)