Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add queries for task attribute names + values #843 #1019

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading