diff --git a/README.md b/README.md index 886712dd7..fb8dd986f 100644 --- a/README.md +++ b/README.md @@ -901,6 +901,7 @@ isBoolean(value); | `@IsISRC()` | Checks if the string is a [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). | | `@IsRFC3339()` | Checks if the string is a valid [RFC 3339](https://tools.ietf.org/html/rfc3339) date. | | `@IsStrongPassword(options?: IsStrongPasswordOptions)` | Checks if the string is a strong password. | +| `@IsDuration()` | Checks if the string is a valid duration (based on [ms](https://github.com/vercel/ms)) | | **Array validation decorators** | | | `@ArrayContains(values: any[])` | Checks if array contains all values from the given array of values. | | `@ArrayNotContains(values: any[])` | Checks if array does not contain any of the given values. | diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index d449e9301..c9df9cc66 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -116,6 +116,7 @@ export * from './string/IsTimeZone'; export * from './string/IsBase58'; export * from './string/is-tax-id'; export * from './string/is-iso4217-currency-code'; +export * from './string/IsDuration'; // ------------------------------------------------------------------------- // Type checkers diff --git a/src/decorator/string/IsDuration.ts b/src/decorator/string/IsDuration.ts new file mode 100644 index 000000000..441cd2550 --- /dev/null +++ b/src/decorator/string/IsDuration.ts @@ -0,0 +1,83 @@ +import { buildMessage, ValidateBy } from '../decorators'; +import { ValidationOptions } from '../ValidationOptions'; + +export const IS_DURATION = 'isDuration'; + +const BaseDurationUnits = [ + 'Years', + 'Year', + 'Yrs', + 'Yr', + 'Y', + 'Weeks', + 'Week', + 'W', + 'Days', + 'Day', + 'D', + 'Hours', + 'Hour', + 'Hrs', + 'Hr', + 'H', + 'Minutes', + 'Minute', + 'Mins', + 'Min', + 'M', + 'Seconds', + 'Second', + 'Secs', + 'Sec', + 's', + 'Milliseconds', + 'Millisecond', + 'Msecs', + 'Msec', + 'Ms', +] as const; + +const AllDurationUnits = new Set(BaseDurationUnits.flatMap(unit => [unit, unit.toUpperCase(), unit.toLowerCase()])); + +/** + * Checks if the string is a valid duration. + * It is designed to match the format used by the [ms](https://github.com/vercel/ms) package. + * The duration can be "1 week","2 days","1h", "30m", "15 s", etc. + */ +export function isDuration(value: unknown): boolean { + if (typeof value !== 'string') { + return false; + } + + // using the same number regex used in the `ms` package + const match = value.match(/^(?-?(?:\d+)?\.?\d+)(?:\s?(?[a-zA-Z]+))?$/); + + if (!match || !match.groups) { + return false; + } + + const { unit } = match.groups as { nbr: string; unit?: string }; + + return unit === undefined || AllDurationUnits.has(unit); +} + +/** + * Checks if the string is a valid duration. + * It is designed to match the format used by the [ms](https://github.com/vercel/ms) package. + * The duration can be "1 week","2 days","1h", "30m", "15 s", etc. + */ +export function IsDuration(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: IS_DURATION, + validator: { + validate: (value, args): boolean => isDuration(value), + defaultMessage: buildMessage( + eachPrefix => eachPrefix + '$property must be a valid duration string.', + validationOptions + ), + }, + }, + validationOptions + ); +} diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts index 9f938616c..cb56847c0 100644 --- a/test/functional/validation-functions-and-decorators.spec.ts +++ b/test/functional/validation-functions-and-decorators.spec.ts @@ -193,6 +193,7 @@ import { isTaxId, IsTaxId, IsISO4217CurrencyCode, + IsDuration, } from '../../src/decorator/decorators'; import { Validator } from '../../src/validation/Validator'; import { ValidatorOptions } from '../../src/validation/ValidatorOptions'; @@ -4789,3 +4790,33 @@ describe('IsISO4217', () => { return checkInvalidValues(new MyClass(), invalidValues); }); }); + +describe('IsDuration', () => { + class MyClass { + @IsDuration() + someProperty: string; + } + + it('should not fail for valid duration strings', () => { + const validValues = ['123', '123Yrs', '123 Yrs', '45min', '45 MIN', '100Ms', '10 Days', '7weeks', '200 SEC', '1d']; + return checkValidValues(new MyClass(), validValues); + }); + + it('should fail for invalid values', () => { + const invalidValues = [ + 'abc', + '123bananas', + '123 Yards', + '12.5 Hours', + ' 123 Hrs', + '123Hrs ', + '123 Hrs', + '123-Hrs', + '', + '0x10Ms', + 10, + null, + ]; + return checkInvalidValues(new MyClass(), invalidValues); + }); +});