From 13addffcb816bca44cf40cd303a206a884618bba Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 06:00:22 -0600 Subject: [PATCH 1/2] Refresh database-generated attributes on save Closes #58 --- models/BaseEntity.cfc | 41 ++++++++++++++++++- .../app/models/DatabaseGeneratedUser.cfc | 9 ++++ .../specs/integration/BaseEntity/SaveSpec.cfc | 12 ++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/resources/app/models/DatabaseGeneratedUser.cfc diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index 8900ec48..ee4a4311 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1292,6 +1292,7 @@ component accessors="true" { } ), arguments.options ); + refreshAttributesOnSave(); assignOriginalAttributes( retrieveAttributesData() ); markLoaded(); fireEvent( @@ -1326,8 +1327,9 @@ component accessors="true" { result.result[ getParentDefinition().joincolumn ] = variables._data[ getParentDefinition().joinColumn ]; } retrieveKeyType().postInsert( this, result ); - assignOriginalAttributes( retrieveAttributesData() ); markLoaded(); + refreshAttributesOnSave(); + assignOriginalAttributes( retrieveAttributesData() ); fireEvent( "postInsert", { @@ -1359,6 +1361,39 @@ component accessors="true" { return this; } + /** + * Refreshes attributes whose values are generated or changed by the database + * during persistence. + */ + private void function refreshAttributesOnSave() { + var attributesToRefresh = variables._attributes.filter( function( name, attribute ) { + return attribute.refreshOnSave; + } ); + + if ( attributesToRefresh.isEmpty() ) { + return; + } + + var refreshedEntity = newQuery() + .withoutGlobalScope() + .where( function( q ) { + arrayZipEach( [ keyNames(), keyValues() ], function( keyName, keyValue ) { + q.where( keyName, keyValue ); + } ); + } ) + .first(); + if ( isNull( refreshedEntity ) ) { + return; + } + + var refreshedData = refreshedEntity.retrieveAttributesData( withNulls = true ); + attributesToRefresh.each( function( name, attribute ) { + var value = refreshedData[ attribute.column ]; + variables._data[ attribute.column ] = isNull( value ) ? javacast( "null", "" ) : value; + variables[ attribute.name ] = isNull( value ) ? javacast( "null", "" ) : value; + } ); + } + /** * Deletes the entity from the database. * This function can only be called on loaded entities. @@ -3411,6 +3446,7 @@ component accessors="true" { param attr.sqltype = ""; param attr.insert = true; param attr.update = true; + param attr.refreshOnSave = false; param attr.virtual = false; param attr.exclude = false; param attr.isParentColumn = false; @@ -3420,6 +3456,9 @@ component accessors="true" { if ( !isBoolean( attr.fillable ) ) { attr.fillable = lCase( trim( attr.fillable & "" ) ) == "true"; } + if ( !isBoolean( attr.refreshOnSave ) ) { + attr.refreshOnSave = lCase( trim( attr.refreshOnSave & "" ) ) == "true"; + } variables._nullValues[ attr.name ] = attr.nullValue; return arguments.attr; } diff --git a/tests/resources/app/models/DatabaseGeneratedUser.cfc b/tests/resources/app/models/DatabaseGeneratedUser.cfc new file mode 100644 index 00000000..8dfd2902 --- /dev/null +++ b/tests/resources/app/models/DatabaseGeneratedUser.cfc @@ -0,0 +1,9 @@ +component extends="quick.models.BaseEntity" accessors="true" table="users" { + + property name="id"; + property name="username"; + property name="firstName" column="first_name"; + property name="lastName" column="last_name"; + property name="createdDate" column="created_date" refreshOnSave="true"; + +} diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index a3b99ef1..5e2eba00 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -56,6 +56,18 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( newUser.retrieveAttributesData() ).toHaveKey( "id" ); } ); + it( "retrieves database-generated attributes marked to refresh on save", function() { + var newUser = getInstance( "DatabaseGeneratedUser" ) + .setUsername( "database-timestamp-user" ) + .setFirstName( "Database" ) + .setLastName( "Timestamp" ) + .save(); + + expect( newUser.getCreatedDate() ).notToBe( "" ); + expect( newUser.getCreatedDate() ).toBeDate(); + expect( newUser.isDirty( "createdDate" ) ).toBeFalse(); + } ); + it( "a saved entity is not dirty", function() { var newUser = getInstance( "User" ); newUser.setUsername( "new_user" ); From e2d0b718deaeb843303623afd0d6eae4ed7230ba Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Aug 2026 13:45:19 -0600 Subject: [PATCH 2/2] refactor: avoid internal closures --- models/BaseEntity.cfc | 39 +++++++++++-------- .../app/models/DatabaseGeneratedUser.cfc | 15 +++++-- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index ee4a4311..22c75983 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1366,32 +1366,39 @@ component accessors="true" { * during persistence. */ private void function refreshAttributesOnSave() { - var attributesToRefresh = variables._attributes.filter( function( name, attribute ) { - return attribute.refreshOnSave; - } ); + var attributesToRefresh = {}; + for ( var name in variables._attributes ) { + if ( variables._attributes[ name ].refreshOnSave ) { + attributesToRefresh[ name ] = variables._attributes[ name ]; + } + } if ( attributesToRefresh.isEmpty() ) { return; } - var refreshedEntity = newQuery() - .withoutGlobalScope() - .where( function( q ) { - arrayZipEach( [ keyNames(), keyValues() ], function( keyName, keyValue ) { - q.where( keyName, keyValue ); - } ); - } ) - .first(); + var refreshQuery = newQuery().withoutGlobalScope(); + var entityKeys = keyNames(); + var entityValues = keyValues(); + for ( var i = 1; i <= entityKeys.len(); i++ ) { + refreshQuery.where( entityKeys[ i ], entityValues[ i ] ); + } + var refreshedEntity = refreshQuery.first(); if ( isNull( refreshedEntity ) ) { return; } var refreshedData = refreshedEntity.retrieveAttributesData( withNulls = true ); - attributesToRefresh.each( function( name, attribute ) { - var value = refreshedData[ attribute.column ]; - variables._data[ attribute.column ] = isNull( value ) ? javacast( "null", "" ) : value; - variables[ attribute.name ] = isNull( value ) ? javacast( "null", "" ) : value; - } ); + for ( var name in attributesToRefresh ) { + var attribute = attributesToRefresh[ name ]; + if ( isNull( refreshedData[ attribute.column ] ) ) { + variables._data[ attribute.column ] = javacast( "null", "" ); + variables[ attribute.name ] = javacast( "null", "" ); + } else { + variables._data[ attribute.column ] = refreshedData[ attribute.column ]; + variables[ attribute.name ] = refreshedData[ attribute.column ]; + } + } } /** diff --git a/tests/resources/app/models/DatabaseGeneratedUser.cfc b/tests/resources/app/models/DatabaseGeneratedUser.cfc index 8dfd2902..df746bdd 100644 --- a/tests/resources/app/models/DatabaseGeneratedUser.cfc +++ b/tests/resources/app/models/DatabaseGeneratedUser.cfc @@ -1,9 +1,16 @@ -component extends="quick.models.BaseEntity" accessors="true" table="users" { +component + extends ="quick.models.BaseEntity" + accessors="true" + table ="users" +{ property name="id"; property name="username"; - property name="firstName" column="first_name"; - property name="lastName" column="last_name"; - property name="createdDate" column="created_date" refreshOnSave="true"; + property name="firstName" column="first_name"; + property name="lastName" column="last_name"; + property + name ="createdDate" + column ="created_date" + refreshOnSave="true"; }