feat(ee): PDF export api (#2112)

* feat(ee): server side PDF export

* feat: pdf export queue

* sync

* sync
This commit is contained in:
Philip Okugbe
2026-04-14 16:26:54 +01:00
committed by GitHub
parent cc00e77dfb
commit a6a7e4370a
12 changed files with 219 additions and 16 deletions
@@ -5,6 +5,9 @@ import { QueueJob, QueueName } from 'src/integrations/queue/constants';
import { FileImportTaskService } from '../services/file-import-task.service';
import { FileTaskStatus } from '../utils/file.utils';
import { StorageService } from '../../storage/storage.service';
import { ModuleRef } from '@nestjs/core';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types';
@Processor(QueueName.FILE_TASK_QUEUE)
export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
@@ -13,6 +16,8 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
constructor(
private readonly fileTaskService: FileImportTaskService,
private readonly storageService: StorageService,
private readonly moduleRef: ModuleRef,
@InjectKysely() private readonly db: KyselyDB,
) {
super();
}
@@ -23,8 +28,11 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
case QueueJob.IMPORT_TASK:
await this.fileTaskService.processZIpImport(job.data.fileTaskId);
break;
case QueueJob.EXPORT_TASK:
// TODO: export task
case QueueJob.PDF_EXPORT_TASK:
await this.processExportTask(job.data.fileTaskId);
break;
case QueueJob.PDF_EXPORT_CLEANUP:
await this.processExportCleanup();
break;
}
} catch (err) {
@@ -33,6 +41,24 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
}
}
private getPdfExportService() {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const PdfExportModule = require('./../../../ee/pdf-export/pdf-export.service');
return this.moduleRef.get(PdfExportModule.PdfExportService, {
strict: false,
});
}
private async processExportTask(fileTaskId: string): Promise<void> {
const pdfExportService = this.getPdfExportService();
await pdfExportService.generateAndStorePdf(fileTaskId);
}
private async processExportCleanup(): Promise<void> {
const pdfExportService = this.getPdfExportService();
await pdfExportService.cleanupExpiredExports();
}
@OnWorkerEvent('active')
onActive(job: Job) {
this.logger.debug(`Processing ${job.name} job`);
@@ -41,32 +67,39 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
@OnWorkerEvent('failed')
async onFailed(job: Job) {
this.logger.error(
`Error processing ${job.name} job. Import Task ID: ${job.data.fileTaskId}. Reason: ${job.failedReason}`,
`Error processing ${job.name} job. File Task ID: ${job.data?.fileTaskId}. Reason: ${job.failedReason}`,
);
await this.handleFailedJob(job);
if (job.name === QueueJob.IMPORT_TASK) {
await this.handleFailedImportJob(job);
} else if (job.name === QueueJob.PDF_EXPORT_TASK) {
await this.handleFailedExportJob(job);
}
}
@OnWorkerEvent('completed')
async onCompleted(job: Job) {
this.logger.log(
`Completed ${job.name} job for File task ID ${job.data.fileTaskId}`,
`Completed ${job.name} job for File task ID ${job.data?.fileTaskId}`,
);
try {
const fileTask = await this.fileTaskService.getFileTask(
job.data.fileTaskId,
);
if (fileTask) {
await this.storageService.delete(fileTask.filePath);
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
if (job.name === QueueJob.IMPORT_TASK) {
try {
const fileTask = await this.fileTaskService.getFileTask(
job.data.fileTaskId,
);
if (fileTask) {
await this.storageService.delete(fileTask.filePath);
this.logger.debug(`Deleted imported zip file: ${fileTask.filePath}`);
}
} catch (err) {
this.logger.error(`Failed to delete imported zip file:`, err);
}
} catch (err) {
this.logger.error(`Failed to delete imported zip file:`, err);
}
// Export tasks: do NOT delete the file on completion (kept for 24h cache)
}
private async handleFailedJob(job: Job) {
private async handleFailedImportJob(job: Job) {
try {
const fileTaskId = job.data.fileTaskId;
const reason = job.failedReason || 'Unknown error';
@@ -86,6 +119,25 @@ export class FileTaskProcessor extends WorkerHost implements OnModuleDestroy {
}
}
private async handleFailedExportJob(job: Job) {
try {
const fileTaskId = job.data.fileTaskId;
const reason = job.failedReason || 'Unknown error';
await this.db
.updateTable('fileTasks')
.set({
status: FileTaskStatus.Failed,
errorMessage: reason,
updatedAt: new Date(),
})
.where('id', '=', fileTaskId)
.execute();
} catch (err) {
this.logger.error(err);
}
}
async onModuleDestroy(): Promise<void> {
if (this.worker) {
await this.worker.close();