@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/CHANGELOG.md +31 -3
- package/dist/index.d.ts +17 -7
- package/dist/index.js +141 -60
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +128 -58
- package/test/index.test.ts +27 -30
- package/tsconfig.tsbuildinfo +1 -1
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
|
|
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
|
|
785
|
+
// Extract actual filters from query object
|
|
758
786
|
let actualFilters = filters;
|
|
759
|
-
|
|
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.
|
|
895
|
-
sort: ast.
|
|
896
|
-
limit: ast.
|
|
897
|
-
skip: ast.
|
|
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
|
|
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
|
|
1064
|
+
* @param condition - FilterCondition object or legacy array
|
|
1029
1065
|
* @returns Legacy filter array format
|
|
1030
1066
|
*/
|
|
1031
|
-
private
|
|
1032
|
-
if (!
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1088
|
+
result.push(...converted);
|
|
1051
1089
|
}
|
|
1052
1090
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1099
|
+
result.push(...converted);
|
|
1066
1100
|
}
|
|
1067
1101
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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,
|
|
1087
|
-
* QueryAST
|
|
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
|
|
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];
|
package/test/index.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
[
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
field: 'age',
|
|
588
|
-
operator: '>',
|
|
589
|
-
value: 25
|
|
586
|
+
where: {
|
|
587
|
+
age: { $gt: 25 }
|
|
590
588
|
},
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
{
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
626
|
+
orderBy: [{ field: 'name', order: 'asc' }],
|
|
627
|
+
offset: 1,
|
|
628
|
+
limit: 1
|
|
632
629
|
});
|
|
633
630
|
|
|
634
631
|
expect(result.value).toHaveLength(1);
|