diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 0000000..b47207b --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,10 @@ +name: Daily Tests + +on: + schedule: + - cron: '0 0 * * *' # Runs at 00:00 UTC every day + +jobs: + tests: + uses: ./.github/workflows/tests.yml + secrets: inherit diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d78c2d7..b0bf721 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -6,10 +6,8 @@ on: - "main" - "master" - "development" - - "releases/v*" pull_request: branches: - - "releases/v*" - development jobs: @@ -17,12 +15,13 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit - formatCheck: + # Format PR + format_check: name: Checks Source Code Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: Ortus-Solutions/commandbox-action@v1.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 883457c..c9069d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,9 +16,13 @@ on: default: false type: boolean + # Manual Trigger + workflow_dispatch: env: - MODULE_ID: cborm + MODULE_ID: ${{ github.event.repository.name }} + JDK: 21 SNAPSHOT: ${{ inputs.snapshot || false }} + BUILD_ID: ${{ github.run_number }} jobs: ########################################################################################## @@ -26,7 +30,12 @@ jobs: ########################################################################################## build: name: Build & Publish - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 + permissions: + checks: write + pull-requests: write + contents: write + issues: write steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -36,6 +45,12 @@ jobs: with: forgeboxAPIKey: ${{ secrets.FORGEBOX_TOKEN }} + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JDK }} + - name: "Setup Environment Variables For Build Process" id: current_version run: | @@ -50,7 +65,7 @@ jobs: fi - name: Update changelog [unreleased] with latest version - uses: thomaseizinger/keep-a-changelog-new-release@3.0.0 + uses: thomaseizinger/keep-a-changelog-new-release@3.1.0 if: env.SNAPSHOT == 'false' with: changelogPath: ./changelog.md @@ -61,9 +76,9 @@ jobs: npm install -g markdownlint-cli markdownlint changelog.md --fix box install commandbox-docbox - box task run taskfile=build/Build target=run :version=${{ env.VERSION }} :projectName=${{ env.MODULE_ID }} :buildID=${{ github.run_number }} :branch=${{ env.BRANCH }} + box task run taskfile=build/Build target=run :version=${{ env.VERSION }} :projectName=${{ env.MODULE_ID }} :buildID=${{ env.BUILD_ID }} :branch=${{ env.BRANCH }} - - name: Commit Changelog To Master + - name: Commit Changelog [unreleased] with latest version uses: EndBug/add-and-commit@v9.1.4 if: env.SNAPSHOT == 'false' with: @@ -127,14 +142,31 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ref: refs/tags/v${{ env.VERSION }} + - name: Inform Slack + if: ${{ always() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: coding + SLACK_COLOR: ${{ job.status }} # or a specific color like 'green' or '#ff00ff' + SLACK_ICON_EMOJI: ":bell:" + SLACK_MESSAGE: "Module ${{ env.MODULE_ID }} v${{ env.VERSION }} Built with ${{ job.status }}!" + SLACK_TITLE: "ColdBox Module ${{ env.MODULE_ID }}" + SLACK_USERNAME: CI + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + ########################################################################################## # Prep Next Release ########################################################################################## prep_next_release: name: Prep Next Release if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [ build ] + permissions: + checks: write + pull-requests: write + contents: write + issues: write steps: # Checkout development - name: Checkout Repository diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 45d7dd1..50c8392 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -5,6 +5,13 @@ on: branches: - 'development' + workflow_dispatch: + +# Unique group name per workflow-branch/tag combo +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: ########################################################################################## # Module Tests @@ -18,9 +25,12 @@ jobs: ########################################################################################## format: name: Code Auto-Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 + permissions: + contents: write + checks: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Auto-format uses: Ortus-Solutions/commandbox-action@v1.0.2 @@ -28,7 +38,7 @@ jobs: cmd: run-script format - name: Commit Format Changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Apply cfformat changes @@ -39,5 +49,10 @@ jobs: uses: ./.github/workflows/release.yml needs: [ tests, format ] secrets: inherit + permissions: + checks: write + pull-requests: write + contents: write + issues: write with: snapshot: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b8eaab..29b6459 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: jobs: tests: name: Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: DB_USER: root DB_PASSWORD: root @@ -18,9 +18,10 @@ jobs: strategy: fail-fast: false matrix: - cfengine: [ "lucee@5", "adobe@2018", "adobe@2021", "adobe@2023" ] - coldboxVersion: [ "^6.0.0", "^7.0.0" ] + cfengine: [ "lucee@5", "adobe@2021", "adobe@2023" ] + coldboxVersion: [ "^7.0.0" ] experimental: [ false ] + # Experimental: ColdBox BE vs All Engines include: - coldboxVersion: "be" cfengine: "lucee@5" @@ -28,29 +29,32 @@ jobs: - coldboxVersion: "be" cfengine: "lucee@6" experimental: true + - coldboxVersion: "be" + cfengine: "adobe@2021" + experimental: true - coldboxVersion: "be" cfengine: "adobe@2023" experimental: true - coldboxVersion: "be" - cfengine: "adobe@2021" + cfengine: "boxlang-cfml@1" experimental: true steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" - java-version: "11" + java-version: "21" - name: Setup Database and Fixtures run: | - sudo systemctl start mysql.service - # Create Database - mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE coolblog;' - # Import Database - mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < test-harness/tests/resources/coolblog.sql + sudo systemctl start mysql.service + # Create Database + mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE coolblog;' + # Import Database + mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < test-harness/tests/resources/coolblog.sql - name: Setup Environment For Testing Process run: | diff --git a/box.json b/box.json index 0304813..7832e9c 100644 --- a/box.json +++ b/box.json @@ -1,6 +1,6 @@ { "name":"ColdBox ORM Extensions", - "version":"4.5.0", + "version":"4.6.0", "location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cborm/@build.version@/cborm-@build.version@.zip", "author":"Ortus Solutions + * // Example Flow + * user + * .setName( "Luis" ) + * .setAge( 21 ) + * .peek( user => println( user.getName() ) ) + * .setEmail( "lmajano@ortus.com" ) + * + * + * @target The closure to execute with the delegate. We pass the target of the delegate as the first argument. + * + * @return Returns itself + */ + function peek( required target ){ + arguments.target( this ); + return this; + } + + /** + * This function evaluates the target boolean expression and if `true` it will execute the `success` closure + * else, if the `failure` closure is passed, it will execute it. + * + * @target The boolean evaluator, this can be a boolean value + * @success The closure/lambda to execute if the boolean value is true + * @failure The closure/lambda to execute if the boolean value is false + * + * @return Returns itself + */ + function when( + required boolean target, + required success, + failure + ){ + if ( arguments.target ) { + arguments.success(); + } else if ( !isNull( arguments.failure ) ) { + arguments.failure(); + } + return this; + } + + /** + * This function evaluates the target boolean expression and if `false` it will execute the `success` closure + * else, if the `failure` closure is passed, it will execute it. + * + * @target The boolean evaluator, this can be a boolean value + * @success The closure/lambda to execute if the boolean value is true + * @failure The closure/lambda to execute if the boolean value is false + * + * @return Returns itself + */ + function unless( + required boolean target, + required success, + failure + ){ + if ( !arguments.target ) { + arguments.success(); + } else if ( !isNull( arguments.failure ) ) { + arguments.failure(); + } + return this; + } + + /** + * This function evaluates the target boolean expression and if `true` it will throw the controlled exception + * + * @target The boolean evaluator, this can be a boolean value + * @type The exception type + * @message The exception message + * @detail The exception detail + * + * @return Returns itself + */ + function throwIf( + required boolean target, + required type, + message = "", + detail = "" + ){ + if ( arguments.target ) { + throw( + type = arguments.type, + message = arguments.message, + detail = arguments.detail + ); + } + return this; + } + + /** + * This function evaluates the target boolean expression and if `false` it will throw the controlled exception + * + * @target The boolean evaluator, this can be a boolean value + * @type The exception type + * @message The exception message + * @detail The exception detail + * + * @return Returns itself + */ + function throwUnless( + required boolean target, + required type, + message = "", + detail = "" + ){ + if ( !arguments.target ) { + throw( + type = arguments.type, + message = arguments.message, + detail = arguments.detail + ); + } + return this; + } + } diff --git a/models/BaseORMService.cfc b/models/BaseORMService.cfc index f5b73b3..21e3c54 100644 --- a/models/BaseORMService.cfc +++ b/models/BaseORMService.cfc @@ -1201,25 +1201,11 @@ component accessors="true" { * Returns the entity name from a given entity object via session lookup or if new object via metadata lookup * * @entity The entity to get it's name from + * + * @return The entity name */ function getEntityGivenName( required entity ){ - // Short-cut discovery via ActiveEntity - if ( structKeyExists( arguments.entity, "getEntityName" ) ) { - return arguments.entity.getEntityName(); - } - - // Hibernate Discovery - try { - var entityName = getOrm() - .getSession( getOrm().getEntityDatasource( arguments.entity ) ) - .getEntityName( arguments.entity ); - } catch ( org.hibernate.TransientObjectException e ) { - // ignore it, it is not in session, go for long-discovery - } - - // Long Discovery - var md = getMetadata( arguments.entity ); - return ( md.keyExists( "entityName" ) ? md.entityName : listLast( md.name, "." ) ); + return getOrm().getEntityGivenName( arguments.entity ); } /*****************************************************************************************/ diff --git a/models/EventHandler.cfc b/models/EventHandler.cfc index 5bb0ebd..06a31fe 100644 --- a/models/EventHandler.cfc +++ b/models/EventHandler.cfc @@ -17,25 +17,18 @@ component extends="coldbox.system.remote.ColdboxProxy" implements="CFIDE.orm.IEv * preLoad called by hibernate which in turn announces a coldbox interception: ORMPreLoad */ public void function preLoad( any entity ){ - announce( "ORMPreLoad", { entity : arguments.entity } ); + announce( "ORMPreLoad", { "entity" : arguments.entity } ); } /** * postLoad called by hibernate which in turn announces a coldbox interception: ORMPostLoad */ public void function postLoad( any entity ){ - var args = { entity : arguments.entity, entityName : "" }; - - // Short-cut discovery via ActiveEntity - if ( structKeyExists( arguments.entity, "getEntityName" ) ) { - args.entityName = arguments.entity.getEntityName(); - } else { - // it must be in session. - args.entityName = ormGetSession().getEntityName( arguments.entity ); - } - + var args = { + "entity" : arguments.entity, + "entityName" : getOrm().getEntityGivenName( arguments.entity ) + }; processEntityInjection( args.entityName, args.entity ); - announce( "ORMPostLoad", args ); } @@ -43,77 +36,83 @@ component extends="coldbox.system.remote.ColdboxProxy" implements="CFIDE.orm.IEv * postDelete called by hibernate which in turn announces a coldbox interception: ORMPostDelete */ public void function postDelete( any entity ){ - announce( "ORMPostDelete", { entity : arguments.entity } ); + announce( "ORMPostDelete", { "entity" : arguments.entity } ); } /** * preDelete called by hibernate which in turn announces a coldbox interception: ORMPreDelete */ public void function preDelete( any entity ){ - announce( "ORMPreDelete", { entity : arguments.entity } ); + announce( "ORMPreDelete", { "entity" : arguments.entity } ); } /** * preUpdate called by hibernate which in turn announces a coldbox interception: ORMPreUpdate */ public void function preUpdate( any entity, Struct oldData = {} ){ - announce( "ORMPreUpdate", { entity : arguments.entity, oldData : arguments.oldData } ); + announce( + "ORMPreUpdate", + { + "entity" : arguments.entity, + "oldData" : arguments.oldData + } + ); } /** * postUpdate called by hibernate which in turn announces a coldbox interception: ORMPostUpdate */ public void function postUpdate( any entity ){ - announce( "ORMPostUpdate", { entity : arguments.entity } ); + announce( "ORMPostUpdate", { "entity" : arguments.entity } ); } /** * preInsert called by hibernate which in turn announces a coldbox interception: ORMPreInsert */ public void function preInsert( any entity ){ - announce( "ORMPreInsert", { entity : arguments.entity } ); + announce( "ORMPreInsert", { "entity" : arguments.entity } ); } /** * postInsert called by hibernate which in turn announces a coldbox interception: ORMPostInsert */ public void function postInsert( any entity ){ - announce( "ORMPostInsert", { entity : arguments.entity } ); + announce( "ORMPostInsert", { "entity" : arguments.entity } ); } /** * preSave called by ColdBox Base service before save() calls */ public void function preSave( any entity ){ - announce( "ORMPreSave", { entity : arguments.entity } ); + announce( "ORMPreSave", { "entity" : arguments.entity } ); } /** * postSave called by ColdBox Base service after transaction commit or rollback via the save() method */ public void function postSave( any entity ){ - announce( "ORMPostSave", { entity : arguments.entity } ); + announce( "ORMPostSave", { "entity" : arguments.entity } ); } /** * Called before the session is flushed. */ public void function preFlush( any entities ){ - announce( "ORMPreFlush", { entities : arguments.entities } ); + announce( "ORMPreFlush", { "entities" : arguments.entities } ); } /** * Called after the session is flushed. */ public void function postFlush( any entities ){ - announce( "ORMPostFlush", { entities : arguments.entities } ); + announce( "ORMPostFlush", { "entities" : arguments.entities } ); } /** * postNew called by ColdBox which in turn announces a coldbox interception: ORMPostNew */ public void function postNew( any entity, any entityName ){ - var args = { entity : arguments.entity, entityName : "" }; + var args = { "entity" : arguments.entity, "entityName" : "" }; // Do we have an incoming name if ( !isNull( arguments.entityName ) && len( arguments.entityName ) ) { @@ -176,4 +175,16 @@ component extends="coldbox.system.remote.ColdboxProxy" implements="CFIDE.orm.IEv return arguments.entity; } + /** + * Lazy loading of the ORM utility according to the CFML engine you are on + * + * @return cborm.models.util.IORMUtil + */ + private function getOrm(){ + if ( isNull( variables.orm ) ) { + variables.orm = new cborm.models.util.ORMUtilFactory().getORMUtil(); + } + return variables.orm; + } + } diff --git a/models/util/support/ORMUtilSupport.cfc b/models/util/support/ORMUtilSupport.cfc index 38703d0..4288e49 100644 --- a/models/util/support/ORMUtilSupport.cfc +++ b/models/util/support/ORMUtilSupport.cfc @@ -186,4 +186,31 @@ component singleton { .getEntityMode(); } + /** + * Returns the entity name from a given entity object via session lookup or if new object via metadata lookup + * + * @entity The entity to get it's name from + * + * @return The entity name + */ + function getEntityGivenName( required entity ){ + // Short-cut discovery via ActiveEntity + if ( structKeyExists( arguments.entity, "getEntityName" ) ) { + return arguments.entity.getEntityName(); + } + + // Hibernate Discovery + try { + var entityName = getSession( getEntityDatasource( arguments.entity ) ).getEntityName( + arguments.entity + ); + } catch ( org.hibernate.TransientObjectException e ) { + // ignore it, it is not in session, go for long-discovery + } + + // Long Discovery + var md = getMetadata( arguments.entity ); + return ( md.keyExists( "entityName" ) ? md.entityName : listLast( md.name, "." ) ); + } + } diff --git a/server-adobe@2018.json b/server-adobe@2018.json deleted file mode 100644 index e9a3901..0000000 --- a/server-adobe@2018.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name":"cborm-adobe@2018", - "app":{ - "serverHomeDirectory":".engine/adobe2018", - "cfengine":"adobe@2018" - }, - "web":{ - "http":{ - "port":"60299" - }, - "rewrites":{ - "enable":"true" - }, - "webroot":"test-harness", - "aliases":{ - "/moduleroot/cborm":"./" - } - }, - "jvm":{ - "heapSize":"1024" - }, - "openBrowser":"false", - "cfconfig":{ - "file":".cfconfig.json" - } -} diff --git a/server-boxlang-cfml@1.json b/server-boxlang-cfml@1.json new file mode 100644 index 0000000..8931e4b --- /dev/null +++ b/server-boxlang-cfml@1.json @@ -0,0 +1,36 @@ +{ + "app":{ + "cfengine":"boxlang@be", + "serverHomeDirectory":".engine/boxlang" + }, + "name":"cborm-boxlang@1", + "force":true, + "openBrowser":false, + "web":{ + "directoryBrowsing":true, + "http":{ + "port":"60299" + }, + "rewrites":{ + "enable":"true" + }, + "webroot":"test-harness", + "aliases":{ + "/moduleroot/cborm":"./" + } + }, + "JVM":{ + "heapSize":"1024", + "javaVersion":"openjdk21_jre", + "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888 -Dboxlang.debugMode=true" + }, + "cfconfig":{ + "file":".cfconfig.json" + }, + "env":{ + "BOXLANG_DEBUG":true + }, + "scripts":{ + "onServerInitialInstall":"install bx-mysql,bx-derby,bx-compat-cfml,bx-orm --noSave" + } +} diff --git a/test-harness/tests/specs/ActiveEntityTest.cfc b/test-harness/tests/specs/ActiveEntityTest.cfc index b3f46e9..60e07e7 100755 --- a/test-harness/tests/specs/ActiveEntityTest.cfc +++ b/test-harness/tests/specs/ActiveEntityTest.cfc @@ -104,7 +104,7 @@ t = activeUser.findByLastName(); } - function testIsValid(){ + function testIsValidWithFlow(){ r = activeUser.isValid(); assertFalse( r ); @@ -112,6 +112,11 @@ activeUser.setLastName( "Majano" ); activeUser.setUsername( "LuisMajano" ); activeUser.setPassword( "LuisMajano" ); + + activeUser.peek( ( user ) => { + debug( "In Peek" ); + } ); + r = activeUser.isValid(); assertTrue( r ); }