Skip to content

Commit 17dca13

Browse files
Copilothotlong
andcommitted
feat: unify Engine/Protocol/Client to QueryAST standard parameters
BREAKING CHANGE: IDataEngine.find/findOne now accept EngineQueryOptions (where/fields/orderBy/limit/offset/expand) instead of DataEngineQueryOptions (filter/select/sort/limit/skip/populate). The deprecated schemas are kept for backward compatibility but the interfaces and engine implementation use the new QueryAST-aligned parameter names. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> Agent-Logs-Url: https://github.com/objectstack-ai/spec/sessions/aa746f20-b81c-4612-9d1f-0404233b14ec
1 parent 8e9d448 commit 17dca13

14 files changed

Lines changed: 516 additions & 286 deletions

File tree

apps/studio/src/lib/create-broker-shim.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function createBrokerShim(kernel: any): BrokerShim {
7575
}
7676

7777
try {
78-
await ql.update(params.object, params.data, { filter: params.id });
78+
await ql.update(params.object, params.data, { where: { id: params.id } });
7979
} catch (err: any) {
8080
console.warn(`[BrokerShim] update failed: ${err.message}`);
8181
throw err;
@@ -87,7 +87,7 @@ export function createBrokerShim(kernel: any): BrokerShim {
8787
}
8888
if (method === 'delete') {
8989
try {
90-
await ql.delete(params.object, { filter: params.id });
90+
await ql.delete(params.object, { where: { id: params.id } });
9191
return { object: params.object, id: params.id, deleted: true };
9292
} catch (err: any) {
9393
console.warn(`[BrokerShim] delete failed: ${err.message}`);

packages/client/src/client.hono.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ describe('ObjectStackClient (with Hono Server)', () => {
5252
if (protocol) {
5353
return await protocol.findData({ object: params.object, query: params.query || params.filters });
5454
}
55-
const records = await ql.find(params.object, { filter: params.filters });
55+
const records = await ql.find(params.object, { where: params.filters });
5656
return { object: params.object, records, total: records.length };
5757
}
5858
if (method === 'find') {
5959
if (protocol) {
6060
return await protocol.findData({ object: params.object, query: params.query || params.filters });
6161
}
62-
const records = await ql.find(params.object, { filter: params.filters });
62+
const records = await ql.find(params.object, { where: params.filters });
6363
return { object: params.object, records, total: records.length };
6464
}
6565
}

packages/client/src/client.msw.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ describe('ObjectStackClient (with MSW Plugin)', () => {
5757
return await protocol.findData({ object: params.object, query: params.query });
5858
}
5959
const queryOpts = params.query || {};
60-
const records = await ql.find(params.object, { filter: queryOpts.filters || queryOpts.filter });
60+
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
6161
return { object: params.object, records, total: records.length };
6262
}
6363
if (method === 'find') {
6464
if (protocol) {
6565
return await protocol.findData({ object: params.object, query: params.query });
6666
}
6767
const queryOpts = params.query || {};
68-
const records = await ql.find(params.object, { filter: queryOpts.filters || queryOpts.filter });
68+
const records = await ql.find(params.object, { where: queryOpts.filters || queryOpts.filter || queryOpts.where });
6969
return { object: params.object, records, total: records.length };
7070
}
7171
}

packages/client/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ export interface ClientConfig {
113113
*/
114114
export type DiscoveryResult = GetDiscoveryResponse;
115115

116+
/**
117+
* @deprecated Use `data.query()` with standard QueryAST parameters instead.
118+
* This interface uses legacy parameter names (filter/sort/top/skip) that
119+
* require translation to QueryAST. Prefer QueryAST fields directly:
120+
* - filter → where
121+
* - select → fields
122+
* - sort → orderBy
123+
* - skip → offset
124+
* - top → limit
125+
*/
116126
export interface QueryOptions {
117127
select?: string[]; // Simplified Selection
118128
/** @canonical Preferred filter parameter (singular). */
@@ -1423,6 +1433,10 @@ export class ObjectStackClient {
14231433
return this.unwrapResponse<PaginatedResult<T>>(res);
14241434
},
14251435

1436+
/**
1437+
* @deprecated Use `data.query()` with standard QueryAST parameters instead.
1438+
* This method uses legacy parameter names. Internally adapts to HTTP GET params.
1439+
*/
14261440
find: async <T = any>(object: string, options: QueryOptions = {}): Promise<PaginatedResult<T>> => {
14271441
const route = this.getRoute('data');
14281442
const queryParams = new URLSearchParams();

packages/core/src/contracts/data-engine.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import {
4-
DataEngineQueryOptions,
4+
EngineQueryOptions,
55
DataEngineInsertOptions,
6-
DataEngineUpdateOptions,
7-
DataEngineDeleteOptions,
8-
DataEngineAggregateOptions,
9-
DataEngineCountOptions,
6+
EngineUpdateOptions,
7+
EngineDeleteOptions,
8+
EngineAggregateOptions,
9+
EngineCountOptions,
1010
DataEngineRequest,
1111
} from '@objectstack/spec/data';
1212

@@ -17,22 +17,26 @@ import {
1717
* Following the Dependency Inversion Principle - plugins depend on this interface,
1818
* not on concrete database implementations.
1919
*
20+
* All query methods use standard QueryAST parameter names
21+
* (where/fields/orderBy/limit/offset/expand) to eliminate mechanical translation
22+
* between the Engine and Driver layers.
23+
*
2024
* Aligned with 'src/data/data-engine.zod.ts' in @objectstack/spec.
2125
*/
2226

2327
export interface IDataEngine {
24-
find(objectName: string, query?: DataEngineQueryOptions): Promise<any[]>;
25-
findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any>;
28+
find(objectName: string, query?: EngineQueryOptions): Promise<any[]>;
29+
findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
2630
insert(objectName: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any>;
27-
update(objectName: string, data: any, options?: DataEngineUpdateOptions): Promise<any>;
28-
delete(objectName: string, options?: DataEngineDeleteOptions): Promise<any>;
29-
count(objectName: string, query?: DataEngineCountOptions): Promise<number>;
30-
aggregate(objectName: string, query: DataEngineAggregateOptions): Promise<any[]>;
31+
update(objectName: string, data: any, options?: EngineUpdateOptions): Promise<any>;
32+
delete(objectName: string, options?: EngineDeleteOptions): Promise<any>;
33+
count(objectName: string, query?: EngineCountOptions): Promise<number>;
34+
aggregate(objectName: string, query: EngineAggregateOptions): Promise<any[]>;
3135

3236
/**
3337
* Vector Search (AI/RAG)
3438
*/
35-
vectorFind?(objectName: string, vector: number[], options?: { filter?: any, limit?: number, select?: string[], threshold?: number }): Promise<any[]>;
39+
vectorFind?(objectName: string, vector: number[], options?: { where?: any, limit?: number, fields?: string[], threshold?: number }): Promise<any[]>;
3640

3741
/**
3842
* Batch Operations (Transactional)

packages/objectql/src/engine.ts

Lines changed: 32 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import { QueryAST, HookContext, ServiceObject } from '@objectstack/spec/data';
44
import {
5-
DataEngineQueryOptions,
5+
EngineQueryOptions,
66
DataEngineInsertOptions,
7-
DataEngineUpdateOptions,
8-
DataEngineDeleteOptions,
9-
DataEngineAggregateOptions,
10-
DataEngineCountOptions
7+
EngineUpdateOptions,
8+
EngineDeleteOptions,
9+
EngineAggregateOptions,
10+
EngineCountOptions
1111
} from '@objectstack/spec/data';
1212
import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel';
1313
import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
@@ -741,59 +741,17 @@ export class ObjectQL implements IDataEngine {
741741
return records;
742742
}
743743

744-
// ============================================
745-
// Helper: Query Conversion
746-
// ============================================
747-
748-
private toQueryAST(object: string, options?: DataEngineQueryOptions): QueryAST {
749-
const ast: QueryAST = { object };
750-
if (!options) return ast;
751-
752-
if (options.filter) {
753-
ast.where = options.filter;
754-
}
755-
if (options.select) {
756-
ast.fields = options.select;
757-
}
758-
if (options.sort) {
759-
// Support DataEngineSortSchema variant
760-
if (Array.isArray(options.sort)) {
761-
// [{ field: 'a', order: 'asc' }]
762-
ast.orderBy = options.sort;
763-
} else {
764-
// Record<string, 'asc' | 'desc' | 1 | -1>
765-
ast.orderBy = Object.entries(options.sort).map(([field, order]) => ({
766-
field,
767-
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
768-
}));
769-
}
770-
}
771-
772-
if (options.top !== undefined) ast.limit = options.top;
773-
else if (options.limit !== undefined) ast.limit = options.limit;
774-
775-
if (options.skip !== undefined) ast.offset = options.skip;
776-
777-
// Map populate (relationship field names) to QueryAST expand entries
778-
if (options.populate && options.populate.length > 0) {
779-
ast.expand = {};
780-
for (const rel of options.populate) {
781-
ast.expand[rel] = { object: rel };
782-
}
783-
}
784-
785-
return ast;
786-
}
787-
788744
// ============================================
789745
// Data Access Methods (IDataEngine Interface)
790746
// ============================================
791747

792-
async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
748+
async find(object: string, query?: EngineQueryOptions): Promise<any[]> {
793749
object = this.resolveObjectName(object);
794750
this.logger.debug('Find operation starting', { object, query });
795751
const driver = this.getDriver(object);
796-
const ast = this.toQueryAST(object, query);
752+
const ast: QueryAST = { object, ...query };
753+
// Remove context from the AST — it's not a driver concern
754+
delete (ast as any).context;
797755

798756
const opCtx: OperationContext = {
799757
object,
@@ -836,12 +794,13 @@ export class ObjectQL implements IDataEngine {
836794
return opCtx.result as any[];
837795
}
838796

839-
async findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any> {
797+
async findOne(objectName: string, query?: EngineQueryOptions): Promise<any> {
840798
objectName = this.resolveObjectName(objectName);
841799
this.logger.debug('FindOne operation', { objectName });
842800
const driver = this.getDriver(objectName);
843-
const ast = this.toQueryAST(objectName, query);
844-
ast.limit = 1;
801+
const ast: QueryAST = { object: objectName, ...query, limit: 1 };
802+
// Remove context from the AST — it's not a driver concern
803+
delete (ast as any).context;
845804

846805
const opCtx: OperationContext = {
847806
object: objectName,
@@ -918,16 +877,16 @@ export class ObjectQL implements IDataEngine {
918877
return opCtx.result;
919878
}
920879

921-
async update(object: string, data: any, options?: DataEngineUpdateOptions): Promise<any> {
880+
async update(object: string, data: any, options?: EngineUpdateOptions): Promise<any> {
922881
object = this.resolveObjectName(object);
923882
this.logger.debug('Update operation starting', { object });
924883
const driver = this.getDriver(object);
925884

926-
// 1. Extract ID from data or filter if it's a single update by ID
885+
// 1. Extract ID from data or where if it's a single update by ID
927886
let id = data.id;
928-
if (!id && options?.filter) {
929-
if (typeof options.filter === 'string') id = options.filter;
930-
else if (options.filter.id) id = options.filter.id;
887+
if (!id && options?.where) {
888+
if (typeof options.where === 'string') id = options.where;
889+
else if (options.where.id) id = options.where.id;
931890
}
932891

933892
const opCtx: OperationContext = {
@@ -954,7 +913,7 @@ export class ObjectQL implements IDataEngine {
954913
if (hookContext.input.id) {
955914
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
956915
} else if (options?.multi && driver.updateMany) {
957-
const ast = this.toQueryAST(object, { filter: options.filter });
916+
const ast: QueryAST = { object, where: options.where };
958917
result = await driver.updateMany(object, ast, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
959918
} else {
960919
throw new Error('Update requires an ID or options.multi=true');
@@ -973,16 +932,16 @@ export class ObjectQL implements IDataEngine {
973932
return opCtx.result;
974933
}
975934

976-
async delete(object: string, options?: DataEngineDeleteOptions): Promise<any> {
935+
async delete(object: string, options?: EngineDeleteOptions): Promise<any> {
977936
object = this.resolveObjectName(object);
978937
this.logger.debug('Delete operation starting', { object });
979938
const driver = this.getDriver(object);
980939

981940
// Extract ID logic similar to update
982941
let id: any = undefined;
983-
if (options?.filter) {
984-
if (typeof options.filter === 'string') id = options.filter;
985-
else if (options.filter.id) id = options.filter.id;
942+
if (options?.where) {
943+
if (typeof options.where === 'string') id = options.where;
944+
else if (options.where.id) id = options.where.id;
986945
}
987946

988947
const opCtx: OperationContext = {
@@ -1008,7 +967,7 @@ export class ObjectQL implements IDataEngine {
1008967
if (hookContext.input.id) {
1009968
result = await driver.delete(object, hookContext.input.id as string, hookContext.input.options as any);
1010969
} else if (options?.multi && driver.deleteMany) {
1011-
const ast = this.toQueryAST(object, { filter: options.filter });
970+
const ast: QueryAST = { object, where: options.where };
1012971
result = await driver.deleteMany(object, ast, hookContext.input.options as any);
1013972
} else {
1014973
throw new Error('Delete requires an ID or options.multi=true');
@@ -1027,7 +986,7 @@ export class ObjectQL implements IDataEngine {
1027986
return opCtx.result;
1028987
}
1029988

1030-
async count(object: string, query?: DataEngineCountOptions): Promise<number> {
989+
async count(object: string, query?: EngineCountOptions): Promise<number> {
1031990
object = this.resolveObjectName(object);
1032991
const driver = this.getDriver(object);
1033992

@@ -1040,18 +999,18 @@ export class ObjectQL implements IDataEngine {
1040999

10411000
await this.executeWithMiddleware(opCtx, async () => {
10421001
if (driver.count) {
1043-
const ast = this.toQueryAST(object, { filter: query?.filter });
1002+
const ast: QueryAST = { object, where: query?.where };
10441003
return driver.count(object, ast);
10451004
}
10461005
// Fallback to find().length
1047-
const res = await this.find(object, { filter: query?.filter, select: ['id'] });
1006+
const res = await this.find(object, { where: query?.where, fields: ['id'] });
10481007
return res.length;
10491008
});
10501009

10511010
return opCtx.result as number;
10521011
}
10531012

1054-
async aggregate(object: string, query: DataEngineAggregateOptions): Promise<any[]> {
1013+
async aggregate(object: string, query: EngineAggregateOptions): Promise<any[]> {
10551014
object = this.resolveObjectName(object);
10561015
const driver = this.getDriver(object);
10571016
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
@@ -1066,13 +1025,9 @@ export class ObjectQL implements IDataEngine {
10661025
await this.executeWithMiddleware(opCtx, async () => {
10671026
const ast: QueryAST = {
10681027
object,
1069-
where: query.filter,
1028+
where: query.where,
10701029
groupBy: query.groupBy,
1071-
aggregations: query.aggregations?.map(agg => ({
1072-
function: agg.method,
1073-
field: agg.field,
1074-
alias: agg.alias || `${agg.method}_${agg.field || 'all'}`,
1075-
})),
1030+
aggregations: query.aggregations,
10761031
};
10771032

10781033
return driver.find(object, ast);
@@ -1355,7 +1310,7 @@ export class ObjectRepository {
13551310
/** Update a single record by ID */
13561311
async updateById(id: string | number, data: any): Promise<any> {
13571312
return this.engine.update(this.objectName, { ...data, id: id }, {
1358-
filter: { id: id },
1313+
where: { id: id },
13591314
context: this.context,
13601315
});
13611316
}
@@ -1370,7 +1325,7 @@ export class ObjectRepository {
13701325
/** Delete a single record by ID */
13711326
async deleteById(id: string | number): Promise<any> {
13721327
return this.engine.delete(this.objectName, {
1373-
filter: { id: id },
1328+
where: { id: id },
13741329
context: this.context,
13751330
});
13761331
}

packages/objectql/src/plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class ObjectQLPlugin implements Plugin {
175175
if (hookCtx.input?.id && !hookCtx.previous) {
176176
try {
177177
const existing = await this.ql!.findOne(hookCtx.object, {
178-
filter: { id: hookCtx.input.id }
178+
where: { id: hookCtx.input.id }
179179
});
180180
if (existing) {
181181
hookCtx.previous = existing;
@@ -191,7 +191,7 @@ export class ObjectQLPlugin implements Plugin {
191191
if (hookCtx.input?.id && !hookCtx.previous) {
192192
try {
193193
const existing = await this.ql!.findOne(hookCtx.object, {
194-
filter: { id: hookCtx.input.id }
194+
where: { id: hookCtx.input.id }
195195
});
196196
if (existing) {
197197
hookCtx.previous = existing;

0 commit comments

Comments
 (0)