From d54c0eca71d29787979bc532235e14e70b42d220 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:01:58 +0000 Subject: [PATCH 01/21] Initial plan From 380b3156f0954641e011c6ffde92d767e95d335b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:06:18 +0000 Subject: [PATCH 02/21] Update @objectstack/spec to 0.3.3 in all package.json files Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- examples/showcase/enterprise-erp/package.json | 2 +- packages/drivers/excel/package.json | 2 +- packages/drivers/fs/package.json | 2 +- packages/drivers/localstorage/package.json | 2 +- packages/drivers/memory/package.json | 2 +- packages/drivers/mongo/package.json | 2 +- packages/drivers/redis/package.json | 2 +- packages/drivers/sdk/package.json | 2 +- packages/drivers/sql/package.json | 2 +- packages/foundation/core/package.json | 2 +- .../foundation/platform-node/package.json | 2 +- packages/foundation/types/package.json | 4 +- pnpm-lock.yaml | 54 +++++++++---------- 13 files changed, 40 insertions(+), 40 deletions(-) diff --git a/examples/showcase/enterprise-erp/package.json b/examples/showcase/enterprise-erp/package.json index 2c478f22..1f0ad45c 100644 --- a/examples/showcase/enterprise-erp/package.json +++ b/examples/showcase/enterprise-erp/package.json @@ -43,7 +43,7 @@ "@objectql/cli": "workspace:*", "@objectql/driver-sql": "workspace:*", "@objectql/platform-node": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "@types/jest": "^30.0.0", "@types/node": "^20.0.0", "jest": "^30.2.0", diff --git a/packages/drivers/excel/package.json b/packages/drivers/excel/package.json index f0164ec5..3563012f 100644 --- a/packages/drivers/excel/package.json +++ b/packages/drivers/excel/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "exceljs": "^4.4.0" }, "devDependencies": { diff --git a/packages/drivers/fs/package.json b/packages/drivers/fs/package.json index 73317899..73422e45 100644 --- a/packages/drivers/fs/package.json +++ b/packages/drivers/fs/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1" + "@objectstack/spec": "^0.3.3" }, "devDependencies": { "@types/jest": "^29.0.0", diff --git a/packages/drivers/localstorage/package.json b/packages/drivers/localstorage/package.json index 1edf4ea6..367ae68b 100644 --- a/packages/drivers/localstorage/package.json +++ b/packages/drivers/localstorage/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1" + "@objectstack/spec": "^0.3.3" }, "devDependencies": { "@types/jest": "^29.0.0", diff --git a/packages/drivers/memory/package.json b/packages/drivers/memory/package.json index b8e301cc..6f8f9da8 100644 --- a/packages/drivers/memory/package.json +++ b/packages/drivers/memory/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "mingo": "^7.1.1" }, "devDependencies": { diff --git a/packages/drivers/mongo/package.json b/packages/drivers/mongo/package.json index f9d6fd8e..a308e136 100644 --- a/packages/drivers/mongo/package.json +++ b/packages/drivers/mongo/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "mongodb": "^5.9.2" }, "devDependencies": { diff --git a/packages/drivers/redis/package.json b/packages/drivers/redis/package.json index 8d834c7b..8188b1ce 100644 --- a/packages/drivers/redis/package.json +++ b/packages/drivers/redis/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "redis": "^4.6.0" }, "devDependencies": { diff --git a/packages/drivers/sdk/package.json b/packages/drivers/sdk/package.json index 5546631c..d6a29626 100644 --- a/packages/drivers/sdk/package.json +++ b/packages/drivers/sdk/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1" + "@objectstack/spec": "^0.3.3" }, "devDependencies": { "typescript": "^5.3.0" diff --git a/packages/drivers/sql/package.json b/packages/drivers/sql/package.json index 7a608ff9..17f024d4 100644 --- a/packages/drivers/sql/package.json +++ b/packages/drivers/sql/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "knex": "^3.1.0", "nanoid": "^3.3.11" }, diff --git a/packages/foundation/core/package.json b/packages/foundation/core/package.json index 0c6ac8da..932a230a 100644 --- a/packages/foundation/core/package.json +++ b/packages/foundation/core/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@objectql/types": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "@objectql/runtime": "workspace:*", "js-yaml": "^4.1.0", "openai": "^4.28.0" diff --git a/packages/foundation/platform-node/package.json b/packages/foundation/platform-node/package.json index 65b2bd1d..1589c3ad 100644 --- a/packages/foundation/platform-node/package.json +++ b/packages/foundation/platform-node/package.json @@ -22,7 +22,7 @@ "dependencies": { "@objectql/types": "workspace:*", "@objectql/core": "workspace:*", - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "fast-glob": "^3.3.2", "js-yaml": "^4.1.1" }, diff --git a/packages/foundation/types/package.json b/packages/foundation/types/package.json index 4e79e427..6c59281f 100644 --- a/packages/foundation/types/package.json +++ b/packages/foundation/types/package.json @@ -27,11 +27,11 @@ "test": "jest --passWithNoTests" }, "peerDependencies": { - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "@objectql/runtime": "^0.2.0" }, "devDependencies": { - "@objectstack/spec": "^0.3.1", + "@objectstack/spec": "^0.3.3", "@objectql/runtime": "workspace:*", "ts-json-schema-generator": "^2.4.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 187f6a7f..2b181c26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,8 +268,8 @@ importers: specifier: workspace:* version: link:../../../packages/foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -325,8 +325,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 exceljs: specifier: ^4.4.0 version: 4.4.0 @@ -350,8 +350,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 devDependencies: '@types/jest': specifier: ^29.0.0 @@ -372,8 +372,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 devDependencies: '@types/jest': specifier: ^29.0.0 @@ -394,8 +394,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 mingo: specifier: ^7.1.1 version: 7.1.1 @@ -416,8 +416,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 mongodb: specifier: ^5.9.2 version: 5.9.2 @@ -432,8 +432,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 redis: specifier: ^4.6.0 version: 4.7.1 @@ -454,8 +454,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 devDependencies: typescript: specifier: ^5.3.0 @@ -467,8 +467,8 @@ importers: specifier: workspace:* version: link:../../foundation/types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 knex: specifier: ^3.1.0 version: 3.1.0(sqlite3@5.1.7) @@ -489,8 +489,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 js-yaml: specifier: ^4.1.0 version: 4.1.1 @@ -514,8 +514,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 fast-glob: specifier: ^3.3.2 version: 3.3.3 @@ -533,8 +533,8 @@ importers: specifier: workspace:* version: link:../../objectstack/runtime '@objectstack/spec': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.3 + version: 0.3.3 ts-json-schema-generator: specifier: ^2.4.0 version: 2.4.0 @@ -1926,8 +1926,8 @@ packages: engines: {node: '>=10'} deprecated: This functionality has been moved to @npmcli/fs - '@objectstack/spec@0.3.1': - resolution: {integrity: sha512-MK/8x2jpdS6PllxadVALK7cKqa6nfwtjzZ1WRKpZwdbJnAUTKZlt6P3h/LSif8gxq78KiYLdWmHjtjoTXPruJQ==} + '@objectstack/spec@0.3.3': + resolution: {integrity: sha512-GGSrRLECgYY1epLocdCkCsicerFKAKlRpluGmCqK1iVNgTgiC04zniLogYQXu5kf6UIB6mHQNBHcbr1x5DOmVQ==} engines: {node: '>=18.0.0'} '@orama/orama@3.1.18': @@ -9470,7 +9470,7 @@ snapshots: rimraf: 3.0.2 optional: true - '@objectstack/spec@0.3.1': + '@objectstack/spec@0.3.3': dependencies: zod: 3.25.76 From bf4ab3fdf9dc0ee15431fb8a700f93e74ce70661 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:12:10 +0000 Subject: [PATCH 03/21] Fix breaking changes from @objectstack/spec upgrade to 0.3.3 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/excel/src/index.ts | 5 +- packages/drivers/fs/src/index.ts | 5 +- packages/drivers/localstorage/src/index.ts | 5 +- packages/drivers/memory/src/index.ts | 5 +- packages/drivers/mongo/src/index.ts | 60 ++------- packages/drivers/redis/src/index.ts | 5 +- packages/drivers/sdk/src/index.ts | 5 +- packages/drivers/sql/src/index.ts | 69 ++-------- packages/foundation/core/src/index.ts | 6 +- .../core/src/query/filter-translator.ts | 123 ++---------------- .../core/src/query/query-analyzer.ts | 9 +- .../core/src/query/query-builder.ts | 8 +- packages/foundation/core/src/repository.ts | 1 - packages/foundation/core/test/app.test.ts | 3 +- .../foundation/platform-node/src/plugin.ts | 4 +- 15 files changed, 57 insertions(+), 256 deletions(-) diff --git a/packages/drivers/excel/src/index.ts b/packages/drivers/excel/src/index.ts index 78d052b2..a95510ca 100644 --- a/packages/drivers/excel/src/index.ts +++ b/packages/drivers/excel/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/fs/src/index.ts b/packages/drivers/fs/src/index.ts index 1692b64a..8f8db3ba 100644 --- a/packages/drivers/fs/src/index.ts +++ b/packages/drivers/fs/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/localstorage/src/index.ts b/packages/drivers/localstorage/src/index.ts index 72deb682..536cff13 100644 --- a/packages/drivers/localstorage/src/index.ts +++ b/packages/drivers/localstorage/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/memory/src/index.ts b/packages/drivers/memory/src/index.ts index 726f61b8..374cb3f9 100644 --- a/packages/drivers/memory/src/index.ts +++ b/packages/drivers/memory/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/mongo/src/index.ts b/packages/drivers/mongo/src/index.ts index a3365493..ef2f90c7 100644 --- a/packages/drivers/mongo/src/index.ts +++ b/packages/drivers/mongo/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. @@ -457,10 +456,10 @@ export class MongoDriver implements Driver { // Convert QueryAST to legacy query format const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - skip: ast.skip, + filters: this.convertFilterNodeToLegacy(ast.where), + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + skip: ast.offset, }; // Use existing find method @@ -575,52 +574,15 @@ export class MongoDriver implements Driver { } /** - * Convert FilterNode (QueryAST format) to legacy filter array format + * Convert FilterCondition (QueryAST format) to legacy filter array format * This allows reuse of existing filter logic while supporting new QueryAST * * @private */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - case 'or': - // Convert AND/OR node to array with separator - if (!node.children || node.children.length === 0) return undefined; - const results: any[] = []; - const separator = node.type; // 'and' or 'or' - - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (results.length > 0) { - results.push(separator); - } - results.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - return results.length > 0 ? results : undefined; - - case 'not': - // NOT is not directly supported in the legacy filter format - // MongoDB supports $not, but legacy array format doesn't have a NOT operator - // Use native MongoDB queries with $not instead: - // Example: { field: { $not: { $eq: value } } } - throw new Error( - 'NOT filters are not supported in legacy filter format. ' + - 'Use native MongoDB queries with $not operator instead. ' + - 'Example: { field: { $not: { $eq: value } } }' - ); - - default: - return undefined; - } + private convertFilterNodeToLegacy(condition?: any): any { + // FilterCondition is already in the modern format, just pass it through + // The legacy format methods can handle it directly + return condition; } /** diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index a6bf9745..67617654 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/sdk/src/index.ts b/packages/drivers/sdk/src/index.ts index 652c13f6..d5a5ce54 100644 --- a/packages/drivers/sdk/src/index.ts +++ b/packages/drivers/sdk/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. diff --git a/packages/drivers/sql/src/index.ts b/packages/drivers/sql/src/index.ts index c03b2a5e..3c158b32 100644 --- a/packages/drivers/sql/src/index.ts +++ b/packages/drivers/sql/src/index.ts @@ -1,8 +1,7 @@ -import { Data, System } from '@objectstack/spec'; +import { Data, Driver as DriverSpec } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; -type DriverInterface = System.DriverInterface; +type DriverInterface = DriverSpec.DriverInterface; /** * ObjectQL * Copyright (c) 2026-present ObjectStack Inc. @@ -977,10 +976,10 @@ export class SqlDriver implements Driver { // Convert QueryAST to legacy query format for internal processing const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map(s => [s.field, s.order]), - limit: ast.top, - offset: ast.skip, + filters: this.convertFilterNodeToLegacy(ast.where), + sort: ast.orderBy?.map(s => [s.field, s.order]), + limit: ast.limit, + offset: ast.offset, }; // Use existing find method for execution @@ -1094,61 +1093,15 @@ export class SqlDriver implements Driver { } /** - * Convert FilterNode (QueryAST format) to legacy filter array format + * Convert FilterCondition (QueryAST format) to legacy filter array format * This allows reuse of existing filter logic while supporting new QueryAST * * @private */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); - } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); - } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is more complex - we'll need to negate the inner condition - // For now, we'll just process the children - if (node.children && node.children.length > 0) { - return this.convertFilterNodeToLegacy(node.children[0]); - } - return undefined; - - default: - return undefined; - } + private convertFilterNodeToLegacy(condition?: any): any { + // FilterCondition is already in the modern format, just pass it through + // The legacy format methods can handle it directly + return condition; } /** diff --git a/packages/foundation/core/src/index.ts b/packages/foundation/core/src/index.ts index f70857ee..0e33aab6 100644 --- a/packages/foundation/core/src/index.ts +++ b/packages/foundation/core/src/index.ts @@ -13,10 +13,10 @@ export type { ObjectStackKernel, ObjectStackRuntimeProtocol } from '@objectql/ru // export type { ObjectQL as ObjectQLEngine, SchemaRegistry } from '@objectstack/objectql'; // Export ObjectStack spec types for driver development -import { Data, System } from '@objectstack/spec'; +import { Data, Driver } from '@objectstack/spec'; export type QueryAST = Data.QueryAST; -export type DriverInterface = System.DriverInterface; -export type DriverOptions = System.DriverOptions; +export type DriverInterface = Driver.DriverInterface; +export type DriverOptions = Driver.DriverOptions; // Export our enhanced runtime components (actual implementations) export * from './repository'; diff --git a/packages/foundation/core/src/query/filter-translator.ts b/packages/foundation/core/src/query/filter-translator.ts index d95171bc..cd5d7144 100644 --- a/packages/foundation/core/src/query/filter-translator.ts +++ b/packages/foundation/core/src/query/filter-translator.ts @@ -8,141 +8,34 @@ import type { Filter } from '@objectql/types'; import { Data } from '@objectstack/spec'; -type FilterNode = Data.FilterNode; +type FilterCondition = Data.FilterCondition; import { ObjectQLError } from '@objectql/types'; /** * Filter Translator * - * Translates ObjectQL Filter (FilterCondition) to ObjectStack FilterNode format. - * Converts modern object-based syntax to legacy array-based syntax for backward compatibility. + * Translates ObjectQL Filter to ObjectStack FilterCondition format. + * Since both now use the same format, this is mostly a pass-through. * * @example * Input: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] } - * Output: [["age", ">=", 18], "or", [["status", "=", "active"], "or", ["role", "=", "admin"]]] + * Output: { age: { $gte: 18 }, $or: [{ status: "active" }, { role: "admin" }] } */ export class FilterTranslator { /** - * Translate filters from ObjectQL format to ObjectStack FilterNode format + * Translate filters from ObjectQL format to ObjectStack FilterCondition format */ - translate(filters?: Filter): FilterNode | undefined { + translate(filters?: Filter): FilterCondition | undefined { if (!filters) { return undefined; } - // Backward compatibility: if it's already an array (old format), pass through - if (Array.isArray(filters)) { - return filters as unknown as FilterNode; - } - // If it's an empty object, return undefined if (typeof filters === 'object' && Object.keys(filters).length === 0) { return undefined; } - return this.convertToNode(filters); - } - - /** - * Recursively converts FilterCondition to FilterNode array format - */ - private convertToNode(filter: Filter): FilterNode { - const nodes: any[] = []; - - // Process logical operators first - if (filter.$and) { - const andNodes = filter.$and.map((f: Filter) => this.convertToNode(f)); - nodes.push(...this.interleaveWithOperator(andNodes, 'and')); - } - - if (filter.$or) { - const orNodes = filter.$or.map((f: Filter) => this.convertToNode(f)); - if (nodes.length > 0) { - nodes.push('and'); - } - nodes.push(...this.interleaveWithOperator(orNodes, 'or')); - } - - // Note: $not operator is not currently supported in the legacy FilterNode format - if (filter.$not) { - throw new ObjectQLError({ - code: 'UNSUPPORTED_OPERATOR', - message: '$not operator is not supported. Use $ne for field negation instead.' - }); - } - - // Process field conditions - for (const [field, value] of Object.entries(filter)) { - if (field.startsWith('$')) { - continue; // Skip logical operators (already processed) - } - - if (nodes.length > 0) { - nodes.push('and'); - } - - // Handle field value - if (value === null || value === undefined) { - nodes.push([field, '=', value]); - } else if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) { - // Explicit operators - multiple operators on same field are AND-ed together - const entries = Object.entries(value); - for (let i = 0; i < entries.length; i++) { - const [op, opValue] = entries[i]; - - // Add 'and' before each operator (except the very first node) - if (nodes.length > 0 || i > 0) { - nodes.push('and'); - } - - const legacyOp = this.mapOperatorToLegacy(op); - nodes.push([field, legacyOp, opValue]); - } - } else { - // Implicit equality - nodes.push([field, '=', value]); - } - } - - // Return as FilterNode (type assertion for backward compatibility) - return (nodes.length === 1 ? nodes[0] : nodes) as unknown as FilterNode; - } - - /** - * Interleaves filter nodes with a logical operator - */ - private interleaveWithOperator(nodes: FilterNode[], operator: string): any[] { - if (nodes.length === 0) return []; - if (nodes.length === 1) return [nodes[0]]; - - const result: any[] = [nodes[0]]; - for (let i = 1; i < nodes.length; i++) { - result.push(operator, nodes[i]); - } - return result; - } - - /** - * Maps modern $-prefixed operators to legacy format - */ - private mapOperatorToLegacy(operator: string): string { - const mapping: Record = { - '$eq': '=', - '$ne': '!=', - '$gt': '>', - '$gte': '>=', - '$lt': '<', - '$lte': '<=', - '$in': 'in', - '$nin': 'nin', - '$contains': 'contains', - '$startsWith': 'startswith', - '$endsWith': 'endswith', - '$null': 'is_null', - '$exist': 'is_not_null', - '$between': 'between', - }; - - return mapping[operator] || operator.replace('$', ''); + // Both ObjectQL Filter and ObjectStack FilterCondition use the same format now + return filters as unknown as FilterCondition; } } diff --git a/packages/foundation/core/src/query/query-analyzer.ts b/packages/foundation/core/src/query/query-analyzer.ts index 5262db55..304abd12 100644 --- a/packages/foundation/core/src/query/query-analyzer.ts +++ b/packages/foundation/core/src/query/query-analyzer.ts @@ -9,7 +9,6 @@ import type { UnifiedQuery, ObjectConfig, MetadataRegistry } from '@objectql/types'; import { Data } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; import { QueryService, QueryOptions } from './query-service'; /** @@ -167,10 +166,10 @@ export class QueryAnalyzer { // Build the QueryAST (without executing) const ast: QueryAST = { object: objectName, - filters: query.filters as any, // FilterCondition is compatible with FilterNode - sort: query.sort as any, // Will be converted to SortNode[] format - top: query.limit, // Changed from limit to top (QueryAST uses 'top') - skip: query.skip, + where: query.filters as any, // FilterCondition format + orderBy: query.sort as any, // Will be converted to SortNode[] format + limit: query.limit, + offset: query.skip, fields: query.fields }; diff --git a/packages/foundation/core/src/query/query-builder.ts b/packages/foundation/core/src/query/query-builder.ts index 340445d6..1dca38dc 100644 --- a/packages/foundation/core/src/query/query-builder.ts +++ b/packages/foundation/core/src/query/query-builder.ts @@ -43,12 +43,12 @@ export class QueryBuilder { // Map filters using FilterTranslator if (query.filters) { - ast.filters = this.filterTranslator.translate(query.filters); + ast.where = this.filterTranslator.translate(query.filters); } // Map sort if (query.sort) { - ast.sort = query.sort.map(([field, order]) => ({ + ast.orderBy = query.sort.map(([field, order]) => ({ field, order: order as 'asc' | 'desc' })); @@ -56,10 +56,10 @@ export class QueryBuilder { // Map pagination if (query.limit !== undefined) { - ast.top = query.limit; + ast.limit = query.limit; } if (query.skip !== undefined) { - ast.skip = query.skip; + ast.offset = query.skip; } // Map groupBy diff --git a/packages/foundation/core/src/repository.ts b/packages/foundation/core/src/repository.ts index 80d2c54b..2ca80ec7 100644 --- a/packages/foundation/core/src/repository.ts +++ b/packages/foundation/core/src/repository.ts @@ -10,7 +10,6 @@ import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionC import type { ObjectStackKernel } from '@objectql/runtime'; import { Data } from '@objectstack/spec'; type QueryAST = Data.QueryAST; -type FilterNode = Data.FilterNode; type SortNode = Data.SortNode; import { Validator } from './validator'; import { FormulaEngine } from './formula-engine'; diff --git a/packages/foundation/core/test/app.test.ts b/packages/foundation/core/test/app.test.ts index 94e272c6..9bdeff80 100644 --- a/packages/foundation/core/test/app.test.ts +++ b/packages/foundation/core/test/app.test.ts @@ -9,7 +9,8 @@ import { ObjectQL } from '../src/app'; import { MockDriver } from './mock-driver'; import { ObjectConfig, HookContext, ActionContext, Metadata } from '@objectql/types'; -import type { PluginDefinition } from '@objectstack/spec'; +import type { Kernel } from '@objectstack/spec'; +type PluginDefinition = Kernel.PluginDefinition; const todoObject: ObjectConfig = { name: 'todo', diff --git a/packages/foundation/platform-node/src/plugin.ts b/packages/foundation/platform-node/src/plugin.ts index cab8f44c..e60a2858 100644 --- a/packages/foundation/platform-node/src/plugin.ts +++ b/packages/foundation/platform-node/src/plugin.ts @@ -6,8 +6,8 @@ * LICENSE file in the root directory of this source tree. */ -import { System } from '@objectstack/spec'; -type PluginDefinition = System.PluginDefinition; +import { Kernel } from '@objectstack/spec'; +type PluginDefinition = Kernel.PluginDefinition; export function loadPlugin(packageName: string): PluginDefinition { let mod: any; From 25421931f7bd5d0635f99c40883c7818b8381f1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:15:10 +0000 Subject: [PATCH 04/21] Fix MongoDB driver and tests for FilterCondition format Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/mongo/src/index.ts | 11 +++++++- packages/drivers/mongo/test/index.test.ts | 32 +++++++---------------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/packages/drivers/mongo/src/index.ts b/packages/drivers/mongo/src/index.ts index ef2f90c7..81d7c19b 100644 --- a/packages/drivers/mongo/src/index.ts +++ b/packages/drivers/mongo/src/index.ts @@ -149,7 +149,16 @@ export class MongoDriver implements Driver { } private mapFilters(filters: any): Filter { - if (!filters || filters.length === 0) return {}; + if (!filters) return {}; + + // If filters is an object (FilterCondition format), return it directly + // MongoDB can handle FilterCondition format natively + if (typeof filters === 'object' && !Array.isArray(filters)) { + return filters as Filter; + } + + // If filters is an array (legacy format), convert it + if (Array.isArray(filters) && filters.length === 0) return {}; const result = this.buildFilterConditions(filters); return result; diff --git a/packages/drivers/mongo/test/index.test.ts b/packages/drivers/mongo/test/index.test.ts index 7ae8e5d9..bc9243c1 100644 --- a/packages/drivers/mongo/test/index.test.ts +++ b/packages/drivers/mongo/test/index.test.ts @@ -341,14 +341,11 @@ describe('MongoDriver', () => { const ast = { object: 'users', fields: ['name', 'email'], - filters: { - type: 'comparison' as const, - field: 'status', - operator: '=', - value: 'active' + where: { + status: 'active' }, - top: 10, - skip: 0 + limit: 10, + offset: 0 }; mockCollection.toArray.mockResolvedValue([ @@ -366,21 +363,10 @@ describe('MongoDriver', () => { it('should handle complex QueryAST with AND filters', async () => { const ast = { object: 'users', - filters: { - type: 'and' as const, - children: [ - { - type: 'comparison' as const, - field: 'status', - operator: '=', - value: 'active' - }, - { - type: 'comparison' as const, - field: 'age', - operator: '>', - value: 18 - } + where: { + $and: [ + { status: 'active' }, + { age: { $gt: 18 } } ] } }; @@ -396,7 +382,7 @@ describe('MongoDriver', () => { it('should handle QueryAST with sort', async () => { const ast = { object: 'users', - sort: [ + orderBy: [ { field: 'name', order: 'asc' as const } ] }; From 847ccb659425486cbfbd45d711c8f28285274a6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:17:26 +0000 Subject: [PATCH 05/21] Fix SQL driver to use skip instead of offset in legacy query format Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/sql/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/drivers/sql/src/index.ts b/packages/drivers/sql/src/index.ts index 3c158b32..004b5e3b 100644 --- a/packages/drivers/sql/src/index.ts +++ b/packages/drivers/sql/src/index.ts @@ -979,7 +979,7 @@ export class SqlDriver implements Driver { filters: this.convertFilterNodeToLegacy(ast.where), sort: ast.orderBy?.map(s => [s.field, s.order]), limit: ast.limit, - offset: ast.offset, + skip: ast.offset, }; // Use existing find method for execution From 00401ce5a849f01f0a35b7aae4b92f390412ec2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:28:58 +0000 Subject: [PATCH 06/21] Initial plan From dafc7cba0937e0877c40a8efb26282c502f69bc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:36:35 +0000 Subject: [PATCH 07/21] Fix QueryAST property name changes in memory, fs, and excel drivers Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/excel/src/index.ts | 115 ++++++++++++++++----------- packages/drivers/fs/src/index.ts | 113 +++++++++++++++----------- packages/drivers/memory/src/index.ts | 92 ++++++--------------- 3 files changed, 158 insertions(+), 162 deletions(-) diff --git a/packages/drivers/excel/src/index.ts b/packages/drivers/excel/src/index.ts index a95510ca..34087c1d 100644 --- a/packages/drivers/excel/src/index.ts +++ b/packages/drivers/excel/src/index.ts @@ -898,12 +898,13 @@ export class ExcelDriver implements Driver { const objectName = ast.object || ''; // Convert QueryAST to legacy query format + // Note: Convert FilterCondition (MongoDB-like) to array format for excel driver const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - skip: ast.skip, + filters: this.convertFilterConditionToArray(ast.where), + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + skip: ast.offset, }; // Use existing find method @@ -1032,60 +1033,80 @@ export class ExcelDriver implements Driver { // ========== Helper Methods ========== /** - * Convert FilterNode from QueryAST to legacy filter format. + * Convert FilterCondition (MongoDB-like format) to legacy array format. + * This allows the excel driver to use its existing filter evaluation logic. * - * @param node - The FilterNode to convert + * @param condition - FilterCondition object or legacy array * @returns Legacy filter array format */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); + private convertFilterConditionToArray(condition?: any): any[] | undefined { + if (!condition) return undefined; + + // If already an array, return as-is + if (Array.isArray(condition)) { + return condition; + } + + // If it's an object (FilterCondition), convert to array format + // This is a simplified conversion - a full implementation would need to handle all operators + const result: any[] = []; + + for (const [key, value] of Object.entries(condition)) { + if (key === '$and' && Array.isArray(value)) { + // Handle $and: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('and'); } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); + } else if (key === '$or' && Array.isArray(value)) { + // Handle $or: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('or'); } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is complex - we'll just process the first child for now - if (node.children && node.children.length > 0) { - return this.convertFilterNodeToLegacy(node.children[0]); + } else if (key === '$not' && typeof value === 'object') { + // Handle $not: { condition } + // Note: NOT is complex to represent in array format, so we skip it for now + const converted = this.convertFilterConditionToArray(value); + if (converted) { + result.push(...converted); } - return undefined; - - default: - return undefined; + } else if (typeof value === 'object' && value !== null) { + // Handle field-level conditions like { field: { $eq: value } } + const field = key; + for (const [operator, operandValue] of Object.entries(value)) { + let op: string; + switch (operator) { + case '$eq': op = '='; break; + case '$ne': op = '!='; break; + case '$gt': op = '>'; break; + case '$gte': op = '>='; break; + case '$lt': op = '<'; break; + case '$lte': op = '<='; break; + case '$in': op = 'in'; break; + case '$nin': op = 'nin'; break; + case '$regex': op = 'like'; break; + default: op = '='; + } + result.push([field, op, operandValue]); + } + } else { + // Handle simple equality: { field: value } + result.push([key, '=', value]); + } } + + return result.length > 0 ? result : undefined; } /** diff --git a/packages/drivers/fs/src/index.ts b/packages/drivers/fs/src/index.ts index 8f8db3ba..d7bafb76 100644 --- a/packages/drivers/fs/src/index.ts +++ b/packages/drivers/fs/src/index.ts @@ -619,12 +619,13 @@ export class FileSystemDriver implements Driver { const objectName = ast.object || ''; // Convert QueryAST to legacy query format + // Note: Convert FilterCondition (MongoDB-like) to array format for fs driver const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - skip: ast.skip, + filters: this.convertFilterConditionToArray(ast.where), + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + skip: ast.offset, }; // Use existing find method @@ -753,60 +754,80 @@ export class FileSystemDriver implements Driver { // ========== Helper Methods ========== /** - * Convert FilterNode from QueryAST to legacy filter format. + * Convert FilterCondition (MongoDB-like format) to legacy array format. + * This allows the fs driver to use its existing filter evaluation logic. * - * @param node - The FilterNode to convert + * @param condition - FilterCondition object or legacy array * @returns Legacy filter array format */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; + private convertFilterConditionToArray(condition?: any): any[] | undefined { + if (!condition) return undefined; - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); + // If already an array, return as-is + if (Array.isArray(condition)) { + return condition; + } + + // If it's an object (FilterCondition), convert to array format + // This is a simplified conversion - a full implementation would need to handle all operators + const result: any[] = []; + + for (const [key, value] of Object.entries(condition)) { + if (key === '$and' && Array.isArray(value)) { + // Handle $and: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('and'); } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); + } else if (key === '$or' && Array.isArray(value)) { + // Handle $or: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('or'); } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is complex - we'll just process the first child for now - if (node.children && node.children.length > 0) { - return this.convertFilterNodeToLegacy(node.children[0]); + } else if (key === '$not' && typeof value === 'object') { + // Handle $not: { condition } + // Note: NOT is complex to represent in array format, so we skip it for now + const converted = this.convertFilterConditionToArray(value); + if (converted) { + result.push(...converted); } - return undefined; - - default: - return undefined; + } else if (typeof value === 'object' && value !== null) { + // Handle field-level conditions like { field: { $eq: value } } + const field = key; + for (const [operator, operandValue] of Object.entries(value)) { + let op: string; + switch (operator) { + case '$eq': op = '='; break; + case '$ne': op = '!='; break; + case '$gt': op = '>'; break; + case '$gte': op = '>='; break; + case '$lt': op = '<'; break; + case '$lte': op = '<='; break; + case '$in': op = 'in'; break; + case '$nin': op = 'nin'; break; + case '$regex': op = 'like'; break; + default: op = '='; + } + result.push([field, op, operandValue]); + } + } else { + // Handle simple equality: { field: value } + result.push([key, '=', value]); + } } + + return result.length > 0 ? result : undefined; } /** diff --git a/packages/drivers/memory/src/index.ts b/packages/drivers/memory/src/index.ts index 374cb3f9..7c5fb4ae 100644 --- a/packages/drivers/memory/src/index.ts +++ b/packages/drivers/memory/src/index.ts @@ -515,18 +515,27 @@ export class MemoryDriver implements Driver { /** * Convert ObjectQL filters to MongoDB query format for Mingo. * - * Supports ObjectQL filter format: - * [ - * ['field', 'operator', value], - * 'or', - * ['field2', 'operator', value2] - * ] + * Supports both: + * 1. Legacy ObjectQL filter format (array): + * [['field', 'operator', value], 'or', ['field2', 'operator', value2']] + * 2. New FilterCondition format (object - already MongoDB-like): + * { $and: [{ field: { $eq: value }}, { field2: { $gt: value2 }}] } * * Converts to MongoDB query format: * { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] } */ - private convertToMongoQuery(filters?: any[]): Record { - if (!filters || filters.length === 0) { + private convertToMongoQuery(filters?: any[] | Record): Record { + if (!filters) { + return {}; + } + + // If filters is already an object (FilterCondition format), return it directly + if (!Array.isArray(filters)) { + return filters; + } + + // Handle legacy array format + if (filters.length === 0) { return {}; } @@ -751,12 +760,14 @@ export class MemoryDriver implements Driver { const objectName = ast.object || ''; // Convert QueryAST to legacy query format + // Note: ast.where is already in FilterCondition format (MongoDB-like with $eq, $and, etc.) + // which can be passed directly to Mingo, so we just pass it as-is const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - offset: ast.skip, + filters: ast.where, // FilterCondition is already MongoDB-compatible + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + offset: ast.offset, }; // Use existing find method @@ -872,63 +883,6 @@ export class MemoryDriver implements Driver { } } - /** - * Convert FilterNode (QueryAST format) to legacy filter array format - * This allows reuse of existing filter logic while supporting new QueryAST - * - * @private - */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); - } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); - } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); - } - } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is complex - we'll just process the first child for now - if (node.children && node.children.length > 0) { - return this.convertFilterNodeToLegacy(node.children[0]); - } - return undefined; - - default: - return undefined; - } - } - /** * Execute command (alternative signature for compatibility) * From e36db696663f58158fdf25ef6eb5b3c2b0472504 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:39:46 +0000 Subject: [PATCH 08/21] Fix QueryAST property names in redis and localstorage drivers Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/localstorage/src/index.ts | 115 ++++++++++++-------- packages/drivers/redis/src/index.ts | 119 ++++++++++++--------- 2 files changed, 137 insertions(+), 97 deletions(-) diff --git a/packages/drivers/localstorage/src/index.ts b/packages/drivers/localstorage/src/index.ts index 536cff13..83ac3078 100644 --- a/packages/drivers/localstorage/src/index.ts +++ b/packages/drivers/localstorage/src/index.ts @@ -585,12 +585,13 @@ export class LocalStorageDriver implements Driver { const objectName = ast.object || ''; // Convert QueryAST to legacy query format + // Note: Convert FilterCondition (MongoDB-like) to array format for localstorage driver const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - skip: ast.skip, + filters: this.convertFilterConditionToArray(ast.where), + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + skip: ast.offset, }; // Use existing find method @@ -719,60 +720,80 @@ export class LocalStorageDriver implements Driver { // ========== Helper Methods (Same as MemoryDriver) ========== /** - * Convert FilterNode from QueryAST to legacy filter format. + * Convert FilterCondition (MongoDB-like format) to legacy array format. + * This allows the localstorage driver to use its existing filter evaluation logic. * - * @param node - The FilterNode to convert + * @param condition - FilterCondition object or legacy array * @returns Legacy filter array format */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); + private convertFilterConditionToArray(condition?: any): any[] | undefined { + if (!condition) return undefined; + + // If already an array, return as-is + if (Array.isArray(condition)) { + return condition; + } + + // If it's an object (FilterCondition), convert to array format + // This is a simplified conversion - a full implementation would need to handle all operators + const result: any[] = []; + + for (const [key, value] of Object.entries(condition)) { + if (key === '$and' && Array.isArray(value)) { + // Handle $and: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('and'); } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); + } else if (key === '$or' && Array.isArray(value)) { + // Handle $or: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('or'); } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is complex - we'll just process the first child for now - if (node.children && node.children.length > 0) { - return this.convertFilterNodeToLegacy(node.children[0]); + } else if (key === '$not' && typeof value === 'object') { + // Handle $not: { condition } + // Note: NOT is complex to represent in array format, so we skip it for now + const converted = this.convertFilterConditionToArray(value); + if (converted) { + result.push(...converted); } - return undefined; - - default: - return undefined; + } else if (typeof value === 'object' && value !== null) { + // Handle field-level conditions like { field: { $eq: value } } + const field = key; + for (const [operator, operandValue] of Object.entries(value)) { + let op: string; + switch (operator) { + case '$eq': op = '='; break; + case '$ne': op = '!='; break; + case '$gt': op = '>'; break; + case '$gte': op = '>='; break; + case '$lt': op = '<'; break; + case '$lte': op = '<='; break; + case '$in': op = 'in'; break; + case '$nin': op = 'nin'; break; + case '$regex': op = 'like'; break; + default: op = '='; + } + result.push([field, op, operandValue]); + } + } else { + // Handle simple equality: { field: value } + result.push([key, '=', value]); + } } + + return result.length > 0 ? result : undefined; } /** diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index 67617654..38423a94 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -415,12 +415,13 @@ export class RedisDriver implements Driver { const objectName = ast.object || ''; // Convert QueryAST to legacy query format + // Note: Convert FilterCondition (MongoDB-like) to array format for redis driver const legacyQuery: any = { fields: ast.fields, - filters: this.convertFilterNodeToLegacy(ast.filters), - sort: ast.sort?.map((s: SortNode) => [s.field, s.order]), - limit: ast.top, - skip: ast.skip, + filters: this.convertFilterConditionToArray(ast.where), + sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]), + limit: ast.limit, + skip: ast.offset, }; // Use existing find method @@ -658,64 +659,82 @@ export class RedisDriver implements Driver { * @private * * @example - * // Input: { type: 'comparison', field: 'age', operator: '>', value: 18 } - * // Output: [['age', '>', 18]] + * // Input: { field: { $gt: 18 } } + * // Output: [['field', '>', 18]] * * @example - * // Input: { type: 'and', children: [...] } + * // Input: { $and: [{ field1: { $eq: 'val1' }}, { field2: { $gt: 10 }}] } * // Output: [['field1', '=', 'val1'], 'and', ['field2', '>', 10]] */ - private convertFilterNodeToLegacy(node?: FilterNode): any { - if (!node) return undefined; - - switch (node.type) { - case 'comparison': - // Convert comparison node to [field, operator, value] format - if (!node.operator) { - console.warn('[RedisDriver] FilterNode comparison missing operator, defaulting to "="'); - } - const operator = node.operator || '='; - return [[node.field, operator, node.value]]; - - case 'and': - // Convert AND node to array with 'and' separator - if (!node.children || node.children.length === 0) return undefined; - const andResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (andResults.length > 0) { - andResults.push('and'); + private convertFilterConditionToArray(condition?: any): any[] | undefined { + if (!condition) return undefined; + + // If already an array, return as-is + if (Array.isArray(condition)) { + return condition; + } + + // If it's an object (FilterCondition), convert to array format + // This is a simplified conversion - a full implementation would need to handle all operators + const result: any[] = []; + + for (const [key, value] of Object.entries(condition)) { + if (key === '$and' && Array.isArray(value)) { + // Handle $and: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('and'); } - andResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return andResults.length > 0 ? andResults : undefined; - - case 'or': - // Convert OR node to array with 'or' separator - if (!node.children || node.children.length === 0) return undefined; - const orResults: any[] = []; - for (const child of node.children) { - const converted = this.convertFilterNodeToLegacy(child); - if (converted) { - if (orResults.length > 0) { - orResults.push('or'); + } else if (key === '$or' && Array.isArray(value)) { + // Handle $or: [cond1, cond2, ...] + for (let i = 0; i < value.length; i++) { + const converted = this.convertFilterConditionToArray(value[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push('or'); } - orResults.push(...(Array.isArray(converted) ? converted : [converted])); + result.push(...converted); } } - return orResults.length > 0 ? orResults : undefined; - - case 'not': - // NOT is not directly supported in legacy format - // We could implement it by negating the child operators + } else if (key === '$not' && typeof value === 'object') { + // Handle $not: { condition } + // Note: NOT is complex to represent in array format, so we skip it for now console.warn('[RedisDriver] NOT operator in filters is not fully supported in legacy format'); - return undefined; - - default: - return undefined; + const converted = this.convertFilterConditionToArray(value); + if (converted) { + result.push(...converted); + } + } else if (typeof value === 'object' && value !== null) { + // Handle field-level conditions like { field: { $eq: value } } + const field = key; + for (const [operator, operandValue] of Object.entries(value)) { + let op: string; + switch (operator) { + case '$eq': op = '='; break; + case '$ne': op = '!='; break; + case '$gt': op = '>'; break; + case '$gte': op = '>='; break; + case '$lt': op = '<'; break; + case '$lte': op = '<='; break; + case '$in': op = 'in'; break; + case '$nin': op = 'nin'; break; + case '$regex': op = 'like'; break; + default: op = '='; + } + result.push([field, op, operandValue]); + } + } else { + // Handle simple equality: { field: value } + result.push([key, '=', value]); + } } + + return result.length > 0 ? result : undefined; } /** From b6530b4c8d8cb94b143f7c21ca5a6c2f5d9dadb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:41:08 +0000 Subject: [PATCH 09/21] Update fs driver tests to use new QueryAST property names Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/fs/test/index.test.ts | 37 +++++++++++--------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/drivers/fs/test/index.test.ts b/packages/drivers/fs/test/index.test.ts index c703f2f6..9858dd61 100644 --- a/packages/drivers/fs/test/index.test.ts +++ b/packages/drivers/fs/test/index.test.ts @@ -439,15 +439,12 @@ describe('FileSystemDriver', () => { const result = await driver.executeQuery({ object: 'users', fields: ['name', 'age'], - filters: { - type: 'comparison', - field: 'age', - operator: '>', - value: 25 + where: { + age: { $gt: 25 } }, - sort: [{ field: 'age', order: 'asc' }], - top: 10, - skip: 0 + orderBy: [{ field: 'age', order: 'asc' }], + limit: 10, + offset: 0 }); expect(result.value).toHaveLength(2); @@ -463,11 +460,10 @@ describe('FileSystemDriver', () => { const result = await driver.executeQuery({ object: 'users', - filters: { - type: 'and', - children: [ - { type: 'comparison', field: 'age', operator: '>', value: 25 }, - { type: 'comparison', field: 'city', operator: '=', value: 'NYC' } + where: { + $and: [ + { age: { $gt: 25 } }, + { city: { $eq: 'NYC' } } ] } }); @@ -483,11 +479,10 @@ describe('FileSystemDriver', () => { const result = await driver.executeQuery({ object: 'users', - filters: { - type: 'or', - children: [ - { type: 'comparison', field: 'age', operator: '=', value: 25 }, - { type: 'comparison', field: 'age', operator: '=', value: 35 } + where: { + $or: [ + { age: { $eq: 25 } }, + { age: { $eq: 35 } } ] } }); @@ -504,9 +499,9 @@ describe('FileSystemDriver', () => { const result = await driver.executeQuery({ object: 'users', - sort: [{ field: 'name', order: 'asc' }], - skip: 1, - top: 1 + orderBy: [{ field: 'name', order: 'asc' }], + offset: 1, + limit: 1 }); expect(result.value).toHaveLength(1); From 31d366df92cba2b07d63586296f53ce5f3138db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:42:38 +0000 Subject: [PATCH 10/21] Update documentation in redis driver to reflect FilterCondition format Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/redis/src/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index 38423a94..7646d6ef 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -378,9 +378,9 @@ export class RedisDriver implements Driver { * This method handles all read operations using the QueryAST format from @objectstack/spec. * It provides a standardized query interface that supports: * - Field selection (projection) - * - Filter conditions (using FilterNode AST) + * - Filter conditions (using FilterCondition format) * - Sorting - * - Pagination (skip/top) + * - Pagination (offset/limit) * - Grouping and aggregations (delegated to find) * * The method converts the QueryAST format to the legacy query format and delegates @@ -639,22 +639,22 @@ export class RedisDriver implements Driver { // ========== Helper Methods ========== /** - * Convert FilterNode (QueryAST format) to legacy filter array format + * Convert FilterCondition (MongoDB-like format) to legacy filter array format * - * This method bridges the gap between the new QueryAST filter format (tree-based) + * This method bridges the gap between the new FilterCondition format (MongoDB-style) * and the legacy array-based filter format used internally by the driver. * - * QueryAST FilterNode format: - * - type: 'comparison' | 'and' | 'or' | 'not' - * - field, operator, value for comparisons - * - children for logical operators + * FilterCondition format (MongoDB-like): + * - Field-level operators: { field: { $eq: value }, field2: { $gt: value } } + * - Logical operators: { $and: [...], $or: [...], $not: {...} } + * - Examples: { age: { $gt: 18 } }, { $and: [{ age: { $gt: 18 }}, { role: { $eq: 'user' }}] } * * Legacy format: * - Array of conditions: [field, operator, value] * - String separators: 'and', 'or' * - Example: [['age', '>', 18], 'and', ['role', '=', 'user']] * - * @param node - The FilterNode to convert + * @param condition - The FilterCondition to convert (object or legacy array) * @returns Legacy filter array format, or undefined if no filters * @private * From 3b17e6e14a4a616d0072d350e2a5cf511d58f801 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:49:17 +0000 Subject: [PATCH 11/21] Initial plan From 17fe1b2b9ba030bd12fa3b3f5ed7fb37258bf9f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 07:53:10 +0000 Subject: [PATCH 12/21] Add zod as devDependency to fix test failures Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/foundation/types/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/foundation/types/package.json b/packages/foundation/types/package.json index 6c59281f..e3abad06 100644 --- a/packages/foundation/types/package.json +++ b/packages/foundation/types/package.json @@ -33,6 +33,7 @@ "devDependencies": { "@objectstack/spec": "^0.3.3", "@objectql/runtime": "workspace:*", - "ts-json-schema-generator": "^2.4.0" + "ts-json-schema-generator": "^2.4.0", + "zod": "^3.23.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b181c26..b6bf643a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -538,6 +538,9 @@ importers: ts-json-schema-generator: specifier: ^2.4.0 version: 2.4.0 + zod: + specifier: ^3.23.8 + version: 3.25.76 packages/objectstack/runtime: {} From 6d581034dbbcfb0c2a8023ba497e055669e88a9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:01:46 +0000 Subject: [PATCH 13/21] Initial plan From c57bbe3169eb7895cc7eb89253fcbf4061dfc1fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:10:22 +0000 Subject: [PATCH 14/21] test: update driver tests to use new QueryAST format from @objectstack/spec v0.3.3 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/fs/test/index.test.ts | 2 +- .../drivers/localstorage/test/index.test.ts | 18 +++++++++--------- packages/drivers/redis/test/index.test.ts | 14 +++++++------- .../drivers/sdk/test/remote-driver.test.ts | 6 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/drivers/fs/test/index.test.ts b/packages/drivers/fs/test/index.test.ts index 9858dd61..5f30c8bd 100644 --- a/packages/drivers/fs/test/index.test.ts +++ b/packages/drivers/fs/test/index.test.ts @@ -492,7 +492,7 @@ describe('FileSystemDriver', () => { expect(result.value.some((u: any) => u.name === 'Charlie')).toBe(true); }); - test('should handle pagination with skip and top', async () => { + test('should handle pagination with offset and limit', async () => { await driver.create('users', { name: 'Alice', age: 30 }); await driver.create('users', { name: 'Bob', age: 25 }); await driver.create('users', { name: 'Charlie', age: 35 }); diff --git a/packages/drivers/localstorage/test/index.test.ts b/packages/drivers/localstorage/test/index.test.ts index 9d9f4fae..e2196819 100644 --- a/packages/drivers/localstorage/test/index.test.ts +++ b/packages/drivers/localstorage/test/index.test.ts @@ -428,15 +428,15 @@ describe('LocalStorageDriver', () => { const result = await driver.executeQuery({ object: 'users', fields: ['name', 'age'], - filters: { + where: { type: 'comparison', field: 'age', operator: '>', value: 25 }, - sort: [{ field: 'age', order: 'asc' }], - top: 10, - skip: 0 + orderBy: [{ field: 'age', order: 'asc' }], + limit: 10, + offset: 0 }); expect(result.value).toHaveLength(2); @@ -452,7 +452,7 @@ describe('LocalStorageDriver', () => { const result = await driver.executeQuery({ object: 'users', - filters: { + where: { type: 'and', children: [ { type: 'comparison', field: 'age', operator: '>', value: 25 }, @@ -465,16 +465,16 @@ describe('LocalStorageDriver', () => { expect(result.value.every((u: any) => u.city === 'NYC')).toBe(true); }); - it('should handle pagination with skip and top', async () => { + it('should handle pagination with offset and limit', async () => { await driver.create('users', { name: 'Alice', age: 30 }); await driver.create('users', { name: 'Bob', age: 25 }); await driver.create('users', { name: 'Charlie', age: 35 }); const result = await driver.executeQuery({ object: 'users', - sort: [{ field: 'name', order: 'asc' }], - skip: 1, - top: 1 + orderBy: [{ field: 'name', order: 'asc' }], + offset: 1, + limit: 1 }); expect(result.value).toHaveLength(1); diff --git a/packages/drivers/redis/test/index.test.ts b/packages/drivers/redis/test/index.test.ts index fba65481..62984e70 100644 --- a/packages/drivers/redis/test/index.test.ts +++ b/packages/drivers/redis/test/index.test.ts @@ -351,7 +351,7 @@ describe('RedisDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - filters: { + where: { type: 'comparison', field: 'role', operator: '=', @@ -368,7 +368,7 @@ describe('RedisDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - sort: [{ field: 'age', order: 'asc' }] + orderBy: [{ field: 'age', order: 'asc' }] }); expect(result.value).toHaveLength(3); @@ -382,9 +382,9 @@ describe('RedisDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - sort: [{ field: 'age', order: 'asc' }], - skip: 1, - top: 1 + orderBy: [{ field: 'age', order: 'asc' }], + offset: 1, + limit: 1 }); expect(result.value).toHaveLength(1); @@ -396,7 +396,7 @@ describe('RedisDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - filters: { + where: { type: 'and', children: [ { @@ -424,7 +424,7 @@ describe('RedisDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - filters: { + where: { type: 'or', children: [ { diff --git a/packages/drivers/sdk/test/remote-driver.test.ts b/packages/drivers/sdk/test/remote-driver.test.ts index 88682862..54414962 100644 --- a/packages/drivers/sdk/test/remote-driver.test.ts +++ b/packages/drivers/sdk/test/remote-driver.test.ts @@ -452,14 +452,14 @@ describe('RemoteDriver', () => { const queryAST = { object: 'users', fields: ['name', 'email'], - filters: { + where: { type: 'comparison' as const, field: 'status', operator: '=', value: 'active' }, - sort: [{ field: 'created_at', order: 'desc' as const }], - top: 10 + orderBy: [{ field: 'created_at', order: 'desc' as const }], + limit: 10 }; const mockResponse = { From bd4e6e7e2e5e60977a8be4a17f54a10a920c16d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:13:18 +0000 Subject: [PATCH 15/21] Fix QueryAST compatibility with @objectstack/spec v0.3.3 Update test files to use correct QueryAST property names: - where (instead of filters) - orderBy (instead of sort) - limit (instead of top) - offset (instead of skip) Also update filter syntax to MongoDB-style format: - { age: { $gt: 25 } } instead of { type: 'comparison', ... } - { $and: [...] } instead of { type: 'and', children: [...] } Fixed in: - packages/drivers/excel/test/index.test.ts - packages/drivers/localstorage/test/index.test.ts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/drivers/excel/test/index.test.ts | 28 ++++++++----------- .../drivers/localstorage/test/index.test.ts | 12 +++----- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/drivers/excel/test/index.test.ts b/packages/drivers/excel/test/index.test.ts index c010968a..5ec1aeea 100644 --- a/packages/drivers/excel/test/index.test.ts +++ b/packages/drivers/excel/test/index.test.ts @@ -582,15 +582,12 @@ describe('ExcelDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, fields: ['name', 'age'], - filters: { - type: 'comparison', - field: 'age', - operator: '>', - value: 25 + where: { + age: { $gt: 25 } }, - sort: [{ field: 'age', order: 'asc' }], - top: 10, - skip: 0 + orderBy: [{ field: 'age', order: 'asc' }], + limit: 10, + offset: 0 }); expect(result.value).toHaveLength(2); @@ -606,11 +603,10 @@ describe('ExcelDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - filters: { - type: 'and', - children: [ - { type: 'comparison', field: 'age', operator: '>', value: 25 }, - { type: 'comparison', field: 'city', operator: '=', value: 'NYC' } + where: { + $and: [ + { age: { $gt: 25 } }, + { city: { $eq: 'NYC' } } ] } }); @@ -626,9 +622,9 @@ describe('ExcelDriver', () => { const result = await driver.executeQuery({ object: TEST_OBJECT, - sort: [{ field: 'name', order: 'asc' }], - skip: 1, - top: 1 + orderBy: [{ field: 'name', order: 'asc' }], + offset: 1, + limit: 1 }); expect(result.value).toHaveLength(1); diff --git a/packages/drivers/localstorage/test/index.test.ts b/packages/drivers/localstorage/test/index.test.ts index e2196819..dce8bbe3 100644 --- a/packages/drivers/localstorage/test/index.test.ts +++ b/packages/drivers/localstorage/test/index.test.ts @@ -429,10 +429,7 @@ describe('LocalStorageDriver', () => { object: 'users', fields: ['name', 'age'], where: { - type: 'comparison', - field: 'age', - operator: '>', - value: 25 + age: { $gt: 25 } }, orderBy: [{ field: 'age', order: 'asc' }], limit: 10, @@ -453,10 +450,9 @@ describe('LocalStorageDriver', () => { const result = await driver.executeQuery({ object: 'users', where: { - type: 'and', - children: [ - { type: 'comparison', field: 'age', operator: '>', value: 25 }, - { type: 'comparison', field: 'city', operator: '=', value: 'NYC' } + $and: [ + { age: { $gt: 25 } }, + { city: { $eq: 'NYC' } } ] } }); From 578608e20e2829205d8762d803be9f2cbaecff2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:29:47 +0000 Subject: [PATCH 16/21] Initial plan From adacc16bdedb6eb5d31e0e8745aa904fe48b00be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:37:42 +0000 Subject: [PATCH 17/21] Fix Redis driver filter conversion for @objectstack/spec v0.3.3 Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- packages/drivers/redis/src/index.ts | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index 7646d6ef..3cd961cd 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -674,8 +674,38 @@ export class RedisDriver implements Driver { return condition; } - // If it's an object (FilterCondition), convert to array format - // This is a simplified conversion - a full implementation would need to handle all operators + // Handle new @objectstack/spec FilterCondition format (v0.3.3+) + // Check if it's the new format with 'type' property + if (condition.type) { + if (condition.type === 'comparison') { + // Handle comparison filter: { type: 'comparison', field, operator, value } + return [[condition.field, condition.operator, condition.value]]; + } else if (condition.type === 'and' || condition.type === 'or') { + // Handle logical filter: { type: 'and' | 'or', children: [...] } + const result: any[] = []; + const logicalOp = condition.type; + + if (condition.children && Array.isArray(condition.children)) { + for (let i = 0; i < condition.children.length; i++) { + const converted = this.convertFilterConditionToArray(condition.children[i]); + if (converted && converted.length > 0) { + if (result.length > 0) { + result.push(logicalOp); + } + result.push(...converted); + } + } + } + + return result.length > 0 ? result : undefined; + } else if (condition.type === 'not') { + // Handle NOT filter: { type: 'not', child: {...} } + console.warn('[RedisDriver] NOT operator in filters is not fully supported in legacy format'); + return this.convertFilterConditionToArray(condition.child); + } + } + + // Fallback: Handle legacy MongoDB-style filters for backward compatibility const result: any[] = []; for (const [key, value] of Object.entries(condition)) { @@ -703,7 +733,6 @@ export class RedisDriver implements Driver { } } else if (key === '$not' && typeof value === 'object') { // Handle $not: { condition } - // Note: NOT is complex to represent in array format, so we skip it for now console.warn('[RedisDriver] NOT operator in filters is not fully supported in legacy format'); const converted = this.convertFilterConditionToArray(value); if (converted) { From 682118cf93e6224416fbdbbb478450817d324082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:38:28 +0000 Subject: [PATCH 18/21] Add safety check for NOT filter child property Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- packages/drivers/redis/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index 3cd961cd..ae1abf67 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -701,7 +701,9 @@ export class RedisDriver implements Driver { } else if (condition.type === 'not') { // Handle NOT filter: { type: 'not', child: {...} } console.warn('[RedisDriver] NOT operator in filters is not fully supported in legacy format'); - return this.convertFilterConditionToArray(condition.child); + if (condition.child) { + return this.convertFilterConditionToArray(condition.child); + } } } From 0708f0c4aab4857d5d8953a1c3bded588601afe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 08:54:06 +0000 Subject: [PATCH 19/21] Initial plan From 514c38fe2d9bb9fc755b494c87882a0ab0a05c74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:01:49 +0000 Subject: [PATCH 20/21] Fix pagination in mock drivers to use offset instead of skip Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- packages/runtime/server/test/graphql.test.ts | 6 +++--- packages/runtime/server/test/rest-advanced.test.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/runtime/server/test/graphql.test.ts b/packages/runtime/server/test/graphql.test.ts index 22f1ff16..daa0d63d 100644 --- a/packages/runtime/server/test/graphql.test.ts +++ b/packages/runtime/server/test/graphql.test.ts @@ -43,10 +43,10 @@ class MockDriver implements Driver { async find(objectName: string, query: any) { let items = this.data[objectName] || []; - // Apply skip and limit if provided + // Apply offset and limit if provided (QueryAST uses 'offset', not 'skip') if (query) { - if (query.skip) { - items = items.slice(query.skip); + if (query.offset) { + items = items.slice(query.offset); } if (query.limit) { items = items.slice(0, query.limit); diff --git a/packages/runtime/server/test/rest-advanced.test.ts b/packages/runtime/server/test/rest-advanced.test.ts index 065b5ca4..31ec0c5e 100644 --- a/packages/runtime/server/test/rest-advanced.test.ts +++ b/packages/runtime/server/test/rest-advanced.test.ts @@ -41,9 +41,9 @@ class MockDriver implements Driver { } } - // Apply skip and top/limit (QueryAST uses 'top' for limit) - if (query?.skip) { - items = items.slice(query.skip); + // Apply offset and limit (QueryAST uses 'offset' for skip) + if (query?.offset) { + items = items.slice(query.offset); } if (query?.top || query?.limit) { items = items.slice(0, query.top || query.limit); @@ -87,8 +87,9 @@ class MockDriver implements Driver { } async count(objectName: string, query: any) { - // Count should not apply skip/limit, only filters + // Count should not apply offset/limit, only filters const countQuery = { ...query }; + delete countQuery.offset; delete countQuery.skip; delete countQuery.top; delete countQuery.limit; From 2a5c3d960a42b8ef030dc0b74093adaf34327c5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:05:59 +0000 Subject: [PATCH 21/21] Remove redundant skip deletion in count query (code review fix) Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com> --- packages/runtime/server/test/rest-advanced.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime/server/test/rest-advanced.test.ts b/packages/runtime/server/test/rest-advanced.test.ts index 31ec0c5e..7d46fdf1 100644 --- a/packages/runtime/server/test/rest-advanced.test.ts +++ b/packages/runtime/server/test/rest-advanced.test.ts @@ -90,7 +90,6 @@ class MockDriver implements Driver { // Count should not apply offset/limit, only filters const countQuery = { ...query }; delete countQuery.offset; - delete countQuery.skip; delete countQuery.top; delete countQuery.limit; const items = await this.find(objectName, countQuery);