From e02a61bb7e304a009494009d3f961c3737c77910 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 06:20:43 -0600 Subject: [PATCH 1/3] Add touch for entity timestamps Closes #53 --- models/BaseEntity.cfc | 14 ++++++++++++++ tests/specs/integration/BaseEntity/SaveSpec.cfc | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index f2fa40ec..cfc81698 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -1414,6 +1414,20 @@ component accessors="true" { return save(); } + /** + * Updates a timestamp attribute to the current time and saves the entity. + * + * @attribute The timestamp attribute to update. Default: `modifiedDate`. + * @options Any options to pass to `queryExecute`. Default: {}. + * + * @return quick.models.BaseEntity + */ + public any function touch( string attribute = "modifiedDate", struct options = {} ) { + guardAgainstNotLoaded( "This instance is not loaded so it cannot be touched." ); + assignAttribute( arguments.attribute, now() ); + return save( arguments.options ); + } + /** * Creates a new entity with the given attributes and then saves the entity. * diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 9ffc5f6d..00e5982a 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -76,6 +76,17 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( userRowsPostSave ).toHaveLength( 5 ); } ); + it( "can touch an entity timestamp", function() { + var user = getInstance( "User" ).findOrFail( 1 ); + var originalModified = user.getModifiedDate(); + + user.touch(); + + expect( dateCompare( user.getModifiedDate(), originalModified ) ).toBe( 1 ); + expect( user.isDirty( "modifiedDate" ) ).toBeFalse(); + expect( dateCompare( user.fresh().getModifiedDate(), originalModified ) ).toBe( 1 ); + } ); + it( "does not allow updating of column where update=false in property", function() { var existingUser = getInstance( "User" ).find( 1 ); existingUser.setEmail( "test2@test.com" ); From 3630e2df5c391d931c5eaf6827d25e56e941b6eb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 24 Aug 2026 15:02:10 -0600 Subject: [PATCH 2/3] test: constrain touch to timestamp fields --- .../app/models/CustomTimestampUser.cfc | 11 +++++++++++ .../specs/integration/BaseEntity/SaveSpec.cfc | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/resources/app/models/CustomTimestampUser.cfc diff --git a/tests/resources/app/models/CustomTimestampUser.cfc b/tests/resources/app/models/CustomTimestampUser.cfc new file mode 100644 index 00000000..19591907 --- /dev/null +++ b/tests/resources/app/models/CustomTimestampUser.cfc @@ -0,0 +1,11 @@ +component extends="quick.models.BaseEntity" accessors="true" table="users" { + + property name="id"; + property name="createdDate" column="created_date"; + property name="modifiedDate" column="modified_date"; + + public array function timestampFields() { + return [ "createdDate" ]; + } + +} diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 00e5982a..b2b91d5b 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -79,12 +79,29 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "can touch an entity timestamp", function() { var user = getInstance( "User" ).findOrFail( 1 ); var originalModified = user.getModifiedDate(); + var originalFirstName = user.getFirstName(); + + user.setFirstName( "This must not be persisted" ); user.touch(); expect( dateCompare( user.getModifiedDate(), originalModified ) ).toBe( 1 ); + expect( user.getFirstName() ).toBe( originalFirstName ); expect( user.isDirty( "modifiedDate" ) ).toBeFalse(); - expect( dateCompare( user.fresh().getModifiedDate(), originalModified ) ).toBe( 1 ); + var freshUser = user.fresh(); + expect( dateCompare( freshUser.getModifiedDate(), originalModified ) ).toBe( 1 ); + expect( freshUser.getFirstName() ).toBe( originalFirstName ); + } ); + + it( "can override the timestamp fields used by touch", function() { + var user = getInstance( "CustomTimestampUser" ).findOrFail( 1 ); + var originalCreated = user.getCreatedDate(); + var originalModified = user.getModifiedDate(); + + user.touch(); + + expect( dateCompare( user.getCreatedDate(), originalCreated ) ).toBe( 1 ); + expect( dateCompare( user.getModifiedDate(), originalModified ) ).toBe( 0 ); } ); it( "does not allow updating of column where update=false in property", function() { From 822a110fab494dbffa5da6ab09ffb74d787c2d65 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 24 Aug 2026 15:12:42 -0600 Subject: [PATCH 3/3] feat: touch only configured timestamp fields --- models/BaseEntity.cfc | 23 +++++++++++++++---- .../app/models/CustomTimestampUser.cfc | 6 ++++- .../specs/integration/BaseEntity/SaveSpec.cfc | 4 ++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index cfc81698..f63141b1 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -439,6 +439,15 @@ component accessors="true" { return arrayWrap( variables._key ); } + /** + * Returns the timestamp fields updated by `touch`. + * + * @return [String] + */ + public array function timestampFields() { + return [ "modifiedDate" ]; + } + /** * Returns the column name for the primary key. * @@ -1415,16 +1424,20 @@ component accessors="true" { } /** - * Updates a timestamp attribute to the current time and saves the entity. + * Resets the entity to its clean state, updates its timestamp fields to the + * current time, and saves the entity. * - * @attribute The timestamp attribute to update. Default: `modifiedDate`. - * @options Any options to pass to `queryExecute`. Default: {}. + * @options Any options to pass to `queryExecute`. Default: {}. * * @return quick.models.BaseEntity */ - public any function touch( string attribute = "modifiedDate", struct options = {} ) { + public any function touch( struct options = {} ) { guardAgainstNotLoaded( "This instance is not loaded so it cannot be touched." ); - assignAttribute( arguments.attribute, now() ); + reset(); + var timestamp = now(); + timestampFields().each( function( field ) { + assignAttribute( arguments.field, timestamp ); + } ); return save( arguments.options ); } diff --git a/tests/resources/app/models/CustomTimestampUser.cfc b/tests/resources/app/models/CustomTimestampUser.cfc index 19591907..7c3866d8 100644 --- a/tests/resources/app/models/CustomTimestampUser.cfc +++ b/tests/resources/app/models/CustomTimestampUser.cfc @@ -1,4 +1,8 @@ -component extends="quick.models.BaseEntity" accessors="true" table="users" { +component + extends ="quick.models.BaseEntity" + accessors="true" + table ="users" +{ property name="id"; property name="createdDate" column="created_date"; diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index b2b91d5b..16af7397 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -77,8 +77,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { } ); it( "can touch an entity timestamp", function() { - var user = getInstance( "User" ).findOrFail( 1 ); - var originalModified = user.getModifiedDate(); + var user = getInstance( "User" ).findOrFail( 1 ); + var originalModified = user.getModifiedDate(); var originalFirstName = user.getFirstName(); user.setFirstName( "This must not be persisted" );