[config-schema] Add min and max options for Duration (#173494)

## Summary

Fix https://github.com/elastic/kibana/issues/97513

Add a `min` and `max` option for the `Duration` schema type.

```ts
schema.object({
   duration: schema.duration({
     min: '5m',
     max: '1d'
   })
});
```
This commit is contained in:
Pierre Gayvallet 2023-12-19 13:36:54 +01:00 committed by GitHub
parent c01b3a450a
commit 96cba63172
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 5 deletions

View file

@ -153,6 +153,46 @@ export const internals: JoiRoot = Joi.extend(
}
return { value };
},
rules: {
min: {
args: [
{
name: 'limit',
assert: Joi.alternatives([Joi.number(), Joi.string()]).required(),
},
],
method(limit) {
return this.$_addRule({ name: 'min', args: { limit } });
},
validate(value, { error }, args) {
const limit = ensureDuration(args.limit);
if (value.asMilliseconds() < limit.asMilliseconds()) {
return error('duration.min', { value, limit });
}
return value;
},
},
max: {
args: [
{
name: 'limit',
assert: Joi.alternatives([Joi.number(), Joi.string()]).required(),
},
],
method(limit) {
return this.$_addRule({ name: 'max', args: { limit } });
},
validate(value, { error }, args) {
const limit = ensureDuration(args.limit);
if (value.asMilliseconds() > limit.asMilliseconds()) {
return error('duration.max', { value, limit });
}
return value;
},
},
},
},
{
type: 'number',

View file

@ -8,6 +8,7 @@
import { duration as momentDuration } from 'moment';
import { schema } from '../..';
import { ensureDuration } from '../duration';
const { duration, object, contextRef, siblingRef } = schema;
@ -135,6 +136,28 @@ describe('#defaultValue', () => {
});
});
describe('#min', () => {
it('returns the value when larger', () => {
expect(duration({ min: '5m' }).validate('7m')).toEqual(ensureDuration('7m'));
});
it('throws error when value is smaller', () => {
expect(() => duration({ min: '5m' }).validate('3m')).toThrowErrorMatchingInlineSnapshot(
`"Value must be equal to or greater than [PT5M]"`
);
});
});
describe('#max', () => {
it('returns the value when smaller', () => {
expect(duration({ max: '10d' }).validate('7d')).toEqual(ensureDuration('7d'));
});
it('throws error when value is greater', () => {
expect(() => duration({ max: '10h' }).validate('17h')).toThrowErrorMatchingInlineSnapshot(
`"Value must be equal to or less than [PT10H]"`
);
});
});
test('returns error when not valid string or non-safe positive integer', () => {
expect(() => duration().validate(-123)).toThrowErrorMatchingInlineSnapshot(
`"Value in milliseconds is expected to be a safe positive integer."`

View file

@ -13,12 +13,14 @@ import { internals } from '../internals';
import { Reference } from '../references';
import { Type } from './type';
type DurationValueType = Duration | string | number;
export type DurationValueType = Duration | string | number;
export interface DurationOptions {
// we need to special-case defaultValue as we want to handle string inputs too
defaultValue?: DurationValueType | Reference<DurationValueType> | (() => DurationValueType);
validate?: (value: Duration) => string | void;
min?: DurationValueType;
max?: DurationValueType;
}
export class DurationType extends Type<Duration> {
@ -36,16 +38,32 @@ export class DurationType extends Type<Duration> {
defaultValue = options.defaultValue;
}
super(internals.duration(), { ...options, defaultValue });
let schema = internals.duration();
if (options.min) {
schema = schema.min(options.min);
}
if (options.max) {
schema = schema.max(options.max);
}
super(schema, { validate: options.validate, defaultValue });
}
protected handleError(type: string, { message, value }: Record<string, any>, path: string[]) {
protected handleError(
type: string,
{ message, value, limit }: Record<string, any>,
path: string[]
) {
switch (type) {
case 'any.required':
case 'duration.base':
return `expected value of type [moment.Duration] but got [${typeDetect(value)}]`;
case 'duration.parse':
return new SchemaTypeError(message, path);
case 'duration.min':
return `Value must be equal to or greater than [${limit.toString()}]`;
case 'duration.max':
return `Value must be equal to or less than [${limit.toString()}]`;
}
}
}

View file

@ -7,7 +7,8 @@
*/
import * as Joi from 'joi';
import { ByteSizeValue } from '../src/byte_size_value';
import type { ByteSizeValue } from '../src/byte_size_value';
import type { DurationValueType } from '../src/types/duration_type';
declare module 'joi' {
interface BytesSchema extends AnySchema {
@ -16,6 +17,12 @@ declare module 'joi' {
max(limit: number | string | ByteSizeValue): this;
}
interface DurationSchema extends AnySchema {
min(limit: DurationValueType): this;
max(limit: DurationValueType): this;
}
interface MapSchema extends AnySchema {
entries(key: AnySchema, value: AnySchema): this;
}
@ -34,7 +41,7 @@ declare module 'joi' {
export type JoiRoot = Joi.Root & {
bytes: () => BytesSchema;
duration: () => AnySchema;
duration: () => DurationSchema;
map: () => MapSchema;
record: () => RecordSchema;
stream: () => AnySchema;