@objectql/driver-excel 3.0.1 → 4.0.0
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/dist/index.d.ts +105 -1
- package/dist/index.js +311 -29
- package/dist/index.js.map +1 -1
- package/jest.config.js +8 -0
- package/package.json +2 -1
- package/src/index.ts +337 -11
- package/test/index.test.ts +184 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectql/driver-excel",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Excel file driver for ObjectQL - Read/write data from Excel files (.xlsx) with flexible storage modes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"objectql",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@objectstack/spec": "^0.2.0",
|
|
19
20
|
"exceljs": "^4.4.0",
|
|
20
21
|
"@objectql/types": "3.0.1"
|
|
21
22
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Excel Driver for ObjectQL (Production-Ready)
|
|
3
11
|
*
|
|
@@ -22,10 +30,35 @@
|
|
|
22
30
|
*/
|
|
23
31
|
|
|
24
32
|
import { Driver, ObjectQLError } from '@objectql/types';
|
|
33
|
+
import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
|
|
25
34
|
import * as ExcelJS from 'exceljs';
|
|
26
35
|
import * as fs from 'fs';
|
|
27
36
|
import * as path from 'path';
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Command interface for executeCommand method
|
|
40
|
+
*/
|
|
41
|
+
export interface Command {
|
|
42
|
+
type: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
|
|
43
|
+
object: string;
|
|
44
|
+
data?: any;
|
|
45
|
+
id?: string | number;
|
|
46
|
+
ids?: Array<string | number>;
|
|
47
|
+
records?: any[];
|
|
48
|
+
updates?: Array<{id: string | number, data: any}>;
|
|
49
|
+
options?: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Command result interface
|
|
54
|
+
*/
|
|
55
|
+
export interface CommandResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
data?: any;
|
|
58
|
+
affected: number;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
29
62
|
/**
|
|
30
63
|
* File storage mode for the Excel driver.
|
|
31
64
|
*/
|
|
@@ -64,8 +97,23 @@ export interface ExcelDriverConfig {
|
|
|
64
97
|
* in a separate worksheet, with the first row containing column headers.
|
|
65
98
|
*
|
|
66
99
|
* Uses ExcelJS library for secure Excel file operations.
|
|
100
|
+
*
|
|
101
|
+
* Implements both the legacy Driver interface from @objectql/types and
|
|
102
|
+
* the standard DriverInterface from @objectstack/spec for compatibility
|
|
103
|
+
* with the new kernel-based plugin system.
|
|
67
104
|
*/
|
|
68
|
-
export class ExcelDriver implements Driver {
|
|
105
|
+
export class ExcelDriver implements Driver, DriverInterface {
|
|
106
|
+
// Driver metadata (ObjectStack-compatible)
|
|
107
|
+
public readonly name = 'ExcelDriver';
|
|
108
|
+
public readonly version = '4.0.0';
|
|
109
|
+
public readonly supports = {
|
|
110
|
+
transactions: false,
|
|
111
|
+
joins: false,
|
|
112
|
+
fullTextSearch: false,
|
|
113
|
+
jsonFields: true,
|
|
114
|
+
arrayFields: true
|
|
115
|
+
};
|
|
116
|
+
|
|
69
117
|
private config: ExcelDriverConfig;
|
|
70
118
|
private workbook!: ExcelJS.Workbook;
|
|
71
119
|
private workbooks: Map<string, ExcelJS.Workbook>; // For file-per-object mode
|
|
@@ -106,6 +154,46 @@ export class ExcelDriver implements Driver {
|
|
|
106
154
|
await this.loadWorkbook();
|
|
107
155
|
}
|
|
108
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Connect to the database (for DriverInterface compatibility)
|
|
159
|
+
* This calls init() to load the workbook.
|
|
160
|
+
*/
|
|
161
|
+
async connect(): Promise<void> {
|
|
162
|
+
await this.init();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check database connection health
|
|
167
|
+
*/
|
|
168
|
+
async checkHealth(): Promise<boolean> {
|
|
169
|
+
try {
|
|
170
|
+
if (this.fileStorageMode === 'single-file') {
|
|
171
|
+
// Check if file exists or can be created
|
|
172
|
+
if (!fs.existsSync(this.filePath)) {
|
|
173
|
+
if (!this.config.createIfMissing) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// Check if directory is writable
|
|
177
|
+
const dir = path.dirname(this.filePath);
|
|
178
|
+
if (!fs.existsSync(dir)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
} else {
|
|
184
|
+
// Check if directory exists or can be created
|
|
185
|
+
if (!fs.existsSync(this.filePath)) {
|
|
186
|
+
if (!this.config.createIfMissing) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
109
197
|
/**
|
|
110
198
|
* Factory method to create and initialize the driver.
|
|
111
199
|
*/
|
|
@@ -507,6 +595,8 @@ export class ExcelDriver implements Driver {
|
|
|
507
595
|
* Find multiple records matching the query criteria.
|
|
508
596
|
*/
|
|
509
597
|
async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
|
|
598
|
+
// Normalize query to support both legacy and QueryAST formats
|
|
599
|
+
const normalizedQuery = this.normalizeQuery(query);
|
|
510
600
|
let results = this.data.get(objectName) || [];
|
|
511
601
|
|
|
512
602
|
// Return empty array if no data
|
|
@@ -518,26 +608,26 @@ export class ExcelDriver implements Driver {
|
|
|
518
608
|
results = results.map(r => ({ ...r }));
|
|
519
609
|
|
|
520
610
|
// Apply filters
|
|
521
|
-
if (
|
|
522
|
-
results = this.applyFilters(results,
|
|
611
|
+
if (normalizedQuery.filters) {
|
|
612
|
+
results = this.applyFilters(results, normalizedQuery.filters);
|
|
523
613
|
}
|
|
524
614
|
|
|
525
615
|
// Apply sorting
|
|
526
|
-
if (
|
|
527
|
-
results = this.applySort(results,
|
|
616
|
+
if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
|
|
617
|
+
results = this.applySort(results, normalizedQuery.sort);
|
|
528
618
|
}
|
|
529
619
|
|
|
530
620
|
// Apply pagination
|
|
531
|
-
if (
|
|
532
|
-
results = results.slice(
|
|
621
|
+
if (normalizedQuery.skip) {
|
|
622
|
+
results = results.slice(normalizedQuery.skip);
|
|
533
623
|
}
|
|
534
|
-
if (
|
|
535
|
-
results = results.slice(0,
|
|
624
|
+
if (normalizedQuery.limit) {
|
|
625
|
+
results = results.slice(0, normalizedQuery.limit);
|
|
536
626
|
}
|
|
537
627
|
|
|
538
628
|
// Apply field projection
|
|
539
|
-
if (
|
|
540
|
-
results = results.map(doc => this.projectFields(doc,
|
|
629
|
+
if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
|
|
630
|
+
results = results.map(doc => this.projectFields(doc, normalizedQuery.fields));
|
|
541
631
|
}
|
|
542
632
|
|
|
543
633
|
return results;
|
|
@@ -784,8 +874,244 @@ export class ExcelDriver implements Driver {
|
|
|
784
874
|
}
|
|
785
875
|
}
|
|
786
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Execute a query using QueryAST (DriverInterface v4.0 method)
|
|
879
|
+
*
|
|
880
|
+
* This method handles all query operations using the standard QueryAST format
|
|
881
|
+
* from @objectstack/spec. It converts the AST to the legacy query format
|
|
882
|
+
* and delegates to the existing find() method.
|
|
883
|
+
*
|
|
884
|
+
* @param ast - The query AST to execute
|
|
885
|
+
* @param options - Optional execution options
|
|
886
|
+
* @returns Query results with value array and count
|
|
887
|
+
*/
|
|
888
|
+
async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
|
|
889
|
+
const objectName = ast.object || '';
|
|
890
|
+
|
|
891
|
+
// Convert QueryAST to legacy query format
|
|
892
|
+
const legacyQuery: any = {
|
|
893
|
+
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,
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// Use existing find method
|
|
901
|
+
const results = await this.find(objectName, legacyQuery, options);
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
value: results,
|
|
905
|
+
count: results.length
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Execute a command (DriverInterface v4.0 method)
|
|
911
|
+
*
|
|
912
|
+
* This method handles all mutation operations (create, update, delete)
|
|
913
|
+
* using a unified command interface.
|
|
914
|
+
*
|
|
915
|
+
* @param command - The command to execute
|
|
916
|
+
* @param options - Optional execution options
|
|
917
|
+
* @returns Command execution result
|
|
918
|
+
*/
|
|
919
|
+
async executeCommand(command: Command, options?: any): Promise<CommandResult> {
|
|
920
|
+
try {
|
|
921
|
+
const cmdOptions = { ...options, ...command.options };
|
|
922
|
+
|
|
923
|
+
switch (command.type) {
|
|
924
|
+
case 'create':
|
|
925
|
+
if (!command.data) {
|
|
926
|
+
throw new Error('Create command requires data');
|
|
927
|
+
}
|
|
928
|
+
const created = await this.create(command.object, command.data, cmdOptions);
|
|
929
|
+
return {
|
|
930
|
+
success: true,
|
|
931
|
+
data: created,
|
|
932
|
+
affected: 1
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
case 'update':
|
|
936
|
+
if (!command.id || !command.data) {
|
|
937
|
+
throw new Error('Update command requires id and data');
|
|
938
|
+
}
|
|
939
|
+
const updated = await this.update(command.object, command.id, command.data, cmdOptions);
|
|
940
|
+
return {
|
|
941
|
+
success: true,
|
|
942
|
+
data: updated,
|
|
943
|
+
affected: 1
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
case 'delete':
|
|
947
|
+
if (!command.id) {
|
|
948
|
+
throw new Error('Delete command requires id');
|
|
949
|
+
}
|
|
950
|
+
await this.delete(command.object, command.id, cmdOptions);
|
|
951
|
+
return {
|
|
952
|
+
success: true,
|
|
953
|
+
affected: 1
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
case 'bulkCreate':
|
|
957
|
+
if (!command.records || !Array.isArray(command.records)) {
|
|
958
|
+
throw new Error('BulkCreate command requires records array');
|
|
959
|
+
}
|
|
960
|
+
const bulkCreated = [];
|
|
961
|
+
for (const record of command.records) {
|
|
962
|
+
const created = await this.create(command.object, record, cmdOptions);
|
|
963
|
+
bulkCreated.push(created);
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
success: true,
|
|
967
|
+
data: bulkCreated,
|
|
968
|
+
affected: command.records.length
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
case 'bulkUpdate':
|
|
972
|
+
if (!command.updates || !Array.isArray(command.updates)) {
|
|
973
|
+
throw new Error('BulkUpdate command requires updates array');
|
|
974
|
+
}
|
|
975
|
+
const updateResults = [];
|
|
976
|
+
for (const update of command.updates) {
|
|
977
|
+
const result = await this.update(command.object, update.id, update.data, cmdOptions);
|
|
978
|
+
updateResults.push(result);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
success: true,
|
|
982
|
+
data: updateResults,
|
|
983
|
+
affected: command.updates.length
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
case 'bulkDelete':
|
|
987
|
+
if (!command.ids || !Array.isArray(command.ids)) {
|
|
988
|
+
throw new Error('BulkDelete command requires ids array');
|
|
989
|
+
}
|
|
990
|
+
let deleted = 0;
|
|
991
|
+
for (const id of command.ids) {
|
|
992
|
+
const result = await this.delete(command.object, id, cmdOptions);
|
|
993
|
+
if (result) deleted++;
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
success: true,
|
|
997
|
+
affected: deleted
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
default:
|
|
1001
|
+
throw new Error(`Unsupported command type: ${(command as any).type}`);
|
|
1002
|
+
}
|
|
1003
|
+
} catch (error: any) {
|
|
1004
|
+
return {
|
|
1005
|
+
success: false,
|
|
1006
|
+
affected: 0,
|
|
1007
|
+
error: error.message || 'Unknown error occurred'
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Execute raw command (for compatibility)
|
|
1014
|
+
*
|
|
1015
|
+
* @param command - Command string or object
|
|
1016
|
+
* @param parameters - Command parameters
|
|
1017
|
+
* @param options - Execution options
|
|
1018
|
+
*/
|
|
1019
|
+
async execute(command: any, parameters?: any[], options?: any): Promise<any> {
|
|
1020
|
+
throw new Error('Excel driver does not support raw command execution. Use executeCommand() instead.');
|
|
1021
|
+
}
|
|
1022
|
+
|
|
787
1023
|
// ========== Helper Methods ==========
|
|
788
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Convert FilterNode from QueryAST to legacy filter format.
|
|
1027
|
+
*
|
|
1028
|
+
* @param node - The FilterNode to convert
|
|
1029
|
+
* @returns Legacy filter array format
|
|
1030
|
+
*/
|
|
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');
|
|
1049
|
+
}
|
|
1050
|
+
andResults.push(...(Array.isArray(converted) ? converted : [converted]));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
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');
|
|
1064
|
+
}
|
|
1065
|
+
orResults.push(...(Array.isArray(converted) ? converted : [converted]));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
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]);
|
|
1074
|
+
}
|
|
1075
|
+
return undefined;
|
|
1076
|
+
|
|
1077
|
+
default:
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
|
|
1084
|
+
* This ensures backward compatibility while supporting the new @objectstack/spec interface.
|
|
1085
|
+
*
|
|
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].
|
|
1088
|
+
*/
|
|
1089
|
+
private normalizeQuery(query: any): any {
|
|
1090
|
+
if (!query) return {};
|
|
1091
|
+
|
|
1092
|
+
const normalized: any = { ...query };
|
|
1093
|
+
|
|
1094
|
+
// Normalize limit/top
|
|
1095
|
+
if (normalized.top !== undefined && normalized.limit === undefined) {
|
|
1096
|
+
normalized.limit = normalized.top;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Normalize sort format
|
|
1100
|
+
if (normalized.sort && Array.isArray(normalized.sort)) {
|
|
1101
|
+
// Check if it's already in the array format [field, order]
|
|
1102
|
+
const firstSort = normalized.sort[0];
|
|
1103
|
+
if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
|
|
1104
|
+
// Convert from QueryAST format {field, order} to internal format [field, order]
|
|
1105
|
+
normalized.sort = normalized.sort.map((item: any) => [
|
|
1106
|
+
item.field,
|
|
1107
|
+
item.order || item.direction || item.dir || 'asc'
|
|
1108
|
+
]);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
return normalized;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
789
1115
|
/**
|
|
790
1116
|
* Apply filters to an array of records (in-memory filtering).
|
|
791
1117
|
*/
|
package/test/index.test.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Excel Driver Tests
|
|
3
11
|
*
|
|
@@ -563,4 +571,180 @@ describe('ExcelDriver', () => {
|
|
|
563
571
|
expect(deleted).toBeNull();
|
|
564
572
|
});
|
|
565
573
|
});
|
|
574
|
+
|
|
575
|
+
describe('DriverInterface v4.0 Methods', () => {
|
|
576
|
+
describe('executeQuery', () => {
|
|
577
|
+
it('should execute query with QueryAST format', async () => {
|
|
578
|
+
await driver.create(TEST_OBJECT, { name: 'Alice', age: 30 });
|
|
579
|
+
await driver.create(TEST_OBJECT, { name: 'Bob', age: 25 });
|
|
580
|
+
await driver.create(TEST_OBJECT, { name: 'Charlie', age: 35 });
|
|
581
|
+
|
|
582
|
+
const result = await driver.executeQuery({
|
|
583
|
+
object: TEST_OBJECT,
|
|
584
|
+
fields: ['name', 'age'],
|
|
585
|
+
filters: {
|
|
586
|
+
type: 'comparison',
|
|
587
|
+
field: 'age',
|
|
588
|
+
operator: '>',
|
|
589
|
+
value: 25
|
|
590
|
+
},
|
|
591
|
+
sort: [{ field: 'age', order: 'asc' }],
|
|
592
|
+
top: 10,
|
|
593
|
+
skip: 0
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(result.value).toHaveLength(2);
|
|
597
|
+
expect(result.value[0].name).toBe('Alice');
|
|
598
|
+
expect(result.value[1].name).toBe('Charlie');
|
|
599
|
+
expect(result.count).toBe(2);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should handle AND filters', async () => {
|
|
603
|
+
await driver.create(TEST_OBJECT, { name: 'Alice', age: 30, city: 'NYC' });
|
|
604
|
+
await driver.create(TEST_OBJECT, { name: 'Bob', age: 25, city: 'LA' });
|
|
605
|
+
await driver.create(TEST_OBJECT, { name: 'Charlie', age: 35, city: 'NYC' });
|
|
606
|
+
|
|
607
|
+
const result = await driver.executeQuery({
|
|
608
|
+
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' }
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(result.value).toHaveLength(2);
|
|
619
|
+
expect(result.value.every((u: any) => u.city === 'NYC')).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should handle pagination with skip and top', async () => {
|
|
623
|
+
await driver.create(TEST_OBJECT, { name: 'Alice', age: 30 });
|
|
624
|
+
await driver.create(TEST_OBJECT, { name: 'Bob', age: 25 });
|
|
625
|
+
await driver.create(TEST_OBJECT, { name: 'Charlie', age: 35 });
|
|
626
|
+
|
|
627
|
+
const result = await driver.executeQuery({
|
|
628
|
+
object: TEST_OBJECT,
|
|
629
|
+
sort: [{ field: 'name', order: 'asc' }],
|
|
630
|
+
skip: 1,
|
|
631
|
+
top: 1
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
expect(result.value).toHaveLength(1);
|
|
635
|
+
expect(result.value[0].name).toBe('Bob');
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe('executeCommand', () => {
|
|
640
|
+
it('should execute create command', async () => {
|
|
641
|
+
const result = await driver.executeCommand({
|
|
642
|
+
type: 'create',
|
|
643
|
+
object: TEST_OBJECT,
|
|
644
|
+
data: { name: 'Alice', email: 'alice@test.com' }
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
expect(result.success).toBe(true);
|
|
648
|
+
expect(result.affected).toBe(1);
|
|
649
|
+
expect(result.data).toHaveProperty('id');
|
|
650
|
+
expect(result.data.name).toBe('Alice');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('should execute update command', async () => {
|
|
654
|
+
const created = await driver.create(TEST_OBJECT, { name: 'Alice', age: 30 });
|
|
655
|
+
|
|
656
|
+
const result = await driver.executeCommand({
|
|
657
|
+
type: 'update',
|
|
658
|
+
object: TEST_OBJECT,
|
|
659
|
+
id: created.id,
|
|
660
|
+
data: { age: 31 }
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
expect(result.success).toBe(true);
|
|
664
|
+
expect(result.affected).toBe(1);
|
|
665
|
+
expect(result.data.age).toBe(31);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('should execute delete command', async () => {
|
|
669
|
+
const created = await driver.create(TEST_OBJECT, { name: 'Alice' });
|
|
670
|
+
|
|
671
|
+
const result = await driver.executeCommand({
|
|
672
|
+
type: 'delete',
|
|
673
|
+
object: TEST_OBJECT,
|
|
674
|
+
id: created.id
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
expect(result.success).toBe(true);
|
|
678
|
+
expect(result.affected).toBe(1);
|
|
679
|
+
|
|
680
|
+
const remaining = await driver.find(TEST_OBJECT, {});
|
|
681
|
+
expect(remaining).toHaveLength(0);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should execute bulkCreate command', async () => {
|
|
685
|
+
const result = await driver.executeCommand({
|
|
686
|
+
type: 'bulkCreate',
|
|
687
|
+
object: TEST_OBJECT,
|
|
688
|
+
records: [
|
|
689
|
+
{ name: 'Alice', age: 30 },
|
|
690
|
+
{ name: 'Bob', age: 25 },
|
|
691
|
+
{ name: 'Charlie', age: 35 }
|
|
692
|
+
]
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(result.success).toBe(true);
|
|
696
|
+
expect(result.affected).toBe(3);
|
|
697
|
+
expect(result.data).toHaveLength(3);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should execute bulkUpdate command', async () => {
|
|
701
|
+
const user1 = await driver.create(TEST_OBJECT, { name: 'Alice', age: 30 });
|
|
702
|
+
const user2 = await driver.create(TEST_OBJECT, { name: 'Bob', age: 25 });
|
|
703
|
+
|
|
704
|
+
const result = await driver.executeCommand({
|
|
705
|
+
type: 'bulkUpdate',
|
|
706
|
+
object: TEST_OBJECT,
|
|
707
|
+
updates: [
|
|
708
|
+
{ id: user1.id, data: { age: 31 } },
|
|
709
|
+
{ id: user2.id, data: { age: 26 } }
|
|
710
|
+
]
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
expect(result.success).toBe(true);
|
|
714
|
+
expect(result.affected).toBe(2);
|
|
715
|
+
expect(result.data).toHaveLength(2);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should execute bulkDelete command', async () => {
|
|
719
|
+
const user1 = await driver.create(TEST_OBJECT, { name: 'Alice' });
|
|
720
|
+
const user2 = await driver.create(TEST_OBJECT, { name: 'Bob' });
|
|
721
|
+
const user3 = await driver.create(TEST_OBJECT, { name: 'Charlie' });
|
|
722
|
+
|
|
723
|
+
const result = await driver.executeCommand({
|
|
724
|
+
type: 'bulkDelete',
|
|
725
|
+
object: TEST_OBJECT,
|
|
726
|
+
ids: [user1.id, user2.id]
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(result.success).toBe(true);
|
|
730
|
+
expect(result.affected).toBe(2);
|
|
731
|
+
|
|
732
|
+
const remaining = await driver.find(TEST_OBJECT, {});
|
|
733
|
+
expect(remaining).toHaveLength(1);
|
|
734
|
+
expect(remaining[0].name).toBe('Charlie');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should return error for invalid command', async () => {
|
|
738
|
+
const result = await driver.executeCommand({
|
|
739
|
+
type: 'create',
|
|
740
|
+
object: TEST_OBJECT
|
|
741
|
+
// Missing data
|
|
742
|
+
} as any);
|
|
743
|
+
|
|
744
|
+
expect(result.success).toBe(false);
|
|
745
|
+
expect(result.affected).toBe(0);
|
|
746
|
+
expect(result.error).toBeDefined();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
});
|
|
566
750
|
});
|