@@ -13,18 +13,25 @@ import { ConfigObjectIdentitiesDTO, ConfigObjectSchemaDTO } from './_dto/config.
1313import { validateOrReject , ValidationError } from 'class-validator' ;
1414import { omit } from 'radash' ;
1515import { IdentitiesCrudService } from '../identities/identities-crud.service' ;
16+ import { SchedulerRegistry } from '@nestjs/schedule' ;
17+ import { CronJob } from 'cron' ;
1618
1719interface LifecycleSource {
1820 [ source : string ] : Partial < ConfigObjectIdentitiesDTO > [ ] ;
1921}
2022
23+ type ConfigObjectIdentitiesDTOWithSource = Omit < ConfigObjectIdentitiesDTO , 'sources' > & {
24+ source : string ;
25+ } ;
26+
2127@Injectable ( )
2228export class LifecycleService extends AbstractServiceSchema implements OnApplicationBootstrap , OnModuleInit {
2329 protected lifecycleSources : LifecycleSource = { } ;
2430
2531 public constructor (
2632 @InjectModel ( Lifecycle . name ) protected _model : Model < Lifecycle > ,
2733 protected readonly identitiesService : IdentitiesCrudService ,
34+ private schedulerRegistry : SchedulerRegistry ,
2835 ) {
2936 super ( ) ;
3037 }
@@ -95,14 +102,120 @@ export class LifecycleService extends AbstractServiceSchema implements OnApplica
95102 if ( ! this . lifecycleSources [ source ] ) {
96103 this . lifecycleSources [ source ] = [ ] ;
97104 }
98- this . lifecycleSources [ source ] . push ( omit ( idRule , [ 'sources' ] ) ) ; // Exclude sources from the stored rules
105+
106+ const rule = omit ( idRule , [ 'sources' ] ) ;
107+ if ( rule . trigger ) {
108+ this . logger . log ( `Trigger found for source <${ source } >: ${ - rule . trigger } , installing cron job !` ) ;
109+
110+ if ( this . schedulerRegistry . doesExist ( 'cron' , `lifecycle-trigger-${ source } ` ) ) {
111+ this . logger . warn ( `Cron job for source <${ source } > already exists, skipping creation.` ) ;
112+ continue ;
113+ }
114+
115+ const cronExpression = this . convertSecondsToCron ( - rule . trigger ) ;
116+ this . logger . debug ( `Creating cron job with pattern: ${ cronExpression } ` ) ;
117+
118+ const job = new CronJob ( cronExpression , this . runJob . bind ( this , {
119+ source, // Pass the source to the job for context
120+ ...rule ,
121+ } ) ) ;
122+
123+ this . schedulerRegistry . addCronJob ( `lifecycle-trigger-${ source } ` , job ) ;
124+ job . start ( ) ;
125+ }
126+
127+ this . lifecycleSources [ source ] . push ( rule ) ;
99128 }
100129 }
101130 }
102131
103132 this . logger . log ( 'LifecycleService bootstraped' ) ;
104133 }
105134
135+ protected async runJob ( rule : ConfigObjectIdentitiesDTOWithSource ) : Promise < void > {
136+ this . logger . debug ( `Running LifecycleService job: <${ JSON . stringify ( rule ) } >` ) ;
137+
138+ try {
139+ const identities = await this . identitiesService . model . find ( {
140+ ...rule . rules ,
141+ lifecycle : rule . source ,
142+ ignoreLifecycle : { $ne : true } ,
143+ } ) ;
144+
145+ this . logger . log ( `Found ${ identities . length } identities to process for trigger in source <${ rule . source } >` ) ;
146+
147+ for ( const identity of identities ) {
148+ const updated = await this . identitiesService . model . findOneAndUpdate (
149+ { _id : identity . _id } ,
150+ { $set : { lifecycle : rule . target } } ,
151+ { new : true }
152+ ) ;
153+
154+ if ( updated ) {
155+ await this . create ( {
156+ refId : identity . _id ,
157+ lifecycle : rule . target ,
158+ date : new Date ( ) ,
159+ } ) ;
160+
161+ this . logger . log ( `Identity <${ identity . _id } > updated to lifecycle <${ rule . target } > by trigger from source <${ rule . source } >` ) ;
162+ }
163+ }
164+ } catch ( error ) {
165+ this . logger . error ( `Error in lifecycle trigger job for source <${ rule . source } >:` , error . message , error . stack ) ;
166+ }
167+ }
168+
169+ /**
170+ * Convert seconds to a proper cron expression
171+ * This method converts a duration in seconds to the most appropriate cron expression.
172+ * It optimizes for readability and performance by using the largest possible time unit.
173+ *
174+ * @param seconds - The number of seconds for the interval
175+ * @returns A cron expression string in the format "second minute hour day month dayOfWeek"
176+ */
177+ private convertSecondsToCron ( seconds : number ) : string {
178+ // Ensure we have a valid positive number
179+ const intervalSeconds = Math . max ( 1 , Math . floor ( seconds ) ) ;
180+
181+ // If it's less than 60 seconds, use seconds
182+ if ( intervalSeconds < 60 ) {
183+ return `*/${ intervalSeconds } * * * * *` ;
184+ }
185+
186+ // If it's exactly divisible by 60 and less than 3600, use minutes
187+ const minutes = intervalSeconds / 60 ;
188+ if ( intervalSeconds % 60 === 0 && minutes < 60 ) {
189+ return `0 */${ Math . floor ( minutes ) } * * * *` ;
190+ }
191+
192+ // If it's exactly divisible by 3600 and less than 86400, use hours
193+ const hours = intervalSeconds / 3600 ;
194+ if ( intervalSeconds % 3600 === 0 && hours < 24 ) {
195+ return `0 0 */${ Math . floor ( hours ) } * * *` ;
196+ }
197+
198+ // If it's exactly divisible by 86400, use days
199+ const days = intervalSeconds / 86400 ;
200+ if ( intervalSeconds % 86400 === 0 && days <= 30 ) {
201+ return `0 0 0 */${ Math . floor ( days ) } * *` ;
202+ }
203+
204+ // For very large intervals or non-standard intervals, fall back to the most appropriate unit
205+ if ( intervalSeconds >= 3600 ) {
206+ // Use hours for intervals >= 1 hour
207+ const hourInterval = Math . max ( 1 , Math . floor ( intervalSeconds / 3600 ) ) ;
208+ return `0 0 */${ hourInterval } * * *` ;
209+ } else if ( intervalSeconds >= 60 ) {
210+ // Use minutes for intervals >= 1 minute
211+ const minuteInterval = Math . max ( 1 , Math . floor ( intervalSeconds / 60 ) ) ;
212+ return `0 */${ minuteInterval } * * * *` ;
213+ } else {
214+ // Fall back to seconds
215+ return `*/${ intervalSeconds } * * * * *` ;
216+ }
217+ }
218+
106219 /**
107220 * Load lifecycle rules from configuration files
108221 *
0 commit comments