Fix filter operators broken across spec and memory driver#867
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add $notContains to StringOperatorSchema, FieldOperatorsSchema, Filter<T>, FILTER_OPERATORS, VALID_AST_OPERATORS, and AST_OPERATOR_MAP with both 'notcontains' and 'not_contains' AST aliases. Fixes #34 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
$notContains operator to filter DSL
|
@copilot 仔细评估是否已经完成任务中要求的所有开发要排查所有的过滤问题 |
…$notContains/$startsWith/$endsWith/$between/$null to Mingo-compatible format The root cause of Bug 1 (contains filter returns empty results): parseFilterAST() converts AST to FilterCondition with $-prefixed operators ($contains, $startsWith, etc.) but the memory driver passes them through to Mingo which doesn't understand these non-standard operators. Changes: - memory-driver.ts: Add normalizeFilterCondition/normalizeFieldOperators to convert $contains→$regex, $notContains→$not/$regex, $startsWith→$regex, $endsWith→$regex, $between→$gte/$lte, $null→$eq/$ne null - memory-driver.ts: Add notcontains/not_contains to convertConditionToMongo - memory-matcher.ts: Add $notContains and $null operator handling - memory-driver.test.ts: Add 12 tests for FilterCondition object format Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… clarity Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
After a thorough audit of all filter-related code paths across the monorepo, I've expanded the fix to cover all spec-repo-side issues from the 7-bug analysis: Commit
12 new driver-level tests confirm all operator formats work end-to-end. Bugs 4-7 (isEmpty rendering, between UI, in/notIn expansion, DataScopeManager case sensitivity) are frontend concerns in the |
$notContains operator to filter DSLThere was a problem hiding this comment.
Pull request overview
This PR fixes critical filter operator bugs in the ObjectStack spec and memory driver. The root cause was that parseFilterAST() converts AST operators to $-prefixed FilterCondition format (e.g., $contains), but the memory driver's convertToMongoQuery() previously had a passthrough branch that sent these directly to Mingo, which doesn't understand non-standard operators like $contains, resulting in empty results for all string/range/null filters.
Changes:
filter.zod.tsinpackages/spec: Adds$notContainstoStringOperatorSchema,FieldOperatorsSchema,Filter<T>type,VALID_AST_OPERATORS,AST_OPERATOR_MAP, andFILTER_OPERATORSmemory-driver.ts: Replaces the Mingo passthrough withnormalizeFilterCondition()andnormalizeFieldOperators()that translate$contains,$notContains,$startsWith,$endsWith,$between, and$nullto Mingo-native operators; also addsnotcontains/not_containstoconvertConditionToMongo()memory-matcher.ts: Adds$notContainsand$nullcases tocheckCondition(); adds corresponding tests acrossfilter.test.tsandmemory-driver.test.ts
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
packages/spec/src/data/filter.zod.ts |
Adds $notContains operator to all relevant schemas, types, maps and constants |
packages/spec/src/data/filter.test.ts |
Adds tests for $notContains schema validation, AST parsing, and isFilterAST detection |
packages/plugins/driver-memory/src/memory-driver.ts |
Replaces Mingo passthrough with normalizeFilterCondition + normalizeFieldOperators; adds notcontains/not_contains to convertConditionToMongo |
packages/plugins/driver-memory/src/memory-matcher.ts |
Adds $notContains and $null cases to checkCondition |
packages/plugins/driver-memory/src/memory-driver.test.ts |
Adds 12 integration tests covering all new operators in FilterCondition object format |
| case '$contains': | ||
| result.$regex = new RegExp(this.escapeRegex(val), 'i'); | ||
| break; | ||
| case '$notContains': | ||
| result.$not = { $regex: new RegExp(this.escapeRegex(val), 'i') }; | ||
| break; | ||
| case '$startsWith': | ||
| result.$regex = new RegExp(`^${this.escapeRegex(val)}`, 'i'); | ||
| break; | ||
| case '$endsWith': | ||
| result.$regex = new RegExp(`${this.escapeRegex(val)}$`, 'i'); | ||
| break; |
There was a problem hiding this comment.
In normalizeFieldOperators, the cases for $contains, $startsWith, and $endsWith all write to result.$regex. If a filter has more than one of these operators on the same field (e.g., { name: { $startsWith: 'A', $endsWith: 'n' } }), the second operator silently overwrites the $regex set by the first. Only the last processed operator's regex pattern is applied, and the earlier constraints are lost. To fix this, each of these operators should be combined into a single regex (e.g., concatenating both ^ and $ anchors into one expression), or this limitation should be documented.
| case '$null': | ||
| // $null: true → value must be null/undefined; $null: false → value must not be null/undefined | ||
| if (target === true && value != null) return false; | ||
| if (target === false && value == null) return false; | ||
| break; |
There was a problem hiding this comment.
In checkCondition, there is an early-return guard at line 106: if (value === undefined && op !== '$exists' && op !== '$ne') { return false; }. Because $null is not excluded from this guard, when a field is missing (value === undefined) and the operator is $null: true, the guard fires and returns false — meaning $null: true fails to match fields that don't exist on a record. Since $null: true is semantically "field is null or missing", the guard should also exclude $null (i.e., the condition should be op !== '$exists' && op !== '$ne' && op !== '$null') so that the $null case in the switch can evaluate correctly for undefined values.
| it('should handle $contains inside $or', async () => { | ||
| const results = await driver.find(testTable, { | ||
| object: testTable, | ||
| where: { $or: [{ name: { $startsWith: 'Al' } }, { name: { $startsWith: 'Ev' } }] }, | ||
| }); | ||
| expect(results).toHaveLength(2); | ||
| expect(results.map((r: any) => r.name).sort()).toEqual(['Alice Johnson', 'Evan Davis']); | ||
| }); |
There was a problem hiding this comment.
The test at line 680 is named 'should handle $contains inside $or', but the test body actually uses $startsWith inside the $or clause — not $contains. The description should be updated to 'should handle $startsWith inside $or' to accurately reflect what is being tested.
|
@copilot fix all |
…st name 1. normalizeFieldOperators: collect regex-producing operators ($contains, $startsWith, $endsWith) into an array; when multiple exist on the same field, promote to $and so no regex silently overwrites another. 2. memory-matcher checkCondition: exclude $null from the undefined early- return guard so $null: true correctly matches missing fields. 3. Rename test from "$contains inside $or" to "$startsWith inside $or" to match actual test body. 4. Add tests: combined $startsWith + $endsWith on same field, $null: true on records with missing fields. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Fixed all three review issues in
|
Comprehensive fix for filter operators (
contains,notContains,startsWith,endsWith,between,null) that were broken or inconsistent across the spec definitions and memory driver.The critical root cause:
parseFilterAST()converts AST operators to$-prefixed FilterCondition format ($contains,$startsWith, etc.), but the memory driver'sconvertToMongoQuery()passthrough branch sent these directly to Mingo — which doesn't understand them. All string/range/null filters returned empty results.Changes
Spec —
filter.zod.tsStringOperatorSchema/FieldOperatorsSchema— add$notContains: z.string().optional()VALID_AST_OPERATORS— addnotcontains,not_containsAST_OPERATOR_MAP— map both aliases →$notContainsFILTER_OPERATORS— add$notContainsto the const tupleFilter<T>— add$notContainsto the generic typeMemory Driver —
memory-driver.ts(Bug 1 root cause fix)convertToMongoQuery()passthrough (return filters) withnormalizeFilterCondition()that recursively translates non-standard operators to Mingo-compatible format:$contains→$regex(case-insensitive)$notContains→$not: { $regex }(case-insensitive)$startsWith→$regexwith^anchor$endsWith→$regexwith$anchor$between→$gte/$lte$null: true/false→$eq: null/$ne: null{ $startsWith: 'A', $endsWith: 'n' }) by promoting to$andso no regex silently overwrites anothernotcontains/not_containstoconvertConditionToMongo()for legacy AST formatMemory Matcher —
memory-matcher.ts$notContainsoperator tocheckCondition()$nulloperator tocheckCondition()$nullso$null: truecorrectly matches records where the field is missingTests
$notContainsschema validation and AST parsing$contains,$notContains,$startsWith,$endsWith,$between,$nullvia FilterCondition object format, including$and/$orlogical combinations, combined$startsWith+$endsWithon the same field, and$null: trueon records with missing fieldsCross-repo note
This addresses bugs 1–3 from the audit (driver passthrough, missing
notContains,startsWith/endsWithnormalization) plus$betweenand$nullhandling. Bugs 4–7 (isEmptyrendering,betweendual-input UI,in/notInexpansion,DataScopeManagercase sensitivity) are frontend concerns that require coordinated changes inobjectui.Original prompt
This section details on the original issue you should resolve
<issue_title>[Bug] Filter operators (contains, notContains, isEmpty, between) broken or inconsistent across frontend and server</issue_title>
<issue_description>## Summary
The
containstext search filter returns no results whileequalsworks correctly (see screenshot). After a thorough code audit, 7 related filter issues were identified across theobjectuiandspecrepos.Root Cause Analysis
Bug 1 (Critical):
containsfilter returns empty results — Server-side double-conversion['name', 'contains', 'Evan']parseFilterAST()converts to{ name: { $contains: 'Evan' } }{ name: { $contains: 'Evan' } }but does not translate$contains→ database-native (SQL/Mongo regex)Other issues:
notContainsoperator missing from mapping tablesisEmpty/isNotEmptyonly matches null, not empty stringsbetweenfilter renders only one input, needs min/maxcontains)in/notInoperator expanded in frontend; drivers support nativelystartsWith/endsWithProposed Fixes
parseFilterAST()call and let drivers process raw AST arrays$contains,$startsWith,$endsWith,$notContainsto native DB queriesReproduction
Affected Files
Cross-Repo Impact
Requires coordinated changes in both objectui and spec repositories (especially data engine drivers).</issue_description>
Comments on the Issue (you are @copilot in this section)
@hotlongLet me do a thorough audit of all filter-related code paths to identify any similar issues before creating a comprehensive issue.
好的,我已经完成了对两个仓库中所有过滤相关代码的全面排查。下面我来总结所有发现的问题,然后创建一个合并的任务。
🔍 全面排查结果
问题 1(原始报告):
contains过滤器在服务端不工作数据流完整追踪:
Contains,生成operator: 'contains'✅mapOperator()→'contains'→'contains'✅['name', 'contains', 'Evan']为 JSON 发送 ✅parseFilterAST()→['name', 'contains', 'Evan']→{ name: { $contains: 'Evan' } }✅toQueryAST()→ 直接透传options.filter到ast.where,此时 filter 已经被parseFilterAST转换成了{ name: { $contains: 'Evan' } }这种 MongoDB 对象格式convertToMongoQuery()→ 接收到的是 已转换的对象格式,走MongoDB format passthrough分支直接返回{ name: { $contains: 'Evan' } }$contains操作符!Mingo 使用$regex做模糊匹配,所以查询返回空���果。根因:
parseFilterAST将 ASTcontains转换为$contains,但 Mingo 不支持$contains。Memory Driver 的convertConditionToMongo方法能正确处理contains→$regex,但 passthrough 分支跳过了这个转换。问题 2:
notContains操作符缺失 AST 映射notcontains在parseFilterAST的AST_OPERATOR_MAP中 没有映射:notcontains会走 fallback 变成$notcontains,而 Driver 同样不认识。问题 3:
startswith操作符大小写不一致前端
mapOperator中没有startsWith/endsWith映射(FilterBuilder 未暴露这些操作符给用户),但VALID_AST_OPERATORS中同时存在startswith和starts_with:而 Memory Driver
convertConditionToMongo处理了startswith/starts_with,但AST_OPERATOR_MAP映射到的是$startsWith(驼峰),如果 filter 对象格式被 passthrough,Mingo 同样不认识。问题 4:
isEmpty/isNotEmpty的 null 转换可能导致意外行为