From 5d2443786e933b72dbcab7b172e824dea263ee58 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 21:42:15 -0600 Subject: [PATCH 1/4] fix: support partial memento settings with full null --- .github/workflows/tests.yml | 5 ++ interceptors/Mementifier.cfc | 63 +++++++++++++------ test-harness/models/PartialMemento.cfc | 12 ++++ test-harness/tests/full-null/Application.cfc | 12 ++++ .../tests/full-null/PartialMemento.cfc | 12 ++++ .../tests/full-null/TestMementifier.cfc | 9 +++ test-harness/tests/full-null/index.cfm | 24 +++++++ .../unit/interceptors/MementifierTest.cfc | 6 ++ 8 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 test-harness/models/PartialMemento.cfc create mode 100644 test-harness/tests/full-null/Application.cfc create mode 100644 test-harness/tests/full-null/PartialMemento.cfc create mode 100644 test-harness/tests/full-null/TestMementifier.cfc create mode 100644 test-harness/tests/full-null/index.cfm diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6535eca..a931224 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,6 +82,11 @@ jobs: box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug curl http://127.0.0.1:60299 + - name: Run Adobe Full Null Regression + if: ${{ matrix.cfengine == 'adobe@2025' }} + run: | + curl --fail-with-body http://127.0.0.1:60299/tests/full-null/index.cfm + - name: Run Tests run: | mkdir -p test-harness/tests/results diff --git a/interceptors/Mementifier.cfc b/interceptors/Mementifier.cfc index 343d181..7cd683a 100644 --- a/interceptors/Mementifier.cfc +++ b/interceptors/Mementifier.cfc @@ -86,8 +86,21 @@ component { arguments.entity.$injectMixin( "$getDeepProperties", variables.$getDeepProperties ); // We do simple date formatters as they are faster than CFML methods - var dateMask = isNull( this.memento.dateMask ) ? variables.settings.dateMask : this.memento.dateMask; - var timeMask = isNull( this.memento.timeMask ) ? variables.settings.timeMask : this.memento.timeMask; + var entityMemento = ( + structKeyExists( arguments.entity, "memento" ) && isStruct( arguments.entity.memento ) + ? arguments.entity.memento + : {} + ); + var dateMask = ( + structKeyExists( entityMemento, "dateMask" ) && !isNull( entityMemento.dateMask ) + ? entityMemento.dateMask + : variables.settings.dateMask + ); + var timeMask = ( + structKeyExists( entityMemento, "timeMask" ) && !isNull( entityMemento.timeMask ) + ? entityMemento.timeMask + : variables.settings.timeMask + ); arguments.entity.$FORMATTER_ISO8601 = variables.jSimpleDateFormat.init( "yyyy-MM-dd'T'HH:mm:ssXXX" ); arguments.entity.$FORMATTER_CUSTOM = variables.jSimpleDateFormat.init( "#dateMask# #timeMask#" ); @@ -145,19 +158,30 @@ component { // Param Default Memento Settings // We do it here, because ACF caches crap! - var thisMemento = { - "autoCastBooleans" : isNull( this.memento.autoCastBooleans ) ? variables.$mementifierSettings.autoCastBooleans : this.memento.autoCastBooleans, - "dateMask" : isNull( this.memento.dateMask ) ? variables.$mementifierSettings.dateMask : this.memento.dateMask, - "defaults" : isNull( this.memento.defaults ) ? {} : this.memento.defaults, - "defaultIncludes" : isNull( this.memento.defaultIncludes ) ? [] : this.memento.defaultIncludes, - "defaultExcludes" : isNull( this.memento.defaultExcludes ) ? [] : this.memento.defaultExcludes, - "iso8601Format" : isNull( this.memento.iso8601Format ) ? variables.$mementifierSettings.iso8601Format : this.memento.iso8601Format, - "mappers" : isNull( this.memento.mappers ) ? {} : this.memento.mappers, - "neverInclude" : isNull( this.memento.neverInclude ) ? [] : this.memento.neverInclude, - "ormAutoIncludes" : isNull( this.memento.ormAutoIncludes ) ? variables.$mementifierSettings.ormAutoIncludes : this.memento.ormAutoIncludes, - "profiles" : isNull( this.memento.profiles ) ? {} : this.memento.profiles, - "timeMask" : isNull( this.memento.timeMask ) ? variables.$mementifierSettings.timeMask : this.memento.timeMask, - "trustedGetters" : isNull( this.memento.trustedGetters ) ? variables.$mementifierSettings.trustedGetters : this.memento.trustedGetters + var entityMemento = structKeyExists( this, "memento" ) && isStruct( this.memento ) ? this.memento : {}; + var thisMemento = { + "autoCastBooleans" : entityMemento.keyExists( "autoCastBooleans" ) && !isNull( + entityMemento.autoCastBooleans + ) ? entityMemento.autoCastBooleans : variables.$mementifierSettings.autoCastBooleans, + "dateMask" : entityMemento.keyExists( "dateMask" ) && !isNull( entityMemento.dateMask ) ? entityMemento.dateMask : variables.$mementifierSettings.dateMask, + "defaults" : entityMemento.keyExists( "defaults" ) && !isNull( entityMemento.defaults ) ? entityMemento.defaults : {}, + "defaultIncludes" : entityMemento.keyExists( "defaultIncludes" ) && !isNull( + entityMemento.defaultIncludes + ) ? entityMemento.defaultIncludes : [], + "defaultExcludes" : entityMemento.keyExists( "defaultExcludes" ) && !isNull( + entityMemento.defaultExcludes + ) ? entityMemento.defaultExcludes : [], + "iso8601Format" : entityMemento.keyExists( "iso8601Format" ) && !isNull( entityMemento.iso8601Format ) ? entityMemento.iso8601Format : variables.$mementifierSettings.iso8601Format, + "mappers" : entityMemento.keyExists( "mappers" ) && !isNull( entityMemento.mappers ) ? entityMemento.mappers : {}, + "neverInclude" : entityMemento.keyExists( "neverInclude" ) && !isNull( entityMemento.neverInclude ) ? entityMemento.neverInclude : [], + "ormAutoIncludes" : entityMemento.keyExists( "ormAutoIncludes" ) && !isNull( + entityMemento.ormAutoIncludes + ) ? entityMemento.ormAutoIncludes : variables.$mementifierSettings.ormAutoIncludes, + "profiles" : entityMemento.keyExists( "profiles" ) && !isNull( entityMemento.profiles ) ? entityMemento.profiles : {}, + "timeMask" : entityMemento.keyExists( "timeMask" ) && !isNull( entityMemento.timeMask ) ? entityMemento.timeMask : variables.$mementifierSettings.timeMask, + "trustedGetters" : entityMemento.keyExists( "trustedGetters" ) && !isNull( + entityMemento.trustedGetters + ) ? entityMemento.trustedGetters : variables.$mementifierSettings.trustedGetters }; // Param arguments according to instance > settings chain precedence @@ -310,7 +334,6 @@ component { reFind( "^\d{4}-\d{2}-\d{2}", thisValue ) // ACF date format begins with YYYY-MM-DD ) ) { - var dateInstance = thisValue; try { @@ -319,7 +342,9 @@ component { // Iso Date? if ( arguments.iso8601Format ) { // we need to convert trailing Zulu time designations offset or JS libs like Moment will not know how to parse it - result[ thisAlias ] = this.$FORMATTER_ISO8601.format( dateInstance ).replace( "Z", "+00:00" ); + result[ thisAlias ] = this.$FORMATTER_ISO8601 + .format( dateInstance ) + .replace( "Z", "+00:00" ); } else { result[ thisAlias ] = customDateFormatter.format( dateInstance ); } @@ -444,7 +469,9 @@ component { * @return The array of default includes for the ORM entity where this function is injected into */ array function $buildOrmIncludes(){ - var thisName = isNull( variables.entityName ) ? "" : variables.entityName; + var thisName = ( + structKeyExists( variables, "entityName" ) && !isNull( variables.entityName ) ? variables.entityName : "" + ); if ( !len( thisName ) ) { var md = getMetadata( this ); thisName = ( md.keyExists( "entityName" ) ? md.entityName : listLast( md.name, "." ) ); diff --git a/test-harness/models/PartialMemento.cfc b/test-harness/models/PartialMemento.cfc new file mode 100644 index 0000000..0166b1d --- /dev/null +++ b/test-harness/models/PartialMemento.cfc @@ -0,0 +1,12 @@ +component accessors="true" { + + property name="id"; + + this.memento = { defaultIncludes : [ "id" ] }; + + function init(){ + variables.id = "partial-memento"; + return this; + } + +} diff --git a/test-harness/tests/full-null/Application.cfc b/test-harness/tests/full-null/Application.cfc new file mode 100644 index 0000000..90b4db2 --- /dev/null +++ b/test-harness/tests/full-null/Application.cfc @@ -0,0 +1,12 @@ +component { + + moduleRoot = createObject( "java", "java.io.File" ) + .init( getDirectoryFromPath( getCurrentTemplatePath() ) & "../../../" ) + .getCanonicalPath(); + + this.name = "mementifier-full-null-regression-#hash( moduleRoot )#"; + this.enableNullSupport = true; + this.mappings[ "/mementifier" ] = moduleRoot; + this.mappings[ "/fullnull" ] = getDirectoryFromPath( getCurrentTemplatePath() ); + +} diff --git a/test-harness/tests/full-null/PartialMemento.cfc b/test-harness/tests/full-null/PartialMemento.cfc new file mode 100644 index 0000000..0166b1d --- /dev/null +++ b/test-harness/tests/full-null/PartialMemento.cfc @@ -0,0 +1,12 @@ +component accessors="true" { + + property name="id"; + + this.memento = { defaultIncludes : [ "id" ] }; + + function init(){ + variables.id = "partial-memento"; + return this; + } + +} diff --git a/test-harness/tests/full-null/TestMementifier.cfc b/test-harness/tests/full-null/TestMementifier.cfc new file mode 100644 index 0000000..0285d42 --- /dev/null +++ b/test-harness/tests/full-null/TestMementifier.cfc @@ -0,0 +1,9 @@ +component extends="mementifier.interceptors.Mementifier" { + + function init( required struct settings ){ + variables.settings = arguments.settings; + configure(); + return this; + } + +} diff --git a/test-harness/tests/full-null/index.cfm b/test-harness/tests/full-null/index.cfm new file mode 100644 index 0000000..98980a7 --- /dev/null +++ b/test-harness/tests/full-null/index.cfm @@ -0,0 +1,24 @@ + + +settings = { + iso8601Format : false, + dateMask : "yyyy-MM-dd", + timeMask : "HH:mm:ss", + ormAutoIncludes : false, + nullDefaultValue : "", + trustedGetters : false, + convertToTimezone : "", + autoCastBooleans : true +}; + +model = new fullnull.PartialMemento(); +interceptor = new fullnull.TestMementifier( settings ); +interceptor.processMemento( model ); +memento = model.getMemento(); + +if ( !memento.keyExists( "id" ) || memento.id != "partial-memento" ) { + throw( type = "RegressionFailure", message = "Partial memento settings failed with full null support." ); +} + +writeOutput( "PASS" ); + diff --git a/test-harness/tests/specs/unit/interceptors/MementifierTest.cfc b/test-harness/tests/specs/unit/interceptors/MementifierTest.cfc index 265f2da..a307cae 100644 --- a/test-harness/tests/specs/unit/interceptors/MementifierTest.cfc +++ b/test-harness/tests/specs/unit/interceptors/MementifierTest.cfc @@ -50,6 +50,12 @@ component extends="coldbox.system.testing.BaseInterceptorTest" interceptor="meme function run(){ describe( "Mementifier", function(){ + it( "supports partial memento settings with full null support", function(){ + var model = getWireBox().getInstance( "PartialMemento" ); + + expect( model.getMemento() ).toBeStruct().toHaveKey( "id" ); + } ); + it( "Won't modify includes/excludes arrays", function(){ var includesList = "userId,blogUrl,fname:firstName,lname:lastName"; var excludesList = "userId,blogUrl,fname:firstName,lname:lastName"; From 8f5c50ea7821c493429fca392d1be55c5e4c0fef Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 22:01:39 -0600 Subject: [PATCH 2/4] ci: initialize the test application --- .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 a931224..1e0cbc4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,7 +80,7 @@ jobs: - name: Start ${{ matrix.cfengine }} Server run: | box server start serverConfigFile="server-${{ matrix.cfengine }}.json" --noSaveSettings --debug - curl http://127.0.0.1:60299 + curl --fail-with-body http://127.0.0.1:60299/tests/index.cfm - name: Run Adobe Full Null Regression if: ${{ matrix.cfengine == 'adobe@2025' }} From af3e55050b23872a9bb98bd4ea2f50fd9573f965 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 22 Aug 2026 22:12:34 -0600 Subject: [PATCH 3/4] ci: resolve Adobe ORM fixture paths --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e0cbc4..3fec47d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,6 +42,12 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 + - name: Create Adobe ORM Component Alias + if: ${{ startsWith( matrix.cfengine, 'adobe@' ) }} + run: | + mkdir -p test-harness/mementifier + ln -s .. test-harness/mementifier/test-harness + - name: Setup Database and Fixtures run: | sudo systemctl start mysql.service From 05ed0a67a5c76e6a46e87d7a4d3a8d77cafaa403 Mon Sep 17 00:00:00 2001 From: Jon Clausen Date: Mon, 24 Aug 2026 13:41:40 -0400 Subject: [PATCH 4/4] fix bad merge --- .github/workflows/tests.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92cfb10..3fec47d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,12 +48,6 @@ jobs: mkdir -p test-harness/mementifier ln -s .. test-harness/mementifier/test-harness - - name: Create Adobe ORM Component Alias - if: ${{ startsWith( matrix.cfengine, 'adobe@' ) }} - run: | - mkdir -p test-harness/mementifier - ln -s .. test-harness/mementifier/test-harness - - name: Setup Database and Fixtures run: | sudo systemctl start mysql.service