Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save programmerShinobi/c70d84c6976f5dc93862a2015d14e9c3 to your computer and use it in GitHub Desktop.
Save programmerShinobi/c70d84c6976f5dc93862a2015d14e9c3 to your computer and use it in GitHub Desktop.
Setup exceljs library in nestjs framework
#1 Install dependecies :
- fs-extra
- exceljs
- multer
- @types/multer
- lodash
- class-validator
- class-transformer
- rxjs
#2 Setup types
a. Create src/core/types/string-number-bigint-object.types.ts :
//file: string-number-bigint-object.types.ts--------------------------------------
export type StringNumberBigintObject = 'string' | 'number' | 'bigint' | 'Object' ;
//file: string-number-bigint-object.types.ts--------------------------------------
#3 Setup DTO for your project
a. Create src/modules/your-module/your-module-sheets.dto.ts :
//file: your-module-sheets.dto.ts----------------------------------------------------------------
import { IsNumber, IsObject, IsString, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
import { StringNumberBigintObject } from "../../../core/types/string-number-bigint-object.types";
export class YourModuleSheetsDto {
@IsString()
name: string;
@IsObject()
@ValidateNested()
@Type(() => HeaderDto)
header: {
rows: number
};
@IsObject()
columnToKey: { [key: string]: string };
@IsObject()
@ValidateNested()
@Type(() => ColumnDto)
columns: { [key: string]: ColumnDto };
}
export class HeaderDto {
@IsNumber()
rows: number;
}
export class ColumnDto {
dataType: StringNumberBigintObject;
maxLength?: number;
}
//file: your-module-sheets.dto.ts----------------------------------------------------------------
b. Create src/modules/your-module/dtos/your-module.dto.ts:
//file : items-your-module.dto.ts-----------------------------------------------------------------
import { Type } from "class-transformer";
import { IsDate, IsNotEmpty, IsNumber, IsString, IsUUID } from "class-validator";
import { randomUUID } from "crypto";
import { StringNumberBigintObject } from "src/core/types/string-number-bigint-object.types";
export class ItemsYourModuleDto {
@IsNotEmpty()
@IsUUID()
id: string = randomUUID();
@IsNotEmpty()
@Type(()=> String)
@IsString()
customerName: string;
@IsNotEmpty()
@Type(()=> String)
@IsString()
aircraftRegistration: string;
@Type(()=> String)
@IsString()
aircraftType: string = null;
@Type(()=> String)
@IsString()
apuSn: string = null;
@IsNumber()
apuNegativeScore: number = null;
@IsDate()
createdAt: Date = null;
static propertyConfig:
Partial<Record<keyof ItemsYourModuleDto, { dataType: StringNumberBigintObject; maxLength?: number }>> = {
aircraftRegistration: { dataType: 'string', maxLength: 15 },
aircraftType: { dataType: 'string', maxLength: 30 },
apuSn: { dataType: 'string', maxLength: 30 },
};
static propertyNames: (keyof ItemsYourModuleDto)[] = [
'aircraftRegistration',
'aircraftType',
'apuSn',
];
}
c. Create src/modules/your-module/dtos/read-your-module.dto.ts:
//file : read-your-module.dto.ts-----------------------------
import { ItemsYourModuleDto } from "./items-your-module.dto";
export class ReadYourModuleSheetDto {
yourModule: ItemsYourModuleDto[];
}
//file : read-your-module.dto.ts-----------------------------
#4 Setup errors
a. Create src/core/errors/invalid-data.error.ts :
//file : invalid-data.error.ts-----------------------
export class InvalidDataError extends Error {
constructor(columnName: string, reason: string) {
super(`Column ${columnName}*, ${reason}`);
this.name = 'InvalidDataError';
}
}
export class MessagesInvalidDataError extends Error {
errors: { sheetName: string; invalidColumn: {[key: string]: string}[] }[];
constructor(errors: { sheetName: string; invalidColumn: {[key: string]: string}[] }[]) {
const errorMessage = "Invalid data in one or more sheets";
super(errorMessage);
this.name = 'MessagesInvalidDataError';
this.errors = errors;
}
}
//file : invalid-data.error.ts-----------------------
#5 Setup utils for your project
a. Create src/core/utils/read-excel-sheet-your-module-builder.util.ts:
//file: read-excel-sheet-your-module-builder.util.ts--------------------------------------------------------
import { camelCase } from 'lodash';
import { YourModuleSheetsDto, HeaderDto } from 'src/modules/your-module/dtos/your-module-sheets.dto';
import { StringNumberBigintObject } from '../../types/string-number-bigint-object.types';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ReadExcelSheetYourModuleBuilder {
private sheet: YourModuleSheetsDto;
getSheetName(name:string): this {
this.sheet = new YourModuleSheetsDto();
this.sheet.name = name;
return this
}
ignoreHeaderRow(rows: number = 1): this {
const header = new HeaderDto();
header.rows = rows;
this.sheet.header = header;
return this;
}
setSheetNameToJsonFields(columns: string[]): this {
const columnToKey: Record<string, string> = {};
columns.forEach((col, index): void => {
columnToKey[String.fromCharCode(65 + index)] = camelCase(col);
});
this.sheet.columnToKey = columnToKey;
return this;
}
setColumnPropertyToJsonFields(columns: { [key: string]: { dataType: StringNumberBigintObject; maxLength?: number } }): this {
this.sheet.columns = columns;
return this;
}
build(): YourModuleSheetsDto {
return this.sheet;
}
}
b. Create src/core/utils/your-module-process-excel-to-json-builder.util.ts:
//file: your-module-process-excel-to-json-builder.util.ts--------------------------------------------------------
import { Cell, CellValue, Row, Workbook, Worksheet } from 'exceljs';
import { randomUUID } from 'crypto';
import { ColumnDto, DataSheetsDto } from 'src/modules/your-module/dtos/your-module-sheets.dto';
import { MessagesInvalidDataError } from '../../errors/invalid-data.error';
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { ReadYourModuleSheetDto } from 'src/modules/your-module/dtos/read-your-module-sheet.dto';
import { ItemsYourModuleDto } from 'src/modules/your-module/dtos/items-your-module.dto';
@Injectable()
export class YourModuleProcessExcelToJsonBuilder {
private filePath: string;
private sheets: YourModuleSheetsDto[];
private errorSheetName: string[] = [];
private errorMessages: {[key: string]: string}[] = [];
constructor() {
super(YourModuleProcessExcelToJsonBuilder?.name);
}
getFile(filePath: string): this {
this.filePath = filePath;
this.sheets = [];
this.errorSheetName = [];
this.errorMessages = [];
return this
}
addSheet(sheet: YourModuleSheetsDto): this {
this.sheets.push(sheet);
return this;
}
static validateCellData(cellValue: CellValue, dataType: string, maxLength?: number): boolean {
if (dataType === 'string' && typeof cellValue !== 'string') return false;
if (dataType === 'number' && (typeof cellValue !== 'number' || isNaN(cellValue))) return false;
if (maxLength && cellValue.toLocaleString().length > maxLength) return false;
return true;
}
processCell(cell: Cell, cellIndex: number, rowData: Record<string, CellValue>, sheetConfig: YourModuleSheetsDto): this {
const columnName: string = sheetConfig.columnToKey[String.fromCharCode(64 + cellIndex)];
if (!columnName) return;
const columnConfig: ColumnDto = sheetConfig.columns[columnName];
if (!columnConfig) return;
const { dataType, maxLength }: ColumnDto = columnConfig;
const cellValue: CellValue = cell?.value['result'] || cell?.value['error'] || cell?.value;
// console.log(`column-name : ${columnName}`);
// console.log(`value : ${cellValue}`);
// console.log(`type: ${typeof cellValue}`);
// console.log("______________________________ ");
if (Object(cellValue).toString().startsWith('Invalid')) {
this.errorSheetName.push(sheetConfig.name);
this.errorMessages.push({[`column[${String.fromCharCode(64 + cellIndex)}]`]: `${columnName} cell format categories must be general* on row ${cell.row}`});
return this;
}
if (!YourModuleProcessExcelToJsonBuilder.validateCellData(cellValue, dataType, maxLength)) {
this.errorSheetName.push(sheetConfig.name);
this.errorMessages.push({[`column[${String.fromCharCode(64 + cellIndex)}]`]: `${columnName} must be of type ${dataType === 'Object' ? 'string' : dataType}* or length limit is ${maxLength}* on row ${cell.row}`});
return this;
}
rowData[columnName] = cellValue;
return this;
}
readSheetData<T>(
worksheet: Worksheet,
headerRows: number,
sheetConfig: YourModuleSheetsDto,
processCellFn: (cell: Cell, cellIndex: number, rowData: Record<string, CellValue>, sheetConfig: YourModuleSheetsDto) => this
): T[] {
const rows: T[] = [];
worksheet.eachRow((row: Row, rowIndex: number): void => {
if (rowIndex > headerRows) {
const rowData: Record<string, CellValue> = {};
row.eachCell((cell: Cell, cellIndex: number): void => {
processCellFn(cell, cellIndex, rowData, sheetConfig);
});
rows.push(rowData as T);
}
});
return rows;
}
textToCamelCase(text: string): string {
const words: string[] = text.split(" ");
const camelCaseWords: string[] = [words[0].toLowerCase()];
for (let i = 1; i < words.length; i++) {
camelCaseWords.push(words[i].charAt(0).toUpperCase() + words[i].slice(1).toLowerCase());
}
return camelCaseWords.join("");
}
async build<T>(req: Request): Promise<ReadYourModuleSheetDto> {
const customerName: string = req?.body["customerName"];
const workbook: Workbook = new Workbook();
await workbook.xlsx.readFile(this.filePath);
const data: ReadYourModuleSheetDto = {
yourModule: null,
};
const $dataSheets: Observable<YourModuleSheetsDto> = of(this.sheets).pipe(
mergeMap((items) => items),
mergeMap((sheet) => {
const { name, header } = sheet;
const worksheet: Worksheet = workbook.getWorksheet(name);
if (worksheet) {
const rows = this.readSheetData(worksheet, header.rows, sheet, this.processCell.bind(this));
data[this.textToCamelCase(name)] = rows;
}
return of(sheet);
})
);
await lastValueFrom($dataSheets);
if (this.errorSheetName.length > 0 && this.errorMessages.length > 0) {
const uniqueSheetNames: string[] = [...new Set(this.errorSheetName)];
const errors = uniqueSheetNames.map((sheetName) => {
const sheetErrorMessages: {[key: string]: string}[] = this.errorMessages
.map((msg, index) => this.errorSheetName[index] === sheetName ? msg : null)
.filter(Boolean);
return { sheetName, invalidColumn: sheetErrorMessages };
});
throw new MessagesInvalidDataError(errors);
}
return data;
}
}
//file: read-excel-sheet-your-module-builder.util.ts--------------------------------------------------------
#6. Setup services for your project
a. Create src/modules/your-module/services/your-module.service.ts:
//file : your-module.service.ts-----------------------------------------------------------------------------------
import { Injectable, BadRequestException } from '@nestjs/common';
import { Request } from 'express';
import * as fs from 'fs-extra';
import { catchError, lastValueFrom, mergeMap, of } from 'rxjs';
import { YourModuleSheetsDto } from '../dtos/your-module-sheets.dto';
import { ItemsYourModuleDto } from '../dtos/items-your-module.dto';
import { MessagesInvalidDataError } from 'src/core/errors/invalid-data.error';
import { ReadExcelSheetYourModuleBuilder } from 'src/core/utils/read-excel-sheet-your-module-builder.util';
import { EngineApuProcessExcelToJsonBuilder } from 'src/core/utils/your-module-process-excel-to-json-builder.util';
import { ReadYourModuleSheetDto } from '../dtos/read-your-module-sheet.dto';
@Injectable()
export class ExcelYourModuleService {
constructor(
private readonly readYourModuleBuilder: ReadExcelSheetYourModuleBuilder,
private readonly processExcelToJsonBuilder: YourModuleProcessExcelToJsonBuilder,
) {
super(ExcelYourModuleService?.name);
}
async readFormatExcel (req: Request): Promise<ReadYourModuleSheetDto> {
try {
const yourModuleSheet: YourModuleSheetsDto = this.readExcelSheetBuilder
.getSheetName('your sheetname')
.ignoreHeaderRow()
.setSheetNameToJsonFields(ItemsYourModuleDto.propertyNames)
.setColumnPropertyToJsonFields(ItemsYourModuleDto.propertyConfig)
.build();
const filePath: string = req?.file?.path;
const data = of(filePath).pipe(
catchError((error) => {
throw new BadRequestException(error);
}),
mergeMap(() => this.processExcelToJsonBuilder
.getFile(filePath)
.addSheet(yourModuleSheet)
.build(req)
)
);
const result = await lastValueFrom(data);
await fs.remove(filePath);
return result;
} catch (error) {
const filePath: string = req?.file?.path;
if (filePath) fs.remove(filePath);
if (error instanceof MessagesInvalidDataError) {
const errorResponse = {
status: 400,
message: error?.errors,
error: "Bad Request"
};
throw new BadRequestException(errorResponse);
} else {
throw new BadRequestException(error?.response);
}
}
}
}
//file : your-module.service.ts-----------------------------------------------------------------------------------
b. Create src/modules/your-module/services/your-module.service.ts:
import { BadRequestException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { format } from 'date-fns';
import { ItemsYourModuleDto } from '../dtos/items-your-module.dto';
import { ExcelYourModuleService } from './excel-your-module.service';
import { ReadYourModuleSheetDto } from '../dtos/read-your-module-sheet.dto';
@Injectable()
export class YourModuleService {
constructor(
private readonly excelService: ExcelEngineYourModuleService,
) {
super(YourModuleService?.name);
}
async convertYourModuleFromExcelToJson<T>(req: Request): Promise<WriteResponseBase> {
try {
const read: ReadYourModuleSheetDto = await this.excelService.readFormatExcel(req);
if (!read?.yourModule) throw new BadRequestException(`Failed to read Excel, sheetname invalid`);
const items: ItemsYourModuleDto[] = read?.yourModule;
return items;
} catch (error) {
throw new BadRequestException(error?.response);
}
}
}
#7. Setup config multer file for your project
a. Create src/config/multer-options.config.ts :
//file : multer-options.config.ts-------------------------------------------------------
import { extname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage } from 'multer';
import { v4 as uuid } from 'uuid';
import { BadRequestException, HttpCode, HttpException, HttpStatus } from '@nestjs/common';
import { Request } from 'express';
// Multer configuration
export const multerConfig = {
dest: process.env.UPLOAD_LOCATION,
};
// Multer upload options
export const multerOptions = {
// Enable file size limits
limits: {
fileSize: +process.env.MAX_FILE_SIZE,
},
// Check the mimetypes to allow for upload
fileFilter: (req: Request, file: Express.Multer.File, cb: Function) => {
// Allow storage of file
if (/\.(xlsx|xls|xlsm|xlsb|xlt|csv|ods)$/i.test(file?.originalname)) cb(null, true);
// Reject file
else cb(new BadRequestException(`Unsupported file type ${extname(file.originalname)}`), false);
},
// Storage properties
storage: diskStorage({
// Destination storage path details
destination: (req: Request, file: Express.Multer.File, cb: Function) => {
const uploadPath = multerConfig.dest;
// Create folder if doesn't exist
if (!existsSync(uploadPath)) mkdirSync(uploadPath);
cb(null, uploadPath);
},
// File modification details
filename: (req: Request, file: Express.Multer.File, cb: Function) => {
const uploadedFileName = `${uuid()}${extname(file.originalname)}`;
// Validation customerName & configName
if (!req?.body?.customerName && !req?.body?.configName)
cb(new BadRequestException([
`customerName should not be empty`,
`configName should not be empty`
]), false);
else if (!req?.body?.customerName) cb(new BadRequestException([`customerName should not be empty`]), false);
else if (!req?.body?.configName) cb(new BadRequestException([`configName should not be empty`]), false);
else
// Calling the callback passing the random name generated with the original extension name
cb(null, uploadedFileName);
},
}),
};
//file : multer-options.config.ts-------------------------------------------------------
#8. Setup controllers for your project
a. Create src/modules/controllers/your-module.controller.ts:
// file : your-module.controller.ts------------------------------------------------------------
import {
Body,
Controller,
Delete,
Param,
Post,
Req,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response, Request } from 'express';
import { multerOptions } from 'src/config/multer-options.config';
import { YourModuleService } from './services/your-module.service';
@Controller()
export default class YourModuleController
{
constructor(
private readonly yourModuleService: YourModuleService,
) {
super(YourModuleController.name);
}
@Post('your-module')
@UseInterceptors(FileInterceptor('file', multerOptions))
async uploadDataBleed<T>(
@UploadedFile() file: Express.Multer.File,
@Req() req: Request,
@Res() res: Response,
): Promise<Response<T, Record<string, T>>> {
try {
const payload = await this.yourModuleService.convertYourModuleFromExcelToJson(req);
return res.status(201).json({
data: payload,
meta: {
fileName: req?.file?.originalname,
status: 'OK',
},
message: 'Data has been converted & saved',
time: new Date()
});
} catch (error) {
return res.status(400).json(error.response);
}
}
}
// file : your-module.controller.ts------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment