@objectql/driver-excel 4.0.0 → 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,3 +1,7 @@
1
+ import { Data, Driver as DriverSpec } from '@objectstack/spec';
2
+ type QueryAST = Data.QueryAST;
3
+ type SortNode = Data.SortNode;
4
+ type DriverInterface = DriverSpec.DriverInterface;
1
5
  /**
2
6
  * ObjectQL
3
7
  * Copyright (c) 2026-present ObjectStack Inc.
@@ -30,7 +34,6 @@
30
34
  */
31
35
 
32
36
  import { Driver, ObjectQLError } from '@objectql/types';
33
- import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
34
37
  import * as ExcelJS from 'exceljs';
35
38
  import * as fs from 'fs';
36
39
  import * as path from 'path';
@@ -102,7 +105,7 @@ export interface ExcelDriverConfig {
102
105
  * the standard DriverInterface from @objectstack/spec for compatibility
103
106
  * with the new kernel-based plugin system.
104
107
  */
105
- export class ExcelDriver implements Driver, DriverInterface {
108
+ export class ExcelDriver implements Driver {
106
109
  // Driver metadata (ObjectStack-compatible)
107
110
  public readonly name = 'ExcelDriver';
108
111
  public readonly version = '4.0.0';
@@ -111,7 +114,13 @@ export class ExcelDriver implements Driver, DriverInterface {
111
114
  joins: false,
112
115
  fullTextSearch: false,
113
116
  jsonFields: true,
114
- arrayFields: true
117
+ arrayFields: true,
118
+ queryFilters: true,
119
+ queryAggregations: false,
120
+ querySorting: true,
121
+ queryPagination: true,
122
+ queryWindowFunctions: false,
123
+ querySubqueries: false
115
124
  };
116
125
 
117
126
  private config: ExcelDriverConfig;
@@ -607,20 +616,39 @@ export class ExcelDriver implements Driver, DriverInterface {
607
616
  // Deep copy to avoid mutations
608
617
  results = results.map(r => ({ ...r }));
609
618
 
610
- // 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
611
628
  if (normalizedQuery.filters) {
612
629
  results = this.applyFilters(results, normalizedQuery.filters);
613
630
  }
614
631
 
615
- // 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
616
638
  if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
617
639
  results = this.applySort(results, normalizedQuery.sort);
618
640
  }
619
641
 
620
- // 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)
621
648
  if (normalizedQuery.skip) {
622
649
  results = results.slice(normalizedQuery.skip);
623
650
  }
651
+
624
652
  if (normalizedQuery.limit) {
625
653
  results = results.slice(0, normalizedQuery.limit);
626
654
  }
@@ -754,9 +782,15 @@ export class ExcelDriver implements Driver, DriverInterface {
754
782
  async count(objectName: string, filters: any, options?: any): Promise<number> {
755
783
  const records = this.data.get(objectName) || [];
756
784
 
757
- // Extract actual filters from query object if needed
785
+ // Extract actual filters from query object
758
786
  let actualFilters = filters;
759
- 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) {
760
794
  actualFilters = filters.filters;
761
795
  }
762
796
 
@@ -889,12 +923,13 @@ export class ExcelDriver implements Driver, DriverInterface {
889
923
  const objectName = ast.object || '';
890
924
 
891
925
  // Convert QueryAST to legacy query format
926
+ // Note: Convert FilterCondition (MongoDB-like) to array format for excel driver
892
927
  const legacyQuery: any = {
893
928
  fields: ast.fields,
894
- filters: this.convertFilterNodeToLegacy(ast.filters),
895
- sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
896
- limit: ast.top,
897
- 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,
898
933
  };
899
934
 
900
935
  // Use existing find method
@@ -1023,68 +1058,89 @@ export class ExcelDriver implements Driver, DriverInterface {
1023
1058
  // ========== Helper Methods ==========
1024
1059
 
1025
1060
  /**
1026
- * 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.
1027
1063
  *
1028
- * @param node - The FilterNode to convert
1064
+ * @param condition - FilterCondition object or legacy array
1029
1065
  * @returns Legacy filter array format
1030
1066
  */
1031
- private convertFilterNodeToLegacy(node?: FilterNode): any {
1032
- if (!node) return undefined;
1033
-
1034
- switch (node.type) {
1035
- case 'comparison':
1036
- // Convert comparison node to [field, operator, value] format
1037
- const operator = node.operator || '=';
1038
- return [[node.field, operator, node.value]];
1039
-
1040
- case 'and':
1041
- // Convert AND node to array with 'and' separator
1042
- if (!node.children || node.children.length === 0) return undefined;
1043
- const andResults: any[] = [];
1044
- for (const child of node.children) {
1045
- const converted = this.convertFilterNodeToLegacy(child);
1046
- if (converted) {
1047
- if (andResults.length > 0) {
1048
- 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');
1049
1087
  }
1050
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
1088
+ result.push(...converted);
1051
1089
  }
1052
1090
  }
1053
- return andResults.length > 0 ? andResults : undefined;
1054
-
1055
- case 'or':
1056
- // Convert OR node to array with 'or' separator
1057
- if (!node.children || node.children.length === 0) return undefined;
1058
- const orResults: any[] = [];
1059
- for (const child of node.children) {
1060
- const converted = this.convertFilterNodeToLegacy(child);
1061
- if (converted) {
1062
- if (orResults.length > 0) {
1063
- 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');
1064
1098
  }
1065
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
1099
+ result.push(...converted);
1066
1100
  }
1067
1101
  }
1068
- return orResults.length > 0 ? orResults : undefined;
1069
-
1070
- case 'not':
1071
- // NOT is complex - we'll just process the first child for now
1072
- if (node.children && node.children.length > 0) {
1073
- 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);
1074
1108
  }
1075
- return undefined;
1076
-
1077
- default:
1078
- 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
+ }
1079
1132
  }
1133
+
1134
+ return result.length > 0 ? result : undefined;
1080
1135
  }
1081
1136
 
1082
1137
  /**
1083
1138
  * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
1084
1139
  * This ensures backward compatibility while supporting the new @objectstack/spec interface.
1085
1140
  *
1086
- * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
1087
- * 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'.
1088
1144
  */
1089
1145
  private normalizeQuery(query: any): any {
1090
1146
  if (!query) return {};
@@ -1096,7 +1152,21 @@ export class ExcelDriver implements Driver, DriverInterface {
1096
1152
  normalized.limit = normalized.top;
1097
1153
  }
1098
1154
 
1099
- // 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)
1100
1170
  if (normalized.sort && Array.isArray(normalized.sort)) {
1101
1171
  // Check if it's already in the array format [field, order]
1102
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);