diff --git a/.github/actions/integration-tests/action.yml b/.github/actions/integration-tests/action.yml index fa1dcd2..190a780 100644 --- a/.github/actions/integration-tests/action.yml +++ b/.github/actions/integration-tests/action.yml @@ -35,19 +35,19 @@ runs: # BTP Auth # Uncomment when you want to run hybrid integration tests - # - name: Authenticate with Cloud Foundry - # shell: bash - # run: | - # echo "::debug::CF_API=${{ inputs.CF_API }}" - # for i in {1..3}; do - # cf login -a ${{ inputs.CF_API }} -u ${{ inputs.CF_USERNAME }} -p ${{ inputs.CF_PASSWORD }} -o ${{ inputs.CF_ORG }} -s ${{ inputs.CF_SPACE }} && break - # echo "cf login failed, retrying ($i/3)..." - # sleep 10 - # if [ "$i" -eq 3 ]; then - # echo "❌ cf login failed after 3 attempts." - # exit 1 - # fi - # done + - name: Authenticate with Cloud Foundry + shell: bash + run: | + echo "::debug::CF_API=${{ inputs.CF_API }}" + for i in {1..3}; do + cf login -a ${{ inputs.CF_API }} -u ${{ inputs.CF_USERNAME }} -p ${{ inputs.CF_PASSWORD }} -o ${{ inputs.CF_ORG }} -s ${{ inputs.CF_SPACE }} && break + echo "cf login failed, retrying ($i/3)..." + sleep 10 + if [ "$i" -eq 3 ]; then + echo "❌ cf login failed after 3 attempts." + exit 1 + fi + done - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -55,21 +55,6 @@ runs: with: node-version: ${{ matrix.node-version }} - - name: Set CDS version-specific dependencies - shell: bash - run: | - if [ "${{ matrix.cds-version }}" == "8" ]; then - echo "Installing CDS 8 compatible packages..." - npm pkg set "devDependencies.@cap-js/sqlite=^1.0.0" - npm pkg set "overrides.@cap-js/sqlite=^1.0.0" - npm pkg set "overrides.@cap-js/hana=^1.0.0" - npm pkg set "overrides.@cap-js/db-service=^1.0.0" - cd tests/bookshop - npm pkg set "dependencies.@cap-js/hana=^1.0.0" - npm pkg set "dependencies.@cap-js/db-service=^1.0.0" - cd ../.. - fi - - name: Install global CDS shell: bash run: npm i -g @sap/cds-dk@${{ matrix.cds-version }} @@ -78,49 +63,52 @@ runs: run: | npm install @sap/cds@${{ matrix.cds-version }} npm install + - name: Generate Types for sample project + shell: bash + run: cd tests/bookshop && npm run cds-typer # HANA Cloud Deployment and binding # Uncomment when you want to run your hybrid integration tests against HANA Cloud - # Replace your-project with your actual project name - # - name: Set node env for HANA - # run: echo "NODE_VERSION_HANA=$(echo ${{ inputs.NODE_VERSION }} | tr . _)" >> $GITHUB_ENV - # shell: bash + # Replace process with your actual project name + - name: Set node env for HANA + run: echo "NODE_VERSION_HANA=$(echo ${{ inputs.NODE_VERSION }} | tr . _)" >> $GITHUB_ENV + shell: bash # Deploy model to HANA - # - name: Create HDI Container - # shell: bash - # run: cf create-service hana hdi-shared cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} - # - run: cd tests/bookshop/ && cds deploy --to hana:cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} - # shell: bash - # # Bind HANA Cloud instance - # - run: cds bind db -2 cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} -o package.json - # shell: bash + - name: Create HDI Container + shell: bash + run: cf create-service hana hdi-shared cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} + - run: cd tests/bookshop/ && cds deploy --to hana:cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} + shell: bash + # Bind HANA Cloud instance + - run: cds bind db -2 cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} -o package.json + shell: bash # Bind against BTP services # Uncomment when actual BTP services should be used. The first argument of `cds bind` is only needed for CAP v8 - # - run: cds bind -2 -o package.json - # shell: bash + - run: cds bind process -2 sbpa-service-instance -o package.json + shell: bash # Run tests in hybrid mode - run: npm run test:hybrid shell: bash # HANA Cleanup # Uncomment when you want to run your hybrid integration tests against HANA Cloud - # Replace your-project with your actual project name - # - name: Delete HDI Container Key - # if: ${{ always() }} - # run: cf delete-service-key cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }}-key -f - # shell: bash - # # Somehow first delete always fails with a "ongoing operation on service binding" error - # - name: Delete HDI Container - # if: ${{ always() }} - # shell: bash - # run: | - # for i in {1..3}; do - # cf delete-service cap-js-your-project-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} -f && break - # echo "HDI container delete failed, retrying ($i/3)..." - # sleep 10 - # if [ "$i" -eq 3 ]; then - # echo "❌ HDI container delete failed after 3 attempts." - # exit 1 - # fi - # done + # Replace process with your actual project name + - name: Delete HDI Container Key + if: ${{ always() }} + run: cf delete-service-key cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }}-key -f + shell: bash + # Somehow first delete always fails with a "ongoing operation on service binding" error + - name: Delete HDI Container + if: ${{ always() }} + shell: bash + run: | + for i in {1..3}; do + cf delete-service cap-js-process-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} -f && break + echo "HDI container delete failed, retrying ($i/3)..." + sleep 10 + if [ "$i" -eq 3 ]; then + echo "❌ HDI container delete failed after 3 attempts." + exit 1 + fi + done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5abcdf2..b3da1bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,23 +26,22 @@ jobs: - run: npm run build - run: cd tests/bookshop && npm run build - run: npm run test - # integration-tests: - # runs-on: ubuntu-latest - # strategy: - # fail-fast: false - # matrix: - # node-version: [20.x, 22.x] - # cds-version: [latest, 8] - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - # - name: Integration tests - # uses: ./.github/actions/integration-tests - # with: - # CF_API: ${{ secrets.CF_API }} - # CF_USERNAME: ${{ secrets.CF_USERNAME }} - # CF_PASSWORD: ${{ secrets.CF_PASSWORD }} - # CF_ORG: ${{ secrets.CF_ORG }} - # CF_SPACE: ${{ secrets.CF_SPACE }} - # NODE_VERSION: ${{ matrix.node-version }} - # - run: npm run test + integration-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20.x, 22.x] + cds-version: [latest] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Integration tests + uses: ./.github/actions/integration-tests + with: + CF_API: ${{ secrets.CF_API }} + CF_USERNAME: ${{ secrets.CF_USERNAME }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + NODE_VERSION: ${{ matrix.node-version }} diff --git a/package.json b/package.json index 5a8d0a1..5571fbb 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test": "jest --silent", "test:watch": "jest --watch", "test:integration": "jest tests/integration", + "test:hybrid": "npx jest tests/integration/annotations/lifeCycleAnnotation.test.ts", "build": "npm run cds-build && npm run cds-typer", "cds-build": "cds-tsx build", "cds-typer": "npx @cap-js/cds-typer \"*\" --outputDirectory @cds-models", diff --git a/tests/bookshop/package.json b/tests/bookshop/package.json index 4697be5..cf6ed2f 100644 --- a/tests/bookshop/package.json +++ b/tests/bookshop/package.json @@ -3,7 +3,10 @@ "version": "1.0.0", "description": "A simple CAP project.", "dependencies": { - "@cap-js/process": "file:../../" + "@cap-js/process": "file:../../", + "@sap-cloud-sdk/connectivity": "^4", + "@sap-cloud-sdk/http-client": "^4", + "@sap-cloud-sdk/resilience": "^4" }, "devDependencies": { "@cap-js/sqlite": "^2", @@ -33,6 +36,14 @@ "eu12.bpm-horizon-walkme.sdshipmentprocessor.ShipmentHandlerService": { "kind": "process-service", "model": "srv/external/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler" + }, + "eu12.cdsmunich.capprocesspluginhybridtest.Lifecycle_Test_ProcessService": { + "kind": "external", + "model": "srv/external/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process" + }, + "eu12.cdsmunich.capprocesspluginhybridtest.Programatically_Lifecycle_ProcessService": { + "kind": "external", + "model": "srv/external/eu12.cdsmunich.capprocesspluginhybridtest.programatically_Lifecycle_Process" } } }, diff --git a/tests/bookshop/srv/annotation-service.cds b/tests/bookshop/srv/annotation-service.cds index 785e1ad..a2c51bf 100644 --- a/tests/bookshop/srv/annotation-service.cds +++ b/tests/bookshop/srv/annotation-service.cds @@ -2,1638 +2,1648 @@ using {sap.capire.bookshop as my} from '../db/shipment'; service AnnotationService { - @bpm.process.start: { - id: 'shipmentProcess', - on: 'CREATE', - } - entity Shipments as - projection on my.Shipments { - ID, - status, - shipmentDate, - expectedDelivery, - origin, - destination, - items : Composition of many ShipmentItems - on items.shipment = $self, - totalValue, - } - - entity ShipmentItems as - projection on my.ShipmentItems { - ID, - shipment : Association to Shipments - on shipment.ID = $self.shipment.ID, - title, - quantity, - price, - weight, - } - - @bpm.process.start: { - id: 'shipmentProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.shipmentDate, - { path: $self.origin, as: 'OriginCountry' }, - $self.items - ] - } - entity InputShipments { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - expectedDelivery : Date; - origin : String(200); - destination : String(200); - items : Composition of many InputShipmentItems - on items.shipment = $self; - } - - - entity InputShipmentItems { - key ID : UUID; - shipment : Association to InputShipments - on shipment.ID = $self.shipmentID; - shipmentID : UUID; - title : String(200); - quantity : Integer; - price : Decimal(15, 2); - } - - @bpm.process.start: { - id: 'carProcess', - on: 'CREATE', - if: (mileage > 1000) - } - @bpm.process.cancel: { - on: 'UPDATE', - cascade: false, - if: (mileage > 1000), - } - @bpm.process.businessKey: (ID) - entity CarWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - @bpm.process.start: { - id: 'carProcess', - on: 'DELETE', - } - entity CarOnDel as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // Isolated entities for START annotation tests - // ============================================ - - // Start on CREATE without when condition - @bpm.process.start: { - id: 'startOnCreateProcess', - on: 'CREATE', - } - entity StartOnCreate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on CREATE with if condition - @bpm.process.start: { - id: 'startOnCreateWhenProcess', - on: 'CREATE', - if: (mileage > 500) - } - entity StartOnCreateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on UPDATE without when condition - @bpm.process.start: { - id: 'startOnUpdateProcess', - on: 'UPDATE', - } - entity StartOnUpdate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on UPDATE with if condition - @bpm.process.start: { - id: 'startOnUpdateWhenProcess', - on: 'UPDATE', - if: (mileage > 500) - } - entity StartOnUpdateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on DELETE without when condition - @bpm.process.start: { - id: 'startOnDeleteProcess', - on: 'DELETE', - } - entity StartOnDelete as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on DELETE with if condition - @bpm.process.start: { - id: 'startOnDeleteWhenProcess', - on: 'DELETE', - if: (mileage > 500) - } - entity StartOnDeleteWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // Isolated entities for CANCEL annotation tests - // ============================================ - - // Cancel on CREATE without when condition - @bpm.process.cancel: { - on: 'CREATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity CancelOnCreate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on CREATE with if condition - @bpm.process.cancel: { - on: 'CREATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity CancelOnCreateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on UPDATE without when condition - @bpm.process.cancel: { - on: 'UPDATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity CancelOnUpdate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on UPDATE with if condition - @bpm.process.cancel: { - on: 'UPDATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity CancelOnUpdateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on DELETE without when condition - @bpm.process.cancel: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity CancelOnDelete as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on DELETE with if condition - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity CancelOnDeleteWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // Isolated entities for SUSPEND annotation tests - // ============================================ - - // Suspend on CREATE without when condition - @bpm.process.suspend: { - on: 'CREATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity SuspendOnCreate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on CREATE with if condition - @bpm.process.suspend: { - on: 'CREATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity SuspendOnCreateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on UPDATE without when condition - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity SuspendOnUpdate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on UPDATE with if condition - @bpm.process.suspend: { - on: 'UPDATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity SuspendOnUpdateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on DELETE without when condition - @bpm.process.suspend: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity SuspendOnDelete as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on DELETE with if condition - @bpm.process.suspend: { - on: 'DELETE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity SuspendOnDeleteWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // Isolated entities for RESUME annotation tests - // ============================================ - - // Resume on CREATE without when condition - @bpm.process.resume: { - on: 'CREATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity ResumeOnCreate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on CREATE with if condition - @bpm.process.resume: { - on: 'CREATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity ResumeOnCreateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on UPDATE without when condition - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity ResumeOnUpdate as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on UPDATE with if condition - @bpm.process.resume: { - on: 'UPDATE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity ResumeOnUpdateWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on DELETE without when condition - @bpm.process.resume: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity ResumeOnDelete as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on DELETE with if condition - @bpm.process.resume: { - on: 'DELETE', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity ResumeOnDeleteWhen as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // DEFAULT CASCADE TESTS (cascade omitted, should default to false) - // ============================================ - - // Cancel on CREATE without cascade (should default to false) - @bpm.process.cancel: {on: 'CREATE' } - @bpm.process.businessKey: (ID) - entity CancelOnCreateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on UPDATE without cascade (should default to false) - @bpm.process.cancel: {on: 'UPDATE' } - @bpm.process.businessKey: (ID) - entity CancelOnUpdateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Cancel on DELETE without cascade (should default to false) - @bpm.process.cancel: {on: 'DELETE' } - @bpm.process.businessKey: (ID) - entity CancelOnDeleteDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on CREATE without cascade (should default to false) - @bpm.process.suspend: {on: 'CREATE' } - @bpm.process.businessKey: (ID) - entity SuspendOnCreateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend on UPDATE without cascade (should default to false) - @bpm.process.suspend: {on: 'UPDATE' } - @bpm.process.businessKey: (ID) - entity SuspendOnUpdateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on CREATE without cascade (should default to false) - @bpm.process.resume: {on: 'CREATE' } - @bpm.process.businessKey: (ID) - entity ResumeOnCreateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume on UPDATE without cascade (should default to false) - @bpm.process.resume: {on: 'UPDATE' } - @bpm.process.businessKey: (ID) - entity ResumeOnUpdateDefaultCascade as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // COMBINATION ENTITIES - Real-world scenarios - // ============================================ - - // -------------------------------------------- - // Scenario 1: Basic Workflow Lifecycle - // Start process on CREATE, Cancel on DELETE - // Use case: Order processing, ticket management - // -------------------------------------------- - @bpm.process.start: { - id: 'basicLifecycleProcess', - on: 'CREATE', - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity BasicLifecycle as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // -------------------------------------------- - // Scenario 2: Status-based Cancellation - // Start on CREATE, Cancel on UPDATE when mileage exceeds threshold - // Use case: Auto-cancel workflow when entity reaches terminal state - // -------------------------------------------- - @bpm.process.start: { - id: 'statusCancelProcess', - on: 'CREATE', - } - @bpm.process.cancel: { - on: 'UPDATE', - cascade: false, - if: (mileage > 1000), - } - @bpm.process.businessKey: (ID) - entity StatusBasedCancel as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // -------------------------------------------- - // Scenario 3: Suspend/Resume Workflow - // Start on CREATE, Suspend on UPDATE (if mileage > 500), - // Resume on UPDATE (if mileage <= 500) - // Use case: Pause processing when item is on hold - // -------------------------------------------- - @bpm.process.start: { - id: 'suspendResumeProcess', - on: 'CREATE', - } - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - if: (mileage > 500), - } - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - if: (mileage <= 500), - } - @bpm.process.businessKey: (ID) - entity SuspendResumeWorkflow as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // -------------------------------------------- - // Scenario 4: Full Lifecycle Management - // Start on CREATE, Suspend/Resume on UPDATE, Cancel on DELETE - // Use case: Complete workflow control with pause capability - // -------------------------------------------- - @bpm.process.start: { - id: 'fullLifecycleProcess', - on: 'CREATE', - } - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - if: (mileage > 800), - } - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - if: (mileage <= 800), - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity FullLifecycle as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // -------------------------------------------- - // Scenario 5: Conditional Start and Cancel - // Start on UPDATE when condition met, Cancel on UPDATE when different condition - // Use case: Workflow triggered by status change, cancelled by another status - // -------------------------------------------- - @bpm.process.start: { - id: 'conditionalStartCancelProcess', - on: 'UPDATE', - if: (mileage > 500) - } - @bpm.process.cancel: { - on: 'UPDATE', - cascade: false, - if: (mileage > 1500), - } - @bpm.process.businessKey: (ID) - entity ConditionalStartCancel as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // -------------------------------------------- - // Scenario 6: External Workflow Management - // No start annotation - workflow started externally - // Suspend/Resume on UPDATE, Cancel on DELETE - // Use case: Entity linked to externally triggered workflow - // -------------------------------------------- - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - if: (mileage > 500), - } - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - if: (mileage <= 500), - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity ExternalWorkflowManagement as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // ============================================ - // MULTI-EVENT DELETE COMBINATIONS - // Testing multiple process events triggered by a single DELETE - // ============================================ - - // -------------------------------------------- - // Start + Cancel on DELETE - // -------------------------------------------- - @bpm.process.start: { - id: 'deleteStartCancelProcess', - on: 'DELETE', - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity DeleteStartCancel as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // Start + Resume on DELETE - // -------------------------------------------- - @bpm.process.start: { - id: 'deleteStartResumeProcess', - on: 'DELETE', - } - @bpm.process.resume: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity DeleteStartResume as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // Cancel + Resume on DELETE - // -------------------------------------------- - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.resume: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity DeleteCancelResume as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // Cancel + Suspend on DELETE - // -------------------------------------------- - @bpm.process.cancel: { - on: 'DELETE', - cascade: false, - } - @bpm.process.suspend: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity DeleteCancelSuspend as projection on my.Car { - ID, model, manufacturer, mileage, year - } - // -------------------------------------------- - // Cancel + Suspend on DELETE with if condition - // -------------------------------------------- - @bpm.process.cancel: { - on: 'DELETE', - cascade: false, - if: (mileage > 500) - } - @bpm.process.suspend: { - on: 'DELETE', - cascade: true, - if: (mileage <= 500) - } - @bpm.process.businessKey: (ID) - entity DeleteCancelSuspendIfExpr as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // Start + Cancel + Resume on DELETE (LocalTestService pattern) - // -------------------------------------------- - @bpm.process.start: { - id: 'deleteStartCancelResumeProcess', - on: 'DELETE', - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.resume: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity DeleteStartCancelResume as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // All four events on DELETE - // -------------------------------------------- - @bpm.process.start: { - id: 'deleteAllEventsProcess', - on: 'DELETE', - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.suspend: { - on: 'DELETE', - cascade: false, - } - @bpm.process.resume: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (ID) - entity DeleteAllEvents as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // -------------------------------------------- - // Start with inputs + Cancel on DELETE - // -------------------------------------------- - @bpm.process.start: { - id: 'deleteStartInputsCancelProcess', - on: 'DELETE', - inputs: [ - { path: $self.model, as: 'CarModel' }, - { path: $self.manufacturer, as: 'CarMaker' } - ] - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity DeleteStartInputsCancel as projection on my.Car { - ID, model, manufacturer, mileage, year - } - - // ============================================ - // START INPUT ANNOTATION TESTS - // Testing inputs array in @bpm.process.start - // ============================================ - - // -------------------------------------------- - // Test 1: No inputs specified - // All entity fields should be included in context - // -------------------------------------------- - @bpm.process.start: { - id: 'startNoInputProcess', - on: 'CREATE', - } - entity StartNoInput as - projection on my.Shipments { - ID, - status, - shipmentDate, - expectedDelivery, - origin, - destination, - totalValue, - notes - } - - // -------------------------------------------- - // Test 2: With inputs array on selected fields - // Only specified fields should be included in context - // -------------------------------------------- - @bpm.process.start: { - id: 'startSelectedInputProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.shipmentDate, - $self.origin - ] - } - entity StartSelectedInput { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - expectedDelivery : Date; - origin : String(200); - destination : String(200); - totalValue : Decimal(15, 2); - } - - // -------------------------------------------- - // Test 3: With inputs array with custom aliases - // Fields should be renamed in context - // -------------------------------------------- - @bpm.process.start: { - id: 'startAliasInputProcess', - on: 'CREATE', - inputs: [ - $self.ID, - { path: $self.shipmentDate, as: 'ProcessStartDate' }, - { path: $self.origin, as: 'SourceLocation' }, - { path: $self.destination, as: 'TargetLocation' }, - { path: $self.totalValue, as: 'Amount' } - ] - } - entity StartAliasInput { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - expectedDelivery : Date; - origin : String(200); - destination : String(200); - totalValue : Decimal(15, 2); - } - - // -------------------------------------------- - // Test 4: With nested Composition in inputs - // Include composition items in context (all fields) - // -------------------------------------------- - @bpm.process.start: { - id: 'startNestedCompositionProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.shipmentDate, - $self.items - ] - } - entity StartNestedComposition { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - items : Composition of many StartNestedCompositionItems - on items.parent = $self; - } - - entity StartNestedCompositionItems { - key ID : UUID; - parent : Association to StartNestedComposition - on parent.ID = $self.parentID; - parentID : UUID; - title : String(200); - quantity : Integer; - price : Decimal(15, 2); - } - - // -------------------------------------------- - // Test 5: With nested Composition - selected child fields - // Include only selected fields from composition items - // -------------------------------------------- - @bpm.process.start: { - id: 'startNestedSelectedProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.shipmentDate, - $self.items.ID, - $self.items.title, - $self.items.price - ] - } - entity StartNestedSelected { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - items : Composition of many StartNestedSelectedItems - on items.parent = $self; - } - - entity StartNestedSelectedItems { - key ID : UUID; - parent : Association to StartNestedSelected; - title : String(200); - quantity : Integer; - price : Decimal(15, 2); - } - - // -------------------------------------------- - // Test 6: With nested Composition and aliases - // Child fields should be renamed in context - // -------------------------------------------- - @bpm.process.start: { - id: 'startNestedAliasProcess', - on: 'CREATE', - inputs: [ - $self.ID, - { path: $self.orderDate, as: 'ProcessDate' }, - { path: $self.items, as: 'OrderLines' }, - $self.items.ID, - { path: $self.items.productName, as: 'Product' }, - { path: $self.items.quantity, as: 'Qty' }, - { path: $self.items.unitPrice, as: 'Price' } - ] - } - entity StartNestedAlias { - key ID : UUID; - status : String(20) default 'PENDING'; - orderDate : Date; - items : Composition of many StartNestedAliasItems - on items.parent = $self; - } - - entity StartNestedAliasItems { - key ID : UUID; - parent : Association to StartNestedAlias; - productName : String(200); - quantity : Integer; - unitPrice : Decimal(15, 2); - } - - // ============================================ - // CUSTOM EVENT / BOUND ACTION TESTS - // Testing process annotations with custom events (bound actions) - // ============================================ - - // -------------------------------------------- - // Start process on bound action (no condition) - // -------------------------------------------- - @bpm.process.start: { - id: 'startOnActionProcess', - on: 'triggerStart', - } - entity StartOnAction as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerStart() returns StartOnAction; - } - - // -------------------------------------------- - // Start process on bound action with condition - // -------------------------------------------- - @bpm.process.start: { - id: 'startOnActionWhenProcess', - on: 'triggerStartWhen', - if: (mileage > 500) - } - entity StartOnActionWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerStartWhen() returns StartOnActionWhen; - } - - // -------------------------------------------- - // Cancel process on bound action (no condition) - // -------------------------------------------- - @bpm.process.cancel: { - on: 'triggerCancel', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity CancelOnAction as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerCancel() returns CancelOnAction; - } - - // -------------------------------------------- - // Cancel process on bound action with condition - // -------------------------------------------- - @bpm.process.cancel: { - on: 'triggerCancelWhen', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity CancelOnActionWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerCancelWhen() returns CancelOnActionWhen; - } - - // -------------------------------------------- - // Suspend process on bound action (no condition) - // -------------------------------------------- - @bpm.process.suspend: { - on: 'triggerSuspend', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity SuspendOnAction as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerSuspend() returns SuspendOnAction; - } - - // -------------------------------------------- - // Suspend process on bound action with condition - // -------------------------------------------- - @bpm.process.suspend: { - on: 'triggerSuspendWhen', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity SuspendOnActionWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerSuspendWhen() returns SuspendOnActionWhen; - } - - // -------------------------------------------- - // Resume process on bound action (no condition) - // -------------------------------------------- - @bpm.process.resume: { - on: 'triggerResume', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity ResumeOnAction as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerResume() returns ResumeOnAction; - } - - // -------------------------------------------- - // Resume process on bound action with condition - // -------------------------------------------- - @bpm.process.resume: { - on: 'triggerResumeWhen', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity ResumeOnActionWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerResumeWhen() returns ResumeOnActionWhen; - } - - // ============================================ - // WILDCARD EVENT TESTS - // Testing process annotations with '*' to trigger on all events - // ============================================ - - // -------------------------------------------- - // Start process on wildcard '*' (all CUD events + bound actions) - // -------------------------------------------- - @bpm.process.start: { - id: 'startOnWildcardProcess', - on: '*', - } - entity StartOnWildcard as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns StartOnWildcard; - } - - // -------------------------------------------- - // Start process on wildcard '*' with condition - // -------------------------------------------- - @bpm.process.start: { - id: 'startOnWildcardWhenProcess', - on: '*', - if: (mileage > 500) - } - entity StartOnWildcardWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns StartOnWildcardWhen; - } - - // -------------------------------------------- - // Cancel process on wildcard '*' (all CUD events + bound actions) - // -------------------------------------------- - @bpm.process.cancel: { - on: '*', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity CancelOnWildcard as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns CancelOnWildcard; - } - - // -------------------------------------------- - // Suspend process on wildcard '*' (all CUD events + bound actions) - // -------------------------------------------- - @bpm.process.suspend: { - on: '*', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity SuspendOnWildcard as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns SuspendOnWildcard; - } - - // -------------------------------------------- - // Resume process on wildcard '*' (all CUD events + bound actions) - // -------------------------------------------- - @bpm.process.resume: { - on: '*', - cascade: false, - } - @bpm.process.businessKey: (ID) - entity ResumeOnWildcard as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns ResumeOnWildcard; - } - - // -------------------------------------------- - // Wildcard with condition - // -------------------------------------------- - @bpm.process.cancel: { - on: '*', - cascade: true, - if: (mileage > 500), - } - @bpm.process.businessKey: (ID) - entity CancelOnWildcardWhen as projection on my.Car { - ID, model, manufacturer, mileage, year - } actions { - action triggerAction() returns CancelOnWildcardWhen; - } - - // -------------------------------------------- - // Test 7: Deep cyclic path in inputs array - // Demonstrates that explicit paths avoid cycle issues - // -------------------------------------------- - @bpm.process.start: { - id: 'startCyclicPathProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.status, - $self.items.ID, - $self.items.title, - $self.items.shipment.ID, - $self.items.shipment.status, - $self.items.shipment.items.ID, - $self.items.shipment.items.title, - $self.items.shipment.items.shipment.ID - ] - } - entity StartCyclicPath { - key ID : UUID; - status : String(20) default 'PENDING'; - items : Composition of many StartCyclicPathItems - on items.shipment = $self; - } - - entity StartCyclicPathItems { - key ID : UUID; - shipment : Association to StartCyclicPath; - title : String(200); - } - - // -------------------------------------------- - // Test 8: $self wildcard - all scalar fields - // Using $self alone to include all scalar fields plus composition - // -------------------------------------------- - @bpm.process.start: { - id: 'startSelfWildcardProcess', - on: 'CREATE', - inputs: [ - $self, // All scalar fields of the entity - $self.items // Plus the composition with all its scalar fields - ] - } - entity StartSelfWildcard { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - totalValue : Decimal(15, 2); - items : Composition of many StartSelfWildcardItems - on items.parent = $self; - } - - entity StartSelfWildcardItems { - key ID : UUID; - parent : Association to StartSelfWildcard; - title : String(200); - quantity : Integer; - } - - // Test 9: $self wildcard with field alias override - // $self expands all scalar fields, but $self.ID with alias should rename ID to OrderId - // -------------------------------------------- - @bpm.process.start: { - id: 'startSelfWildcardAliasProcess', - on: 'CREATE', - inputs: [ - $self, // All scalar fields: ID, status, shipmentDate, totalValue - $self.items, // Composition with all its scalar fields - { path: $self.ID, as: 'OrderId' } // Rename ID to OrderId - ] - } - entity StartSelfWildcardAlias { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - totalValue : Decimal(15, 2); - items : Composition of many StartSelfWildcardAliasItems - on items.parent = $self; - } - - entity StartSelfWildcardAliasItems { - key ID : UUID; - parent : Association to StartSelfWildcardAlias; - title : String(200); - quantity : Integer; - } - - // Test 10: $self.items (composition wildcard) with child field alias - // $self.items expands all child fields, but $self.items.ID with alias should add ItemId - // -------------------------------------------- - @bpm.process.start: { - id: 'startCompositionWildcardAliasProcess', - on: 'CREATE', - inputs: [ - $self.ID, - $self.status, - $self.items, // All scalar fields of items: ID, title, quantity, parent_ID - { path: $self.items.ID, as: 'ItemId' } // Rename items.ID to ItemId (adds second copy) - ] - } - entity StartCompositionWildcardAlias { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - totalValue : Decimal(15, 2); - items : Composition of many StartCompositionWildcardAliasItems + @bpm.process.start: { + id: 'shipmentProcess', + on: 'CREATE', + } + entity Shipments as + projection on my.Shipments { + ID, + status, + shipmentDate, + expectedDelivery, + origin, + destination, + items : Composition of many ShipmentItems + on items.shipment = $self, + totalValue, + } + + entity ShipmentItems as + projection on my.ShipmentItems { + ID, + shipment : Association to Shipments + on shipment.ID = $self.shipment.ID, + title, + quantity, + price, + weight, + } + + @bpm.process.start: { + id : 'shipmentProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.shipmentDate, + { + path: $self.origin, + as : 'OriginCountry' + }, + $self.items + ] + } + entity InputShipments { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + expectedDelivery : Date; + origin : String(200); + destination : String(200); + items : Composition of many InputShipmentItems + on items.shipment = $self; + } + + + entity InputShipmentItems { + key ID : UUID; + shipment : Association to InputShipments + on shipment.ID = $self.shipmentID; + shipmentID : UUID; + title : String(200); + quantity : Integer; + price : Decimal(15, 2); + } + + @bpm.process.start : { + id: 'carProcess', + on: 'CREATE', + if: (mileage > 1000) + } + @bpm.process.cancel : { + on : 'UPDATE', + cascade: false, + if : (mileage > 1000), + } + @bpm.process.businessKey: (ID) + entity CarWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + @bpm.process.start: { + id: 'carProcess', + on: 'DELETE', + } + entity CarOnDel as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // Isolated entities for START annotation tests + // ============================================ + + // Start on CREATE without when condition + @bpm.process.start: { + id: 'startOnCreateProcess', + on: 'CREATE', + } + entity StartOnCreate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on CREATE with if condition + @bpm.process.start: { + id: 'startOnCreateWhenProcess', + on: 'CREATE', + if: (mileage > 500) + } + entity StartOnCreateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on UPDATE without when condition + @bpm.process.start: { + id: 'startOnUpdateProcess', + on: 'UPDATE', + } + entity StartOnUpdate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on UPDATE with if condition + @bpm.process.start: { + id: 'startOnUpdateWhenProcess', + on: 'UPDATE', + if: (mileage > 500) + } + entity StartOnUpdateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on DELETE without when condition + @bpm.process.start: { + id: 'startOnDeleteProcess', + on: 'DELETE', + } + entity StartOnDelete as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on DELETE with if condition + @bpm.process.start: { + id: 'startOnDeleteWhenProcess', + on: 'DELETE', + if: (mileage > 500) + } + entity StartOnDeleteWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // Isolated entities for CANCEL annotation tests + // ============================================ + + // Cancel on CREATE without when condition + @bpm.process.cancel : { + on : 'CREATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity CancelOnCreate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on CREATE with if condition + @bpm.process.cancel : { + on : 'CREATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity CancelOnCreateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on UPDATE without when condition + @bpm.process.cancel : { + on : 'UPDATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity CancelOnUpdate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on UPDATE with if condition + @bpm.process.cancel : { + on : 'UPDATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity CancelOnUpdateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on DELETE without when condition + @bpm.process.cancel : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity CancelOnDelete as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on DELETE with if condition + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity CancelOnDeleteWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // Isolated entities for SUSPEND annotation tests + // ============================================ + + // Suspend on CREATE without when condition + @bpm.process.suspend : { + on : 'CREATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity SuspendOnCreate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on CREATE with if condition + @bpm.process.suspend : { + on : 'CREATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity SuspendOnCreateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on UPDATE without when condition + @bpm.process.suspend : { + on : 'UPDATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity SuspendOnUpdate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on UPDATE with if condition + @bpm.process.suspend : { + on : 'UPDATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity SuspendOnUpdateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on DELETE without when condition + @bpm.process.suspend : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity SuspendOnDelete as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on DELETE with if condition + @bpm.process.suspend : { + on : 'DELETE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity SuspendOnDeleteWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // Isolated entities for RESUME annotation tests + // ============================================ + + // Resume on CREATE without when condition + @bpm.process.resume : { + on : 'CREATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity ResumeOnCreate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on CREATE with if condition + @bpm.process.resume : { + on : 'CREATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity ResumeOnCreateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on UPDATE without when condition + @bpm.process.resume : { + on : 'UPDATE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity ResumeOnUpdate as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on UPDATE with if condition + @bpm.process.resume : { + on : 'UPDATE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity ResumeOnUpdateWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on DELETE without when condition + @bpm.process.resume : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity ResumeOnDelete as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on DELETE with if condition + @bpm.process.resume : { + on : 'DELETE', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity ResumeOnDeleteWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // DEFAULT CASCADE TESTS (cascade omitted, should default to false) + // ============================================ + + // Cancel on CREATE without cascade (should default to false) + @bpm.process.cancel : {on: 'CREATE'} + @bpm.process.businessKey: (ID) + entity CancelOnCreateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on UPDATE without cascade (should default to false) + @bpm.process.cancel : {on: 'UPDATE'} + @bpm.process.businessKey: (ID) + entity CancelOnUpdateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Cancel on DELETE without cascade (should default to false) + @bpm.process.cancel : {on: 'DELETE'} + @bpm.process.businessKey: (ID) + entity CancelOnDeleteDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on CREATE without cascade (should default to false) + @bpm.process.suspend : {on: 'CREATE'} + @bpm.process.businessKey: (ID) + entity SuspendOnCreateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend on UPDATE without cascade (should default to false) + @bpm.process.suspend : {on: 'UPDATE'} + @bpm.process.businessKey: (ID) + entity SuspendOnUpdateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on CREATE without cascade (should default to false) + @bpm.process.resume : {on: 'CREATE'} + @bpm.process.businessKey: (ID) + entity ResumeOnCreateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume on UPDATE without cascade (should default to false) + @bpm.process.resume : {on: 'UPDATE'} + @bpm.process.businessKey: (ID) + entity ResumeOnUpdateDefaultCascade as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + + // ============================================ + // MULTI-EVENT DELETE COMBINATIONS + // Testing multiple process events triggered by a single DELETE + // ============================================ + + // -------------------------------------------- + // Start + Cancel on DELETE + // -------------------------------------------- + @bpm.process.start : { + id: 'deleteStartCancelProcess', + on: 'DELETE', + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + } + @bpm.process.businessKey: (ID) + entity DeleteStartCancel as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Start + Resume on DELETE + // -------------------------------------------- + @bpm.process.start : { + id: 'deleteStartResumeProcess', + on: 'DELETE', + } + @bpm.process.resume : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity DeleteStartResume as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Cancel + Resume on DELETE + // -------------------------------------------- + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + } + @bpm.process.resume : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity DeleteCancelResume as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Cancel + Suspend on DELETE + // -------------------------------------------- + @bpm.process.cancel : { + on : 'DELETE', + cascade: false, + } + @bpm.process.suspend : { + on : 'DELETE', + cascade: true, + } + @bpm.process.businessKey: (ID) + entity DeleteCancelSuspend as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Cancel + Suspend on DELETE with if condition + // -------------------------------------------- + @bpm.process.cancel : { + on : 'DELETE', + cascade: false, + if : (mileage > 500) + } + @bpm.process.suspend : { + on : 'DELETE', + cascade: true, + if : (mileage <= 500) + } + @bpm.process.businessKey: (ID) + entity DeleteCancelSuspendIfExpr as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Start + Cancel + Resume on DELETE (LocalTestService pattern) + // -------------------------------------------- + @bpm.process.start : { + id: 'deleteStartCancelResumeProcess', + on: 'DELETE', + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + } + @bpm.process.resume : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity DeleteStartCancelResume as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // All four events on DELETE + // -------------------------------------------- + @bpm.process.start : { + id: 'deleteAllEventsProcess', + on: 'DELETE', + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + } + @bpm.process.suspend : { + on : 'DELETE', + cascade: false, + } + @bpm.process.resume : { + on : 'DELETE', + cascade: true, + } + @bpm.process.businessKey: (ID) + entity DeleteAllEvents as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Start with inputs + Cancel on DELETE + // -------------------------------------------- + @bpm.process.start : { + id : 'deleteStartInputsCancelProcess', + on : 'DELETE', + inputs: [ + { + path: $self.model, + as : 'CarModel' + }, + { + path: $self.manufacturer, + as : 'CarMaker' + } + ] + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity DeleteStartInputsCancel as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // ============================================ + // START INPUT ANNOTATION TESTS + // Testing inputs array in @bpm.process.start + // ============================================ + + // -------------------------------------------- + // Test 1: No inputs specified + // All entity fields should be included in context + // -------------------------------------------- + @bpm.process.start: { + id: 'startNoInputProcess', + on: 'CREATE', + } + entity StartNoInput as + projection on my.Shipments { + ID, + status, + shipmentDate, + expectedDelivery, + origin, + destination, + totalValue, + notes + } + + // -------------------------------------------- + // Test 2: With inputs array on selected fields + // Only specified fields should be included in context + // -------------------------------------------- + @bpm.process.start: { + id : 'startSelectedInputProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.shipmentDate, + $self.origin + ] + } + entity StartSelectedInput { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + expectedDelivery : Date; + origin : String(200); + destination : String(200); + totalValue : Decimal(15, 2); + } + + // -------------------------------------------- + // Test 3: With inputs array with custom aliases + // Fields should be renamed in context + // -------------------------------------------- + @bpm.process.start: { + id : 'startAliasInputProcess', + on : 'CREATE', + inputs: [ + $self.ID, + { + path: $self.shipmentDate, + as : 'ProcessStartDate' + }, + { + path: $self.origin, + as : 'SourceLocation' + }, + { + path: $self.destination, + as : 'TargetLocation' + }, + { + path: $self.totalValue, + as : 'Amount' + } + ] + } + entity StartAliasInput { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + expectedDelivery : Date; + origin : String(200); + destination : String(200); + totalValue : Decimal(15, 2); + } + + // -------------------------------------------- + // Test 4: With nested Composition in inputs + // Include composition items in context (all fields) + // -------------------------------------------- + @bpm.process.start: { + id : 'startNestedCompositionProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.shipmentDate, + $self.items + ] + } + entity StartNestedComposition { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + items : Composition of many StartNestedCompositionItems + on items.parent = $self; + } + + entity StartNestedCompositionItems { + key ID : UUID; + parent : Association to StartNestedComposition + on parent.ID = $self.parentID; + parentID : UUID; + title : String(200); + quantity : Integer; + price : Decimal(15, 2); + } + + // -------------------------------------------- + // Test 5: With nested Composition - selected child fields + // Include only selected fields from composition items + // -------------------------------------------- + @bpm.process.start: { + id : 'startNestedSelectedProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.shipmentDate, + $self.items.ID, + $self.items.title, + $self.items.price + ] + } + entity StartNestedSelected { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + items : Composition of many StartNestedSelectedItems + on items.parent = $self; + } + + entity StartNestedSelectedItems { + key ID : UUID; + parent : Association to StartNestedSelected; + title : String(200); + quantity : Integer; + price : Decimal(15, 2); + } + + // -------------------------------------------- + // Test 6: With nested Composition and aliases + // Child fields should be renamed in context + // -------------------------------------------- + @bpm.process.start: { + id : 'startNestedAliasProcess', + on : 'CREATE', + inputs: [ + $self.ID, + { + path: $self.orderDate, + as : 'ProcessDate' + }, + { + path: $self.items, + as : 'OrderLines' + }, + $self.items.ID, + { + path: $self.items.productName, + as : 'Product' + }, + { + path: $self.items.quantity, + as : 'Qty' + }, + { + path: $self.items.unitPrice, + as : 'Price' + } + ] + } + entity StartNestedAlias { + key ID : UUID; + status : String(20) default 'PENDING'; + orderDate : Date; + items : Composition of many StartNestedAliasItems + on items.parent = $self; + } + + entity StartNestedAliasItems { + key ID : UUID; + parent : Association to StartNestedAlias; + productName : String(200); + quantity : Integer; + unitPrice : Decimal(15, 2); + } + + // ============================================ + // CUSTOM EVENT / BOUND ACTION TESTS + // Testing process annotations with custom events (bound actions) + // ============================================ + + // -------------------------------------------- + // Start process on bound action (no condition) + // -------------------------------------------- + @bpm.process.start: { + id: 'startOnActionProcess', + on: 'triggerStart', + } + entity StartOnAction as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerStart() returns StartOnAction; + } + + // -------------------------------------------- + // Start process on bound action with condition + // -------------------------------------------- + @bpm.process.start: { + id: 'startOnActionWhenProcess', + on: 'triggerStartWhen', + if: (mileage > 500) + } + entity StartOnActionWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerStartWhen() returns StartOnActionWhen; + } + + // -------------------------------------------- + // Cancel process on bound action (no condition) + // -------------------------------------------- + @bpm.process.cancel : { + on : 'triggerCancel', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity CancelOnAction as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerCancel() returns CancelOnAction; + } + + // -------------------------------------------- + // Cancel process on bound action with condition + // -------------------------------------------- + @bpm.process.cancel : { + on : 'triggerCancelWhen', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity CancelOnActionWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerCancelWhen() returns CancelOnActionWhen; + } + + // -------------------------------------------- + // Suspend process on bound action (no condition) + // -------------------------------------------- + @bpm.process.suspend : { + on : 'triggerSuspend', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity SuspendOnAction as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerSuspend() returns SuspendOnAction; + } + + // -------------------------------------------- + // Suspend process on bound action with condition + // -------------------------------------------- + @bpm.process.suspend : { + on : 'triggerSuspendWhen', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity SuspendOnActionWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerSuspendWhen() returns SuspendOnActionWhen; + } + + // -------------------------------------------- + // Resume process on bound action (no condition) + // -------------------------------------------- + @bpm.process.resume : { + on : 'triggerResume', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity ResumeOnAction as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerResume() returns ResumeOnAction; + } + + // -------------------------------------------- + // Resume process on bound action with condition + // -------------------------------------------- + @bpm.process.resume : { + on : 'triggerResumeWhen', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity ResumeOnActionWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerResumeWhen() returns ResumeOnActionWhen; + } + + // ============================================ + // WILDCARD EVENT TESTS + // Testing process annotations with '*' to trigger on all events + // ============================================ + + // -------------------------------------------- + // Start process on wildcard '*' (all CUD events + bound actions) + // -------------------------------------------- + @bpm.process.start: { + id: 'startOnWildcardProcess', + on: '*', + } + entity StartOnWildcard as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns StartOnWildcard; + } + + // -------------------------------------------- + // Start process on wildcard '*' with condition + // -------------------------------------------- + @bpm.process.start: { + id: 'startOnWildcardWhenProcess', + on: '*', + if: (mileage > 500) + } + entity StartOnWildcardWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns StartOnWildcardWhen; + } + + // -------------------------------------------- + // Cancel process on wildcard '*' (all CUD events + bound actions) + // -------------------------------------------- + @bpm.process.cancel : { + on : '*', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity CancelOnWildcard as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns CancelOnWildcard; + } + + // -------------------------------------------- + // Suspend process on wildcard '*' (all CUD events + bound actions) + // -------------------------------------------- + @bpm.process.suspend : { + on : '*', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity SuspendOnWildcard as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns SuspendOnWildcard; + } + + // -------------------------------------------- + // Resume process on wildcard '*' (all CUD events + bound actions) + // -------------------------------------------- + @bpm.process.resume : { + on : '*', + cascade: false, + } + @bpm.process.businessKey: (ID) + entity ResumeOnWildcard as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns ResumeOnWildcard; + } + + // -------------------------------------------- + // Wildcard with condition + // -------------------------------------------- + @bpm.process.cancel : { + on : '*', + cascade: true, + if : (mileage > 500), + } + @bpm.process.businessKey: (ID) + entity CancelOnWildcardWhen as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + actions { + action triggerAction() returns CancelOnWildcardWhen; + } + + // -------------------------------------------- + // Test 7: Deep cyclic path in inputs array + // Demonstrates that explicit paths avoid cycle issues + // -------------------------------------------- + @bpm.process.start: { + id : 'startCyclicPathProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.status, + $self.items.ID, + $self.items.title, + $self.items.shipment.ID, + $self.items.shipment.status, + $self.items.shipment.items.ID, + $self.items.shipment.items.title, + $self.items.shipment.items.shipment.ID + ] + } + entity StartCyclicPath { + key ID : UUID; + status : String(20) default 'PENDING'; + items : Composition of many StartCyclicPathItems + on items.shipment = $self; + } + + entity StartCyclicPathItems { + key ID : UUID; + shipment : Association to StartCyclicPath; + title : String(200); + } + + // -------------------------------------------- + // Test 8: $self wildcard - all scalar fields + // Using $self alone to include all scalar fields plus composition + // -------------------------------------------- + @bpm.process.start: { + id : 'startSelfWildcardProcess', + on : 'CREATE', + inputs: [ + $self, // All scalar fields of the entity + $self.items // Plus the composition with all its scalar fields + ] + } + entity StartSelfWildcard { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + totalValue : Decimal(15, 2); + items : Composition of many StartSelfWildcardItems + on items.parent = $self; + } + + entity StartSelfWildcardItems { + key ID : UUID; + parent : Association to StartSelfWildcard; + title : String(200); + quantity : Integer; + } + + // Test 9: $self wildcard with field alias override + // $self expands all scalar fields, but $self.ID with alias should rename ID to OrderId + // -------------------------------------------- + @bpm.process.start: { + id : 'startSelfWildcardAliasProcess', + on : 'CREATE', + inputs: [ + $self, // All scalar fields: ID, status, shipmentDate, totalValue + $self.items, // Composition with all its scalar fields + { + path: $self.ID, + as : 'OrderId' + } // Rename ID to OrderId + ] + } + entity StartSelfWildcardAlias { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + totalValue : Decimal(15, 2); + items : Composition of many StartSelfWildcardAliasItems + on items.parent = $self; + } + + entity StartSelfWildcardAliasItems { + key ID : UUID; + parent : Association to StartSelfWildcardAlias; + title : String(200); + quantity : Integer; + } + + // Test 10: $self.items (composition wildcard) with child field alias + // $self.items expands all child fields, but $self.items.ID with alias should add ItemId + // -------------------------------------------- + @bpm.process.start: { + id : 'startCompositionWildcardAliasProcess', + on : 'CREATE', + inputs: [ + $self.ID, + $self.status, + $self.items, // All scalar fields of items: ID, title, quantity, parent_ID + { + path: $self.items.ID, + as : 'ItemId' + } // Rename items.ID to ItemId (adds second copy) + ] + } + entity StartCompositionWildcardAlias { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + totalValue : Decimal(15, 2); + items : Composition of many StartCompositionWildcardAliasItems + on items.parent = $self; + } + + entity StartCompositionWildcardAliasItems { + key ID : UUID; + parent : Association to StartCompositionWildcardAlias; + title : String(200); + quantity : Integer; + } + + // Test 11: Multiple aliases on the same scalar field + // Same field (ID) should appear under two different names + // -------------------------------------------- + @bpm.process.start: { + id : 'startMultipleAliasScalarProcess', + on : 'CREATE', + inputs: [ + { + path: $self.ID, + as : 'OrderId' + }, // ID as OrderId + { + path: $self.ID, + as : 'ReferenceId' + } // ID as ReferenceId (same source, different alias) + ] + } + entity StartMultipleAliasScalar { + key ID : UUID; + status : String(20) default 'PENDING'; + shipmentDate : Date; + totalValue : Decimal(15, 2); + } + + // Test 12: Multiple aliases on the same composition + // Same composition (items) should appear under two different names + // -------------------------------------------- + @bpm.process.start: { + id : 'startMultipleAliasCompositionProcess', + on : 'CREATE', + inputs: [ + $self.ID, + { + path: $self.items, + as : 'Orders' + }, // items as Orders + { + path: $self.items, + as : 'LineItems' + } // items as LineItems (same source, different alias) + ] + } + entity StartMultipleAliasComposition { + key ID : UUID; + status : String(20) default 'PENDING'; + items : Composition of many StartMultipleAliasCompositionItems on items.parent = $self; - } - - entity StartCompositionWildcardAliasItems { - key ID : UUID; - parent : Association to StartCompositionWildcardAlias; - title : String(200); - quantity : Integer; - } - - // Test 11: Multiple aliases on the same scalar field - // Same field (ID) should appear under two different names - // -------------------------------------------- - @bpm.process.start: { - id: 'startMultipleAliasScalarProcess', - on: 'CREATE', - inputs: [ - { path: $self.ID, as: 'OrderId' }, // ID as OrderId - { path: $self.ID, as: 'ReferenceId' } // ID as ReferenceId (same source, different alias) - ] - } - entity StartMultipleAliasScalar { - key ID : UUID; - status : String(20) default 'PENDING'; - shipmentDate : Date; - totalValue : Decimal(15, 2); - } - - // Test 12: Multiple aliases on the same composition - // Same composition (items) should appear under two different names - // -------------------------------------------- - @bpm.process.start: { - id: 'startMultipleAliasCompositionProcess', - on: 'CREATE', - inputs: [ - $self.ID, - { path: $self.items, as: 'Orders' }, // items as Orders - { path: $self.items, as: 'LineItems' } // items as LineItems (same source, different alias) - ] - } - entity StartMultipleAliasComposition { - key ID : UUID; - status : String(20) default 'PENDING'; - items : Composition of many StartMultipleAliasCompositionItems + } + + entity StartMultipleAliasCompositionItems { + key ID : UUID; + parent : Association to StartMultipleAliasComposition; + title : String(200); + quantity : Integer; + } + + // Test 13: $self with Composition and Association + // $self should only include scalar fields, NOT compositions or associations + // -------------------------------------------- + @bpm.process.start: { + id : 'startSelfWithAssocProcess', + on : 'CREATE', + inputs: [$self] + } + entity StartSelfWithAssoc { + key ID : UUID; + status : String(20) default 'PENDING'; + author : Association to one StartSelfWithAssocAuthors; + } + + entity StartSelfWithAssocAuthors { + key ID : UUID; + } + + // Test 14: No inputs (all fields including Composition and Association) + // Without inputs array, all fields should be included + // -------------------------------------------- + @bpm.process.start: { + id: 'startNoInputWithAssocProcess', + on: 'CREATE' + } + entity StartNoInputWithAssoc { + key ID : UUID; + status : String(20) default 'PENDING'; + author : Association to one StartNoInputWithAssocAuthors; + } + + entity StartNoInputWithAssocAuthors { + key ID : UUID; + } + + // Test 15: $self.author - explicitly include association + // Should expand the author association with all its fields + // -------------------------------------------- + @bpm.process.start: { + id : 'startWithAuthorInputProcess', + on : 'CREATE', + inputs: [ + $self, + $self.author + ] + } + entity StartWithAuthorInput { + key ID : UUID; + status : String(20) default 'PENDING'; + items : Composition of many StartWithAuthorInputItems on items.parent = $self; - } - - entity StartMultipleAliasCompositionItems { - key ID : UUID; - parent : Association to StartMultipleAliasComposition; - title : String(200); - quantity : Integer; - } - - // Test 13: $self with Composition and Association - // $self should only include scalar fields, NOT compositions or associations - // -------------------------------------------- - @bpm.process.start: { - id: 'startSelfWithAssocProcess', - on: 'CREATE', - inputs: [ - $self - ] - } - entity StartSelfWithAssoc { - key ID : UUID; - status : String(20) default 'PENDING'; - author : Association to one StartSelfWithAssocAuthors; - } - entity StartSelfWithAssocAuthors { - key ID : UUID; - } - - // Test 14: No inputs (all fields including Composition and Association) - // Without inputs array, all fields should be included - // -------------------------------------------- - @bpm.process.start: { - id: 'startNoInputWithAssocProcess', - on: 'CREATE' - } - entity StartNoInputWithAssoc { - key ID : UUID; - status : String(20) default 'PENDING'; - author : Association to one StartNoInputWithAssocAuthors; - } - - entity StartNoInputWithAssocAuthors { - key ID : UUID; - } - - // Test 15: $self.author - explicitly include association - // Should expand the author association with all its fields - // -------------------------------------------- - @bpm.process.start: { - id: 'startWithAuthorInputProcess', - on: 'CREATE', - inputs: [ - $self, - $self.author - ] - } - entity StartWithAuthorInput { - key ID : UUID; - status : String(20) default 'PENDING'; - items : Composition of many StartWithAuthorInputItems - on items.parent = $self; - author : Association to one StartWithAuthorInputAuthors; - } - - entity StartWithAuthorInputItems { - key ID : UUID; - parent : Association to StartWithAuthorInput; - title : String(200); - quantity : Integer; - } - - entity StartWithAuthorInputAuthors { - key ID : UUID; - name : String(100); - } - - // ============================================ - // BUSINESS KEY LENGTH VALIDATION TESTS - // Testing businessKey max length (255 chars) on processStart - // ============================================ - - // Start on CREATE with businessKey (short value - well under 255 chars) - @bpm.process.start: { - id: 'startShortBusinessKeyProcess', - on: 'CREATE', - } - @bpm.process.businessKey: (ID) - entity StartWithShortBusinessKey as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Start on CREATE with businessKey at exactly 255 chars - @bpm.process.start: { - id: 'startExactLimitBusinessKeyProcess', - on: 'CREATE', - } - @bpm.process.businessKey: (longValue) - entity StartWithExactLimitBusinessKey { - key ID : UUID; - longValue : String(300); - name : String(100); - } - - // Start on CREATE with businessKey exceeding 255 chars - @bpm.process.start: { - id: 'startExceedingBusinessKeyProcess', - on: 'CREATE', - } - @bpm.process.businessKey: (longValue) - entity StartWithExceedingBusinessKey { - key ID : UUID; - longValue : String(300); - name : String(100); - } - - // Start on DELETE with businessKey exceeding 255 chars - @bpm.process.start: { - id: 'startOnDeleteExceedingBusinessKeyProcess', - on: 'DELETE', - } - @bpm.process.businessKey: (longValue) - entity StartOnDeleteExceedingBusinessKey { - key ID : UUID; - longValue : String(300); - name : String(100); - } - - // Start on UPDATE with businessKey exceeding 255 chars - @bpm.process.start: { - id: 'startOnUpdateExceedingBusinessKeyProcess', - on: 'UPDATE', - } - @bpm.process.businessKey: (longValue) - entity StartOnUpdateExceedingBusinessKey { - key ID : UUID; - longValue : String(300); - name : String(100); - } - - // ============================================ - // COMPOSITE BUSINESS KEY TESTS - // Testing businessKey with concat expressions - // ============================================ - - // Cancel with businessKey composed from two fields - @bpm.process.cancel: { - on: 'DELETE', - cascade: false, - } - @bpm.process.businessKey: (model || '-' || manufacturer) - entity CancelCompositeKey as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Suspend with businessKey composed from two fields - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - if: (mileage > 500), - } - @bpm.process.businessKey: (manufacturer || '_' || model) - entity SuspendCompositeKey as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Resume with businessKey composed from two fields - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - if: (mileage <= 500), - } - @bpm.process.businessKey: (manufacturer || '_' || model) - entity ResumeCompositeKey as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year - } - - // Full lifecycle with composite businessKey on all action annotations - @bpm.process.start: { - id: 'compositeKeyLifecycleProcess', - on: 'CREATE', - } - @bpm.process.suspend: { - on: 'UPDATE', - cascade: false, - if: (mileage > 500), - } - @bpm.process.resume: { - on: 'UPDATE', - cascade: false, - if: (mileage <= 500), - } - @bpm.process.cancel: { - on: 'DELETE', - cascade: true, - } - @bpm.process.businessKey: (model || '/' || manufacturer) - entity CompositeKeyLifecycle as - projection on my.Car { - ID, - model, - manufacturer, - mileage, - year + author : Association to one StartWithAuthorInputAuthors; + } + + entity StartWithAuthorInputItems { + key ID : UUID; + parent : Association to StartWithAuthorInput; + title : String(200); + quantity : Integer; + } + + entity StartWithAuthorInputAuthors { + key ID : UUID; + name : String(100); + } + + // ============================================ + // BUSINESS KEY LENGTH VALIDATION TESTS + // Testing businessKey max length (255 chars) on processStart + // ============================================ + + // Start on CREATE with businessKey (short value - well under 255 chars) + @bpm.process.start : { + id: 'startShortBusinessKeyProcess', + on: 'CREATE', + } + @bpm.process.businessKey: (ID) + entity StartWithShortBusinessKey as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Start on CREATE with businessKey at exactly 255 chars + @bpm.process.start : { + id: 'startExactLimitBusinessKeyProcess', + on: 'CREATE', + } + @bpm.process.businessKey: (longValue) + entity StartWithExactLimitBusinessKey { + key ID : UUID; + longValue : String(300); + name : String(100); + } + + // Start on CREATE with businessKey exceeding 255 chars + @bpm.process.start : { + id: 'startExceedingBusinessKeyProcess', + on: 'CREATE', + } + @bpm.process.businessKey: (longValue) + entity StartWithExceedingBusinessKey { + key ID : UUID; + longValue : String(300); + name : String(100); + } + + // Start on DELETE with businessKey exceeding 255 chars + @bpm.process.start : { + id: 'startOnDeleteExceedingBusinessKeyProcess', + on: 'DELETE', + } + @bpm.process.businessKey: (longValue) + entity StartOnDeleteExceedingBusinessKey { + key ID : UUID; + longValue : String(300); + name : String(100); + } + + // Start on UPDATE with businessKey exceeding 255 chars + @bpm.process.start : { + id: 'startOnUpdateExceedingBusinessKeyProcess', + on: 'UPDATE', + } + @bpm.process.businessKey: (longValue) + entity StartOnUpdateExceedingBusinessKey { + key ID : UUID; + longValue : String(300); + name : String(100); + } + + // ============================================ + // COMPOSITE BUSINESS KEY TESTS + // Testing businessKey with concat expressions + // ============================================ + + // Cancel with businessKey composed from two fields + @bpm.process.cancel : { + on : 'DELETE', + cascade: false, + } + @bpm.process.businessKey: (model || '-' || manufacturer) + entity CancelCompositeKey as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Suspend with businessKey composed from two fields + @bpm.process.suspend : { + on : 'UPDATE', + cascade: false, + if : (mileage > 500), + } + @bpm.process.businessKey: (manufacturer || '_' || model) + entity SuspendCompositeKey as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Resume with businessKey composed from two fields + @bpm.process.resume : { + on : 'UPDATE', + cascade: false, + if : (mileage <= 500), + } + @bpm.process.businessKey: (manufacturer || '_' || model) + entity ResumeCompositeKey as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // Full lifecycle with composite businessKey on all action annotations + @bpm.process.start : { + id: 'compositeKeyLifecycleProcess', + on: 'CREATE', + } + @bpm.process.suspend : { + on : 'UPDATE', + cascade: false, + if : (mileage > 500), + } + @bpm.process.resume : { + on : 'UPDATE', + cascade: false, + if : (mileage <= 500), + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, } + @bpm.process.businessKey: (model || '/' || manufacturer) + entity CompositeKeyLifecycle as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } } diff --git a/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.cds b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.cds new file mode 100644 index 0000000..328abf0 --- /dev/null +++ b/tests/bookshop/srv/external/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.cds @@ -0,0 +1,68 @@ +/* checksum : 5d15df873426db3131530b7d110f59be */ +namespace eu12.cdsmunich.capprocesspluginhybridtest; + +/** DO NOT EDIT. THIS IS A GENERATED SERVICE THAT WILL BE OVERRIDDEN ON NEXT IMPORT. */ +@protocol : 'none' +@bpm.process : 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process' +service Lifecycle_Test_ProcessService { + type ProcessInputs { + ID : String not null; + }; + + type ProcessOutputs { }; + + type ProcessAttribute { + id : String not null; + label : String not null; + value : String; + type : String not null; + }; + + type ProcessAttributes : many ProcessAttribute; + + type ProcessInstance { + definitionId : String; + definitionVersion : String; + id : String; + status : String; + startedAt : String; + startedBy : String; + }; + + type ProcessInstances : many ProcessInstance; + + type ProcessInstanceStatus : many String; + + action start( + inputs : ProcessInputs not null + ); + + function getAttributes( + processInstanceId : String not null + ) returns ProcessAttributes; + + function getOutputs( + processInstanceId : String not null + ) returns ProcessOutputs; + + function getInstancesByBusinessKey( + businessKey : String not null, + status : ProcessInstanceStatus + ) returns ProcessInstances; + + action suspend( + businessKey : String not null, + cascade : Boolean + ); + + action resume( + businessKey : String not null, + cascade : Boolean + ); + + action cancel( + businessKey : String not null, + cascade : Boolean + ); +}; + diff --git a/tests/bookshop/srv/lifecycle-annotation-test.cds b/tests/bookshop/srv/lifecycle-annotation-test.cds new file mode 100644 index 0000000..6d18222 --- /dev/null +++ b/tests/bookshop/srv/lifecycle-annotation-test.cds @@ -0,0 +1,163 @@ +using {sap.capire.bookshop as my} from '../db/shipment'; + +service LifecycleAnnotationService { + + // ============================================ + // COMBINATION ENTITIES - Real-world scenarios + // ============================================ + + // -------------------------------------------- + // Scenario 1: Basic Workflow Lifecycle + // Start process on CREATE, Cancel on DELETE + // Use case: Order processing, ticket management + // -------------------------------------------- + @bpm.process.start : { + id: 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + on: 'CREATE', + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true, + } + @bpm.process.businessKey: (ID) + entity BasicLifecycle as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Scenario 2: Status-based Cancellation + // Start on CREATE, Cancel on UPDATE when mileage exceeds threshold + // Use case: Auto-cancel workflow when entity reaches terminal state + // -------------------------------------------- + @bpm.process.start : { + id: 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + on: 'CREATE', + } + @bpm.process.cancel : { + on: 'UPDATE', + if: (mileage > 1000), + } + @bpm.process.businessKey: (ID) + entity StatusBasedCancel as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Scenario 3: Suspend/Resume Workflow + // Start on CREATE, Suspend on UPDATE (if mileage > 500), + // Resume on UPDATE (if mileage <= 500) + // Use case: Pause processing when item is on hold + // -------------------------------------------- + @bpm.process.start : { + id: 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + on: 'CREATE', + } + @bpm.process.suspend : { + on: 'UPDATE', + if: (mileage > 500), + } + @bpm.process.resume : { + on: 'UPDATE', + if: (mileage <= 500), + } + @bpm.process.businessKey: (ID) + entity SuspendResumeWorkflow as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Scenario 4: Full Lifecycle Management + // Start on CREATE, Suspend/Resume on UPDATE, Cancel on DELETE + // Use case: Complete workflow control with pause capability + // -------------------------------------------- + @bpm.process.start : { + id: 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + on: 'CREATE', + } + @bpm.process.suspend : { + on: 'UPDATE', + if: (mileage > 800), + } + @bpm.process.resume : { + on: 'UPDATE', + if: (mileage <= 800), + } + @bpm.process.cancel : { + on : 'DELETE', + cascade: true + } + @bpm.process.businessKey: (ID) + entity FullLifecycle as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Scenario 5: Conditional Start and Cancel + // Start on UPDATE when condition met, Cancel on UPDATE when different condition + // Use case: Workflow triggered by status change, cancelled by another status + // -------------------------------------------- + @bpm.process.start : { + id: 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + on: 'UPDATE', + if: (mileage > 500) + } + @bpm.process.cancel : { + on: 'UPDATE', + if: (mileage > 1500), + } + @bpm.process.businessKey: (ID) + entity ConditionalStartCancel as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + + // -------------------------------------------- + // Scenario 6: External Workflow Management + // No start annotation - workflow started externally + // Suspend/Resume on UPDATE, Cancel on DELETE + // Use case: Entity linked to externally triggered workflow + // -------------------------------------------- + @bpm.process.suspend : { + on: 'UPDATE', + if: (mileage > 500), + } + @bpm.process.resume : { + on: 'UPDATE', + if: (mileage <= 500), + } + @bpm.process.cancel : {on: 'DELETE', } + @bpm.process.businessKey: (ID) + entity ExternalWorkflowManagement as + projection on my.Car { + ID, + model, + manufacturer, + mileage, + year + } + +} diff --git a/tests/bookshop/srv/programmatical-service.cds b/tests/bookshop/srv/programmatical-service.cds new file mode 100644 index 0000000..01d470d --- /dev/null +++ b/tests/bookshop/srv/programmatical-service.cds @@ -0,0 +1,15 @@ +service ProgramaticalService { + action updateProcess(shipmentID: UUID, + @mandatory newStatus: String); + + action cancelProcess(shipmentID: UUID); + + action startShipment(shipmentID: UUID); + + action getShipmentAttributes(shipmentID: UUID) returns String; + + action getShipmentOutputs(shipmentID: UUID) returns String; + + action getInstancesByShipmentID(shipmentID: UUID, + status: many String) returns String; +} \ No newline at end of file diff --git a/tests/bookshop/srv/programmatical-service.ts b/tests/bookshop/srv/programmatical-service.ts new file mode 100644 index 0000000..2358810 --- /dev/null +++ b/tests/bookshop/srv/programmatical-service.ts @@ -0,0 +1,14 @@ +import cds from '@sap/cds'; + +class ProgramaticalService extends cds.ApplicationService { + async init() { + this.on('startProcess', async (req: cds.Request) => { + const { ID } = req.data; + }); + this.on('updateProcess', async (req: cds.Request) => {}); + this.on('suspendProcess', async (req: cds.Request) => {}); + this.on('getInstancesByShipmentID', async (req: cds.Request) => {}); + this.on('getAttributes', async (req: cds.Request) => {}); + this.on('getOutputs', async (req: cds.Request) => {}); + } +} diff --git a/tests/bookshop/workflows/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.json b/tests/bookshop/workflows/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.json new file mode 100644 index 0000000..d74b713 --- /dev/null +++ b/tests/bookshop/workflows/eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process.json @@ -0,0 +1,70 @@ +{ + "uid": "7f5f89d7-a48d-4019-b206-1c89cbc96f4a", + "name": " Lifecycle_Test_Process", + "description": "", + "type": "bpi.process", + "createdAt": "2026-03-11T12:45:55.991322Z", + "updatedAt": "2026-03-12T12:39:19.653411Z", + "header": { + "inputs": { + "title": "inputs", + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "date": { + "type": "string", + "format": "date" + }, + "dateTime": { + "type": "string", + "format": "date-time" + }, + "password": { + "type": "string", + "password": true + }, + "time": { + "type": "string", + "format": "time" + }, + "documentFolder": { + "type": "string", + "format": "document-folder" + } + }, + "properties": { + "ID": { + "type": "string", + "title": "ID", + "description": "" + } + }, + "required": [ + "ID" + ] + }, + "outputs": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "outputs", + "type": "object", + "properties": {} + }, + "processAttributes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "processAttributes", + "type": "object", + "properties": {}, + "required": [] + } + }, + "dependencies": [ + { + "artifactUid": "25ab1d56-63e5-4082-806f-9e41763f40f3", + "type": "content" + } + ], + "identifier": "lifecycle_Test_Process", + "valid": true, + "projectId": "eu12.cdsmunich.capprocesspluginhybridtest", + "dataTypes": [] +} \ No newline at end of file diff --git a/tests/integration/annotations/combinations.test.ts b/tests/integration/annotations/lifeCycleAnnotation.test.ts similarity index 65% rename from tests/integration/annotations/combinations.test.ts rename to tests/integration/annotations/lifeCycleAnnotation.test.ts index 96395b7..0f3a33f 100644 --- a/tests/integration/annotations/combinations.test.ts +++ b/tests/integration/annotations/lifeCycleAnnotation.test.ts @@ -49,25 +49,29 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should start process on CREATE', async () => { const car = createTestCar(); - const response = await POST('/odata/v4/annotation/BasicLifecycle', car); + const response = await POST('/odata/v4/lifecycle-annotation/BasicLifecycle', car); expect(response.status).toBe(201); expect(foundMessages.length).toBe(1); const startMessages = findStartMessages(); expect(startMessages.length).toBe(1); - expect(startMessages[0].data.definitionId).toBe('basicLifecycleProcess'); + expect(startMessages[0].data.definitionId).toBe( + 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + ); }); it('should cancel process on DELETE', async () => { const car = createTestCar(); // Create entity - await POST('/odata/v4/annotation/BasicLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/BasicLifecycle', car); foundMessages = []; // Delete entity - const deleteResponse = await DELETE(`/odata/v4/annotation/BasicLifecycle('${car.ID}')`); + const deleteResponse = await DELETE( + `/odata/v4/lifecycle-annotation/BasicLifecycle('${car.ID}')`, + ); expect(deleteResponse.status).toBe(204); expect(foundMessages.length).toBe(1); @@ -84,13 +88,16 @@ describe('Integration tests for Process Annotation Combinations', () => { const car = createTestCar(); // Create entity - await POST('/odata/v4/annotation/BasicLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/BasicLifecycle', car); foundMessages = []; // Update entity - const updateResponse = await PATCH(`/odata/v4/annotation/BasicLifecycle('${car.ID}')`, { - mileage: 500, - }); + const updateResponse = await PATCH( + `/odata/v4/lifecycle-annotation/BasicLifecycle('${car.ID}')`, + { + mileage: 500, + }, + ); expect(updateResponse.status).toBe(200); expect(foundMessages.length).toBe(0); @@ -100,17 +107,17 @@ describe('Integration tests for Process Annotation Combinations', () => { const car = createTestCar(); // CREATE - should start - const createResponse = await POST('/odata/v4/annotation/BasicLifecycle', car); + const createResponse = await POST('/odata/v4/lifecycle-annotation/BasicLifecycle', car); expect(createResponse.status).toBe(201); expect(findStartMessages().length).toBe(1); foundMessages = []; // UPDATE - should do nothing - await PATCH(`/odata/v4/annotation/BasicLifecycle('${car.ID}')`, { mileage: 500 }); + await PATCH(`/odata/v4/lifecycle-annotation/BasicLifecycle('${car.ID}')`, { mileage: 500 }); expect(foundMessages.length).toBe(0); // DELETE - should cancel - await DELETE(`/odata/v4/annotation/BasicLifecycle('${car.ID}')`); + await DELETE(`/odata/v4/lifecycle-annotation/BasicLifecycle('${car.ID}')`); expect(findCancelMessages().length).toBe(1); }); }); @@ -123,23 +130,28 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should start process on CREATE', async () => { const car = createTestCar(undefined, 100); - const response = await POST('/odata/v4/annotation/StatusBasedCancel', car); + const response = await POST('/odata/v4/lifecycle-annotation/StatusBasedCancel', car); expect(response.status).toBe(201); expect(findStartMessages().length).toBe(1); - expect(findStartMessages()[0].data.definitionId).toBe('statusCancelProcess'); + expect(findStartMessages()[0].data.definitionId).toBe( + 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + ); }); it('should NOT cancel process on UPDATE when condition NOT met', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/StatusBasedCancel', car); + await POST('/odata/v4/lifecycle-annotation/StatusBasedCancel', car); foundMessages = []; // Update but keep mileage <= 1000 - const updateResponse = await PATCH(`/odata/v4/annotation/StatusBasedCancel('${car.ID}')`, { - mileage: 500, - }); + const updateResponse = await PATCH( + `/odata/v4/lifecycle-annotation/StatusBasedCancel('${car.ID}')`, + { + mileage: 500, + }, + ); expect(updateResponse.status).toBe(200); expect(foundMessages.length).toBe(0); @@ -148,13 +160,16 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should cancel process on UPDATE when condition IS met', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/StatusBasedCancel', car); + await POST('/odata/v4/lifecycle-annotation/StatusBasedCancel', car); foundMessages = []; // Update to mileage > 1000 - const updateResponse = await PATCH(`/odata/v4/annotation/StatusBasedCancel('${car.ID}')`, { - mileage: 1500, - }); + const updateResponse = await PATCH( + `/odata/v4/lifecycle-annotation/StatusBasedCancel('${car.ID}')`, + { + mileage: 1500, + }, + ); expect(updateResponse.status).toBe(200); expect(findCancelMessages().length).toBe(1); @@ -165,16 +180,20 @@ describe('Integration tests for Process Annotation Combinations', () => { const car = createTestCar(undefined, 100); // CREATE - await POST('/odata/v4/annotation/StatusBasedCancel', car); + await POST('/odata/v4/lifecycle-annotation/StatusBasedCancel', car); expect(findStartMessages().length).toBe(1); foundMessages = []; // UPDATE below threshold - no cancel - await PATCH(`/odata/v4/annotation/StatusBasedCancel('${car.ID}')`, { mileage: 800 }); + await PATCH(`/odata/v4/lifecycle-annotation/StatusBasedCancel('${car.ID}')`, { + mileage: 800, + }); expect(foundMessages.length).toBe(0); // UPDATE above threshold - cancel - await PATCH(`/odata/v4/annotation/StatusBasedCancel('${car.ID}')`, { mileage: 1200 }); + await PATCH(`/odata/v4/lifecycle-annotation/StatusBasedCancel('${car.ID}')`, { + mileage: 1200, + }); expect(findCancelMessages().length).toBe(1); }); }); @@ -187,21 +206,23 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should start process on CREATE', async () => { const car = createTestCar(undefined, 100); - const response = await POST('/odata/v4/annotation/SuspendResumeWorkflow', car); + const response = await POST('/odata/v4/lifecycle-annotation/SuspendResumeWorkflow', car); expect(response.status).toBe(201); expect(findStartMessages().length).toBe(1); - expect(findStartMessages()[0].data.definitionId).toBe('suspendResumeProcess'); + expect(findStartMessages()[0].data.definitionId).toBe( + 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + ); }); it('should suspend process on UPDATE when mileage > 500', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/SuspendResumeWorkflow', car); + await POST('/odata/v4/lifecycle-annotation/SuspendResumeWorkflow', car); foundMessages = []; const updateResponse = await PATCH( - `/odata/v4/annotation/SuspendResumeWorkflow('${car.ID}')`, + `/odata/v4/lifecycle-annotation/SuspendResumeWorkflow('${car.ID}')`, { mileage: 600, }, @@ -215,11 +236,11 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should resume process on UPDATE when mileage <= 500', async () => { const car = createTestCar(undefined, 600); - await POST('/odata/v4/annotation/SuspendResumeWorkflow', car); + await POST('/odata/v4/lifecycle-annotation/SuspendResumeWorkflow', car); foundMessages = []; const updateResponse = await PATCH( - `/odata/v4/annotation/SuspendResumeWorkflow('${car.ID}')`, + `/odata/v4/lifecycle-annotation/SuspendResumeWorkflow('${car.ID}')`, { mileage: 400, }, @@ -234,22 +255,28 @@ describe('Integration tests for Process Annotation Combinations', () => { const car = createTestCar(undefined, 100); // CREATE - start - await POST('/odata/v4/annotation/SuspendResumeWorkflow', car); + await POST('/odata/v4/lifecycle-annotation/SuspendResumeWorkflow', car); expect(findStartMessages().length).toBe(1); foundMessages = []; // UPDATE to high mileage - suspend - await PATCH(`/odata/v4/annotation/SuspendResumeWorkflow('${car.ID}')`, { mileage: 700 }); + await PATCH(`/odata/v4/lifecycle-annotation/SuspendResumeWorkflow('${car.ID}')`, { + mileage: 700, + }); expect(findSuspendMessages().length).toBe(1); foundMessages = []; // UPDATE to low mileage - resume - await PATCH(`/odata/v4/annotation/SuspendResumeWorkflow('${car.ID}')`, { mileage: 300 }); + await PATCH(`/odata/v4/lifecycle-annotation/SuspendResumeWorkflow('${car.ID}')`, { + mileage: 300, + }); expect(findResumeMessages().length).toBe(1); foundMessages = []; // UPDATE to high mileage again - suspend - await PATCH(`/odata/v4/annotation/SuspendResumeWorkflow('${car.ID}')`, { mileage: 900 }); + await PATCH(`/odata/v4/lifecycle-annotation/SuspendResumeWorkflow('${car.ID}')`, { + mileage: 900, + }); expect(findSuspendMessages().length).toBe(1); }); }); @@ -262,20 +289,22 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should start process on CREATE', async () => { const car = createTestCar(undefined, 100); - const response = await POST('/odata/v4/annotation/FullLifecycle', car); + const response = await POST('/odata/v4/lifecycle-annotation/FullLifecycle', car); expect(response.status).toBe(201); expect(findStartMessages().length).toBe(1); - expect(findStartMessages()[0].data.definitionId).toBe('fullLifecycleProcess'); + expect(findStartMessages()[0].data.definitionId).toBe( + 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + ); }); it('should suspend on UPDATE when mileage > 800', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/FullLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/FullLifecycle', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/FullLifecycle('${car.ID}')`, { mileage: 900 }); + await PATCH(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`, { mileage: 900 }); expect(findSuspendMessages().length).toBe(1); expect(findResumeMessages().length).toBe(0); @@ -284,10 +313,10 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should resume on UPDATE when mileage <= 800', async () => { const car = createTestCar(undefined, 900); - await POST('/odata/v4/annotation/FullLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/FullLifecycle', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/FullLifecycle('${car.ID}')`, { mileage: 700 }); + await PATCH(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`, { mileage: 700 }); expect(findResumeMessages().length).toBe(1); expect(findSuspendMessages().length).toBe(0); @@ -296,10 +325,10 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should cancel on DELETE', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/FullLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/FullLifecycle', car); foundMessages = []; - await DELETE(`/odata/v4/annotation/FullLifecycle('${car.ID}')`); + await DELETE(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`); expect(findCancelMessages().length).toBe(1); expect(findCancelMessages()[0].data.cascade).toBe(true); @@ -309,22 +338,22 @@ describe('Integration tests for Process Annotation Combinations', () => { const car = createTestCar(undefined, 100); // CREATE - await POST('/odata/v4/annotation/FullLifecycle', car); + await POST('/odata/v4/lifecycle-annotation/FullLifecycle', car); expect(findStartMessages().length).toBe(1); foundMessages = []; // SUSPEND - await PATCH(`/odata/v4/annotation/FullLifecycle('${car.ID}')`, { mileage: 900 }); + await PATCH(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`, { mileage: 900 }); expect(findSuspendMessages().length).toBe(1); foundMessages = []; // RESUME - await PATCH(`/odata/v4/annotation/FullLifecycle('${car.ID}')`, { mileage: 500 }); + await PATCH(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`, { mileage: 500 }); expect(findResumeMessages().length).toBe(1); foundMessages = []; // DELETE (CANCEL) - await DELETE(`/odata/v4/annotation/FullLifecycle('${car.ID}')`); + await DELETE(`/odata/v4/lifecycle-annotation/FullLifecycle('${car.ID}')`); expect(findCancelMessages().length).toBe(1); }); }); @@ -337,7 +366,7 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should NOT start process on CREATE', async () => { const car = createTestCar(undefined, 100); - const response = await POST('/odata/v4/annotation/ConditionalStartCancel', car); + const response = await POST('/odata/v4/lifecycle-annotation/ConditionalStartCancel', car); expect(response.status).toBe(201); expect(foundMessages.length).toBe(0); @@ -346,10 +375,12 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should NOT start process on UPDATE when condition NOT met', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ConditionalStartCancel', car); + await POST('/odata/v4/lifecycle-annotation/ConditionalStartCancel', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/ConditionalStartCancel('${car.ID}')`, { mileage: 400 }); + await PATCH(`/odata/v4/lifecycle-annotation/ConditionalStartCancel('${car.ID}')`, { + mileage: 400, + }); expect(foundMessages.length).toBe(0); }); @@ -357,27 +388,35 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should start process on UPDATE when start condition IS met', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ConditionalStartCancel', car); + await POST('/odata/v4/lifecycle-annotation/ConditionalStartCancel', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/ConditionalStartCancel('${car.ID}')`, { mileage: 600 }); + await PATCH(`/odata/v4/lifecycle-annotation/ConditionalStartCancel('${car.ID}')`, { + mileage: 600, + }); expect(findStartMessages().length).toBe(1); - expect(findStartMessages()[0].data.definitionId).toBe('conditionalStartCancelProcess'); + expect(findStartMessages()[0].data.definitionId).toBe( + 'eu12.cdsmunich.capprocesspluginhybridtest.lifecycle_Test_Process', + ); }); it('should cancel process on UPDATE when cancel condition IS met', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ConditionalStartCancel', car); + await POST('/odata/v4/lifecycle-annotation/ConditionalStartCancel', car); foundMessages = []; // Start the process first - await PATCH(`/odata/v4/annotation/ConditionalStartCancel('${car.ID}')`, { mileage: 600 }); + await PATCH(`/odata/v4/lifecycle-annotation/ConditionalStartCancel('${car.ID}')`, { + mileage: 600, + }); foundMessages = []; // Cancel - await PATCH(`/odata/v4/annotation/ConditionalStartCancel('${car.ID}')`, { mileage: 1600 }); + await PATCH(`/odata/v4/lifecycle-annotation/ConditionalStartCancel('${car.ID}')`, { + mileage: 1600, + }); expect(findCancelMessages().length).toBe(1); }); @@ -385,11 +424,13 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should trigger BOTH start and cancel when both conditions met in one update', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ConditionalStartCancel', car); + await POST('/odata/v4/lifecycle-annotation/ConditionalStartCancel', car); foundMessages = []; // Update to value that meets both conditions (> 500 for start, > 1500 for cancel) - await PATCH(`/odata/v4/annotation/ConditionalStartCancel('${car.ID}')`, { mileage: 2000 }); + await PATCH(`/odata/v4/lifecycle-annotation/ConditionalStartCancel('${car.ID}')`, { + mileage: 2000, + }); // Both should be triggered expect(findStartMessages().length).toBe(1); @@ -405,7 +446,7 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should NOT start any process on CREATE', async () => { const car = createTestCar(undefined, 100); - const response = await POST('/odata/v4/annotation/ExternalWorkflowManagement', car); + const response = await POST('/odata/v4/lifecycle-annotation/ExternalWorkflowManagement', car); expect(response.status).toBe(201); expect(foundMessages.length).toBe(0); @@ -414,10 +455,12 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should suspend on UPDATE when mileage > 500', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ExternalWorkflowManagement', car); + await POST('/odata/v4/lifecycle-annotation/ExternalWorkflowManagement', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/ExternalWorkflowManagement('${car.ID}')`, { mileage: 600 }); + await PATCH(`/odata/v4/lifecycle-annotation/ExternalWorkflowManagement('${car.ID}')`, { + mileage: 600, + }); expect(findSuspendMessages().length).toBe(1); }); @@ -425,10 +468,12 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should resume on UPDATE when mileage <= 500', async () => { const car = createTestCar(undefined, 600); - await POST('/odata/v4/annotation/ExternalWorkflowManagement', car); + await POST('/odata/v4/lifecycle-annotation/ExternalWorkflowManagement', car); foundMessages = []; - await PATCH(`/odata/v4/annotation/ExternalWorkflowManagement('${car.ID}')`, { mileage: 400 }); + await PATCH(`/odata/v4/lifecycle-annotation/ExternalWorkflowManagement('${car.ID}')`, { + mileage: 400, + }); expect(findResumeMessages().length).toBe(1); }); @@ -436,10 +481,10 @@ describe('Integration tests for Process Annotation Combinations', () => { it('should cancel on DELETE', async () => { const car = createTestCar(undefined, 100); - await POST('/odata/v4/annotation/ExternalWorkflowManagement', car); + await POST('/odata/v4/lifecycle-annotation/ExternalWorkflowManagement', car); foundMessages = []; - await DELETE(`/odata/v4/annotation/ExternalWorkflowManagement('${car.ID}')`); + await DELETE(`/odata/v4/lifecycle-annotation/ExternalWorkflowManagement('${car.ID}')`); expect(findCancelMessages().length).toBe(1); });