Skip to content

Commit d4d19f2

Browse files
Merge pull request #192 from objectstack-ai/copilot/update-objectstack-to-latest
2 parents bb13805 + 0a56ba9 commit d4d19f2

File tree

36 files changed

+515
-661
lines changed

36 files changed

+515
-661
lines changed

examples/showcase/enterprise-erp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@objectql/cli": "workspace:*",
4444
"@objectql/driver-sql": "workspace:*",
4545
"@objectql/platform-node": "workspace:*",
46-
"@objectstack/spec": "^0.3.1",
46+
"@objectstack/spec": "^0.3.3",
4747
"@types/jest": "^30.0.0",
4848
"@types/node": "^20.0.0",
4949
"jest": "^30.2.0",

packages/drivers/excel/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"dependencies": {
2323
"@objectql/types": "workspace:*",
24-
"@objectstack/spec": "^0.3.1",
24+
"@objectstack/spec": "^0.3.3",
2525
"exceljs": "^4.4.0"
2626
},
2727
"devDependencies": {

packages/drivers/excel/src/index.ts

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Data, System } from '@objectstack/spec';
1+
import { Data, Driver as DriverSpec } from '@objectstack/spec';
22
type QueryAST = Data.QueryAST;
3-
type FilterNode = Data.FilterNode;
43
type SortNode = Data.SortNode;
5-
type DriverInterface = System.DriverInterface;
4+
type DriverInterface = DriverSpec.DriverInterface;
65
/**
76
* ObjectQL
87
* Copyright (c) 2026-present ObjectStack Inc.
@@ -899,12 +898,13 @@ export class ExcelDriver implements Driver {
899898
const objectName = ast.object || '';
900899

901900
// Convert QueryAST to legacy query format
901+
// Note: Convert FilterCondition (MongoDB-like) to array format for excel driver
902902
const legacyQuery: any = {
903903
fields: ast.fields,
904-
filters: this.convertFilterNodeToLegacy(ast.filters),
905-
sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
906-
limit: ast.top,
907-
skip: ast.skip,
904+
filters: this.convertFilterConditionToArray(ast.where),
905+
sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]),
906+
limit: ast.limit,
907+
skip: ast.offset,
908908
};
909909

910910
// Use existing find method
@@ -1033,60 +1033,80 @@ export class ExcelDriver implements Driver {
10331033
// ========== Helper Methods ==========
10341034

10351035
/**
1036-
* Convert FilterNode from QueryAST to legacy filter format.
1036+
* Convert FilterCondition (MongoDB-like format) to legacy array format.
1037+
* This allows the excel driver to use its existing filter evaluation logic.
10371038
*
1038-
* @param node - The FilterNode to convert
1039+
* @param condition - FilterCondition object or legacy array
10391040
* @returns Legacy filter array format
10401041
*/
1041-
private convertFilterNodeToLegacy(node?: FilterNode): any {
1042-
if (!node) return undefined;
1043-
1044-
switch (node.type) {
1045-
case 'comparison':
1046-
// Convert comparison node to [field, operator, value] format
1047-
const operator = node.operator || '=';
1048-
return [[node.field, operator, node.value]];
1049-
1050-
case 'and':
1051-
// Convert AND node to array with 'and' separator
1052-
if (!node.children || node.children.length === 0) return undefined;
1053-
const andResults: any[] = [];
1054-
for (const child of node.children) {
1055-
const converted = this.convertFilterNodeToLegacy(child);
1056-
if (converted) {
1057-
if (andResults.length > 0) {
1058-
andResults.push('and');
1042+
private convertFilterConditionToArray(condition?: any): any[] | undefined {
1043+
if (!condition) return undefined;
1044+
1045+
// If already an array, return as-is
1046+
if (Array.isArray(condition)) {
1047+
return condition;
1048+
}
1049+
1050+
// If it's an object (FilterCondition), convert to array format
1051+
// This is a simplified conversion - a full implementation would need to handle all operators
1052+
const result: any[] = [];
1053+
1054+
for (const [key, value] of Object.entries(condition)) {
1055+
if (key === '$and' && Array.isArray(value)) {
1056+
// Handle $and: [cond1, cond2, ...]
1057+
for (let i = 0; i < value.length; i++) {
1058+
const converted = this.convertFilterConditionToArray(value[i]);
1059+
if (converted && converted.length > 0) {
1060+
if (result.length > 0) {
1061+
result.push('and');
10591062
}
1060-
andResults.push(...(Array.isArray(converted) ? converted : [converted]));
1063+
result.push(...converted);
10611064
}
10621065
}
1063-
return andResults.length > 0 ? andResults : undefined;
1064-
1065-
case 'or':
1066-
// Convert OR node to array with 'or' separator
1067-
if (!node.children || node.children.length === 0) return undefined;
1068-
const orResults: any[] = [];
1069-
for (const child of node.children) {
1070-
const converted = this.convertFilterNodeToLegacy(child);
1071-
if (converted) {
1072-
if (orResults.length > 0) {
1073-
orResults.push('or');
1066+
} else if (key === '$or' && Array.isArray(value)) {
1067+
// Handle $or: [cond1, cond2, ...]
1068+
for (let i = 0; i < value.length; i++) {
1069+
const converted = this.convertFilterConditionToArray(value[i]);
1070+
if (converted && converted.length > 0) {
1071+
if (result.length > 0) {
1072+
result.push('or');
10741073
}
1075-
orResults.push(...(Array.isArray(converted) ? converted : [converted]));
1074+
result.push(...converted);
10761075
}
10771076
}
1078-
return orResults.length > 0 ? orResults : undefined;
1079-
1080-
case 'not':
1081-
// NOT is complex - we'll just process the first child for now
1082-
if (node.children && node.children.length > 0) {
1083-
return this.convertFilterNodeToLegacy(node.children[0]);
1077+
} else if (key === '$not' && typeof value === 'object') {
1078+
// Handle $not: { condition }
1079+
// Note: NOT is complex to represent in array format, so we skip it for now
1080+
const converted = this.convertFilterConditionToArray(value);
1081+
if (converted) {
1082+
result.push(...converted);
10841083
}
1085-
return undefined;
1086-
1087-
default:
1088-
return undefined;
1084+
} else if (typeof value === 'object' && value !== null) {
1085+
// Handle field-level conditions like { field: { $eq: value } }
1086+
const field = key;
1087+
for (const [operator, operandValue] of Object.entries(value)) {
1088+
let op: string;
1089+
switch (operator) {
1090+
case '$eq': op = '='; break;
1091+
case '$ne': op = '!='; break;
1092+
case '$gt': op = '>'; break;
1093+
case '$gte': op = '>='; break;
1094+
case '$lt': op = '<'; break;
1095+
case '$lte': op = '<='; break;
1096+
case '$in': op = 'in'; break;
1097+
case '$nin': op = 'nin'; break;
1098+
case '$regex': op = 'like'; break;
1099+
default: op = '=';
1100+
}
1101+
result.push([field, op, operandValue]);
1102+
}
1103+
} else {
1104+
// Handle simple equality: { field: value }
1105+
result.push([key, '=', value]);
1106+
}
10891107
}
1108+
1109+
return result.length > 0 ? result : undefined;
10901110
}
10911111

10921112
/**

packages/drivers/excel/test/index.test.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -582,15 +582,12 @@ describe('ExcelDriver', () => {
582582
const result = await driver.executeQuery({
583583
object: TEST_OBJECT,
584584
fields: ['name', 'age'],
585-
filters: {
586-
type: 'comparison',
587-
field: 'age',
588-
operator: '>',
589-
value: 25
585+
where: {
586+
age: { $gt: 25 }
590587
},
591-
sort: [{ field: 'age', order: 'asc' }],
592-
top: 10,
593-
skip: 0
588+
orderBy: [{ field: 'age', order: 'asc' }],
589+
limit: 10,
590+
offset: 0
594591
});
595592

596593
expect(result.value).toHaveLength(2);
@@ -606,11 +603,10 @@ describe('ExcelDriver', () => {
606603

607604
const result = await driver.executeQuery({
608605
object: TEST_OBJECT,
609-
filters: {
610-
type: 'and',
611-
children: [
612-
{ type: 'comparison', field: 'age', operator: '>', value: 25 },
613-
{ type: 'comparison', field: 'city', operator: '=', value: 'NYC' }
606+
where: {
607+
$and: [
608+
{ age: { $gt: 25 } },
609+
{ city: { $eq: 'NYC' } }
614610
]
615611
}
616612
});
@@ -626,9 +622,9 @@ describe('ExcelDriver', () => {
626622

627623
const result = await driver.executeQuery({
628624
object: TEST_OBJECT,
629-
sort: [{ field: 'name', order: 'asc' }],
630-
skip: 1,
631-
top: 1
625+
orderBy: [{ field: 'name', order: 'asc' }],
626+
offset: 1,
627+
limit: 1
632628
});
633629

634630
expect(result.value).toHaveLength(1);

packages/drivers/fs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"dependencies": {
2828
"@objectql/types": "workspace:*",
29-
"@objectstack/spec": "^0.3.1"
29+
"@objectstack/spec": "^0.3.3"
3030
},
3131
"devDependencies": {
3232
"@types/jest": "^29.0.0",

packages/drivers/fs/src/index.ts

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Data, System } from '@objectstack/spec';
1+
import { Data, Driver as DriverSpec } from '@objectstack/spec';
22
type QueryAST = Data.QueryAST;
3-
type FilterNode = Data.FilterNode;
43
type SortNode = Data.SortNode;
5-
type DriverInterface = System.DriverInterface;
4+
type DriverInterface = DriverSpec.DriverInterface;
65
/**
76
* ObjectQL
87
* Copyright (c) 2026-present ObjectStack Inc.
@@ -620,12 +619,13 @@ export class FileSystemDriver implements Driver {
620619
const objectName = ast.object || '';
621620

622621
// Convert QueryAST to legacy query format
622+
// Note: Convert FilterCondition (MongoDB-like) to array format for fs driver
623623
const legacyQuery: any = {
624624
fields: ast.fields,
625-
filters: this.convertFilterNodeToLegacy(ast.filters),
626-
sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
627-
limit: ast.top,
628-
skip: ast.skip,
625+
filters: this.convertFilterConditionToArray(ast.where),
626+
sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]),
627+
limit: ast.limit,
628+
skip: ast.offset,
629629
};
630630

631631
// Use existing find method
@@ -754,60 +754,80 @@ export class FileSystemDriver implements Driver {
754754
// ========== Helper Methods ==========
755755

756756
/**
757-
* Convert FilterNode from QueryAST to legacy filter format.
757+
* Convert FilterCondition (MongoDB-like format) to legacy array format.
758+
* This allows the fs driver to use its existing filter evaluation logic.
758759
*
759-
* @param node - The FilterNode to convert
760+
* @param condition - FilterCondition object or legacy array
760761
* @returns Legacy filter array format
761762
*/
762-
private convertFilterNodeToLegacy(node?: FilterNode): any {
763-
if (!node) return undefined;
763+
private convertFilterConditionToArray(condition?: any): any[] | undefined {
764+
if (!condition) return undefined;
764765

765-
switch (node.type) {
766-
case 'comparison':
767-
// Convert comparison node to [field, operator, value] format
768-
const operator = node.operator || '=';
769-
return [[node.field, operator, node.value]];
770-
771-
case 'and':
772-
// Convert AND node to array with 'and' separator
773-
if (!node.children || node.children.length === 0) return undefined;
774-
const andResults: any[] = [];
775-
for (const child of node.children) {
776-
const converted = this.convertFilterNodeToLegacy(child);
777-
if (converted) {
778-
if (andResults.length > 0) {
779-
andResults.push('and');
766+
// If already an array, return as-is
767+
if (Array.isArray(condition)) {
768+
return condition;
769+
}
770+
771+
// If it's an object (FilterCondition), convert to array format
772+
// This is a simplified conversion - a full implementation would need to handle all operators
773+
const result: any[] = [];
774+
775+
for (const [key, value] of Object.entries(condition)) {
776+
if (key === '$and' && Array.isArray(value)) {
777+
// Handle $and: [cond1, cond2, ...]
778+
for (let i = 0; i < value.length; i++) {
779+
const converted = this.convertFilterConditionToArray(value[i]);
780+
if (converted && converted.length > 0) {
781+
if (result.length > 0) {
782+
result.push('and');
780783
}
781-
andResults.push(...(Array.isArray(converted) ? converted : [converted]));
784+
result.push(...converted);
782785
}
783786
}
784-
return andResults.length > 0 ? andResults : undefined;
785-
786-
case 'or':
787-
// Convert OR node to array with 'or' separator
788-
if (!node.children || node.children.length === 0) return undefined;
789-
const orResults: any[] = [];
790-
for (const child of node.children) {
791-
const converted = this.convertFilterNodeToLegacy(child);
792-
if (converted) {
793-
if (orResults.length > 0) {
794-
orResults.push('or');
787+
} else if (key === '$or' && Array.isArray(value)) {
788+
// Handle $or: [cond1, cond2, ...]
789+
for (let i = 0; i < value.length; i++) {
790+
const converted = this.convertFilterConditionToArray(value[i]);
791+
if (converted && converted.length > 0) {
792+
if (result.length > 0) {
793+
result.push('or');
795794
}
796-
orResults.push(...(Array.isArray(converted) ? converted : [converted]));
795+
result.push(...converted);
797796
}
798797
}
799-
return orResults.length > 0 ? orResults : undefined;
800-
801-
case 'not':
802-
// NOT is complex - we'll just process the first child for now
803-
if (node.children && node.children.length > 0) {
804-
return this.convertFilterNodeToLegacy(node.children[0]);
798+
} else if (key === '$not' && typeof value === 'object') {
799+
// Handle $not: { condition }
800+
// Note: NOT is complex to represent in array format, so we skip it for now
801+
const converted = this.convertFilterConditionToArray(value);
802+
if (converted) {
803+
result.push(...converted);
805804
}
806-
return undefined;
807-
808-
default:
809-
return undefined;
805+
} else if (typeof value === 'object' && value !== null) {
806+
// Handle field-level conditions like { field: { $eq: value } }
807+
const field = key;
808+
for (const [operator, operandValue] of Object.entries(value)) {
809+
let op: string;
810+
switch (operator) {
811+
case '$eq': op = '='; break;
812+
case '$ne': op = '!='; break;
813+
case '$gt': op = '>'; break;
814+
case '$gte': op = '>='; break;
815+
case '$lt': op = '<'; break;
816+
case '$lte': op = '<='; break;
817+
case '$in': op = 'in'; break;
818+
case '$nin': op = 'nin'; break;
819+
case '$regex': op = 'like'; break;
820+
default: op = '=';
821+
}
822+
result.push([field, op, operandValue]);
823+
}
824+
} else {
825+
// Handle simple equality: { field: value }
826+
result.push([key, '=', value]);
827+
}
810828
}
829+
830+
return result.length > 0 ? result : undefined;
811831
}
812832

813833
/**

0 commit comments

Comments
 (0)