rl-core-api 0.16.8 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/export/export.constants.d.ts +1 -0
- package/dist/core/export/export.constants.js +4 -0
- package/dist/core/export/export.controller.js +8 -7
- package/dist/core/export/export.module.js +13 -2
- package/dist/core/export/export.processor.d.ts +5 -2
- package/dist/core/export/export.processor.js +30 -8
- package/dist/core/export/export.service.d.ts +2 -0
- package/dist/core/export/export.service.js +29 -9
- package/dist/core/export/exportDocument.interface.d.ts +19 -0
- package/dist/core/export/exportDocument.interface.js +7 -0
- package/dist/core/export/exportExtension.enum.d.ts +5 -0
- package/dist/core/export/exportExtension.enum.js +12 -0
- package/dist/core/export/exportHandler.interface.d.ts +3 -1
- package/dist/core/export/exportHandler.registry.d.ts +12 -2
- package/dist/core/export/exportHandler.registry.js +26 -10
- package/dist/core/export/exportKind.enum.d.ts +4 -0
- package/dist/core/export/exportKind.enum.js +8 -0
- package/dist/core/export/exportStorage.service.d.ts +5 -4
- package/dist/core/export/exportStorage.service.js +9 -9
- package/dist/core/export/pdf/pdf.constants.d.ts +32 -0
- package/dist/core/export/pdf/pdf.constants.js +32 -0
- package/dist/core/export/pdf/pdfColor.util.d.ts +1 -0
- package/dist/core/export/pdf/pdfColor.util.js +15 -0
- package/dist/core/export/pdf/pdfDocument.service.d.ts +28 -0
- package/dist/core/export/pdf/pdfDocument.service.js +243 -0
- package/dist/core/export/pdf/pdfFonts.util.d.ts +3 -0
- package/dist/core/export/pdf/pdfFonts.util.js +14 -0
- package/dist/core/export/pdf/pdfTheme.interface.d.ts +12 -0
- package/dist/core/export/pdf/pdfTheme.interface.js +2 -0
- package/dist/core/export/requestExport.command.d.ts +1 -0
- package/dist/core/export/requestExport.command.js +9 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +17 -2
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const EXPORT_PARAMS_MAX_LENGTH = 4000;
|
|
@@ -20,11 +20,11 @@ const currentUser_decorator_1 = require("../auth/decorators/currentUser.decorato
|
|
|
20
20
|
const permissions_guard_1 = require("../auth/guards/permissions.guard");
|
|
21
21
|
const export_service_1 = require("./export.service");
|
|
22
22
|
const exportAccepted_response_1 = require("./exportAccepted.response");
|
|
23
|
+
const exportExtension_enum_1 = require("./exportExtension.enum");
|
|
23
24
|
const exportStatus_response_1 = require("./exportStatus.response");
|
|
24
25
|
const exportStorage_service_1 = require("./exportStorage.service");
|
|
25
26
|
const requestExport_command_1 = require("./requestExport.command");
|
|
26
|
-
const
|
|
27
|
-
const sanitizeFileName = (name) => name.replace(/["\\/\r\n]+/g, "_").slice(0, 120) || "relatorio.xlsx";
|
|
27
|
+
const sanitizeFileName = (name, extension) => name.replace(/["\\/\r\n]+/g, "_").slice(0, 120) || `relatorio.${extension}`;
|
|
28
28
|
let ExportController = class ExportController {
|
|
29
29
|
constructor(exports, storage) {
|
|
30
30
|
this.exports = exports;
|
|
@@ -52,13 +52,14 @@ let ExportController = class ExportController {
|
|
|
52
52
|
if (status.state !== "completed" || !status.summary) {
|
|
53
53
|
throw new common_1.ConflictException("O relatório ainda está sendo gerado");
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
const extension = status.summary.extension ?? exportExtension_enum_1.ExportExtension.Xlsx;
|
|
56
|
+
if (!(await this.storage.exists(user.id, jobId, extension))) {
|
|
56
57
|
throw new common_1.GoneException("Este relatório expirou. Gere novamente para baixar.");
|
|
57
58
|
}
|
|
58
|
-
const fileName = sanitizeFileName(status.summary.fileName);
|
|
59
|
-
response.setHeader("Content-Type",
|
|
59
|
+
const fileName = sanitizeFileName(status.summary.fileName, extension);
|
|
60
|
+
response.setHeader("Content-Type", exportExtension_enum_1.EXPORT_CONTENT_TYPES[extension]);
|
|
60
61
|
response.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
|
61
|
-
return new common_1.StreamableFile((0, fs_1.createReadStream)(this.storage.pathFor(user.id, jobId)));
|
|
62
|
+
return new common_1.StreamableFile((0, fs_1.createReadStream)(this.storage.pathFor(user.id, jobId, extension)));
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
exports.ExportController = ExportController;
|
|
@@ -87,7 +88,7 @@ __decorate([
|
|
|
87
88
|
__decorate([
|
|
88
89
|
(0, common_1.Get)(":jobId"),
|
|
89
90
|
(0, swagger_1.ApiParam)({ name: "jobId", example: "184" }),
|
|
90
|
-
(0, swagger_1.ApiOkResponse)({ description: "
|
|
91
|
+
(0, swagger_1.ApiOkResponse)({ description: "O arquivo gerado", type: common_1.StreamableFile }),
|
|
91
92
|
(0, swagger_1.ApiResponse)({ status: 409, description: "Ainda em geração" }),
|
|
92
93
|
(0, swagger_1.ApiResponse)({ status: 410, description: "O arquivo expirou" }),
|
|
93
94
|
__param(0, (0, currentUser_decorator_1.CurrentUser)()),
|
|
@@ -12,13 +12,24 @@ const core_1 = require("@nestjs/core");
|
|
|
12
12
|
const exportEngine_service_1 = require("./exportEngine.service");
|
|
13
13
|
const exportHandler_registry_1 = require("./exportHandler.registry");
|
|
14
14
|
const exportStorage_service_1 = require("./exportStorage.service");
|
|
15
|
+
const pdfDocument_service_1 = require("./pdf/pdfDocument.service");
|
|
15
16
|
let ExportModule = class ExportModule {
|
|
16
17
|
};
|
|
17
18
|
exports.ExportModule = ExportModule;
|
|
18
19
|
exports.ExportModule = ExportModule = __decorate([
|
|
19
20
|
(0, common_1.Module)({
|
|
20
21
|
imports: [core_1.DiscoveryModule],
|
|
21
|
-
providers: [
|
|
22
|
-
|
|
22
|
+
providers: [
|
|
23
|
+
exportHandler_registry_1.ExportHandlerRegistry,
|
|
24
|
+
exportEngine_service_1.ExportEngineService,
|
|
25
|
+
exportStorage_service_1.ExportStorageService,
|
|
26
|
+
pdfDocument_service_1.PdfDocumentService,
|
|
27
|
+
],
|
|
28
|
+
exports: [
|
|
29
|
+
exportHandler_registry_1.ExportHandlerRegistry,
|
|
30
|
+
exportEngine_service_1.ExportEngineService,
|
|
31
|
+
exportStorage_service_1.ExportStorageService,
|
|
32
|
+
pdfDocument_service_1.PdfDocumentService,
|
|
33
|
+
],
|
|
23
34
|
})
|
|
24
35
|
], ExportModule);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Logger } from "@nestjs/common";
|
|
2
|
+
import { DocumentJobPayload } from "./exportDocument.interface";
|
|
2
3
|
import { ExportEngineService } from "./exportEngine.service";
|
|
3
4
|
import { ExportJobPayload, ExportJobSummary } from "./exportHandler.interface";
|
|
4
5
|
import { ExportHandlerRegistry } from "./exportHandler.registry";
|
|
@@ -6,11 +7,13 @@ import { ExportStorageService } from "./exportStorage.service";
|
|
|
6
7
|
import { JobContext } from "../queue/job.interface";
|
|
7
8
|
import { JobEventsService } from "../queue/jobEvents.service";
|
|
8
9
|
import { JobWorkerHost } from "../queue/jobWorkerHost";
|
|
9
|
-
export declare class ExportProcessor extends JobWorkerHost<ExportJobPayload, ExportJobSummary> {
|
|
10
|
+
export declare class ExportProcessor extends JobWorkerHost<ExportJobPayload | DocumentJobPayload, ExportJobSummary> {
|
|
10
11
|
private readonly registry;
|
|
11
12
|
private readonly engine;
|
|
12
13
|
private readonly storage;
|
|
13
14
|
protected readonly logger: Logger;
|
|
14
15
|
constructor(jobEvents: JobEventsService, registry: ExportHandlerRegistry, engine: ExportEngineService, storage: ExportStorageService);
|
|
15
|
-
handle(job: JobContext<ExportJobPayload>): Promise<ExportJobSummary>;
|
|
16
|
+
handle(job: JobContext<ExportJobPayload | DocumentJobPayload>): Promise<ExportJobSummary>;
|
|
17
|
+
private sheet;
|
|
18
|
+
private document;
|
|
16
19
|
}
|
|
@@ -15,11 +15,14 @@ const bullmq_1 = require("@nestjs/bullmq");
|
|
|
15
15
|
const common_1 = require("@nestjs/common");
|
|
16
16
|
const bullmq_2 = require("bullmq");
|
|
17
17
|
const exportEngine_service_1 = require("./exportEngine.service");
|
|
18
|
+
const exportExtension_enum_1 = require("./exportExtension.enum");
|
|
18
19
|
const exportHandler_registry_1 = require("./exportHandler.registry");
|
|
20
|
+
const exportKind_enum_1 = require("./exportKind.enum");
|
|
19
21
|
const exportStorage_service_1 = require("./exportStorage.service");
|
|
20
22
|
const jobEvents_service_1 = require("../queue/jobEvents.service");
|
|
21
23
|
const jobWorkerHost_1 = require("../queue/jobWorkerHost");
|
|
22
24
|
const queue_constants_1 = require("../queue/queue.constants");
|
|
25
|
+
const PERCENT_TOTAL = 100;
|
|
23
26
|
let ExportProcessor = ExportProcessor_1 = class ExportProcessor extends jobWorkerHost_1.JobWorkerHost {
|
|
24
27
|
constructor(jobEvents, registry, engine, storage) {
|
|
25
28
|
super(jobEvents);
|
|
@@ -30,23 +33,42 @@ let ExportProcessor = ExportProcessor_1 = class ExportProcessor extends jobWorke
|
|
|
30
33
|
}
|
|
31
34
|
async handle(job) {
|
|
32
35
|
const { jobId, ownerId, payload } = job;
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
36
|
+
const entry = this.registry.find(payload.handler);
|
|
37
|
+
if (!entry) {
|
|
35
38
|
throw new bullmq_2.UnrecoverableError(`Relatório "${payload.handler}" não registrado neste consumer`);
|
|
36
39
|
}
|
|
37
|
-
const
|
|
40
|
+
const extension = entry.kind === exportKind_enum_1.ExportKind.Document
|
|
41
|
+
? entry.handler.extension
|
|
42
|
+
: exportExtension_enum_1.ExportExtension.Xlsx;
|
|
43
|
+
const sink = await this.storage.openWriteStream(ownerId, jobId, extension);
|
|
38
44
|
try {
|
|
39
|
-
const
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
return { fileName: payload.fileName, totalRows };
|
|
45
|
+
const totalRows = entry.kind === exportKind_enum_1.ExportKind.Document
|
|
46
|
+
? await this.document(entry.handler, payload, job, sink)
|
|
47
|
+
: await this.sheet(entry.handler, payload, job, sink);
|
|
48
|
+
return { fileName: payload.fileName, totalRows, extension };
|
|
43
49
|
}
|
|
44
50
|
catch (error) {
|
|
45
51
|
sink.destroy();
|
|
46
|
-
await this.storage
|
|
52
|
+
await this.storage
|
|
53
|
+
.remove(ownerId, jobId, extension)
|
|
54
|
+
.catch(() => undefined);
|
|
47
55
|
throw error;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
58
|
+
async sheet(handler, payload, job, sink) {
|
|
59
|
+
const { totalRows } = await this.engine.run(handler, payload, job.ownerId, sink, async (processed) => {
|
|
60
|
+
await job.report(processed, 0, `${processed} linha(s)`);
|
|
61
|
+
});
|
|
62
|
+
return totalRows;
|
|
63
|
+
}
|
|
64
|
+
async document(handler, payload, job, sink) {
|
|
65
|
+
const params = handler.parseParams(payload.params);
|
|
66
|
+
await handler.render({ ownerId: job.ownerId }, params, sink, async (percent, message) => {
|
|
67
|
+
await job.report(percent, PERCENT_TOTAL, message);
|
|
68
|
+
});
|
|
69
|
+
this.logger.log(`Documento "${handler.name}" gerado`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
50
72
|
};
|
|
51
73
|
exports.ExportProcessor = ExportProcessor;
|
|
52
74
|
exports.ExportProcessor = ExportProcessor = ExportProcessor_1 = __decorate([
|
|
@@ -10,4 +10,6 @@ export declare class ExportService {
|
|
|
10
10
|
constructor(registry: ExportHandlerRegistry, producer: JobProducer);
|
|
11
11
|
request(user: AuthenticatedUser, name: string, command: RequestExportCommand): Promise<JobRef>;
|
|
12
12
|
status(jobId: string, ownerId: string): Promise<JobStatus<ExportJobSummary>>;
|
|
13
|
+
private sheetPayload;
|
|
14
|
+
private documentPayload;
|
|
13
15
|
}
|
|
@@ -11,7 +11,9 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ExportService = void 0;
|
|
13
13
|
const common_1 = require("@nestjs/common");
|
|
14
|
+
const export_constants_1 = require("./export.constants");
|
|
14
15
|
const exportHandler_registry_1 = require("./exportHandler.registry");
|
|
16
|
+
const exportKind_enum_1 = require("./exportKind.enum");
|
|
15
17
|
const parseFilter_util_1 = require("../query/parseFilter.util");
|
|
16
18
|
const job_producer_1 = require("../queue/job.producer");
|
|
17
19
|
const queue_constants_1 = require("../queue/queue.constants");
|
|
@@ -21,15 +23,28 @@ let ExportService = class ExportService {
|
|
|
21
23
|
this.producer = producer;
|
|
22
24
|
}
|
|
23
25
|
async request(user, name, command) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
+
const entry = this.registry.require(name);
|
|
27
|
+
const { permission } = entry.handler;
|
|
28
|
+
if (permission && !user.permissions.includes(permission)) {
|
|
26
29
|
throw new common_1.ForbiddenException("Você não tem permissão para exportar este relatório");
|
|
27
30
|
}
|
|
28
31
|
if (!this.producer.isAvailable(queue_constants_1.EXPORTS_QUEUE)) {
|
|
29
32
|
throw new common_1.ServiceUnavailableException("Geração de relatórios indisponível neste ambiente");
|
|
30
33
|
}
|
|
34
|
+
const payload = entry.kind === exportKind_enum_1.ExportKind.Document
|
|
35
|
+
? this.documentPayload(entry.handler, command)
|
|
36
|
+
: this.sheetPayload(entry.handler, command, user);
|
|
37
|
+
return await this.producer.enqueue(entry.handler.name, payload, {
|
|
38
|
+
ownerId: user.id,
|
|
39
|
+
queue: queue_constants_1.EXPORTS_QUEUE,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async status(jobId, ownerId) {
|
|
43
|
+
return await this.producer.status(jobId, ownerId, queue_constants_1.EXPORTS_QUEUE);
|
|
44
|
+
}
|
|
45
|
+
sheetPayload(handler, command, user) {
|
|
31
46
|
const filter = (0, parseFilter_util_1.parseFilter)(command.filter, handler.catalog, user.permissions);
|
|
32
|
-
|
|
47
|
+
return {
|
|
33
48
|
handler: handler.name,
|
|
34
49
|
filter,
|
|
35
50
|
search: command.search,
|
|
@@ -37,13 +52,18 @@ let ExportService = class ExportService {
|
|
|
37
52
|
sortDir: command.sortDir,
|
|
38
53
|
fileName: handler.buildFileName(),
|
|
39
54
|
};
|
|
40
|
-
return await this.producer.enqueue(handler.name, payload, {
|
|
41
|
-
ownerId: user.id,
|
|
42
|
-
queue: queue_constants_1.EXPORTS_QUEUE,
|
|
43
|
-
});
|
|
44
55
|
}
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
documentPayload(handler, command) {
|
|
57
|
+
const raw = command.params ?? {};
|
|
58
|
+
if (JSON.stringify(raw).length > export_constants_1.EXPORT_PARAMS_MAX_LENGTH) {
|
|
59
|
+
throw new common_1.BadRequestException("Parâmetros do relatório grandes demais");
|
|
60
|
+
}
|
|
61
|
+
const params = handler.parseParams(raw);
|
|
62
|
+
return {
|
|
63
|
+
handler: handler.name,
|
|
64
|
+
params: raw,
|
|
65
|
+
fileName: handler.buildFileName(params),
|
|
66
|
+
};
|
|
47
67
|
}
|
|
48
68
|
};
|
|
49
69
|
exports.ExportService = ExportService;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Writable } from "stream";
|
|
2
|
+
import { ExportExtension } from "./exportExtension.enum";
|
|
3
|
+
import { ExportContext } from "./exportHandler.interface";
|
|
4
|
+
export type ExportProgress = (percent: number, message: string) => Promise<void>;
|
|
5
|
+
export interface DocumentHandler<TParams = unknown> {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly permission?: string;
|
|
8
|
+
readonly extension: ExportExtension;
|
|
9
|
+
parseParams(raw: unknown): TParams;
|
|
10
|
+
buildFileName(params: TParams): string;
|
|
11
|
+
render(context: ExportContext, params: TParams, sink: Writable, progress: ExportProgress): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare const EXPORT_DOCUMENT = "rl-core:export-document";
|
|
14
|
+
export declare const ExportDocument: () => ClassDecorator;
|
|
15
|
+
export interface DocumentJobPayload {
|
|
16
|
+
handler: string;
|
|
17
|
+
params: unknown;
|
|
18
|
+
fileName: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExportDocument = exports.EXPORT_DOCUMENT = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
exports.EXPORT_DOCUMENT = "rl-core:export-document";
|
|
6
|
+
const ExportDocument = () => (0, common_1.SetMetadata)(exports.EXPORT_DOCUMENT, true);
|
|
7
|
+
exports.ExportDocument = ExportDocument;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXPORT_CONTENT_TYPES = exports.ExportExtension = void 0;
|
|
4
|
+
var ExportExtension;
|
|
5
|
+
(function (ExportExtension) {
|
|
6
|
+
ExportExtension["Xlsx"] = "xlsx";
|
|
7
|
+
ExportExtension["Pdf"] = "pdf";
|
|
8
|
+
})(ExportExtension || (exports.ExportExtension = ExportExtension = {}));
|
|
9
|
+
exports.EXPORT_CONTENT_TYPES = {
|
|
10
|
+
[ExportExtension.Xlsx]: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11
|
+
[ExportExtension.Pdf]: "application/pdf",
|
|
12
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
2
|
import { ExportColumn } from "./exportColumn.interface";
|
|
3
|
+
import { ExportExtension } from "./exportExtension.enum";
|
|
3
4
|
import { FilterCatalog, FilterNode } from "../query/filterField.interface";
|
|
4
5
|
import { SortDir } from "../utils/pagination.util";
|
|
5
6
|
export interface ExportContext {
|
|
@@ -28,5 +29,6 @@ export interface ExportJobPayload {
|
|
|
28
29
|
}
|
|
29
30
|
export interface ExportJobSummary {
|
|
30
31
|
fileName: string;
|
|
31
|
-
totalRows: number;
|
|
32
|
+
totalRows: number | null;
|
|
33
|
+
extension?: ExportExtension;
|
|
32
34
|
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { OnModuleInit } from "@nestjs/common";
|
|
2
2
|
import { DiscoveryService } from "@nestjs/core";
|
|
3
|
+
import { DocumentHandler } from "./exportDocument.interface";
|
|
3
4
|
import { ExportHandler } from "./exportHandler.interface";
|
|
5
|
+
import { ExportKind } from "./exportKind.enum";
|
|
6
|
+
export type RegisteredExport = {
|
|
7
|
+
kind: ExportKind.Sheet;
|
|
8
|
+
handler: ExportHandler;
|
|
9
|
+
} | {
|
|
10
|
+
kind: ExportKind.Document;
|
|
11
|
+
handler: DocumentHandler;
|
|
12
|
+
};
|
|
4
13
|
export declare class ExportHandlerRegistry implements OnModuleInit {
|
|
5
14
|
private readonly discovery;
|
|
6
15
|
private readonly logger;
|
|
7
16
|
private readonly handlers;
|
|
8
17
|
constructor(discovery: DiscoveryService);
|
|
9
18
|
onModuleInit(): void;
|
|
10
|
-
find(name: string):
|
|
11
|
-
require(name: string):
|
|
19
|
+
find(name: string): RegisteredExport | undefined;
|
|
20
|
+
require(name: string): RegisteredExport;
|
|
12
21
|
names(): string[];
|
|
22
|
+
private classify;
|
|
13
23
|
}
|
|
@@ -13,7 +13,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.ExportHandlerRegistry = void 0;
|
|
14
14
|
const common_1 = require("@nestjs/common");
|
|
15
15
|
const core_1 = require("@nestjs/core");
|
|
16
|
+
const exportDocument_interface_1 = require("./exportDocument.interface");
|
|
16
17
|
const exportHandler_interface_1 = require("./exportHandler.interface");
|
|
18
|
+
const exportKind_enum_1 = require("./exportKind.enum");
|
|
17
19
|
let ExportHandlerRegistry = ExportHandlerRegistry_1 = class ExportHandlerRegistry {
|
|
18
20
|
constructor(discovery) {
|
|
19
21
|
this.discovery = discovery;
|
|
@@ -21,17 +23,16 @@ let ExportHandlerRegistry = ExportHandlerRegistry_1 = class ExportHandlerRegistr
|
|
|
21
23
|
this.handlers = new Map();
|
|
22
24
|
}
|
|
23
25
|
onModuleInit() {
|
|
24
|
-
const
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.map((wrapper) => wrapper.instance)
|
|
29
|
-
.filter(Boolean);
|
|
30
|
-
for (const handler of found) {
|
|
31
|
-
if (this.handlers.has(handler.name)) {
|
|
32
|
-
throw new Error(`Dois relatórios registrados com o nome "${handler.name}"`);
|
|
26
|
+
for (const wrapper of this.discovery.getProviders()) {
|
|
27
|
+
const entry = this.classify(wrapper.metatype, wrapper.instance);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
continue;
|
|
33
30
|
}
|
|
34
|
-
|
|
31
|
+
const { name } = entry.handler;
|
|
32
|
+
if (this.handlers.has(name)) {
|
|
33
|
+
throw new Error(`Dois relatórios registrados com o nome "${name}"`);
|
|
34
|
+
}
|
|
35
|
+
this.handlers.set(name, entry);
|
|
35
36
|
}
|
|
36
37
|
if (this.handlers.size > 0) {
|
|
37
38
|
this.logger.log(`Relatórios disponíveis: ${this.names().join(", ")}`);
|
|
@@ -50,6 +51,21 @@ let ExportHandlerRegistry = ExportHandlerRegistry_1 = class ExportHandlerRegistr
|
|
|
50
51
|
names() {
|
|
51
52
|
return [...this.handlers.keys()];
|
|
52
53
|
}
|
|
54
|
+
classify(metatype, instance) {
|
|
55
|
+
if (typeof metatype !== "function" || !instance) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (Reflect.getMetadata(exportHandler_interface_1.EXPORT_HANDLER, metatype) === true) {
|
|
59
|
+
return { kind: exportKind_enum_1.ExportKind.Sheet, handler: instance };
|
|
60
|
+
}
|
|
61
|
+
if (Reflect.getMetadata(exportDocument_interface_1.EXPORT_DOCUMENT, metatype) === true) {
|
|
62
|
+
return {
|
|
63
|
+
kind: exportKind_enum_1.ExportKind.Document,
|
|
64
|
+
handler: instance,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
53
69
|
};
|
|
54
70
|
exports.ExportHandlerRegistry = ExportHandlerRegistry;
|
|
55
71
|
exports.ExportHandlerRegistry = ExportHandlerRegistry = ExportHandlerRegistry_1 = __decorate([
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExportKind = void 0;
|
|
4
|
+
var ExportKind;
|
|
5
|
+
(function (ExportKind) {
|
|
6
|
+
ExportKind["Sheet"] = "SHEET";
|
|
7
|
+
ExportKind["Document"] = "DOCUMENT";
|
|
8
|
+
})(ExportKind || (exports.ExportKind = ExportKind = {}));
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { WriteStream } from "fs";
|
|
2
2
|
import { EnvService } from "../config/env.service";
|
|
3
|
+
import { ExportExtension } from "./exportExtension.enum";
|
|
3
4
|
export declare class ExportStorageService {
|
|
4
5
|
private readonly env;
|
|
5
6
|
private readonly logger;
|
|
6
7
|
private readonly root;
|
|
7
8
|
constructor(env: EnvService);
|
|
8
|
-
pathFor(ownerId: string, jobId: string): string;
|
|
9
|
-
openWriteStream(ownerId: string, jobId: string): Promise<WriteStream>;
|
|
10
|
-
exists(ownerId: string, jobId: string): Promise<boolean>;
|
|
11
|
-
remove(ownerId: string, jobId: string): Promise<void>;
|
|
9
|
+
pathFor(ownerId: string, jobId: string, extension?: ExportExtension): string;
|
|
10
|
+
openWriteStream(ownerId: string, jobId: string, extension?: ExportExtension): Promise<WriteStream>;
|
|
11
|
+
exists(ownerId: string, jobId: string, extension?: ExportExtension): Promise<boolean>;
|
|
12
|
+
remove(ownerId: string, jobId: string, extension?: ExportExtension): Promise<void>;
|
|
12
13
|
removeExpired(maxAgeMs: number): Promise<number>;
|
|
13
14
|
}
|
|
@@ -16,8 +16,8 @@ const fs_1 = require("fs");
|
|
|
16
16
|
const promises_1 = require("fs/promises");
|
|
17
17
|
const path_1 = require("path");
|
|
18
18
|
const env_service_1 = require("../config/env.service");
|
|
19
|
+
const exportExtension_enum_1 = require("./exportExtension.enum");
|
|
19
20
|
const SAFE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
20
|
-
const EXTENSION = ".xlsx";
|
|
21
21
|
let ExportStorageService = ExportStorageService_1 = class ExportStorageService {
|
|
22
22
|
constructor(env) {
|
|
23
23
|
this.env = env;
|
|
@@ -27,28 +27,28 @@ let ExportStorageService = ExportStorageService_1 = class ExportStorageService {
|
|
|
27
27
|
? configured
|
|
28
28
|
: (0, path_1.join)(process.cwd(), configured);
|
|
29
29
|
}
|
|
30
|
-
pathFor(ownerId, jobId) {
|
|
30
|
+
pathFor(ownerId, jobId, extension = exportExtension_enum_1.ExportExtension.Xlsx) {
|
|
31
31
|
if (!SAFE_ID.test(ownerId) || !SAFE_ID.test(jobId)) {
|
|
32
32
|
throw new Error("Identificador inválido para caminho de relatório");
|
|
33
33
|
}
|
|
34
|
-
const path = (0, path_1.resolve)(this.root, ownerId, `${jobId}
|
|
34
|
+
const path = (0, path_1.resolve)(this.root, ownerId, `${jobId}.${extension}`);
|
|
35
35
|
if (!path.startsWith((0, path_1.resolve)(this.root))) {
|
|
36
36
|
throw new Error("Caminho de relatório fora da pasta de exportação");
|
|
37
37
|
}
|
|
38
38
|
return path;
|
|
39
39
|
}
|
|
40
|
-
async openWriteStream(ownerId, jobId) {
|
|
41
|
-
const path = this.pathFor(ownerId, jobId);
|
|
40
|
+
async openWriteStream(ownerId, jobId, extension = exportExtension_enum_1.ExportExtension.Xlsx) {
|
|
41
|
+
const path = this.pathFor(ownerId, jobId, extension);
|
|
42
42
|
await (0, promises_1.mkdir)((0, path_1.dirname)(path), { recursive: true });
|
|
43
43
|
return (0, fs_1.createWriteStream)(path);
|
|
44
44
|
}
|
|
45
|
-
async exists(ownerId, jobId) {
|
|
46
|
-
return await (0, promises_1.stat)(this.pathFor(ownerId, jobId))
|
|
45
|
+
async exists(ownerId, jobId, extension = exportExtension_enum_1.ExportExtension.Xlsx) {
|
|
46
|
+
return await (0, promises_1.stat)(this.pathFor(ownerId, jobId, extension))
|
|
47
47
|
.then(() => true)
|
|
48
48
|
.catch(() => false);
|
|
49
49
|
}
|
|
50
|
-
async remove(ownerId, jobId) {
|
|
51
|
-
await (0, promises_1.rm)(this.pathFor(ownerId, jobId), { force: true });
|
|
50
|
+
async remove(ownerId, jobId, extension = exportExtension_enum_1.ExportExtension.Xlsx) {
|
|
51
|
+
await (0, promises_1.rm)(this.pathFor(ownerId, jobId, extension), { force: true });
|
|
52
52
|
}
|
|
53
53
|
async removeExpired(maxAgeMs) {
|
|
54
54
|
const cutoff = Date.now() - maxAgeMs;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const PDF_PAGE_SIZE = "A4";
|
|
2
|
+
export declare const PDF_A4: {
|
|
3
|
+
readonly width: 595.28;
|
|
4
|
+
readonly height: 841.89;
|
|
5
|
+
};
|
|
6
|
+
export declare const PDF_PAGE_MARGINS: [number, number, number, number];
|
|
7
|
+
export declare const PDF_FONT_SIZE: {
|
|
8
|
+
readonly base: 9;
|
|
9
|
+
readonly small: 7.5;
|
|
10
|
+
readonly h1: 18;
|
|
11
|
+
readonly h2: 12;
|
|
12
|
+
readonly section: 10.5;
|
|
13
|
+
};
|
|
14
|
+
export declare const PDF_LOGO_TIMEOUT_MS = 5000;
|
|
15
|
+
export declare const PDF_LOGO_MAX_BYTES: number;
|
|
16
|
+
export declare const PDF_LOGO_FIT: [number, number];
|
|
17
|
+
export declare const PDF_LOGO_SCALE = 3;
|
|
18
|
+
export declare const PDF_LOGO_TYPES: readonly ["image/png", "image/jpeg"];
|
|
19
|
+
export declare const PDF_STYLE: {
|
|
20
|
+
readonly h1: "h1";
|
|
21
|
+
readonly h2: "h2";
|
|
22
|
+
readonly section: "section";
|
|
23
|
+
readonly tableHeader: "tableHeader";
|
|
24
|
+
readonly money: "money";
|
|
25
|
+
readonly muted: "muted";
|
|
26
|
+
readonly small: "small";
|
|
27
|
+
readonly strong: "strong";
|
|
28
|
+
};
|
|
29
|
+
export declare const PDF_TABLE_LAYOUT: {
|
|
30
|
+
readonly clean: "rlClean";
|
|
31
|
+
readonly striped: "rlStriped";
|
|
32
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PDF_TABLE_LAYOUT = exports.PDF_STYLE = exports.PDF_LOGO_TYPES = exports.PDF_LOGO_SCALE = exports.PDF_LOGO_FIT = exports.PDF_LOGO_MAX_BYTES = exports.PDF_LOGO_TIMEOUT_MS = exports.PDF_FONT_SIZE = exports.PDF_PAGE_MARGINS = exports.PDF_A4 = exports.PDF_PAGE_SIZE = void 0;
|
|
4
|
+
exports.PDF_PAGE_SIZE = "A4";
|
|
5
|
+
exports.PDF_A4 = { width: 595.28, height: 841.89 };
|
|
6
|
+
exports.PDF_PAGE_MARGINS = [28, 44, 28, 40];
|
|
7
|
+
exports.PDF_FONT_SIZE = {
|
|
8
|
+
base: 9,
|
|
9
|
+
small: 7.5,
|
|
10
|
+
h1: 18,
|
|
11
|
+
h2: 12,
|
|
12
|
+
section: 10.5,
|
|
13
|
+
};
|
|
14
|
+
exports.PDF_LOGO_TIMEOUT_MS = 5_000;
|
|
15
|
+
exports.PDF_LOGO_MAX_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
exports.PDF_LOGO_FIT = [120, 36];
|
|
17
|
+
exports.PDF_LOGO_SCALE = 3;
|
|
18
|
+
exports.PDF_LOGO_TYPES = ["image/png", "image/jpeg"];
|
|
19
|
+
exports.PDF_STYLE = {
|
|
20
|
+
h1: "h1",
|
|
21
|
+
h2: "h2",
|
|
22
|
+
section: "section",
|
|
23
|
+
tableHeader: "tableHeader",
|
|
24
|
+
money: "money",
|
|
25
|
+
muted: "muted",
|
|
26
|
+
small: "small",
|
|
27
|
+
strong: "strong",
|
|
28
|
+
};
|
|
29
|
+
exports.PDF_TABLE_LAYOUT = {
|
|
30
|
+
clean: "rlClean",
|
|
31
|
+
striped: "rlStriped",
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const mixWithWhite: (hex: string, ratio: number) => string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mixWithWhite = void 0;
|
|
4
|
+
const parseHex = (hex) => [
|
|
5
|
+
parseInt(hex.slice(1, 3), 16),
|
|
6
|
+
parseInt(hex.slice(3, 5), 16),
|
|
7
|
+
parseInt(hex.slice(5, 7), 16),
|
|
8
|
+
];
|
|
9
|
+
const toHex = (channel) => Math.round(channel).toString(16).padStart(2, "0");
|
|
10
|
+
const mixWithWhite = (hex, ratio) => {
|
|
11
|
+
const [r, g, b] = parseHex(hex);
|
|
12
|
+
const mix = (channel) => channel + (255 - channel) * ratio;
|
|
13
|
+
return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
|
|
14
|
+
};
|
|
15
|
+
exports.mixWithWhite = mixWithWhite;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Content } from "pdfmake/interfaces";
|
|
2
|
+
import { Writable } from "stream";
|
|
3
|
+
import { EnvService } from "../../config/env.service";
|
|
4
|
+
import { PdfTheme } from "./pdfTheme.interface";
|
|
5
|
+
export interface PdfFrame {
|
|
6
|
+
title: string;
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
details?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface PdfDocument {
|
|
11
|
+
frame: PdfFrame;
|
|
12
|
+
content: Content;
|
|
13
|
+
landscape?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare class PdfDocumentService {
|
|
16
|
+
private readonly env;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
readonly theme: PdfTheme;
|
|
19
|
+
private logo;
|
|
20
|
+
constructor(env: EnvService);
|
|
21
|
+
write(document: PdfDocument, sink: Writable): Promise<void>;
|
|
22
|
+
private definition;
|
|
23
|
+
private cover;
|
|
24
|
+
private styles;
|
|
25
|
+
private tableLayouts;
|
|
26
|
+
private loadLogo;
|
|
27
|
+
private fetchLogo;
|
|
28
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
var PdfDocumentService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PdfDocumentService = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const pdfmake_1 = __importDefault(require("pdfmake"));
|
|
19
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
20
|
+
const promises_1 = require("stream/promises");
|
|
21
|
+
const env_service_1 = require("../../config/env.service");
|
|
22
|
+
const pdf_constants_1 = require("./pdf.constants");
|
|
23
|
+
const pdfColor_util_1 = require("./pdfColor.util");
|
|
24
|
+
const pdfFonts_util_1 = require("./pdfFonts.util");
|
|
25
|
+
const contentWidth = (landscape) => (landscape ? pdf_constants_1.PDF_A4.height : pdf_constants_1.PDF_A4.width) -
|
|
26
|
+
pdf_constants_1.PDF_PAGE_MARGINS[0] -
|
|
27
|
+
pdf_constants_1.PDF_PAGE_MARGINS[2];
|
|
28
|
+
let PdfDocumentService = PdfDocumentService_1 = class PdfDocumentService {
|
|
29
|
+
constructor(env) {
|
|
30
|
+
this.env = env;
|
|
31
|
+
this.logger = new common_1.Logger(PdfDocumentService_1.name);
|
|
32
|
+
this.logo = null;
|
|
33
|
+
const brand = this.env.get("APP_BRAND_COLOR");
|
|
34
|
+
this.theme = {
|
|
35
|
+
appName: this.env.get("APP_NAME"),
|
|
36
|
+
brand,
|
|
37
|
+
brandTint: (0, pdfColor_util_1.mixWithWhite)(brand, 0.9),
|
|
38
|
+
text: "#1f2937",
|
|
39
|
+
muted: "#6b7280",
|
|
40
|
+
line: "#e5e7eb",
|
|
41
|
+
stripe: "#f9fafb",
|
|
42
|
+
positive: "#15803d",
|
|
43
|
+
warning: "#b45309",
|
|
44
|
+
onBrand: "#ffffff",
|
|
45
|
+
};
|
|
46
|
+
pdfmake_1.default.setFonts({ Roboto: (0, pdfFonts_util_1.robotoFonts)() });
|
|
47
|
+
pdfmake_1.default.setTableLayouts(this.tableLayouts());
|
|
48
|
+
pdfmake_1.default.setUrlAccessPolicy(() => false);
|
|
49
|
+
pdfmake_1.default.setLocalAccessPolicy(pdfFonts_util_1.isRobotoFile);
|
|
50
|
+
}
|
|
51
|
+
async write(document, sink) {
|
|
52
|
+
const definition = await this.definition(document);
|
|
53
|
+
const stream = await pdfmake_1.default.createPdf(definition).getStream();
|
|
54
|
+
stream.pipe(sink);
|
|
55
|
+
stream.end();
|
|
56
|
+
await (0, promises_1.finished)(sink);
|
|
57
|
+
}
|
|
58
|
+
async definition(document) {
|
|
59
|
+
const { frame, content, landscape } = document;
|
|
60
|
+
const { theme } = this;
|
|
61
|
+
const logo = await this.loadLogo();
|
|
62
|
+
const running = [theme.appName, frame.title, frame.subtitle]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(" · ");
|
|
65
|
+
return {
|
|
66
|
+
pageSize: pdf_constants_1.PDF_PAGE_SIZE,
|
|
67
|
+
pageOrientation: landscape ? "landscape" : "portrait",
|
|
68
|
+
pageMargins: pdf_constants_1.PDF_PAGE_MARGINS,
|
|
69
|
+
info: { title: running, author: theme.appName, creator: theme.appName },
|
|
70
|
+
defaultStyle: {
|
|
71
|
+
font: "Roboto",
|
|
72
|
+
fontSize: pdf_constants_1.PDF_FONT_SIZE.base,
|
|
73
|
+
color: theme.text,
|
|
74
|
+
lineHeight: 1.2,
|
|
75
|
+
},
|
|
76
|
+
styles: this.styles(),
|
|
77
|
+
header: (page) => page === 1
|
|
78
|
+
? ""
|
|
79
|
+
: {
|
|
80
|
+
text: running,
|
|
81
|
+
style: pdf_constants_1.PDF_STYLE.small,
|
|
82
|
+
color: theme.muted,
|
|
83
|
+
margin: [pdf_constants_1.PDF_PAGE_MARGINS[0], 18, pdf_constants_1.PDF_PAGE_MARGINS[2], 0],
|
|
84
|
+
},
|
|
85
|
+
footer: (page, pages) => ({
|
|
86
|
+
columns: [
|
|
87
|
+
{ text: running, style: pdf_constants_1.PDF_STYLE.small, color: theme.muted },
|
|
88
|
+
{
|
|
89
|
+
text: `Página ${page} de ${pages}`,
|
|
90
|
+
style: pdf_constants_1.PDF_STYLE.small,
|
|
91
|
+
color: theme.muted,
|
|
92
|
+
alignment: "right",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
margin: [pdf_constants_1.PDF_PAGE_MARGINS[0], 14, pdf_constants_1.PDF_PAGE_MARGINS[2], 0],
|
|
96
|
+
}),
|
|
97
|
+
content: [this.cover(frame, logo, landscape === true), content],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
cover(frame, logo, landscape) {
|
|
101
|
+
const { theme } = this;
|
|
102
|
+
const left = logo
|
|
103
|
+
? [{ text: frame.title, style: pdf_constants_1.PDF_STYLE.h1 }]
|
|
104
|
+
: [
|
|
105
|
+
{ text: theme.appName, style: pdf_constants_1.PDF_STYLE.small, color: theme.brand },
|
|
106
|
+
{ text: frame.title, style: pdf_constants_1.PDF_STYLE.h1 },
|
|
107
|
+
];
|
|
108
|
+
if (frame.subtitle) {
|
|
109
|
+
left.push({ text: frame.subtitle, style: pdf_constants_1.PDF_STYLE.h2, color: theme.muted });
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
stack: [
|
|
113
|
+
{
|
|
114
|
+
columns: [
|
|
115
|
+
logo
|
|
116
|
+
? {
|
|
117
|
+
width: pdf_constants_1.PDF_LOGO_FIT[0] + 12,
|
|
118
|
+
image: logo,
|
|
119
|
+
fit: pdf_constants_1.PDF_LOGO_FIT,
|
|
120
|
+
margin: [0, 2, 12, 0],
|
|
121
|
+
}
|
|
122
|
+
: { width: 0, text: "" },
|
|
123
|
+
{ width: "*", stack: left },
|
|
124
|
+
{
|
|
125
|
+
width: "auto",
|
|
126
|
+
stack: (frame.details ?? []).map((line) => ({
|
|
127
|
+
text: line,
|
|
128
|
+
style: pdf_constants_1.PDF_STYLE.small,
|
|
129
|
+
color: theme.muted,
|
|
130
|
+
alignment: "right",
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
columnGap: 8,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
canvas: [
|
|
138
|
+
{
|
|
139
|
+
type: "line",
|
|
140
|
+
x1: 0,
|
|
141
|
+
y1: 0,
|
|
142
|
+
x2: contentWidth(landscape),
|
|
143
|
+
y2: 0,
|
|
144
|
+
lineWidth: 1.5,
|
|
145
|
+
lineColor: theme.brand,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
margin: [0, 10, 0, 14],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
styles() {
|
|
154
|
+
const { theme } = this;
|
|
155
|
+
return {
|
|
156
|
+
[pdf_constants_1.PDF_STYLE.h1]: { fontSize: pdf_constants_1.PDF_FONT_SIZE.h1, bold: true },
|
|
157
|
+
[pdf_constants_1.PDF_STYLE.h2]: { fontSize: pdf_constants_1.PDF_FONT_SIZE.h2 },
|
|
158
|
+
[pdf_constants_1.PDF_STYLE.section]: {
|
|
159
|
+
fontSize: pdf_constants_1.PDF_FONT_SIZE.section,
|
|
160
|
+
bold: true,
|
|
161
|
+
color: theme.brand,
|
|
162
|
+
margin: [0, 12, 0, 6],
|
|
163
|
+
},
|
|
164
|
+
[pdf_constants_1.PDF_STYLE.tableHeader]: {
|
|
165
|
+
bold: true,
|
|
166
|
+
fontSize: pdf_constants_1.PDF_FONT_SIZE.small,
|
|
167
|
+
color: theme.brand,
|
|
168
|
+
fillColor: theme.brandTint,
|
|
169
|
+
characterSpacing: 0.4,
|
|
170
|
+
},
|
|
171
|
+
[pdf_constants_1.PDF_STYLE.money]: { alignment: "right" },
|
|
172
|
+
[pdf_constants_1.PDF_STYLE.muted]: { color: theme.muted },
|
|
173
|
+
[pdf_constants_1.PDF_STYLE.small]: { fontSize: pdf_constants_1.PDF_FONT_SIZE.small },
|
|
174
|
+
[pdf_constants_1.PDF_STYLE.strong]: { bold: true },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
tableLayouts() {
|
|
178
|
+
const { theme } = this;
|
|
179
|
+
const clean = {
|
|
180
|
+
hLineWidth: (i, node) => (i === 0 || i === node.table.body.length ? 0 : 0.5),
|
|
181
|
+
vLineWidth: () => 0,
|
|
182
|
+
hLineColor: () => theme.line,
|
|
183
|
+
paddingLeft: () => 6,
|
|
184
|
+
paddingRight: () => 6,
|
|
185
|
+
paddingTop: () => 4,
|
|
186
|
+
paddingBottom: () => 4,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
[pdf_constants_1.PDF_TABLE_LAYOUT.clean]: clean,
|
|
190
|
+
[pdf_constants_1.PDF_TABLE_LAYOUT.striped]: {
|
|
191
|
+
...clean,
|
|
192
|
+
fillColor: (row, node) => row >= (node.table.headerRows ?? 0) && row % 2 === 0
|
|
193
|
+
? theme.stripe
|
|
194
|
+
: null,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
loadLogo() {
|
|
199
|
+
if (!this.logo) {
|
|
200
|
+
this.logo = this.fetchLogo().catch((error) => {
|
|
201
|
+
this.logger.warn(`Relatório sem logo: ${error.message}`);
|
|
202
|
+
this.logo = null;
|
|
203
|
+
return null;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return this.logo;
|
|
207
|
+
}
|
|
208
|
+
async fetchLogo() {
|
|
209
|
+
const url = this.env.get("APP_LOGO_URL");
|
|
210
|
+
if (!url) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const response = await fetch(url, {
|
|
214
|
+
signal: AbortSignal.timeout(pdf_constants_1.PDF_LOGO_TIMEOUT_MS),
|
|
215
|
+
});
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`${url} respondeu ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
const type = (response.headers.get("content-type") ?? "").split(";")[0];
|
|
220
|
+
if (!pdf_constants_1.PDF_LOGO_TYPES.includes(type)) {
|
|
221
|
+
throw new Error(`${url} não é PNG nem JPEG (${type || "sem tipo"})`);
|
|
222
|
+
}
|
|
223
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
224
|
+
if (bytes.byteLength > pdf_constants_1.PDF_LOGO_MAX_BYTES) {
|
|
225
|
+
throw new Error(`${url} passa de ${pdf_constants_1.PDF_LOGO_MAX_BYTES} bytes`);
|
|
226
|
+
}
|
|
227
|
+
const reduced = await (0, sharp_1.default)(bytes)
|
|
228
|
+
.resize({
|
|
229
|
+
width: pdf_constants_1.PDF_LOGO_FIT[0] * pdf_constants_1.PDF_LOGO_SCALE,
|
|
230
|
+
height: pdf_constants_1.PDF_LOGO_FIT[1] * pdf_constants_1.PDF_LOGO_SCALE,
|
|
231
|
+
fit: "inside",
|
|
232
|
+
withoutEnlargement: true,
|
|
233
|
+
})
|
|
234
|
+
.png()
|
|
235
|
+
.toBuffer();
|
|
236
|
+
return `data:image/png;base64,${reduced.toString("base64")}`;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
exports.PdfDocumentService = PdfDocumentService;
|
|
240
|
+
exports.PdfDocumentService = PdfDocumentService = PdfDocumentService_1 = __decorate([
|
|
241
|
+
(0, common_1.Injectable)(),
|
|
242
|
+
__metadata("design:paramtypes", [env_service_1.EnvService])
|
|
243
|
+
], PdfDocumentService);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRobotoFile = exports.robotoFonts = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const ROBOTO_DIR = (0, path_1.join)((0, path_1.dirname)(require.resolve("pdfmake/package.json")), "build", "fonts", "Roboto");
|
|
6
|
+
const robotoFonts = () => ({
|
|
7
|
+
normal: (0, path_1.join)(ROBOTO_DIR, "Roboto-Regular.ttf"),
|
|
8
|
+
bold: (0, path_1.join)(ROBOTO_DIR, "Roboto-Medium.ttf"),
|
|
9
|
+
italics: (0, path_1.join)(ROBOTO_DIR, "Roboto-Italic.ttf"),
|
|
10
|
+
bolditalics: (0, path_1.join)(ROBOTO_DIR, "Roboto-MediumItalic.ttf"),
|
|
11
|
+
});
|
|
12
|
+
exports.robotoFonts = robotoFonts;
|
|
13
|
+
const isRobotoFile = (path) => (0, path_1.resolve)(path).startsWith(ROBOTO_DIR + path_1.sep);
|
|
14
|
+
exports.isRobotoFile = isRobotoFile;
|
|
@@ -49,3 +49,12 @@ __decorate([
|
|
|
49
49
|
(0, class_validator_1.IsIn)(["ASC", "DESC"]),
|
|
50
50
|
__metadata("design:type", String)
|
|
51
51
|
], RequestExportCommand.prototype, "sortDir", void 0);
|
|
52
|
+
__decorate([
|
|
53
|
+
(0, swagger_1.ApiPropertyOptional)({
|
|
54
|
+
description: "Parâmetros do documento, validados pelo relatório pedido",
|
|
55
|
+
example: { referenceMonth: "2026-09" },
|
|
56
|
+
}),
|
|
57
|
+
(0, class_validator_1.IsOptional)(),
|
|
58
|
+
(0, class_validator_1.IsObject)(),
|
|
59
|
+
__metadata("design:type", Object)
|
|
60
|
+
], RequestExportCommand.prototype, "params", void 0);
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export { QueueModule } from "./core/queue/queue.module";
|
|
|
75
75
|
export { buildRedisOptions } from "./core/queue/redisConnection";
|
|
76
76
|
export { RedisJobEventsBridge } from "./core/queue/redisJobEvents.bridge";
|
|
77
77
|
export { jobEventsChannel, JobEventsMessage, RedisJobEventsPublisher, } from "./core/queue/redisJobEvents.publisher";
|
|
78
|
+
export { EXPORT_PARAMS_MAX_LENGTH } from "./core/export/export.constants";
|
|
78
79
|
export { ExportController } from "./core/export/export.controller";
|
|
79
80
|
export { ExportModule } from "./core/export/export.module";
|
|
80
81
|
export { ExportProcessor } from "./core/export/export.processor";
|
|
@@ -83,13 +84,20 @@ export { ExportAcceptedResponse } from "./core/export/exportAccepted.response";
|
|
|
83
84
|
export { ExportApiModule } from "./core/export/exportApi.module";
|
|
84
85
|
export { ExportCleanupService } from "./core/export/exportCleanup.service";
|
|
85
86
|
export { ExportColumn, ExportColumnFormat, } from "./core/export/exportColumn.interface";
|
|
87
|
+
export { DocumentHandler, DocumentJobPayload, EXPORT_DOCUMENT, ExportDocument, ExportProgress, } from "./core/export/exportDocument.interface";
|
|
86
88
|
export { ExportEngineService } from "./core/export/exportEngine.service";
|
|
89
|
+
export { EXPORT_CONTENT_TYPES, ExportExtension, } from "./core/export/exportExtension.enum";
|
|
87
90
|
export { EXPORT_HANDLER, ExportContext, ExportHandler, ExportJobPayload, ExportJobSummary, ExportReport, } from "./core/export/exportHandler.interface";
|
|
88
|
-
export { ExportHandlerRegistry } from "./core/export/exportHandler.registry";
|
|
91
|
+
export { ExportHandlerRegistry, RegisteredExport, } from "./core/export/exportHandler.registry";
|
|
92
|
+
export { ExportKind } from "./core/export/exportKind.enum";
|
|
89
93
|
export { ExportStatusResponse } from "./core/export/exportStatus.response";
|
|
90
94
|
export { ExportStorageService } from "./core/export/exportStorage.service";
|
|
91
95
|
export { ExportWorkerModule } from "./core/export/exportWorker.module";
|
|
96
|
+
export { PDF_STYLE, PDF_TABLE_LAYOUT, } from "./core/export/pdf/pdf.constants";
|
|
97
|
+
export { PdfDocument, PdfDocumentService, PdfFrame, } from "./core/export/pdf/pdfDocument.service";
|
|
98
|
+
export { PdfTheme } from "./core/export/pdf/pdfTheme.interface";
|
|
92
99
|
export { RequestExportCommand } from "./core/export/requestExport.command";
|
|
100
|
+
export type { Content, ContentColumns, ContentStack, ContentTable, ContentText, TableCell, } from "pdfmake/interfaces";
|
|
93
101
|
export { ImageCleanupService } from "./core/images/imageCleanup.service";
|
|
94
102
|
export { ImageFetchService, isBlockedAddress } from "./core/images/imageFetch.service";
|
|
95
103
|
export { ImageProcessingService } from "./core/images/imageProcessing.service";
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Paginated = exports.parseFilter = exports.paginateWithFilter = exports.applySearch = exports.resolveCatalogOrder = exports.toFilterSchema = exports.FilterOptionResponse = exports.FilterFieldResponse = exports.FilterOperator = exports.FilterCombinator = exports.FilterFieldType = exports.FILTER_MAX_NODES = exports.FILTER_MAX_LENGTH = exports.FILTER_MAX_IN_ITEMS = exports.FILTER_MAX_DEPTH = exports.DEFAULT_OPERATORS = exports.applyOwnershipScope = exports.applyFilter = exports.SCOPE_TEAM = exports.SCOPE_OWN = exports.SCOPE_ANY = exports.anyOrTeam = exports.anyOrOwn = exports.resolveOwnershipScope = exports.PermissionsGuard = exports.JwtAuthGuard = exports.CsrfGuard = exports.AppThrottlerGuard = exports.RequirePermission = exports.RequireAnyPermission = exports.PERMISSIONS_KEY = exports.ANY_PERMISSIONS_KEY = exports.Public = exports.IS_PUBLIC_KEY = exports.CurrentUser = exports.setAuthCookies = exports.REFRESH_COOKIE = exports.clearAuthCookies = exports.ACCESS_COOKIE = exports.EnvService = exports.validateEnv = exports.envSchema = exports.ConfigModule = exports.RlCoreModule = exports.coreEntities = exports.coreMigrations = exports.createCoreDataSource = exports.CoreModule = exports.bootstrapWorker = exports.bootstrapCore = void 0;
|
|
4
4
|
exports.EXPORT_CONCURRENCY = exports.COMPLETED_RETENTION_SECONDS = exports.toSafeError = exports.JobWorkerHost = exports.JobStatusResponse = exports.JobProgressResponse = exports.JobErrorResponse = exports.JobEventsService = exports.JOB_EVENTS_PUBLISHER = exports.JobProducer = exports.BullConnectionModule = exports.Auditable = exports.RequestOutcome = exports.ErrorType = exports.codedUnauthorized = exports.codedTooManyRequests = exports.codedBadRequest = exports.AppErrorCode = exports.AppEvent = exports.auditContext = exports.User = exports.PasswordHistory = exports.Scope = exports.Role = exports.Resource = exports.Permission = exports.Group = exports.Action = exports.NotificationsPublisher = exports.NotificationsModule = exports.NotificationScope = exports.NotificationRead = exports.Notification = exports.NotificationsService = exports.ScopeType = exports.NotificationType = exports.SCOPES = exports.ROLES = exports.RESOURCES = exports.PERMISSIONS = exports.ACTIONS = exports.AuthToken = exports.RequestLog = exports.AuditDataChange = exports.seedPermissions = exports.seedAdmin = exports.BaseEntity = exports.PaginationMetaResponse = exports.resolveOrder = exports.PaginationQuery = void 0;
|
|
5
|
-
exports.
|
|
6
|
-
exports.normalizeIp = exports.sha256 = exports.randomToken = exports.numericCode = exports.backupCode = exports.encryptSecret = exports.decryptSecret = exports.MailerService = exports.createAppLogger = exports.WS_USER_RESOLVER = exports.WsAuthService = exports.wrapSocketEvent = exports.SOCKET_EVENT_VERSION = exports.userRoom = exports.SESSION_REFRESH_EVENT = exports.roleRoom = exports.NOTIFICATIONS_NAMESPACE = exports.BaseGateway = exports.UploadImageFromUrlCommand = exports.ImageStatusResponse = exports.ImageProfileResponse = exports.ImageAcceptedResponse = exports.ImagesWorkerModule = exports.ImageStorageService = exports.ImagesApiModule = exports.ImagesService = exports.ImagesProcessor = exports.ImagesModule = void 0;
|
|
5
|
+
exports.isBlockedAddress = exports.ImageFetchService = exports.ImageCleanupService = exports.RequestExportCommand = exports.PdfDocumentService = exports.PDF_TABLE_LAYOUT = exports.PDF_STYLE = exports.ExportWorkerModule = exports.ExportStorageService = exports.ExportStatusResponse = exports.ExportKind = exports.ExportHandlerRegistry = exports.ExportReport = exports.EXPORT_HANDLER = exports.ExportExtension = exports.EXPORT_CONTENT_TYPES = exports.ExportEngineService = exports.ExportDocument = exports.EXPORT_DOCUMENT = exports.ExportColumnFormat = exports.ExportCleanupService = exports.ExportApiModule = exports.ExportAcceptedResponse = exports.ExportService = exports.ExportProcessor = exports.ExportModule = exports.ExportController = exports.EXPORT_PARAMS_MAX_LENGTH = exports.RedisJobEventsPublisher = exports.jobEventsChannel = exports.RedisJobEventsBridge = exports.buildRedisOptions = exports.QueueModule = exports.queuePrefix = exports.QUEUE_NAMES = exports.JOBS_QUEUE = exports.JOB_PROGRESS_EVENT = exports.JOB_FAILED_EVENT = exports.JOB_CONCURRENCY = exports.JOB_COMPLETED_EVENT = exports.JOB_BACKOFF_DELAY_MS = exports.JOB_ATTEMPTS = exports.isRedisConfigured = exports.isQueueName = exports.IMAGES_QUEUE = exports.IMAGE_LOCK_DURATION_MS = exports.IMAGE_CONCURRENCY = exports.FAILED_RETENTION_SECONDS = exports.EXPORTS_QUEUE = exports.EXPORT_LOCK_DURATION_MS = void 0;
|
|
6
|
+
exports.normalizeIp = exports.sha256 = exports.randomToken = exports.numericCode = exports.backupCode = exports.encryptSecret = exports.decryptSecret = exports.MailerService = exports.createAppLogger = exports.WS_USER_RESOLVER = exports.WsAuthService = exports.wrapSocketEvent = exports.SOCKET_EVENT_VERSION = exports.userRoom = exports.SESSION_REFRESH_EVENT = exports.roleRoom = exports.NOTIFICATIONS_NAMESPACE = exports.BaseGateway = exports.UploadImageFromUrlCommand = exports.ImageStatusResponse = exports.ImageProfileResponse = exports.ImageAcceptedResponse = exports.ImagesWorkerModule = exports.ImageStorageService = exports.ImagesApiModule = exports.ImagesService = exports.ImagesProcessor = exports.ImagesModule = exports.ImagesController = exports.ImageProfileRegistry = exports.ImageUploadProfile = exports.ImageSourceKind = exports.ImageFormat = exports.ImageFit = exports.IMAGE_PROFILE = exports.EXTENSION_BY_FORMAT = exports.ImageProcessingService = void 0;
|
|
7
7
|
var bootstrapCore_util_1 = require("./core/bootstrap/bootstrapCore.util");
|
|
8
8
|
Object.defineProperty(exports, "bootstrapCore", { enumerable: true, get: function () { return bootstrapCore_util_1.bootstrapCore; } });
|
|
9
9
|
var bootstrapWorker_util_1 = require("./core/bootstrap/bootstrapWorker.util");
|
|
@@ -198,6 +198,8 @@ Object.defineProperty(exports, "RedisJobEventsBridge", { enumerable: true, get:
|
|
|
198
198
|
var redisJobEvents_publisher_1 = require("./core/queue/redisJobEvents.publisher");
|
|
199
199
|
Object.defineProperty(exports, "jobEventsChannel", { enumerable: true, get: function () { return redisJobEvents_publisher_1.jobEventsChannel; } });
|
|
200
200
|
Object.defineProperty(exports, "RedisJobEventsPublisher", { enumerable: true, get: function () { return redisJobEvents_publisher_1.RedisJobEventsPublisher; } });
|
|
201
|
+
var export_constants_1 = require("./core/export/export.constants");
|
|
202
|
+
Object.defineProperty(exports, "EXPORT_PARAMS_MAX_LENGTH", { enumerable: true, get: function () { return export_constants_1.EXPORT_PARAMS_MAX_LENGTH; } });
|
|
201
203
|
var export_controller_1 = require("./core/export/export.controller");
|
|
202
204
|
Object.defineProperty(exports, "ExportController", { enumerable: true, get: function () { return export_controller_1.ExportController; } });
|
|
203
205
|
var export_module_1 = require("./core/export/export.module");
|
|
@@ -214,19 +216,32 @@ var exportCleanup_service_1 = require("./core/export/exportCleanup.service");
|
|
|
214
216
|
Object.defineProperty(exports, "ExportCleanupService", { enumerable: true, get: function () { return exportCleanup_service_1.ExportCleanupService; } });
|
|
215
217
|
var exportColumn_interface_1 = require("./core/export/exportColumn.interface");
|
|
216
218
|
Object.defineProperty(exports, "ExportColumnFormat", { enumerable: true, get: function () { return exportColumn_interface_1.ExportColumnFormat; } });
|
|
219
|
+
var exportDocument_interface_1 = require("./core/export/exportDocument.interface");
|
|
220
|
+
Object.defineProperty(exports, "EXPORT_DOCUMENT", { enumerable: true, get: function () { return exportDocument_interface_1.EXPORT_DOCUMENT; } });
|
|
221
|
+
Object.defineProperty(exports, "ExportDocument", { enumerable: true, get: function () { return exportDocument_interface_1.ExportDocument; } });
|
|
217
222
|
var exportEngine_service_1 = require("./core/export/exportEngine.service");
|
|
218
223
|
Object.defineProperty(exports, "ExportEngineService", { enumerable: true, get: function () { return exportEngine_service_1.ExportEngineService; } });
|
|
224
|
+
var exportExtension_enum_1 = require("./core/export/exportExtension.enum");
|
|
225
|
+
Object.defineProperty(exports, "EXPORT_CONTENT_TYPES", { enumerable: true, get: function () { return exportExtension_enum_1.EXPORT_CONTENT_TYPES; } });
|
|
226
|
+
Object.defineProperty(exports, "ExportExtension", { enumerable: true, get: function () { return exportExtension_enum_1.ExportExtension; } });
|
|
219
227
|
var exportHandler_interface_1 = require("./core/export/exportHandler.interface");
|
|
220
228
|
Object.defineProperty(exports, "EXPORT_HANDLER", { enumerable: true, get: function () { return exportHandler_interface_1.EXPORT_HANDLER; } });
|
|
221
229
|
Object.defineProperty(exports, "ExportReport", { enumerable: true, get: function () { return exportHandler_interface_1.ExportReport; } });
|
|
222
230
|
var exportHandler_registry_1 = require("./core/export/exportHandler.registry");
|
|
223
231
|
Object.defineProperty(exports, "ExportHandlerRegistry", { enumerable: true, get: function () { return exportHandler_registry_1.ExportHandlerRegistry; } });
|
|
232
|
+
var exportKind_enum_1 = require("./core/export/exportKind.enum");
|
|
233
|
+
Object.defineProperty(exports, "ExportKind", { enumerable: true, get: function () { return exportKind_enum_1.ExportKind; } });
|
|
224
234
|
var exportStatus_response_1 = require("./core/export/exportStatus.response");
|
|
225
235
|
Object.defineProperty(exports, "ExportStatusResponse", { enumerable: true, get: function () { return exportStatus_response_1.ExportStatusResponse; } });
|
|
226
236
|
var exportStorage_service_1 = require("./core/export/exportStorage.service");
|
|
227
237
|
Object.defineProperty(exports, "ExportStorageService", { enumerable: true, get: function () { return exportStorage_service_1.ExportStorageService; } });
|
|
228
238
|
var exportWorker_module_1 = require("./core/export/exportWorker.module");
|
|
229
239
|
Object.defineProperty(exports, "ExportWorkerModule", { enumerable: true, get: function () { return exportWorker_module_1.ExportWorkerModule; } });
|
|
240
|
+
var pdf_constants_1 = require("./core/export/pdf/pdf.constants");
|
|
241
|
+
Object.defineProperty(exports, "PDF_STYLE", { enumerable: true, get: function () { return pdf_constants_1.PDF_STYLE; } });
|
|
242
|
+
Object.defineProperty(exports, "PDF_TABLE_LAYOUT", { enumerable: true, get: function () { return pdf_constants_1.PDF_TABLE_LAYOUT; } });
|
|
243
|
+
var pdfDocument_service_1 = require("./core/export/pdf/pdfDocument.service");
|
|
244
|
+
Object.defineProperty(exports, "PdfDocumentService", { enumerable: true, get: function () { return pdfDocument_service_1.PdfDocumentService; } });
|
|
230
245
|
var requestExport_command_1 = require("./core/export/requestExport.command");
|
|
231
246
|
Object.defineProperty(exports, "RequestExportCommand", { enumerable: true, get: function () { return requestExport_command_1.RequestExportCommand; } });
|
|
232
247
|
var imageCleanup_service_1 = require("./core/images/imageCleanup.service");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Core NestJS: autenticação com 2FA, RBAC, auditoria, notificações e listagens com filtro dinâmico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"@nestjs/schedule": "^6.1.3",
|
|
79
79
|
"@nestjs/throttler": "^6.5.0",
|
|
80
80
|
"@nestjs/websockets": "^11.2.1",
|
|
81
|
+
"@types/pdfmake": "^0.3.3",
|
|
81
82
|
"argon2": "^0.45.1",
|
|
82
83
|
"bullmq": "^5.81.4",
|
|
83
84
|
"cookie-parser": "^1.4.7",
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
"otplib": "^13.4.1",
|
|
92
93
|
"passport": "^0.7.0",
|
|
93
94
|
"passport-jwt": "^4.0.1",
|
|
95
|
+
"pdfmake": "^0.3.11",
|
|
94
96
|
"qrcode": "^1.5.4",
|
|
95
97
|
"sharp": "^0.35.3",
|
|
96
98
|
"socket.io": "^4.8.3",
|