database-excel-reader.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. /* eslint-disable @typescript-eslint/no-unused-vars */
  3. import * as XLSX from 'xlsx';
  4. import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
  5. import { prisma } from '@/lib/prisma';
  6. // Simple logger utility for debugging
  7. const logger = {
  8. debug: (message: string, ...args: any[]) => {
  9. //console.debug(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  10. },
  11. info: (message: string, ...args: any[]) => {
  12. //console.info(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  13. },
  14. warn: (message: string, ...args: any[]) => {
  15. //console.warn(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  16. },
  17. error: (message: string, ...args: any[]) => {
  18. //console.error(`[DatabaseExcelReaderService] ${new Date().toISOString()} - ${message}`, ...args);
  19. }
  20. };
  21. export class DatabaseExcelReaderService {
  22. async readExcelFromDatabase(
  23. fileId: string,
  24. layoutConfig: any,
  25. onProgress: (progress: ImportProgress) => void
  26. ): Promise<ReadSectionData[]> {
  27. logger.info('Starting Excel file import from database', {
  28. fileId,
  29. layoutConfigSections: layoutConfig.sections?.length || 0
  30. });
  31. const startTime = Date.now();
  32. try {
  33. // Fetch file from database
  34. const fileRecord = await prisma.file.findUnique({
  35. where: { id: fileId }
  36. });
  37. if (!fileRecord) {
  38. throw new Error(`File with ID ${fileId} not found in database`);
  39. }
  40. logger.info('File retrieved from database', {
  41. filename: fileRecord.filename,
  42. size: fileRecord.size,
  43. mimetype: fileRecord.mimetype
  44. });
  45. // Convert Buffer to ArrayBuffer for xlsx library
  46. const arrayBuffer = fileRecord.data.buffer.slice(
  47. fileRecord.data.byteOffset,
  48. fileRecord.data.byteOffset + fileRecord.data.byteLength
  49. );
  50. logger.debug('Loading Excel workbook from buffer...');
  51. const workbook = XLSX.read(arrayBuffer, { type: 'array' });
  52. logger.info('Excel workbook loaded successfully from database', {
  53. worksheets: workbook.SheetNames.map(name => ({
  54. name,
  55. rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length
  56. }))
  57. });
  58. const results: ReadSectionData[] = [];
  59. const totalSections = layoutConfig.sections?.length || 0;
  60. logger.info('Processing Excel import from database', { totalSections });
  61. // Initialize progress
  62. onProgress({
  63. importId: 0,
  64. status: 'processing',
  65. currentSection: '',
  66. currentRow: 0,
  67. totalRows: 0,
  68. errors: [],
  69. processedSections: 0,
  70. totalSections
  71. });
  72. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  73. const section = layoutConfig.sections[sectionIndex];
  74. logger.debug(`Processing section ${sectionIndex + 1}/${totalSections}`, {
  75. sectionName: section.name,
  76. sheetName: section.sheetName,
  77. startingRow: section.startingRow,
  78. endingRow: section.endingRow
  79. });
  80. const worksheet = workbook.Sheets[section.sheetName];
  81. if (!worksheet) {
  82. const error = `Worksheet '${section.sheetName}' not found`;
  83. logger.warn(error, { availableWorksheets: workbook.SheetNames });
  84. onProgress({
  85. importId: 0,
  86. status: 'processing',
  87. currentSection: section.name,
  88. currentRow: 0,
  89. totalRows: 0,
  90. errors: [error],
  91. processedSections: sectionIndex + 1,
  92. totalSections
  93. });
  94. continue;
  95. }
  96. const sectionData = await this.processSectionFromWorksheet(
  97. worksheet,
  98. section,
  99. sectionIndex,
  100. totalSections,
  101. onProgress
  102. );
  103. results.push(sectionData);
  104. logger.info(`Section ${section.name} processed successfully`, {
  105. rowsProcessed: sectionData.data.length,
  106. fields: sectionData.fields.length
  107. });
  108. }
  109. const totalTime = Date.now() - startTime;
  110. logger.info('Excel file import from database completed', {
  111. totalSections: results.length,
  112. totalRows: results.reduce((sum, section) => sum + section.data.length, 0),
  113. totalTimeMs: totalTime
  114. });
  115. return results;
  116. } catch (error) {
  117. logger.error('Error reading Excel file from database', {
  118. error: error instanceof Error ? error.message : String(error),
  119. stack: error instanceof Error ? error.stack : undefined
  120. });
  121. throw error;
  122. }
  123. }
  124. async readExcelFromBuffer(
  125. buffer: Buffer,
  126. layoutConfig: any,
  127. onProgress: (progress: ImportProgress) => void
  128. ): Promise<ReadSectionData[]> {
  129. logger.info('Starting Excel file import from buffer', {
  130. bufferSize: buffer.length,
  131. layoutConfigSections: layoutConfig.sections?.length || 0
  132. });
  133. const startTime = Date.now();
  134. try {
  135. // Convert Buffer to ArrayBuffer for xlsx library
  136. const arrayBuffer = buffer.buffer.slice(
  137. buffer.byteOffset,
  138. buffer.byteOffset + buffer.byteLength
  139. );
  140. logger.debug('Loading Excel workbook from buffer...');
  141. const workbook = XLSX.read(arrayBuffer, { type: 'array' });
  142. logger.info('Excel workbook loaded successfully from buffer', {
  143. worksheets: workbook.SheetNames.map(name => ({
  144. name,
  145. rowCount: XLSX.utils.sheet_to_json(workbook.Sheets[name]).length
  146. }))
  147. });
  148. const results: ReadSectionData[] = [];
  149. const totalSections = layoutConfig.sections?.length || 0;
  150. logger.info('Processing Excel import from buffer', { totalSections });
  151. // Initialize progress
  152. onProgress({
  153. importId: 0,
  154. status: 'processing',
  155. currentSection: '',
  156. currentRow: 0,
  157. totalRows: 0,
  158. errors: [],
  159. processedSections: 0,
  160. totalSections
  161. });
  162. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  163. const section = layoutConfig.sections[sectionIndex];
  164. logger.debug(`Processing section ${sectionIndex + 1}/${totalSections}`, {
  165. sectionName: section.name,
  166. sheetName: section.sheetName,
  167. startingRow: section.startingRow,
  168. endingRow: section.endingRow
  169. });
  170. const worksheet = workbook.Sheets[section.sheetName];
  171. if (!worksheet) {
  172. const error = `Worksheet '${section.sheetName}' not found`;
  173. logger.warn(error, { availableWorksheets: workbook.SheetNames });
  174. onProgress({
  175. importId: 0,
  176. status: 'processing',
  177. currentSection: section.name,
  178. currentRow: 0,
  179. totalRows: 0,
  180. errors: [error],
  181. processedSections: sectionIndex + 1,
  182. totalSections
  183. });
  184. continue;
  185. }
  186. const sectionData = await this.processSectionFromWorksheet(
  187. worksheet,
  188. section,
  189. sectionIndex,
  190. totalSections,
  191. onProgress
  192. );
  193. results.push(sectionData);
  194. logger.info(`Section ${section.name} processed successfully`, {
  195. rowsProcessed: sectionData.data.length,
  196. fields: sectionData.fields.length
  197. });
  198. }
  199. const totalTime = Date.now() - startTime;
  200. logger.info('Excel file import from buffer completed', {
  201. totalSections: results.length,
  202. totalRows: results.reduce((sum, section) => sum + section.data.length, 0),
  203. totalTimeMs: totalTime
  204. });
  205. return results;
  206. } catch (error) {
  207. logger.error('Error reading Excel file from buffer', {
  208. error: error instanceof Error ? error.message : String(error),
  209. stack: error instanceof Error ? error.stack : undefined
  210. });
  211. throw error;
  212. }
  213. }
  214. private async processSectionFromWorksheet(
  215. worksheet: XLSX.WorkSheet,
  216. section: any,
  217. sectionIndex: number,
  218. totalSections: number,
  219. onProgress: (progress: ImportProgress) => void
  220. ): Promise<ReadSectionData> {
  221. const sectionStartTime = Date.now();
  222. logger.info(`Starting section processing from worksheet`, {
  223. sectionName: section.name,
  224. sheetName: section.sheetName,
  225. sectionIndex: sectionIndex + 1,
  226. totalSections
  227. });
  228. const startingRow = section.startingRow || 2; // Default to 2 to skip header
  229. const endingRow = section.endingRow || Infinity;
  230. logger.debug('Section configuration', {
  231. sectionName: section.name,
  232. startingRow,
  233. endingRow,
  234. fieldsCount: section.fields?.length || 0
  235. });
  236. // Convert worksheet to JSON array
  237. const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
  238. // Process data rows
  239. const data: Record<string, any>[] = [];
  240. const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
  241. let processedRows = 0;
  242. let skippedRows = 0;
  243. for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
  244. const row = worksheetData[rowNum - 1]; // Convert to 0-based index
  245. if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
  246. skippedRows++;
  247. logger.debug(`Skipping empty row ${rowNum}`);
  248. continue;
  249. }
  250. const rowData: Record<string, any> = {};
  251. let fieldsProcessed = 0;
  252. // Map cell values based on field configuration
  253. for (const field of section.fields || []) {
  254. try {
  255. const cellAddress = this.parseCellAddress(field.cellPosition, rowNum);
  256. const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
  257. logger.debug(`Processing field`, {
  258. fieldName: field.name,
  259. cellPosition: field.cellPosition,
  260. cellAddress,
  261. rawValue: cellValue,
  262. rowNum
  263. });
  264. if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
  265. console.log(field.name, field.dataType, field.dataTypeFormat, cellValue, field.parsedType);
  266. const value = this.convertCellValue(
  267. cellValue,
  268. field.parsedType || FieldTypeEnum.String
  269. );
  270. logger.debug(`Value converted`, {
  271. fieldName: field.name,
  272. originalValue: cellValue,
  273. convertedValue: value,
  274. fieldType: field.parsedType || FieldTypeEnum.String
  275. });
  276. // Map to the correct column name for Prisma model
  277. const columnName = field.importTableColumnName;
  278. rowData[columnName] = value;
  279. fieldsProcessed++;
  280. }
  281. } catch (error) {
  282. logger.error(`Error processing field ${field.name} at row ${rowNum}`, {
  283. error: error instanceof Error ? error.message : String(error),
  284. field,
  285. rowNum
  286. });
  287. }
  288. }
  289. // Only add non-empty rows
  290. if (Object.keys(rowData).length > 0) {
  291. data.push(rowData);
  292. processedRows++;
  293. if (processedRows <= 5 || processedRows % 100 === 0) {
  294. logger.debug(`Row processed`, {
  295. rowNum,
  296. fieldsProcessed,
  297. rowDataKeys: Object.keys(rowData),
  298. dataLength: data.length
  299. });
  300. }
  301. } else {
  302. logger.debug(`Skipping row with no valid data`, { rowNum });
  303. }
  304. // Update progress every 100 rows
  305. if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
  306. onProgress({
  307. importId: 0,
  308. status: 'processing',
  309. currentSection: section.name,
  310. currentRow: rowNum - startingRow + 1,
  311. totalRows,
  312. errors: [],
  313. processedSections: sectionIndex,
  314. totalSections
  315. });
  316. }
  317. }
  318. const sectionTime = Date.now() - sectionStartTime;
  319. logger.info(`Section processing completed`, {
  320. sectionName: section.name,
  321. processedRows,
  322. skippedRows,
  323. totalRows,
  324. dataRows: data.length,
  325. processingTimeMs: sectionTime
  326. });
  327. const result = {
  328. id: section.id || 0,
  329. name: section.name || '',
  330. tableName: section.tableName || '',
  331. sheet: section.sheetName || '',
  332. type: section.type || '',
  333. startingRow,
  334. endingRow,
  335. parsedType: this.mapSectionType(section.type),
  336. fields: this.mapFields(section.fields || []),
  337. data
  338. };
  339. logger.debug('Section result', {
  340. sectionName: section.name,
  341. resultSummary: {
  342. id: result.id,
  343. name: result.name,
  344. tableName: result.tableName,
  345. dataRows: result.data.length,
  346. fields: result.fields.length
  347. }
  348. });
  349. return result;
  350. }
  351. private parseCellAddress(cellPosition: string, rowNumber: number): { row: number; col: number } {
  352. logger.debug(`Parsing cell address: ${cellPosition}`);
  353. let match = cellPosition.match(/([A-Z]+)(\d+)/);
  354. if (!match) {
  355. logger.warn(`Invalid cell position format: ${cellPosition}, trying to add row number ${rowNumber}`);
  356. const appendedCellPosition = `${cellPosition}${rowNumber}`;
  357. match = appendedCellPosition.match(/([A-Z]+)(\d+)/);
  358. if (!match) {
  359. logger.warn(`Invalid cell position format: ${appendedCellPosition}, using default 1,1`);
  360. return { row: 1, col: 1 };
  361. }
  362. }
  363. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  364. const row = parseInt(match[2]);
  365. logger.debug(`Parsed cell address`, {
  366. original: cellPosition,
  367. row,
  368. col
  369. });
  370. return { row, col };
  371. }
  372. private mapSectionType(type: string): SectionTypeEnum {
  373. logger.debug(`Mapping section type: ${type}`);
  374. const mappedType = (() => {
  375. switch (type?.toLowerCase()) {
  376. case 'grid':
  377. return SectionTypeEnum.Grid;
  378. case 'properties':
  379. return SectionTypeEnum.Properties;
  380. default:
  381. return SectionTypeEnum.Unknown;
  382. }
  383. })();
  384. logger.debug(`Section type mapped`, {
  385. originalType: type,
  386. mappedType: SectionTypeEnum[mappedType]
  387. });
  388. return mappedType;
  389. }
  390. private mapFields(fields: any[]): LayoutSectionField[] {
  391. logger.debug(`Mapping ${fields.length} fields`);
  392. const mappedFields = fields.map((field, index) => {
  393. const mappedField = {
  394. id: field.id || index,
  395. cellPosition: field.cellPosition || '',
  396. name: field.name || '',
  397. dataType: field.dataType || 'string',
  398. dataTypeFormat: field.dataTypeFormat,
  399. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  400. importColumnOrderNumber: field.importColumnOrderNumber || index,
  401. parsedType: this.mapFieldType(field.dataType)
  402. };
  403. logger.debug(`Field mapped`, {
  404. index,
  405. originalName: field.name,
  406. mappedName: mappedField.name,
  407. cellPosition: mappedField.cellPosition,
  408. parsedType: FieldTypeEnum[mappedField.parsedType]
  409. });
  410. return mappedField;
  411. });
  412. return mappedFields;
  413. }
  414. private mapFieldType(dataType: string): FieldTypeEnum {
  415. const type = dataType?.toLowerCase();
  416. const mappedType = (() => {
  417. switch (type) {
  418. case 'time':
  419. return FieldTypeEnum.Time;
  420. case 'decimal':
  421. case 'number':
  422. case 'float':
  423. return FieldTypeEnum.Decimal;
  424. case 'date':
  425. return FieldTypeEnum.Date;
  426. case 'int':
  427. case 'integer':
  428. case 'numeric':
  429. return FieldTypeEnum.Numeric;
  430. default:
  431. return FieldTypeEnum.String;
  432. }
  433. })();
  434. logger.debug(`Field type mapped`, {
  435. originalDataType: dataType,
  436. mappedType: FieldTypeEnum[mappedType]
  437. });
  438. return mappedType;
  439. }
  440. private convertCellValue(value: any, fieldType: FieldTypeEnum): any {
  441. if (value === null || value === undefined) {
  442. logger.debug(`Converting null/undefined value to null`, { fieldType: FieldTypeEnum[fieldType] });
  443. return null;
  444. }
  445. logger.debug(`Converting cell value`, {
  446. originalValue: value,
  447. originalType: typeof value,
  448. targetFieldType: FieldTypeEnum[fieldType]
  449. });
  450. const convertedValue = (() => {
  451. if (fieldType === FieldTypeEnum.Date)
  452. console.log(value, fieldType);
  453. switch (fieldType) {
  454. case FieldTypeEnum.Time:
  455. if (typeof value === 'number') {
  456. // Excel time is fraction of a day
  457. const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  458. logger.debug(`Time conversion`, { original: value, converted: result });
  459. return result;
  460. }
  461. return value;
  462. case FieldTypeEnum.Decimal:
  463. const decimalResult = parseFloat(value.toString()) || 0;
  464. logger.debug(`Decimal conversion`, { original: value, converted: decimalResult });
  465. return decimalResult;
  466. case FieldTypeEnum.Date:
  467. if (typeof value === 'number') {
  468. // Excel date is days since 1900-01-01
  469. const excelEpoch = new Date(1900, 0, 1);
  470. const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  471. logger.debug(`Date conversion`, { original: value, converted: dateResult });
  472. return dateResult;
  473. }
  474. const dateResult = new Date(value);
  475. logger.debug(`Date conversion from string`, { original: value, converted: dateResult });
  476. return dateResult;
  477. case FieldTypeEnum.Numeric:
  478. const numericResult = parseInt(value.toString()) || 0;
  479. logger.debug(`Numeric conversion`, { original: value, converted: numericResult });
  480. return numericResult;
  481. case FieldTypeEnum.String:
  482. default:
  483. const stringResult = value.toString();
  484. logger.debug(`String conversion`, { original: value, converted: stringResult });
  485. return stringResult;
  486. }
  487. })();
  488. return convertedValue;
  489. }
  490. }