diff --git a/apps/ebitemp-api/src/app/app.module.ts b/apps/ebitemp-api/src/app/app.module.ts index e35b0c2..ba1b4c5 100644 --- a/apps/ebitemp-api/src/app/app.module.ts +++ b/apps/ebitemp-api/src/app/app.module.ts @@ -8,6 +8,7 @@ import { EnumifyModule } from './modules/enumify/enumify.module'; import { AppFileTransactionsModule } from './modules/file-transactions/file-transactions.module'; import { AppHealthModule } from './modules/health/health.module'; import { AppLoggerModule } from './modules/logger/logger.module'; +import { AppScheduleModule } from './modules/schedule/schedule.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { AppLoggerModule } from './modules/logger/logger.module'; AppDatabaseModule, AppHealthModule, AppFileTransactionsModule, + AppScheduleModule, EnumifyModule, ], controllers: [AppController], diff --git a/apps/ebitemp-api/src/app/modules/database/database.module.ts b/apps/ebitemp-api/src/app/modules/database/database.module.ts index dd9b74c..6f811a6 100644 --- a/apps/ebitemp-api/src/app/modules/database/database.module.ts +++ b/apps/ebitemp-api/src/app/modules/database/database.module.ts @@ -1,12 +1,11 @@ import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { DatabaseConfig, databaseConfig } from './database.config'; import { APP_DATASOURCES } from './database.constants'; import { typeormTransactionalDataSourceFactory } from './utils/typeorm-data-source-factory'; -import { typeormEntitiesFromImport } from './utils/typeorm-import-entities'; import { typeormModuleOptionsFactory } from './utils/typeorm-module-options-factory'; -import { ConfigModule } from '@nestjs/config'; const dataSources: DataSource[] = []; @@ -15,10 +14,7 @@ const typeormModules = [ imports: databaseConfig.asProvider().imports, dataSourceFactory: typeormTransactionalDataSourceFactory(), useFactory: async (dbConfig: DatabaseConfig) => { - // const entities = await import('./path/to/entities'); - const entities = {}; - - const config = await typeormModuleOptionsFactory(dbConfig, [typeormEntitiesFromImport(entities)]); + const config = await typeormModuleOptionsFactory(dbConfig); return config; }, inject: [databaseConfig.KEY], diff --git a/apps/ebitemp-api/src/app/modules/database/entities/index.ts b/apps/ebitemp-api/src/app/modules/database/entities/index.ts new file mode 100644 index 0000000..4b4c18a --- /dev/null +++ b/apps/ebitemp-api/src/app/modules/database/entities/index.ts @@ -0,0 +1 @@ +export { TipiJobsEntity } from './tipi_jobs.entity'; diff --git a/apps/ebitemp-api/src/app/modules/database/entities/tipi_jobs.entity.ts b/apps/ebitemp-api/src/app/modules/database/entities/tipi_jobs.entity.ts new file mode 100644 index 0000000..8524d93 --- /dev/null +++ b/apps/ebitemp-api/src/app/modules/database/entities/tipi_jobs.entity.ts @@ -0,0 +1,50 @@ +import { Column, Entity, Index } from 'typeorm'; +import { Enumify } from '../../enumify/enumify'; + +export class InviaMailSeErrori { + @Column('bit', { name: 'flag', default: () => '(0)' }) + flagAttivo: boolean; + + @Column('varchar', { name: 'oggetto', nullable: true, length: 255 }) + oggetto: string | null; + + @Column('varchar', { name: 'destinatari', nullable: true, length: 255 }) + destinatari: string | null; + + @Column('varchar', { name: 'cc', nullable: true, length: 255 }) + cc: string | null; +} + +@Entity('tipi_jobs') +export class TipiJobsEntity extends Enumify { + static _ = TipiJobsEntity.closeEnum(); + + // Columns + + @Column('int', { primary: true, name: 'id' }) + id: number; + + @Column('varchar', { name: 'nome', length: 50 }) + nome: string; + + @Column('varchar', { name: 'descrizione', nullable: true, length: 255 }) + descrizione: string | null; + + @Column(() => InviaMailSeErrori, { prefix: 'invia_mail_se_errori'}) + inviaMailSeErrori: InviaMailSeErrori; + + @Column('bit', { name: 'flag_attivo', default: () => '(0)' }) + flagAttivo: boolean; + + @Column('varchar', { name: 'pattern_cron', length: 20 }) + patternCron: string; + + public constructor( + id: number, + nome: string, + patternCron: string, + ) { + super(id, nome); + this.patternCron = patternCron; + } +} diff --git a/apps/ebitemp-api/src/app/modules/database/utils/typeorm-module-options-factory.ts b/apps/ebitemp-api/src/app/modules/database/utils/typeorm-module-options-factory.ts index c40448f..62606a1 100644 --- a/apps/ebitemp-api/src/app/modules/database/utils/typeorm-module-options-factory.ts +++ b/apps/ebitemp-api/src/app/modules/database/utils/typeorm-module-options-factory.ts @@ -1,9 +1,9 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { DatabaseConfig } from '../database.config'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; export const typeormModuleOptionsFactory = async ( databaseConfig: DatabaseConfig, - entities: any[], name?: string ): Promise => { return { @@ -14,9 +14,10 @@ export const typeormModuleOptionsFactory = async ( username: databaseConfig.username, password: databaseConfig.password, database: databaseConfig.database, - entities: [...entities], + autoLoadEntities: true, synchronize: false, logging: process.env['NODE_ENV'] !== 'production', + namingStrategy: new SnakeNamingStrategy(), options: { ...(!databaseConfig.secure ? { trustServerCertificate: true } : {}), }, diff --git a/apps/ebitemp-api/src/app/modules/schedule/schedule.module.ts b/apps/ebitemp-api/src/app/modules/schedule/schedule.module.ts new file mode 100644 index 0000000..9691e16 --- /dev/null +++ b/apps/ebitemp-api/src/app/modules/schedule/schedule.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TipiJobsEntity } from '../database/entities'; +import { SchedulerService } from './schedule.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([TipiJobsEntity]), ScheduleModule.forRoot()], + providers: [SchedulerService], + exports: [], +}) +export class AppScheduleModule {} diff --git a/apps/ebitemp-api/src/app/modules/schedule/schedule.service.ts b/apps/ebitemp-api/src/app/modules/schedule/schedule.service.ts new file mode 100644 index 0000000..a379490 --- /dev/null +++ b/apps/ebitemp-api/src/app/modules/schedule/schedule.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { SchedulerRegistry } from '@nestjs/schedule'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CronJob, CronTime } from 'cron'; +import { Repository } from 'typeorm'; +import { TipiJobsEntity } from '../database/entities/tipi_jobs.entity'; + +@Injectable() +export class SchedulerService { + private readonly logger = new Logger(SchedulerService.name); + + private static readonly TIME_ZONE = 'Europe/Rome'; + + constructor( + private readonly schedulerRegistry: SchedulerRegistry, + @InjectRepository(TipiJobsEntity) private readonly tipiJobsRepository: Repository + ) {} + + async onApplicationBootstrap() { + try { + const jobs = this.schedulerRegistry.getCronJobs(); + const jobTypes = await this.tipiJobsRepository.find(); + + for (const [jobName, job] of jobs) { + this.rescheduleJobAccordingToJobType(jobTypes, jobName, job); + } + } catch (err) { + this.logger.error(err.message); + this.logger.warn(`Error while retrieving scheduled jobs on database. Skipping jobs.`); + } + } + + private rescheduleJobAccordingToJobType(jobTypes: TipiJobsEntity[], jobName: string, job: CronJob) { + const jobType = jobTypes.find((jobType) => jobType.nome == jobName); + + if (!jobType) { + this.logger.warn(`Job type for job '${jobName}' not found on database. Skipping job.`); + this.schedulerRegistry.deleteCronJob(jobName); + return; + } + + const jobTime = new CronTime(jobType.patternCron, SchedulerService.TIME_ZONE); + job.setTime(jobTime); + job.start(); + + this.logger.log( + `Job type id '${jobType.id}' found for job '${jobName}' [isActive: ${jobType.flagAttivo}, cronPattern: ${ + jobType.patternCron + }]. Upcoming date => ${job.nextDate()}` + ); + } +} diff --git a/package.json b/package.json index 860e3b2..b65fff8 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,14 @@ "@nestjs/core": "^11.0.8", "@nestjs/platform-express": "^11.0.8", "@nestjs/platform-fastify": "^11.0.8", + "@nestjs/schedule": "^5.0.1", "@nestjs/terminus": "^11.0.0", "@nestjs/typeorm": "^11.0.0", "@nx/devkit": "20.4.2", "axios": "^1.7.9", "cacheable": "^1.8.8", "connection-string": "^4.4.0", + "cron": "^3.5.0", "dayjs": "^1.11.13", "flatted": "^3.3.2", "lodash": "^4.17.21", @@ -34,6 +36,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.20", + "typeorm-naming-strategies": "^4.1.0", "typeorm-scoped": "^1.2.0", "typeorm-transactional": "^0.5.0", "zod": "^3.24.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9eeae3..d90b6f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: '@nestjs/platform-fastify': specifier: ^11.0.8 version: 11.0.8(@nestjs/common@11.0.8)(@nestjs/core@11.0.8) + '@nestjs/schedule': + specifier: ^5.0.1 + version: 5.0.1(@nestjs/common@11.0.8)(@nestjs/core@11.0.8) '@nestjs/terminus': specifier: ^11.0.0 version: 11.0.0(@nestjs/common@11.0.8)(@nestjs/core@11.0.8)(@nestjs/typeorm@11.0.0)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20) @@ -47,6 +50,9 @@ dependencies: connection-string: specifier: ^4.4.0 version: 4.4.0 + cron: + specifier: ^3.5.0 + version: 3.5.0 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -77,6 +83,9 @@ dependencies: typeorm: specifier: ^0.3.20 version: 0.3.20(mssql@11.0.1)(ts-node@10.9.2) + typeorm-naming-strategies: + specifier: ^4.1.0 + version: 4.1.0(typeorm@0.3.20) typeorm-scoped: specifier: ^1.2.0 version: 1.2.0(typeorm@0.3.20) @@ -2524,6 +2533,17 @@ packages: tslib: 2.8.1 dev: false + /@nestjs/schedule@5.0.1(@nestjs/common@11.0.8)(@nestjs/core@11.0.8): + resolution: {integrity: sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + dependencies: + '@nestjs/common': 11.0.8(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 11.0.8(@nestjs/common@11.0.8)(@nestjs/platform-express@11.0.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + cron: 3.5.0 + dev: false + /@nestjs/schematics@11.0.0(typescript@5.7.3): resolution: {integrity: sha512-wts8lG0GfNWw3Wk9aaG5I/wcMIAdm7HjjeThQfUZhJxeIFT82Z3F5+0cYdHH4ii2pYQGiCSrR1VcuMwPiHoecg==} peerDependencies: @@ -3794,6 +3814,10 @@ packages: resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==} dev: true + /@types/luxon@3.4.2: + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + dev: false + /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true @@ -5308,6 +5332,13 @@ packages: /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + /cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + dev: false + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -8082,6 +8113,11 @@ packages: yallist: 3.1.1 dev: true + /luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + dev: false + /magic-string@0.30.0: resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} engines: {node: '>=12'} @@ -10875,6 +10911,14 @@ packages: - typeorm-aurora-data-api-driver dev: true + /typeorm-naming-strategies@4.1.0(typeorm@0.3.20): + resolution: {integrity: sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==} + peerDependencies: + typeorm: ^0.2.0 || ^0.3.0 + dependencies: + typeorm: 0.3.20(mssql@11.0.1)(ts-node@10.9.2) + dev: false + /typeorm-scoped@1.2.0(typeorm@0.3.20): resolution: {integrity: sha512-fVZUFIAHCib6Sq/k1wGzwtnFb9uP+g/hLDaoosczJpSNAG2YA2CyPR5J3+UuTz11kvdAfQO0Udk+6rL8xX2/Wg==} peerDependencies: