Skip to content

Commit 516e337

Browse files
committed
feat(cron): add cron module for scheduled tasks management
- Introduced CronModule to manage scheduled tasks with cron expressions. - Added DTOs for cron task configuration and creation. - Implemented load function to read and validate cron tasks from YAML files. - Created CronHooksService to handle the scheduling and execution of cron jobs. - Added CronController for manual triggering of cron tasks. - Integrated lifecycle management with cron tasks execution. - Enhanced error handling and logging for cron operations. - Updated lifecycle command to include execution of lifecycle rules for sources.
1 parent 1d331d5 commit 516e337

26 files changed

+1127
-38
lines changed

.cursor/release.mdc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Fichier MDC pour génération de message de release
2+
3+
# Ce fichier permet de générer un message de release en français basé sur le changelog GitHub complet entre la version précédente et la version cible.
4+
# Utilisation typique : automatisation de la génération de notes de version lors d'une release.
5+
6+
[release]
7+
lang = "fr"
8+
source = "github-fullchangelog"
9+
template = "release-fr"
10+
11+
# Variables disponibles :
12+
# - previous_version : la version précédente
13+
# - target_version : la version cible
14+
# - changelog : le changelog complet entre les deux versions
15+
16+
message = """
17+
## Notes de version pour la version {target_version}
18+
19+
Voici les changements apportés depuis la version {previous_version} :
20+
21+
{changelog}
22+
23+
Merci à tous les contributeurs !
24+
"""

.cursor/release_notes.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Fichier MDC pour génération de message de release
2+
3+
# Ce fichier permet de générer un message de release en français basé sur le changelog GitHub complet entre la version précédente et la version cible.
4+
# Utilisation typique : automatisation de la génération de notes de version lors d'une release.
5+
6+
[release]
7+
lang = "fr"
8+
source = "github-fullchangelog"
9+
template = "release-fr"
10+
11+
# Variables disponibles :
12+
# - previous_version : la version précédente
13+
# - target_version : la version cible
14+
# - changelog : le changelog complet entre les deux versions
15+
16+
message = """
17+
## Notes de version pour la version 2.0.3
18+
19+
Voici les changements apportés depuis la version 2.0.2 :
20+
21+
- v2.0.3 (0d3c5db)
22+
- feat: add deletedFlag update in executeJob for identity state management (3039903)
23+
- refactor: comment out unused settings dialog and related function (638e4cb)
24+
- refactor: restructure settings components and pages (05268c2)
25+
26+
Merci à tous les contributeurs !
27+
"""
28+
29+
Comparaison complète : https://github.com/Libertech-FR/sesame-orchestrator/compare/2.0.2...2.0.3

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ prod: ## Start production environment
7575
$(IMG_NAME) yarn start:prod
7676

7777
dev: ## Start development environment
78+
@mkdir -p $(CURDIR)/apps/api/logs/handlers
7879
@docker run --rm -it \
7980
-e NODE_ENV=development \
8081
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \

apps/api/configs/cron/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
tasks:
2+
- name: "lifecycle-execute-check-identities"
3+
description: "Execute lifecycle trigger for check identities"
4+
enabled: false
5+
schedule: "0 2 15 6 *" # Le 15 juin de chaque année à 02:00
6+
handler: "lifecycle-execute"
7+
options:

apps/api/src/management/lifecycle/_functions/format-validation-errors.function.ts renamed to apps/api/src/_common/functions/format-validation-errors.function.ts

