From c8123d89ed5c9fb1580c3689542c62fe3039cc7b Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 21 Jan 2018 02:07:39 +0100 Subject: [PATCH 1/4] package: add lodash and @types/lodash --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 7d4cd142be..ba295b664c 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,14 @@ "typescript-validator" ], "dependencies": { + "lodash": "^4.17.4", "validator": "9.2.0" }, "devDependencies": { "@types/chai": "^3.4.35", "@types/chai-as-promised": "0.0.30", "@types/gulp": "^4.0.2", + "@types/lodash": "^4.14.93", "@types/mocha": "^2.2.40", "@types/node": "^8.10.11", "@types/sinon": "^1.16.36", From b0d02ce4d9cbd0874a739509245e47cd233c8822 Mon Sep 17 00:00:00 2001 From: vince Date: Sun, 21 Jan 2018 02:07:53 +0100 Subject: [PATCH 2/4] decorator: new decorator "@InheritValidation" --- .../inherit-validation.spec.ts | 182 ++++++++++++++++++ .../inherit-validation/inherit-validation.ts | 64 ++++++ 2 files changed, 246 insertions(+) create mode 100644 src/decorator/inherit-validation/inherit-validation.spec.ts create mode 100644 src/decorator/inherit-validation/inherit-validation.ts diff --git a/src/decorator/inherit-validation/inherit-validation.spec.ts b/src/decorator/inherit-validation/inherit-validation.spec.ts new file mode 100644 index 0000000000..307ac1bf2c --- /dev/null +++ b/src/decorator/inherit-validation/inherit-validation.spec.ts @@ -0,0 +1,182 @@ +import * as _ from "lodash"; +import {getFromContainer} from "../../container"; +import {validate} from "../../index"; +import {MetadataStorage} from "../../metadata/MetadataStorage"; +import {ValidationMetadata} from "../../metadata/ValidationMetadata"; +import {IsString, MaxLength, IsNumber, Max, IsEmail, IsOptional} from "../decorators"; +import InheritValidation from "./inherit-validation"; + +/** + * Used as a base for validation, in order for partial classes + * to pick validation metadatas, property by property. + */ +class Dto { + @IsNumber() + @Max(999) + readonly id: number; + + @IsString() + @MaxLength(10) + readonly name: number; + + @IsEmail() + @IsOptional() + readonly email: string; +} +const validationsCount = 6; + +describe("@InheritValidation", () => { + let dtoMetaDatas: ValidationMetadata[]; + + beforeEach(() => { + dtoMetaDatas = getMetadatasFrom(Dto); + expect(dtoMetaDatas).toHaveLength(validationsCount); + }); + + it("does not modify metadatas of the source class", () => { + const dtoMetadatasNow = getMetadatasFrom(Dto); + expect(dtoMetaDatas).toEqual(dtoMetadatasNow); + }); + + it("does a deep copy of validation metadatas", () => { + class SubDto { + @InheritValidation(Dto, "name") + readonly name: string; + } + + const dtoMetadatas = getMetadatasFrom(Dto, "name"); + const subMetadatas = getMetadatasFrom(SubDto, "name"); + const areEqual = areMetadatasEqual( + [dtoMetadatas, subMetadatas], + // `propertyName` did not change ("name" => "name"), but `target` did: remove it + ["target"], + ); + // with `target` removed, this is a perfect copy + expect(areEqual).toBe(true); + }); + + it("uses destination property name as a source property name if none is given", () => { + class SubDto { + @InheritValidation(Dto) + readonly name: string; + } + + const dtoMetadatas = getMetadatasFrom(Dto, "name"); + const subMetadatas = getMetadatasFrom(SubDto, "name"); + const areEqual = areMetadatasEqual( + [dtoMetadatas, subMetadatas], + ["target"], + ); + expect(areEqual).toBe(true); + }); + + it("allows inheriting validation metadatas with a different property name", () => { + class SubDto { + @InheritValidation(Dto, "name") + readonly nickname: string; + } + + const dtoMetadatas = getMetadatasFrom(Dto, "name"); + const subMetadatas = getMetadatasFrom(SubDto, "nickname"); + const areEqual = areMetadatasEqual( + [dtoMetadatas, subMetadatas], + // `propertyName` and `target` changed: remove them before checking equality + ["propertyName", "target"], + ); + expect(areEqual).toBe(true); + }); + + it("can be used on multiple properties", () => { + class SubDto { + @InheritValidation(Dto) + readonly id: number; + + @InheritValidation(Dto) + readonly name: string; + } + + const dtoMetadatas = _.concat( + // only get metadatas from fields used by SubDto + getMetadatasFrom(Dto, "id"), + getMetadatasFrom(Dto, "name"), + ); + const subMetadatas = getMetadatasFrom(SubDto); + const areEqual = areMetadatasEqual( + [dtoMetadatas, subMetadatas], + ["target"], + ); + expect(areEqual).toBe(true); + }); + + it("uses the inherited metadatas for objects validation", async () => { + class SubDto { + constructor(name: string) { + this.name = name; + } + + @InheritValidation(Dto) + readonly name: string; + } + + const validSubDto = new SubDto("Mike"); + expect(await validate(validSubDto)).toHaveLength(0); + + const invalidSubDto = new SubDto("way_too_long_name"); + const errors = await validate(invalidSubDto); + expect(errors).toHaveLength(1); + expect(errors[0].constraints).toHaveProperty("maxLength"); + }); +}); + +/** + * Use `class-validator`"s `MetadataStorage` to get the `ValidationMetadata`s + * of a given class, or (more specific) one of its property. + * + * @param fromClass Class to get `ValidationMetadata`s from. + * @param property Source property (if none is given, get metadatas from all properties). + * + * @return {ValidationMetadata[]} Target metadatas. + */ +function getMetadatasFrom( + fromClass: new () => object, + property?: string, +): ValidationMetadata[] { + const metadataStorage = getFromContainer(MetadataStorage); + const metadatas = _.cloneDeep(metadataStorage.getTargetValidationMetadatas( + fromClass, + undefined, + )); + + if (!property) { + return metadatas; + } + + return metadatas.filter((vm) => vm.propertyName === property); +} + +/** + * Determine whether two collections of `ValidationMetadata`s are + * the same, eventually after having removed a few fields (which are + * known to have been changed by design). + * + * @param metaDataCollections Array of 2 `ValidationMetadata[]` to be compared. + * @param withoutFields Fields to be removed from the metadatas before comparing them. + * + * @return {boolean} `true` if both collections are equal. + */ +function areMetadatasEqual( + metaDataCollections: ValidationMetadata[][], + withoutFields: string[], +): boolean { + if (metaDataCollections.length !== 2) { + throw new TypeError("Misuse of metadatasAreEqual"); + } + + _.each(withoutFields, (field) => { + _.each(metaDataCollections, (metadatas) => { + _.each(metadatas, (md) => _.unset(md, field)); + }); + }); + + return _.isEqual(metaDataCollections[0], metaDataCollections[1]); +} diff --git a/src/decorator/inherit-validation/inherit-validation.ts b/src/decorator/inherit-validation/inherit-validation.ts new file mode 100644 index 0000000000..6305d54d6a --- /dev/null +++ b/src/decorator/inherit-validation/inherit-validation.ts @@ -0,0 +1,64 @@ +import * as _ from "lodash"; +import {getFromContainer} from "../../container"; +import {MetadataStorage} from "../../metadata/MetadataStorage"; + +/** + * Allow copying validation metadatas set by `class-validator` from + * a given Class property to an other. Copied `ValidationMetadata`s + * will have their `target` and `propertyName` changed according to + * the decorated class and property. + * + * @param fromClass Class to inherit validation metadatas from. + * @param fromProperty Name of the target property (default to decorated property). + * + * @return {PropertyDecorator} Responsible for copying and registering `ValidationMetada`s. + * + * @example + * class SubDto { + * @InheritValidation(Dto) + * readonly id: number; + * + * @InheritValidation(Dto, 'name') + * readonly firstName: string; + * + * @InheritValidation(Dto, 'name') + * readonly lastName: string; + * } + */ +function InheritValidation( + fromClass: new () => object, + fromProperty?: string, +): PropertyDecorator { + const metadataStorage = getFromContainer(MetadataStorage); + const validationMetadatas = metadataStorage.getTargetValidationMetadatas( + fromClass, + undefined, + ); + + /** + * Change the `target` and `propertyName` of each `ValidationMetaData` + * and add it to `MetadataStorage`. Thus, `class-validator` uses it + * during validation. + * + * @param toClass Class owning the decorated property. + * @param toProperty Name of the decorated property. + */ + return (toClass: object, toProperty: string) => { + const sourceProperty = fromProperty || toProperty; + + const metadatasCopy = _.cloneDeep( + validationMetadatas.filter((vm) => + vm.target === fromClass && + vm.propertyName === sourceProperty, + ), + ); + + metadatasCopy.forEach((vm) => { + vm.target = toClass.constructor; + vm.propertyName = toProperty; + metadataStorage.addValidationMetadata(vm); + }); + }; +} + +export default InheritValidation; \ No newline at end of file From b4d0cbd7a82486af4dcdd80c1648cc1b3beeb977 Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 23 Jan 2018 15:29:21 +0100 Subject: [PATCH 3/4] inherit-validation: (fix) strict TS checking --- .../inherit-validation/inherit-validation.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/decorator/inherit-validation/inherit-validation.ts b/src/decorator/inherit-validation/inherit-validation.ts index 6305d54d6a..6dfc31e1b4 100644 --- a/src/decorator/inherit-validation/inherit-validation.ts +++ b/src/decorator/inherit-validation/inherit-validation.ts @@ -1,6 +1,7 @@ import * as _ from "lodash"; import {getFromContainer} from "../../container"; import {MetadataStorage} from "../../metadata/MetadataStorage"; +import {ValidationMetadata} from "../../metadata/ValidationMetadata"; /** * Allow copying validation metadatas set by `class-validator` from @@ -32,7 +33,7 @@ function InheritValidation( const metadataStorage = getFromContainer(MetadataStorage); const validationMetadatas = metadataStorage.getTargetValidationMetadatas( fromClass, - undefined, + typeof fromClass, ); /** @@ -43,7 +44,11 @@ function InheritValidation( * @param toClass Class owning the decorated property. * @param toProperty Name of the decorated property. */ - return (toClass: object, toProperty: string) => { + return (toClass: object, toProperty: string | symbol) => { + const toPropertyName: string = toProperty instanceof Symbol ? + typeof toProperty : + toProperty; + const sourceProperty = fromProperty || toProperty; const metadatasCopy = _.cloneDeep( @@ -53,12 +58,12 @@ function InheritValidation( ), ); - metadatasCopy.forEach((vm) => { + metadatasCopy.forEach((vm: ValidationMetadata) => { vm.target = toClass.constructor; - vm.propertyName = toProperty; + vm.propertyName = toPropertyName; metadataStorage.addValidationMetadata(vm); }); }; } -export default InheritValidation; \ No newline at end of file +export default InheritValidation; From 7d5d199ccba7408893687dacb0d92169e2acfaae Mon Sep 17 00:00:00 2001 From: vince Date: Tue, 23 Jan 2018 15:31:51 +0100 Subject: [PATCH 4/4] inherit-validation.spec: (fix) add missing typedefs (jest) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ba295b664c..4ff2fa47fd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/chai": "^3.4.35", "@types/chai-as-promised": "0.0.30", "@types/gulp": "^4.0.2", + "@types/jest": "^22.0.1", "@types/lodash": "^4.14.93", "@types/mocha": "^2.2.40", "@types/node": "^8.10.11",