feat: IsDuration nestjs field validation
This commit is contained in:
1 parent
61f1f71343
commit
2770c649c6
2 files changed
+107
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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".';
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user