From c18f1b90885ed286bef531a5f2df621e740989c8 Mon Sep 17 00:00:00 2001 From: Joelbrit0 Date: Mon, 26 Jan 2026 18:31:11 -0300 Subject: [PATCH 1/4] chore: ignore zed workspace files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5c69b1f..2de8d20 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,6 @@ Temporary Items dist .webpack .serverless/**/*.zip + +# Zed editor workspace +.zed/ From 4c5e5bdf5a5f7edabb902cce66219deb579fdea6 Mon Sep 17 00:00:00 2001 From: Joelbrit0 Date: Mon, 26 Jan 2026 18:31:32 -0300 Subject: [PATCH 2/4] refactor: centralize infrastructure and organize printer modules --- src/app.module.ts | 19 ++---- src/config/BullModule.ts | 18 +++-- src/config/typeorm.config.ts | 68 ++++++++++++++----- ...er.controller.ts => printer.controller.ts} | 35 +++++++--- src/database/database.module.ts | 31 +++++++++ src/database/oracle-db.initializer.ts | 64 +++++++++++++++++ src/helpers/env.helper.ts | 15 ++++ src/infrastructure/infrastructure.module.ts | 15 ++++ src/main.ts | 12 +++- .../{create-printer.ts => printer.service.ts} | 40 +++++++---- 10 files changed, 254 insertions(+), 63 deletions(-) rename src/controller/{list-printer.controller.ts => printer.controller.ts} (72%) create mode 100644 src/database/database.module.ts create mode 100644 src/database/oracle-db.initializer.ts create mode 100644 src/helpers/env.helper.ts create mode 100644 src/infrastructure/infrastructure.module.ts rename src/services/{create-printer.ts => printer.service.ts} (73%) 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 +} From ef83703f5a1908997d8f660c065ff626f7b937f6 Mon Sep 17 00:00:00 2001 From: Joelbrit0 Date: Mon, 26 Jan 2026 18:31:37 -0300 Subject: [PATCH 3/4] refactor: reuse puppeteer browser and log print job phases --- src/services/printer.processor.ts | 198 ++++++++++++++++++++++++++---- 1 file changed, 176 insertions(+), 22 deletions(-) diff --git a/src/services/printer.processor.ts b/src/services/printer.processor.ts index c235893..e02bc83 100644 --- a/src/services/printer.processor.ts +++ b/src/services/printer.processor.ts @@ -1,26 +1,113 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Logger, OnApplicationShutdown } from '@nestjs/common'; import { Job } from 'bullmq'; +import { performance } from 'node:perf_hooks'; import * as puppeteer from 'puppeteer'; import * as pdfPrinter from 'pdf-to-printer'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; +import { PrintHtmlDto } from '../dto/print-html.dto'; @Processor('printer') -export class PrinterProcessor extends WorkerHost { - - async process(job: Job): Promise { +export class PrinterProcessor + extends WorkerHost + implements OnApplicationShutdown +{ + private readonly logger = new Logger(PrinterProcessor.name); + private browser: puppeteer.Browser | undefined; + private browserLaunching: Promise | undefined; + + private async getBrowser(): Promise { + if (this.browser?.isConnected()) return this.browser; + if (this.browserLaunching) return this.browserLaunching; + + this.browserLaunching = puppeteer + .launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }) + .then((browser) => { + this.browser = browser; + return browser; + }) + .finally(() => { + this.browserLaunching = undefined; + }); + + return this.browserLaunching; + } + + async onApplicationShutdown(): Promise { + try { + await this.browser?.close(); + } catch (err) { + this.logger.warn(`Failed to close Puppeteer browser: ${String(err)}`); + } finally { + this.browser = undefined; + } + } + + private logJob( + level: 'log' | 'warn' | 'error' | 'debug', + payload: Record, + error?: unknown, + ): void { + const message = JSON.stringify(payload); + if (level === 'error') { + const stack = error instanceof Error ? error.stack : undefined; + this.logger.error(message, stack); + return; + } + + if (level === 'warn') { + this.logger.warn(message); + return; + } + + if (level === 'debug') { + this.logger.debug(message); + return; + } + + this.logger.log(message); + } + + async process(job: Job): Promise { const data = job.data; - const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`); - let browser; + const printerName = data?.printerName; + const jobCtx = { + jobId: job.id, + jobName: job.name, + printerName, + }; + + const tempFilePath = path.join( + os.tmpdir(), + `print-${job.id}-${Date.now()}.pdf`, + ); + let page: puppeteer.Page | undefined; + let phase: 'render' | 'pdf' | 'spool' | 'unknown' = 'unknown'; + + const durationsMs: { + render?: number; + pdf?: number; + spool?: number; + total?: number; + } = {}; + + const totalStart = performance.now(); + + this.logJob('log', { + event: 'print_job_start', + ...jobCtx, + width: data?.width, + height: data?.height, + }); try { - browser = await puppeteer.launch({ - headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'] - }); - - const page = await browser.newPage(); + const browser = await this.getBrowser(); + page = await browser.newPage(); const width = data.width || '80mm'; const height = data.height; @@ -40,30 +127,97 @@ export class PrinterProcessor extends WorkerHost { `; + phase = 'render'; + const renderStart = performance.now(); await page.setContent(fullHtml, { waitUntil: 'networkidle0' }); - - const pdfOptions: any = { - path: tempFilePath, + durationsMs.render = performance.now() - renderStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.render, + ...jobCtx, + }); + + const pdfOptions: any = { + path: tempFilePath, printBackground: true, width: width, - margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' } + margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }, }; if (height) pdfOptions.height = height; + phase = 'pdf'; + const pdfStart = performance.now(); await page.pdf(pdfOptions); - await browser.close(); + durationsMs.pdf = performance.now() - pdfStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.pdf, + ...jobCtx, + }); - await pdfPrinter.print(tempFilePath, { + await page.close(); + page = undefined; + + phase = 'spool'; + const spoolStart = performance.now(); + await pdfPrinter.print(tempFilePath, { printer: data.printerName, - silent: true + silent: true, + }); + durationsMs.spool = performance.now() - spoolStart; + this.logJob('log', { + event: 'print_job_phase', + phase, + durationMs: durationsMs.spool, + ...jobCtx, + }); + + durationsMs.total = performance.now() - totalStart; + this.logJob('log', { + event: 'print_job_success', + ...jobCtx, + durationsMs, }); return { status: 'success', jobId: job.id }; - } catch (error) { - if (browser) await browser.close(); - console.error(`Erro no Job ${job.id}:`, error); + if (page) { + try { + await page.close(); + } catch { + // ignore + } + } + + const message = error instanceof Error ? error.message : String(error); + durationsMs.total = performance.now() - totalStart; + this.logJob( + 'error', + { + event: 'print_job_error', + ...jobCtx, + phase, + durationsMs, + errorMessage: message, + errorType: error instanceof Error ? error.name : typeof error, + }, + error, + ); + + // If the shared browser crashed/closed, reset it so next job can relaunch. + if (/Target closed|Browser closed|Protocol error/i.test(message)) { + try { + await this.browser?.close(); + } catch { + // ignore + } finally { + this.browser = undefined; + } + } + throw error; } finally { if (fs.existsSync(tempFilePath)) { @@ -71,4 +225,4 @@ export class PrinterProcessor extends WorkerHost { } } } -} \ No newline at end of file +} From 258b97eb1554918a60f0cf0609cc464ed1656f8a Mon Sep 17 00:00:00 2001 From: Joelbrit0 Date: Mon, 26 Jan 2026 18:31:41 -0300 Subject: [PATCH 4/4] docs: update API contracts for queued printing --- API_CONTRACTS.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/API_CONTRACTS.md b/API_CONTRACTS.md index d3f041d..8211e10 100644 --- a/API_CONTRACTS.md +++ b/API_CONTRACTS.md @@ -37,33 +37,35 @@ Retorna a lista de impressoras instaladas no servidor onde a API está rodando. ### 2. Imprimir HTML (Etiquetas/Layouts) -Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impressora selecionada. Ideal para etiquetas. +Converte um código HTML (com suporte a Tailwind CSS) para PDF e imprime na impressora selecionada via Fila de Processamento (BullMQ). - **Método:** `POST` -- **Rota:** `/printer/print-html` +- **Rota:** `/printer/print-html` ou `/printer/:printerName/print-html` - **Corpo da Requisição (JSON):** ```json { "html": "
Minha Etiqueta
...", - "printerName": "POS-80", // Nome exato da impressora (conforme retornado na listagem) + "printerName": "POS-80", // Opcional se passado na URL "width": "60mm", // Opcional. Largura do papel/etiqueta. Default: 80mm - "height": "40mm" // Opcional. Altura da etiqueta. Default: auto + "height": "40mm", // Opcional. Altura da etiqueta. Default: auto + "jobId": "meu-id-unico-123" // Opcional. ID para idempotência na fila. } ``` - **Detalhes:** - O backend injeta automaticamente o script do Tailwind CSS. - O HTML é renderizado via Puppeteer (Chrome headless). + - Tarefas são processadas de forma assíncrona via Redis/BullMQ. --- ### 3. Imprimir Texto Simples (ESC/POS) -Envia comandos de texto diretamente para a impressora. Ideal para cupons simples e rápidos, sem formatação complexa. +Envia comandos de texto diretamente para a impressora. Ideal para cupons simples e rápidos. - **Método:** `POST` -- **Rota:** `/printer/print` +- **Rota:** `/printer/print` ou `/printer/:printerName/print` - **Corpo da Requisição (JSON):** ```json @@ -76,7 +78,7 @@ Envia comandos de texto diretamente para a impressora. Ideal para cupons simples ], "alignment": "center", // "left", "center", "right". Default: left "upsideDown": false, // Se true, imprime de cabeça para baixo - "printerInterface": "POS-80" // Pode ser nome da impressora (USB) ou "tcp://192.168.1.100" (Rede) + "printerInterface": "POS-80" // Opcional se passado na URL. Suporta "tcp://192.168.1.100" ou nome da impressora. } ``` @@ -98,6 +100,7 @@ export interface PrintHtmlPayload { printerName: string; width?: string; height?: string; + jobId?: string; } export interface PrintTextPayload { @@ -107,3 +110,7 @@ export interface PrintTextPayload { printerInterface?: string; } ``` + +## Swagger + +A documentação interativa completa (OpenAPI) está disponível em `/api`. Lá você pode testar todos os endpoints e ver os esquemas detalhados de cada DTO.