From bedc2cef7b13d2b6ece5383818020c8fd6c077ec Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Sat, 5 Oct 2024 12:54:45 +0530 Subject: [PATCH 1/8] feat: integrated pg-boss for job scheduling --- apps/api/package.json | 3 +- apps/api/src/common/common.module.ts | 5 +- apps/api/src/common/job.handler.ts | 68 +++++++ pnpm-lock.yaml | 279 ++++++++++++++++++++++++++- 4 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 apps/api/src/common/job.handler.ts diff --git a/apps/api/package.json b/apps/api/package.json index ca6f224f..ee098e09 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -44,13 +44,13 @@ "passport-github2": "^0.1.12", "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", + "pg-boss": "^10.1.4", "redis": "^4.6.13", "rxjs": "^7.8.1", "socket.io": "^4.7.5", "uuid": "^9.0.1" }, "devDependencies": { - "reflect-metadata": "^0.2.2", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", @@ -67,6 +67,7 @@ "jest-mock-extended": "^3.0.5", "prettier": "^3.0.0", "prisma": "5.19.1", + "reflect-metadata": "^0.2.2", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", diff --git a/apps/api/src/common/common.module.ts b/apps/api/src/common/common.module.ts index 29c83e0f..08712bd9 100644 --- a/apps/api/src/common/common.module.ts +++ b/apps/api/src/common/common.module.ts @@ -1,11 +1,12 @@ import { Global, Module } from '@nestjs/common' import { AuthorityCheckerService } from './authority-checker.service' import { CustomLoggerService } from './logger.service' +import JobHandler from './job.handler' @Global() @Module({ imports: [], - providers: [AuthorityCheckerService, CustomLoggerService], - exports: [AuthorityCheckerService, CustomLoggerService] + providers: [AuthorityCheckerService, CustomLoggerService, JobHandler], + exports: [AuthorityCheckerService, CustomLoggerService, JobHandler] }) export class CommonModule {} diff --git a/apps/api/src/common/job.handler.ts b/apps/api/src/common/job.handler.ts new file mode 100644 index 00000000..73869a01 --- /dev/null +++ b/apps/api/src/common/job.handler.ts @@ -0,0 +1,68 @@ +import PgBoss from 'pg-boss' + +class JobHandler { + private boss: PgBoss + + constructor(connectionString: string) { + this.boss = new PgBoss(connectionString) + this.initialize() + } + + private async initialize() { + try { + await this.boss.start() + console.log('PgBoss started successfully') + } catch (error) { + console.error('Error starting PgBoss:', error) + throw error + } + } + + async registerJob( + queue: string, + callback: (job: V) => Promise + ): Promise { + try { + await this.boss.work(queue, async ([job]) => { + try { + await callback(job.data) + await this.boss.complete(queue, job.id) + } catch (error) { + console.error(`Error processing job in queue ${queue}:`, error) + throw error + } + }) + console.log(`Registered job handler for queue: ${queue}`) + } catch (error) { + console.error(`Error registering job handler for queue ${queue}:`, error) + throw error + } + } + + async scheduleJob( + name: string, + cron: string, + data: V, + options?: PgBoss.ScheduleOptions + ): Promise { + try { + const jobId = await this.boss.schedule(name, cron, data, options) + console.log(`Scheduled job in queue ${name} with ID: ${jobId}`) + } catch (error) { + console.error(`Error scheduling job in queue ${name}:`, error) + throw error + } + } + + async stop(): Promise { + try { + await this.boss.stop() + console.log('PgBoss stopped successfully') + } catch (error) { + console.error('Error stopping PgBoss:', error) + throw error + } + } +} + +export default JobHandler diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9028fe9..7b30df4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,9 @@ importers: passport-google-oauth20: specifier: ^2.0.0 version: 2.0.0 + pg-boss: + specifier: ^10.1.4 + version: 10.1.4 redis: specifier: ^4.6.13 version: 4.7.0 @@ -252,10 +255,10 @@ importers: version: 7.4.2 jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)) + version: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) jest-mock-extended: specifier: ^3.0.5 - version: 3.0.7(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.3.3) + version: 3.0.7(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)))(typescript@5.3.3) prettier: specifier: ^3.0.0 version: 3.3.3 @@ -273,7 +276,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.3.3) + version: 29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)))(typescript@5.3.3) ts-loader: specifier: ^9.4.3 version: 9.5.1(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.7.26)) @@ -325,7 +328,7 @@ importers: devDependencies: '@swc/cli': specifier: ^0.4.0 - version: 0.4.0(@swc/core@1.7.26)(chokidar@3.6.0) + version: 0.4.0(@swc/core@1.7.26(@swc/helpers@0.5.2))(chokidar@3.6.0) '@swc/core': specifier: ^1.6.13 version: 1.7.26(@swc/helpers@0.5.2) @@ -4527,6 +4530,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cron@3.1.7: resolution: {integrity: sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==} @@ -7364,6 +7371,44 @@ packages: periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + pg-boss@10.1.4: + resolution: {integrity: sha512-JM6wV4Kk3h+7QXJDicD3w0FJR2KAKwKsK6koj6+yVRslsQD/BMq9GAF52St//ZDZnbPv6JLO0KOCh6MfWSp1UA==} + engines: {node: '>=20'} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.13.0: + resolution: {integrity: sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} @@ -7483,6 +7528,22 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -7989,6 +8050,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -10290,6 +10355,41 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.16.5 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2))': dependencies: '@jest/console': 29.7.0 @@ -10562,7 +10662,7 @@ snapshots: webpack: 5.94.0(@swc/core@1.7.26) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/cli': 0.4.0(@swc/core@1.7.26)(chokidar@3.6.0) + '@swc/cli': 0.4.0(@swc/core@1.7.26(@swc/helpers@0.5.2))(chokidar@3.6.0) '@swc/core': 1.7.26(@swc/helpers@0.5.2) transitivePeerDependencies: - esbuild @@ -11952,7 +12052,7 @@ snapshots: - supports-color - typescript - '@swc/cli@0.4.0(@swc/core@1.7.26)(chokidar@3.6.0)': + '@swc/cli@0.4.0(@swc/core@1.7.26(@swc/helpers@0.5.2))(chokidar@3.6.0)': dependencies: '@mole-inc/bin-wrapper': 8.0.1 '@swc/core': 1.7.26(@swc/helpers@0.5.2) @@ -13637,6 +13737,21 @@ snapshots: sha.js: 2.4.11 optional: true + create-jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)): dependencies: '@jest/types': 29.6.3 @@ -13654,6 +13769,10 @@ snapshots: create-require@1.1.1: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.4.4 + cron@3.1.7: dependencies: '@types/luxon': 3.4.2 @@ -15715,6 +15834,25 @@ snapshots: - babel-plugin-macros - supports-color + jest-cli@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)) @@ -15734,6 +15872,37 @@ snapshots: - supports-color - ts-node + jest-config@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)): + dependencies: + '@babel/core': 7.25.2 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.25.2) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.16.5 + ts-node: 10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)): dependencies: '@babel/core': 7.25.2 @@ -15835,9 +16004,9 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-mock-extended@3.0.7(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.3.3): + jest-mock-extended@3.0.7(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)))(typescript@5.3.3): dependencies: - jest: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) ts-essentials: 10.0.2(typescript@5.3.3) typescript: 5.3.3 @@ -15992,6 +16161,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)) @@ -17156,6 +17337,49 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.2 + pg-boss@10.1.4: + dependencies: + cron-parser: 4.9.0 + pg: 8.13.0 + serialize-error: 8.1.0 + transitivePeerDependencies: + - pg-native + + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.7.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.7.0(pg@8.13.0): + dependencies: + pg: 8.13.0 + + pg-protocol@1.7.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.13.0: + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.0) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.0: {} picomatch@2.3.1: {} @@ -17263,6 +17487,16 @@ snapshots: picocolors: 1.1.0 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -17812,6 +18046,10 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error@8.1.0: + dependencies: + type-fest: 0.20.2 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -18432,12 +18670,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.3.3): + ts-jest@29.2.5(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)))(typescript@5.3.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2)) + jest: 29.7.0(@types/node@20.16.5)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -18480,6 +18718,27 @@ snapshots: typescript: 5.3.3 webpack: 5.94.0(@swc/core@1.7.26) + ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.3.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.16.5 + acorn: 8.12.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.7.26(@swc/helpers@0.5.2) + optional: true + ts-node@10.9.2(@swc/core@1.7.26)(@types/node@20.16.5)(typescript@5.6.2): dependencies: '@cspotcode/source-map-support': 0.8.1 From f291ba458a3c1568f288e702f28df7a219dbd26b Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Sat, 5 Oct 2024 13:34:39 +0530 Subject: [PATCH 2/8] feat: enhance error handling and type assertions --- apps/api/src/common/job.handler.ts | 34 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/api/src/common/job.handler.ts b/apps/api/src/common/job.handler.ts index 73869a01..b1592693 100644 --- a/apps/api/src/common/job.handler.ts +++ b/apps/api/src/common/job.handler.ts @@ -1,7 +1,9 @@ +import { LoggerService } from '@nestjs/common' import PgBoss from 'pg-boss' class JobHandler { private boss: PgBoss + private readonly logger: LoggerService constructor(connectionString: string) { this.boss = new PgBoss(connectionString) @@ -11,10 +13,10 @@ class JobHandler { private async initialize() { try { await this.boss.start() - console.log('PgBoss started successfully') + this.logger.log('PgBoss started successfully') } catch (error) { - console.error('Error starting PgBoss:', error) - throw error + this.logger.error(`Error starting PgBoss ${error}`) + throw new Error(`Error starting PgBoss ${error}`) } } @@ -25,21 +27,22 @@ class JobHandler { try { await this.boss.work(queue, async ([job]) => { try { - await callback(job.data) + const jobData = job.data as V + await callback(jobData) await this.boss.complete(queue, job.id) } catch (error) { - console.error(`Error processing job in queue ${queue}:`, error) - throw error + this.logger.error(`Error processing job in queue ${queue}`) + throw new Error(`Error processing job in queue ${queue}`) } }) - console.log(`Registered job handler for queue: ${queue}`) + this.logger.log(`Registered job handler for queue ${queue}`) } catch (error) { - console.error(`Error registering job handler for queue ${queue}:`, error) - throw error + this.logger.error(`Error registering job handler for queue ${queue}`) + throw new Error(`Error registering job handler for queue ${queue}`) } } - async scheduleJob( + async scheduleJob( name: string, cron: string, data: V, @@ -47,10 +50,10 @@ class JobHandler { ): Promise { try { const jobId = await this.boss.schedule(name, cron, data, options) - console.log(`Scheduled job in queue ${name} with ID: ${jobId}`) + this.logger.log(`Scheduled job in queue ${name} with ID: ${jobId}`) } catch (error) { - console.error(`Error scheduling job in queue ${name}:`, error) - throw error + this.logger.error(`Error scheduling job in queue ${name}`) + throw new Error(`Error scheduling job in queue ${name}:`, error) } } @@ -58,9 +61,10 @@ class JobHandler { try { await this.boss.stop() console.log('PgBoss stopped successfully') + this.logger.log('PgBoss stopped successfully') } catch (error) { - console.error('Error stopping PgBoss:', error) - throw error + this.logger.error('Error stopping PgBoss:', error) + throw new Error('Error stopping PgBoss:', error) } } } From af9765795fe813f7df3ae0834b63c65f477df6ab Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Sat, 5 Oct 2024 13:49:28 +0530 Subject: [PATCH 3/8] fix: follow naming conventions & add dependency injection --- apps/api/src/common/common.module.ts | 6 +++--- apps/api/src/common/job.handler.ts | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/api/src/common/common.module.ts b/apps/api/src/common/common.module.ts index 08712bd9..3ba6d1f4 100644 --- a/apps/api/src/common/common.module.ts +++ b/apps/api/src/common/common.module.ts @@ -1,12 +1,12 @@ import { Global, Module } from '@nestjs/common' import { AuthorityCheckerService } from './authority-checker.service' import { CustomLoggerService } from './logger.service' -import JobHandler from './job.handler' +import JobHandlerService from './job.handler' @Global() @Module({ imports: [], - providers: [AuthorityCheckerService, CustomLoggerService, JobHandler], - exports: [AuthorityCheckerService, CustomLoggerService, JobHandler] + providers: [AuthorityCheckerService, CustomLoggerService, JobHandlerService], + exports: [AuthorityCheckerService, CustomLoggerService, JobHandlerService] }) export class CommonModule {} diff --git a/apps/api/src/common/job.handler.ts b/apps/api/src/common/job.handler.ts index b1592693..e33cd10b 100644 --- a/apps/api/src/common/job.handler.ts +++ b/apps/api/src/common/job.handler.ts @@ -1,7 +1,8 @@ -import { LoggerService } from '@nestjs/common' +import { Injectable, LoggerService } from '@nestjs/common' import PgBoss from 'pg-boss' -class JobHandler { +@Injectable() +export default class JobHandlerService { private boss: PgBoss private readonly logger: LoggerService @@ -68,5 +69,3 @@ class JobHandler { } } } - -export default JobHandler From e6a521ec5cf7ee4400c93bfe8d13d8b9995ed408 Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Sat, 5 Oct 2024 15:18:42 +0530 Subject: [PATCH 4/8] fix: pass connection string from common module --- apps/api/src/common/common.module.ts | 12 ++++++++++-- apps/api/src/common/job.handler.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/api/src/common/common.module.ts b/apps/api/src/common/common.module.ts index 3ba6d1f4..074a9995 100644 --- a/apps/api/src/common/common.module.ts +++ b/apps/api/src/common/common.module.ts @@ -1,12 +1,20 @@ import { Global, Module } from '@nestjs/common' import { AuthorityCheckerService } from './authority-checker.service' import { CustomLoggerService } from './logger.service' -import JobHandlerService from './job.handler' +import { JobHandlerService } from './job.handler' @Global() @Module({ imports: [], - providers: [AuthorityCheckerService, CustomLoggerService, JobHandlerService], + providers: [ + AuthorityCheckerService, + CustomLoggerService, + JobHandlerService, + { + provide: 'DATABASE_CONNECTION_STRING', + useValue: process.env.DATABASE_URL + } + ], exports: [AuthorityCheckerService, CustomLoggerService, JobHandlerService] }) export class CommonModule {} diff --git a/apps/api/src/common/job.handler.ts b/apps/api/src/common/job.handler.ts index e33cd10b..9e7dfb6f 100644 --- a/apps/api/src/common/job.handler.ts +++ b/apps/api/src/common/job.handler.ts @@ -1,13 +1,15 @@ -import { Injectable, LoggerService } from '@nestjs/common' +import { Inject, Injectable, LoggerService } from '@nestjs/common' import PgBoss from 'pg-boss' @Injectable() -export default class JobHandlerService { +export class JobHandlerService { private boss: PgBoss private readonly logger: LoggerService - constructor(connectionString: string) { - this.boss = new PgBoss(connectionString) + constructor( + @Inject('DATABASE_CONNECTION_STRING') private connectionString: string + ) { + this.boss = new PgBoss(this.connectionString) this.initialize() } From 929448ad66d174535375d163a10d73f83e8056d9 Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Wed, 9 Oct 2024 22:58:09 +0530 Subject: [PATCH 5/8] feat: integrated pg-boss service and added relevant mock tests --- apps/api/src/common/job-handler.service.ts | 62 ++++++++ .../src/mail/templates/workspace-removal.tsx | 146 ++++++++++++++++++ apps/api/src/provider/pgboss.provider.ts | 15 ++ apps/api/src/provider/provider.module.ts | 7 +- .../controller/secret.controller.spec.ts | 4 + .../src/secret/service/secret.service.spec.ts | 4 + .../controller/variable.controller.spec.ts | 4 + .../variable/service/variable.service.spec.ts | 4 + 8 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/common/job-handler.service.ts create mode 100644 apps/api/src/mail/templates/workspace-removal.tsx create mode 100644 apps/api/src/provider/pgboss.provider.ts diff --git a/apps/api/src/common/job-handler.service.ts b/apps/api/src/common/job-handler.service.ts new file mode 100644 index 00000000..f20ba4d1 --- /dev/null +++ b/apps/api/src/common/job-handler.service.ts @@ -0,0 +1,62 @@ +import { PG_BOSS } from '@/provider/pgboss.provider' +import { Inject, Injectable, LoggerService } from '@nestjs/common' +import PgBoss from 'pg-boss' + +@Injectable() +export class JobHandlerService { + private readonly logger: LoggerService + + constructor( + @Inject(PG_BOSS) private boss: PgBoss, + @Inject('Logger') logger: LoggerService + ) { + this.logger = logger + } + + async registerJob( + queue: string, + callback: (job: V) => Promise + ): Promise { + try { + await this.boss.work(queue, async ([job]) => { + try { + const jobData = job.data as V + await callback(jobData) + await this.boss.complete(queue, job.id) + } catch (error) { + this.logger.error(`Error processing job in queue ${queue}`) + throw new Error(`Error processing job in queue ${queue}`) + } + }) + this.logger.log(`Registered job handler for queue ${queue}`) + } catch (error) { + this.logger.error(`Error registering job handler for queue ${queue}`) + throw new Error(`Error registering job handler for queue ${queue}`) + } + } + + async scheduleJob( + name: string, + cron: string, + data: V, + options?: PgBoss.ScheduleOptions + ): Promise { + try { + const jobId = await this.boss.schedule(name, cron, data, options) + this.logger.log(`Scheduled job in queue ${name} with ID: ${jobId}`) + } catch (error) { + this.logger.error(`Error scheduling job in queue ${name}`) + throw new Error(`Error scheduling job in queue ${name}: ${error.message}`) + } + } + + async stop(): Promise { + try { + await this.boss.stop() + this.logger.log('PgBoss stopped successfully') + } catch (error) { + this.logger.error(`Error stopping PgBoss: ${error.message}`) + throw new Error(`Error stopping PgBoss: ${error.message}`) + } + } +} diff --git a/apps/api/src/mail/templates/workspace-removal.tsx b/apps/api/src/mail/templates/workspace-removal.tsx new file mode 100644 index 00000000..336e8a19 --- /dev/null +++ b/apps/api/src/mail/templates/workspace-removal.tsx @@ -0,0 +1,146 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Link, + Preview, + Section, + Text +} from '@react-email/components' +import * as React from 'react' + +interface WorkspaceRemovalEmailProps { + workspaceName: string + removedOn: string +} + +export const RemovedFromWorkspaceEmail = ({ + workspaceName, + removedOn +}: WorkspaceRemovalEmailProps) => { + return ( + + + Removal from Workspace + + +
+ Removal from Workspace + Dear User, + + We hope this email finds you well. We are writing to inform you + that your access to the following workspace has been removed: + +
+ + Workspace Name: {workspaceName} + + + Removed On: {removedOn} + +
+ + If you believe this action was taken in error or have any + questions regarding this change, please contact your project + administrator or our support team. + + + We appreciate your understanding and thank you for your + contributions to the project. + + + Cheers, +
+ Team Keyshade +
+
+
+ + This is an automated message. Please do not reply to this email. + + + Read our{' '} + + Privacy Policy + {' '} + and{' '} + + Terms and Conditions + {' '} + for more information on how we manage your data and services. + +
+
+ + + ) +} + +export default RemovedFromWorkspaceEmail + +const main = { + fontFamily: "'Segoe UI', 'Roboto', sans-serif", + lineHeight: '1.6', + color: '#04050a', + backgroundColor: '#fafafa', + margin: '0', + padding: '20px' +} + +const container = { + maxWidth: '600px', + margin: '0 auto', + backgroundColor: '#fff', + borderRadius: '5px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' +} + +const content = { + padding: '40px' +} + +const h1 = { + color: '#000', + marginBottom: '20px', + fontSize: '24px', + fontWeight: '600' +} + +const text = { + marginBottom: '15px', + color: '#666' +} + +const workspaceDetails = { + width: '100%', + backgroundColor: '#fafafa', + borderRadius: '5px', + marginBottom: '20px', + padding: '18px' +} + +const workspaceInfo = { + marginBottom: '5px' +} + +const footer = { + borderTop: '1px solid #eaeaea', + padding: '20px 40px' +} + +const footerText = { + fontSize: '12px', + color: '#999', + textAlign: 'center' as const, + marginBottom: '5px' +} + +const link = { + color: '#000', + textDecoration: 'underline' +} diff --git a/apps/api/src/provider/pgboss.provider.ts b/apps/api/src/provider/pgboss.provider.ts new file mode 100644 index 00000000..14a0fcd6 --- /dev/null +++ b/apps/api/src/provider/pgboss.provider.ts @@ -0,0 +1,15 @@ +// pgboss.provider.ts +import { Provider } from '@nestjs/common' +import PgBoss from 'pg-boss' + +export const PG_BOSS = 'PG_BOSS' + +export const PgBossProvider: Provider = { + provide: PG_BOSS, + useFactory: async () => { + const connectionString = process.env.DATABASE_CONNECTION_STRING + const boss = new PgBoss(connectionString) + await boss.start() + return boss + } +} diff --git a/apps/api/src/provider/provider.module.ts b/apps/api/src/provider/provider.module.ts index d9d5a700..d7ca6947 100644 --- a/apps/api/src/provider/provider.module.ts +++ b/apps/api/src/provider/provider.module.ts @@ -1,6 +1,7 @@ import { Global, Module } from '@nestjs/common' import { REDIS_CLIENT, RedisProvider } from './redis.provider' import { MINIO_CLIENT, MinioProvider } from './minio.provider' +import { PG_BOSS, PgBossProvider } from './pgboss.provider' @Global() @Module({ @@ -12,8 +13,12 @@ import { MINIO_CLIENT, MinioProvider } from './minio.provider' { provide: MINIO_CLIENT, useValue: MinioProvider + }, + { + provide: PG_BOSS, + useValue: PgBossProvider } ], - providers: [RedisProvider, MinioProvider] + providers: [RedisProvider, MinioProvider, PgBossProvider] }) export class ProviderModule {} diff --git a/apps/api/src/secret/controller/secret.controller.spec.ts b/apps/api/src/secret/controller/secret.controller.spec.ts index 353b9114..c372bc26 100644 --- a/apps/api/src/secret/controller/secret.controller.spec.ts +++ b/apps/api/src/secret/controller/secret.controller.spec.ts @@ -10,6 +10,8 @@ import { RedisClientType } from 'redis' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('SecretController', () => { let controller: SecretController @@ -32,6 +34,8 @@ describe('SecretController', () => { .useValue(mockDeep()) .overrideProvider(PrismaService) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() controller = module.get(SecretController) diff --git a/apps/api/src/secret/service/secret.service.spec.ts b/apps/api/src/secret/service/secret.service.spec.ts index 421ede60..657b494a 100644 --- a/apps/api/src/secret/service/secret.service.spec.ts +++ b/apps/api/src/secret/service/secret.service.spec.ts @@ -9,6 +9,8 @@ import { RedisClientType } from 'redis' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('SecretService', () => { let service: SecretService @@ -30,6 +32,8 @@ describe('SecretService', () => { .useValue(mockDeep()) .overrideProvider(PrismaService) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() service = module.get(SecretService) diff --git a/apps/api/src/variable/controller/variable.controller.spec.ts b/apps/api/src/variable/controller/variable.controller.spec.ts index 4abddf2d..52ff7204 100644 --- a/apps/api/src/variable/controller/variable.controller.spec.ts +++ b/apps/api/src/variable/controller/variable.controller.spec.ts @@ -10,6 +10,8 @@ import { mockDeep } from 'jest-mock-extended' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('VariableController', () => { let controller: VariableController @@ -30,6 +32,8 @@ describe('VariableController', () => { }) .overrideProvider(REDIS_CLIENT) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() controller = module.get(VariableController) diff --git a/apps/api/src/variable/service/variable.service.spec.ts b/apps/api/src/variable/service/variable.service.spec.ts index 3efd6efd..17cc1ffc 100644 --- a/apps/api/src/variable/service/variable.service.spec.ts +++ b/apps/api/src/variable/service/variable.service.spec.ts @@ -9,6 +9,8 @@ import { mockDeep } from 'jest-mock-extended' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('VariableService', () => { let service: VariableService @@ -28,6 +30,8 @@ describe('VariableService', () => { }) .overrideProvider(REDIS_CLIENT) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() service = module.get(VariableService) From 07ad4def331fb9268d24ac1f8b74e06b25cbb5c1 Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Wed, 9 Oct 2024 23:10:53 +0530 Subject: [PATCH 6/8] fix: remove job handler usage from common module --- apps/api/src/common/common.module.ts | 13 +- apps/api/src/common/job-handler.service.ts | 62 ++++++++ .../src/mail/templates/workspace-removal.tsx | 146 ++++++++++++++++++ apps/api/src/provider/pgboss.provider.ts | 15 ++ apps/api/src/provider/provider.module.ts | 7 +- .../controller/secret.controller.spec.ts | 4 + .../src/secret/service/secret.service.spec.ts | 4 + .../controller/variable.controller.spec.ts | 4 + .../variable/service/variable.service.spec.ts | 4 + 9 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 apps/api/src/common/job-handler.service.ts create mode 100644 apps/api/src/mail/templates/workspace-removal.tsx create mode 100644 apps/api/src/provider/pgboss.provider.ts diff --git a/apps/api/src/common/common.module.ts b/apps/api/src/common/common.module.ts index 074a9995..29c83e0f 100644 --- a/apps/api/src/common/common.module.ts +++ b/apps/api/src/common/common.module.ts @@ -1,20 +1,11 @@ import { Global, Module } from '@nestjs/common' import { AuthorityCheckerService } from './authority-checker.service' import { CustomLoggerService } from './logger.service' -import { JobHandlerService } from './job.handler' @Global() @Module({ imports: [], - providers: [ - AuthorityCheckerService, - CustomLoggerService, - JobHandlerService, - { - provide: 'DATABASE_CONNECTION_STRING', - useValue: process.env.DATABASE_URL - } - ], - exports: [AuthorityCheckerService, CustomLoggerService, JobHandlerService] + providers: [AuthorityCheckerService, CustomLoggerService], + exports: [AuthorityCheckerService, CustomLoggerService] }) export class CommonModule {} diff --git a/apps/api/src/common/job-handler.service.ts b/apps/api/src/common/job-handler.service.ts new file mode 100644 index 00000000..f20ba4d1 --- /dev/null +++ b/apps/api/src/common/job-handler.service.ts @@ -0,0 +1,62 @@ +import { PG_BOSS } from '@/provider/pgboss.provider' +import { Inject, Injectable, LoggerService } from '@nestjs/common' +import PgBoss from 'pg-boss' + +@Injectable() +export class JobHandlerService { + private readonly logger: LoggerService + + constructor( + @Inject(PG_BOSS) private boss: PgBoss, + @Inject('Logger') logger: LoggerService + ) { + this.logger = logger + } + + async registerJob( + queue: string, + callback: (job: V) => Promise + ): Promise { + try { + await this.boss.work(queue, async ([job]) => { + try { + const jobData = job.data as V + await callback(jobData) + await this.boss.complete(queue, job.id) + } catch (error) { + this.logger.error(`Error processing job in queue ${queue}`) + throw new Error(`Error processing job in queue ${queue}`) + } + }) + this.logger.log(`Registered job handler for queue ${queue}`) + } catch (error) { + this.logger.error(`Error registering job handler for queue ${queue}`) + throw new Error(`Error registering job handler for queue ${queue}`) + } + } + + async scheduleJob( + name: string, + cron: string, + data: V, + options?: PgBoss.ScheduleOptions + ): Promise { + try { + const jobId = await this.boss.schedule(name, cron, data, options) + this.logger.log(`Scheduled job in queue ${name} with ID: ${jobId}`) + } catch (error) { + this.logger.error(`Error scheduling job in queue ${name}`) + throw new Error(`Error scheduling job in queue ${name}: ${error.message}`) + } + } + + async stop(): Promise { + try { + await this.boss.stop() + this.logger.log('PgBoss stopped successfully') + } catch (error) { + this.logger.error(`Error stopping PgBoss: ${error.message}`) + throw new Error(`Error stopping PgBoss: ${error.message}`) + } + } +} diff --git a/apps/api/src/mail/templates/workspace-removal.tsx b/apps/api/src/mail/templates/workspace-removal.tsx new file mode 100644 index 00000000..336e8a19 --- /dev/null +++ b/apps/api/src/mail/templates/workspace-removal.tsx @@ -0,0 +1,146 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Link, + Preview, + Section, + Text +} from '@react-email/components' +import * as React from 'react' + +interface WorkspaceRemovalEmailProps { + workspaceName: string + removedOn: string +} + +export const RemovedFromWorkspaceEmail = ({ + workspaceName, + removedOn +}: WorkspaceRemovalEmailProps) => { + return ( + + + Removal from Workspace + + +
+ Removal from Workspace + Dear User, + + We hope this email finds you well. We are writing to inform you + that your access to the following workspace has been removed: + +
+ + Workspace Name: {workspaceName} + + + Removed On: {removedOn} + +
+ + If you believe this action was taken in error or have any + questions regarding this change, please contact your project + administrator or our support team. + + + We appreciate your understanding and thank you for your + contributions to the project. + + + Cheers, +
+ Team Keyshade +
+
+
+ + This is an automated message. Please do not reply to this email. + + + Read our{' '} + + Privacy Policy + {' '} + and{' '} + + Terms and Conditions + {' '} + for more information on how we manage your data and services. + +
+
+ + + ) +} + +export default RemovedFromWorkspaceEmail + +const main = { + fontFamily: "'Segoe UI', 'Roboto', sans-serif", + lineHeight: '1.6', + color: '#04050a', + backgroundColor: '#fafafa', + margin: '0', + padding: '20px' +} + +const container = { + maxWidth: '600px', + margin: '0 auto', + backgroundColor: '#fff', + borderRadius: '5px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' +} + +const content = { + padding: '40px' +} + +const h1 = { + color: '#000', + marginBottom: '20px', + fontSize: '24px', + fontWeight: '600' +} + +const text = { + marginBottom: '15px', + color: '#666' +} + +const workspaceDetails = { + width: '100%', + backgroundColor: '#fafafa', + borderRadius: '5px', + marginBottom: '20px', + padding: '18px' +} + +const workspaceInfo = { + marginBottom: '5px' +} + +const footer = { + borderTop: '1px solid #eaeaea', + padding: '20px 40px' +} + +const footerText = { + fontSize: '12px', + color: '#999', + textAlign: 'center' as const, + marginBottom: '5px' +} + +const link = { + color: '#000', + textDecoration: 'underline' +} diff --git a/apps/api/src/provider/pgboss.provider.ts b/apps/api/src/provider/pgboss.provider.ts new file mode 100644 index 00000000..14a0fcd6 --- /dev/null +++ b/apps/api/src/provider/pgboss.provider.ts @@ -0,0 +1,15 @@ +// pgboss.provider.ts +import { Provider } from '@nestjs/common' +import PgBoss from 'pg-boss' + +export const PG_BOSS = 'PG_BOSS' + +export const PgBossProvider: Provider = { + provide: PG_BOSS, + useFactory: async () => { + const connectionString = process.env.DATABASE_CONNECTION_STRING + const boss = new PgBoss(connectionString) + await boss.start() + return boss + } +} diff --git a/apps/api/src/provider/provider.module.ts b/apps/api/src/provider/provider.module.ts index d9d5a700..d7ca6947 100644 --- a/apps/api/src/provider/provider.module.ts +++ b/apps/api/src/provider/provider.module.ts @@ -1,6 +1,7 @@ import { Global, Module } from '@nestjs/common' import { REDIS_CLIENT, RedisProvider } from './redis.provider' import { MINIO_CLIENT, MinioProvider } from './minio.provider' +import { PG_BOSS, PgBossProvider } from './pgboss.provider' @Global() @Module({ @@ -12,8 +13,12 @@ import { MINIO_CLIENT, MinioProvider } from './minio.provider' { provide: MINIO_CLIENT, useValue: MinioProvider + }, + { + provide: PG_BOSS, + useValue: PgBossProvider } ], - providers: [RedisProvider, MinioProvider] + providers: [RedisProvider, MinioProvider, PgBossProvider] }) export class ProviderModule {} diff --git a/apps/api/src/secret/controller/secret.controller.spec.ts b/apps/api/src/secret/controller/secret.controller.spec.ts index 353b9114..c372bc26 100644 --- a/apps/api/src/secret/controller/secret.controller.spec.ts +++ b/apps/api/src/secret/controller/secret.controller.spec.ts @@ -10,6 +10,8 @@ import { RedisClientType } from 'redis' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('SecretController', () => { let controller: SecretController @@ -32,6 +34,8 @@ describe('SecretController', () => { .useValue(mockDeep()) .overrideProvider(PrismaService) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() controller = module.get(SecretController) diff --git a/apps/api/src/secret/service/secret.service.spec.ts b/apps/api/src/secret/service/secret.service.spec.ts index 421ede60..657b494a 100644 --- a/apps/api/src/secret/service/secret.service.spec.ts +++ b/apps/api/src/secret/service/secret.service.spec.ts @@ -9,6 +9,8 @@ import { RedisClientType } from 'redis' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('SecretService', () => { let service: SecretService @@ -30,6 +32,8 @@ describe('SecretService', () => { .useValue(mockDeep()) .overrideProvider(PrismaService) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() service = module.get(SecretService) diff --git a/apps/api/src/variable/controller/variable.controller.spec.ts b/apps/api/src/variable/controller/variable.controller.spec.ts index 4abddf2d..52ff7204 100644 --- a/apps/api/src/variable/controller/variable.controller.spec.ts +++ b/apps/api/src/variable/controller/variable.controller.spec.ts @@ -10,6 +10,8 @@ import { mockDeep } from 'jest-mock-extended' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('VariableController', () => { let controller: VariableController @@ -30,6 +32,8 @@ describe('VariableController', () => { }) .overrideProvider(REDIS_CLIENT) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() controller = module.get(VariableController) diff --git a/apps/api/src/variable/service/variable.service.spec.ts b/apps/api/src/variable/service/variable.service.spec.ts index 3efd6efd..17cc1ffc 100644 --- a/apps/api/src/variable/service/variable.service.spec.ts +++ b/apps/api/src/variable/service/variable.service.spec.ts @@ -9,6 +9,8 @@ import { mockDeep } from 'jest-mock-extended' import { ProviderModule } from '@/provider/provider.module' import { AuthorityCheckerService } from '@/common/authority-checker.service' import { CommonModule } from '@/common/common.module' +import { PG_BOSS } from '@/provider/pgboss.provider' +import PgBoss from 'pg-boss' describe('VariableService', () => { let service: VariableService @@ -28,6 +30,8 @@ describe('VariableService', () => { }) .overrideProvider(REDIS_CLIENT) .useValue(mockDeep()) + .overrideProvider(PG_BOSS) + .useValue(mockDeep()) .compile() service = module.get(VariableService) From 6ee16bcb885df86fdbb82e8cc52aea34481f7ce5 Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Wed, 9 Oct 2024 23:15:31 +0530 Subject: [PATCH 7/8] nit: remove changes that are not required --- .../src/mail/templates/workspace-removal.tsx | 146 ------------------ 1 file changed, 146 deletions(-) delete mode 100644 apps/api/src/mail/templates/workspace-removal.tsx diff --git a/apps/api/src/mail/templates/workspace-removal.tsx b/apps/api/src/mail/templates/workspace-removal.tsx deleted file mode 100644 index 336e8a19..00000000 --- a/apps/api/src/mail/templates/workspace-removal.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { - Body, - Container, - Head, - Heading, - Html, - Link, - Preview, - Section, - Text -} from '@react-email/components' -import * as React from 'react' - -interface WorkspaceRemovalEmailProps { - workspaceName: string - removedOn: string -} - -export const RemovedFromWorkspaceEmail = ({ - workspaceName, - removedOn -}: WorkspaceRemovalEmailProps) => { - return ( - - - Removal from Workspace - - -
- Removal from Workspace - Dear User, - - We hope this email finds you well. We are writing to inform you - that your access to the following workspace has been removed: - -
- - Workspace Name: {workspaceName} - - - Removed On: {removedOn} - -
- - If you believe this action was taken in error or have any - questions regarding this change, please contact your project - administrator or our support team. - - - We appreciate your understanding and thank you for your - contributions to the project. - - - Cheers, -
- Team Keyshade -
-
-
- - This is an automated message. Please do not reply to this email. - - - Read our{' '} - - Privacy Policy - {' '} - and{' '} - - Terms and Conditions - {' '} - for more information on how we manage your data and services. - -
-
- - - ) -} - -export default RemovedFromWorkspaceEmail - -const main = { - fontFamily: "'Segoe UI', 'Roboto', sans-serif", - lineHeight: '1.6', - color: '#04050a', - backgroundColor: '#fafafa', - margin: '0', - padding: '20px' -} - -const container = { - maxWidth: '600px', - margin: '0 auto', - backgroundColor: '#fff', - borderRadius: '5px', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)' -} - -const content = { - padding: '40px' -} - -const h1 = { - color: '#000', - marginBottom: '20px', - fontSize: '24px', - fontWeight: '600' -} - -const text = { - marginBottom: '15px', - color: '#666' -} - -const workspaceDetails = { - width: '100%', - backgroundColor: '#fafafa', - borderRadius: '5px', - marginBottom: '20px', - padding: '18px' -} - -const workspaceInfo = { - marginBottom: '5px' -} - -const footer = { - borderTop: '1px solid #eaeaea', - padding: '20px 40px' -} - -const footerText = { - fontSize: '12px', - color: '#999', - textAlign: 'center' as const, - marginBottom: '5px' -} - -const link = { - color: '#000', - textDecoration: 'underline' -} From e4e409fc3d40937f8fe0fc80c54e4f9d1214771f Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes Date: Wed, 9 Oct 2024 23:17:53 +0530 Subject: [PATCH 8/8] nit: remove old job-handler file --- apps/api/src/common/job.handler.ts | 73 ------------------------------ 1 file changed, 73 deletions(-) delete mode 100644 apps/api/src/common/job.handler.ts diff --git a/apps/api/src/common/job.handler.ts b/apps/api/src/common/job.handler.ts deleted file mode 100644 index 9e7dfb6f..00000000 --- a/apps/api/src/common/job.handler.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Inject, Injectable, LoggerService } from '@nestjs/common' -import PgBoss from 'pg-boss' - -@Injectable() -export class JobHandlerService { - private boss: PgBoss - private readonly logger: LoggerService - - constructor( - @Inject('DATABASE_CONNECTION_STRING') private connectionString: string - ) { - this.boss = new PgBoss(this.connectionString) - this.initialize() - } - - private async initialize() { - try { - await this.boss.start() - this.logger.log('PgBoss started successfully') - } catch (error) { - this.logger.error(`Error starting PgBoss ${error}`) - throw new Error(`Error starting PgBoss ${error}`) - } - } - - async registerJob( - queue: string, - callback: (job: V) => Promise - ): Promise { - try { - await this.boss.work(queue, async ([job]) => { - try { - const jobData = job.data as V - await callback(jobData) - await this.boss.complete(queue, job.id) - } catch (error) { - this.logger.error(`Error processing job in queue ${queue}`) - throw new Error(`Error processing job in queue ${queue}`) - } - }) - this.logger.log(`Registered job handler for queue ${queue}`) - } catch (error) { - this.logger.error(`Error registering job handler for queue ${queue}`) - throw new Error(`Error registering job handler for queue ${queue}`) - } - } - - async scheduleJob( - name: string, - cron: string, - data: V, - options?: PgBoss.ScheduleOptions - ): Promise { - try { - const jobId = await this.boss.schedule(name, cron, data, options) - this.logger.log(`Scheduled job in queue ${name} with ID: ${jobId}`) - } catch (error) { - this.logger.error(`Error scheduling job in queue ${name}`) - throw new Error(`Error scheduling job in queue ${name}:`, error) - } - } - - async stop(): Promise { - try { - await this.boss.stop() - console.log('PgBoss stopped successfully') - this.logger.log('PgBoss stopped successfully') - } catch (error) { - this.logger.error('Error stopping PgBoss:', error) - throw new Error('Error stopping PgBoss:', error) - } - } -}