diff --git a/src/decorator/string/Length.ts b/src/decorator/string/Length.ts index 04427bdfb..db2d2956d 100644 --- a/src/decorator/string/Length.ts +++ b/src/decorator/string/Length.ts @@ -12,6 +12,32 @@ export function length(value: unknown, min: number, max?: number): boolean { return typeof value === 'string' && isLengthValidator(value, { min, max }); } +/** + * Character length used to choose the default Length message. + * With `{ each: true }`, `args.value` is the whole collection, so collection.size would pick the wrong bound. + */ +function characterLengthForMessage(value: unknown, min: number, max?: number): number | undefined { + if (typeof value === 'string') { + return value.length; + } + + let items: unknown[] | undefined; + if (Array.isArray(value)) { + items = value; + } else if (value instanceof Set) { + items = Array.from(value); + } else if (value instanceof Map) { + items = Array.from(value.values()); + } + + if (items) { + const failing = items.find(item => typeof item === 'string' && !length(item, min, max)); + return typeof failing === 'string' ? failing.length : undefined; + } + + return value != null ? (value as { length?: number }).length : undefined; +} + /** * Checks if the string's length falls in a range. Note: this function takes into account surrogate pairs. * If given value is not a string, then it returns false. @@ -24,11 +50,15 @@ export function Length(min: number, max?: number, validationOptions?: Validation validator: { validate: (value, args): boolean => length(value, args?.constraints[0], args?.constraints[1]), defaultMessage: buildMessage((eachPrefix, args) => { - const isMinLength = args?.constraints[0] !== null && args?.constraints[0] !== undefined; - const isMaxLength = args?.constraints[1] !== null && args?.constraints[1] !== undefined; - if (isMinLength && (!args.value || args.value.length < args?.constraints[0])) { + const min = args?.constraints[0]; + const max = args?.constraints[1]; + const isMinLength = min !== null && min !== undefined; + const isMaxLength = max !== null && max !== undefined; + const charLength = characterLengthForMessage(args?.value, min, max); + + if (isMinLength && (charLength === undefined || charLength < min)) { return eachPrefix + '$property must be longer than or equal to $constraint1 characters'; - } else if (isMaxLength && args.value.length > args?.constraints[1]) { + } else if (isMaxLength && charLength !== undefined && charLength > max) { return eachPrefix + '$property must be shorter than or equal to $constraint2 characters'; } return ( diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts index 4c266f02e..fa9e7c34d 100644 --- a/test/functional/validation-functions-and-decorators.spec.ts +++ b/test/functional/validation-functions-and-decorators.spec.ts @@ -4267,6 +4267,33 @@ describe('Length', () => { const message = 'someProperty must be shorter than or equal to ' + constraintToString(constraint2) + ' characters'; return checkReturnedError(new MyClass(), ['aaaa', 'azzazza'], validationType, message); }); + + describe('with each: true', () => { + class TagsClass { + @Length(constraint1, constraint2, { each: true }) + someProperty: string[]; + } + + it('should not fail for valid items', () => { + return checkValidValues(new TagsClass(), [['ab', 'abc'], ['de']]); + }); + + it('should report max-length when an item is too long', () => { + const validationType = 'isLength'; + const message = + 'each value in someProperty must be shorter than or equal to ' + + constraintToString(constraint2) + + ' characters'; + return checkReturnedError(new TagsClass(), [['abcd'], ['aaaa']], validationType, message); + }); + + it('should report min-length when items are too short', () => { + const validationType = 'isLength'; + const message = + 'each value in someProperty must be longer than or equal to ' + constraintToString(constraint1) + ' characters'; + return checkReturnedError(new TagsClass(), [['a', 'a', 'a', 'a']], validationType, message); + }); + }); }); describe('MinLength', () => {