database-excel-reader.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import * as XLSX from 'xlsx';
  2. import { ReadSectionData, LayoutSectionField, SectionTypeEnum, FieldTypeEnum, ImportProgress } from './types';
  3. import { prisma } from '@/lib/prisma';
  4. export class DatabaseExcelReaderService {
  5. async readExcelFromDatabase(
  6. fileId: string,
  7. layoutConfig: any,
  8. onProgress: (progress: ImportProgress) => void
  9. ): Promise<ReadSectionData[]> {
  10. try {
  11. // Fetch file from database
  12. const fileRecord = await prisma.file.findUnique({
  13. where: { id: fileId }
  14. });
  15. if (!fileRecord) {
  16. throw new Error(`File with ID ${fileId} not found in database`);
  17. }
  18. // Convert Buffer to ArrayBuffer for xlsx library
  19. const arrayBuffer = fileRecord.data.buffer.slice(
  20. fileRecord.data.byteOffset,
  21. fileRecord.data.byteOffset + fileRecord.data.byteLength
  22. );
  23. const workbook = XLSX.read(arrayBuffer, { type: 'array' });
  24. const results: ReadSectionData[] = [];
  25. const totalSections = layoutConfig.sections?.length || 0;
  26. // Initialize progress
  27. onProgress({
  28. importId: 0,
  29. status: 'processing',
  30. currentSection: '',
  31. currentRow: 0,
  32. totalRows: 0,
  33. errors: [],
  34. processedSections: 0,
  35. totalSections
  36. });
  37. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  38. const section = layoutConfig.sections[sectionIndex];
  39. const worksheet = workbook.Sheets[section.sheetName];
  40. if (!worksheet) {
  41. const error = `Worksheet '${section.sheetName}' not found`;
  42. onProgress({
  43. importId: 0,
  44. status: 'processing',
  45. currentSection: section.name,
  46. currentRow: 0,
  47. totalRows: 0,
  48. errors: [error],
  49. processedSections: sectionIndex + 1,
  50. totalSections
  51. });
  52. continue;
  53. }
  54. const sectionData = await this.processSectionFromWorksheet(
  55. worksheet,
  56. section,
  57. sectionIndex,
  58. totalSections,
  59. onProgress
  60. );
  61. results.push(sectionData);
  62. }
  63. return results;
  64. } catch (error) {
  65. throw error;
  66. }
  67. }
  68. async readExcelFromBuffer(
  69. buffer: Buffer,
  70. layoutConfig: any,
  71. onProgress: (progress: ImportProgress) => void
  72. ): Promise<ReadSectionData[]> {
  73. try {
  74. // Convert Buffer to ArrayBuffer for xlsx library
  75. const arrayBuffer = buffer.buffer.slice(
  76. buffer.byteOffset,
  77. buffer.byteOffset + buffer.byteLength
  78. );
  79. const workbook = XLSX.read(arrayBuffer, { type: 'array' });
  80. const results: ReadSectionData[] = [];
  81. const totalSections = layoutConfig.sections?.length || 0;
  82. // Initialize progress
  83. onProgress({
  84. importId: 0,
  85. status: 'processing',
  86. currentSection: '',
  87. currentRow: 0,
  88. totalRows: 0,
  89. errors: [],
  90. processedSections: 0,
  91. totalSections
  92. });
  93. for (let sectionIndex = 0; sectionIndex < totalSections; sectionIndex++) {
  94. const section = layoutConfig.sections[sectionIndex];
  95. const worksheet = workbook.Sheets[section.sheetName];
  96. if (!worksheet) {
  97. const error = `Worksheet '${section.sheetName}' not found`;
  98. onProgress({
  99. importId: 0,
  100. status: 'processing',
  101. currentSection: section.name,
  102. currentRow: 0,
  103. totalRows: 0,
  104. errors: [error],
  105. processedSections: sectionIndex + 1,
  106. totalSections
  107. });
  108. continue;
  109. }
  110. const sectionData = await this.processSectionFromWorksheet(
  111. worksheet,
  112. section,
  113. sectionIndex,
  114. totalSections,
  115. onProgress
  116. );
  117. results.push(sectionData);
  118. }
  119. return results;
  120. } catch (error) {
  121. throw error;
  122. }
  123. }
  124. private async processSectionFromWorksheet(
  125. worksheet: XLSX.WorkSheet,
  126. section: any,
  127. sectionIndex: number,
  128. totalSections: number,
  129. onProgress: (progress: ImportProgress) => void
  130. ): Promise<ReadSectionData> {
  131. const startingRow = section.startingRow || 2; // Default to 2 to skip header
  132. const endingRow = section.endingRow || Infinity;
  133. // Convert worksheet to JSON array
  134. const worksheetData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
  135. // Process data rows
  136. const data: Record<string, any>[] = [];
  137. const totalRows = Math.min(endingRow, worksheetData.length) - startingRow + 1;
  138. for (let rowNum = startingRow; rowNum <= Math.min(endingRow, worksheetData.length); rowNum++) {
  139. const row = worksheetData[rowNum - 1]; // Convert to 0-based index
  140. if (!row || row.every(cell => cell === null || cell === undefined || cell === '')) {
  141. continue;
  142. }
  143. const rowData: Record<string, any> = {};
  144. // Map cell values based on field configuration
  145. for (const field of section.fields || []) {
  146. try {
  147. const cellAddress = this.parseCellAddress(field.cellPosition, rowNum);
  148. const cellValue = row[cellAddress.col - 1]; // Convert to 0-based index
  149. if (cellValue !== null && cellValue !== undefined && cellValue !== '') {
  150. const value = this.convertCellValue(
  151. field.dataType,
  152. field.dataTypeFormat,
  153. cellValue,
  154. field.parsedType || FieldTypeEnum.String
  155. );
  156. // Map to the correct column name for Prisma model
  157. const columnName = field.importTableColumnName;
  158. rowData[columnName] = value;
  159. }
  160. } catch (error) {
  161. console.log(`Error processing field ${field.name} at row ${rowNum}`, {
  162. error: error instanceof Error ? error.message : String(error),
  163. field,
  164. rowNum
  165. });
  166. }
  167. }
  168. // Only add non-empty rows
  169. if (Object.keys(rowData).length > 0) {
  170. data.push(rowData);
  171. }
  172. // Update progress every 100 rows
  173. if (rowNum % 100 === 0 || rowNum === Math.min(endingRow, worksheetData.length)) {
  174. onProgress({
  175. importId: 0,
  176. status: 'processing',
  177. currentSection: section.name,
  178. currentRow: rowNum - startingRow + 1,
  179. totalRows,
  180. errors: [],
  181. processedSections: sectionIndex,
  182. totalSections
  183. });
  184. }
  185. }
  186. const result = {
  187. id: section.id || 0,
  188. name: section.name || '',
  189. tableName: section.tableName || '',
  190. sheet: section.sheetName || '',
  191. type: section.type || '',
  192. startingRow,
  193. endingRow,
  194. parsedType: this.mapSectionType(section.type),
  195. fields: this.mapFields(section.fields || []),
  196. data
  197. };
  198. return result;
  199. }
  200. private parseCellAddress(cellPosition: string, rowNumber: number): { row: number; col: number } {
  201. let match = cellPosition.match(/([A-Z]+)(\d+)/);
  202. if (!match) {
  203. const appendedCellPosition = `${cellPosition}${rowNumber}`;
  204. match = appendedCellPosition.match(/([A-Z]+)(\d+)/);
  205. if (!match) {
  206. return { row: 1, col: 1 };
  207. }
  208. }
  209. const col = match[1].charCodeAt(0) - 'A'.charCodeAt(0) + 1;
  210. const row = parseInt(match[2]);
  211. return { row, col };
  212. }
  213. private mapSectionType(type: string): SectionTypeEnum {
  214. const mappedType = (() => {
  215. switch (type?.toLowerCase()) {
  216. case 'grid':
  217. return SectionTypeEnum.Grid;
  218. case 'properties':
  219. return SectionTypeEnum.Properties;
  220. default:
  221. return SectionTypeEnum.Unknown;
  222. }
  223. })();
  224. return mappedType;
  225. }
  226. private mapFields(fields: any[]): LayoutSectionField[] {
  227. const mappedFields = fields.map((field, index) => {
  228. const mappedField = {
  229. id: field.id || index,
  230. cellPosition: field.cellPosition || '',
  231. name: field.name || '',
  232. dataType: field.dataType || 'string',
  233. dataTypeFormat: field.dataTypeFormat,
  234. importTableColumnName: field.importTableColumnName || field.name || `column_${index}`,
  235. importColumnOrderNumber: field.importColumnOrderNumber || index,
  236. parsedType: this.mapFieldType(field.dataType)
  237. };
  238. return mappedField;
  239. });
  240. return mappedFields;
  241. }
  242. private mapFieldType(dataType: string): FieldTypeEnum {
  243. const type = dataType?.toLowerCase();
  244. const mappedType = (() => {
  245. switch (type) {
  246. case 'time':
  247. return FieldTypeEnum.Time;
  248. case 'decimal':
  249. case 'number':
  250. case 'float':
  251. return FieldTypeEnum.Decimal;
  252. case 'date':
  253. return FieldTypeEnum.Date;
  254. case 'int':
  255. case 'integer':
  256. case 'numeric':
  257. return FieldTypeEnum.Numeric;
  258. default:
  259. return FieldTypeEnum.String;
  260. }
  261. })();
  262. return mappedType;
  263. }
  264. private convertCellValue(dataType: string, dataTypeFormat: string | undefined, value: any, fieldType: FieldTypeEnum): any {
  265. if (value === null || value === undefined) {
  266. return null;
  267. }
  268. const convertedValue = (() => {
  269. if (dataType === "DATE")
  270. return XLSX.SSF.format(dataTypeFormat || 'yyyy-mm-dd', value);
  271. switch (fieldType) {
  272. case FieldTypeEnum.Time:
  273. if (typeof value === 'number') {
  274. // Excel time is fraction of a day
  275. const result = value * 24 * 60 * 60 * 1000; // Convert to milliseconds
  276. return result;
  277. }
  278. return value;
  279. case FieldTypeEnum.Decimal:
  280. const decimalResult = parseFloat(value.toString()) || 0;
  281. return decimalResult;
  282. case FieldTypeEnum.Date:
  283. if (typeof value === 'number') {
  284. // Excel date is days since 1900-01-01
  285. const excelEpoch = new Date(1900, 0, 1);
  286. const dateResult = new Date(excelEpoch.getTime() + (value - 1) * 24 * 60 * 60 * 1000);
  287. return dateResult;
  288. }
  289. const dateResult = new Date(value);
  290. return dateResult;
  291. case FieldTypeEnum.Numeric:
  292. const numericResult = parseInt(value.toString()) || 0;
  293. return numericResult;
  294. case FieldTypeEnum.String:
  295. default:
  296. const stringResult = value.toString();
  297. return stringResult;
  298. }
  299. })();
  300. return convertedValue;
  301. }
  302. }