File renamed without changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { ConfigService } from '@nestjs/config'
4+
import { Logger } from '@nestjs/common'
5+
6+
function ensureDir(dir: string) {
7+
try {
8+
fs.mkdirSync(dir, { recursive: true, mode: 0o777 })
9+
} catch (err) {
10+
Logger.warn(`Could not create log directory ${dir}: ${(err as Error).message}`, 'ensureDir')
11+
throw err
12+
}
13+
}
14+
15+
function stripAnsiCodes(str: string): string {
16+
// eslint-disable-next-line no-control-regex
17+
return str.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '')
18+
}
19+
20+
export function createHandlerLogger(config: ConfigService, handler: string) {
21+
const safeHandler = handler.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase()
22+
const logDir = config.get('cron.logDirectory') || path.join(process.cwd(), 'logs', 'handlers')
23+
const logFile = path.join(logDir, `${safeHandler}.log`)
24+
25+
let stream: fs.WriteStream | null = null
26+
let streamError: Error | null = null
27+
28+
try {
29+
ensureDir(logDir)
30+
stream = fs.createWriteStream(logFile, { flags: 'a' })
31+
32+
stream.on('error', (err) => {
33+
streamError = err
34+
Logger.error(`Error writing to handler log file ${logFile}: ${err.message}`, createHandlerLogger.name)
35+
})
36+
37+
Logger.debug(`Handler logger created for <${handler}> at ${logFile}`, createHandlerLogger.name)
38+
} catch (err) {
39+
streamError = err as Error
40+
Logger.error(`Failed to create handler logger for <${handler}>: ${(err as Error).message}`, createHandlerLogger.name)
41+
}
42+
43+
const log = (msg: string) => {
44+
if (stream && !streamError) {
45+
stream.write(`${stripAnsiCodes(msg)}`)
46+
}
47+
}
48+
49+
const error = (msg: string) => {
50+
if (stream && !streamError) {
51+
stream.write(`⚠ ${stripAnsiCodes(msg)}`)
52+
}
53+
}
54+
55+
const jump = (quantity: number) => {
56+
if (stream && !streamError) {
57+
stream.write(`${stripAnsiCodes('\n'.repeat(quantity))}`)
58+
}
59+
}
60+
61+
return {
62+
log: (msg: string) => log(msg),
63+
error: (msg: string) => error(msg),
64+
close: () => () => {
65+
jump(2)
66+
stream?.close()
67+
},
68+
file: logFile,
69+
}
70+
}

apps/api/src/config.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { JwtModuleOptions } from '@nestjs/jwt'
88
import { StorageManagerConfig } from '~/_common/factorydrive'
99
import { AmazonWebServicesS3StorageConfig } from '~/_common/factorydrive'
1010
import { HttpModuleOptions } from '@nestjs/axios'
11-
import { join } from 'path'
11+
import path, { join } from 'path'
12+
import { CronExpression } from '@nestjs/schedule'
1213

