feat: IsDuration nestjs field validation

This commit is contained in:
devlikepro committed 2026-08-31 10:44:22 +07:00
1 parent 61f1f71343
commit 2770c649c6
2 files changed
+107

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import { validate } from 'class-validator';
import { IsDuration, parseDurationMs } from './IsDuration';
describe('parseDurationMs', () => {
it('parses human duration strings into milliseconds', () => {
expect(parseDurationMs('1s')).toBe(1000);
expect(parseDurationMs('30m')).toBe(30 * 60 * 1000);
expect(parseDurationMs('24h')).toBe(24 * 60 * 60 * 1000);
expect(parseDurationMs('7d')).toBe(7 * 24 * 60 * 60 * 1000);
expect(parseDurationMs('7 days')).toBe(7 * 24 * 60 * 60 * 1000);
});
it('parses bare numbers as milliseconds', () => {
expect(parseDurationMs('500')).toBe(500);
});
it('rejects invalid values', () => {
expect(parseDurationMs('junk')).toBeNull();
expect(parseDurationMs('')).toBeNull();
expect(parseDurationMs(' ')).toBeNull();
expect(parseDurationMs(null)).toBeNull();
expect(parseDurationMs(undefined)).toBeNull();
expect(parseDurationMs(123)).toBeNull();
expect(parseDurationMs({})).toBeNull();
});
it('rejects negative durations', () => {
expect(parseDurationMs('-1h')).toBeNull();
expect(parseDurationMs('-5')).toBeNull();
});
});
class TestDto {
@IsDuration()
ttl: string;
}
async function validateTtl(ttl: any) {
const dto = new TestDto();
dto.ttl = ttl;
return await validate(dto);
}
describe('IsDuration', () => {
it('accepts valid duration strings', async () => {
expect(await validateTtl('30m')).toHaveLength(0);
expect(await validateTtl('24h')).toHaveLength(0);
expect(await validateTtl('7d')).toHaveLength(0);
});
it('rejects invalid values with a helpful message', async () => {
const errors = await validateTtl('junk');
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({
isDuration: 'must be a duration string like "30m", "24h" or "7d".',
});
});
it('rejects negative durations and non-strings', async () => {
expect(await validateTtl('-1h')).toHaveLength(1);
expect(await validateTtl(3600)).toHaveLength(1);
expect(await validateTtl(undefined)).toHaveLength(1);
});
});
+41
View File
@@ -0,0 +1,41 @@
import { registerDecorator, ValidationOptions } from 'class-validator';
import * as ms from 'ms';
/**
* Parse a human duration ('30m', '24h', '7d') into milliseconds.
* Returns null when the value is not a valid non-negative duration.
*/
export function parseDurationMs(value: any): number | null {
if (typeof value !== 'string' || !value.trim()) {
return null;
}
let duration: number | undefined;
try {
duration = ms(value as ms.StringValue);
} catch {
return null;
}
if (typeof duration !== 'number' || Number.isNaN(duration) || duration < 0) {
return null;
}
return duration;
}
export function IsDuration(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'isDuration',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
return parseDurationMs(value) !== null;
},
defaultMessage() {
return 'must be a duration string like "30m", "24h" or "7d".';
},
},
});
};
}