diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 600485c0..4085f336 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -616,12 +616,14 @@ component accessors="true" { if ( !hasAttribute( key ) ) { continue; } - variables._data[ retrieveColumnForAlias( key ) ] = ( - !arguments.attributes.keyExists( key ) || isNull( arguments.attributes[ key ] ) - ) ? javacast( "null", "" ) : castValueForGetter( key, arguments.attributes[ key ] ); - variables[ retrieveAliasForColumn( key ) ] = ( + var value = castValueForGetter( + key, !arguments.attributes.keyExists( key ) || isNull( arguments.attributes[ key ] ) - ) ? javacast( "null", "" ) : castValueForGetter( key, arguments.attributes[ key ] ); + ? javacast( "null", "" ) + : arguments.attributes[ key ] + ); + variables._data[ retrieveColumnForAlias( key ) ] = isNull( value ) ? javacast( "null", "" ) : value; + variables[ retrieveAliasForColumn( key ) ] = isNull( value ) ? javacast( "null", "" ) : value; } } @@ -3456,24 +3458,26 @@ component accessors="true" { } if ( !structKeyExists( variables._casts, arguments.key ) ) { - return arguments.value; - } - - if ( !isVirtualAttribute( arguments.key ) && isNullValue( arguments.key, arguments.value ) ) { - return arguments.value; + return isNull( arguments.value ) ? javacast( "null", "" ) : arguments.value; } var castMapping = variables._casts[ arguments.key ]; if ( !variables._casterCache.keyExists( arguments.key ) ) { variables._casterCache[ arguments.key ] = variables._wirebox.getInstance( dsl = castMapping ); } - var caster = variables._casterCache[ arguments.key ]; - variables._castCache[ arguments.key ] = caster.get( + var caster = variables._casterCache[ arguments.key ]; + var castedValue = caster.get( entity = this, key = arguments.key, value = isNull( arguments.value ) ? javacast( "null", "" ) : arguments.value ); - return variables._castCache[ arguments.key ]; + if ( isNull( castedValue ) ) { + structDelete( variables._castCache, arguments.key ); + return javacast( "null", "" ); + } + + variables._castCache[ arguments.key ] = castedValue; + return castedValue; } /** @@ -3524,6 +3528,14 @@ component accessors="true" { } var caster = variables._casterCache[ key ]; var attrs = caster.set( this, key, castedValue ); + if ( isNull( attrs ) ) { + assignAttribute( + name = key, + value = javacast( "null", "" ), + cast = false + ); + continue; + } if ( !isStruct( attrs ) ) { attrs = { "#key#" : attrs }; } diff --git a/models/Casts/BooleanCast.cfc b/models/Casts/BooleanCast.cfc index b125834c..292efe26 100644 --- a/models/Casts/BooleanCast.cfc +++ b/models/Casts/BooleanCast.cfc @@ -14,7 +14,11 @@ component singleton { required string key, any value ) { - return isNull( arguments.value ) ? false : !!arguments.value; + if ( isNull( arguments.value ) ) { + return javacast( "null", "" ); + } + + return arguments.entity.isNullValue( arguments.key, arguments.value ) ? arguments.value : !!arguments.value; } /** @@ -32,6 +36,10 @@ component singleton { required string key, any value ) { + if ( isNull( arguments.value ) || arguments.entity.isNullValue( arguments.key, arguments.value ) ) { + return javacast( "null", "" ); + } + return arguments.value ? 1 : 0; } diff --git a/tests/resources/app/models/CustomCastPhoneNumber.cfc b/tests/resources/app/models/CustomCastPhoneNumber.cfc new file mode 100644 index 00000000..46e61601 --- /dev/null +++ b/tests/resources/app/models/CustomCastPhoneNumber.cfc @@ -0,0 +1,10 @@ +component + extends ="quick.models.BaseEntity" + table ="phone_numbers" + accessors="true" +{ + + property name="id"; + property name="confirmed" casts="NullValueCast"; + +} diff --git a/tests/resources/app/models/NullValueCast.cfc b/tests/resources/app/models/NullValueCast.cfc new file mode 100644 index 00000000..afebdcbe --- /dev/null +++ b/tests/resources/app/models/NullValueCast.cfc @@ -0,0 +1,21 @@ +component singleton { + + public any function get( + required any entity, + required string key, + any value + ) { + return isNull( arguments.value ) || arguments.entity.isNullValue( arguments.key, arguments.value ) + ? "casted-null" + : arguments.value; + } + + public any function set( + required any entity, + required string key, + any value + ) { + return isNull( arguments.value ) ? javacast( "null", "" ) : arguments.value; + } + +} diff --git a/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc index ddd629cc..c5457845 100644 --- a/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc @@ -97,13 +97,21 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( theme.getConfig().primaryColor ).toBe( "orange" ); } ); - it( "can still allow nulls when using casts", () => { + it( "preserves null values when using BooleanCast", () => { var pn = getInstance( "PhoneNumber" ).find( 3 ); expect( pn.isNullAttribute( "confirmed" ) ).toBeTrue( "[confirmed] should be considered null" ); expect( pn.getConfirmed() ).toBe( "" ); - // expect( function() { - pn.update( { "active" : false } ); - // } ).notToThrow( message = "PhoneNumber should be able to be saved with a `null` [confirmed] value" ); + + pn.update( { "active" : false } ).refresh(); + + expect( pn.isNullAttribute( "confirmed" ) ).toBeTrue( "[confirmed] should remain null after saving" ); + expect( pn.getConfirmed() ).toBe( "" ); + } ); + + it( "allows custom casts to handle null database values", () => { + var pn = getInstance( "CustomCastPhoneNumber" ).find( 3 ); + + expect( pn.getConfirmed() ).toBe( "casted-null" ); } ); it( "correctly casts child entities", () => {