11import { ApiProperty } from '@nestjs/swagger' ;
2- import { Type } from 'class-transformer' ;
3- import { IsArray , IsEnum , IsNegative , IsNotEmpty , IsNumber , IsObject , IsOptional , ValidateNested } from 'class-validator' ;
2+ import { Type , Transform } from 'class-transformer' ;
3+ import { IsArray , IsEnum , IsNegative , IsNotEmpty , IsNumber , IsObject , IsOptional , ValidateNested , registerDecorator , ValidationOptions , ValidationArguments , isString , isNumber } from 'class-validator' ;
44import { IdentityLifecycle } from '~/management/identities/_enums/lifecycle.enum' ;
55
6+ /**
7+ * Transform trigger values to seconds.
8+ * - Numbers are interpreted as days and converted to seconds
9+ * - Strings with 'd' suffix are interpreted as days and converted to seconds
10+ * - Strings with 'm' suffix are interpreted as minutes and converted to seconds
11+ * - Strings with 's' suffix are already in seconds
12+ *
13+ * @param value The trigger value to transform
14+ * @returns The value converted to seconds
15+ */
16+ function transformTriggerToSeconds ( value : number | string ) : number | undefined {
17+ let isValid = false ;
18+
19+ if ( value === undefined || value === null ) {
20+ return undefined ;
21+ }
22+
23+ /**
24+ * Check if the value is a negative number.
25+ * If it's a number, we check if it's less than 0.
26+ * If it's a string, we check if it matches the regex for negative time strings.
27+ */
28+ if ( isNumber ( value ) ) {
29+ isValid = value < 0 ;
30+ } else if ( isString ( value ) ) {
31+ const timeRegex = / ^ - ? \d + [ d m s ] $ / ;
32+ if ( timeRegex . test ( value ) ) {
33+ // Extract the number part and check if it's negative
34+ const numberPart = value . replace ( / [ d m s ] $ / , '' ) ;
35+ const num = parseInt ( numberPart , 10 ) ;
36+ isValid = num < 0 ;
37+ }
38+ }
39+
40+ if ( ! isValid ) {
41+ throw new Error ( 'Trigger must be a negative number (days) or a negative time string with units (e.g., "-90d", "-10m", "-45s")' ) ;
42+ }
43+
44+ /**
45+ * If the value is a number, we assume it's in days and convert it to seconds.
46+ * We multiply by 24 (hours) * 60 (minutes) * 60 (seconds) to get the total seconds.
47+ * This conversion preserves the sign of the number,
48+ * so if the input is negative, the output will also be negative.
49+ */
50+ if ( isNumber ( value ) ) {
51+ return value * 24 * 60 * 60 ; // Convert days to seconds, preserving sign
52+ }
53+
54+ /**
55+ * If the value is a string, we check if it matches the regex for negative time strings.
56+ * If it does, we extract the number and unit, then convert it to seconds.
57+ * - 'd' is converted to seconds by multiplying by 24 * 60 * 60
58+ * - 'm' is converted to seconds by multiplying by 60
59+ * - 's' is already in seconds
60+ * This conversion preserves the sign of the number,
61+ * so if the input is negative, the output will also be negative.
62+ */
63+ if ( isString ( value ) ) {
64+ const match = value . match ( / ^ ( - ? \d + ) ( [ d m s ] ) $ / ) ;
65+ if ( match ) {
66+ const numValue = parseInt ( match [ 1 ] , 10 ) ;
67+ const unit = match [ 2 ] ;
68+
69+ switch ( unit ) {
70+ case 'd' : // days
71+ return numValue * 24 * 60 * 60 ;
72+
73+ case 'm' : // minutes
74+ return numValue * 60 ;
75+
76+ case 's' : // seconds
77+ return numValue ;
78+
79+ default :
80+ throw new Error ( `Unsupported time unit: ${ unit } ` ) ;
81+ }
82+ }
83+ }
84+
85+ // If we can't parse it, try to convert to number
86+ return Number ( value ) || undefined ;
87+ }
88+
89+ /**
90+ * Custom decorator to validate that at least one of the properties 'rules' or 'trigger' is defined and not empty.
91+ * This decorator can be applied to a class to enforce this validation rule.
92+ *
93+ * @param validationOptions
94+ * @returns
95+ */
96+ function ValidateRulesOrTrigger ( validationOptions ?: ValidationOptions ) {
97+ return function ( constructor : Function ) {
98+ registerDecorator ( {
99+ name : 'validateRulesOrTrigger' ,
100+ target : constructor ,
101+ propertyName : undefined ,
102+ options : validationOptions ,
103+ validator : {
104+ validate ( _ : any , args : ValidationArguments ) {
105+ const obj = args . object as ConfigObjectIdentitiesDTO ;
106+
107+ /**
108+ * Check if either 'rules' or 'trigger' is defined and not empty.
109+ * 'rules' should be an object with at least one key-value pair,
110+ * and 'trigger' should be a number that is not null.
111+ */
112+ const hasRules = obj . rules !== undefined && obj . rules !== null && ( typeof obj . rules === 'object' && Object . keys ( obj . rules ) . length > 0 ) ;
113+ const hasTrigger = obj . trigger !== undefined && obj . trigger !== null ;
114+ return hasRules || hasTrigger ;
115+ } ,
116+ defaultMessage ( _ : ValidationArguments ) {
117+ return 'Either rules or trigger must be provided' ;
118+ }
119+ }
120+ } ) ;
121+ } ;
122+ }
123+
124+ @ValidateRulesOrTrigger ( { message : 'Either rules or trigger must be provided' } )
6125export class ConfigObjectIdentitiesDTO {
7126 @IsEnum ( IdentityLifecycle , { each : true } )
8127 @ApiProperty ( {
@@ -19,10 +138,17 @@ export class ConfigObjectIdentitiesDTO {
19138 public rules : object ;
20139
21140 @IsOptional ( )
141+ @Transform ( ( { value } ) => transformTriggerToSeconds ( value ) )
22142 @IsNumber ( )
23- @IsNegative ( )
24- @Type ( ( ) => Number )
25- @ApiProperty ( { type : Number , required : false } )
143+ @ApiProperty ( {
144+ oneOf : [
145+ { type : 'number' , description : 'Negative number representing days' } ,
146+ { type : 'string' , description : 'Negative time string with units (d=days, m=minutes, s=seconds)' }
147+ ] ,
148+ required : false ,
149+ description : 'Trigger time as negative number (days) or negative time string with units (converted to negative seconds internally)' ,
150+ examples : [ - 90 , '-90d' , '-10m' , '-45s' ]
151+ } )
26152 public trigger : number ;
27153
28154 @IsNotEmpty ( )
0 commit comments