Skip to content

Fix filter AST array format incompatibility with query engine#800

Merged
hotlong merged 3 commits into
mainfrom
copilot/fix-filters-parameter-issue
Feb 24, 2026
Merged

Fix filter AST array format incompatibility with query engine#800
hotlong merged 3 commits into
mainfrom
copilot/fix-filters-parameter-issue

Conversation

Copilot AI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Client sends filters as JSON array AST (["and", ["priority", "=", "high"], ["status", "=", "active"]]), but the query engine expects FilterCondition objects ({ $and: [{ priority: "high" }, { status: "active" }] }). Two bugs: (1) no AST→object conversion after JSON.parse, (2) dispatcher passes { filters: query } instead of { query }, so findData never sees the params.

Changes

  • packages/spec/src/data/filter.zod.ts — Add parseFilterAST() utility that converts array AST to FilterCondition objects. Supports all comparison operators (=, !=, >, >=, <, <=, in, not_in, contains, like, between, is_null, is_not_null) and logical combinators (and, or). Passthrough for inputs already in object format.

  • packages/objectql/src/protocol.ts — Call parseFilterAST() after JSON.parse in findData when the parsed filter is an array.

  • packages/runtime/src/http-dispatcher.ts — Fix param shape from { object, filters: query }{ object, query } so protocol receives query params at request.query as expected.

  • Broker shims (apps/studio, packages/client) — Updated to handle corrected param format.

Example

import { parseFilterAST } from '@objectstack/spec/data';

parseFilterAST(["and", ["priority", "=", "high"], ["status", "!=", "deleted"]])
// → { $and: [{ priority: "high" }, { status: { $ne: "deleted" } }] }

parseFilterAST(["status", "=", "active"])
// → { status: "active" }

parseFilterAST({ status: "active" })
// → { status: "active" }  (object passthrough)
Original prompt

This section details on the original issue you should resolve

<issue_title>filters 参数格式与查询引擎不兼容导致筛选无效</issue_title>
<issue_description>## 问题描述
列表筛选时,浏览器请求的 filters 参数为 JSON 数组 AST 格式(如:["and", ["priority", "=", "high"], ["status", "=", "active"]]),服务端接收到参数后,并未按照 spec 规范转换为正确的 FilterCondition(对象)格式,导致服务端未对数据进行筛选,返回了全部记录。

复现步骤

  1. 在 ObjectUI 列表页面进行多条件筛选(如按照 priority=high 且 status=active)。
  2. 浏览器发出的请求如:
GET /api/v1/data/contact?top=100&sort=name&filters=["and",["priority","=","high"],["status","=","active"]]
  1. 服务端收到后,最终在 findData 处理时,仅做了 JSON.parse,但未将 AST 数组格式转换为 FilterCondition 对象格式,导致 filter 永远无法生效。

期望行为

  • 服务端能够识别 filters 为 AST (数组)格式时,自动转换为符合 FilterCondition 对象结构,传给底层查询引擎。
  • 筛选条件能准确生效,返回预期的结果集。

建议修复点

  • 建议在 packages/objectql/src/protocol.tsfindData 方法里处理 filters,如果解析后为数组 AST,自动转换为 { $and: [...] } 结构传递。
  • 并在 Dispatcher 层需保证参数在整个链路中以规范形式传递(不要丢失/多包一层)。

参考


如需英文描述请告知。</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel

vercel Bot commented Feb 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Error Error Feb 24, 2026 7:20am
spec Error Error Feb 24, 2026 7:20am

Request Review

Copilot AI and others added 2 commits February 24, 2026 06:58
- Add parseFilterAST() to packages/spec/src/data/filter.zod.ts that
  converts array AST format (e.g. ["and", ["field", "=", "val"]]) to
  FilterCondition object format (e.g. { $and: [{ field: "val" }] })
- Fix protocol.ts findData to convert AST arrays after JSON.parse
- Fix http-dispatcher.ts to pass query params as { query } not { filters }
- Update broker shims to handle new param format
- Add 27 tests for parseFilterAST covering all operators and edge cases

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix filters parameter format incompatibility with query engine Fix filter AST array format incompatibility with query engine Feb 24, 2026
Copilot AI requested a review from hotlong February 24, 2026 07:03
@hotlong

hotlong commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

@copilot 仔细检查确认已经完成任务要求的所有开发内容

Copilot AI commented Feb 24, 2026

Copy link
Copy Markdown
Contributor Author

@copilot 仔细检查确认已经完成任务要求的所有开发内容

