|
30 | 30 | */ |
31 | 31 |
|
32 | 32 | import { Driver, ObjectQLError } from '@objectql/types'; |
| 33 | +import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec'; |
33 | 34 | import * as ExcelJS from 'exceljs'; |
34 | 35 | import * as fs from 'fs'; |
35 | 36 | import * as path from 'path'; |
36 | 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 | + |
37 | 62 | /** |
38 | 63 | * File storage mode for the Excel driver. |
39 | 64 | */ |
@@ -77,10 +102,10 @@ export interface ExcelDriverConfig { |
77 | 102 | * the standard DriverInterface from @objectstack/spec for compatibility |
78 | 103 | * with the new kernel-based plugin system. |
79 | 104 | */ |
80 | | -export class ExcelDriver implements Driver { |
| 105 | +export class ExcelDriver implements Driver, DriverInterface { |
81 | 106 | // Driver metadata (ObjectStack-compatible) |
82 | 107 | public readonly name = 'ExcelDriver'; |
83 | | - public readonly version = '3.0.1'; |
| 108 | + public readonly version = '4.0.0'; |
84 | 109 | public readonly supports = { |
85 | 110 | transactions: false, |
86 | 111 | joins: false, |
@@ -849,8 +874,211 @@ export class ExcelDriver implements Driver { |
849 | 874 | } |
850 | 875 | } |
851 | 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 | + |
852 | 1023 | // ========== Helper Methods ========== |
853 | 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 | + |
854 | 1082 | /** |
855 | 1083 | * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. |
856 | 1084 | * This ensures backward compatibility while supporting the new @objectstack/spec interface. |
|
0 commit comments