|
1 | | -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; |
| 1 | +import { BadRequestException, ConflictException, Injectable, Logger } from '@nestjs/common'; |
2 | 2 | import { SchedulerRegistry } from '@nestjs/schedule'; |
3 | 3 | import { CronHooksService } from './cron-hooks.service'; |
4 | 4 | import { pick } from 'radash'; |
5 | 5 | import { ConfigTaskDTO, CronTaskDTO } from './_dto/config-task.dto'; |
6 | | -import { CronUpdateDto } from './_dto/cron.dto'; |
| 6 | +import { CronCreateDto, CronUpdateDto } from './_dto/cron.dto'; |
7 | 7 | import { CronJob } from 'cron'; |
8 | 8 | import path from 'node:path'; |
9 | | -import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync, writeFileSync } from 'node:fs'; |
| 9 | +import { |
| 10 | + closeSync, |
| 11 | + existsSync, |
| 12 | + mkdirSync, |
| 13 | + openSync, |
| 14 | + readFileSync, |
| 15 | + readSync, |
| 16 | + readdirSync, |
| 17 | + statSync, |
| 18 | + unlinkSync, |
| 19 | + writeFileSync, |
| 20 | +} from 'node:fs'; |
10 | 21 | import { ConfigService } from '@nestjs/config'; |
11 | 22 | import { toSafeHandlerName } from '~/_common/functions/handler-logger'; |
12 | 23 | import { formatValidationErrors } from '~/_common/functions/format-validation-errors.function'; |
@@ -118,6 +129,55 @@ export class CronService { |
118 | 129 | return this.update(name, { enabled }); |
119 | 130 | } |
120 | 131 |
|
| 132 | + public async create(payload: CronCreateDto): Promise<(CronTaskDTO & { _job: Partial<CronJob> }) | null> { |
| 133 | + const existing = this.cronHooksService.getCronTasks().find((task) => task.name === payload.name); |
| 134 | + if (existing) { |
| 135 | + throw new ConflictException(`Cron task <${payload.name}> already exists`); |
| 136 | + } |
| 137 | + |
| 138 | + try { |
| 139 | + if (payload.options !== undefined) { |
| 140 | + validateCronTaskOptions(payload.handler, payload.options); |
| 141 | + } |
| 142 | + } catch (error) { |
| 143 | + if (error instanceof BadRequestException) { |
| 144 | + throw error; |
| 145 | + } |
| 146 | + throw new BadRequestException(`Options invalides pour le handler "${payload.handler}".`); |
| 147 | + } |
| 148 | + |
| 149 | + const task = plainToInstance(CronTaskDTO, payload); |
| 150 | + |
| 151 | + try { |
| 152 | + await validateOrReject(task, { whitelist: true }); |
| 153 | + } catch (errors) { |
| 154 | + throw new BadRequestException(formatValidationErrors(errors, payload.name)); |
| 155 | + } |
| 156 | + |
| 157 | + const created = this.createTaskInConfig(task); |
| 158 | + if (!created) { |
| 159 | + throw new BadRequestException(`Impossible de créer la tâche cron <${payload.name}> dans la configuration.`); |
| 160 | + } |
| 161 | + |
| 162 | + await this.cronHooksService.syncCronJobs(); |
| 163 | + return this.read(payload.name); |
| 164 | + } |
| 165 | + |
| 166 | + public async delete(name: string): Promise<boolean> { |
| 167 | + const current = this.cronHooksService.getCronTasks().find((task) => task.name === name); |
| 168 | + if (!current) { |
| 169 | + return false; |
| 170 | + } |
| 171 | + |
| 172 | + const deleted = this.deleteTaskFromConfig(name); |
| 173 | + if (!deleted) { |
| 174 | + return false; |
| 175 | + } |
| 176 | + |
| 177 | + await this.cronHooksService.syncCronJobs(); |
| 178 | + return true; |
| 179 | + } |
| 180 | + |
121 | 181 | public async update( |
122 | 182 | name: string, |
123 | 183 | payload: CronUpdateDto, |
@@ -278,16 +338,72 @@ export class CronService { |
278 | 338 | }; |
279 | 339 | } |
280 | 340 |
|
281 | | - private updateTaskInConfig(name: string, updater: (task: CronTaskDTO) => void): boolean { |
282 | | - const configDir = path.join(process.cwd(), 'configs', 'cron'); |
| 341 | + private getConfigDir(): string { |
| 342 | + return path.join(process.cwd(), 'configs', 'cron'); |
| 343 | + } |
| 344 | + |
| 345 | + private listConfigFiles(): string[] { |
| 346 | + const configDir = this.getConfigDir(); |
283 | 347 | if (!existsSync(configDir)) { |
284 | | - return false; |
| 348 | + return []; |
| 349 | + } |
| 350 | + |
| 351 | + return readdirSync(configDir).filter((file) => file.endsWith('.yml') || file.endsWith('.yaml')); |
| 352 | + } |
| 353 | + |
| 354 | + private createTaskInConfig(task: CronTaskDTO): boolean { |
| 355 | + const configDir = this.getConfigDir(); |
| 356 | + if (!existsSync(configDir)) { |
| 357 | + mkdirSync(configDir, { recursive: true }); |
| 358 | + } |
| 359 | + |
| 360 | + const fileName = `${toSafeHandlerName(task.name)}.yml`; |
| 361 | + const filePath = path.join(configDir, fileName); |
| 362 | + |
| 363 | + if (existsSync(filePath)) { |
| 364 | + const raw = readFileSync(filePath, 'utf-8'); |
| 365 | + const parsed = parse(raw) as ConfigTaskDTO; |
| 366 | + if (parsed?.tasks?.length) { |
| 367 | + throw new ConflictException(`Le fichier de configuration <${fileName}> existe déjà.`); |
| 368 | + } |
| 369 | + } |
| 370 | + |
| 371 | + writeFileSync(filePath, stringify({ tasks: [task] } satisfies ConfigTaskDTO)); |
| 372 | + return true; |
| 373 | + } |
| 374 | + |
| 375 | + private deleteTaskFromConfig(name: string): boolean { |
| 376 | + for (const file of this.listConfigFiles()) { |
| 377 | + const filePath = path.join(this.getConfigDir(), file); |
| 378 | + const raw = readFileSync(filePath, 'utf-8'); |
| 379 | + const parsed = parse(raw) as ConfigTaskDTO; |
| 380 | + const tasks = parsed?.tasks; |
| 381 | + |
| 382 | + if (!tasks || !Array.isArray(tasks)) { |
| 383 | + continue; |
| 384 | + } |
| 385 | + |
| 386 | + const taskIndex = tasks.findIndex((task) => task.name === name); |
| 387 | + if (taskIndex === -1) { |
| 388 | + continue; |
| 389 | + } |
| 390 | + |
| 391 | + tasks.splice(taskIndex, 1); |
| 392 | + if (tasks.length === 0) { |
| 393 | + unlinkSync(filePath); |
| 394 | + } else { |
| 395 | + writeFileSync(filePath, stringify(parsed)); |
| 396 | + } |
| 397 | + |
| 398 | + return true; |
285 | 399 | } |
286 | 400 |
|
287 | | - const files = readdirSync(configDir).filter((file) => file.endsWith('.yml') || file.endsWith('.yaml')); |
| 401 | + return false; |
| 402 | + } |
288 | 403 |
|
289 | | - for (const file of files) { |
290 | | - const filePath = path.join(configDir, file); |
| 404 | + private updateTaskInConfig(name: string, updater: (task: CronTaskDTO) => void): boolean { |
| 405 | + for (const file of this.listConfigFiles()) { |
| 406 | + const filePath = path.join(this.getConfigDir(), file); |
291 | 407 | const raw = readFileSync(filePath, 'utf-8'); |
292 | 408 | const parsed = parse(raw) as ConfigTaskDTO; |
293 | 409 | const tasks = parsed?.tasks; |
|
0 commit comments