Эх сурвалжийг харах

Now Builds
feat(cintas): enhance Excel import processor with buffer-based file handling and improved error handling

- Replace file path-based Excel reading with buffer-based approach for better cloud compatibility
- Add support for base64-encoded file data in Cintas import processor
- Introduce ProcessedSection interface for type safety in section processing
- Improve error handling and logging throughout import workflow
- Update import processor to use fileId relationship instead of direct file embedding
- Add null safety checks and optional chaining for robust file handling
- Remove console logging from Excel reader service for cleaner production output

vtugulan 6 сар өмнө
parent
commit
0607347cae

+ 23 - 13
app/lib/excel-import/cintas-import-processor.ts

@@ -8,6 +8,11 @@ import { ImportProgressServer } from './websocket-server';
 import { ImportProgress, ImportResult } from './types';
 import { FileDownloader } from './file-downloader';
 
+interface ProcessedSection {
+  sectionData: any;
+  insertedRows: number;
+}
+
 export class CintasImportProcessor {
   private prisma: PrismaClient;
   private reader: ExcelReaderService;
@@ -25,10 +30,10 @@ export class CintasImportProcessor {
 
   async processCintasImport(importId: number): Promise<ImportResult> {
     let filePath: string | null = null;
-    
+
     try {
       console.log(`[${new Date().toISOString()}] [CintasImport] Starting import processing for ID: ${importId}`);
-      
+
       // Initialize the progress server if not already done
       if (!this.progressServer.isServerInitialized()) {
         this.progressServer.initialize();
@@ -75,26 +80,31 @@ export class CintasImportProcessor {
       // Save file to temporary location
       const filename = `import_${importId}_${Date.now()}.xlsx`;
       filePath = path.join(this.fileDownloader.getTempDir(), filename);
-      
+
       let fileBuffer: Buffer;
       if (Buffer.isBuffer(file.data)) {
         fileBuffer = file.data;
       } else if (file.data instanceof Uint8Array) {
         fileBuffer = Buffer.from(file.data);
+      } else if (typeof file.data === 'string') {
+        fileBuffer = Buffer.from(file.data, 'base64');
       } else {
-        fileBuffer = Buffer.from(file.data as any);
+        fileBuffer = Buffer.from(file.data as Buffer);
       }
-      
+
       fs.writeFileSync(filePath, fileBuffer);
       console.log(`[${new Date().toISOString()}] [CintasImport] File saved to: ${filePath}`);
 
       // Read Excel file
       console.log(`[${new Date().toISOString()}] [CintasImport] Starting Excel file reading...`);
-      
+
+      // Read file content as buffer
+      const fileContent = fs.readFileSync(filePath);
+
       const sections = await this.reader.readExcelFile(
-        filePath,
+        fileContent,
         importRecord.layout,
-        (sectionProgress) => {
+        (sectionProgress: ImportProgress) => {
           this.progressServer.broadcastProgress(importId, sectionProgress);
         }
       );
@@ -102,13 +112,13 @@ export class CintasImportProcessor {
       console.log(`[${new Date().toISOString()}] [CintasImport] Excel file read successfully. Found ${sections.length} sections`);
 
       // Process each section
-      const processedSections = [];
+      const processedSections: ProcessedSection[] = [];
       let totalInserted = 0;
 
       for (let i = 0; i < sections.length; i++) {
         const section = sections[i];
-        
-        console.log(`[${new Date().toISOString()}] [CintasImport] Processing section ${i+1}/${sections.length}: ${section.name}`);
+
+        console.log(`[${new Date().toISOString()}] [CintasImport] Processing section ${i + 1}/${sections.length}: ${section.name}`);
         progress.currentSection = section.name;
         progress.processedSections = i + 1;
         this.progressServer.broadcastProgress(importId, progress);
@@ -117,11 +127,11 @@ export class CintasImportProcessor {
           // Ensure table exists for this section
           console.log(`[${new Date().toISOString()}] [CintasImport] Creating table ${section.tableName} for section ${section.name}`);
           await this.inserter.createImportTable(section.tableName, section.fields);
-          
+
           const insertedRows = await this.inserter.insertSectionData(
             section,
             importId,
-            (rows) => {
+            (rows: number) => {
               progress.currentRow = rows;
               this.progressServer.broadcastProgress(importId, progress);
             }

+ 9 - 9
app/lib/excel-import/excel-reader.ts

@@ -1,41 +1,41 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 /* eslint-disable @typescript-eslint/no-require-imports */
+/* eslint-disable @typescript-eslint/no-unused-vars */
 import * as XLSX from 'xlsx';
 import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
 
 // Simple logger utility for debugging
 const logger = {
   debug: (message: string, ...args: any[]) => {
-    console.debug(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.debug(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   info: (message: string, ...args: any[]) => {
-    console.info(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.info(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   warn: (message: string, ...args: any[]) => {
-    console.warn(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.warn(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   },
   error: (message: string, ...args: any[]) => {
-    console.error(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
+    //console.error(`[ExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
   }
 };
 
 export class ExcelReaderService {
   async readExcelFile(
-    filePath: string,
+    fileData: Buffer | Uint8Array,
     layoutConfig: any,
     onProgress: (progress: ImportProgress) => void
   ): Promise<ReadSectionData[]> {
     logger.info('Starting Excel file import', {
-      filePath,
-      fileSize: require('fs').statSync(filePath).size,
+      fileSize: fileData.length,
       layoutConfigSections: layoutConfig.sections?.length || 0
     });
 
     const startTime = Date.now();
 
     try {
-      logger.debug('Loading Excel workbook from file path...');
-      const workbook = XLSX.readFile(filePath);
+      logger.debug('Loading Excel workbook from buffer...');
+      const workbook = XLSX.read(fileData, { type: 'buffer' });
       logger.info('Excel workbook loaded successfully', {
         worksheets: workbook.SheetNames.map(name => ({ name, rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length }))
       });

+ 50 - 38
app/lib/excel-import/import-processor.ts

@@ -29,13 +29,25 @@ export class ImportProcessor {
                 include: { fields: true }
               }
             }
-          },
-          file: true
+          }
         }
       });
 
-      if (!importRecord || !importRecord.file) {
-        throw new Error('Import not found or no file attached');
+      if (!importRecord) {
+        throw new Error('Import not found');
+      }
+
+      if (!importRecord.fileId) {
+        throw new Error('No file attached to import');
+      }
+
+      // Get the actual file
+      const fileRecord = await this.prisma.file.findUnique({
+        where: { id: importRecord.fileId }
+      });
+
+      if (!fileRecord) {
+        throw new Error('File not found');
       }
 
       // Initialize progress tracking
@@ -47,14 +59,14 @@ export class ImportProcessor {
         totalRows: 0,
         errors: [],
         processedSections: 0,
-        totalSections: importRecord.layout?.sections?.length || 0
+        totalSections: importRecord.layout?.sections?.length ?? 0
       };
 
       // Read Excel file
       const sections = await this.reader.readExcelFile(
-        Buffer.from(importRecord.file.data),
+        fileRecord.data,
         importRecord.layout,
-        (sectionProgress) => {
+        (sectionProgress: ImportProgress) => {
           this.progressServer.broadcastProgress(importId, sectionProgress);
         }
       );
@@ -63,39 +75,40 @@ export class ImportProcessor {
       const processedSections: ProcessedSection[] = [];
       let totalInserted = 0;
 
-      for (let i = 0; i < sections.length; i++) {
+      for (let i = 0; i < sections.length; i += 1) {
         const section = sections[i];
 
-        progress.currentSection = section.name;
-        progress.processedSections = i + 1;
-        this.progressServer.broadcastProgress(importId, progress);
-
-        try {
-          const insertedRows = await this.inserter.insertSectionData(
-            section,
-            importId,
-            (rows) => {
-              progress.currentRow = rows;
-              this.progressServer.broadcastProgress(importId, progress);
-            }
-          );
+        if (section) {
+          progress.currentSection = section.name ?? '';
+          progress.processedSections = i + 1;
+          this.progressServer.broadcastProgress(importId, progress);
 
-          processedSections.push({
-            sectionData: section,
-            insertedRows
-          });
+          try {
+            const insertedRows = await this.inserter.insertSectionData(
+              section,
+              importId,
+              (rows: number) => {
+                progress.currentRow = rows;
+                this.progressServer.broadcastProgress(importId, progress);
+              }
+            );
 
-          totalInserted += insertedRows;
+            processedSections.push({
+              sectionData: section,
+              insertedRows
+            });
 
-        } catch (error) {
-          const errorMessage = `Error processing section ${section.name}: ${error instanceof Error ? error.message : 'Unknown error'}`;
-          progress.errors.push(errorMessage);
-          this.progressServer.broadcastProgress(importId, progress);
+            totalInserted += insertedRows;
+
+          } catch (error: unknown) {
+            const errorMessage = `Error processing section ${section.name ?? 'unknown'}: ${error instanceof Error ? error.message : 'Unknown error'}`;
+            progress.errors.push(errorMessage);
+            this.progressServer.broadcastProgress(importId, progress);
+          }
         }
       }
 
-      // Import processing completed - no status field to update
-
+      // Import processing completed
       progress.status = 'completed';
       this.progressServer.broadcastProgress(importId, progress);
 
@@ -104,10 +117,8 @@ export class ImportProcessor {
         totalInserted,
         sections: processedSections
       };
-
     } catch (error) {
-      // Import processing failed - no status field to update
-
+      // Import processing failed
       const progress: ImportProgress = {
         importId,
         status: 'failed',
@@ -136,7 +147,9 @@ export class ImportProcessor {
     try {
       const importRecord = await this.prisma.import.findUnique({
         where: { id: importId },
-        include: { file: true, layout: true }
+        include: {
+          layout: true
+        }
       });
 
       if (!importRecord) {
@@ -144,7 +157,7 @@ export class ImportProcessor {
         return { valid: false, errors };
       }
 
-      if (!importRecord.file) {
+      if (!importRecord.fileId) {
         errors.push('No file attached to import');
       }
 
@@ -153,7 +166,6 @@ export class ImportProcessor {
       }
 
       return { valid: errors.length === 0, errors };
-
     } catch (error) {
       errors.push(`Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
       return { valid: false, errors };