diff --git a/.docker/.manifests/apps__worker__package.json b/.docker/.manifests/apps__worker__package.json index 223464d..59924d4 100644 --- a/.docker/.manifests/apps__worker__package.json +++ b/.docker/.manifests/apps__worker__package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsup", "start": "node dist/main.js project-q:work async", - "dev": "nodemon --watch src -e ts --exec \"ts-node src/main.ts project-q:work async\"", + "dev": "nodemon --watch src -e ts --exec \"ts-node -r tsconfig-paths/register src/main.ts project-q:work async\"", "typecheck": "tsc -p tsconfig.build.json --noEmit", "lint": "biome check .", "lint:fix": "biome check --write ." @@ -40,6 +40,7 @@ "@types/pg": "^8.15.0", "nodemon": "^3.1.7", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsup": "^8.5.1", "typescript": "^5.6.3" } diff --git a/apps/api/package.json b/apps/api/package.json index 3fc05ea..883b72e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -55,9 +55,6 @@ "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.8.0", "@riotbyte-com/nest-service-locator": "0.3.0-rc.8", - "@riotbyte-com/project-q-core": "1.0.19-rc.5", - "@riotbyte-com/project-q-nestjs": "1.0.19-rc.5", - "@riotbyte-com/project-q-prisma": "1.0.19-rc.5", "@types/cookie-parser": "^1.4.10", "@types/handlebars": "^4.0.40", "@types/multer": "^2.0.0", diff --git a/apps/api/src/modules/app.module.ts b/apps/api/src/modules/app.module.ts index 047c301..516afb6 100644 --- a/apps/api/src/modules/app.module.ts +++ b/apps/api/src/modules/app.module.ts @@ -4,6 +4,7 @@ import { BaseModule, DatabaseModule, EventsModule, + ProjectQMessagingModule, UserModule, } from "@cv/core"; import { FileStorageModule } from "@cv/file-storage"; @@ -46,7 +47,6 @@ import { EmploymentModule } from "./job-experience/employment/employment.module" import { LevelModule } from "./job-experience/level/level.module"; import { RoleModule } from "./job-experience/role/role.module"; import { SkillModule } from "./job-experience/skill/skill.module"; -import { ProjectQMessengerModule } from "./messenger/messenger.module"; import { OnboardingModule } from "./onboarding/onboarding.module"; import { OrganizationModule } from "./organization/organization.module"; import { ProfileModule } from "./profile/profile.module"; @@ -124,7 +124,7 @@ import { VacancyModule } from "./vacancies/vacancy.module"; CVParserModule, CVTemplateModule, DataImportModule, - ProjectQMessengerModule, + ProjectQMessagingModule.forRoot({ queueName: "default" }), EducationModule, SeedModule, UserAiSettingsModule, diff --git a/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts b/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts index 2af2056..692c99d 100644 --- a/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts +++ b/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts @@ -2,10 +2,7 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { AuthorizationService, CV as CVEntity, CVRendererService, CVService, CVTemplateService, User as DomainUser, PaginationService, UuidFactoryService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; -import { MessageBus } from "@riotbyte-com/project-q-core"; -import { InjectMessageBus } from "@riotbyte-com/project-q-nestjs"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; -import { RenderPdfMessage } from "@/modules/messenger/messages/render-pdf.message"; import { CV } from "./cv.type"; import { CreateCVInput, UpdateCVInput } from "./cv-input.type"; import { @@ -61,7 +58,6 @@ export class CVResolver { private readonly uuidFactory: UuidFactoryService, readonly _paginationService: PaginationService, private readonly authorizationService: AuthorizationService, - @InjectMessageBus() private readonly messageBus: MessageBus, ) {} @Query(() => CV, { nullable: true }) @@ -155,17 +151,7 @@ export class CVResolver { ): Promise { const cv = await this.cvService.findByIdOrFail(cvId); await this.authorizationService.canView(user, cv, CVEntity); - - const html = await this.cvRendererService.render(cv.id, cv.template.id); - - await this.messageBus.dispatch( - RenderPdfMessage.create({ - cvId: cv.id, - html, - requestedBy: user.id, - }), - ); - + await this.cvRendererService.requestPdfRender(cv.id, user.id); return true; } } diff --git a/apps/api/src/modules/messenger/messenger.module.ts b/apps/api/src/modules/messenger/messenger.module.ts deleted file mode 100644 index aec7b42..0000000 --- a/apps/api/src/modules/messenger/messenger.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Module } from "@nestjs/common"; -import { - LoggerProvider, - MessengerModule, -} from "@riotbyte-com/project-q-nestjs"; -import { NestProjectQLogger } from "./logger.provider"; -import { PrismaTransportModule } from "./prisma-transport.module"; - -@Module({ - imports: [ - PrismaTransportModule, - MessengerModule.forRoot({ - transports: { - async: { dsn: "prisma://?queue=default", retry: true }, - }, - routing: new Map([["render-pdf", "async"]]), - }), - ], - providers: [ - { - provide: LoggerProvider, - useClass: NestProjectQLogger, - }, - ], -}) -export class ProjectQMessengerModule {} diff --git a/apps/worker/package.json b/apps/worker/package.json index 223464d..59924d4 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsup", "start": "node dist/main.js project-q:work async", - "dev": "nodemon --watch src -e ts --exec \"ts-node src/main.ts project-q:work async\"", + "dev": "nodemon --watch src -e ts --exec \"ts-node -r tsconfig-paths/register src/main.ts project-q:work async\"", "typecheck": "tsc -p tsconfig.build.json --noEmit", "lint": "biome check .", "lint:fix": "biome check --write ." @@ -40,6 +40,7 @@ "@types/pg": "^8.15.0", "nodemon": "^3.1.7", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsup": "^8.5.1", "typescript": "^5.6.3" } diff --git a/apps/worker/src/logger.provider.ts b/apps/worker/src/logger.provider.ts deleted file mode 100644 index d03a1e1..0000000 --- a/apps/worker/src/logger.provider.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Logger as NestLogger } from "@nestjs/common"; -import { Logger } from "@riotbyte-com/project-q-core"; - -export class NestProjectQLogger implements Logger { - private readonly logger = new NestLogger("ProjectQ"); - - info(message: string): void { - this.logger.log(message); - } - - error(message: string): void { - this.logger.error(message); - } - - debug(message: string): void { - this.logger.debug(message); - } -} diff --git a/apps/worker/src/worker.module.ts b/apps/worker/src/worker.module.ts index 2031bb9..b39f056 100644 --- a/apps/worker/src/worker.module.ts +++ b/apps/worker/src/worker.module.ts @@ -1,25 +1,12 @@ -import { DatabaseModule, PrismaService } from "@cv/core"; +import { DatabaseModule, ProjectQMessagingModule } from "@cv/core"; import { FileStorageModule } from "@cv/file-storage"; -import { - HandlersModule, - HtmlToPdfService, - RenderPdfHandler, -} from "@cv/handlers"; +import { HandlersModule } from "@cv/handlers"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { EventEmitterModule } from "@nestjs/event-emitter"; -import { - LoggerProvider, - MessengerModule, -} from "@riotbyte-com/project-q-nestjs"; -import { - PrismaClientToken, - PrismaTransportFactory, -} from "@riotbyte-com/project-q-prisma"; import { WorkerConfig } from "./config"; import { config } from "./config"; import { HeartbeatModule } from "./heartbeat/heartbeat.module"; -import { NestProjectQLogger } from "./logger.provider"; export const WORKER_CONFIG = Symbol("WORKER_CONFIG"); @@ -47,18 +34,10 @@ export const WORKER_CONFIG = Symbol("WORKER_CONFIG"); } : { driver: "disk", baseDir: config.pdfOutputDir }, ), - MessengerModule.forRoot({ - transports: { - async: { dsn: `prisma://?queue=${config.queueName}`, retry: true }, - }, - routing: new Map([["render-pdf", "async"]]), - }), + ProjectQMessagingModule.forRoot({ queueName: config.queueName }), ], providers: [ { provide: WORKER_CONFIG, useValue: config satisfies WorkerConfig }, - { provide: PrismaClientToken, useExisting: PrismaService }, - { provide: LoggerProvider, useClass: NestProjectQLogger }, - PrismaTransportFactory, ], }) export class WorkerModule {} diff --git a/docker-compose.yml b/docker-compose.yml index 9f8ffa2..094e386 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -123,7 +123,12 @@ services: - ./packages/core/src:/app/packages/core/src - ./packages/cv-renderer/src:/app/packages/cv-renderer/src - ./packages/file-storage/src:/app/packages/file-storage/src + - ./packages/file-upload/src:/app/packages/file-upload/src - ./packages/handlers/src:/app/packages/handlers/src + - ./packages/handlebars/src:/app/packages/handlebars/src + - ./packages/mail/src:/app/packages/mail/src + - ./packages/ai-parser/src:/app/packages/ai-parser/src + - ./packages/ai-provider/src:/app/packages/ai-provider/src - worker-output:/app/pdf-output restart: unless-stopped healthcheck: diff --git a/packages/core/package.json b/packages/core/package.json index 54c50b2..d3cb261 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,7 +5,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "imports": { - "#core/*": "./src/*" + "#core/*": ["./src/*", "./src/*/index.ts", "./src/*.ts"] }, "scripts": { "test": "vitest run", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0c01c8b..bd45763 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,7 +10,8 @@ export * from "./modules/job-experience/employment"; export * from "./modules/job-experience/level"; export * from "./modules/job-experience/role"; export * from "./modules/job-experience/skill"; +export * from "./modules/messenger"; export * from "./modules/organization"; export * from "./modules/profile"; export * from "./modules/vacancies"; -export * from "./shared"; +export * from "./shared/index"; diff --git a/packages/core/src/modules/cv-template/__tests__/cv-renderer.service.spec.ts b/packages/core/src/modules/cv-template/__tests__/cv-renderer.service.spec.ts index eb6e12d..580ad98 100644 --- a/packages/core/src/modules/cv-template/__tests__/cv-renderer.service.spec.ts +++ b/packages/core/src/modules/cv-template/__tests__/cv-renderer.service.spec.ts @@ -33,9 +33,13 @@ describe("CVRendererService", () => { beforeEach(() => { cvTemplateService = { findByIdOrFail: vi.fn() }; dataAssembler = { assemble: vi.fn() }; + const cvService = { findByIdOrFail: vi.fn() }; + const messageBus = { dispatch: vi.fn() }; service = new CVRendererService( cvTemplateService as unknown as CVTemplateService, + cvService as never, dataAssembler as unknown as CVDataAssemblerService, + messageBus as never, ); }); diff --git a/packages/core/src/modules/cv-template/cv-data-assembler.service.ts b/packages/core/src/modules/cv-template/cv-data-assembler.service.ts index faec014..fe1b093 100644 --- a/packages/core/src/modules/cv-template/cv-data-assembler.service.ts +++ b/packages/core/src/modules/cv-template/cv-data-assembler.service.ts @@ -1,8 +1,8 @@ import { CVRenderContext } from "@cv/cv-renderer"; import { Injectable } from "@nestjs/common"; +import { ClockService } from "#core/shared"; import { PrismaService } from "../database"; import { ProfileService } from "../profile/profile.service"; -import { ClockService } from "../../shared"; export type { CVRenderContext }; diff --git a/packages/core/src/modules/cv-template/cv-renderer.service.ts b/packages/core/src/modules/cv-template/cv-renderer.service.ts index 4d3afce..c0ddb15 100644 --- a/packages/core/src/modules/cv-template/cv-renderer.service.ts +++ b/packages/core/src/modules/cv-template/cv-renderer.service.ts @@ -1,7 +1,11 @@ import { HandlebarsEngine, TemplateEngine, wrapInDocument, } from "@cv/cv-renderer"; import { Injectable } from "@nestjs/common"; +import type { MessageBus } from "@riotbyte-com/project-q-core"; +import { InjectMessageBus } from "@riotbyte-com/project-q-nestjs"; +import { RenderPdfMessage } from "../messenger/messages/render-pdf.message"; import { CVDataAssemblerService } from "./cv-data-assembler.service"; +import { CVService } from "./cv.service"; import { CVTemplateService } from "./cv-template.service"; const engines: Record = { @@ -12,7 +16,9 @@ const engines: Record = { export class CVRendererService { constructor( private readonly cvTemplateService: CVTemplateService, + private readonly cvService: CVService, private readonly dataAssembler: CVDataAssemblerService, + @InjectMessageBus() private readonly messageBus: MessageBus, ) {} async render(cvId: string, templateId: string): Promise { @@ -32,4 +38,17 @@ export class CVRendererService { return wrapInDocument(rendered, template.css ?? ""); } + + /** + * Render the CV's HTML and queue a PDF render job. Caller (resolver, + * REST endpoint, etc.) is responsible for authorization; this service + * just turns "I want a PDF for this CV" into a queued message. + */ + async requestPdfRender(cvId: string, requestedBy: string): Promise { + const cv = await this.cvService.findByIdOrFail(cvId); + const html = await this.render(cv.id, cv.template.id); + await this.messageBus.dispatch( + RenderPdfMessage.create({ cvId: cv.id, html, requestedBy }), + ); + } } diff --git a/packages/core/src/modules/messenger/index.ts b/packages/core/src/modules/messenger/index.ts new file mode 100644 index 0000000..89130ea --- /dev/null +++ b/packages/core/src/modules/messenger/index.ts @@ -0,0 +1,13 @@ +export { + ProjectQMessagingModule, + type ProjectQMessagingOptions, +} from "./messenger.module"; +export { NestProjectQLogger } from "./logger.provider"; +export { PrismaTransportModule } from "./prisma-transport.module"; +export { RenderPdfMessage } from "./messages/render-pdf.message"; +// Re-exports so apps don't need a direct dep on @riotbyte-com/project-q-*. +export type { Envelope, MessageBus } from "@riotbyte-com/project-q-core"; +export { + HandlerTag, + InjectMessageBus, +} from "@riotbyte-com/project-q-nestjs"; diff --git a/apps/api/src/modules/messenger/logger.provider.ts b/packages/core/src/modules/messenger/logger.provider.ts similarity index 100% rename from apps/api/src/modules/messenger/logger.provider.ts rename to packages/core/src/modules/messenger/logger.provider.ts diff --git a/apps/api/src/modules/messenger/messages/render-pdf.message.ts b/packages/core/src/modules/messenger/messages/render-pdf.message.ts similarity index 100% rename from apps/api/src/modules/messenger/messages/render-pdf.message.ts rename to packages/core/src/modules/messenger/messages/render-pdf.message.ts diff --git a/packages/core/src/modules/messenger/messenger.module.ts b/packages/core/src/modules/messenger/messenger.module.ts new file mode 100644 index 0000000..1f8753f --- /dev/null +++ b/packages/core/src/modules/messenger/messenger.module.ts @@ -0,0 +1,39 @@ +import { DynamicModule, Module } from "@nestjs/common"; +import { + LoggerProvider, + MessengerModule as ProjectQMessengerModule, +} from "@riotbyte-com/project-q-nestjs"; +import { NestProjectQLogger } from "./logger.provider"; +import { PrismaTransportModule } from "./prisma-transport.module"; + +export interface ProjectQMessagingOptions { + /** + * Postgres queue name. Both api and worker must agree on this so the + * worker picks up messages the api dispatches. + */ + queueName: string; +} + +/** + * Bundles the project-q messenger config (prisma transport + nest logger + * adapter) so apps/api and apps/worker import a single module instead of + * each repeating the `forRoot` boilerplate. + */ +@Module({}) +export class ProjectQMessagingModule { + static forRoot({ queueName }: ProjectQMessagingOptions): DynamicModule { + return { + module: ProjectQMessagingModule, + imports: [ + PrismaTransportModule, + ProjectQMessengerModule.forRoot({ + transports: { + async: { dsn: `prisma://?queue=${queueName}`, retry: true }, + }, + routing: new Map([["render-pdf", "async"]]), + }), + ], + providers: [{ provide: LoggerProvider, useClass: NestProjectQLogger }], + }; + } +} diff --git a/apps/api/src/modules/messenger/prisma-transport.module.ts b/packages/core/src/modules/messenger/prisma-transport.module.ts similarity index 77% rename from apps/api/src/modules/messenger/prisma-transport.module.ts rename to packages/core/src/modules/messenger/prisma-transport.module.ts index a287781..6b41442 100644 --- a/apps/api/src/modules/messenger/prisma-transport.module.ts +++ b/packages/core/src/modules/messenger/prisma-transport.module.ts @@ -1,9 +1,10 @@ -import { DatabaseModule, PrismaService } from "@cv/core"; import { Module } from "@nestjs/common"; import { PrismaClientToken, PrismaTransportFactory, } from "@riotbyte-com/project-q-prisma"; +import { DatabaseModule } from "../database"; +import { PrismaService } from "../database/prisma.service"; @Module({ imports: [DatabaseModule], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4aa7615..c93f040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,15 +115,6 @@ importers: '@riotbyte-com/nest-service-locator': specifier: 0.3.0-rc.8 version: 0.3.0-rc.8(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2) - '@riotbyte-com/project-q-core': - specifier: 1.0.19-rc.5 - version: 1.0.19-rc.5 - '@riotbyte-com/project-q-nestjs': - specifier: 1.0.19-rc.5 - version: 1.0.19-rc.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.8(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) - '@riotbyte-com/project-q-prisma': - specifier: 1.0.19-rc.5 - version: 1.0.19-rc.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.5)(@riotbyte-com/project-q-nestjs@1.0.19-rc.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.8(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3))) '@types/cookie-parser': specifier: ^1.4.10 version: 1.4.10(@types/express@5.0.6) @@ -572,6 +563,9 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 tsup: specifier: ^8.5.1 version: 8.5.1(@swc/core@1.15.13)(jiti@2.7.0)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2)