1314
/**
1415
* Répertoire de base de l'application API
@@ -147,6 +148,15 @@ export const validationSchema = Joi.object({
147148
SESAME_SMPP_REGIONCODE: Joi
148149
.string()
149150
.default('FR'),
151+
152+
SESAME_CRON_HANDLER_EXPRESSION: Joi
153+
.string()
154+
.pattern(/^(\*|([0-5]?\d))(\/\d+)? (\*|([01]?\d|2[0-3]))(\/\d+)? (\*|([01]?\d|2[0-9]|3[01]))(\/\d+)? (\*|(1[0-2]|0?[1-9]))(\/\d+)? (\*|([0-6]))(\/\d+)?$/)
155+
.default(CronExpression.EVERY_HOUR),
156+
157+
SESAME_CRON_LOG_DIRECTORY: Joi
158+
.string()
159+
.default(path.join(process.cwd(), 'logs', 'handlers')),
150160
});
151161

152162
/**
@@ -189,14 +199,18 @@ export interface ConfigInstance {
189199
uri: string;
190200
options: MongooseModuleOptions
191201
plugins: MongoosePlugin[]
192-
};
202+
}
193203
ioredis: {
194204
uri: string
195205
options: RedisOptions
196-
};
206+
}
197207
axios: {
198208
options: HttpModuleOptions
199-
};
209+
}
210+
cron: {
211+
handlerExpression: string
212+
logDirectory: string
213+
}
200214
factorydrive: {
201215
options:
202216
| StorageManagerConfig
@@ -307,6 +321,10 @@ export default (): ConfigInstance => ({
307321
maxRedirects: parseInt(process.env['SESAME_AXIOS_MAX_REDIRECTS'], 10) || 5,
308322
},
309323
},
324+
cron: {
325+
handlerExpression: process.env['SESAME_CRON_HANDLER_EXPRESSION'] || CronExpression.EVERY_HOUR,
326+
logDirectory: process.env['SESAME_CRON_LOG_DIRECTORY'] || path.join(process.cwd(), 'logs', 'handlers'),
327+
},
310328
factorydrive: {
311329
options: {
312330
default: 'local',

apps/api/src/core/agents/agents.command.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ModuleRef } from '@nestjs/core'
33
import { Command, CommandRunner, InquirerService, Question, QuestionSet, SubCommand } from 'nest-commander'
44
import { AgentsCreateDto } from '~/core/agents/_dto/agents.dto'
55
import { AgentsService } from '~/core/agents/agents.service'
6+
import { Agents } from './_schemas/agents.schema'
67

78
/**
89
* Ensemble de questions interactives pour la création d'un agent.
@@ -116,6 +117,33 @@ export class AgentsCreateCommand extends CommandRunner {
116117
}
117118
}
118119

120+
@SubCommand({ name: 'list' })
121+
export class AgentsListCommand extends CommandRunner {
122+
private readonly logger = new Logger(AgentsListCommand.name)
123+
124+
public constructor(
125+
protected moduleRef: ModuleRef,
126+
private readonly agentsService: AgentsService,
127+
) {
128+
super()
129+
}
130+
131+
async run(_inputs: string[], _options: any): Promise<void> {
132+
this.logger.log('Listing agents...')
133+
try {
134+
const agents = await this.agentsService.find<Agents>({})
135+
console.table(agents.map((agent: any) => ({
136+
username: agent.username,
137+
email: agent.email,
138+
displayName: agent.displayName,
139+
currentState: agent.state.current,
140+
})), ['username', 'email', 'displayName', 'currentState'])
141+
} catch (error) {
142+
console.error('Error listing agents', error)
143+
}
144+
}
145+
}
146+
119147
/**
120148
* Commande principale pour la gestion des agents.
121149
*
@@ -127,9 +155,10 @@ export class AgentsCreateCommand extends CommandRunner {
127155
* @example
128156
* ```bash
129157
* yarn run console agents create
158+
* yarn run console agents list
130159
* ```
131160
*/
132-
@Command({ name: 'agents', arguments: '<task>', subCommands: [AgentsCreateCommand] })
161+
@Command({ name: 'agents', arguments: '<task>', subCommands: [AgentsCreateCommand, AgentsListCommand] })
133162
export class AgentsCommand extends CommandRunner {
134163
/**
135164
* Constructeur de la commande agents

apps/api/src/core/agents/agents.controller.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Body, Controller, Delete, Get, HttpStatus, Param, Patch, Post, Res } from '@nestjs/common'
1+
import { Body, Controller, Delete, Get, HttpStatus, Param, Patch, Post, Query, Res } from '@nestjs/common'
22
import { ApiParam, ApiTags } from '@nestjs/swagger'
33
import {
44
FilterOptions,
@@ -52,6 +52,12 @@ export class AgentsController extends AbstractController {
5252
hidden: 1,
5353
}
5454

55+
protected static readonly searchFields: PartialProjectionType<any> = {
56+
username: 1,
57+
displayName: 1,
58+
email: 1,
59+
};
60+
5561
/**
5662
* Constructeur du contrôleur agents
5763
*
@@ -99,12 +105,26 @@ export class AgentsController extends AbstractController {
99105
@ApiPaginatedDecorator(PickProjectionHelper(AgentsDto, AgentsController.projection))
100106
public async search(
101107
@Res() res: Response,
108+
@Query('search') search: string,
102109
@SearchFilterSchema() searchFilterSchema: FilterSchema,
103110
@SearchFilterOptions() searchFilterOptions: FilterOptions,
104111
): Promise<Response> {
105-
//TODO: search tree by parentId
112+
113+
const searchFilter = {}
114+
115+
if (search && search.trim().length > 0) {
116+
const searchRequest = {}
117+
searchRequest['$or'] = Object.keys(AgentsController.searchFields).map((key) => {
118+
return { [key]: { $regex: `^${search}`, $options: 'i' } }
119+
}).filter(item => item !== undefined)
120+
searchFilter['$and'] = [searchRequest]
121+
searchFilter['$and'].push(searchFilterSchema)
122+
} else {
123+
Object.assign(searchFilter, searchFilterSchema)
124+
}
125+
106126
const [data, total] = await this._service.findAndCount(
107-
searchFilterSchema,
127+
searchFilter,
108128
AgentsController.projection,
109129
searchFilterOptions,
110130
)

0 commit comments

Comments
 (0)