Skip to content

Commit c463c23

Browse files
feat(client): add QueryOptionsV2 and update find() for canonical query 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>
1 parent 3791f83 commit c463c23

2 files changed

Lines changed: 74 additions & 16 deletions

File tree

packages/client/src/index.ts

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,37 @@ export interface QueryOptions {
145145
groupBy?: string[];
146146
}
147147

148+
/**
149+
* Canonical query options using Spec protocol field names.
150+
* This is the recommended interface for `data.find()` queries.
151+
*
152+
* Canonical field mapping (QueryAST-aligned):
153+
* - `where` — filter conditions (replaces legacy `filter`/`filters`)
154+
* - `fields` — field selection (replaces legacy `select`)
155+
* - `orderBy` — sort definition (replaces legacy `sort`)
156+
* - `limit` — max records (replaces legacy `top`)
157+
* - `offset` — skip records (replaces legacy `skip`)
158+
* - `expand` — relation loading (replaces legacy `populate`)
159+
*/
160+
export interface QueryOptionsV2 {
161+
/** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */
162+
where?: Record<string, any> | unknown[];
163+
/** Fields to retrieve (SELECT clause). */
164+
fields?: string[];
165+
/** Sort definition (ORDER BY clause). */
166+
orderBy?: string | string[] | SortNode[];
167+
/** Maximum number of records to return (LIMIT). */
168+
limit?: number;
169+
/** Number of records to skip (OFFSET). */
170+
offset?: number;
171+
/** Relations to expand (JOIN / eager-load). */
172+
expand?: Record<string, any> | string[];
173+
/** Aggregation functions. */
174+
aggregations?: AggregationNode[];
175+
/** Group by fields. */
176+
groupBy?: string[];
177+
}
178+
148179
export interface PaginatedResult<T = any> {
149180
/** Spec-compliant: array of matching records */
150181
records: T[];
@@ -1445,34 +1476,52 @@ export class ObjectStackClient {
14451476
* @deprecated Use `data.query()` with standard QueryAST parameters instead.
14461477
* This method uses legacy parameter names. Internally adapts to HTTP GET params.
14471478
*/
1448-
find: async <T = any>(object: string, options: QueryOptions = {}): Promise<PaginatedResult<T>> => {
1479+
find: async <T = any>(object: string, options: QueryOptions | QueryOptionsV2 = {}): Promise<PaginatedResult<T>> => {
14491480
const route = this.getRoute('data');
14501481
const queryParams = new URLSearchParams();
1451-
1482+
1483+
// ── Normalize V2 canonical options → HTTP transport params ───
1484+
// Detect V2 options by presence of canonical-only keys.
1485+
const v2 = options as QueryOptionsV2;
1486+
const normalizedOptions: QueryOptions = {} as QueryOptions;
1487+
if ('where' in options || 'fields' in options || 'orderBy' in options || 'offset' in options) {
1488+
// V2 canonical options detected — map to legacy HTTP transport keys
1489+
if (v2.where) normalizedOptions.filter = v2.where as any;
1490+
if (v2.fields) normalizedOptions.select = v2.fields;
1491+
if (v2.orderBy) normalizedOptions.sort = v2.orderBy as any;
1492+
if (v2.limit != null) normalizedOptions.top = v2.limit;
1493+
if (v2.offset != null) normalizedOptions.skip = v2.offset;
1494+
if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
1495+
if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
1496+
} else {
1497+
// Legacy QueryOptions — pass through as-is
1498+
Object.assign(normalizedOptions, options);
1499+
}
1500+
14521501
// 1. Handle Pagination
1453-
if (options.top) queryParams.set('top', options.top.toString());
1454-
if (options.skip) queryParams.set('skip', options.skip.toString());
1502+
if (normalizedOptions.top) queryParams.set('top', normalizedOptions.top.toString());
1503+
if (normalizedOptions.skip) queryParams.set('skip', normalizedOptions.skip.toString());
14551504

14561505
// 2. Handle Sort
1457-
if (options.sort) {
1506+
if (normalizedOptions.sort) {
14581507
// Check if it's AST
1459-
if (Array.isArray(options.sort) && typeof options.sort[0] === 'object') {
1460-
queryParams.set('sort', JSON.stringify(options.sort));
1508+
if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === 'object') {
1509+
queryParams.set('sort', JSON.stringify(normalizedOptions.sort));
14611510
} else {
1462-
const sortVal = Array.isArray(options.sort) ? options.sort.join(',') : options.sort;
1511+
const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(',') : normalizedOptions.sort;
14631512
queryParams.set('sort', sortVal as string);
14641513
}
14651514
}
14661515

14671516
// 3. Handle Select
1468-
if (options.select) {
1469-
queryParams.set('select', options.select.join(','));
1517+
if (normalizedOptions.select) {
1518+
queryParams.set('select', normalizedOptions.select.join(','));
14701519
}
14711520

14721521
// 4. Handle Filters (Simple vs AST)
14731522
// Canonical HTTP param name: `filter` (singular). `filters` (plural) is accepted
14741523
// for backward compatibility but `filter` is the standard going forward.
1475-
const filterValue = options.filter ?? options.filters;
1524+
const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
14761525
if (filterValue) {
14771526
// Detect AST filter format vs simple key-value map. AST filters use an array structure
14781527
// with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
@@ -1491,11 +1540,11 @@ export class ObjectStackClient {
14911540
}
14921541

14931542
// 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
1494-
if (options.aggregations) {
1495-
queryParams.set('aggregations', JSON.stringify(options.aggregations));
1543+
if (normalizedOptions.aggregations) {
1544+
queryParams.set('aggregations', JSON.stringify(normalizedOptions.aggregations));
14961545
}
1497-
if (options.groupBy) {
1498-
queryParams.set('groupBy', options.groupBy.join(','));
1546+
if (normalizedOptions.groupBy) {
1547+
queryParams.set('groupBy', normalizedOptions.groupBy.join(','));
14991548
}
15001549

15011550
const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);

packages/client/src/query-builder.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,22 @@ export class QueryBuilder<T = any> {
236236
}
237237

238238
/**
239-
* Skip records (for pagination)
239+
* Skip records (for pagination).
240+
* @deprecated Prefer `.offset()` for alignment with Spec canonical field names.
240241
*/
241242
skip(count: number): this {
242243
this.query.offset = count;
243244
return this;
244245
}
245246

247+
/**
248+
* Offset records (for pagination) — canonical alias for `.skip()`
249+
*/
250+
offset(count: number): this {
251+
this.query.offset = count;
252+
return this;
253+
}
254+
246255
/**
247256
* Paginate results
248257
*/

0 commit comments

Comments
 (0)