@hed-hog/core 0.0.400 → 0.0.402
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/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +2 -0
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.service.d.ts +86 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +304 -23
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/document-kit/document-kit.module.d.ts +3 -0
- package/dist/document-kit/document-kit.module.d.ts.map +1 -0
- package/dist/document-kit/document-kit.module.js +24 -0
- package/dist/document-kit/document-kit.module.js.map +1 -0
- package/dist/document-kit/document-kit.registry.d.ts +55 -0
- package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
- package/dist/document-kit/document-kit.registry.js +91 -0
- package/dist/document-kit/document-kit.registry.js.map +1 -0
- package/dist/document-kit/document-kit.service.d.ts +49 -0
- package/dist/document-kit/document-kit.service.d.ts.map +1 -0
- package/dist/document-kit/document-kit.service.js +135 -0
- package/dist/document-kit/document-kit.service.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +136 -13
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +297 -27
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
- package/dist/file/mcp-file-source.schema.js +8 -5
- package/dist/file/mcp-file-source.schema.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +26 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/ai.testers.js +46 -8
- package/dist/integration-profile/testers/ai.testers.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +2 -0
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +72 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
- package/dist/oauth/oauth-token-broker.service.js +105 -3
- package/dist/oauth/oauth-token-broker.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
- package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-bar.js +117 -0
- package/dist/pdf/document/charts/chart-bar.js.map +1 -0
- package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
- package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-donut.js +89 -0
- package/dist/pdf/document/charts/chart-donut.js.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.js +60 -0
- package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
- package/dist/pdf/document/charts/chart-line.d.ts +29 -0
- package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-line.js +89 -0
- package/dist/pdf/document/charts/chart-line.js.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.js +110 -0
- package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
- package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
- package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-radar.js +83 -0
- package/dist/pdf/document/charts/chart-radar.js.map +1 -0
- package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
- package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-scale.js +118 -0
- package/dist/pdf/document/charts/chart-scale.js.map +1 -0
- package/dist/pdf/document/charts/index.d.ts +8 -0
- package/dist/pdf/document/charts/index.d.ts.map +1 -0
- package/dist/pdf/document/charts/index.js +24 -0
- package/dist/pdf/document/charts/index.js.map +1 -0
- package/dist/pdf/document/index.d.ts +8 -0
- package/dist/pdf/document/index.d.ts.map +1 -0
- package/dist/pdf/document/index.js +24 -0
- package/dist/pdf/document/index.js.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.js +36 -0
- package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
- package/dist/pdf/document/pdf-document.css.d.ts +20 -0
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.css.js +327 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.js +226 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
- package/dist/pdf/document/pdf-document.service.d.ts +27 -0
- package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.service.js +81 -0
- package/dist/pdf/document/pdf-document.service.js.map +1 -0
- package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
- package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.theme.js +105 -0
- package/dist/pdf/document/pdf-document.theme.js.map +1 -0
- package/dist/pdf/document/pdf-document.types.d.ts +164 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.types.js +18 -0
- package/dist/pdf/document/pdf-document.types.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -1
- package/dist/pdf/pdf.module.js +3 -2
- package/dist/pdf/pdf.module.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +75 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +167 -63
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
- package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
- package/dist/pdf/templates/commercial-proposal.template.js +276 -0
- package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
- package/dist/setting/favicon.controller.d.ts +26 -0
- package/dist/setting/favicon.controller.d.ts.map +1 -0
- package/dist/setting/favicon.controller.js +130 -0
- package/dist/setting/favicon.controller.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +2 -1
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/data/setting_group.yaml +52 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +81 -2
- package/hedhog/frontend/messages/pt.json +81 -2
- package/hedhog/table/file.yaml +3 -1
- package/package.json +5 -5
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/ai/ai-attachment.fragments.spec.ts +56 -3
- package/src/ai/ai.service.responses.spec.ts +429 -0
- package/src/ai/ai.service.ts +409 -27
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
- package/src/core.module.ts +3 -0
- package/src/document-kit/document-kit.module.ts +12 -0
- package/src/document-kit/document-kit.registry.ts +125 -0
- package/src/document-kit/document-kit.service.spec.ts +156 -0
- package/src/document-kit/document-kit.service.ts +173 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.spec.ts +168 -1
- package/src/file/file.service.ts +435 -36
- package/src/file/mcp-file-source.schema.ts +8 -5
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/index.ts +5 -0
- package/src/integration-profile/integration-profile.secrets.ts +26 -0
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/ai.testers.ts +70 -5
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
- package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/mcp/mcp.module.ts +2 -0
- package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
- package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
- package/src/meeting/meeting-provider.registry.ts +80 -0
- package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
- package/src/oauth/oauth-token-broker.service.ts +129 -2
- package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
- package/src/pdf/document/charts/chart-bar.ts +217 -0
- package/src/pdf/document/charts/chart-donut.ts +163 -0
- package/src/pdf/document/charts/chart-gauge.ts +94 -0
- package/src/pdf/document/charts/chart-line.ts +159 -0
- package/src/pdf/document/charts/chart-primitives.ts +188 -0
- package/src/pdf/document/charts/chart-radar.ts +136 -0
- package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
- package/src/pdf/document/charts/chart-scale.ts +141 -0
- package/src/pdf/document/charts/charts.spec.ts +157 -0
- package/src/pdf/document/charts/index.ts +7 -0
- package/src/pdf/document/index.ts +7 -0
- package/src/pdf/document/pdf-document.chrome.ts +38 -0
- package/src/pdf/document/pdf-document.css.ts +326 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
- package/src/pdf/document/pdf-document.renderer.ts +317 -0
- package/src/pdf/document/pdf-document.service.ts +77 -0
- package/src/pdf/document/pdf-document.theme.ts +146 -0
- package/src/pdf/document/pdf-document.types.ts +149 -0
- package/src/pdf/pdf.module.ts +3 -2
- package/src/pdf/pdf.service.spec.ts +131 -0
- package/src/pdf/pdf.service.ts +256 -82
- package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
- package/src/pdf/templates/commercial-proposal.template.ts +400 -0
- package/src/setting/favicon.controller.spec.ts +92 -0
- package/src/setting/favicon.controller.ts +135 -0
- package/src/setting/setting.module.ts +2 -1
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
package/src/pdf/pdf.module.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common';
|
|
|
2
2
|
import { ConfigModule } from '@nestjs/config';
|
|
3
3
|
|
|
4
4
|
import { SettingModule } from '../setting/setting.module';
|
|
5
|
+
import { PdfDocumentService } from './document/pdf-document.service';
|
|
5
6
|
import { PdfController } from './pdf.controller';
|
|
6
7
|
import { PdfService } from './pdf.service';
|
|
7
8
|
|
|
@@ -10,8 +11,8 @@ import { PdfService } from './pdf.service';
|
|
|
10
11
|
ConfigModule,
|
|
11
12
|
forwardRef(() => SettingModule),
|
|
12
13
|
],
|
|
13
|
-
providers: [PdfService],
|
|
14
|
-
exports: [PdfService],
|
|
14
|
+
providers: [PdfService, PdfDocumentService],
|
|
15
|
+
exports: [PdfService, PdfDocumentService],
|
|
15
16
|
controllers: [PdfController],
|
|
16
17
|
})
|
|
17
18
|
export class PdfModule {}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ServiceUnavailableException } from '@nestjs/common';
|
|
2
|
+
import { afterEach, describe, expect, it, jest } from '@jest/globals';
|
|
3
|
+
|
|
4
|
+
import { PdfService, buildPdfPageOptions } from './pdf.service';
|
|
5
|
+
|
|
6
|
+
/** O serviço só toca em SettingService/BrandAssetService fora do caminho de render. */
|
|
7
|
+
const makeService = (env: Record<string, string> = {}): PdfService =>
|
|
8
|
+
new PdfService({} as any, {} as any, {
|
|
9
|
+
get: (key: string) => env[key],
|
|
10
|
+
} as any);
|
|
11
|
+
|
|
12
|
+
describe('buildPdfPageOptions', () => {
|
|
13
|
+
// O contrato que protege ceia, certificado e material de aula: quem não passa
|
|
14
|
+
// nenhuma das opções novas tem que receber o mesmo objeto de antes delas
|
|
15
|
+
// existirem — a simples presença de `displayHeaderFooter` muda o resultado.
|
|
16
|
+
it('não acrescenta chave nenhuma quando só as opções antigas vêm', () => {
|
|
17
|
+
expect(buildPdfPageOptions({})).toEqual({
|
|
18
|
+
format: 'A4',
|
|
19
|
+
printBackground: true,
|
|
20
|
+
landscape: false,
|
|
21
|
+
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(Object.keys(buildPdfPageOptions({ format: 'A4' })).sort()).toEqual([
|
|
25
|
+
'format',
|
|
26
|
+
'landscape',
|
|
27
|
+
'margin',
|
|
28
|
+
'printBackground',
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('troca format/landscape por width/height quando os dois vêm', () => {
|
|
33
|
+
const options = buildPdfPageOptions({ width: '1600px', height: '1131px' });
|
|
34
|
+
|
|
35
|
+
expect(options).toEqual({
|
|
36
|
+
width: '1600px',
|
|
37
|
+
height: '1131px',
|
|
38
|
+
printBackground: true,
|
|
39
|
+
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Com um template só, o Chromium volta a injetar o dele (URL e data) no outro.
|
|
44
|
+
it('preenche os dois templates quando o cabeçalho/rodapé é ligado', () => {
|
|
45
|
+
const options = buildPdfPageOptions({
|
|
46
|
+
displayHeaderFooter: true,
|
|
47
|
+
headerTemplate: '<div class="bar"></div>',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(options.displayHeaderFooter).toBe(true);
|
|
51
|
+
expect(options.headerTemplate).toBe('<div class="bar"></div>');
|
|
52
|
+
expect(options.footerTemplate).toBe('<span></span>');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('ignora os templates enquanto displayHeaderFooter não for ligado', () => {
|
|
56
|
+
const options = buildPdfPageOptions({ footerTemplate: '<span></span>' });
|
|
57
|
+
|
|
58
|
+
expect(options).not.toHaveProperty('displayHeaderFooter');
|
|
59
|
+
expect(options).not.toHaveProperty('footerTemplate');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('só repassa scale e pageRanges quando vêm', () => {
|
|
63
|
+
expect(buildPdfPageOptions({ scale: 0.9, pageRanges: '1-2' })).toMatchObject({
|
|
64
|
+
scale: 0.9,
|
|
65
|
+
pageRanges: '1-2',
|
|
66
|
+
});
|
|
67
|
+
expect(buildPdfPageOptions({})).not.toHaveProperty('scale');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('fila de renderização', () => {
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
jest.useRealTimers();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('segura o render acima do teto até uma vaga ser devolvida', async () => {
|
|
77
|
+
const service = makeService({ PDF_MAX_CONCURRENCY: '1' });
|
|
78
|
+
const releaseFirst = await (service as any).acquireRenderSlot('primeiro');
|
|
79
|
+
|
|
80
|
+
let liberado = false;
|
|
81
|
+
const second = (service as any)
|
|
82
|
+
.acquireRenderSlot('segundo')
|
|
83
|
+
.then((release: () => void) => {
|
|
84
|
+
liberado = true;
|
|
85
|
+
return release;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve) => process.nextTick(resolve));
|
|
89
|
+
expect(liberado).toBe(false);
|
|
90
|
+
|
|
91
|
+
releaseFirst();
|
|
92
|
+
const releaseSecond = await second;
|
|
93
|
+
expect(liberado).toBe(true);
|
|
94
|
+
releaseSecond();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('responde 503 quando a espera na fila estoura', async () => {
|
|
98
|
+
jest.useFakeTimers();
|
|
99
|
+
const service = makeService({ PDF_MAX_CONCURRENCY: '1', PDF_QUEUE_WAIT_MS: '50' });
|
|
100
|
+
const release = await (service as any).acquireRenderSlot('primeiro');
|
|
101
|
+
|
|
102
|
+
const queued = (service as any).acquireRenderSlot('segundo');
|
|
103
|
+
const rejeicao = expect(queued).rejects.toBeInstanceOf(ServiceUnavailableException);
|
|
104
|
+
|
|
105
|
+
jest.advanceTimersByTime(50);
|
|
106
|
+
await rejeicao;
|
|
107
|
+
|
|
108
|
+
release();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Liberar duas vezes (ex.: `finally` somado a um caminho de erro) não pode
|
|
112
|
+
// inflar a capacidade e deixar dois Chromium subirem juntos.
|
|
113
|
+
it('ignora a segunda liberação do mesmo render', async () => {
|
|
114
|
+
const service = makeService({ PDF_MAX_CONCURRENCY: '1' });
|
|
115
|
+
const release = await (service as any).acquireRenderSlot('primeiro');
|
|
116
|
+
release();
|
|
117
|
+
release();
|
|
118
|
+
|
|
119
|
+
const releaseSecond = await (service as any).acquireRenderSlot('segundo');
|
|
120
|
+
|
|
121
|
+
let terceiroEntrou = false;
|
|
122
|
+
void (service as any).acquireRenderSlot('terceiro').then(() => {
|
|
123
|
+
terceiroEntrou = true;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await new Promise((resolve) => process.nextTick(resolve));
|
|
127
|
+
expect(terceiroEntrou).toBe(false);
|
|
128
|
+
|
|
129
|
+
releaseSecond();
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/pdf/pdf.service.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Inject,
|
|
3
|
+
Injectable,
|
|
4
|
+
InternalServerErrorException,
|
|
5
|
+
Logger,
|
|
6
|
+
ServiceUnavailableException,
|
|
7
|
+
forwardRef,
|
|
8
|
+
} from '@nestjs/common';
|
|
2
9
|
import { ConfigService } from '@nestjs/config';
|
|
3
10
|
|
|
4
11
|
import { BrandAssetService } from '../setting/brand-asset.service';
|
|
5
12
|
import { SettingService } from '../setting/setting.service';
|
|
6
13
|
import { PdfAreaType, PdfThemeConfig } from './interfaces/pdf-theme-config.interface';
|
|
14
|
+
import {
|
|
15
|
+
CommercialDocumentTheme,
|
|
16
|
+
CommercialProposalData,
|
|
17
|
+
commercialProposalTemplate,
|
|
18
|
+
} from './templates/commercial-proposal.template';
|
|
7
19
|
import { hcodeInstitutionalTemplate } from './templates/hcode-institutional.template';
|
|
8
20
|
import { AreaDefaults, HCODE_DEFAULTS } from './themes/hcode.theme';
|
|
9
21
|
|
|
@@ -20,8 +32,46 @@ export interface PdfGenerateOptions {
|
|
|
20
32
|
width?: string;
|
|
21
33
|
/** Explicit CSS page size (e.g. '1131px'). Takes precedence over `format`/`landscape` when set. */
|
|
22
34
|
height?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Turns on Chromium's own per-page header and footer. This is the only way to
|
|
38
|
+
* get a repeating accent bar and a real page number: `counter(page)` resolves
|
|
39
|
+
* to nothing outside `@page` margin boxes, so a footer rendered as flow
|
|
40
|
+
* content (like `pdfFooter()` in ./components) prints an empty page slot.
|
|
41
|
+
*
|
|
42
|
+
* The templates are mini-documents, not part of the page: they start at
|
|
43
|
+
* `font-size: 0`, need their own `print-color-adjust: exact` to keep
|
|
44
|
+
* backgrounds, and only `<span class="pageNumber">` / `<span class="totalPages">`
|
|
45
|
+
* get substituted. Keep `margin.top`/`margin.bottom` larger than the templates,
|
|
46
|
+
* or the page content prints over them.
|
|
47
|
+
*/
|
|
48
|
+
displayHeaderFooter?: boolean;
|
|
49
|
+
headerTemplate?: string;
|
|
50
|
+
footerTemplate?: string;
|
|
51
|
+
|
|
52
|
+
/** `page.setContent` wait condition. Defaults to 'networkidle'. */
|
|
53
|
+
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
|
|
54
|
+
/** Ceiling for `page.setContent`, in ms. Defaults to Playwright's own (30s). */
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* JS expression evaluated in the page after `setContent`; the snapshot waits
|
|
58
|
+
* until it is truthy. For HTML whose layout settles asynchronously (webfonts
|
|
59
|
+
* loaded via `document.fonts`, measuring scripts that run after them).
|
|
60
|
+
*/
|
|
61
|
+
readyExpression?: string;
|
|
62
|
+
scale?: number;
|
|
63
|
+
pageRanges?: string;
|
|
64
|
+
/** Short identifier for the logs when a render fails or the queue saturates. */
|
|
65
|
+
label?: string;
|
|
23
66
|
}
|
|
24
67
|
|
|
68
|
+
/** A render waiting for a Chromium slot. */
|
|
69
|
+
type QueuedRender = {
|
|
70
|
+
resolve: () => void;
|
|
71
|
+
reject: (error: Error) => void;
|
|
72
|
+
timer: ReturnType<typeof setTimeout>;
|
|
73
|
+
};
|
|
74
|
+
|
|
25
75
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
76
|
// Helpers
|
|
27
77
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -33,6 +83,55 @@ function parsePxSize(value: string | undefined, fallback: number): number {
|
|
|
33
83
|
return match ? Math.round(Number(match[1])) : fallback;
|
|
34
84
|
}
|
|
35
85
|
|
|
86
|
+
const ZERO_MARGIN = { top: '0', right: '0', bottom: '0', left: '0' };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* With `displayHeaderFooter` on, a template left undefined makes Chromium fall
|
|
90
|
+
* back to its own — the page URL and the print date. The unused slot gets
|
|
91
|
+
* something deliberately empty instead.
|
|
92
|
+
*/
|
|
93
|
+
const EMPTY_TEMPLATE = '<span></span>';
|
|
94
|
+
|
|
95
|
+
const DEFAULT_MAX_CONCURRENCY = 2;
|
|
96
|
+
const DEFAULT_QUEUE_WAIT_MS = 30_000;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds the object handed to `page.pdf()`.
|
|
100
|
+
*
|
|
101
|
+
* Assembled key by key on purpose: a caller that passes none of the optional
|
|
102
|
+
* fields has to get exactly the object `page.pdf()` received before they
|
|
103
|
+
* existed — Chromium changes behaviour on the mere presence of some of them
|
|
104
|
+
* (`displayHeaderFooter` with a single template filled in brings back its own
|
|
105
|
+
* URL/date footer).
|
|
106
|
+
*/
|
|
107
|
+
export function buildPdfPageOptions(options: PdfGenerateOptions): Record<string, unknown> {
|
|
108
|
+
const pdfOptions: Record<string, unknown> =
|
|
109
|
+
options.width && options.height
|
|
110
|
+
? {
|
|
111
|
+
width: options.width,
|
|
112
|
+
height: options.height,
|
|
113
|
+
printBackground: options.printBackground ?? true,
|
|
114
|
+
margin: options.margin ?? ZERO_MARGIN,
|
|
115
|
+
}
|
|
116
|
+
: {
|
|
117
|
+
format: options.format ?? 'A4',
|
|
118
|
+
printBackground: options.printBackground ?? true,
|
|
119
|
+
landscape: options.landscape ?? false,
|
|
120
|
+
margin: options.margin ?? ZERO_MARGIN,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (options.displayHeaderFooter) {
|
|
124
|
+
pdfOptions.displayHeaderFooter = true;
|
|
125
|
+
pdfOptions.headerTemplate = options.headerTemplate ?? EMPTY_TEMPLATE;
|
|
126
|
+
pdfOptions.footerTemplate = options.footerTemplate ?? EMPTY_TEMPLATE;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.scale !== undefined) pdfOptions.scale = options.scale;
|
|
130
|
+
if (options.pageRanges) pdfOptions.pageRanges = options.pageRanges;
|
|
131
|
+
|
|
132
|
+
return pdfOptions;
|
|
133
|
+
}
|
|
134
|
+
|
|
36
135
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
136
|
// Service
|
|
38
137
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -41,6 +140,10 @@ function parsePxSize(value: string | undefined, fallback: number): number {
|
|
|
41
140
|
export class PdfService {
|
|
42
141
|
private readonly logger = new Logger(PdfService.name);
|
|
43
142
|
|
|
143
|
+
/** Renders holding a Chromium right now, and the ones waiting for a slot. */
|
|
144
|
+
private activeRenders = 0;
|
|
145
|
+
private readonly renderQueue: QueuedRender[] = [];
|
|
146
|
+
|
|
44
147
|
constructor(
|
|
45
148
|
@Inject(forwardRef(() => SettingService))
|
|
46
149
|
private readonly settingService: SettingService,
|
|
@@ -120,15 +223,136 @@ export class PdfService {
|
|
|
120
223
|
}
|
|
121
224
|
}
|
|
122
225
|
|
|
123
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Theme for commercial documents (proposals and the like): the area theme
|
|
228
|
+
* plus the legal footer, partner line and font stacks of the `pdf` group.
|
|
229
|
+
* The logo is embedded as a data URI so the render does not depend on the
|
|
230
|
+
* API reaching its own public URL.
|
|
231
|
+
*/
|
|
232
|
+
async loadCommercialDocumentTheme(
|
|
233
|
+
area: PdfAreaType = 'training',
|
|
234
|
+
): Promise<CommercialDocumentTheme> {
|
|
235
|
+
const base = await this.loadThemeConfig(area);
|
|
236
|
+
|
|
237
|
+
let values: Record<string, any> = {};
|
|
238
|
+
try {
|
|
239
|
+
values = await this.settingService.getSettingValues([
|
|
240
|
+
'pdf-legal-footer',
|
|
241
|
+
'pdf-partner-line',
|
|
242
|
+
'pdf-font-family',
|
|
243
|
+
'pdf-font-mono',
|
|
244
|
+
]);
|
|
245
|
+
} catch {
|
|
246
|
+
this.logger.warn('Could not load commercial PDF settings – using defaults.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const logoOverride = base.logoOverride
|
|
250
|
+
? ((await this.brandAsset.fetchAsDataUri(base.logoOverride)) ?? base.logoOverride)
|
|
251
|
+
: '';
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...base,
|
|
255
|
+
logoOverride,
|
|
256
|
+
legalFooter: String(values['pdf-legal-footer'] ?? '').trim(),
|
|
257
|
+
partnerLine: String(values['pdf-partner-line'] ?? '').trim(),
|
|
258
|
+
fontFamily: String(values['pdf-font-family'] ?? '').trim() || "'Inter', sans-serif",
|
|
259
|
+
fontMono: String(values['pdf-font-mono'] ?? '').trim() || "'JetBrains Mono', monospace",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
124
262
|
|
|
125
263
|
/**
|
|
126
|
-
* Renders
|
|
264
|
+
* Renders a commercial proposal with the installation's branding. Returns
|
|
265
|
+
* the HTML too, so the caller can keep the exact source of what was printed.
|
|
266
|
+
*/
|
|
267
|
+
async renderCommercialProposal(
|
|
268
|
+
data: CommercialProposalData,
|
|
269
|
+
area: PdfAreaType = 'training',
|
|
270
|
+
): Promise<{ html: string; buffer: Buffer }> {
|
|
271
|
+
const theme = await this.loadCommercialDocumentTheme(area);
|
|
272
|
+
const { html, pdfOptions } = commercialProposalTemplate(data, theme);
|
|
273
|
+
const buffer = await this.generatePdfFromHtml(html, pdfOptions);
|
|
274
|
+
return { html, buffer };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Chromium runtime ──────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
private get maxConcurrency(): number {
|
|
280
|
+
const raw = Number(this.configService.get<string>('PDF_MAX_CONCURRENCY'));
|
|
281
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : DEFAULT_MAX_CONCURRENCY;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private get queueWaitMs(): number {
|
|
285
|
+
const raw = Number(this.configService.get<string>('PDF_QUEUE_WAIT_MS'));
|
|
286
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : DEFAULT_QUEUE_WAIT_MS;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Every render launches a Chromium of its own, which costs a few hundred MB.
|
|
291
|
+
* The API pod is capped well below what a burst of simultaneous report
|
|
292
|
+
* downloads would ask for, and an OOM kill takes the whole API down — not
|
|
293
|
+
* just the PDF. So renders queue, and a queue that waits too long answers a
|
|
294
|
+
* retryable 503 instead of dragging the process down with it.
|
|
295
|
+
*/
|
|
296
|
+
private async acquireRenderSlot(label?: string): Promise<() => void> {
|
|
297
|
+
if (this.activeRenders < this.maxConcurrency) {
|
|
298
|
+
this.activeRenders += 1;
|
|
299
|
+
} else {
|
|
300
|
+
await new Promise<void>((resolve, reject) => {
|
|
301
|
+
const entry: QueuedRender = {
|
|
302
|
+
resolve,
|
|
303
|
+
reject,
|
|
304
|
+
timer: setTimeout(() => {
|
|
305
|
+
const index = this.renderQueue.indexOf(entry);
|
|
306
|
+
if (index >= 0) this.renderQueue.splice(index, 1);
|
|
307
|
+
this.logger.warn(
|
|
308
|
+
`PDF render queue is saturated; gave up after ${this.queueWaitMs}ms${label ? ` (${label})` : ''}.`,
|
|
309
|
+
);
|
|
310
|
+
reject(
|
|
311
|
+
new ServiceUnavailableException(
|
|
312
|
+
'The server is busy rendering other documents. Try again in a moment.',
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}, this.queueWaitMs),
|
|
316
|
+
};
|
|
317
|
+
this.renderQueue.push(entry);
|
|
318
|
+
});
|
|
319
|
+
// Resolved by releaseRenderSlot, which hands its slot over without
|
|
320
|
+
// decrementing — the counter already accounts for this render.
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let released = false;
|
|
324
|
+
return () => {
|
|
325
|
+
if (released) return;
|
|
326
|
+
released = true;
|
|
327
|
+
this.releaseRenderSlot();
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private releaseRenderSlot(): void {
|
|
332
|
+
const next = this.renderQueue.shift();
|
|
333
|
+
if (next) {
|
|
334
|
+
clearTimeout(next.timer);
|
|
335
|
+
next.resolve();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.activeRenders = Math.max(0, this.activeRenders - 1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Opens a Chromium page with the HTML loaded and hands it to `run`, taking
|
|
343
|
+
* care of the slot, the viewport and the teardown.
|
|
127
344
|
*
|
|
128
345
|
* Uses dynamic `import()` via `new Function` to remain compatible with
|
|
129
346
|
* CommonJS bundle output (same pattern as operations.service.ts).
|
|
130
347
|
*/
|
|
131
|
-
async
|
|
348
|
+
private async renderWithChromium(
|
|
349
|
+
kind: 'PDF' | 'Image',
|
|
350
|
+
html: string,
|
|
351
|
+
options: PdfGenerateOptions,
|
|
352
|
+
run: (page: any) => Promise<any>,
|
|
353
|
+
): Promise<Buffer> {
|
|
354
|
+
const release = await this.acquireRenderSlot(options.label);
|
|
355
|
+
|
|
132
356
|
const executablePath = this.configService.get<string>('CHROMIUM_EXECUTABLE_PATH');
|
|
133
357
|
const noSandbox = this.configService.get<string>('CHROMIUM_NO_SANDBOX') === 'true';
|
|
134
358
|
|
|
@@ -163,25 +387,17 @@ export class PdfService {
|
|
|
163
387
|
width: parsePxSize(options.width, 794),
|
|
164
388
|
height: parsePxSize(options.height, 1123),
|
|
165
389
|
});
|
|
166
|
-
await page.setContent(html, {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
: {
|
|
177
|
-
format: options.format ?? 'A4',
|
|
178
|
-
printBackground: options.printBackground ?? true,
|
|
179
|
-
landscape: options.landscape ?? false,
|
|
180
|
-
margin: options.margin ?? { top: '0', right: '0', bottom: '0', left: '0' },
|
|
181
|
-
},
|
|
182
|
-
);
|
|
390
|
+
await page.setContent(html, {
|
|
391
|
+
waitUntil: options.waitUntil ?? 'networkidle',
|
|
392
|
+
...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
|
|
393
|
+
});
|
|
394
|
+
if (options.readyExpression) {
|
|
395
|
+
await page.waitForFunction(options.readyExpression, undefined, {
|
|
396
|
+
...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
183
399
|
|
|
184
|
-
return Buffer.from(
|
|
400
|
+
return Buffer.from(await run(page));
|
|
185
401
|
} catch (error) {
|
|
186
402
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
187
403
|
const errorStack = error instanceof Error ? (error.stack ?? errorMessage) : String(error);
|
|
@@ -191,7 +407,7 @@ export class PdfService {
|
|
|
191
407
|
errorMessage,
|
|
192
408
|
);
|
|
193
409
|
|
|
194
|
-
this.logger.error(
|
|
410
|
+
this.logger.error(`${kind} generation failed. ${errorMessage}`, errorStack);
|
|
195
411
|
|
|
196
412
|
if (missingRuntime) {
|
|
197
413
|
throw new InternalServerErrorException(
|
|
@@ -199,7 +415,7 @@ export class PdfService {
|
|
|
199
415
|
);
|
|
200
416
|
}
|
|
201
417
|
|
|
202
|
-
throw new InternalServerErrorException(
|
|
418
|
+
throw new InternalServerErrorException(`${kind} generation failed: ${errorMessage}`);
|
|
203
419
|
} finally {
|
|
204
420
|
if (browser) {
|
|
205
421
|
try {
|
|
@@ -208,9 +424,21 @@ export class PdfService {
|
|
|
208
424
|
// Ignore close errors
|
|
209
425
|
}
|
|
210
426
|
}
|
|
427
|
+
release();
|
|
211
428
|
}
|
|
212
429
|
}
|
|
213
430
|
|
|
431
|
+
// ── HTML → PDF ────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Renders an HTML string to a PDF buffer using Playwright/Chromium.
|
|
435
|
+
*/
|
|
436
|
+
async generatePdfFromHtml(html: string, options: PdfGenerateOptions = {}): Promise<Buffer> {
|
|
437
|
+
const pdfOptions = buildPdfPageOptions(options);
|
|
438
|
+
|
|
439
|
+
return this.renderWithChromium('PDF', html, options, (page) => page.pdf(pdfOptions));
|
|
440
|
+
}
|
|
441
|
+
|
|
214
442
|
// ── HTML → PNG image ──────────────────────────────────────────────────────
|
|
215
443
|
|
|
216
444
|
/**
|
|
@@ -221,69 +449,15 @@ export class PdfService {
|
|
|
221
449
|
* certificate thumbnails on the web/app.
|
|
222
450
|
*/
|
|
223
451
|
async generateImageFromHtml(html: string, options: PdfGenerateOptions = {}): Promise<Buffer> {
|
|
224
|
-
const executablePath = this.configService.get<string>('CHROMIUM_EXECUTABLE_PATH');
|
|
225
|
-
const noSandbox = this.configService.get<string>('CHROMIUM_NO_SANDBOX') === 'true';
|
|
226
|
-
|
|
227
|
-
const launchArgs = noSandbox
|
|
228
|
-
? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
|
|
229
|
-
: [];
|
|
230
|
-
|
|
231
452
|
const width = parsePxSize(options.width, 794);
|
|
232
453
|
const height = parsePxSize(options.height, 1123);
|
|
233
454
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const importPlaywright = new Function(
|
|
238
|
-
'moduleName',
|
|
239
|
-
'return import(moduleName);',
|
|
240
|
-
) as (moduleName: string) => Promise<any>;
|
|
241
|
-
|
|
242
|
-
const playwright = await importPlaywright('playwright');
|
|
243
|
-
|
|
244
|
-
browser = await playwright.chromium.launch({
|
|
245
|
-
headless: true,
|
|
246
|
-
...(executablePath ? { executablePath } : {}),
|
|
247
|
-
args: launchArgs,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const page = await browser.newPage();
|
|
251
|
-
await page.setViewportSize({ width, height });
|
|
252
|
-
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
253
|
-
|
|
254
|
-
const imageBuffer = await page.screenshot({
|
|
455
|
+
return this.renderWithChromium('Image', html, options, (page) =>
|
|
456
|
+
page.screenshot({
|
|
255
457
|
type: 'png',
|
|
256
458
|
clip: { x: 0, y: 0, width, height },
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
return Buffer.from(imageBuffer);
|
|
260
|
-
} catch (error) {
|
|
261
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
262
|
-
const errorStack = error instanceof Error ? (error.stack ?? errorMessage) : String(error);
|
|
263
|
-
|
|
264
|
-
const missingRuntime =
|
|
265
|
-
/Cannot find package ['"]playwright['"]|Cannot find module ['"]playwright['"]|Executable doesn't exist|browserType\.launch|Failed to launch|Please run the following command/i.test(
|
|
266
|
-
errorMessage,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
this.logger.error(`Image generation failed. ${errorMessage}`, errorStack);
|
|
270
|
-
|
|
271
|
-
if (missingRuntime) {
|
|
272
|
-
throw new InternalServerErrorException(
|
|
273
|
-
'Playwright runtime is not available. Run `pnpm --filter api run playwright:install` in the API environment.',
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
throw new InternalServerErrorException(`Image generation failed: ${errorMessage}`);
|
|
278
|
-
} finally {
|
|
279
|
-
if (browser) {
|
|
280
|
-
try {
|
|
281
|
-
await browser.close();
|
|
282
|
-
} catch {
|
|
283
|
-
// Ignore close errors
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
287
461
|
}
|
|
288
462
|
|
|
289
463
|
// ── Template engine ───────────────────────────────────────────────────────
|