diff --git a/.gitignore b/.gitignore index 4f1afa2d..aa35f9b5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,7 @@ /modules .vscode -!.engine/ -.engine/* -!.engine/WEB-INF/ -.engine/WEB-INF/* -!.engine/WEB-INF/lib -.engine/WEB-INF/lib/* -!.engine/WEB-INF/lib/h2-1.4.196.jar +/.engine/ .env .tmp diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index 34c0eca0..0b6cc09b 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -12,6 +12,7 @@ component { "defaultQueryOptions" : {}, "preventDuplicateJoins" : true, "preventLazyLoading" : false, + "automaticTimestamps" : true, "refreshOnSaveFallback" : true, "lazyLoadingViolationCallback" : ( entity, relationName ) => { throw( diff --git a/models/BaseEntity.cfc b/models/BaseEntity.cfc index c98ffa7e..19fc4ee8 100644 --- a/models/BaseEntity.cfc +++ b/models/BaseEntity.cfc @@ -221,6 +221,22 @@ component accessors="true" { persistent="false" inject ="box:setting:refreshOnSaveFallback@quick"; + /** + * The module-level default for automatic entity timestamps. + */ + property + name ="_automaticTimestampsDefault" + persistent="false" + inject ="box:setting:automaticTimestamps@quick"; + + /** + * Whether automatic timestamps are disabled for the current operation chain. + */ + property + name ="_withoutAutomaticTimestamps" + default ="false" + persistent="false"; + /** * A boolean flag representing that events should not be fired. */ @@ -289,21 +305,23 @@ component accessors="true" { private any function assignDefaultProperties() { assignAttributesData( {} ); assignOriginalAttributes( {} ); - variables._globalScopeExclusions = []; - param variables._key = "id"; - param variables._meta = {}; - param variables._data = {}; - param variables._relationshipsData = {}; - param variables._relationshipsLoaded = {}; - param variables._with = []; - variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); - variables._applyingGlobalScopes = false; - variables._globalScopesApplied = false; - variables._ignoreNotLoadedGuard = false; - variables._withoutFiringEvents = false; - variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); - param variables._preventLazyLoading = false; - param variables._refreshOnSaveFallback = true; + variables._globalScopeExclusions = []; + param variables._key = "id"; + param variables._meta = {}; + param variables._data = {}; + param variables._relationshipsData = {}; + param variables._relationshipsLoaded = {}; + param variables._with = []; + variables._withoutRelationshipConstraints = createObject( "java", "java.util.HashSet" ).init(); + variables._applyingGlobalScopes = false; + variables._globalScopesApplied = false; + variables._ignoreNotLoadedGuard = false; + variables._withoutFiringEvents = false; + variables._nullValueArgumentSentinel = createObject( "java", "java.lang.Object" ).init(); + param variables._preventLazyLoading = false; + param variables._refreshOnSaveFallback = true; + param variables._automaticTimestampsDefault = true; + param variables._withoutAutomaticTimestamps = false; if ( !variables.keyExists( "_lazyLoadingViolationCallback" ) || isNull( variables._lazyLoadingViolationCallback ) ) { variables._lazyLoadingViolationCallback = ( entity, relationName ) => { throw( @@ -490,7 +508,55 @@ component accessors="true" { * @return [String] */ public array function timestampFields() { - return [ "createdDate", "modifiedDate" ]; + var fields = []; + var createdDateAttribute = retrieveCreatedDateAttribute(); + if ( len( createdDateAttribute ) ) { + fields.append( createdDateAttribute ); + } + var modifiedDateAttribute = retrieveModifiedDateAttribute(); + if ( len( modifiedDateAttribute ) ) { + fields.append( modifiedDateAttribute ); + } + return fields; + } + + /** + * Returns whether this entity automatically maintains timestamps. + */ + public boolean function usesAutomaticTimestamps() { + return variables.automaticTimestamps && !variables._withoutAutomaticTimestamps; + } + + /** + * Returns the configured created timestamp attribute when it exists on the entity. + */ + public string function retrieveCreatedDateAttribute() { + return hasAttribute( variables.createdDateAttribute ) ? variables.createdDateAttribute : ""; + } + + /** + * Returns the configured modified timestamp attribute when it exists on the entity. + */ + public string function retrieveModifiedDateAttribute() { + return hasAttribute( variables.modifiedDateAttribute ) ? variables.modifiedDateAttribute : ""; + } + + /** + * Applies conventional timestamps to the current insert or update when configured attributes exist. + */ + private void function applyAutomaticTimestamps() { + if ( !usesAutomaticTimestamps() ) { + return; + } + var timestamp = now(); + var modifiedDateAttribute = retrieveModifiedDateAttribute(); + if ( len( modifiedDateAttribute ) && !isDirty( modifiedDateAttribute ) ) { + assignAttribute( modifiedDateAttribute, timestamp ); + } + var createdDateAttribute = retrieveCreatedDateAttribute(); + if ( !isLoaded() && len( createdDateAttribute ) && !isDirty( createdDateAttribute ) ) { + assignAttribute( createdDateAttribute, timestamp ); + } } /** @@ -1354,13 +1420,15 @@ component accessors="true" { */ public any function newEntity( string name ) { if ( isNull( arguments.name ) ) { - return variables._wirebox.getInstance( - name = mappingName(), - initArguments = { - meta : variables._meta, - runtimeAttributeOverlay : variables._runtimeAttributeOverlay - } - ); + return variables._wirebox + .getInstance( + name = mappingName(), + initArguments = { + meta : variables._meta, + runtimeAttributeOverlay : variables._runtimeAttributeOverlay + } + ) + .set_withoutAutomaticTimestamps( variables._withoutAutomaticTimestamps ); } // Custom named instance return variables._wirebox.getInstance( arguments.name ); @@ -1563,6 +1631,7 @@ component accessors="true" { } guardNoAttributes(); guardReadOnly(); + applyAutomaticTimestamps(); fireEvent( "preSave", { @@ -1971,7 +2040,12 @@ component accessors="true" { var timestamp = now(); var timestampAttributes = {}; for ( var field in timestampFields() ) { - timestampAttributes[ field ] = timestamp; + if ( hasAttribute( field ) ) { + timestampAttributes[ field ] = timestamp; + } + } + if ( timestampAttributes.isEmpty() ) { + return this; } guardAgainstReadOnlyAttributes( timestampAttributes ); @@ -3753,13 +3827,16 @@ component accessors="true" { meta[ "entityName" ] = meta.originalMetadata.entityName; param meta.localMetadata.properties = []; guardDuplicatePropertyNames( meta.localMetadata, meta.mapping ); - param meta.originalMetadata.table = variables._str.plural( variables._str.snake( meta.entityName ) ); - meta[ "table" ] = meta.originalMetadata.table; - param meta.originalMetadata.readonly = false; - meta[ "readonly" ] = meta.originalMetadata.readonly; - param meta.originalMetadata.softDeletes = false; - param meta.originalMetadata.softDeleteColumn = "deletedDate"; - meta[ "softDeletes" ] = isBoolean( meta.originalMetadata.softDeletes ) + param meta.originalMetadata.table = variables._str.plural( variables._str.snake( meta.entityName ) ); + meta[ "table" ] = meta.originalMetadata.table; + param meta.originalMetadata.readonly = false; + meta[ "readonly" ] = meta.originalMetadata.readonly; + param meta.originalMetadata.softDeletes = false; + param meta.originalMetadata.softDeleteColumn = "deletedDate"; + param meta.originalMetadata.automaticTimestamps = variables._automaticTimestampsDefault; + param meta.originalMetadata.createdDateAttribute = "createdDate"; + param meta.originalMetadata.modifiedDateAttribute = "modifiedDate"; + meta[ "softDeletes" ] = isBoolean( meta.originalMetadata.softDeletes ) ? meta.originalMetadata.softDeletes : lCase( trim( meta.originalMetadata.softDeletes & "" ) ) == "true"; meta[ "softDeleteColumn" ] = meta.originalMetadata.softDeleteColumn; @@ -3883,14 +3960,20 @@ component accessors="true" { if ( variables._queryOptions.isEmpty() && variables._meta.originalMetadata.keyExists( "datasource" ) ) { variables._queryOptions = { datasource : variables._meta.originalMetadata.datasource }; } - variables._readonly = variables._meta.readonly; - variables._softDeletes = variables._meta.softDeletes; - variables._softDeleteColumn = variables._meta.softDeleteColumn; - variables._attributes = variables._meta.attributes; - variables._columns = variables._meta.columns; - variables._functionNames = variables._meta.functionNames; - variables._nonPersistentProperties = variables._meta.nonPersistentProperties; - variables._grammar = variables._meta.originalMetadata.keyExists( "grammar" ) + variables._readonly = variables._meta.readonly; + variables._softDeletes = variables._meta.softDeletes; + variables._softDeleteColumn = variables._meta.softDeleteColumn; + var metadataAutomaticTimestamps = isBoolean( variables._meta.originalMetadata.automaticTimestamps ) + ? variables._meta.originalMetadata.automaticTimestamps + : lCase( trim( variables._meta.originalMetadata.automaticTimestamps & "" ) ) == "true"; + param variables.automaticTimestamps = metadataAutomaticTimestamps; + param variables.createdDateAttribute = variables._meta.originalMetadata.createdDateAttribute; + param variables.modifiedDateAttribute = variables._meta.originalMetadata.modifiedDateAttribute; + variables._attributes = variables._meta.attributes; + variables._columns = variables._meta.columns; + variables._functionNames = variables._meta.functionNames; + variables._nonPersistentProperties = variables._meta.nonPersistentProperties; + variables._grammar = variables._meta.originalMetadata.keyExists( "grammar" ) ? variables._meta.originalMetadata.grammar : ""; variables._discriminatorColumn = variables._meta.localMetadata.keyExists( "discriminatorColumn" ) @@ -3959,7 +4042,10 @@ component accessors="true" { "singleTableInheritance", "datasource", "grammar", - "discriminatorColumn" + "discriminatorColumn", + "automaticTimestamps", + "createdDateAttribute", + "modifiedDateAttribute" ] ) { if ( arguments.metadata.annotations.keyExists( key ) && !isNull( arguments.metadata.annotations[ key ] ) ) { diff --git a/models/QuickBuilder.cfc b/models/QuickBuilder.cfc index 2701e2aa..8105492f 100644 --- a/models/QuickBuilder.cfc +++ b/models/QuickBuilder.cfc @@ -80,6 +80,11 @@ component accessors="true" transientCache="false" { */ property name="_entityTransformers"; + /** + * Whether automatic timestamps are disabled for mutations created by this builder. + */ + property name="_withoutAutomaticTimestamps" default="false"; + /** * Used to quickly identify QueryBuilder instances * instead of resorting to `isInstanceOf` which is slow. @@ -93,14 +98,15 @@ component accessors="true" transientCache="false" { this.isQuickBuilder = true; function init() { - variables._eagerLoad = []; - variables._globalScopesApplied = false; - variables._globalScopeExcludeAll = false; - variables._asMemento = false; - variables._asQuery = false; - variables._withAliases = false; - variables._entityTransformers = []; - param variables._preventLazyLoading = false; + variables._eagerLoad = []; + variables._globalScopesApplied = false; + variables._globalScopeExcludeAll = false; + variables._asMemento = false; + variables._asQuery = false; + variables._withAliases = false; + variables._entityTransformers = []; + variables._withoutAutomaticTimestamps = false; + param variables._preventLazyLoading = false; if ( !variables.keyExists( "_lazyLoadingViolationCallback" ) || isNull( variables._lazyLoadingViolationCallback ) ) { variables._lazyLoadingViolationCallback = ( entity, relationName ) => { throw( @@ -136,11 +142,21 @@ component accessors="true" transientCache="false" { public QuickBuilder function setEntity( required any newEntity ) { variables.entity = arguments.newEntity; + variables.entity.set_withoutAutomaticTimestamps( variables._withoutAutomaticTimestamps ); variables.qb.setEntity( arguments.newEntity ); variables.aliasMap[ arguments.newEntity.tableAlias() ] = arguments.newEntity; return this; } + /** + * Disables automatic timestamps for mutations and entities created by this builder. + */ + public QuickBuilder function withoutAutomaticTimestamps() { + variables._withoutAutomaticTimestamps = true; + getEntity().set_withoutAutomaticTimestamps( true ); + return this; + } + /** * Resolves a relationship without allocating nested guard callbacks. */ @@ -492,6 +508,7 @@ component accessors="true" transientCache="false" { getEntity().guardReadOnly(); getEntity().guardAgainstReadOnlyAttributes( arguments.attributes ); } + arguments.attributes = appendUpdatedTimestamp( arguments.attributes ); return variables.qb.update( prepareBulkMutationAttributes( arguments.attributes ) ); } @@ -534,9 +551,21 @@ component accessors="true" transientCache="false" { } } + arguments.values = appendInsertTimestamps( arguments.values ); arguments.values = prepareBulkMutationValues( arguments.values ); if ( structKeyExists( arguments, "update" ) && isStruct( arguments.update ) ) { + arguments.update = appendUpdatedTimestamp( arguments.update ); arguments.update = prepareBulkMutationAttributes( arguments.update ); + } else if ( structKeyExists( arguments, "update" ) && isArray( arguments.update ) ) { + var modifiedDateAttribute = getEntity().retrieveModifiedDateAttribute(); + if ( + !variables._withoutAutomaticTimestamps + && getEntity().usesAutomaticTimestamps() + && len( modifiedDateAttribute ) + && !arguments.update.findNoCase( modifiedDateAttribute ) + ) { + arguments.update.append( modifiedDateAttribute ); + } } structDelete( arguments, "force" ); @@ -557,6 +586,48 @@ component accessors="true" transientCache="false" { return preparedAttributes; } + /** + * Adds the configured modified timestamp to bulk mutation attributes when available. + */ + private struct function appendUpdatedTimestamp( required struct attributes ) { + var timestampAttributes = duplicate( arguments.attributes ); + var modifiedDateAttribute = getEntity().retrieveModifiedDateAttribute(); + if ( + !variables._withoutAutomaticTimestamps + && getEntity().usesAutomaticTimestamps() + && len( modifiedDateAttribute ) + && !timestampAttributes.keyExists( modifiedDateAttribute ) + ) { + timestampAttributes[ modifiedDateAttribute ] = now(); + } + return timestampAttributes; + } + + /** + * Adds configured insert timestamps to literal upsert rows when available. + */ + private any function appendInsertTimestamps( required any values ) { + if ( variables._withoutAutomaticTimestamps || !getEntity().usesAutomaticTimestamps() ) { + return arguments.values; + } + if ( isArray( arguments.values ) ) { + return arguments.values.map( ( value ) => isStruct( value ) ? appendInsertTimestamps( value ) : value ); + } + if ( + !isStruct( arguments.values ) + || structKeyExists( arguments.values, "isBuilder" ) + || structKeyExists( arguments.values, "isQuickBuilder" ) + ) { + return arguments.values; + } + var timestampAttributes = appendUpdatedTimestamp( arguments.values ); + var createdDateAttribute = getEntity().retrieveCreatedDateAttribute(); + if ( len( createdDateAttribute ) && !timestampAttributes.keyExists( createdDateAttribute ) ) { + timestampAttributes[ createdDateAttribute ] = now(); + } + return timestampAttributes; + } + /** * Applies Quick query parameter metadata to literal upsert rows. */ @@ -2052,6 +2123,9 @@ component accessors="true" transientCache="false" { newBuilder.set_asMemento( this.get_asMemento() ); newBuilder.set_asMementoSettings( this.get_asMementoSettings() ); newBuilder.set_entityTransformers( this.get_entityTransformers() ); + if ( variables._withoutAutomaticTimestamps ) { + newBuilder.withoutAutomaticTimestamps(); + } return newBuilder; } diff --git a/tests/resources/app/models/AnnotatedTimestampUser.cfc b/tests/resources/app/models/AnnotatedTimestampUser.cfc new file mode 100644 index 00000000..02914000 --- /dev/null +++ b/tests/resources/app/models/AnnotatedTimestampUser.cfc @@ -0,0 +1,17 @@ +component + extends ="quick.models.BaseEntity" + accessors ="true" + table ="users" + createdDateAttribute ="createdDate" + modifiedDateAttribute="modifiedDate" +{ + + property name="id"; + property name="username"; + property name="firstName" column="first_name"; + property name="lastName" column="last_name"; + property name="password"; + property name="createdDate" column="created_date"; + property name="modifiedDate" column="modified_date"; + +} diff --git a/tests/resources/app/models/AutomaticTimestampUser.cfc b/tests/resources/app/models/AutomaticTimestampUser.cfc new file mode 100644 index 00000000..5bfcbc95 --- /dev/null +++ b/tests/resources/app/models/AutomaticTimestampUser.cfc @@ -0,0 +1,15 @@ +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="password"; + property name="createdDate" column="created_date"; + property name="modifiedDate" column="modified_date"; + +} diff --git a/tests/resources/app/models/DatabaseGeneratedUser.cfc b/tests/resources/app/models/DatabaseGeneratedUser.cfc index 04c0c4e8..04abab13 100644 --- a/tests/resources/app/models/DatabaseGeneratedUser.cfc +++ b/tests/resources/app/models/DatabaseGeneratedUser.cfc @@ -20,6 +20,8 @@ component refreshOnSave="true" casts ="UppercaseCast"; + variables.automaticTimestamps = false; + function postLoad( eventData ) { param request.databaseGeneratedUserPostLoadCount = 0; request.databaseGeneratedUserPostLoadCount++; diff --git a/tests/resources/app/models/DisabledAutomaticTimestampUser.cfc b/tests/resources/app/models/DisabledAutomaticTimestampUser.cfc new file mode 100644 index 00000000..f62223e0 --- /dev/null +++ b/tests/resources/app/models/DisabledAutomaticTimestampUser.cfc @@ -0,0 +1,17 @@ +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="password"; + property name="createdDate" column="created_date"; + property name="modifiedDate" column="modified_date"; + + variables.automaticTimestamps = false; + +} diff --git a/tests/resources/app/models/Link.cfc b/tests/resources/app/models/Link.cfc index ac80be42..e9d8c9dc 100644 --- a/tests/resources/app/models/Link.cfc +++ b/tests/resources/app/models/Link.cfc @@ -1,11 +1,18 @@ component extends="quick.models.BaseEntity" accessors="true" { - property name="wirebox" inject="wirebox" persistent="false"; + property + name ="wirebox" + inject ="wirebox" + persistent="false"; - property name="link_id" column="link_id"; - property name="url" column="link_url"; - property name="createdDate" column="created_date" readonly="true"; + property name="link_id" column="link_id"; + property name="url" column="link_url"; + property + name ="createdDate" + column ="created_date" + readonly="true"; - variables._key = "link_id"; + variables.automaticTimestamps = false; + variables._key = "link_id"; } diff --git a/tests/specs/integration/BaseEntity/AutomaticTimestampsSpec.cfc b/tests/specs/integration/BaseEntity/AutomaticTimestampsSpec.cfc new file mode 100644 index 00000000..8db12fe4 --- /dev/null +++ b/tests/specs/integration/BaseEntity/AutomaticTimestampsSpec.cfc @@ -0,0 +1,119 @@ +component extends="tests.resources.ModuleIntegrationSpec" { + + function run() { + describe( "Automatic Timestamps", function() { + it( "uses the module setting as the per-entity default", function() { + var user = getInstance( "AutomaticTimestampUser" ); + + expect( user.get_automaticTimestampsDefault() ).toBeTrue(); + expect( user.usesAutomaticTimestamps() ).toBeTrue(); + expect( user.timestampFields() ).toBe( [ "createdDate", "modifiedDate" ] ); + } ); + + it( "touches the configured timestamp fields", function() { + var user = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ); + var originalCreatedDate = user.getCreatedDate(); + queryExecute( "UPDATE users SET modified_date = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY) WHERE id = 1" ); + user = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ); + var originalModifiedDate = user.getModifiedDate(); + + user.touch(); + var freshUser = user.fresh(); + + expect( dateCompare( freshUser.getCreatedDate(), originalCreatedDate ) ).toBe( 1 ); + expect( dateCompare( freshUser.getModifiedDate(), originalModifiedDate ) ).toBe( 1 ); + } ); + + it( "sets conventional timestamps during inserts and updates", function() { + var user = getInstance( "AutomaticTimestampUser" ).create( { + "username" : "automatic-timestamps", + "firstName" : "Automatic", + "lastName" : "Timestamps", + "password" : "secret" + } ); + + expect( user.getCreatedDate() ).toBeDate(); + expect( user.getModifiedDate() ).toBeDate(); + + queryExecute( + "UPDATE users SET modified_date = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY) WHERE id = :id", + { "id" : user.getId() } + ); + user = getInstance( "AutomaticTimestampUser" ).findOrFail( user.getId() ); + var previousModifiedDate = user.getModifiedDate(); + user.update( { "firstName" : "Updated" } ); + + expect( dateCompare( user.getModifiedDate(), previousModifiedDate ) ).toBe( 1 ); + } ); + + it( "preserves explicitly assigned timestamps", function() { + var createdDate = dateAdd( "y", -1, now() ); + var modifiedDate = dateAdd( "m", -1, now() ); + var user = getInstance( "AutomaticTimestampUser" ).create( { + "username" : "explicit-timestamps", + "firstName" : "Explicit", + "lastName" : "Timestamps", + "password" : "secret", + "createdDate" : createdDate, + "modifiedDate" : modifiedDate + } ); + + expect( dateCompare( user.getCreatedDate(), createdDate ) ).toBe( 0 ); + expect( dateCompare( user.getModifiedDate(), modifiedDate ) ).toBe( 0 ); + } ); + + it( "supports component metadata timestamp attribute names", function() { + var user = getInstance( "AnnotatedTimestampUser" ).create( { + "username" : "annotated-timestamps", + "firstName" : "Annotated", + "lastName" : "Timestamps", + "password" : "secret" + } ); + + expect( user.getCreatedDate() ).toBeDate(); + expect( user.getModifiedDate() ).toBeDate(); + } ); + + it( "can disable automatic timestamps per entity", function() { + queryExecute( "UPDATE users SET modified_date = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY) WHERE id = 1" ); + var user = getInstance( "DisabledAutomaticTimestampUser" ).findOrFail( 1 ); + var originalModifiedDate = user.getModifiedDate(); + user.update( { "firstName" : "No Timestamp" } ); + + expect( user.usesAutomaticTimestamps() ).toBeFalse(); + expect( dateCompare( user.refresh().getModifiedDate(), originalModifiedDate ) ).toBe( 0 ); + } ); + + it( "does not add SQL for timestamp attributes missing from the entity", function() { + var country = getInstance( "Country" ).firstOrFail(); + country.update( { "name" : "No Blind Timestamp SQL" } ); + + expect( country.getName() ).toBe( "No Blind Timestamp SQL" ); + } ); + + it( "can disable automatic timestamps for a builder chain", function() { + queryExecute( "UPDATE users SET modified_date = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY) WHERE id = 1" ); + var originalModifiedDate = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ).getModifiedDate(); + + getInstance( "AutomaticTimestampUser" ) + .whereId( 1 ) + .withoutAutomaticTimestamps() + .updateAll( { "firstName" : "Builder Disabled" } ); + + var user = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ); + expect( dateCompare( user.getModifiedDate(), originalModifiedDate ) ).toBe( 0 ); + } ); + + it( "adds the update timestamp to normal bulk updates", function() { + queryExecute( "UPDATE users SET modified_date = DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY) WHERE id = 1" ); + var previousModifiedDate = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ).getModifiedDate(); + + getInstance( "AutomaticTimestampUser" ).whereId( 1 ).updateAll( { "firstName" : "Builder Timestamp" } ); + + var user = getInstance( "AutomaticTimestampUser" ).findOrFail( 1 ); + expect( dateCompare( user.getModifiedDate(), previousModifiedDate ) ).toBe( 1 ); + } ); + } ); + } + +} diff --git a/tests/specs/integration/BaseEntity/GetSpec.cfc b/tests/specs/integration/BaseEntity/GetSpec.cfc index d3556d3b..2b58edfe 100644 --- a/tests/specs/integration/BaseEntity/GetSpec.cfc +++ b/tests/specs/integration/BaseEntity/GetSpec.cfc @@ -378,6 +378,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( newUser.isLoaded() ).toBeTrue(); var attrs = newUser.retrieveAttributesData( aliased = true ); attrs.delete( "id" ); + attrs.delete( "createdDate" ); + attrs.delete( "modifiedDate" ); expect( attrs ).toBe( { "username" : "doesntexist", "firstName" : "doesnt", @@ -398,6 +400,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( newUser.isLoaded() ).toBeTrue(); var attrs = newUser.retrieveAttributesData( aliased = true ); attrs.delete( "id" ); + attrs.delete( "createdDate" ); + attrs.delete( "modifiedDate" ); expect( attrs ).toBe( { "username" : "doesntexist", "firstName" : "doesnt", @@ -485,6 +489,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( newUser.isLoaded() ).toBeTrue(); var attrs = newUser.retrieveAttributesData( aliased = true ); attrs.delete( "id" ); + attrs.delete( "createdDate" ); + attrs.delete( "modifiedDate" ); expect( attrs ).toBe( { "username" : "doesntexist", "firstName" : "doesnt",