diff --git a/src/app.module.ts b/src/app.module.ts index 2b69590..df88a81 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,21 +1,14 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { typeOrmConfig } from './config/typeorm.config'; -import { ListPrinterController } from './controller/list-printer.controller'; -import { ListPrinterService } from './services/create-printer'; +import { PrinterController } from './controller/printer.controller'; +import { PrinterService } from './services/printer.service'; import { PrinterProcessor } from './services/printer.processor'; -import { BullQueuesModule } from './config/BullModule'; +import { InfrastructureModule } from './infrastructure/infrastructure.module'; @Module({ - imports: [ - ConfigModule.forRoot({ isGlobal: true }), - TypeOrmModule.forRoot(typeOrmConfig), - BullQueuesModule - ], - controllers: [AppController, ListPrinterController], - providers: [AppService, ListPrinterService, PrinterProcessor], + imports: [InfrastructureModule], + controllers: [AppController, PrinterController], + providers: [AppService, PrinterService, PrinterProcessor], }) export class AppModule {} diff --git a/src/config/BullModule.ts b/src/config/BullModule.ts index a5dedce..e360332 100644 --- a/src/config/BullModule.ts +++ b/src/config/BullModule.ts @@ -5,15 +5,23 @@ import { ExpressAdapter } from '@bull-board/express'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { ConfigModule, ConfigService } from '@nestjs/config'; +function stripWrappingQuotes(value: string | undefined): string | undefined { + if (value == null) return undefined; + const trimmed = value.trim(); + return trimmed.replace(/^['"](.*)['"]$/, '$1'); +} + @Module({ imports: [ BullModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ connection: { - host: configService.get('REDIS_HOST'), - port: configService.get('REDIS_PORT') || 6379, - password: configService.get('REDIS_PASSWORD'), + host: stripWrappingQuotes(configService.get('REDIS_HOST')), + port: parseInt(configService.get('REDIS_PORT') ?? '6379', 10), + password: stripWrappingQuotes( + configService.get('REDIS_PASSWORD'), + ), }, }), inject: [ConfigService], @@ -32,7 +40,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; type: 'exponential', delay: 1000, }, - removeOnComplete: false, + removeOnComplete: false, removeOnFail: { count: 500 }, }, }), @@ -44,4 +52,4 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; ], exports: [BullModule], }) -export class BullQueuesModule {} \ No newline at end of file +export class BullQueuesModule {} diff --git a/src/config/typeorm.config.ts b/src/config/typeorm.config.ts index 38d27ca..0d918ca 100644 --- a/src/config/typeorm.config.ts +++ b/src/config/typeorm.config.ts @@ -1,20 +1,52 @@ -import { DataSource, DataSourceOptions } from 'typeorm'; +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { DataSourceOptions } from 'typeorm'; -export const typeOrmConfig: DataSourceOptions = { - type: 'oracle', - username: process.env.DB_USERNAME || 'teste', - password: process.env.DB_PASSWORD || 'teste', - connectString: - process.env.DB_CONNECT_STRING || - '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=BDTESTE)))', - synchronize: false, - logging: true, - entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'], - subscribers: [process.env.DB_SUBSCRIBERS_PATH || 'dist/subscriber/*.js'], - extra: { - poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10), - maxRows: parseInt(process.env.DB_MAX_ROWS || '1000', 10), - }, -}; +export function createTypeOrmOptions( + configService: ConfigService, +): TypeOrmModuleOptions { + const baseOptions: DataSourceOptions = { + type: 'oracle', + username: configService.get('DB_USERNAME') || 'teste', + password: configService.get('DB_PASSWORD') || 'teste', + connectString: + configService.get('DB_CONNECT_STRING') || + '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=10.1.1.241)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=BDTESTE)))', + synchronize: false, + logging: true, + entities: [__dirname + '/../**/*.{entity,view}.{js,ts}'], + subscribers: [ + configService.get('DB_SUBSCRIBERS_PATH') || + 'dist/subscriber/*.js', + ], + extra: { + poolSize: parseInt(configService.get('DB_POOL_SIZE') || '10', 10), + maxRows: parseInt(configService.get('DB_MAX_ROWS') || '1000', 10), + }, + }; -export const AppDataSource = new DataSource(typeOrmConfig); + const retryAttempts = parseInt( + configService.get('DB_RETRY_ATTEMPTS') || '5', + 10, + ); + const retryDelayMs = parseInt( + configService.get('DB_RETRY_DELAY_MS') || '2000', + 10, + ); + + const logger = new Logger('TypeORM'); + + logger.debug( + `TypeORM configured for manual initialization (retryAttempts=${retryAttempts}, retryDelayMs=${retryDelayMs}).`, + ); + + return { + ...baseOptions, + + manualInitialization: true, + + retryAttempts: 0, + retryDelay: 0, + }; +} diff --git a/src/controller/list-printer.controller.ts b/src/controller/printer.controller.ts similarity index 72% rename from src/controller/list-printer.controller.ts rename to src/controller/printer.controller.ts index 590d7a9..3ba5e95 100644 --- a/src/controller/list-printer.controller.ts +++ b/src/controller/printer.controller.ts @@ -1,6 +1,14 @@ -import { Controller, Post, Body, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; +import { + Controller, + Post, + Body, + Get, + HttpCode, + HttpStatus, + Param, +} from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ListPrinterService } from '../services/create-printer'; +import { PrinterService } from '../services/printer.service'; import { PrintDataDto } from '../dto/print-data.dto'; import { PrintHtmlDto } from '../dto/print-html.dto'; import { PrinterDto } from '../dto/printer.dto'; @@ -8,16 +16,18 @@ import { getPrinters } from '../services/get-printer'; @ApiTags('Printer') @Controller('printer') -export class ListPrinterController { - constructor(private readonly printerService: ListPrinterService) {} +export class PrinterController { + constructor(private readonly printerService: PrinterService) {} @Post(':printerName/print') @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Envia um comando de impressão de texto simples (ESC/POS)' }) + @ApiOperation({ + summary: 'Envia um comando de impressão de texto simples (ESC/POS)', + }) @ApiResponse({ status: 200, description: 'Impressão enviada com sucesso' }) async print( @Param('printerName') printerName: string, - @Body() printData: PrintDataDto + @Body() printData: PrintDataDto, ) { printData.printerInterface = printerName; return this.printerService.printReceipt(printData); @@ -32,11 +42,14 @@ export class ListPrinterController { @Post(':printerName/print-html') @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Converte HTML para PDF e imprime na impressora especificada na URL' }) + @ApiOperation({ + summary: + 'Converte HTML para PDF e imprime na impressora especificada na URL', + }) @ApiResponse({ status: 200, description: 'HTML enviado para impressão' }) async printHtml( @Param('printerName') printerName: string, - @Body() printHtmlDto: PrintHtmlDto + @Body() printHtmlDto: PrintHtmlDto, ) { printHtmlDto.printerName = printerName; return this.printerService.printHtml(printHtmlDto); @@ -51,10 +64,10 @@ export class ListPrinterController { @Get('list') @ApiOperation({ summary: 'Lista impressoras disponíveis no sistema' }) - @ApiResponse({ - status: 200, + @ApiResponse({ + status: 200, description: 'Lista de impressoras encontradas', - type: [PrinterDto] + type: [PrinterDto], }) async listPrinters() { return getPrinters(); diff --git a/src/database/database.module.ts b/src/database/database.module.ts new file mode 100644 index 0000000..b17a942 --- /dev/null +++ b/src/database/database.module.ts @@ -0,0 +1,31 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { createTypeOrmOptions } from '../config/typeorm.config'; +import { parseBooleanEnv } from '../helpers/env.helper'; +import { OracleDbInitializer } from './oracle-db.initializer'; + +@Module({}) +export class DatabaseModule { + static register(): DynamicModule { + const dbEnabled = parseBooleanEnv(process.env.DB_ENABLED, true); + + if (!dbEnabled) { + return { module: DatabaseModule }; + } + + return { + module: DatabaseModule, + imports: [ + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => + createTypeOrmOptions(configService), + }), + ], + providers: [OracleDbInitializer], + exports: [TypeOrmModule], + }; + } +} diff --git a/src/database/oracle-db.initializer.ts b/src/database/oracle-db.initializer.ts new file mode 100644 index 0000000..89da2d9 --- /dev/null +++ b/src/database/oracle-db.initializer.ts @@ -0,0 +1,64 @@ +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DataSource } from 'typeorm'; +import { parseBooleanEnv } from '../helpers/env.helper'; + +@Injectable() +export class OracleDbInitializer implements OnApplicationBootstrap { + private readonly logger = new Logger(OracleDbInitializer.name); + + constructor( + private readonly dataSource: DataSource, + private readonly configService: ConfigService, + ) {} + + onApplicationBootstrap(): void { + const enabled = parseBooleanEnv( + this.configService.get('DB_ENABLED'), + true, + ); + if (!enabled) { + this.logger.log('DB initialization skipped (DB_ENABLED=false).'); + return; + } + + if (this.dataSource.isInitialized) return; + + void this.initializeWithRetry(); + } + + private async initializeWithRetry(): Promise { + const attempts = parseInt( + this.configService.get('DB_RETRY_ATTEMPTS') || '5', + 10, + ); + const baseDelayMs = parseInt( + this.configService.get('DB_RETRY_DELAY_MS') || '2000', + 10, + ); + + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + await this.dataSource.initialize(); + this.logger.log( + `Oracle DB connected (attempt ${attempt}/${attempts}).`, + ); + return; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + this.logger.warn( + `Oracle DB connection failed (attempt ${attempt}/${attempts}): ${message}`, + ); + + if (attempt < attempts) { + const delay = baseDelayMs * attempt; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + this.logger.error( + `Oracle DB unreachable after ${attempts} attempts; continuing without DB (degraded mode).`, + ); + } +} diff --git a/src/helpers/env.helper.ts b/src/helpers/env.helper.ts new file mode 100644 index 0000000..cbc9828 --- /dev/null +++ b/src/helpers/env.helper.ts @@ -0,0 +1,15 @@ +export function parseBooleanEnv( + value: string | undefined, + defaultValue: boolean, +): boolean { + if (value == null) return defaultValue; + + const normalized = value + .trim() + .replace(/^['"]|['"]$/g, '') + .toLowerCase(); + + if (normalized === 'true') return true; + if (normalized === 'false') return false; + return defaultValue; +} diff --git a/src/infrastructure/infrastructure.module.ts b/src/infrastructure/infrastructure.module.ts new file mode 100644 index 0000000..ec2a30d --- /dev/null +++ b/src/infrastructure/infrastructure.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { BullQueuesModule } from '../config/BullModule'; +import { DatabaseModule } from '../database/database.module'; + +@Global() +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + DatabaseModule.register(), + BullQueuesModule, + ], + exports: [BullQueuesModule, DatabaseModule], +}) +export class InfrastructureModule {} diff --git a/src/main.ts b/src/main.ts index 5cf35a1..cdbf065 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,21 @@ import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { json, urlencoded } from 'express'; import { AppModule } from './app.module'; import { initializeOracleClient } from './helpers/oracle.helper'; +function getBodyLimit(): string { + const raw = process.env.BODY_LIMIT || '2mb'; + return raw.trim().replace(/^['"]|['"]$/g, ''); +} + async function bootstrap() { initializeOracleClient(); - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bodyParser: false }); + + const bodyLimit = getBodyLimit(); + app.use(json({ limit: bodyLimit })); + app.use(urlencoded({ extended: true, limit: bodyLimit })); const config = new DocumentBuilder() .setTitle('API Documentation') diff --git a/src/services/create-printer.ts b/src/services/printer.service.ts similarity index 73% rename from src/services/create-printer.ts rename to src/services/printer.service.ts index e56fd20..8e4681c 100644 --- a/src/services/create-printer.ts +++ b/src/services/printer.service.ts @@ -6,12 +6,14 @@ import { PrintHtmlDto } from '../dto/print-html.dto'; import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer'; @Injectable() -export class ListPrinterService { +export class PrinterService { private readonly printerType = PrinterTypes.EPSON; constructor(@InjectQueue('printer') private printerQueue: Queue) {} - async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> { + async printReceipt( + data: PrintDataDto, + ): Promise<{ success: boolean; message: string }> { if (!data.lines?.length) { return { success: false, message: 'No lines provided' }; } @@ -19,7 +21,11 @@ export class ListPrinterService { try { let printerInterface = data.printerInterface; - if (printerInterface && !printerInterface.includes('://') && !printerInterface.startsWith('printer:')) { + if ( + printerInterface && + !printerInterface.includes('://') && + !printerInterface.startsWith('printer:') + ) { printerInterface = `printer:${printerInterface}`; } @@ -44,12 +50,12 @@ export class ListPrinterService { printer.beep(); await printer.execute(); - + return { success: true, message: 'Print successful!' }; } catch (error) { - return { - success: false, - message: `Print failed: ${error.message || error}` + return { + success: false, + message: `Print failed: ${error.message || error}`, }; } } @@ -58,7 +64,7 @@ export class ListPrinterService { if (alignment === 'center') { return printer.alignCenter(); } - + if (alignment === 'right') { return printer.alignRight(); } @@ -66,20 +72,24 @@ export class ListPrinterService { return printer.alignLeft(); } - async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string; jobId?: string }> { + async printHtml( + data: PrintHtmlDto, + ): Promise<{ success: boolean; message: string; jobId?: string }> { const jobOptions: any = {}; - + if (data.jobId) { - const safeJobId = /^\d+$/.test(data.jobId) ? `print-${data.jobId}` : data.jobId; + const safeJobId = /^\d+$/.test(data.jobId) + ? `print-${data.jobId}` + : data.jobId; jobOptions.jobId = safeJobId; } const job = await this.printerQueue.add('print-html-job', data, jobOptions); - return { - success: true, + return { + success: true, message: 'Tarefa enviada para a fila de impressão', - jobId: job.id + jobId: job.id, }; } -} \ No newline at end of file +}