From 2fd4996df5681f45da4a9b8721c46ebf8ce6218d Mon Sep 17 00:00:00 2001 From: Diluka W Date: Mon, 7 Dec 2020 16:43:00 +0800 Subject: [PATCH] feat: ArrayUnique support identifer function when identifer function supplied, the comparison will base on the mapped array. close #602 --- README.md | 2 +- src/decorator/array/ArrayUnique.ts | 24 ++++++---- ...alidation-functions-and-decorators.spec.ts | 45 +++++++++++++++++++ 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5277715dd0..0ed11bd5cd 100644 --- a/README.md +++ b/README.md @@ -895,7 +895,7 @@ isBoolean(value); | `@ArrayNotEmpty()` | Checks if given array is not empty. | | `@ArrayMinSize(min: number)` | Checks if array's length is as minimal this number. | | `@ArrayMaxSize(max: number)` | Checks if array's length is as maximal this number. | -| `@ArrayUnique()` | Checks if all array's values are unique. Comparison for objects is reference-based. | +| `@ArrayUnique(identifier?: (o) => any)` | Checks if all array's values are unique. Comparison for objects is reference-based. | | **Object validation decorators** | | `@IsInstance(value: any)` | Checks if the property is an instance of the passed value. | | **Other decorators** | diff --git a/src/decorator/array/ArrayUnique.ts b/src/decorator/array/ArrayUnique.ts index 873e4e5dc3..332b87d42a 100644 --- a/src/decorator/array/ArrayUnique.ts +++ b/src/decorator/array/ArrayUnique.ts @@ -2,14 +2,19 @@ import { ValidationOptions } from '../ValidationOptions'; import { buildMessage, ValidateBy } from '../common/ValidateBy'; export const ARRAY_UNIQUE = 'arrayUnique'; +export type ArrayUniqueIdentifier = (o: T) => any; /** * Checks if all array's values are unique. Comparison for objects is reference-based. * If null or undefined is given then this function returns false. */ -export function arrayUnique(array: unknown): boolean { +export function arrayUnique(array: unknown[], identifier?: ArrayUniqueIdentifier): boolean { if (!(array instanceof Array)) return false; + if (identifier) { + array = array.map(o => (o != null ? identifier(o) : o)); + } + const uniqueItems = array.filter((a, b, c) => c.indexOf(a) === b); return array.length === uniqueItems.length; } @@ -18,18 +23,21 @@ export function arrayUnique(array: unknown): boolean { * Checks if all array's values are unique. Comparison for objects is reference-based. * If null or undefined is given then this function returns false. */ -export function ArrayUnique(validationOptions?: ValidationOptions): PropertyDecorator { +export function ArrayUnique( + identifierOrOptions?: ArrayUniqueIdentifier | ValidationOptions, + validationOptions?: ValidationOptions +): PropertyDecorator { + const identifier = typeof identifierOrOptions === 'function' ? identifierOrOptions : undefined; + const options = typeof identifierOrOptions !== 'function' ? identifierOrOptions : validationOptions; + return ValidateBy( { name: ARRAY_UNIQUE, validator: { - validate: (value, args): boolean => arrayUnique(value), - defaultMessage: buildMessage( - eachPrefix => eachPrefix + "All $property's elements must be unique", - validationOptions - ), + validate: (value, args): boolean => arrayUnique(value, identifier), + defaultMessage: buildMessage(eachPrefix => eachPrefix + "All $property's elements must be unique", options), }, }, - validationOptions + options ); } diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts index 8e77b7fc79..2ab85eb14e 100644 --- a/test/functional/validation-functions-and-decorators.spec.ts +++ b/test/functional/validation-functions-and-decorators.spec.ts @@ -4359,6 +4359,7 @@ describe('ArrayUnique', () => { ['world', 'hello', 'superman'], ['world', 'superman', 'hello'], ['superman', 'world', 'hello'], + ['1', '2', null, undefined], ]; const invalidValues: any[] = [ null, @@ -4396,6 +4397,50 @@ describe('ArrayUnique', () => { }); }); +describe('ArrayUnique with identifier', () => { + const identifier = o => o.name; + const validValues = [ + ['world', 'hello', 'superman'], + ['world', 'superman', 'hello'], + ['superman', 'world', 'hello'], + ['1', '2', null, undefined], + ].map(list => list.map(name => ({ name }))); + const invalidValues: any[] = [ + null, + undefined, + ['world', 'hello', 'hello'], + ['world', 'hello', 'world'], + ['1', '1', '1'], + ].map(list => list?.map(name => (name != null ? { name } : name))); + + class MyClass { + @ArrayUnique(identifier) + someProperty: { name: string }[]; + } + + it('should not fail if validator.validate said that its valid', () => { + return checkValidValues(new MyClass(), validValues); + }); + + it('should fail if validator.validate said that its invalid', () => { + return checkInvalidValues(new MyClass(), invalidValues); + }); + + it('should not fail if method in validator said that its valid', () => { + validValues.forEach(value => expect(arrayUnique(value, identifier)).toBeTruthy()); + }); + + it('should fail if method in validator said that its invalid', () => { + invalidValues.forEach(value => expect(arrayUnique(value, identifier)).toBeFalsy()); + }); + + it('should return error object with proper data', () => { + const validationType = 'arrayUnique'; + const message = "All someProperty's elements must be unique"; + return checkReturnedError(new MyClass(), invalidValues, validationType, message); + }); +}); + describe('isInstance', () => { class MySubClass { // Empty