From aa3dcf830f116b686d39d7bfaa4f26919b3429cb Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 18 Sep 2025 16:44:31 +0000 Subject: [PATCH 01/10] Version bump --- box.json | 2 +- changelog.md | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/box.json b/box.json index f0a62e1..0a698ac 100644 --- a/box.json +++ b/box.json @@ -1,7 +1,7 @@ { "name":"ColdBox Validation", "author":"Ortus Solutions ", - "version":"4.6.0", + "version":"4.7.0", "location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cbvalidation/@build.version@/cbvalidation-@build.version@.zip", "slug":"cbvalidation", "type":"modules", diff --git a/changelog.md b/changelog.md index b5afa1a..0b8a406 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.6.0] - 2025-09-18 + ### Fixed - Fix for cases where a non-empty value wasn't passing an `empty: false` validation check @@ -306,14 +308,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create first module version -[Unreleased]: https://github.com/coldbox-modules/cbvalidation/compare/v4.5.0...HEAD - +[unreleased]: https://github.com/coldbox-modules/cbvalidation/compare/v4.6.0...HEAD +[4.6.0]: https://github.com/coldbox-modules/cbvalidation/compare/v4.5.0...v4.6.0 [4.5.0]: https://github.com/coldbox-modules/cbvalidation/compare/v4.4.0...v4.5.0 - [4.4.0]: https://github.com/coldbox-modules/cbvalidation/compare/v4.3.1...v4.4.0 - [4.3.1]: https://github.com/coldbox-modules/cbvalidation/compare/v4.3.0...v4.3.1 - [4.3.0]: https://github.com/coldbox-modules/cbvalidation/compare/v4.2.0...v4.3.0 - [4.2.0]: https://github.com/coldbox-modules/cbvalidation/compare/v4.2.0...v4.2.0 From a46751764146b25e11e48a95a7125df208cc8441 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 19 Sep 2025 13:30:58 -0600 Subject: [PATCH 02/10] BoxLang Compat for null checks --- models/ValidationManager.cfc | 4 +++- models/result/ValidationResult.cfc | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index 1c6c298..e7dda24 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -387,7 +387,9 @@ component accessors="true" serialize="false" singleton { */ struct function getSharedConstraints( string name ){ return ( - structKeyExists( arguments, "name" ) ? variables.sharedConstraints[ arguments.name ] : variables.sharedConstraints + structKeyExists( arguments, "name" ) && !isNull( arguments.name ) ? variables.sharedConstraints[ + arguments.name + ] : variables.sharedConstraints ); } diff --git a/models/result/ValidationResult.cfc b/models/result/ValidationResult.cfc index e20bab2..1d626d3 100644 --- a/models/result/ValidationResult.cfc +++ b/models/result/ValidationResult.cfc @@ -247,7 +247,7 @@ component accessors="true" { array function getAllErrors( string field ){ var errorTarget = variables.errors; - if ( structKeyExists( arguments, "field" ) ) { + if ( structKeyExists( arguments, "field" ) && !isNull( arguments.field ) ) { errorTarget = getFieldErrors( arguments.field ); } @@ -266,7 +266,7 @@ component accessors="true" { var errorTarget = variables.errors; // filter by field? - if ( structKeyExists( arguments, "field" ) ) { + if ( structKeyExists( arguments, "field" ) && !isNull( arguments.field ) ) { errorTarget = getFieldErrors( arguments.field ); } From fa59d7d8f40d3058cac674ff1900e54a7361d701 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 8 Oct 2025 13:58:39 -0600 Subject: [PATCH 03/10] fix(ValidateOrFail): Handle filtering nested structs and arrays --- models/ValidationManager.cfc | 47 ++++++- test-harness/handlers/Main.cfc | 118 ++++++++++-------- .../tests/specs/ValidationIntegrations.cfc | 77 ++++++++++++ 3 files changed, 190 insertions(+), 52 deletions(-) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index e7dda24..4787a1a 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -302,9 +302,50 @@ component accessors="true" serialize="false" singleton { } // Return validated keys - return arguments.target.filter( function( key ){ - return constraints.keyExists( key ); - } ); + return filterTargetForConstraints( arguments.target, constraints ); + } + + private any function filterTargetForConstraints( required any target, required struct constraints ){ + var filteredTarget = {}; + for ( var key in arguments.target ) { + if ( !arguments.constraints.keyExists( key ) ) { + continue; + } + + var constraint = arguments.constraints[ key ]; + if ( constraint.keyExists( "items" ) || constraint.keyExists( "arrayItem" ) ) { + var filteredArray = []; + var arrayConstraints = ( constraint.keyExists( "items" ) ? constraint.items : constraint.arrayItem ); + if ( arrayConstraints.keyExists( "constraints" ) || arrayConstraints.keyExists( "nestedConstraints" ) ) { + for ( var item in arguments.target[ key ] ) { + if ( isStruct( item ) ) { + arrayAppend( + filteredArray, + filterTargetForConstraints( + target = item, + constraints = arrayConstraints.keyExists( "constraints" ) ? arrayConstraints.constraints : arrayConstraints.nestedConstraints + ) + ); + } + } + } else { + filteredArray = arguments.target[ key ]; + } + filteredTarget[ key ] = filteredArray; + } else if ( + constraints[ key ].keyExists( "constraints" ) || constraints[ key ].keyExists( "nestedConstraints" ) + ) { + filteredTarget[ key ] = filterTargetForConstraints( + target = arguments.target[ key ], + constraints = ( + constraint.keyExists( "constraints" ) ? constraint.constraints : constraint.nestedConstraints + ) + ); + } else { + filteredTarget[ key ] = arguments.target[ key ]; + } + } + return filteredTarget; } /** diff --git a/test-harness/handlers/Main.cfc b/test-harness/handlers/Main.cfc index fdd2629..4b5911d 100644 --- a/test-harness/handlers/Main.cfc +++ b/test-harness/handlers/Main.cfc @@ -5,15 +5,14 @@ component { // Index any function index( event, rc, prc ){ - // Test Mixins log.info( "validateHasValue #validateHasValue( "true" )# has passed!" ); log.info( "validateIsNullOrEmpty #validateIsNullOrEmpty( "true" )# has passed!" ); assert( true ); - try{ + try { assert( false, "bogus line" ); - } catch( AssertException e ){} - catch( any e ){ + } catch ( AssertException e ) { + } catch ( any e ) { rethrow; } @@ -26,39 +25,29 @@ component { password : { required : true, size : "6..20" } }; // validation - validate( - target = rc, - constraints = constraints - ).onError( function( results ){ - flash.put( - "notice", - arguments.results.getAllErrors().tostring() - ); - return index( event, rc, prc ); - }) - .onSuccess( function( results ){ - flash.put( "notice", "User info validated!" ); - relocate( "main" ); - } ) + validate( target = rc, constraints = constraints ) + .onError( function( results ){ + flash.put( "notice", arguments.results.getAllErrors().tostring() ); + return index( event, rc, prc ); + } ) + .onSuccess( function( results ){ + flash.put( "notice", "User info validated!" ); + relocate( "main" ); + } ) ; } any function saveShared( event, rc, prc ){ // validation - validate( - target = rc, - constraints = "sharedUser" - ).onError( function( results ){ - flash.put( - "notice", - results.getAllErrors().tostring() - ); - return index( event, rc, prc ); - }) - .onSuccess( function( results ){ - flash.put( "User info validated!" ); - setNextEvent( "main" ); - } ); + validate( target = rc, constraints = "sharedUser" ) + .onError( function( results ){ + flash.put( "notice", results.getAllErrors().tostring() ); + return index( event, rc, prc ); + } ) + .onSuccess( function( results ){ + flash.put( "User info validated!" ); + setNextEvent( "main" ); + } ); } /** @@ -71,10 +60,46 @@ component { }; // validate - prc.keys = validateOrFail( - target = rc, - constraints = constraints - ); + prc.keys = validateOrFail( target = rc, constraints = constraints ); + + return prc.keys; + } + + /** + * validateOrFailWithNestedKeys + */ + function validateOrFailWithNestedKeys( event, rc, prc ){ + var constraints = { + "keep0" : { "required" : true, "type" : "string" }, + "keepNested0" : { + "required" : true, + "type" : "struct", + "constraints" : { + "keepNested1" : { + "required" : true, + "type" : "struct", + "constraints" : { "keep2" : { "required" : true, "type" : "string" } } + }, + "keepArray1" : { + "required" : true, + "type" : "array", + "items" : { + "type" : "struct", + "constraints" : { "keepNested3" : { "required" : true, "type" : "string" } } + } + }, + "keepArray1B" : { + "required" : true, + "type" : "array", + "items" : { "type" : "array", "arrayItem" : { "type" : "string" } } + } + } + }, + "keepNested0B.keep1B" : { "required" : true, "type" : "string" } + }; + + // validate + prc.keys = validateOrFail( target = rc, constraints = constraints ); return prc.keys; } @@ -98,27 +123,22 @@ component { var oModel = populateModel( "User" ); // validate - prc.object = validateOrFail( - target = oModel, - profiles = rc._profiles - ); + prc.object = validateOrFail( target = oModel, profiles = rc._profiles ); return "Validated"; - } - - - /** + } + + + /** * validateOnly */ - function validateOnly( event, rc, prc){ - - var oModel = populateModel( "User" ); + function validateOnly( event, rc, prc ){ + var oModel = populateModel( "User" ); // validate - prc.result = validate( oModel ); + prc.result = validate( oModel ); return "Validated"; - } diff --git a/test-harness/tests/specs/ValidationIntegrations.cfc b/test-harness/tests/specs/ValidationIntegrations.cfc index bb4274f..ebf5236 100644 --- a/test-harness/tests/specs/ValidationIntegrations.cfc +++ b/test-harness/tests/specs/ValidationIntegrations.cfc @@ -59,6 +59,83 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { .notToHaveKey( "anotherBogus" ); } ); } ); + + given( "valid nested data", function(){ + then( "it should give you back only the validated keys including in nested structs", function(){ + var e = this.request( + route = "/main/validateOrFailWithNestedKeys", + params = { + "keepNested0" : { + "keepNested1" : { "keep2" : "foo", "remove2" : "foo" }, + "keepArray1" : [ + { "keepNested3" : "foo", "removeNested3" : "foo" }, + { "keepNested3" : "bar", "removeNested3" : "bar" } + ], + "keepArray1B" : [ [ "foo", "bar" ], [ "baz", "qux" ] ], + "removeNested1" : { "foo" : "bar" }, + "remove1" : "foo" + }, + "keepNested0B" : { "keep1B" : "foo", "remove1B" : "foo" }, + "keep0" : "foo", + "remove0" : "foo" + }, + method = "post" + ); + + var keys = e.getPrivateValue( "keys" ); + debug( keys ); + expect( keys ).toBeStruct(); + expect( keys ).toHaveKey( "keepNested0" ); + expect( keys ).toHaveKey( "keepNested0B" ); + expect( keys ).toHaveKey( "keep0" ); + expect( keys ).notToHaveKey( "remove0" ); + + var nested0 = keys.keepNested0; + expect( nested0 ).toBeStruct(); + expect( nested0 ).toHaveKey( "keepNested1" ); + expect( nested0 ).toHaveKey( "keepArray1" ); + expect( nested0 ).toHaveKey( "keepArray1B" ); + expect( nested0 ).notToHaveKey( "remove1" ); + expect( nested0 ).notToHaveKey( "removeNested1" ); + + var nested1 = nested0.keepNested1; + expect( nested1 ).toBeStruct(); + expect( nested1 ).toHaveKey( "keep2" ); + expect( nested1 ).notToHaveKey( "remove2" ); + + var array1 = nested0.keepArray1; + expect( array1 ).toBeArray(); + expect( array1 ).toHaveLength( 2 ); + expect( array1[ 1 ] ).toBeStruct(); + expect( array1[ 1 ] ).toHaveKey( "keepNested3" ); + expect( array1[ 1 ] ).notToHaveKey( "removeNested3" ); + expect( array1[ 2 ] ).toBeStruct(); + expect( array1[ 2 ] ).toHaveKey( "keepNested3" ); + expect( array1[ 2 ] ).notToHaveKey( "removeNested3" ); + + var array1B = nested0.keepArray1B; + expect( array1B ).toBeArray(); + expect( array1B ).toHaveLength( 2 ); + expect( array1B[ 1 ] ).toBeArray(); + expect( array1B[ 1 ] ).toHaveLength( 2 ); + expect( array1B[ 1 ][ 1 ] ).toBeString(); + expect( array1B[ 1 ][ 1 ] ).toBe( "foo" ); + expect( array1B[ 1 ][ 2 ] ).toBeString(); + expect( array1B[ 1 ][ 2 ] ).toBe( "bar" ); + + expect( array1B[ 2 ] ).toBeArray(); + expect( array1B[ 2 ] ).toHaveLength( 2 ); + expect( array1B[ 2 ][ 1 ] ).toBeString(); + expect( array1B[ 2 ][ 1 ] ).toBe( "baz" ); + expect( array1B[ 2 ][ 2 ] ).toBeString(); + expect( array1B[ 2 ][ 2 ] ).toBe( "qux" ); + + var nested0B = keys.keepNested0B; + expect( nested0B ).toBeStruct(); + expect( nested0B ).toHaveKey( "keep1B" ); + expect( nested0B ).notToHaveKey( "remove1B" ); + } ); + } ); } ); story( "validate or fail with objects", function(){ From b6bc445c0bf33091a2b7bad0a0a3d3f26b10b619 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 8 Oct 2025 14:04:13 -0600 Subject: [PATCH 04/10] Update models/ValidationManager.cfc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- models/ValidationManager.cfc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index 4787a1a..457221b 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -326,6 +326,8 @@ component accessors="true" serialize="false" singleton { constraints = arrayConstraints.keyExists( "constraints" ) ? arrayConstraints.constraints : arrayConstraints.nestedConstraints ) ); + } else { + arrayAppend( filteredArray, item ); } } } else { From b8888a7e5d071ef6ecc397a30c58a0e32cc98a37 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 8 Oct 2025 14:04:39 -0600 Subject: [PATCH 05/10] Update models/ValidationManager.cfc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- models/ValidationManager.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index 457221b..d06b1eb 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -335,7 +335,7 @@ component accessors="true" serialize="false" singleton { } filteredTarget[ key ] = filteredArray; } else if ( - constraints[ key ].keyExists( "constraints" ) || constraints[ key ].keyExists( "nestedConstraints" ) + constraint.keyExists( "constraints" ) || constraint.keyExists( "nestedConstraints" ) ) { filteredTarget[ key ] = filterTargetForConstraints( target = arguments.target[ key ], From fea56944322ac4fbeeb65c79d642b042f0b4d706 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 8 Oct 2025 14:06:02 -0600 Subject: [PATCH 06/10] Update models/ValidationManager.cfc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- models/ValidationManager.cfc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index d06b1eb..092e4c6 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -305,6 +305,19 @@ component accessors="true" serialize="false" singleton { return filterTargetForConstraints( arguments.target, constraints ); } + /** + * Recursively filters the given target structure or object according to the provided constraints. + * + * This method processes the target and returns a new structure containing only the keys that exist in the constraints. + * It handles nested constraints (via "constraints" or "nestedConstraints" keys) and array item constraints (via "items" or "arrayItem" keys) + * by recursively filtering nested objects and arrays as needed. + * + * @target The target structure or object to filter. Can be a struct or an object containing fields to validate. + * @constraints The structure of constraints to use for filtering the target. Keys correspond to fields in the target. + * + * @return struct: A new structure containing only the fields from the target that match the provided constraints, + * with nested structures and arrays filtered recursively as specified by the constraints. + */ private any function filterTargetForConstraints( required any target, required struct constraints ){ var filteredTarget = {}; for ( var key in arguments.target ) { From ce99d9ba36ef79c3b6c7fa46326109c976d351ea Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 9 Oct 2025 12:40:32 -0600 Subject: [PATCH 07/10] Formatting --- models/ValidationManager.cfc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index 092e4c6..34d65d8 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -312,11 +312,12 @@ component accessors="true" serialize="false" singleton { * It handles nested constraints (via "constraints" or "nestedConstraints" keys) and array item constraints (via "items" or "arrayItem" keys) * by recursively filtering nested objects and arrays as needed. * + * with nested structures and arrays filtered recursively as specified by the constraints. + * * @target The target structure or object to filter. Can be a struct or an object containing fields to validate. * @constraints The structure of constraints to use for filtering the target. Keys correspond to fields in the target. * * @return struct: A new structure containing only the fields from the target that match the provided constraints, - * with nested structures and arrays filtered recursively as specified by the constraints. */ private any function filterTargetForConstraints( required any target, required struct constraints ){ var filteredTarget = {}; @@ -347,9 +348,7 @@ component accessors="true" serialize="false" singleton { filteredArray = arguments.target[ key ]; } filteredTarget[ key ] = filteredArray; - } else if ( - constraint.keyExists( "constraints" ) || constraint.keyExists( "nestedConstraints" ) - ) { + } else if ( constraint.keyExists( "constraints" ) || constraint.keyExists( "nestedConstraints" ) ) { filteredTarget[ key ] = filterTargetForConstraints( target = arguments.target[ key ], constraints = ( From fad62bfc069a59ff3eb858ac133cfe9ec1c1bcfc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 9 Oct 2025 12:40:44 -0600 Subject: [PATCH 08/10] Only test BoxLang Prime on ColdBox BE (8) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1105aa3..37dcbbb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - cfengine: [ "boxlang@1", "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ] + cfengine: [ "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ] coldboxVersion: [ "^7.0.0" ] experimental: [ false ] # Experimental: ColdBox BE vs All Engines From 5adf1b2889519458c6b40116d5d727bf7a274133 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:45:38 +0000 Subject: [PATCH 09/10] Initial plan From 74602e71820840351a0a461bca456091f18b2be9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 09:48:59 +0000 Subject: [PATCH 10/10] Update changelog with unreleased changes for PR #85 Co-authored-by: lmajano <137111+lmajano@users.noreply.github.com> --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index 0b8a406..8d79cf6 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `validateOrFail` now filters nested structs and arrays to only return keys matching constraints, not just top-level keys (PR #85) + ## [4.6.0] - 2025-09-18 ### Fixed