@lyrolab/nest-shared 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.husky/commit-msg +1 -0
  2. package/.husky/pre-commit +1 -0
  3. package/.nvmrc +1 -0
  4. package/.prettierrc +5 -0
  5. package/README.md +98 -0
  6. package/dist/ai/ai.module.d.ts +2 -0
  7. package/dist/ai/ai.module.js +21 -0
  8. package/dist/ai/ai.module.js.map +1 -0
  9. package/dist/ai/services/ai.service.d.ts +10 -0
  10. package/dist/ai/services/ai.service.js +54 -0
  11. package/dist/ai/services/ai.service.js.map +1 -0
  12. package/dist/app.module.d.ts +2 -0
  13. package/dist/app.module.js +21 -0
  14. package/dist/app.module.js.map +1 -0
  15. package/dist/bull/shared-bull.module.d.ts +4 -0
  16. package/dist/bull/shared-bull.module.js +40 -0
  17. package/dist/bull/shared-bull.module.js.map +1 -0
  18. package/dist/cache/shared-cache.module.d.ts +4 -0
  19. package/dist/cache/shared-cache.module.js +40 -0
  20. package/dist/cache/shared-cache.module.js.map +1 -0
  21. package/dist/database/filters/typeorm-exception.filter.d.ts +7 -0
  22. package/dist/database/filters/typeorm-exception.filter.js +35 -0
  23. package/dist/database/filters/typeorm-exception.filter.js.map +1 -0
  24. package/dist/database/shared-database.module.d.ts +18 -0
  25. package/dist/database/shared-database.module.js +119 -0
  26. package/dist/database/shared-database.module.js.map +1 -0
  27. package/dist/index.d.ts +12 -0
  28. package/dist/index.js +29 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/queue/controllers/queue.controller.d.ts +7 -0
  31. package/dist/queue/controllers/queue.controller.js +40 -0
  32. package/dist/queue/controllers/queue.controller.js.map +1 -0
  33. package/dist/queue/decorators/queue.decorator.d.ts +1 -0
  34. package/dist/queue/decorators/queue.decorator.js +6 -0
  35. package/dist/queue/decorators/queue.decorator.js.map +1 -0
  36. package/dist/queue/models/dto/queue-add.dto.d.ts +4 -0
  37. package/dist/queue/models/dto/queue-add.dto.js +28 -0
  38. package/dist/queue/models/dto/queue-add.dto.js.map +1 -0
  39. package/dist/queue/models/job-processor-interface.d.ts +4 -0
  40. package/dist/queue/models/job-processor-interface.js +3 -0
  41. package/dist/queue/models/job-processor-interface.js.map +1 -0
  42. package/dist/queue/processors/queue.processor.d.ts +11 -0
  43. package/dist/queue/processors/queue.processor.js +63 -0
  44. package/dist/queue/processors/queue.processor.js.map +1 -0
  45. package/dist/queue/queue.constants.d.ts +1 -0
  46. package/dist/queue/queue.constants.js +5 -0
  47. package/dist/queue/queue.constants.js.map +1 -0
  48. package/dist/queue/queue.module.d.ts +2 -0
  49. package/dist/queue/queue.module.js +28 -0
  50. package/dist/queue/queue.module.js.map +1 -0
  51. package/dist/queue/services/queue.service.d.ts +6 -0
  52. package/dist/queue/services/queue.service.js +35 -0
  53. package/dist/queue/services/queue.service.js.map +1 -0
  54. package/dist/redis/redis.config.d.ts +4 -0
  55. package/dist/redis/redis.config.js +11 -0
  56. package/dist/redis/redis.config.js.map +1 -0
  57. package/dist/redis/shared-redis.module.d.ts +9 -0
  58. package/dist/redis/shared-redis.module.js +68 -0
  59. package/dist/redis/shared-redis.module.js.map +1 -0
  60. package/dist/tsconfig.build.tsbuildinfo +1 -0
  61. package/eslint.config.mjs +35 -0
  62. package/nest-cli.json +9 -0
  63. package/package.json +126 -0
  64. package/src/ai/ai.module.ts +8 -0
  65. package/src/ai/services/ai.service.ts +51 -0
  66. package/src/app.module.ts +8 -0
  67. package/src/bull/shared-bull.module.ts +27 -0
  68. package/src/cache/shared-cache.module.ts +30 -0
  69. package/src/database/filters/typeorm-exception.filter.ts +35 -0
  70. package/src/database/shared-database.module.ts +136 -0
  71. package/src/index.ts +12 -0
  72. package/src/queue/controllers/queue.controller.ts +13 -0
  73. package/src/queue/decorators/queue.decorator.ts +3 -0
  74. package/src/queue/models/dto/queue-add.dto.ts +10 -0
  75. package/src/queue/models/job-processor-interface.ts +5 -0
  76. package/src/queue/processors/queue.processor.ts +53 -0
  77. package/src/queue/queue.constants.ts +1 -0
  78. package/src/queue/queue.module.ts +14 -0
  79. package/src/queue/services/queue.service.ts +17 -0
  80. package/src/redis/redis.config.ts +3 -0
  81. package/src/redis/shared-redis.module.ts +66 -0
  82. package/tsconfig.build.json +4 -0
  83. package/tsconfig.json +25 -0
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import eslint from '@eslint/js';
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4
+ import globals from 'globals';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: ['eslint.config.mjs'],
10
+ },
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommendedTypeChecked,
13
+ eslintPluginPrettierRecommended,
14
+ {
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.jest,
19
+ },
20
+ ecmaVersion: 5,
21
+ sourceType: 'module',
22
+ parserOptions: {
23
+ projectService: true,
24
+ tsconfigRootDir: import.meta.dirname,
25
+ },
26
+ },
27
+ },
28
+ {
29
+ rules: {
30
+ '@typescript-eslint/no-explicit-any': 'off',
31
+ '@typescript-eslint/no-floating-promises': 'warn',
32
+ '@typescript-eslint/no-unsafe-argument': 'warn'
33
+ },
34
+ },
35
+ );
package/nest-cli.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true,
7
+ "webpack": true
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,126 @@
1
+ {
2
+ "name": "@lyrolab/nest-shared",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "license": "UNLICENSED",
7
+ "commitlint": {
8
+ "extends": [
9
+ "@commitlint/config-conventional"
10
+ ]
11
+ },
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "scripts": {
15
+ "prebuild": "rimraf dist",
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
18
+ "start": "nest start",
19
+ "start:dev": "nest start --watch",
20
+ "start:debug": "nest start --debug --watch",
21
+ "start:prod": "node dist/main",
22
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
23
+ "test": "jest",
24
+ "test:watch": "jest --watch",
25
+ "test:cov": "jest --coverage",
26
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
27
+ "test:e2e": "jest --config ./test/jest-e2e.json",
28
+ "prepare": "husky && npm run build",
29
+ "lint-staged": "npx lint-staged"
30
+ },
31
+ "dependencies": {
32
+ "@nestjs/common": "^11.0.1",
33
+ "@nestjs/platform-express": "^11.0.1",
34
+ "reflect-metadata": "^0.2.2",
35
+ "rxjs": "^7.8.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@nestjs/bullmq": "^11.0.0",
39
+ "@nestjs/cache-manager": "^3.0.0",
40
+ "@nestjs/config": "^4.0.0",
41
+ "@nestjs/typeorm": "^11.0.0",
42
+ "@openrouter/ai-sdk-provider": "^0.4.0",
43
+ "ai": "^4.2.0",
44
+ "bullmq": "^5.0.0",
45
+ "cache-manager": "^6.0.0",
46
+ "class-validator": "^0.14.0",
47
+ "testcontainers": "^10.0.0",
48
+ "typeorm": "^0.3.22"
49
+ },
50
+ "devDependencies": {
51
+ "@commitlint/cli": "^19.8.0",
52
+ "@commitlint/config-conventional": "^19.8.0",
53
+ "@eslint/eslintrc": "^3.2.0",
54
+ "@eslint/js": "^9.18.0",
55
+ "@keyv/redis": "^4.3.2",
56
+ "@nestjs/bullmq": "^11.0.2",
57
+ "@nestjs/cache-manager": "^3.0.1",
58
+ "@nestjs/cli": "^11.0.0",
59
+ "@nestjs/config": "^4.0.2",
60
+ "@nestjs/core": "^11.0.13",
61
+ "@nestjs/schematics": "^11.0.0",
62
+ "@nestjs/testing": "^11.0.1",
63
+ "@nestjs/typeorm": "^11.0.0",
64
+ "@openrouter/ai-sdk-provider": "^0.4.5",
65
+ "@swc/cli": "^0.6.0",
66
+ "@swc/core": "^1.10.7",
67
+ "@types/express": "^5.0.0",
68
+ "@types/jest": "^29.5.14",
69
+ "@types/node": "^22.10.7",
70
+ "@types/supertest": "^6.0.2",
71
+ "ai": "^4.2.11",
72
+ "bullmq": "^5.46.1",
73
+ "cache-manager": "^6.4.1",
74
+ "class-validator": "^0.14.1",
75
+ "eslint": "^9.18.0",
76
+ "eslint-config-prettier": "^10.1.1",
77
+ "eslint-plugin-prettier": "^5.2.6",
78
+ "globals": "^16.0.0",
79
+ "husky": "^9.1.7",
80
+ "jest": "^29.7.0",
81
+ "prettier": "^3.5.3",
82
+ "rimraf": "^6.0.1",
83
+ "source-map-support": "^0.5.21",
84
+ "supertest": "^7.0.0",
85
+ "testcontainers": "^10.24.0",
86
+ "ts-jest": "^29.2.5",
87
+ "ts-loader": "^9.5.2",
88
+ "ts-node": "^10.9.2",
89
+ "tsconfig-paths": "^4.2.0",
90
+ "typeorm": "^0.3.22",
91
+ "typescript": "^5.7.3",
92
+ "typescript-eslint": "^8.20.0"
93
+ },
94
+ "jest": {
95
+ "moduleFileExtensions": [
96
+ "js",
97
+ "json",
98
+ "ts"
99
+ ],
100
+ "rootDir": ".",
101
+ "testRegex": ".*\\.spec\\.ts$",
102
+ "transform": {
103
+ "^.+\\.(t|j)s$": "ts-jest"
104
+ },
105
+ "collectCoverageFrom": [
106
+ "**/*.(t|j)s"
107
+ ],
108
+ "coverageDirectory": "./coverage",
109
+ "testEnvironment": "node",
110
+ "roots": [
111
+ "<rootDir>/src/",
112
+ "<rootDir>/libs/"
113
+ ],
114
+ "moduleNameMapper": {
115
+ "^@app/nest-shared(|/.*)$": "<rootDir>/libs/nest-shared/src/$1"
116
+ }
117
+ },
118
+ "lint-staged": {
119
+ "*.{js,ts,json,css,scss,md}": [
120
+ "prettier --write"
121
+ ],
122
+ "*.{js,ts}": [
123
+ "eslint --fix"
124
+ ]
125
+ }
126
+ }
@@ -0,0 +1,8 @@
1
+ import { Module } from "@nestjs/common"
2
+ import { AiService } from "./services/ai.service"
3
+
4
+ @Module({
5
+ providers: [AiService],
6
+ exports: [AiService],
7
+ })
8
+ export class AiModule {}
@@ -0,0 +1,51 @@
1
+ import { CACHE_MANAGER } from "@nestjs/cache-manager"
2
+ import { Inject, Injectable } from "@nestjs/common"
3
+ import {
4
+ createOpenRouter,
5
+ OpenRouterProvider,
6
+ } from "@openrouter/ai-sdk-provider"
7
+ import {
8
+ LanguageModelV1,
9
+ LanguageModelV1Middleware,
10
+ wrapLanguageModel,
11
+ } from "ai"
12
+ import { Cache } from "cache-manager"
13
+
14
+ @Injectable()
15
+ export class AiService {
16
+ private readonly openrouter: OpenRouterProvider
17
+ private readonly openrouterChat: LanguageModelV1
18
+
19
+ constructor(@Inject(CACHE_MANAGER) private cache: Cache) {
20
+ this.openrouter = createOpenRouter({
21
+ apiKey: process.env.OPENROUTER_API_KEY,
22
+ })
23
+ this.openrouterChat = wrapLanguageModel({
24
+ model: this.openrouter.chat("google/gemini-2.0-flash-001"),
25
+ middleware: [{ wrapGenerate: (options) => this.wrapGenerate(options) }],
26
+ })
27
+ }
28
+
29
+ get model() {
30
+ return this.openrouterChat
31
+ }
32
+
33
+ private async wrapGenerate({
34
+ doGenerate,
35
+ params,
36
+ }: Parameters<
37
+ NonNullable<LanguageModelV1Middleware["wrapGenerate"]>
38
+ >[0]): Promise<Awaited<ReturnType<LanguageModelV1["doGenerate"]>>> {
39
+ const cacheKey = "ai:" + JSON.stringify(params)
40
+
41
+ const cachedResult = await this.cache.get(cacheKey)
42
+ if (cachedResult) {
43
+ return cachedResult as Awaited<ReturnType<LanguageModelV1["doGenerate"]>>
44
+ }
45
+
46
+ const result = await doGenerate()
47
+
48
+ await this.cache.set(cacheKey, result)
49
+ return result
50
+ }
51
+ }
@@ -0,0 +1,8 @@
1
+ import { Module } from "@nestjs/common"
2
+
3
+ @Module({
4
+ imports: [],
5
+ controllers: [],
6
+ providers: [],
7
+ })
8
+ export class AppModule {}
@@ -0,0 +1,27 @@
1
+ import { BullModule } from "@nestjs/bullmq"
2
+ import { DynamicModule, Module } from "@nestjs/common"
3
+ import { ConfigModule } from "@nestjs/config"
4
+ import { SharedRedisModule } from "../redis/shared-redis.module"
5
+ import { RedisConfig } from "../redis/redis.config"
6
+
7
+ @Module({})
8
+ export class SharedBullModule {
9
+ static forRoot(): DynamicModule {
10
+ return {
11
+ module: SharedBullModule,
12
+ imports: [
13
+ SharedRedisModule,
14
+ BullModule.forRootAsync({
15
+ imports: [ConfigModule],
16
+ inject: [RedisConfig],
17
+ useFactory: (redisConfig: RedisConfig) => ({
18
+ connection: {
19
+ url: redisConfig.url,
20
+ },
21
+ }),
22
+ }),
23
+ ],
24
+ exports: [BullModule],
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,30 @@
1
+ import KeyvRedis from "@keyv/redis"
2
+ import { CacheModule } from "@nestjs/cache-manager"
3
+ import { DynamicModule, Module } from "@nestjs/common"
4
+ import { ConfigService } from "@nestjs/config"
5
+ import { RedisConfig } from "../redis/redis.config"
6
+ import { SharedRedisModule } from "../redis/shared-redis.module"
7
+
8
+ @Module({})
9
+ export class SharedCacheModule {
10
+ static forRoot(): DynamicModule {
11
+ return {
12
+ module: SharedCacheModule,
13
+ imports: [
14
+ SharedRedisModule,
15
+ CacheModule.registerAsync({
16
+ isGlobal: true,
17
+ useFactory: (
18
+ configService: ConfigService,
19
+ redisConfig: RedisConfig,
20
+ ) => ({
21
+ ttl: +configService.get("CACHE_TTL") * 1000,
22
+ stores: [new KeyvRedis(redisConfig.url)],
23
+ }),
24
+ inject: [ConfigService, RedisConfig],
25
+ }),
26
+ ],
27
+ exports: [CacheModule],
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ ExceptionFilter,
3
+ Catch,
4
+ ArgumentsHost,
5
+ NotFoundException,
6
+ Logger,
7
+ } from "@nestjs/common"
8
+ import { Response } from "express"
9
+ import { EntityNotFoundError } from "typeorm"
10
+
11
+ @Catch(EntityNotFoundError)
12
+ export class TypeOrmExceptionFilter implements ExceptionFilter {
13
+ private readonly logger = new Logger(TypeOrmExceptionFilter.name)
14
+
15
+ catch(exception: EntityNotFoundError, host: ArgumentsHost) {
16
+ const ctx = host.switchToHttp()
17
+ const response = ctx.getResponse<Response>()
18
+
19
+ this.logger.debug(`Original TypeORM error: ${exception.message}`)
20
+
21
+ const entityName = this.extractEntityName(exception.message)
22
+ const message = entityName ? `${entityName} not found` : "Entity not found"
23
+
24
+ const notFoundException = new NotFoundException(message)
25
+
26
+ response
27
+ .status(notFoundException.getStatus())
28
+ .json(notFoundException.getResponse())
29
+ }
30
+
31
+ private extractEntityName(errorMessage: string): string | null {
32
+ const match = errorMessage.match(/entity of type "([^"]+)"/)
33
+ return match ? match[1] : null
34
+ }
35
+ }
@@ -0,0 +1,136 @@
1
+ import { DynamicModule, Module, OnModuleDestroy } from "@nestjs/common"
2
+ import { ConfigModule, ConfigService } from "@nestjs/config"
3
+ import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm"
4
+ import { join } from "path"
5
+ import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"
6
+ import { DataSource } from "typeorm"
7
+
8
+ type SharedDatabaseModuleOptions = {
9
+ entities?: string[]
10
+ migrations?: string[]
11
+ }
12
+
13
+ @Module({})
14
+ export class SharedDatabaseModule implements OnModuleDestroy {
15
+ private static testContainer: StartedTestContainer
16
+ private static testDataSource: DataSource
17
+
18
+ static forRoot(options: SharedDatabaseModuleOptions = {}): DynamicModule {
19
+ return {
20
+ module: SharedDatabaseModule,
21
+ imports: [
22
+ TypeOrmModule.forRootAsync({
23
+ imports: [ConfigModule],
24
+ inject: [ConfigService],
25
+ useFactory: async (configService: ConfigService) => {
26
+ const isTestEnvironment = process.env.NODE_ENV === "test"
27
+
28
+ if (isTestEnvironment) {
29
+ return await this.createTestConfiguration(options)
30
+ }
31
+
32
+ return this.createProductionConfiguration(configService, options)
33
+ },
34
+ }),
35
+ ],
36
+ exports: [TypeOrmModule],
37
+ }
38
+ }
39
+
40
+ private static async createTestConfiguration(
41
+ options: SharedDatabaseModuleOptions,
42
+ ): Promise<TypeOrmModuleOptions> {
43
+ if (!this.testContainer) {
44
+ this.testContainer = await new GenericContainer("postgres")
45
+ .withExposedPorts(5432)
46
+ .withEnvironment({
47
+ POSTGRES_PASSWORD: "secret",
48
+ POSTGRES_DB: "test",
49
+ })
50
+ .withWaitStrategy(
51
+ Wait.forAll([
52
+ Wait.forListeningPorts(),
53
+ Wait.forLogMessage(
54
+ "database system is ready to accept connections",
55
+ ),
56
+ ]),
57
+ )
58
+ .start()
59
+ }
60
+
61
+ const connectionUri = `postgres://postgres:secret@localhost:${this.testContainer.getMappedPort(
62
+ 5432,
63
+ )}/test`
64
+
65
+ const entities = options.entities || [
66
+ join(__dirname, "../../../**/*.entity{.ts,.js}"),
67
+ ]
68
+
69
+ const migrations = options.migrations || [
70
+ join(__dirname, "../../../migrations/**/*.{ts,js}"),
71
+ ]
72
+
73
+ if (!this.testDataSource) {
74
+ this.testDataSource = new DataSource({
75
+ type: "postgres",
76
+ url: connectionUri,
77
+ entities,
78
+ synchronize: true,
79
+ migrations,
80
+ })
81
+
82
+ await this.testDataSource.initialize()
83
+ }
84
+
85
+ return {
86
+ type: "postgres",
87
+ url: connectionUri,
88
+ entities,
89
+ synchronize: true,
90
+ autoLoadEntities: true,
91
+ }
92
+ }
93
+
94
+ private static createProductionConfiguration(
95
+ configService: ConfigService,
96
+ options: SharedDatabaseModuleOptions,
97
+ ): TypeOrmModuleOptions {
98
+ return {
99
+ type: "postgres",
100
+ url: configService.get("DATABASE_URL"),
101
+ entities: options.entities || [
102
+ join(__dirname, "../../../**/*.entity{.ts,.js}"),
103
+ ],
104
+ migrations: options.migrations || [
105
+ join(__dirname, "../../../migrations/**/*.{ts,js}"),
106
+ ],
107
+ synchronize: false,
108
+ autoLoadEntities: true,
109
+ }
110
+ }
111
+
112
+ static getTestDataSource(): DataSource {
113
+ return this.testDataSource
114
+ }
115
+
116
+ static async closeTestConnection(): Promise<void> {
117
+ if (this.testDataSource) {
118
+ await this.testDataSource.destroy()
119
+ }
120
+ if (this.testContainer) {
121
+ await this.testContainer.stop()
122
+ }
123
+ }
124
+
125
+ static async clearTestDatabase(): Promise<void> {
126
+ if (this.testDataSource) {
127
+ await this.testDataSource.synchronize(true)
128
+ }
129
+ }
130
+
131
+ async onModuleDestroy() {
132
+ if (SharedDatabaseModule.testDataSource) {
133
+ await SharedDatabaseModule.testDataSource.destroy()
134
+ }
135
+ }
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./ai/ai.module"
2
+ export * from "./ai/services/ai.service"
3
+ export * from "./bull/shared-bull.module"
4
+ export * from "./cache/shared-cache.module"
5
+ export * from "./database/shared-database.module"
6
+ export * from "./database/filters/typeorm-exception.filter"
7
+ export * from "./queue/queue.module"
8
+ export * from "./queue/services/queue.service"
9
+ export * from "./queue/models/job-processor-interface"
10
+ export * from "./queue/decorators/queue.decorator"
11
+ export * from "./redis/shared-redis.module"
12
+ export * from "./redis/redis.config"
@@ -0,0 +1,13 @@
1
+ import { Body, Controller, Post } from "@nestjs/common"
2
+ import { QueueService } from "../services/queue.service"
3
+ import { QueueAddDto } from "../models/dto/queue-add.dto"
4
+
5
+ @Controller("queue")
6
+ export class QueueController {
7
+ constructor(private readonly queueService: QueueService) {}
8
+
9
+ @Post("add")
10
+ async add(@Body() body: QueueAddDto) {
11
+ return this.queueService.add(body.name, body.data)
12
+ }
13
+ }
@@ -0,0 +1,3 @@
1
+ import { DiscoveryService } from "@nestjs/core"
2
+
3
+ export const JobProcessor = DiscoveryService.createDecorator()
@@ -0,0 +1,10 @@
1
+ import { IsObject, IsOptional, IsString } from "class-validator"
2
+
3
+ export class QueueAddDto {
4
+ @IsString()
5
+ name: string
6
+
7
+ @IsObject()
8
+ @IsOptional()
9
+ data?: Record<string, any>
10
+ }
@@ -0,0 +1,5 @@
1
+ import { Job } from "bullmq"
2
+
3
+ export type JobProcessorInterface = {
4
+ process: (job: Job) => Promise<void>
5
+ }
@@ -0,0 +1,53 @@
1
+ import { InjectQueue, Processor, WorkerHost } from "@nestjs/bullmq"
2
+ import { DiscoveryService } from "@nestjs/core"
3
+ import { Job, Queue } from "bullmq"
4
+ import { JobProcessor } from "../decorators/queue.decorator"
5
+ import { JobProcessorInterface } from "../models/job-processor-interface"
6
+ import { DEFAULT_QUEUE } from "../queue.constants"
7
+
8
+ @Processor(DEFAULT_QUEUE, { concurrency: 100 })
9
+ export class QueueProcessor extends WorkerHost {
10
+ constructor(
11
+ private readonly discoveryService: DiscoveryService,
12
+ @InjectQueue(DEFAULT_QUEUE) private readonly queue: Queue,
13
+ ) {
14
+ super()
15
+ }
16
+
17
+ async process(job: Job) {
18
+ const jobProcessor = this.jobProcessorFor(job.name)
19
+ if (!jobProcessor) return
20
+
21
+ const shouldProcess = await this.shouldProcessJob(job)
22
+ if (!shouldProcess) return
23
+
24
+ try {
25
+ await jobProcessor.process(job)
26
+ } catch (error) {
27
+ console.error(error)
28
+ }
29
+ }
30
+
31
+ private jobProcessorFor(jobName: string) {
32
+ return this.discoveryService
33
+ .getProviders({ metadataKey: JobProcessor.KEY })
34
+ .find(
35
+ (provider) =>
36
+ this.discoveryService.getMetadataByDecorator(
37
+ JobProcessor,
38
+ provider,
39
+ ) === jobName,
40
+ )?.instance as JobProcessorInterface
41
+ }
42
+
43
+ private async shouldProcessJob(job: Job) {
44
+ if (!job.repeatJobKey) return true
45
+
46
+ const activeJobs = await this.queue.getActive()
47
+ const activeJobsOfSameCronjob = activeJobs.filter(
48
+ ({ name, id }) => name === job.name && id !== job.id,
49
+ )
50
+
51
+ return activeJobsOfSameCronjob.length === 0
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ export const DEFAULT_QUEUE = "default"
@@ -0,0 +1,14 @@
1
+ import { BullModule } from "@nestjs/bullmq"
2
+ import { Module } from "@nestjs/common"
3
+ import { DiscoveryModule } from "@nestjs/core"
4
+ import { QueueProcessor } from "./processors/queue.processor"
5
+ import { DEFAULT_QUEUE } from "./queue.constants"
6
+ import { QueueService } from "./services/queue.service"
7
+ import { QueueController } from "./controllers/queue.controller"
8
+ @Module({
9
+ imports: [BullModule.registerQueue({ name: DEFAULT_QUEUE }), DiscoveryModule],
10
+ providers: [QueueService, QueueProcessor],
11
+ exports: [QueueService],
12
+ controllers: [QueueController],
13
+ })
14
+ export class QueueModule {}
@@ -0,0 +1,17 @@
1
+ import { InjectQueue } from "@nestjs/bullmq"
2
+ import { Injectable } from "@nestjs/common"
3
+ import { Job, JobsOptions, Queue } from "bullmq"
4
+ import { DEFAULT_QUEUE } from "../queue.constants"
5
+
6
+ @Injectable()
7
+ export class QueueService {
8
+ constructor(@InjectQueue(DEFAULT_QUEUE) private readonly queue: Queue) {}
9
+
10
+ add(
11
+ name: string,
12
+ data: any,
13
+ opts?: JobsOptions,
14
+ ): Promise<Job<any, any, string>> {
15
+ return this.queue.add(name, data, opts)
16
+ }
17
+ }
@@ -0,0 +1,3 @@
1
+ export class RedisConfig {
2
+ constructor(public readonly url: string) {}
3
+ }