Skip to content

Commit

Permalink
Add JPA Queries for task attribute names + values #843
Browse files Browse the repository at this point in the history
  • Loading branch information
p-wunderlich committed Jul 15, 2024
1 parent 781cb01 commit 2f31eac
Show file tree
Hide file tree
Showing 17 changed files with 391 additions and 19 deletions.
28 changes: 15 additions & 13 deletions docs/reference-guide/components/view-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,21 @@ and generic query paging and sorting.

The Task API allows to query for tasks handled by the task-pool.

| Query Type | Description | Payload types | In-Memory | JPA | Mongo DB |
|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|-----------|------------|----------|
| AllTasksQuery | Retrieves a list of tasks applying additional filters | List<Task> | yes | yes | no |
| TasksForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters | List<Task> | yes | yes | yes |
| TasksForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters | List<Task> | yes | yes | no |
| TasksForCandidateUserAndGroupQuery | Retrieves a list of tasks accessible by the user because listed as candidate and the user's group and applying additional filters | List<Task> | yes | yes | no |
| TaskForIdQuery | Retrieves a task by id (without any other filters) | Task or null | yes | yes | yes |
| TasksForApplicationQuery | Retrieves all tasks by given application name (without any further filters) | List<Task> | yes | yes | yes |
| AllTasksWithDataEntriesQuery | Retrieves a list of tasks applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | no |
| TasksWithDataEntriesForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | no |
| TasksWithDataEntriesForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | yes |
| TaskWithDataEntriesForIdQuery | Retrieves a task by id and correlates result with data entries, if available | (Task, List<DataEntry>) or null | yes | yes | yes |
| TaskCountByApplicationQuery | Counts tasks grouped by application names, useful for monitoring | List<(ApplicationName, Count)> | yes | no | yes |
| Query Type | Description | Payload types | In-Memory | JPA | Mongo DB |
|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|-----------|------------|----------|
| AllTasksQuery | Retrieves a list of tasks applying additional filters | List<Task> | yes | yes | no |
| TasksForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters | List<Task> | yes | yes | yes |
| TasksForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters | List<Task> | yes | yes | no |
| TasksForCandidateUserAndGroupQuery | Retrieves a list of tasks accessible by the user because listed as candidate and the user's group and applying additional filters | List<Task> | yes | yes | no |
| TaskForIdQuery | Retrieves a task by id (without any other filters) | Task or null | yes | yes | yes |
| TasksForApplicationQuery | Retrieves all tasks by given application name (without any further filters) | List<Task> | yes | yes | yes |
| AllTasksWithDataEntriesQuery | Retrieves a list of tasks applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | no |
| TasksWithDataEntriesForGroupQuery | Retrieves a list of tasks accessible by the user's group and applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | no |
| TasksWithDataEntriesForUserQuery | Retrieves a list of tasks accessible by the user and applying additional filters and correlates result with data entries, if available | List<(Task, List<DataEntry>) | yes | incubation | yes |
| TaskWithDataEntriesForIdQuery | Retrieves a task by id and correlates result with data entries, if available | (Task, List<DataEntry>) or null | yes | yes | yes |
| TaskCountByApplicationQuery | Counts tasks grouped by application names, useful for monitoring | List<(ApplicationName, Count)> | yes | no | yes |
| TaskAttributeNamesQuery | Retrieves a list of all task (payload) attribut names | List<(String, Count)> | yes | no | yes |
| TaskAttributeValuesQuery | Retrieves a list of task (payload) attribut values for given name | List<(String, Count)> | yes | yes | no |


### Process Definition API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ internal fun Pair<String, Any?>.toJsonPathWithValue(
} else if (value is List<*>) {
value.map { (key to it).toJsonPathWithValue(prefix, limit, filter) }.flatten()
} else {
// ignore complex objects
// ignore complex objects, in default scenarios, complex objects got already deserialized by the sender in ProjectingCommandAccumulator.serializePayloadIfNeeded
listOf()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.holixon.axon.gateway.query.RevisionValue
import io.holunda.camunda.taskpool.api.task.*
import io.holunda.polyflow.view.Task
import io.holunda.polyflow.view.TaskWithDataEntries
import io.holunda.polyflow.view.auth.User
import io.holunda.polyflow.view.filter.toCriteria
import io.holunda.polyflow.view.jpa.JpaPolyflowViewTaskService.Companion.PROCESSING_GROUP
import io.holunda.polyflow.view.jpa.auth.AuthorizationPrincipal
Expand Down Expand Up @@ -251,6 +252,28 @@ class JpaPolyflowViewTaskService(
)
}

