Repository: typestack/class-validator
Pull request #635 (2020-06-12 10:48:57)

feat: add nullable option for IsNotEmptyObject rule.
_Note: PR description was moved to issue for easier discoverability by @NoNameProvided ._

references #678.

NoNameProvided: Hi @ytetsuro! 

This looks good, however the decorator signature should be `IsNotEmptyObject({ nullable: boolean })`. 

Can you rebase this to the current develop or create a new PR for this? (You will need to update the tests as we moved from Mocha to Jest.
ytetsuro: > This looks good, however the decorator signature should be IsNotEmptyObject({ nullable: boolean }).

> Can you rebase this to the current develop or create a new PR for this? (You will need to update the tests as we moved from Mocha to Jest.

OK.
Thanks, good solution response.
ytetsuro: @NoNameProvided 
fixed.

Please re-review.

Patch:
diff --git a/src/decorator/object/IsNotEmptyObject.ts b/src/decorator/object/IsNotEmptyObject.ts
index c436bf8..1fe85b9 100644
--- a/src/decorator/object/IsNotEmptyObject.ts
+++ b/src/decorator/object/IsNotEmptyObject.ts
@@ -8,10 +8,15 @@ export const IS_NOT_EMPTY_OBJECT = 'isNotEmptyObject';
  * Checks if the value is valid Object & not empty.
  * Returns false if the value is not an object or an empty valid object.
  */
-export function isNotEmptyObject(value: unknown): boolean {
+export function isNotEmptyObject(value: unknown, options?: { nullable?: boolean }): boolean {
   if (!isObject(value)) {
     return false;
   }
+
+  if (options?.nullable === true) {
+    return !Object.values(value).every(propertyValue => propertyValue === null || propertyValue === undefined);
+  }
+
   for (const key in value) {
     if (value.hasOwnProperty(key)) {
       return true;
@@ -25,12 +30,16 @@ export function isNotEmptyObject(value: unknown): boolean {
  * Checks if the value is valid Object & not empty.
  * Returns false if the value is not an object or an empty valid object.
  */
-export function IsNotEmptyObject(validationOptions?: ValidationOptions): PropertyDecorator {
+export function IsNotEmptyObject(
+  options?: { nullable?: boolean },
+  validationOptions?: ValidationOptions
+): PropertyDecorator {
   return ValidateBy(
     {
       name: IS_NOT_EMPTY_OBJECT,
+      constraints: [options],
       validator: {
-        validate: (value, args): boolean => isNotEmptyObject(value),
+        validate: (value, args): boolean => isNotEmptyObject(value, args.constraints[0]),
         defaultMessage: buildMessage(
           eachPrefix => eachPrefix + '$property must be a non-empty object',
           validationOptions
diff --git a/src/decorator/string/Matches.ts b/src/decorator/string/Matches.ts
index db08659..fb16484 100644
--- a/src/decorator/string/Matches.ts
+++ b/src/decorator/string/Matches.ts
@@ -37,7 +37,7 @@ export function Matches(
       name: MATCHES,
       constraints: [pattern, modifiers],
       validator: {
-        validate: (value, args): boolean => matches(value, args.constraints[0], args.constraints[0]),
+        validate: (value, args): boolean => matches(value, args.constraints[0], args.constraints[1]),
         defaultMessage: buildMessage(
           (eachPrefix, args) => eachPrefix + '$property must match $constraint1 regular expression',
           validationOptions


Test patch:
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index 48f5111..8e77b7f 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -3051,7 +3051,7 @@ describe('IsObject', () => {
 });
 
 describe('IsNotEmptyObject', () => {
-  const validValues = [{ key: 'value' }, { key: 'value' }];
+  const validValues = [{ key: 'value' }, { key: 'value' }, { key: undefined }, { key: null }];
   const invalidValues = [
     null,
     undefined,
@@ -3064,26 +3064,54 @@ describe('IsNotEmptyObject', () => {
     [],
     [{ key: 'value' }],
   ];
+  const nullableValidValues = [{ key: 'value' }, { key: 'value' }];
+  const nullableInvalidValues = [
+    null,
+    undefined,
+    '{ key: "value" }',
+    "{ 'key': 'value' }",
+    'string',
+    1234,
+    false,
+    {},
+    { key: undefined },
+    { key: null },
+    [],
+    [{ key: 'value' }],
+  ];
 
   class MyClass {
     @IsNotEmptyObject()
     someProperty: object;
   }
 
-  it('should not fail if validator.validate said that its valid', () => {
-    return checkValidValues(new MyClass(), validValues);
+  class NullableMyClass {
+    @IsNotEmptyObject({ nullable: true })
+    someProperty: object;
+  }
+
+  it.each([
+    [new MyClass(), validValues],
+    [new NullableMyClass(), nullableValidValues],
+  ])('should not fail if validator.validate said that its valid', (validationObject, values) => {
+    return checkValidValues(validationObject, values);
   });
 
-  it('should fail if validator.validate said that its invalid', () => {
-    return checkInvalidValues(new MyClass(), invalidValues);
+  it.each([
+    [new MyClass(), invalidValues],
+    [new NullableMyClass(), nullableInvalidValues],
+  ])('should fail if validator.validate said that its invalid', (validationObject, values) => {
+    return checkInvalidValues(validationObject, values);
   });
 
   it('should not fail if method in validator said that its valid', () => {
     validValues.forEach(value => expect(isNotEmptyObject(value)).toBeTruthy());
+    nullableValidValues.forEach(value => expect(isNotEmptyObject(value, { nullable: true })).toBeTruthy());
   });
 
   it('should fail if method in validator said that its invalid', () => {
     invalidValues.forEach(value => expect(isNotEmptyObject(value)).toBeFalsy());
+    nullableInvalidValues.forEach(value => expect(isNotEmptyObject(value, { nullable: true })).toBeFalsy());
   });
 
   it('should return error object with proper data', () => {
@@ -3706,7 +3734,7 @@ describe('MaxLength', () => {
   });
 });
 
-describe('Matches', () => {
+describe('Matches pattern RegExp', () => {
   const constraint = /abc/;
   const validValues = ['abc', 'abcdef', '123abc'];
   const invalidValues = [null, undefined, 'acb', 'Abc'];
@@ -3739,6 +3767,40 @@ describe('Matches', () => {
   });
 });
 
+describe('Matches pattern string with modifier', () => {
+  const constraint = 'abc';
+  const modifier = 'i';
+  const validValues = ['abc', 'abcdef', '123abc', 'AbC'];
+  const invalidValues = [null, undefined, 'acb'];
+
+  class MyClass {
+    @Matches(constraint, modifier)
+    someProperty: 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(matches(value, constraint, modifier)).toBeTruthy());
+  });
+
+  it('should fail if method in validator said that its invalid', () => {
+    invalidValues.forEach(value => expect(matches(value, constraint, modifier)).toBeFalsy());
+  });
+
+  it('should return error object with proper data', () => {
+    const validationType = 'matches';
+    const message = 'someProperty must match ' + constraint + ' regular expression';
+    return checkReturnedError(new MyClass(), invalidValues, validationType, message);
+  });
+});
+
 describe('IsMilitaryTime', () => {
   class MyClass {
     @IsMilitaryTime()