feat: unify API query syntax to Spec canonical format across all layers#990
feat: unify API query syntax to Spec canonical format across all layers#990
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…y syntax - Add QueryOptionsV2 interface with canonical field names (where, fields, orderBy, limit, offset, expand) - Update find() to accept both QueryOptions and QueryOptionsV2 with normalization logic - Add offset() method to QueryBuilder as canonical alias for skip() - Deprecate skip() in favor of offset() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
…studio, and docs - Add QueryOptionsV2 interface with canonical field names (where, fields, orderBy, limit, offset, expand) - Update find() to accept both QueryOptions and QueryOptionsV2 - Add offset() alias to QueryBuilder, deprecate skip() - Update React hooks (useQuery, usePagination, useInfiniteQuery) to support canonical names with legacy fallbacks - Update Studio QueryBuilder buildQueryJson to output canonical format - Add HTTP dispatcher normalization of transport params to canonical QueryAST - Update client-sdk.mdx documentation with canonical examples - Update client README.md query options documentation - Update http-protocol.mdx with canonical mapping table Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/d243fae5-51a7-4440-926f-46f040744cb0 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
…OADMAP.md Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/d243fae5-51a7-4440-926f-46f040744cb0 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
…T naming in dispatcher Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/d243fae5-51a7-4440-926f-46f040744cb0 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
|
@copilot 12:17:35.003 @objectstack/spec:build: ✓ data/TenantDatabaseLifecycle.json |
…l format
The DiscoverySchema.capabilities expects Record<string, { enabled: boolean }>
(hierarchical format), but ObjectStackProtocolImplementation.getDiscovery()
was returning WellKnownCapabilities (flat booleans), causing TS2416.
- Convert flat WellKnownCapabilities booleans to { enabled: boolean } objects
in protocol.ts getDiscovery()
- Update discovery tests to expect hierarchical format
- Update client capabilities getter to normalize both hierarchical and flat
formats back to WellKnownCapabilities for backward compatibility
Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/069cea55-358d-4b17-b1b8-505aaa03ccb6
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Fixed in 30f69db. The Changes:
All 151 objectql tests, 66 client tests, and 6,723 spec tests pass. |
There was a problem hiding this comment.
Pull request overview
This PR standardizes query option naming across the stack to the Spec canonical (QueryAST-aligned) keys (where, fields, orderBy, limit, offset, expand) while keeping legacy keys accepted via normalization and deprecation markers.
Changes:
- Added canonical query options (
QueryOptionsV2) in the Client SDK and mapped them to HTTP transport params used bydata.find(). - Updated React hooks and Studio QueryBuilder output to emit canonical key names (with legacy support where applicable).
- Documented the transport→protocol normalization behavior and updated examples/tests accordingly.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime/src/http-dispatcher.ts | Pre-normalizes HTTP GET transport params to canonical QueryAST keys before broker dispatch |
| packages/client/src/index.ts | Introduces QueryOptionsV2 and normalizes canonical options for data.find() |
| packages/client/src/query-builder.ts | Adds .offset() alias and deprecates .skip() |
| packages/client-react/src/data-hooks.tsx | Supports canonical + legacy option names; canonical takes precedence |
| apps/studio/src/components/QueryBuilder.tsx | Emits canonical query JSON keys (fields/where/orderBy/limit/offset) |
| packages/client/src/client.test.ts | Adds tests for canonical find() usage and .offset()/.skip() compatibility |
| packages/client/README.md | Updates docs to canonical option names and deprecation notes |
| content/docs/protocol/objectos/http-protocol.mdx | Updates query param table + documents normalization |
| content/docs/guides/client-sdk.mdx | Updates SDK examples and query options to canonical names |
| ROADMAP.md | Updates roadmap date and records completion of query syntax unification |
Comments suppressed due to low confidence (1)
apps/studio/src/components/QueryBuilder.tsx:94
wherein QueryAST is defined as a FilterCondition object (or record), but this builder outputs an array of{ field, operator, value }objects. That shape won’t validate against the Spec and likely won’t be interpreted correctly by the backend. Consider translating these UI conditions into a properwhereobject (e.g.{ fieldName: { $op: value } }, combined via$and/$or) or keeping the legacyfilterskey if the backend expects the custom array form.
if (state.filters.length > 0) {
query.where = state.filters.map((f) => ({
field: f.field,
operator: f.operator,
value: f.value,
}));
| if (normalized.select != null && normalized.fields == null) { | ||
| normalized.fields = normalized.select; |
There was a problem hiding this comment.
If both fields and legacy select are present, this block leaves select in the forwarded query (because deletion only happens when fields == null). To ensure the broker consistently receives canonical keys, delete select whenever it is present, while keeping fields as the source of truth.
| if (normalized.select != null && normalized.fields == null) { | |
| normalized.fields = normalized.select; | |
| if (normalized.select != null) { | |
| if (normalized.fields == null) { | |
| normalized.fields = normalized.select; | |
| } |
| * @deprecated Use `data.query()` with standard QueryAST parameters instead. | ||
| * This method uses legacy parameter names. Internally adapts to HTTP GET params. | ||
| */ |
There was a problem hiding this comment.
V2 detection only checks where/fields/orderBy/offset, so canonical-only calls like { limit: 10 } or { expand: [...] } are treated as legacy QueryOptions and those values are silently ignored. Update the discriminator to include all V2-only keys (e.g. limit, expand) or use a more robust rule (canonical present and no legacy keys present).
| | `page` | number | Page number (1-indexed) | — | `2` | | ||
| | `per_page` | number | Items per page (max from limits) | — | `50` | | ||
|
|
There was a problem hiding this comment.
page and per_page are still documented here, but runtime query normalization only supports offset-based pagination (top/skip → limit/offset) and there’s no handling for page/per_page. As-is, these would be treated as implicit field filters rather than pagination. Either document a mapping to limit/offset or remove these params from the table.
| expect(url).toContain('skip=5'); | ||
| expect(url).toContain('select=name%2Cemail'); | ||
| expect(url).toContain('sort=-created_at'); | ||
| // where → filter as JSON |
There was a problem hiding this comment.
The comment says "where → filter as JSON", but data.find() serializes object-form filters as flat query params (e.g. status=active) rather than filter={...}. Update the comment to match the actual behavior to avoid confusion when maintaining this test.
| // where → filter as JSON | |
| // where object is flattened into query params (e.g. status=active) |
| const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(',') : normalizedOptions.sort; | ||
| queryParams.set('sort', sortVal as string); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
QueryOptionsV2.where is documented as supporting MongoDB-style operator objects, but find() currently only serializes object-form filters as flat query params (via Object.entries(...).append(k, String(v))), which will produce "[object Object]" for operator objects like { priority: { $gte: 2 } }. Consider encoding non-scalar/object-valued filters as filter=<JSON> (or narrowing the docs/types to only promise simple equality maps).
| if (v2.fields) normalizedOptions.select = v2.fields; | ||
| if (v2.orderBy) normalizedOptions.sort = v2.orderBy as any; | ||
| if (v2.limit != null) normalizedOptions.top = v2.limit; |
There was a problem hiding this comment.
This pagination serialization uses truthy checks (if (normalizedOptions.top), if (normalizedOptions.skip)), so values like 0 are dropped even though the earlier normalization treats 0 as a meaningful value (!= null). Use != null checks when deciding whether to include top/skip in the query string.
| if (normalized.skip != null && normalized.offset == null) { | ||
| normalized.offset = normalized.skip; |
There was a problem hiding this comment.
If both offset and legacy skip are present, skip is forwarded unchanged due to the offset == null guard. Consider always deleting skip when present so only canonical keys are forwarded (with offset taking precedence).
| if (normalized.skip != null && normalized.offset == null) { | |
| normalized.offset = normalized.skip; | |
| if (normalized.skip != null) { | |
| if (normalized.offset == null) { | |
| normalized.offset = normalized.skip; | |
| } |
| * - `expand` — relation loading (replaces legacy `populate`) | ||
| */ | ||
| export interface QueryOptionsV2 { | ||
| /** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */ | ||
| where?: Record<string, any> | unknown[]; | ||
| /** Fields to retrieve (SELECT clause). */ | ||
| fields?: string[]; | ||
| /** Sort definition (ORDER BY clause). */ | ||
| orderBy?: string | string[] | SortNode[]; | ||
| /** Maximum number of records to return (LIMIT). */ | ||
| limit?: number; | ||
| /** Number of records to skip (OFFSET). */ | ||
| offset?: number; | ||
| /** Relations to expand (JOIN / eager-load). */ | ||
| expand?: Record<string, any> | string[]; | ||
| /** Aggregation functions. */ | ||
| aggregations?: AggregationNode[]; | ||
| /** Group by fields. */ | ||
| groupBy?: string[]; |
There was a problem hiding this comment.
QueryOptionsV2 includes expand, but data.find() never serializes any expand value into HTTP query params (and the V2→transport mapping does not include it). This makes the public API misleading. Either implement expand support for find() (likely as expand=a,b) or remove expand from QueryOptionsV2/docs for find() and point users to data.query() for expansions.
| * - `expand` — relation loading (replaces legacy `populate`) | |
| */ | |
| export interface QueryOptionsV2 { | |
| /** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */ | |
| where?: Record<string, any> | unknown[]; | |
| /** Fields to retrieve (SELECT clause). */ | |
| fields?: string[]; | |
| /** Sort definition (ORDER BY clause). */ | |
| orderBy?: string | string[] | SortNode[]; | |
| /** Maximum number of records to return (LIMIT). */ | |
| limit?: number; | |
| /** Number of records to skip (OFFSET). */ | |
| offset?: number; | |
| /** Relations to expand (JOIN / eager-load). */ | |
| expand?: Record<string, any> | string[]; | |
| /** Aggregation functions. */ | |
| aggregations?: AggregationNode[]; | |
| /** Group by fields. */ | |
| groupBy?: string[]; | |
| * | |
| * Note: `data.find()` does not support relation expansion. For `expand` / | |
| * eager-loading semantics, use the more advanced `data.query()` API. | |
| */ | |
| export interface QueryOptionsV2 { | |
| /** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */ | |
| where?: Record<string, any> | unknown[]; | |
| /** Fields to retrieve (SELECT clause). */ | |
| fields?: string[]; | |
| /** Sort definition (ORDER BY clause). */ | |
| orderBy?: string | string[] | SortNode[]; | |
| /** Maximum number of records to return (LIMIT). */ | |
| limit?: number; | |
| /** Number of records to skip (OFFSET). */ | |
| offset?: number; | |
| /** Aggregation functions. */ | |
| aggregations?: AggregationNode[]; | |
| /** Group by fields. */ | |
| groupBy?: string[]; |
| - `orderBy`: Sort definition — `'name'`, `['-created_at', 'name']`, or `SortNode[]`. | ||
| - `limit`: Maximum number of records to return (LIMIT). | ||
| - `offset`: Number of records to skip (OFFSET). | ||
| - `expand`: Relations to expand (JOIN / eager-load). |
There was a problem hiding this comment.
This README section lists expand as a supported find() option, but the current data.find() implementation does not serialize expand at all. Update the README to match actual behavior, or add expand handling in find() so this documentation stays accurate.
| - `expand`: Relations to expand (JOIN / eager-load). | |
| - `expand`: Planned support for relation expansion (JOIN / eager-load). Currently ignored by the client. |
| fields: ['subject', 'priority'], | ||
| where: { priority: { $gte: 2 } }, | ||
| orderBy: ['-priority'], | ||
| limit: 10 |
There was a problem hiding this comment.
This example uses client.data.find() with where: { priority: { $gte: 2 } }, but the current find() implementation serializes object-valued filters as String(value) in flat query params, which results in priority=[object Object]. Either switch the example to client.data.query() for operator-based filters, or update find() to JSON-encode operator objects (e.g. via the filter param).
Normalizes query parameter naming to Spec canonical format (
where/fields/orderBy/limit/offset/expand) across Client SDK, React hooks, Studio, HTTP dispatcher, and documentation. Legacy names (filter/select/sort/top/skip) remain accepted with@deprecatedmarkers.Client SDK (
packages/client)QueryOptionsV2interface with canonical field namesfind()accepts bothQueryOptions | QueryOptionsV2; auto-detects V2 by presence of canonical-only keysQueryBuilder.offset()added as canonical alias for deprecated.skip()React Hooks (
packages/client-react)UseQueryOptionssupports both canonical and legacy names; canonical takes precedenceusePagination/useInfiniteQueryinternals switched to canonical callsUsePaginationOptionsandUseInfiniteQueryOptionsOmit types updated to include canonical keysStudio QueryBuilder (
apps/studio)buildQueryJsonoutput changed from{select, filters, sort, top, skip}to{fields, where, orderBy, limit, offset}HTTP Dispatcher (
packages/runtime)filter→where,select→fields,sort→orderBy,top→limit,skip→offset) before broker dispatchDocumentation
client-sdk.mdx: all examples and query options table updated to canonical syntax with deprecation callouthttp-protocol.mdx: added canonical equivalent column and normalization explanationpackages/client/README.md: query options rewritten for canonical namesTests
.offset(), and.skip()compat💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.