From 378fd7529eda66206c66851dd2501432681f081a Mon Sep 17 00:00:00 2001 From: Philipp Ossler Date: Wed, 14 Dec 2022 13:39:42 +0100 Subject: [PATCH] Get user tasks forms as metadata (#314) * feat: expose user task form as metadata * feat: filter process elements by type --- .../io/zeebe/zeeqs/data/entity/UserTask.kt | 3 +- .../zeeqs/data/service/BpmnElementMetadata.kt | 3 +- .../zeeqs/data/service/ProcessService.kt | 25 +++- .../zeebe/zeeqs/data/service}/UserTaskForm.kt | 4 +- .../io/zeebe/zeeqs/ProcessServiceTest.kt | 110 +++++++++++------- .../graphql/resolvers/type/ProcessResolver.kt | 8 +- .../resolvers/type/UserTaskResolver.kt | 14 +-- .../main/resources/graphql/Element.graphqls | 4 +- .../main/resources/graphql/Process.graphqls | 5 +- .../io/zeebe/zeeqs/ZeebeGraphqlProcessTest.kt | 87 +++++++++++++- .../zeebe/zeeqs/ZeebeGraphqlUserTaskTest.kt | 3 +- .../importer/hazelcast/HazelcastImporter.kt | 7 +- .../zeebe/zeeqs/HazelcastImporterJobTest.kt | 3 +- 13 files changed, 199 insertions(+), 77 deletions(-) rename {graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type => data/src/main/kotlin/io/zeebe/zeeqs/data/service}/UserTaskForm.kt (62%) diff --git a/data/src/main/kotlin/io/zeebe/zeeqs/data/entity/UserTask.kt b/data/src/main/kotlin/io/zeebe/zeeqs/data/entity/UserTask.kt index c8f365b8..71461059 100644 --- a/data/src/main/kotlin/io/zeebe/zeeqs/data/entity/UserTask.kt +++ b/data/src/main/kotlin/io/zeebe/zeeqs/data/entity/UserTask.kt @@ -14,8 +14,7 @@ data class UserTask( val elementInstanceKey: Long, val assignee: String?, val candidateGroups: String?, - val formKey: String?, - val isCamundaForm: Boolean + val formKey: String? ) { @Enumerated(EnumType.STRING) var state: UserTaskState = UserTaskState.CREATED diff --git a/data/src/main/kotlin/io/zeebe/zeeqs/data/service/BpmnElementMetadata.kt b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/BpmnElementMetadata.kt index ae32d7f4..080c815e 100644 --- a/data/src/main/kotlin/io/zeebe/zeeqs/data/service/BpmnElementMetadata.kt +++ b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/BpmnElementMetadata.kt @@ -7,5 +7,6 @@ data class BpmnElementMetadata( val errorCode: String? = null, val calledProcessId: String? = null, val messageSubscriptionDefinition: MessageSubscriptionDefinition? = null, - val userTaskAssignmentDefinition: UserTaskAssignmentDefinition? = null + val userTaskAssignmentDefinition: UserTaskAssignmentDefinition? = null, + val userTaskForm: UserTaskForm? = null ) diff --git a/data/src/main/kotlin/io/zeebe/zeeqs/data/service/ProcessService.kt b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/ProcessService.kt index faef3fba..70961f9e 100644 --- a/data/src/main/kotlin/io/zeebe/zeeqs/data/service/ProcessService.kt +++ b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/ProcessService.kt @@ -7,10 +7,13 @@ import io.camunda.zeebe.model.bpmn.instance.* import io.camunda.zeebe.model.bpmn.instance.zeebe.* import io.zeebe.zeeqs.data.entity.BpmnElementType import io.zeebe.zeeqs.data.repository.ProcessRepository +import org.camunda.bpm.model.xml.ModelInstance import org.springframework.cache.annotation.Cacheable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component +private const val CAMUNDA_FORM_KEY_PREFIX = "camunda-forms:bpmn:" + @Component class ProcessService(val processRepository: ProcessRepository) { @@ -126,6 +129,18 @@ class ProcessService(val processRepository: ProcessRepository) { assignee = it.assignee, candidateGroups = it.candidateGroups ) + }, + userTaskForm = element + .getSingleExtensionElement(ZeebeFormDefinition::class.java) + ?.formKey + ?.let { formKey -> + UserTaskForm( + key = formKey, + resource = getForm( + model = element.modelInstance, + formKey = formKey + ) + ) } ) } @@ -133,8 +148,14 @@ class ProcessService(val processRepository: ProcessRepository) { @Cacheable(cacheNames = ["userTaskForm"]) fun getForm(processDefinitionKey: Long, formKey: String): String? { return getBpmnModel(processDefinitionKey) - ?.getModelElementsByType(ZeebeUserTaskForm::class.java) - ?.firstOrNull { it.id == formKey } + ?.let { getForm(model = it, formKey = formKey) } + } + + private fun getForm(model: ModelInstance, formKey: String): String? { + val normalizedFormKey = formKey.replace(CAMUNDA_FORM_KEY_PREFIX, "") + + return model.getModelElementsByType(ZeebeUserTaskForm::class.java) + ?.firstOrNull { it.id == normalizedFormKey } ?.textContent } diff --git a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskForm.kt b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/UserTaskForm.kt similarity index 62% rename from graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskForm.kt rename to data/src/main/kotlin/io/zeebe/zeeqs/data/service/UserTaskForm.kt index 1a79d0cb..0d20af17 100644 --- a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskForm.kt +++ b/data/src/main/kotlin/io/zeebe/zeeqs/data/service/UserTaskForm.kt @@ -1,6 +1,6 @@ -package io.zeebe.zeeqs.graphql.resolvers.type +package io.zeebe.zeeqs.data.service data class UserTaskForm( val key: String, val resource: String? -) +) \ No newline at end of file diff --git a/data/src/test/kotlin/io/zeebe/zeeqs/ProcessServiceTest.kt b/data/src/test/kotlin/io/zeebe/zeeqs/ProcessServiceTest.kt index 80bd1cd1..337243b1 100644 --- a/data/src/test/kotlin/io/zeebe/zeeqs/ProcessServiceTest.kt +++ b/data/src/test/kotlin/io/zeebe/zeeqs/ProcessServiceTest.kt @@ -1,13 +1,11 @@ package io.zeebe.zeeqs import io.camunda.zeebe.model.bpmn.Bpmn +import io.camunda.zeebe.model.bpmn.BpmnModelInstance import io.zeebe.zeeqs.data.entity.BpmnElementType import io.zeebe.zeeqs.data.entity.Process import io.zeebe.zeeqs.data.repository.ProcessRepository -import io.zeebe.zeeqs.data.service.BpmnElementInfo -import io.zeebe.zeeqs.data.service.BpmnElementMetadata -import io.zeebe.zeeqs.data.service.ProcessService -import io.zeebe.zeeqs.data.service.UserTaskAssignmentDefinition +import io.zeebe.zeeqs.data.service.* import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.entry import org.junit.jupiter.api.BeforeEach @@ -36,25 +34,16 @@ class ProcessServiceTest( // given val processDefinitionKey = 1L - val bpmn = Bpmn.createExecutableProcess("process") - .startEvent("s").name("start") - .serviceTask("t").name("task") - .zeebeJobType("test") - .userTask("u").name("userTask") - .zeebeAssignee("user1").zeebeCandidateGroups("group1") - .endEvent("e").name("") - .done() - - processRepository.save( - Process( - key = processDefinitionKey, - bpmnProcessId = "process", - version = 1, - bpmnXML = Bpmn.convertToString(bpmn), - deployTime = Instant.now().toEpochMilli(), - resourceName = "process.bpmn", - checksum = "checksum" - ) + createProcess( + processDefinitionKey = processDefinitionKey, + bpmn = Bpmn.createExecutableProcess("process") + .startEvent("s").name("start") + .serviceTask("t").name("task") + .zeebeJobType("test") + .userTask("u").name("userTask") + .zeebeAssignee("user1").zeebeCandidateGroups("group1") + .endEvent("e").name("") + .done() ) // when @@ -71,6 +60,41 @@ class ProcessServiceTest( .contains(entry("e", BpmnElementInfo("e", null, BpmnElementType.END_EVENT, BpmnElementMetadata()))) } + @Test + fun `should return user task form`() { + // given + val processDefinitionKey = 1L + + createProcess( + processDefinitionKey = processDefinitionKey, + bpmn = Bpmn.createExecutableProcess("process") + .startEvent() + .userTask("user_task_A").name("A") + .zeebeUserTaskForm("form_A", """{"x":1}""") + .done() + ) + + // when + val info = processService.getBpmnElementInfo(processDefinitionKey)!! + + // then + assertThat(info["user_task_A"]) + .isNotNull() + .isEqualTo( + BpmnElementInfo( + elementId = "user_task_A", + elementName = "A", + elementType = BpmnElementType.USER_TASK, + metadata = BpmnElementMetadata( + userTaskForm = UserTaskForm( + key = "camunda-forms:bpmn:form_A", + resource = """{"x":1}""" + ) + ) + ) + ) + } + @Test fun `should return nothing if process does not exist`() { // given @@ -94,24 +118,14 @@ class ProcessServiceTest( @BeforeEach fun `store process`() { - val bpmn = Bpmn.createExecutableProcess("process") - .startEvent() - .userTask("A") - .zeebeUserTaskForm(formKey, userForm) - .endEvent() - .done() - - processRepository.save( - Process( - key = processDefinitionKey, - bpmnProcessId = "process", - version = 1, - bpmnXML = Bpmn.convertToString(bpmn), - deployTime = Instant.now().toEpochMilli(), - resourceName = "process.bpmn", - checksum = "checksum" - ) - ) + createProcess( + processDefinitionKey = 1L, + bpmn = Bpmn.createExecutableProcess("process") + .startEvent() + .userTask("A") + .zeebeUserTaskForm(formKey, userForm) + .endEvent() + .done()) } @Test @@ -150,4 +164,18 @@ class ProcessServiceTest( } } + private fun createProcess(processDefinitionKey: Long, bpmn: BpmnModelInstance?) { + processRepository.save( + Process( + key = processDefinitionKey, + bpmnProcessId = "process", + version = 1, + bpmnXML = Bpmn.convertToString(bpmn), + deployTime = Instant.now().toEpochMilli(), + resourceName = "process.bpmn", + checksum = "checksum" + ) + ) + } + } diff --git a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/ProcessResolver.kt b/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/ProcessResolver.kt index cac6fffb..964a40fc 100644 --- a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/ProcessResolver.kt +++ b/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/ProcessResolver.kt @@ -1,10 +1,7 @@ package io.zeebe.zeeqs.graphql.resolvers.type import graphql.kickstart.tools.GraphQLResolver -import io.zeebe.zeeqs.data.entity.MessageSubscription -import io.zeebe.zeeqs.data.entity.Timer -import io.zeebe.zeeqs.data.entity.Process -import io.zeebe.zeeqs.data.entity.ProcessInstanceState +import io.zeebe.zeeqs.data.entity.* import io.zeebe.zeeqs.data.repository.MessageSubscriptionRepository import io.zeebe.zeeqs.data.repository.TimerRepository import io.zeebe.zeeqs.data.repository.ProcessInstanceRepository @@ -41,10 +38,11 @@ class ProcessResolver( return messageSubscriptionRepository.findByProcessDefinitionKeyAndElementInstanceKeyIsNull(process.key) } - fun elements(process: Process): List { + fun elements(process: Process, elementTypeIn: List): List { return processService .getBpmnElementInfo(process.key) ?.values + ?.filter { elementTypeIn.isEmpty() || elementTypeIn.contains(it.elementType) } ?.map { asBpmnElement(process, it) } ?: emptyList() } diff --git a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskResolver.kt b/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskResolver.kt index 9984ed52..d2faba38 100644 --- a/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskResolver.kt +++ b/graphql-api/src/main/kotlin/io/zeebe/zeeqs/graphql/resolvers/type/UserTaskResolver.kt @@ -4,9 +4,9 @@ import graphql.kickstart.tools.GraphQLResolver import io.zeebe.zeeqs.data.entity.ElementInstance import io.zeebe.zeeqs.data.entity.ProcessInstance import io.zeebe.zeeqs.data.entity.UserTask +import io.zeebe.zeeqs.data.service.UserTaskForm import io.zeebe.zeeqs.data.repository.ElementInstanceRepository import io.zeebe.zeeqs.data.repository.ProcessInstanceRepository -import io.zeebe.zeeqs.data.repository.UserTaskRepository import io.zeebe.zeeqs.data.service.ProcessService import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component @@ -42,14 +42,10 @@ class UserTaskResolver( return userTask.formKey?.let { formKey -> UserTaskForm( key = formKey, - resource = formKey - .takeIf { userTask.isCamundaForm } - ?.let { - processService.getForm( - processDefinitionKey = userTask.processDefinitionKey, - formKey = formKey - ) - } + resource = processService.getForm( + processDefinitionKey = userTask.processDefinitionKey, + formKey = formKey + ) ) } } diff --git a/graphql-api/src/main/resources/graphql/Element.graphqls b/graphql-api/src/main/resources/graphql/Element.graphqls index 9660914b..0d03fea9 100644 --- a/graphql-api/src/main/resources/graphql/Element.graphqls +++ b/graphql-api/src/main/resources/graphql/Element.graphqls @@ -62,8 +62,10 @@ type BpmnElementMetadata { calledProcessId: String # the definition of the message subscription if the element is a message catch event messageSubscriptionDefinition: MessageSubscriptionDefinition - # the assignment definition if the element is an user task + # the assignment definition if the element is a user task userTaskAssignmentDefinition: UserTaskAssignmentDefinition + # the user form if the element is a user task + userTaskForm: UserTaskForm } # The definition of a message subscription from a BPMN element. diff --git a/graphql-api/src/main/resources/graphql/Process.graphqls b/graphql-api/src/main/resources/graphql/Process.graphqls index e476ce92..f4914ccb 100644 --- a/graphql-api/src/main/resources/graphql/Process.graphqls +++ b/graphql-api/src/main/resources/graphql/Process.graphqls @@ -19,7 +19,10 @@ type Process { # the opened message subscriptions of the message start events of the process messageSubscriptions: [MessageSubscription!] # all BPMN elements of the process - elements: [BpmnElement!] + elements( + # Filter the BPMN elements by the given types. If empty, return all elements. + elementTypeIn: [BpmnElementType!] = [] + ): [BpmnElement!] # BPMN element of the process by its id element(elementId: String): BpmnElement } diff --git a/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlProcessTest.kt b/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlProcessTest.kt index c1182745..538eb93a 100644 --- a/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlProcessTest.kt +++ b/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlProcessTest.kt @@ -22,14 +22,25 @@ class ZeebeGraphqlProcessTest( private val graphqlAssertions = GraphqlAssertions(port = port) + private val processDefinitionKey = 10L + @BeforeEach fun `deploy process`() { + val process = Bpmn.createExecutableProcess("process") + .startEvent("start") + .sequenceFlowId("to-task") + .serviceTask("service-task") + .zeebeJobType("test") + .sequenceFlowId("to-end") + .endEvent("end") + .done(); + processRepository.save( Process( - key = 1, + key = processDefinitionKey, bpmnProcessId = "process", version = 1, - bpmnXML = "<...>", + bpmnXML = Bpmn.convertToString(process), deployTime = Instant.now().toEpochMilli(), resourceName = "process.bpmn", checksum = "checksum" @@ -58,7 +69,7 @@ class ZeebeGraphqlProcessTest( "processes": { "nodes": [ { - "key": "1", + "key": "$processDefinitionKey", "bpmnProcessId": "process", "version": 1 } @@ -70,6 +81,76 @@ class ZeebeGraphqlProcessTest( ) } + @Test + fun `should get elements of process`() { + // when/then + graphqlAssertions.assertQuery( + query = """ + { + process(key: $processDefinitionKey) { + elements { + elementId + } + } + } + """, + expectedResponseBody = """ + { + "data": { + "process": { + "elements": [ + { + "elementId": "to-task" + }, + { + "elementId": "to-end" + }, + { + "elementId": "end" + }, + { + "elementId": "service-task" + }, + { + "elementId": "start" + } + ] + } + } + } + """ + ) + } + + @Test + fun `should filter elements of process by their type`() { + // when/then + graphqlAssertions.assertQuery( + query = """ + { + process(key: $processDefinitionKey) { + elements(elementTypeIn: [SERVICE_TASK]) { + elementId + } + } + } + """, + expectedResponseBody = """ + { + "data": { + "process": { + "elements": [ + { + "elementId": "service-task" + } + ] + } + } + } + """ + ) + } + @SpringBootApplication class TestConfiguration diff --git a/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlUserTaskTest.kt b/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlUserTaskTest.kt index 208efbf3..e653e68c 100644 --- a/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlUserTaskTest.kt +++ b/graphql-api/src/test/kotlin/io/zeebe/zeeqs/ZeebeGraphqlUserTaskTest.kt @@ -84,8 +84,7 @@ class ZeebeGraphqlUserTaskTest( elementInstanceKey = 1L, assignee = "test", candidateGroups = "[\"test-group\"]", - formKey = formKey, - isCamundaForm = true + formKey = formKey ) ) } diff --git a/hazelcast-importer/src/main/kotlin/io/zeebe/zeeqs/importer/hazelcast/HazelcastImporter.kt b/hazelcast-importer/src/main/kotlin/io/zeebe/zeeqs/importer/hazelcast/HazelcastImporter.kt index f01d8264..ea3f3717 100644 --- a/hazelcast-importer/src/main/kotlin/io/zeebe/zeeqs/importer/hazelcast/HazelcastImporter.kt +++ b/hazelcast-importer/src/main/kotlin/io/zeebe/zeeqs/importer/hazelcast/HazelcastImporter.kt @@ -13,9 +13,6 @@ import io.zeebe.zeeqs.data.repository.* import org.springframework.stereotype.Component import java.time.Duration - -private const val CAMUNDA_FORM_KEY_PREFIX = "camunda-forms:bpmn:" - @Component class HazelcastImporter( val hazelcastConfigRepository: HazelcastConfigRepository, @@ -421,7 +418,6 @@ class HazelcastImporter( val assignee = customHeaders[Protocol.USER_TASK_ASSIGNEE_HEADER_NAME]?.stringValue val candidateGroups = customHeaders[Protocol.USER_TASK_CANDIDATE_GROUPS_HEADER_NAME]?.stringValue val formKey = customHeaders[Protocol.USER_TASK_FORM_KEY_HEADER_NAME]?.stringValue - val isCamundaForm = formKey?.startsWith(CAMUNDA_FORM_KEY_PREFIX) ?: false return UserTask( key = record.metadata.key, position = record.metadata.position, @@ -430,8 +426,7 @@ class HazelcastImporter( elementInstanceKey = record.elementInstanceKey, assignee = assignee, candidateGroups = candidateGroups, - formKey = formKey?.replace(CAMUNDA_FORM_KEY_PREFIX, ""), - isCamundaForm = isCamundaForm + formKey = formKey ) } diff --git a/hazelcast-importer/src/test/kotlin/io/zeebe/zeeqs/HazelcastImporterJobTest.kt b/hazelcast-importer/src/test/kotlin/io/zeebe/zeeqs/HazelcastImporterJobTest.kt index bb94e33b..d1781eb9 100644 --- a/hazelcast-importer/src/test/kotlin/io/zeebe/zeeqs/HazelcastImporterJobTest.kt +++ b/hazelcast-importer/src/test/kotlin/io/zeebe/zeeqs/HazelcastImporterJobTest.kt @@ -124,8 +124,7 @@ class HazelcastImporterJobTest( assertThat(userTask.endTime).isNull() assertThat(userTask.assignee).isEqualTo("test") assertThat(userTask.candidateGroups).isEqualTo("""["test-group"]""") - assertThat(userTask.formKey).isEqualTo("form_A") - assertThat(userTask.isCamundaForm).isTrue() + assertThat(userTask.formKey).isEqualTo("camunda-forms:bpmn:form_A") } @SpringBootApplication