From 49f46bca829494cd1bc0952b3b413e978065e6c8 Mon Sep 17 00:00:00 2001 From: Marijn Verbeek Date: Thu, 13 Aug 2026 13:55:00 +0200 Subject: [PATCH 1/3] When deleting a process definition linked, tables linked to this weren't all cleaned up correctly because a specific event wasn't being thrown. Also fixed this retroactively with liquibase changesets --- .../CustomRepositoryServiceImpl.java | 20 +- .../liquibase/13-42-0/13-42-0-master.xml | 25 +++ ...ned-process-definition-case-definition.xml | 34 +++ .../config/liquibase/changelog-master.xml | 1 + ...teOrphanedProcessDefinitionLinksIntTest.kt | 210 ++++++++++++++++++ 5 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 backend/core/src/main/resources/config/liquibase/13-42-0/13-42-0-master.xml create mode 100644 backend/core/src/main/resources/config/liquibase/13-42-0/20260813-delete-orphaned-process-definition-case-definition.xml create mode 100644 backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt diff --git a/backend/core/src/main/java/com/ritense/valtimo/operaton/repository/CustomRepositoryServiceImpl.java b/backend/core/src/main/java/com/ritense/valtimo/operaton/repository/CustomRepositoryServiceImpl.java index a7ab887f66..47cfc5e3a1 100644 --- a/backend/core/src/main/java/com/ritense/valtimo/operaton/repository/CustomRepositoryServiceImpl.java +++ b/backend/core/src/main/java/com/ritense/valtimo/operaton/repository/CustomRepositoryServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 Ritense BV, the Netherlands. + * Copyright 2015-2026 Ritense BV, the Netherlands. * * Licensed under EUPL, Version 1.2 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package com.ritense.valtimo.operaton.repository; +import com.ritense.valtimo.contract.BlueprintId; import com.ritense.valtimo.contract.audit.utils.AuditHelper; +import com.ritense.valtimo.contract.buildingblock.BuildingBlockDefinitionId; +import com.ritense.valtimo.contract.case_.CaseDefinitionId; import com.ritense.valtimo.contract.utils.RequestHelper; import com.ritense.valtimo.domain.process.event.ProcessDefinitionDeletedEvent; +import com.ritense.valtimo.event.ProcessDefinitionDeleted; import java.time.LocalDateTime; import java.util.UUID; import org.operaton.bpm.engine.impl.RepositoryServiceImpl; @@ -46,6 +50,20 @@ public void deleteProcessDefinition(String processDefinitionId, boolean cascade, processDefinition.getKey() ) ); + applicationEventPublisher.publishEvent( + new ProcessDefinitionDeleted( + processDefinitionId, + getBlueprintId(processDefinition.getVersionTag()) + ) + ); + } + + private BlueprintId getBlueprintId(String versionTag) { + CaseDefinitionId caseDefinitionId = CaseDefinitionId.fromProcessVersionTag(versionTag); + if (caseDefinitionId != null) { + return caseDefinitionId; + } + return BuildingBlockDefinitionId.Companion.fromProcessVersionTag(versionTag); } } \ No newline at end of file diff --git a/backend/core/src/main/resources/config/liquibase/13-42-0/13-42-0-master.xml b/backend/core/src/main/resources/config/liquibase/13-42-0/13-42-0-master.xml new file mode 100644 index 0000000000..796bbee74a --- /dev/null +++ b/backend/core/src/main/resources/config/liquibase/13-42-0/13-42-0-master.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/backend/core/src/main/resources/config/liquibase/13-42-0/20260813-delete-orphaned-process-definition-case-definition.xml b/backend/core/src/main/resources/config/liquibase/13-42-0/20260813-delete-orphaned-process-definition-case-definition.xml new file mode 100644 index 0000000000..6883c3dacd --- /dev/null +++ b/backend/core/src/main/resources/config/liquibase/13-42-0/20260813-delete-orphaned-process-definition-case-definition.xml @@ -0,0 +1,34 @@ + + + + + + + + process_definition_id NOT IN (SELECT id_ FROM act_re_procdef) + + + + + + process_definition_id NOT IN (SELECT id_ FROM act_re_procdef) + + + + diff --git a/backend/core/src/main/resources/config/liquibase/changelog-master.xml b/backend/core/src/main/resources/config/liquibase/changelog-master.xml index 68ca8fe1a3..502f3ed8c2 100644 --- a/backend/core/src/main/resources/config/liquibase/changelog-master.xml +++ b/backend/core/src/main/resources/config/liquibase/changelog-master.xml @@ -34,5 +34,6 @@ + diff --git a/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt b/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt new file mode 100644 index 0000000000..46ed2a7756 --- /dev/null +++ b/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt @@ -0,0 +1,210 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.valtimo.liquibase + +import com.ritense.valtimo.BaseIntegrationTest +import com.ritense.valtimo.contract.config.LiquibaseRunner +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.operaton.bpm.engine.RepositoryService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.TransactionTemplate +import java.util.UUID + +class DeleteOrphanedProcessDefinitionLinksIntTest : BaseIntegrationTest() { + + @Autowired + lateinit var jdbcTemplate: JdbcTemplate + + @Autowired + lateinit var repositoryService: RepositoryService + + @Autowired + lateinit var liquibaseRunner: LiquibaseRunner + + @Autowired + lateinit var transactionManager: PlatformTransactionManager + + companion object { + const val CHANGESET_FILENAME = "13-42-0/20260813-delete-orphaned-process-definition-case-definition.xml" + } + + @AfterEach + fun cleanup() { + TransactionTemplate(transactionManager).execute { + jdbcTemplate.update("DELETE FROM process_definition_case_definition WHERE case_definition_key IN ('valid-case', 'orphan-case')") + jdbcTemplate.update("DELETE FROM process_link WHERE activity_id IN ('TestServiceTask', 'SomeTask')") + } + } + + @Test + fun `changeset should delete orphaned process_definition_case_definition rows`() { + val txTemplate = TransactionTemplate(transactionManager) + + val validProcDefId = txTemplate.execute { + repositoryService.createDeployment() + .addClasspathResource("config/global/bpmn/test-process.bpmn") + .deployWithResult() + .deployedProcessDefinitions + .first() + .id + }!! + + txTemplate.execute { + jdbcTemplate.update( + """ + INSERT INTO process_definition_case_definition + (process_definition_id, case_definition_key, case_definition_version_tag, can_initialize_document, startable_by_user) + VALUES (?, 'valid-case', '1.0.0', false, true) + """.trimIndent(), + validProcDefId + ) + + jdbcTemplate.update( + """ + INSERT INTO process_definition_case_definition + (process_definition_id, case_definition_key, case_definition_version_tag, can_initialize_document, startable_by_user) + VALUES ('non-existent:1:12345', 'orphan-case', '1.0.0', false, true) + """ + ) + } + + val totalCountBefore = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_definition_case_definition", + Int::class.java + ) + + val orphanCountBefore = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_definition_case_definition WHERE case_definition_key = 'orphan-case'", + Int::class.java + ) + assertThat(orphanCountBefore).isEqualTo(1) + + txTemplate.execute { + jdbcTemplate.update( + """ + DELETE FROM DATABASECHANGELOG + WHERE FILENAME LIKE '%20260813-delete-orphaned%' AND AUTHOR = 'Ritense' AND ID = '1' + """.trimIndent() + ) + } + + liquibaseRunner.run() + + val validCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_definition_case_definition WHERE case_definition_key = 'valid-case'", + Int::class.java + ) + assertThat(validCountAfter).isEqualTo(1) + + val orphanCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_definition_case_definition WHERE case_definition_key = 'orphan-case'", + Int::class.java + ) + assertThat(orphanCountAfter).isEqualTo(0) + + val totalCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_definition_case_definition", + Int::class.java + ) + assertThat(totalCountAfter).isEqualTo(totalCountBefore!! - 1) + } + + @Test + fun `changeset should delete orphaned process_link rows`() { + val txTemplate = TransactionTemplate(transactionManager) + + val validProcDefId = txTemplate.execute { + repositoryService.createDeployment() + .addClasspathResource("config/global/bpmn/test-process.bpmn") + .deployWithResult() + .deployedProcessDefinitions + .first() + .id + }!! + + val validLinkId = UUID.randomUUID() + val orphanLinkId = UUID.randomUUID() + + txTemplate.execute { + jdbcTemplate.update( + """ + INSERT INTO process_link + (id, process_definition_id, activity_id, activity_type, process_link_type) + VALUES (?, ?, 'TestServiceTask', 'bpmn:ServiceTask:start', 'test') + """.trimIndent(), + validLinkId, + validProcDefId + ) + + jdbcTemplate.update( + """ + INSERT INTO process_link + (id, process_definition_id, activity_id, activity_type, process_link_type) + VALUES (?, 'non-existent:1:12345', 'SomeTask', 'bpmn:ServiceTask:start', 'test') + """.trimIndent(), + orphanLinkId + ) + } + + val totalCountBefore = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_link", + Int::class.java + ) + + val orphanCountBefore = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_link WHERE id = ?", + Int::class.java, + orphanLinkId + ) + assertThat(orphanCountBefore).isEqualTo(1) + + txTemplate.execute { + jdbcTemplate.update( + """ + DELETE FROM DATABASECHANGELOG + WHERE FILENAME LIKE '%20260813-delete-orphaned%' AND AUTHOR = 'Ritense' AND ID = '2' + """.trimIndent() + ) + } + + liquibaseRunner.run() + + val validCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_link WHERE id = ?", + Int::class.java, + validLinkId + ) + assertThat(validCountAfter).isEqualTo(1) + + val orphanCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_link WHERE id = ?", + Int::class.java, + orphanLinkId + ) + assertThat(orphanCountAfter).isEqualTo(0) + + val totalCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM process_link", + Int::class.java + ) + assertThat(totalCountAfter).isEqualTo(totalCountBefore!! - 1) + } +} From d95fdb4daa804a67b74b4d2e60d7582970a51fe8 Mon Sep 17 00:00:00 2001 From: Marijn Verbeek Date: Thu, 13 Aug 2026 13:58:55 +0200 Subject: [PATCH 2/3] release notes --- documentation/release-notes/13.x.x/13.42.0/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/documentation/release-notes/13.x.x/13.42.0/README.md b/documentation/release-notes/13.x.x/13.42.0/README.md index 5691046043..aded0f4ff2 100644 --- a/documentation/release-notes/13.x.x/13.42.0/README.md +++ b/documentation/release-notes/13.x.x/13.42.0/README.md @@ -18,4 +18,8 @@ ## Bugfixes -* New bugfix. +* **Deleting a process linked to a case now cleans up properly** + + When a process that was linked to a case definition was deleted, the link remained in the database. + This could cause errors when viewing or exporting the case definition. Existing orphaned + links from earlier versions are automatically cleaned up during upgrade. From baac203509c592f2d150e4fc2753495e3cc2356d Mon Sep 17 00:00:00 2001 From: Marijn Verbeek Date: Thu, 13 Aug 2026 14:11:57 +0200 Subject: [PATCH 3/3] added additional test for building blocks --- ...teOrphanedProcessDefinitionLinksIntTest.kt | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt b/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt index 46ed2a7756..e8e88ef94c 100644 --- a/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt +++ b/backend/core/src/test/kotlin/com/ritense/valtimo/liquibase/DeleteOrphanedProcessDefinitionLinksIntTest.kt @@ -50,7 +50,8 @@ class DeleteOrphanedProcessDefinitionLinksIntTest : BaseIntegrationTest() { fun cleanup() { TransactionTemplate(transactionManager).execute { jdbcTemplate.update("DELETE FROM process_definition_case_definition WHERE case_definition_key IN ('valid-case', 'orphan-case')") - jdbcTemplate.update("DELETE FROM process_link WHERE activity_id IN ('TestServiceTask', 'SomeTask')") + jdbcTemplate.update("DELETE FROM building_block_process_link WHERE building_block_definition_key = 'test-building-block'") + jdbcTemplate.update("DELETE FROM process_link WHERE activity_id IN ('TestServiceTask', 'SomeTask', 'OrphanTask')") } } @@ -207,4 +208,100 @@ class DeleteOrphanedProcessDefinitionLinksIntTest : BaseIntegrationTest() { ) assertThat(totalCountAfter).isEqualTo(totalCountBefore!! - 1) } + + @Test + fun `changeset should cascade delete building_block_process_link when process_link is deleted`() { + val txTemplate = TransactionTemplate(transactionManager) + + val validProcDefId = txTemplate.execute { + repositoryService.createDeployment() + .addClasspathResource("config/global/bpmn/test-process.bpmn") + .deployWithResult() + .deployedProcessDefinitions + .first() + .id + }!! + + val validLinkId = UUID.randomUUID() + val orphanLinkId = UUID.randomUUID() + + txTemplate.execute { + // Valid process_link with building_block_process_link child + jdbcTemplate.update( + """ + INSERT INTO process_link + (id, process_definition_id, activity_id, activity_type, process_link_type) + VALUES (?, ?, 'TestServiceTask', 'bpmn:ServiceTask:start', 'test') + """.trimIndent(), + validLinkId, + validProcDefId + ) + jdbcTemplate.update( + """ + INSERT INTO building_block_process_link + (process_link_id, building_block_definition_key, building_block_definition_version_tag, plugin_configuration_mappings) + VALUES (?, 'test-building-block', '1.0.0', '{}') + """.trimIndent(), + validLinkId + ) + + // Orphan process_link with building_block_process_link child + jdbcTemplate.update( + """ + INSERT INTO process_link + (id, process_definition_id, activity_id, activity_type, process_link_type) + VALUES (?, 'non-existent:1:12345', 'OrphanTask', 'bpmn:ServiceTask:start', 'test') + """.trimIndent(), + orphanLinkId + ) + jdbcTemplate.update( + """ + INSERT INTO building_block_process_link + (process_link_id, building_block_definition_key, building_block_definition_version_tag, plugin_configuration_mappings) + VALUES (?, 'test-building-block', '2.0.0', '{}') + """.trimIndent(), + orphanLinkId + ) + } + + val bbLinkCountBefore = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM building_block_process_link WHERE building_block_definition_key = 'test-building-block'", + Int::class.java + ) + assertThat(bbLinkCountBefore).isEqualTo(2) + + txTemplate.execute { + jdbcTemplate.update( + """ + DELETE FROM DATABASECHANGELOG + WHERE FILENAME LIKE '%20260813-delete-orphaned%' AND AUTHOR = 'Ritense' AND ID = '2' + """.trimIndent() + ) + } + + liquibaseRunner.run() + + // Verify valid building_block_process_link remains + val validBbLinkCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM building_block_process_link WHERE process_link_id = ?", + Int::class.java, + validLinkId + ) + assertThat(validBbLinkCount).isEqualTo(1) + + // Verify orphan building_block_process_link was cascade-deleted + val orphanBbLinkCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM building_block_process_link WHERE process_link_id = ?", + Int::class.java, + orphanLinkId + ) + assertThat(orphanBbLinkCount).isEqualTo(0) + + // Verify total count decreased by 1 + val bbLinkCountAfter = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM building_block_process_link WHERE building_block_definition_key = 'test-building-block'", + Int::class.java + ) + assertThat(bbLinkCountAfter).isEqualTo(1) + } }