@objectql/driver-excel 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Data, System } from '@objectstack/spec';
1
+ import { Data, Driver as DriverSpec } from '@objectstack/spec';
2
2
  type QueryAST = Data.QueryAST;
3
- type FilterNode = Data.FilterNode;
4
3
  type SortNode = Data.SortNode;
5
- type DriverInterface = System.DriverInterface;
4
+ type DriverInterface = DriverSpec.DriverInterface;
6
5
  /**
7
6
  * ObjectQL
8
7
  * Copyright (c) 2026-present ObjectStack Inc.
@@ -617,20 +616,39 @@ export class ExcelDriver implements Driver {
617
616
  // Deep copy to avoid mutations
618
617
  results = results.map(r => ({ ...r }));
619
618
 
620
- // Apply filters
619
+ // Apply filters from where clause (QueryAST format)
620
+ if (normalizedQuery.where) {
621
+ const legacyFilters = this.convertFilterConditionToArray(normalizedQuery.where);
622
+ if (legacyFilters) {
623
+ results = this.applyFilters(results, legacyFilters);
624
+ }
625
+ }
626
+
627
+ // Apply filters from legacy filters clause
621
628
  if (normalizedQuery.filters) {
622
629
  results = this.applyFilters(results, normalizedQuery.filters);
623
630
  }
624
631
 
625
- // Apply sorting
632
+ // Apply sorting from orderBy (QueryAST format)
633
+ if (normalizedQuery.orderBy && Array.isArray(normalizedQuery.orderBy)) {
634
+ results = this.applySort(results, normalizedQuery.orderBy);
635
+ }
636
+
637
+ // Apply sorting from legacy sort clause
626
638
  if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
627
639
  results = this.applySort(results, normalizedQuery.sort);
628
640
  }
629
641
 
630
- // Apply pagination
642
+ // Apply pagination with offset (QueryAST format)
643
+ if (normalizedQuery.offset) {
644
+ results = results.slice(normalizedQuery.offset);
645
+ }
646
+
647
+ // Apply pagination with skip (legacy format)
631
648
  if (normalizedQuery.skip) {
632
649
  results = results.slice(normalizedQuery.skip);
633
650
  }
651
+
634
652
  if (normalizedQuery.limit) {
635
653
  results = results.slice(0, normalizedQuery.limit);
636
654
  }
@@ -764,9 +782,15 @@ export class ExcelDriver implements Driver {
764
782
  async count(objectName: string, filters: any, options?: any): Promise<number> {
765
783
  const records = this.data.get(objectName) || [];
766
784
 
767
- // Extract actual filters from query object if needed
785
+ // Extract actual filters from query object
768
786
  let actualFilters = filters;
769
- if (filters && !Array.isArray(filters) && filters.filters) {
787
+
788
+ // Support QueryAST format with 'where' clause
789
+ if (filters && filters.where) {
790
+ actualFilters = this.convertFilterConditionToArray(filters.where);
791
+ }
792
+ // Support legacy format with 'filters' clause
793
+ else if (filters && !Array.isArray(filters) && filters.filters) {
770
794
  actualFilters = filters.filters;
771
795
  }
772
796
 
@@ -899,12 +923,13 @@ export class ExcelDriver implements Driver {
899
923
  const objectName = ast.object || '';
900
924
 
901
925
  // Convert QueryAST to legacy query format
926
+ // Note: Convert FilterCondition (MongoDB-like) to array format for excel driver
902
927
  const legacyQuery: any = {
903
928
  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,
929
+ filters: this.convertFilterConditionToArray(ast.where),
930
+ sort: ast.orderBy?.map((s: SortNode) => [s.field, s.order]),
931
+ limit: ast.limit,
932
+ skip: ast.offset,
908
933
  };
909
934
 
910
935
  // Use existing find method
@@ -1033,68 +1058,89 @@ export class ExcelDriver implements Driver {
1033
1058
  // ========== Helper Methods ==========
1034
1059
 
1035
1060
  /**
1036
- * Convert FilterNode from QueryAST to legacy filter format.
1061
+ * Convert FilterCondition (MongoDB-like format) to legacy array format.
1062
+ * This allows the excel driver to use its existing filter evaluation logic.
1037
1063
  *
1038
- * @param node - The FilterNode to convert
1064
+ * @param condition - FilterCondition object or legacy array
1039
1065
  * @returns Legacy filter array format
1040
1066
  */
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');
1067
+ private convertFilterConditionToArray(condition?: any): any[] | undefined {
1068
+ if (!condition) return undefined;
1069
+
1070
+ // If already an array, return as-is
1071
+ if (Array.isArray(condition)) {
1072
+ return condition;
1073
+ }
1074
+
1075
+ // If it's an object (FilterCondition), convert to array format
1076
+ // This is a simplified conversion - a full implementation would need to handle all operators
1077
+ const result: any[] = [];
1078
+
1079
+ for (const [key, value] of Object.entries(condition)) {
1080
+ if (key === '$and' && Array.isArray(value)) {
1081
+ // Handle $and: [cond1, cond2, ...]
1082
+ for (let i = 0; i < value.length; i++) {
1083
+ const converted = this.convertFilterConditionToArray(value[i]);
1084
+ if (converted && converted.length > 0) {
1085
+ if (result.length > 0) {
1086
+ result.push('and');
1059
1087
  }
1060
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
1088
+ result.push(...converted);
1061
1089
  }
1062
1090
  }
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');
1091
+ } else if (key === '$or' && Array.isArray(value)) {
1092
+ // Handle $or: [cond1, cond2, ...]
1093
+ for (let i = 0; i < value.length; i++) {
1094
+ const converted = this.convertFilterConditionToArray(value[i]);
1095
+ if (converted && converted.length > 0) {
1096
+ if (result.length > 0) {
1097
+ result.push('or');
1074
1098
  }
1075
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
1099
+ result.push(...converted);
1076
1100
  }
1077
1101
  }
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]);
1102
+ } else if (key === '$not' && typeof value === 'object') {
1103
+ // Handle $not: { condition }
1104
+ // Note: NOT is complex to represent in array format, so we skip it for now
1105
+ const converted = this.convertFilterConditionToArray(value);
1106
+ if (converted) {
1107
+ result.push(...converted);
1084
1108
  }
1085
- return undefined;
1086
-
1087
- default:
1088
- return undefined;
1109
+ } else if (typeof value === 'object' && value !== null) {
1110
+ // Handle field-level conditions like { field: { $eq: value } }
1111
+ const field = key;
1112
+ for (const [operator, operandValue] of Object.entries(value)) {
1113
+ let op: string;
1114
+ switch (operator) {
1115
+ case '$eq': op = '='; break;
1116
+ case '$ne': op = '!='; break;
1117
+ case '$gt': op = '>'; break;
1118
+ case '$gte': op = '>='; break;
1119
+ case '$lt': op = '<'; break;
1120
+ case '$lte': op = '<='; break;
1121
+ case '$in': op = 'in'; break;
1122
+ case '$nin': op = 'nin'; break;
1123
+ case '$regex': op = 'like'; break;
1124
+ default: op = '=';
1125
+ }
1126
+ result.push([field, op, operandValue]);
1127
+ }
1128
+ } else {
1129
+ // Handle simple equality: { field: value }
1130
+ result.push([key, '=', value]);
1131
+ }
1089
1132
  }
