|
1 | 1 | --- |
2 | 2 | title: NormalizedFilter |
3 | | -description: Internal AST representation of filter conditions after normalization |
| 3 | +description: NormalizedFilter Schema Reference |
4 | 4 | --- |
5 | 5 |
|
6 | | -# NormalizedFilter |
7 | | - |
8 | | -NormalizedFilter is the internal Abstract Syntax Tree (AST) representation of filter conditions after converting all syntactic sugar to explicit operators. This simplified structure makes it easier for driver implementations to process filters consistently. |
9 | | - |
10 | | -## Overview |
11 | | - |
12 | | -During the normalization pass, implicit syntax is converted to explicit operator-based conditions: |
13 | | - |
14 | | -**Input (User-friendly):** |
15 | | -```typescript |
16 | | -{ age: 18, role: "admin" } |
17 | | -``` |
18 | | - |
19 | | -**Output (Normalized):** |
20 | | -```typescript |
21 | | -{ |
22 | | - $and: [ |
23 | | - { age: { $eq: 18 } }, |
24 | | - { role: { $eq: "admin" } } |
25 | | - ] |
26 | | -} |
27 | | -``` |
28 | | - |
29 | | -## Schema |
30 | | - |
31 | | -```typescript |
32 | | -{ |
33 | | - $and?: Array<FieldCondition | NormalizedFilter>, |
34 | | - $or?: Array<FieldCondition | NormalizedFilter>, |
35 | | - $not?: FieldCondition | NormalizedFilter |
36 | | -} |
37 | | - |
38 | | -type FieldCondition = Record<string, FieldOperators> |
39 | | -``` |
40 | | -
|
41 | 6 | ## Properties |
42 | 7 |
|
43 | 8 | | Property | Type | Required | Description | |
44 | 9 | | :--- | :--- | :--- | :--- | |
45 | | -| **$and** | `Array<FieldCondition \| NormalizedFilter>` | optional | All conditions must be true (logical AND) | |
46 | | -| **$or** | `Array<FieldCondition \| NormalizedFilter>` | optional | At least one condition must be true (logical OR) | |
47 | | -| **$not** | `FieldCondition \| NormalizedFilter` | optional | Negates the condition (logical NOT) | |
48 | | -
|
49 | | -## Normalization Process |
50 | | -
|
51 | | -### Stage 1: Implicit Equality → Explicit $eq |
52 | | -
|
53 | | -**Before:** |
54 | | -```typescript |
55 | | -{ status: "active", verified: true } |
56 | | -``` |
57 | | -
|
58 | | -**After:** |
59 | | -```typescript |
60 | | -{ |
61 | | - $and: [ |
62 | | - { status: { $eq: "active" } }, |
63 | | - { verified: { $eq: true } } |
64 | | - ] |
65 | | -} |
66 | | -``` |
67 | | -
|
68 | | -### Stage 2: Flatten Top-level AND |
69 | | -
|
70 | | -**Before:** |
71 | | -```typescript |
72 | | -{ |
73 | | - status: "active", |
74 | | - age: { $gte: 18 }, |
75 | | - role: "admin" |
76 | | -} |
77 | | -``` |
78 | | -
|
79 | | -**After:** |
80 | | -```typescript |
81 | | -{ |
82 | | - $and: [ |
83 | | - { status: { $eq: "active" } }, |
84 | | - { age: { $gte: 18 } }, |
85 | | - { role: { $eq: "admin" } } |
86 | | - ] |
87 | | -} |
88 | | -``` |
89 | | -
|
90 | | -### Stage 3: Preserve Logical Operators |
91 | | -
|
92 | | -**Before:** |
93 | | -```typescript |
94 | | -{ |
95 | | - status: "active", |
96 | | - $or: [ |
97 | | - { role: "admin" }, |
98 | | - { permissions: { $contains: "write" } } |
99 | | - ] |
100 | | -} |
101 | | -``` |
102 | | -
|
103 | | -**After:** |
104 | | -```typescript |
105 | | -{ |
106 | | - $and: [ |
107 | | - { status: { $eq: "active" } }, |
108 | | - { |
109 | | - $or: [ |
110 | | - { role: { $eq: "admin" } }, |
111 | | - { permissions: { $contains: "write" } } |
112 | | - ] |
113 | | - } |
114 | | - ] |
115 | | -} |
116 | | -``` |
117 | | -
|
118 | | -## Examples |
119 | | -
|
120 | | -### Simple AND Condition |
121 | | -
|
122 | | -**Input:** |
123 | | -```typescript |
124 | | -const filter: QueryFilter = { |
125 | | - where: { |
126 | | - status: "active", |
127 | | - age: { $gte: 18 } |
128 | | - } |
129 | | -}; |
130 | | -``` |
131 | | - |
132 | | -**Normalized:** |
133 | | -```typescript |
134 | | -{ |
135 | | - $and: [ |
136 | | - { status: { $eq: "active" } }, |
137 | | - { age: { $gte: 18 } } |
138 | | - ] |
139 | | -} |
140 | | -``` |
141 | | - |
142 | | -### OR with Nested AND |
143 | | - |
144 | | -**Input:** |
145 | | -```typescript |
146 | | -const filter: QueryFilter = { |
147 | | - where: { |
148 | | - $or: [ |
149 | | - { role: "admin" }, |
150 | | - { |
151 | | - $and: [ |
152 | | - { verified: true }, |
153 | | - { score: { $gt: 80 } } |
154 | | - ] |
155 | | - } |
156 | | - ] |
157 | | - } |
158 | | -}; |
159 | | -``` |
160 | | - |
161 | | -**Normalized:** |
162 | | -```typescript |
163 | | -{ |
164 | | - $or: [ |
165 | | - { role: { $eq: "admin" } }, |
166 | | - { |
167 | | - $and: [ |
168 | | - { verified: { $eq: true } }, |
169 | | - { score: { $gt: 80 } } |
170 | | - ] |
171 | | - } |
172 | | - ] |
173 | | -} |
174 | | -``` |
175 | | - |
176 | | -### NOT Condition |
177 | | - |
178 | | -**Input:** |
179 | | -```typescript |
180 | | -const filter: QueryFilter = { |
181 | | - where: { |
182 | | - $not: { |
183 | | - status: "deleted", |
184 | | - archived: true |
185 | | - } |
186 | | - } |
187 | | -}; |
188 | | -``` |
189 | | - |
190 | | -**Normalized:** |
191 | | -```typescript |
192 | | -{ |
193 | | - $not: { |
194 | | - $and: [ |
195 | | - { status: { $eq: "deleted" } }, |
196 | | - { archived: { $eq: true } } |
197 | | - ] |
198 | | - } |
199 | | -} |
200 | | -``` |
201 | | - |
202 | | -### Complex Nested Structure |
203 | | - |
204 | | -**Input:** |
205 | | -```typescript |
206 | | -const filter: QueryFilter = { |
207 | | - where: { |
208 | | - status: "active", |
209 | | - $and: [ |
210 | | - { age: { $gte: 18 } }, |
211 | | - { |
212 | | - $or: [ |
213 | | - { role: "admin" }, |
214 | | - { permissions: { $contains: "edit" } } |
215 | | - ] |
216 | | - } |
217 | | - ], |
218 | | - department: { |
219 | | - name: "Engineering" |
220 | | - } |
221 | | - } |
222 | | -}; |
223 | | -``` |
224 | | - |
225 | | -**Normalized:** |
226 | | -```typescript |
227 | | -{ |
228 | | - $and: [ |
229 | | - { status: { $eq: "active" } }, |
230 | | - { age: { $gte: 18 } }, |
231 | | - { |
232 | | - $or: [ |
233 | | - { role: { $eq: "admin" } }, |
234 | | - { permissions: { $contains: "edit" } } |
235 | | - ] |
236 | | - }, |
237 | | - { "department.name": { $eq: "Engineering" } } // Flattened path |
238 | | - ] |
239 | | -} |
240 | | -``` |
241 | | - |
242 | | -## Benefits for Driver Implementation |
243 | | - |
244 | | -### 1. Consistent Structure |
245 | | -Every filter is guaranteed to have explicit operators, eliminating ambiguity. |
246 | | - |
247 | | -### 2. Simplified Traversal |
248 | | -Drivers can use a simple recursive pattern: |
249 | | -```typescript |
250 | | -function processFilter(filter: NormalizedFilter) { |
251 | | - if (filter.$and) return processAnd(filter.$and); |
252 | | - if (filter.$or) return processOr(filter.$or); |
253 | | - if (filter.$not) return processNot(filter.$not); |
254 | | - // Process field conditions |
255 | | -} |
256 | | -``` |
257 | | - |
258 | | -### 3. SQL Generation Example |
259 | | -```typescript |
260 | | -function toSQL(filter: NormalizedFilter): string { |
261 | | - if (filter.$and) { |
262 | | - return filter.$and.map(toSQL).join(' AND '); |
263 | | - } |
264 | | - if (filter.$or) { |
265 | | - return filter.$or.map(toSQL).join(' OR '); |
266 | | - } |
267 | | - if (filter.$not) { |
268 | | - return `NOT (${toSQL(filter.$not)})`; |
269 | | - } |
270 | | - // Handle field conditions |
271 | | -} |
272 | | -``` |
273 | | - |
274 | | -### 4. MongoDB Query Example |
275 | | -```typescript |
276 | | -function toMongo(filter: NormalizedFilter) { |
277 | | - if (filter.$and) { |
278 | | - return { $and: filter.$and.map(toMongo) }; |
279 | | - } |
280 | | - if (filter.$or) { |
281 | | - return { $or: filter.$or.map(toMongo) }; |
282 | | - } |
283 | | - if (filter.$not) { |
284 | | - return { $nor: [toMongo(filter.$not)] }; |
285 | | - } |
286 | | - // Handle field conditions |
287 | | -} |
288 | | -``` |
289 | | - |
290 | | -## Field Operators in Normalized Filters |
291 | | - |
292 | | -Each field condition uses explicit operators: |
293 | | - |
294 | | -```typescript |
295 | | -{ |
296 | | - $eq?: any, // Equal to |
297 | | - $ne?: any, // Not equal to |
298 | | - $gt?: number | Date, // Greater than |
299 | | - $gte?: number | Date,// Greater than or equal |
300 | | - $lt?: number | Date, // Less than |
301 | | - $lte?: number | Date,// Less than or equal |
302 | | - $in?: any[], // In array |
303 | | - $nin?: any[], // Not in array |
304 | | - $between?: [number | Date, number | Date], // Between range |
305 | | - $contains?: string, // Contains substring |
306 | | - $startsWith?: string,// Starts with prefix |
307 | | - $endsWith?: string, // Ends with suffix |
308 | | - $null?: boolean, // Is/isn't null |
309 | | - $exist?: boolean // Field exists |
310 | | -} |
311 | | -``` |
312 | | - |
313 | | -## Usage in Driver Implementation |
314 | | - |
315 | | -Drivers should normalize user input before processing: |
316 | | - |
317 | | -```typescript |
318 | | -import { normalizeFilter } from '@objectstack/spec'; |
319 | | - |
320 | | -class MyDriver implements Driver { |
321 | | - async find(object: string, query: Query) { |
322 | | - // Normalize filter |
323 | | - const normalized = normalizeFilter(query.where); |
324 | | - |
325 | | - // Convert to database-specific query |
326 | | - const dbQuery = this.toNativeQuery(normalized); |
327 | | - |
328 | | - // Execute |
329 | | - return this.execute(dbQuery); |
330 | | - } |
331 | | - |
332 | | - private toNativeQuery(filter: NormalizedFilter) { |
333 | | - // Driver-specific implementation |
334 | | - } |
335 | | -} |
336 | | -``` |
337 | | - |
338 | | -## See Also |
339 | | - |
340 | | -- [QueryFilter](./QueryFilter.mdx) - User-friendly filter syntax |
341 | | -- [FilterCondition](./FilterCondition.mdx) - Recursive filter structure |
342 | | -- [Driver Implementation Guide](/docs/guides/custom-driver.mdx) - Building custom drivers |
| 10 | +| **$and** | `Record<string, object> \| any[]` | optional | | |
| 11 | +| **$or** | `Record<string, object> \| any[]` | optional | | |
| 12 | +| **$not** | `Record<string, object> \| any` | optional | | |
0 commit comments