已全部完成并验证,以下为变更清单:

  1. packages/spec/src/data/filter.zod.ts — 新增 parseFilterAST() 工具函数,支持全部比较运算符(=, !=, >, >=, <, <=, in, not_in, contains, like, between, is_null, is_not_null)和逻辑组合(and, or),已导出可用。
  2. packages/objectql/src/protocol.tsfindData 在 JSON.parse 后,若 filter 为数组则自动调用 parseFilterAST() 转换为 FilterCondition 对象。
  3. packages/runtime/src/http-dispatcher.ts — 修复 Dispatcher 参数传递,从 { filters: query } 改为 { query }(两处),确保 findData 正确接收查询参数。
  4. Broker shimsapps/studiopackages/client 的 broker 适配新的参数格式。
  5. 29 条单元测试 — 覆盖所有运算符、边界情况(null/undefined/空数组)、嵌套逻辑、大小写不敏感、Issue 中的浏览器真实请求示例。

全部 6412 条测试通过,无回归。完整链路已验证:浏览器 AST 数组 → Dispatcher → findData JSON.parse + parseFilterAST() → FilterCondition 对象 → 查询引擎正确执行筛选。

@hotlong
hotlong marked this pull request as ready for review February 24, 2026 07:12
Copilot AI review requested due to automatic review settings February 24, 2026 07:12
@hotlong
hotlong merged commit 974573e into main Feb 24, 2026
2 of 5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes server-side filter handling so JSON array AST filters from clients are converted into FilterCondition objects that the query engine understands, and corrects dispatcher param shape so query params reach findData.

Changes:

  • Added parseFilterAST() to convert array-based filter AST into FilterCondition object format (with tests).
  • Updated protocol findData path to convert parsed array filters via parseFilterAST().
  • Fixed HTTP dispatcher and broker shims/tests to pass { query } instead of { filters: query }.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/spec/src/data/filter.zod.ts Adds AST→FilterCondition conversion utility used by server-side query handling.
packages/spec/src/data/filter.test.ts Adds unit tests validating AST conversion and schema compatibility.
packages/objectql/src/protocol.ts Applies AST conversion after JSON.parse when filter is an array.
packages/runtime/src/http-dispatcher.ts Fixes broker call param shape to forward query params correctly.
packages/client/src/client.msw.test.ts Updates MSW broker shim tests to read filters from params.query.
apps/studio/src/mocks/createKernel.ts Updates studio broker shim to accept new { query } shape (fallback to legacy).

*/
export function parseFilterAST(filter: unknown): FilterCondition | undefined {
if (filter == null) return undefined;
if (!Array.isArray(filter)) return filter as FilterCondition;

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseFilterAST currently “passes through” any non-array input by casting to FilterCondition. That includes primitives (e.g., a string/number/boolean) which would silently become an invalid FilterCondition at runtime. Consider only passing through when typeof filter === 'object' (and not null), otherwise return undefined (or throw) to avoid propagating invalid filters.

Suggested change
if (!Array.isArray(filter)) return filter as FilterCondition;
if (!Array.isArray(filter)) {
if (typeof filter === 'object') {
return filter as FilterCondition;
}
return undefined;
}

Copilot uses AI. Check for mistakes.
}

// Comparison node: [field, operator, value]
if (filter.length >= 2 && typeof first === 'string') {

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch will treat any array with at least 2 elements as a comparison node, which can produce { field: { $op: undefined } } when the value is missing (e.g. ["age", ">"]) and also doesn’t ensure the operator is a string. Tighten the shape check (e.g., require filter.length === 3 and typeof filter[1] === 'string'), while still allowing unary operators like is_null / is_not_null if you intend to support [field, op] forms.

Suggested change
if (filter.length >= 2 && typeof first === 'string') {
if (filter.length === 3 && typeof first === 'string' && typeof filter[1] === 'string') {

Copilot uses AI. Check for mistakes.
Comment on lines +364 to +365
'is_null': '$null',
'is_not_null': '$null',

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_null / is_not_null are mapped in AST_OPERATOR_MAP, but convertComparison handles both via special-cases and never uses these mappings. Removing these entries would reduce confusion about whether $null should receive a boolean vs. the raw AST value.

Suggested change
'is_null': '$null',
'is_not_null': '$null',

Copilot uses AI. Check for mistakes.
Comment on lines +433 to +434
const children = filter.slice(1).map((child: unknown) => parseFilterAST(child)).filter(Boolean) as FilterCondition[];
if (children.length === 0) return undefined;

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .filter(Boolean) for narrowing works here but is a bit opaque and relies on truthiness. A typed predicate (e.g., filtering only undefined results from parseFilterAST) is clearer and keeps TypeScript inference safer if the return type later changes.

Copilot uses AI. Check for mistakes.
if (!all) all = [];

const filters = params.filters;
const filters = params.query || params.filters;

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filters now holds either the entire query object or the legacy filters value, so the name is misleading (it’s no longer necessarily just filters). Renaming to something like queryParams (and then extracting filters/filter from it explicitly) would make the subsequent logic easier to follow.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filters 参数格式与查询引擎不兼容导致筛选无效

3 participants