1133
+
1134
+ return result.length > 0 ? result : undefined;
1090
1135
  }
1091
1136
 
1092
1137
  /**
1093
1138
  * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
1094
1139
  * This ensures backward compatibility while supporting the new @objectstack/spec interface.
1095
1140
  *
1096
- * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
1097
- * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
1141
+ * QueryAST format uses 'top' for limit, 'offset' for skip, and 'orderBy' for sort.
1142
+ * QueryAST uses 'where' for filters with MongoDB-like operators.
1143
+ * UnifiedQuery uses 'limit', 'skip', 'sort', and 'filters'.
1098
1144
  */
1099
1145
  private normalizeQuery(query: any): any {
1100
1146
  if (!query) return {};
@@ -1106,7 +1152,21 @@ export class ExcelDriver implements Driver {
1106
1152
  normalized.limit = normalized.top;
1107
1153
  }
1108
1154
 
1109
- // Normalize sort format
1155
+ // Normalize offset/skip (both are preserved for backward compatibility)
1156
+ // The find() method will check both
1157
+
1158
+ // Normalize orderBy to sort format if orderBy is present
1159
+ if (normalized.orderBy && Array.isArray(normalized.orderBy)) {
1160
+ // Check if it's already in the legacy array format [field, order]
1161
+ const firstSort = normalized.orderBy[0];
1162
+ if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort) && firstSort.field) {
1163
+ // It's in QueryAST format {field, order}, keep as-is
1164
+ // The find() method will handle it
1165
+ normalized.orderBy = normalized.orderBy;
1166
+ }
1167
+ }
1168
+
1169
+ // Normalize sort format (legacy)
1110
1170
  if (normalized.sort && Array.isArray(normalized.sort)) {
1111
1171
  // Check if it's already in the array format [field, order]
1112
1172
  const firstSort = normalized.sort[0];
@@ -233,7 +233,7 @@ describe('ExcelDriver', () => {
233
233
 
234
234
  it('should filter records with equality operator', async () => {
235
235
  const results = await driver.find(TEST_OBJECT, {
236
- filters: [['role', '=', 'user']]
236
+ where: { role: { $eq: 'user' } }
237
237
  });
238
238
  expect(results.length).toBe(2);
239
239
  expect(results.every(r => r.role === 'user')).toBe(true);
@@ -241,7 +241,7 @@ describe('ExcelDriver', () => {
241
241
 
242
242
  it('should filter records with greater than operator', async () => {
243
243
  const results = await driver.find(TEST_OBJECT, {
244
- filters: [['age', '>', 25]]
244
+ where: { age: { $gt: 25 } }
245
245
  });
246
246
  expect(results.length).toBe(2);
247
247
  expect(results.every(r => r.age > 25)).toBe(true);
@@ -249,25 +249,26 @@ describe('ExcelDriver', () => {
249
249
 
250
250
  it('should filter records with contains operator', async () => {
251
251
  const results = await driver.find(TEST_OBJECT, {
252
- filters: [['name', 'contains', 'li']]
252
+ where: { name: { $regex: 'li' } }
253
253
  });
254
254
  expect(results.length).toBe(2); // Alice and Charlie
255
255
  });
256
256
 
257
257
  it('should support OR filters', async () => {
258
258
  const results = await driver.find(TEST_OBJECT, {
259
- filters: [
260
- ['name', '=', 'Alice'],
261
- 'or',
262
- ['name', '=', 'Bob']
263
- ]
259
+ where: {
260
+ $or: [
261
+ { name: { $eq: 'Alice' } },
262
+ { name: { $eq: 'Bob' } }
263
+ ]
264
+ }
264
265
  });
265
266
  expect(results.length).toBe(2);
266
267
  });
267
268
 
268
269
  it('should sort records ascending', async () => {
269
270
  const results = await driver.find(TEST_OBJECT, {
270
- sort: [['age', 'asc']]
271
+ orderBy: [{ field: 'age', order: 'asc' }]
271
272
  });
272
273
  expect(results[0].age).toBe(25);
273
274
  expect(results[1].age).toBe(30);
@@ -276,7 +277,7 @@ describe('ExcelDriver', () => {
276
277
 
277
278
  it('should sort records descending', async () => {
278
279
  const results = await driver.find(TEST_OBJECT, {
279
- sort: [['age', 'desc']]
280
+ orderBy: [{ field: 'age', order: 'desc' }]
280
281
  });
281
282
  expect(results[0].age).toBe(35);
282
283
  expect(results[1].age).toBe(30);
@@ -292,7 +293,7 @@ describe('ExcelDriver', () => {
292
293
 
293
294
  it('should support pagination with skip', async () => {
294
295
  const results = await driver.find(TEST_OBJECT, {
295
- skip: 1,
296
+ offset: 1,
296
297
  limit: 2
297
298
  });
298
299
  expect(results.length).toBe(2);
@@ -315,7 +316,7 @@ describe('ExcelDriver', () => {
315
316
 
316
317
  it('should count filtered records', async () => {
317
318
  const count = await driver.count(TEST_OBJECT, {
318
- filters: [['role', '=', 'user']]
319
+ where: { role: { $eq: 'user' } }
319
320
  });
320
321
  expect(count).toBe(2);
321
322
  });
@@ -354,7 +355,7 @@ describe('ExcelDriver', () => {
354
355
  expect(result.modifiedCount).toBe(2);
355
356
 
356
357
  const users = await driver.find(TEST_OBJECT, {
357
- filters: [['role', '=', 'member']]
358
+ where: { role: { $eq: 'member' } }
358
359
  });
359
360
  expect(users).toHaveLength(2);
360
361
  });
@@ -465,7 +466,7 @@ describe('ExcelDriver', () => {
465
466
 
466
467
  it('should handle filters on empty data', async () => {
467
468
  const results = await driver.find(TEST_OBJECT, {
468
- filters: [['name', '=', 'Alice']]
469
+ where: { name: { $eq: 'Alice' } }
469
470
  });
470
471
  expect(results).toEqual([]);
471
472
  });
@@ -582,15 +583,12 @@ describe('ExcelDriver', () => {
582
583
  const result = await driver.executeQuery({
583
584
  object: TEST_OBJECT,
584
585
  fields: ['name', 'age'],
585
- filters: {
586
- type: 'comparison',
587
- field: 'age',
588
- operator: '>',
589
- value: 25
586
+ where: {
587
+ age: { $gt: 25 }
590
588
  },
591
- sort: [{ field: 'age', order: 'asc' }],
592
- top: 10,
593
- skip: 0
589
+ orderBy: [{ field: 'age', order: 'asc' }],
590
+ limit: 10,
591
+ offset: 0
594
592
  });
595
593
 
596
594
  expect(result.value).toHaveLength(2);
@@ -606,11 +604,10 @@ describe('ExcelDriver', () => {
606
604
 
607
605
  const result = await driver.executeQuery({
608
606
  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' }
607
+ where: {
608
+ $and: [
609
+ { age: { $gt: 25 } },
610
+ { city: { $eq: 'NYC' } }
614
611
  ]
615
612
  }
616
613
  });
@@ -626,9 +623,9 @@ describe('ExcelDriver', () => {
626
623
 
627
624
  const result = await driver.executeQuery({
628
625
  object: TEST_OBJECT,
629
- sort: [{ field: 'name', order: 'asc' }],
630
- skip: 1,
631
- top: 1
626
+ orderBy: [{ field: 'name', order: 'asc' }],
627
+ offset: 1,
628
+ limit: 1
632
629
  });
633
630
 
634
631
  expect(result.value).toHaveLength(1);