@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/CHANGELOG.md +14 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +127 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +118 -58
- package/test/index.test.ts +27 -30
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Data,
|
|
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 =
|
|
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
|
|
785
|
+
// Extract actual filters from query object
|
|
768
786
|
let actualFilters = filters;
|
|
769
|
-
|
|
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.
|
|
905
|
-
sort: ast.
|
|
906
|
-
limit: ast.
|
|
907
|
-
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,
|
|
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
|
|
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
|
|
1064
|
+
* @param condition - FilterCondition object or legacy array
|
|
1039
1065
|
* @returns Legacy filter array format
|
|
1040
1066
|
*/
|
|
1041
|
-
private
|
|
1042
|
-
if (!
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1088
|
+
result.push(...converted);
|
|
1061
1089
|
}
|
|
1062
1090
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1099
|
+
result.push(...converted);
|
|
1076
1100
|
}
|
|
1077
1101
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (
|
|
1083
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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,
|
|
1097
|
-
* 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'.
|
|
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
|
|
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];
|
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);
|