diff --git a/src/modules/moodle/controllers/moodle-sync.controller.ts b/src/modules/moodle/controllers/moodle-sync.controller.ts index 8889ab4..8f31f68 100644 --- a/src/modules/moodle/controllers/moodle-sync.controller.ts +++ b/src/modules/moodle/controllers/moodle-sync.controller.ts @@ -218,7 +218,23 @@ export class MoodleSyncController { async UpdateSyncSchedule( @Body() dto: UpdateSyncScheduleDto, ): Promise { - await this.syncScheduler.updateSchedule(dto.intervalMinutes); + if (dto.intervalMinutes === undefined && dto.enabled === undefined) { + throw new HttpException( + { + error: 'At least one of intervalMinutes or enabled must be provided', + }, + HttpStatus.BAD_REQUEST, + ); + } + + if (dto.intervalMinutes !== undefined) { + await this.syncScheduler.updateSchedule(dto.intervalMinutes); + } + + if (dto.enabled !== undefined) { + await this.syncScheduler.setEnabled(dto.enabled); + } + return this.syncScheduler.getSchedule(); } } diff --git a/src/modules/moodle/dto/requests/update-sync-schedule.request.dto.ts b/src/modules/moodle/dto/requests/update-sync-schedule.request.dto.ts index 4046019..372142f 100644 --- a/src/modules/moodle/dto/requests/update-sync-schedule.request.dto.ts +++ b/src/modules/moodle/dto/requests/update-sync-schedule.request.dto.ts @@ -1,14 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, Min } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsInt, IsOptional, Min } from 'class-validator'; import { MOODLE_SYNC_MIN_INTERVAL_MINUTES } from '../../schedulers/moodle-sync.constants'; export class UpdateSyncScheduleDto { - @ApiProperty({ + @ApiPropertyOptional({ description: `Sync interval in minutes (minimum ${MOODLE_SYNC_MIN_INTERVAL_MINUTES})`, example: 60, minimum: MOODLE_SYNC_MIN_INTERVAL_MINUTES, }) + @IsOptional() @IsInt() @Min(MOODLE_SYNC_MIN_INTERVAL_MINUTES) - intervalMinutes: number; + intervalMinutes?: number; + + @ApiPropertyOptional({ + description: 'Enable or disable the sync cron job', + }) + @IsOptional() + @IsBoolean() + enabled?: boolean; } diff --git a/src/modules/moodle/dto/responses/sync-schedule.response.dto.ts b/src/modules/moodle/dto/responses/sync-schedule.response.dto.ts index 3383df0..8c758cc 100644 --- a/src/modules/moodle/dto/responses/sync-schedule.response.dto.ts +++ b/src/modules/moodle/dto/responses/sync-schedule.response.dto.ts @@ -9,4 +9,7 @@ export class SyncScheduleResponseDto { @ApiPropertyOptional() nextExecution: string | null; + + @ApiProperty({ description: 'Whether the sync cron job is enabled' }) + enabled: boolean; } diff --git a/src/modules/moodle/schedulers/moodle-sync.constants.ts b/src/modules/moodle/schedulers/moodle-sync.constants.ts index a683927..b3aca42 100644 --- a/src/modules/moodle/schedulers/moodle-sync.constants.ts +++ b/src/modules/moodle/schedulers/moodle-sync.constants.ts @@ -2,6 +2,8 @@ export const MOODLE_SYNC_JOB_NAME = 'moodle-sync-cron'; export const MOODLE_SYNC_CONFIG_KEY = 'MOODLE_SYNC_INTERVAL_MINUTES'; +export const MOODLE_SYNC_ENABLED_CONFIG_KEY = 'MOODLE_SYNC_ENABLED'; + export const MOODLE_SYNC_MIN_INTERVAL_MINUTES = 30; export const MOODLE_SYNC_INTERVAL_DEFAULTS: Record = { diff --git a/src/modules/moodle/schedulers/moodle-sync.scheduler.ts b/src/modules/moodle/schedulers/moodle-sync.scheduler.ts index bdb74a2..8056c1c 100644 --- a/src/modules/moodle/schedulers/moodle-sync.scheduler.ts +++ b/src/modules/moodle/schedulers/moodle-sync.scheduler.ts @@ -10,6 +10,7 @@ import { env } from 'src/configurations/env'; import { MOODLE_SYNC_JOB_NAME, MOODLE_SYNC_CONFIG_KEY, + MOODLE_SYNC_ENABLED_CONFIG_KEY, MOODLE_SYNC_INTERVAL_DEFAULTS, MOODLE_SYNC_MIN_INTERVAL_MINUTES, minutesToCron, @@ -20,6 +21,7 @@ export class MoodleSyncScheduler implements OnModuleInit { private readonly logger = new Logger(MoodleSyncScheduler.name); private currentIntervalMinutes: number; private currentCronExpression: string; + private isEnabled: boolean; constructor( @InjectQueue(QueueName.MOODLE_SYNC) private readonly syncQueue: Queue, @@ -29,18 +31,20 @@ export class MoodleSyncScheduler implements OnModuleInit { async onModuleInit() { const interval = await this.resolveInterval(); + const enabled = await this.resolveEnabled(); this.currentIntervalMinutes = interval; this.currentCronExpression = minutesToCron(interval); + this.isEnabled = enabled; const job = CronJob.from({ cronTime: this.currentCronExpression, onTick: () => this.handleScheduledSync(), - start: true, + start: enabled, }); this.schedulerRegistry.addCronJob(MOODLE_SYNC_JOB_NAME, job); this.logger.log( - `Sync scheduler initialized: every ${interval}min (${this.currentCronExpression})`, + `Sync scheduler initialized: every ${interval}min (${this.currentCronExpression}), enabled=${enabled}`, ); } @@ -56,7 +60,7 @@ export class MoodleSyncScheduler implements OnModuleInit { const job = CronJob.from({ cronTime: cronExpression, onTick: () => this.handleScheduledSync(), - start: true, + start: this.isEnabled, }); this.schedulerRegistry.addCronJob(MOODLE_SYNC_JOB_NAME, job); @@ -89,6 +93,7 @@ export class MoodleSyncScheduler implements OnModuleInit { intervalMinutes: number; cronExpression: string; nextExecution: string | null; + enabled: boolean; } { const job = this.schedulerRegistry.getCronJob(MOODLE_SYNC_JOB_NAME); const nextDate = job.nextDate(); @@ -96,10 +101,41 @@ export class MoodleSyncScheduler implements OnModuleInit { return { intervalMinutes: this.currentIntervalMinutes, cronExpression: this.currentCronExpression, - nextExecution: nextDate?.toISO() ?? null, + nextExecution: this.isEnabled ? (nextDate?.toISO() ?? null) : null, + enabled: this.isEnabled, }; } + async setEnabled(enabled: boolean): Promise { + const job = this.schedulerRegistry.getCronJob(MOODLE_SYNC_JOB_NAME); + + if (enabled && !this.isEnabled) { + job.start(); + this.logger.log('Sync cron job enabled'); + } else if (!enabled && this.isEnabled) { + await job.stop(); + this.logger.log('Sync cron job disabled'); + } + + const fork = this.em.fork(); + const config = await fork.findOne(SystemConfig, { + key: MOODLE_SYNC_ENABLED_CONFIG_KEY, + }); + + if (config) { + config.value = String(enabled); + } else { + const newConfig = new SystemConfig(); + newConfig.key = MOODLE_SYNC_ENABLED_CONFIG_KEY; + newConfig.value = String(enabled); + newConfig.description = 'Whether the Moodle sync cron job is enabled'; + fork.persist(newConfig); + } + await fork.flush(); + + this.isEnabled = enabled; + } + private async handleScheduledSync() { try { await this.syncQueue.add( @@ -164,4 +200,21 @@ export class MoodleSyncScheduler implements OnModuleInit { ); return defaultInterval; } + + private async resolveEnabled(): Promise { + try { + const fork = this.em.fork(); + const config = await fork.findOne(SystemConfig, { + key: MOODLE_SYNC_ENABLED_CONFIG_KEY, + }); + if (config?.value) { + return config.value === 'true'; + } + } catch { + this.logger.warn( + 'Could not read sync enabled state from database, defaulting to enabled', + ); + } + return true; + } } diff --git a/src/seeders/infrastructure/system-config.seeder.ts b/src/seeders/infrastructure/system-config.seeder.ts index 6b0ee60..f0ca12d 100644 --- a/src/seeders/infrastructure/system-config.seeder.ts +++ b/src/seeders/infrastructure/system-config.seeder.ts @@ -20,6 +20,11 @@ export class SystemConfigSeeder extends Seeder { value: '60', description: 'Interval for Moodle synchronization in minutes.', }, + { + key: 'MOODLE_SYNC_ENABLED', + value: 'true', + description: 'Whether the Moodle sync cron job is enabled.', + }, { key: 'SENTIMENT_VLLM_CONFIG', value: JSON.stringify({ url: '', model: '', enabled: false }), diff --git a/src/seeders/tests/database.seeder.spec.ts b/src/seeders/tests/database.seeder.spec.ts index 1b08f6c..eb399d0 100644 --- a/src/seeders/tests/database.seeder.spec.ts +++ b/src/seeders/tests/database.seeder.spec.ts @@ -64,9 +64,9 @@ describe('DatabaseSeeders', () => { await seeder.run(em); // APP_NAME, MAINTENANCE_MODE, MOODLE_SYNC_INTERVAL_MINUTES, - // SENTIMENT_VLLM_CONFIG + // MOODLE_SYNC_ENABLED, SENTIMENT_VLLM_CONFIG // eslint-disable-next-line @typescript-eslint/unbound-method - expect(em.persist).toHaveBeenCalledTimes(4); + expect(em.persist).toHaveBeenCalledTimes(5); }); it('should not seed duplicates for existing configurations', async () => {