Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d54c0ec
Initial plan
Copilot Jan 25, 2026
380b315
Update @objectstack/spec to 0.3.3 in all package.json files
Copilot Jan 25, 2026
bf4ab3f
Fix breaking changes from @objectstack/spec upgrade to 0.3.3
Copilot Jan 25, 2026
2542193
Fix MongoDB driver and tests for FilterCondition format
Copilot Jan 25, 2026
847ccb6
Fix SQL driver to use skip instead of offset in legacy query format
Copilot Jan 25, 2026
00401ce
Initial plan
Copilot Jan 25, 2026
dafc7cb
Fix QueryAST property name changes in memory, fs, and excel drivers
Copilot Jan 25, 2026
e36db69
Fix QueryAST property names in redis and localstorage drivers
Copilot Jan 25, 2026
b6530b4
Update fs driver tests to use new QueryAST property names
Copilot Jan 25, 2026
31d366d
Update documentation in redis driver to reflect FilterCondition format
Copilot Jan 25, 2026
f473aec
Merge pull request #193 from objectstack-ai/copilot/fix-action-step-i…
hotlong Jan 25, 2026
3b17e6e
Initial plan
Copilot Jan 25, 2026
17fe1b2
Add zod as devDependency to fix test failures
Copilot Jan 25, 2026
3b03434
Merge pull request #194 from objectstack-ai/copilot/fix-api-integrati…
hotlong Jan 25, 2026
6d58103
Initial plan
Copilot Jan 25, 2026
c57bbe3
test: update driver tests to use new QueryAST format from @objectstac…
Copilot Jan 25, 2026
bd4e6e7
Fix QueryAST compatibility with @objectstack/spec v0.3.3
Copilot Jan 25, 2026
332243e
Merge pull request #195 from objectstack-ai/copilot/check-action-stat…
hotlong Jan 25, 2026
578608e
Initial plan
Copilot Jan 25, 2026
adacc16
Fix Redis driver filter conversion for @objectstack/spec v0.3.3
Copilot Jan 25, 2026
682118c
Add safety check for NOT filter child property
Copilot Jan 25, 2026
1759209
Merge pull request #196 from objectstack-ai/copilot/fix-action-run-error
xuyushun441-sys Jan 25, 2026
0708f0c
Initial plan
Copilot Jan 25, 2026
514c38f
Fix pagination in mock drivers to use offset instead of skip
Copilot Jan 25, 2026
2a5c3d9
Remove redundant skip deletion in count query (code review fix)
Copilot Jan 25, 2026
0a56ba9
Merge pull request #197 from objectstack-ai/copilot/fix-action-job-issue
xuyushun441-sys Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/showcase/enterprise-erp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/drivers/excel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"dependencies": {
"@objectql/types": "workspace:*",
"@objectstack/spec": "^0.3.1",
"@objectstack/spec": "^0.3.3",
"exceljs": "^4.4.0"
},
"devDependencies": {
Expand Down
120 changes: 70 additions & 50 deletions packages/drivers/excel/src/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -899,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
Expand Down Expand Up @@ -1033,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]);
}
Comment on lines +1098 to +1102
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

convertFilterConditionToArray appends multiple [field, op, value] entries without inserting 'and' separators (for multiple fields or multiple operators on the same field). Since matchesFilters combines conditions only via explicit operator tokens, the additional conditions may be skipped. Please interleave 'and' between generated conditions by default.

Copilot uses AI. Check for mistakes.
} else {
// Handle simple equality: { field: value }
result.push([key, '=', value]);
}
}

return result.length > 0 ? result : undefined;
}

/**
Expand Down
28 changes: 12 additions & 16 deletions packages/drivers/excel/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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' } }
]
}
});
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/drivers/fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"dependencies": {
"@objectql/types": "workspace:*",
"@objectstack/spec": "^0.3.1"
"@objectstack/spec": "^0.3.3"
},
"devDependencies": {
"@types/jest": "^29.0.0",
Expand Down
118 changes: 69 additions & 49 deletions packages/drivers/fs/src/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -620,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
Expand Down Expand Up @@ -754,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]);
Comment on lines +822 to +826
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

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

convertFilterConditionToArray pushes multiple conditions into result without inserting 'and' separators (e.g., multiple fields in the same object, or multiple operators on one field). matchesFilters only combines conditions using explicit operator tokens, so extra conditions beyond the first can be ignored. Please interleave 'and' between generated conditions by default when building result.

Copilot uses AI. Check for mistakes.
}
}

return result.length > 0 ? result : undefined;
}

/**
Expand Down
Loading