@QueryHandler
override fun query(query: TaskAttributeNamesQuery): TaskAttributeNamesQueryResult {
val assignee = if(query.assignedToMeOnly) query.user?.username else null
val distinctKeys = taskRepository.getTaskAttributeNames(assignee, query.user?.toAuthorizationPrincipalStrings())

return TaskAttributeNamesQueryResult(
elements = distinctKeys.toList(),
totalElementCount = distinctKeys.size
)
}

@QueryHandler
override fun query(query: TaskAttributeValuesQuery): TaskAttributeValuesQueryResult {
val assignee = if(query.assignedToMeOnly) query.user?.username else null
val distinctValues = taskRepository.getTaskAttributeValues(query.attributeName, assignee, query.user?.toAuthorizationPrincipalStrings())

return TaskAttributeValuesQueryResult(
elements = distinctValues.toList(),
totalElementCount = distinctValues.size
)
}

@QueryHandler
override fun query(query: TaskWithDataEntriesForIdQuery): Optional<TaskWithDataEntries> {
return Optional.ofNullable(taskRepository.findByIdOrNull(query.id)?.let { taskEntity ->
Expand Down Expand Up @@ -496,3 +519,7 @@ class JpaPolyflowViewTaskService(
}

}

private fun User.toAuthorizationPrincipalStrings(): Set<String> {
return this.groups.map(AuthorizationPrincipal.Companion::group).map { it.toString() }.toSet() + user(this.username).toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,6 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
}
}


/**
* Counts user tasks grouped by application name, resulting in a total amount of tasks per application (=process engine).
* Helpful for monitoring of tasks on the task pool projection side vs. engine side.
Expand All @@ -375,4 +374,36 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
@Query("select new io.holunda.polyflow.view.jpa.CountByApplication(t.sourceReference.applicationName, count(t) ) from TaskEntity t group by t.sourceReference.applicationName")
fun getCountByApplication(): List<CountByApplication>

/**
* Returns all task payload attribut names (path).
* If assignee is given, just attributes for the given assignee is queried.
* If authorizedPrincipals are given, just attributes for the given authorizedPrincipals are queried.
* @return list of task payload attribut names (path)
*/
@Query("""
select att.path
from TaskEntity task
join task.payloadAttributes att
join task.authorizedPrincipals auth
where (?1 is null or task.assignee = ?1)
and (?2 is null or auth in ?2)
""")
fun getTaskAttributeNames(assignee: String?, principals: Set<String>?): Set<String>

/**
* Returns a list of all task payload attribute values for the given task payload attribute name.
* If assignee is given, just attributes for the given assignee is queried.
* If authorizedPrincipals are given, just attributes for the given authorizedPrincipals are queried.
* @return list of task payload attribut values
*/
@Query("""
select att.value
from TaskEntity task
join task.payloadAttributes att
join task.authorizedPrincipals auth
where att.path = ?1
and (?2 is null or task.assignee = ?2)
and (?3 is null or auth in ?3)
""")
fun getTaskAttributeValues(attributeName: String, assignee: String?, principals: Set<String>?): Set<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,34 @@ internal class JpaPolyflowViewServiceTaskITest {
assertThat(counts[0].taskCount).isEqualTo(3)
}

@Test
fun `should find task attribute names`() {
// Some for zoro in muppets
val names = jpaPolyflowViewService.query(TaskAttributeNamesQuery(user = User("zoro", setOf("muppets"))))
assertThat(names).isNotNull
assertThat(names.elements).hasSize(4)
assertThat(names.elements).contains("key", "key-int", "complex.attribute1", "complex.attribute2")

// But none for bud in heros
val namesOSH = jpaPolyflowViewService.query(TaskAttributeNamesQuery(user = User("bud", setOf("old_school_heros"))))
assertThat(namesOSH).isNotNull
assertThat(namesOSH.elements).hasSize(0)
}

@Test
fun `should find task attribute values`() {
// Some for zoro in muppets
val names = jpaPolyflowViewService.query(TaskAttributeValuesQuery(user = User("zoro", setOf("muppets")), attributeName = "key"))
assertThat(names).isNotNull
assertThat(names.elements).hasSize(2)
assertThat(names.elements).contains("value", "otherValue")

// But none for bud in heros
val namesOSH = jpaPolyflowViewService.query(TaskAttributeValuesQuery(user = User("bud", setOf("old_school_heros")), attributeName = "key"))
assertThat(namesOSH).isNotNull
assertThat(namesOSH.elements).hasSize(0)
}

private fun captureEmittedQueryUpdates(): List<QueryUpdate<Any>> {
val queryTypeCaptor = argumentCaptor<Class<Any>>()
val predicateCaptor = argumentCaptor<Predicate<Any>>()
Expand Down Expand Up @@ -561,7 +589,9 @@ internal class JpaPolyflowViewServiceTaskITest {
return mapOf(
"key" to value,
"key-int" to 1,
"complex" to Pojo(
"complex.attribute1" to "value",
"complex.attribute2" to Date.from(now),
"complexIgnored" to Pojo( // Normally, the event will never have a complex object like this in the payload. (Got already deserialized by the sender in ProjectingCommandAccumulator.serializePayloadIfNeeded)
attribute1 = "value",
attribute2 = Date.from(now)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.holunda.polyflow.view.TaskWithDataEntries
import io.holunda.polyflow.view.filter.createTaskPredicates
import io.holunda.polyflow.view.filter.filterByPredicate
import io.holunda.polyflow.view.filter.toCriteria
import io.holunda.polyflow.view.filter.toPayloadPredicates
import io.holunda.polyflow.view.query.task.*
import io.holunda.polyflow.view.simple.updateMapFilterQuery
import io.holunda.polyflow.view.sort.taskComparator
Expand All @@ -18,6 +19,7 @@ import org.axonframework.config.ProcessingGroup
import org.axonframework.eventhandling.EventHandler
import org.axonframework.queryhandling.QueryHandler
import org.axonframework.queryhandling.QueryUpdateEmitter
import org.camunda.bpm.engine.variable.VariableMap
import org.springframework.stereotype.Component
import java.util.*
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -199,6 +201,52 @@ class SimpleTaskPoolService(
return queryForTasks(query)
}

/**
* Retrieves all task attribute names
*/
@QueryHandler
override fun query(query: TaskAttributeNamesQuery): TaskAttributeNamesQueryResult {
val filterAssignee = query.assignedToMeOnly && query.user != null
val filterCandidates = query.user != null

val distinctFilteredKeys = tasks.values.asSequence()
.filter { !filterAssignee || it.assignee == query.user!!.username }
.filter { task -> !filterCandidates || (task.candidateUsers.contains(query.user!!.username) || task.candidateGroups.any { query.user!!.groups.contains(it) } ) }
.map(Task::payload)
.flatMap(VariableMap::keys)
.distinct()
.toList()

return TaskAttributeNamesQueryResult(
elements = distinctFilteredKeys,
totalElementCount = distinctFilteredKeys.size
)
}

/**
* Retrieves all task attribute values for an attribute name
*/
@QueryHandler
override fun query(query: TaskAttributeValuesQuery): TaskAttributeValuesQueryResult {
val filterAssignee = query.assignedToMeOnly && query.user != null
val filterCandidates = query.user != null

val distinctFilteredValues = tasks.values.asSequence()
.filter { !filterAssignee || it.assignee == query.user!!.username }
.filter { task -> !filterCandidates || (task.candidateUsers.contains(query.user!!.username) || task.candidateGroups.any { query.user!!.groups.contains(it) } ) }
.map(Task::payload)
.filter { it.containsKey(query.attributeName) }
.mapNotNull { it[query.attributeName] }
.distinct()
.toList()

return TaskAttributeValuesQueryResult(
elements = distinctFilteredValues,
totalElementCount = distinctFilteredValues.size
)
}

@QueryHandler
private fun queryForTasks(query: PageableSortableFilteredTaskQuery): TaskQueryResult {
val predicates = createTaskPredicates(toCriteria(query.filters))
val filtered = tasks.values.filter { query.applyFilter(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,78 @@ class SimpleTaskPoolServiceTest : ScenarioTest<SimpleTaskPoolGivenStage<*>, Simp
.all_task_are_returned_and_sorted_by(reversed = true) { it.task.businessKey }
}

@Test
fun `should find task attribute names`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_names_are_queried("kermit", "muppetshow")

then()
.attribute_names_are_returned(3)
}

@Test
fun `should not find task attribute names if there is no matching candidate user`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_names_are_queried("bud", "old_school_heros")

then()
.attribute_names_are_returned(0)
}

@Test
fun `should not find task attribute names for wrong assignee`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_names_are_queried_for_assigned_user(user = "bud", group = null)

then()
.attribute_names_are_returned(0)
}

@Test
fun `should find task attribute values`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_values_are_queried("payloadIdString", "kermit", "muppetshow")

then()
.attribute_values_are_returned(3)
}

@Test
fun `should not find task attribute values if there is no matching candidate user`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_values_are_queried("payloadIdString", "bud", "old_school_heros")

then()
.attribute_values_are_returned(0)
}

@Test
fun `should not find task attribute values for wrong assignee`() {
given()
.tasks_exist(3)

`when`()
.task_attribute_values_are_queried_for_assigned_user("payloadIdString", "bud", null)

then()
.attribute_values_are_returned(0)
}

private infix fun String.withTaskCount(taskCount: Int) = ApplicationWithTaskCount(this, taskCount)
}

Expand Down
Loading

0 comments on commit 2f31eac

Please sign in to comment.