Skip to content

Commit

Permalink
Add date range operator for due date and follow-up date of tasks (#1060)
Browse files Browse the repository at this point in the history
* feat(#1059): added date range operator

* feat(#1059): added tests

* chore(#1059): fixed tests

* chore(#1059): update checkout and upload-sarif versions

also added github-actions to dependabot.yml
  • Loading branch information
S-Tim authored Oct 10, 2024
1 parent 9691cd2 commit 0269a03
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 25 deletions.
21 changes: 13 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
version: 2
updates:
- package-ecosystem: maven
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
labels:
- "Type: dependencies"

- package-ecosystem: maven
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 19
labels:
- "Type: dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "Type: dependencies"
4 changes: 2 additions & 2 deletions .github/workflows/codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
Expand All @@ -58,6 +58,6 @@ jobs:

# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
Expand Down
1 change: 1 addition & 0 deletions docs/reference-guide/components/view-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Following operations are supported:
| `<` | Less than | all, payload | `followUpDate`, `dueDate` | none | all, payload | all, payload |
| `>` | Greater than | all, payload | `followUpDate`, `dueDate` | none | all, payload | all, payload |
| `=` | Equals | all, payload | payload, `businessKey`, `followUpDate`, `dueDate`, `priority` | `entryId`, `entryType`, `type`, payload, `processingState`, `userStatus` | all, payload | all, payload |
| `[]` | Between | comparable | `followUpDate`, `dueDate` | none | none | none |
| `%` | Like | all, payload | `businessKey`, `name`, `description`, `processName`, `textSearch` | none | none | none |

!!! info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasBusinessKey
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDate
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDateAfter
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDateBefore
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasDueDateBetween
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDate
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDateAfter
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDateBefore
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasFollowUpDateBetween
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasPriority
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasProcessName
import io.holunda.polyflow.view.jpa.task.TaskRepository.Companion.hasTaskOrDataEntryPayloadAttribute
Expand Down Expand Up @@ -266,10 +268,24 @@ internal fun Criterion.TaskCriterion.toTaskSpecification(): Specification<TaskEn
}
}

BETWEEN -> {
when (this.name) {
Task::dueDate.name -> hasDueDateBetween(this.value.toDatePair())
Task::followUpDate.name -> hasFollowUpDateBetween(this.value.toDatePair())
else -> throw IllegalArgumentException("JPA View found unsupported task attribute for [] (date range) comparison: ${this.name}.")
}
}

else -> throw IllegalArgumentException("JPA View found unsupported comparison ${this.operator} for attribute ${this.name}.")
}
}

private fun String.toDatePair(): Pair<Instant, Instant> {
val dates = this.split("|")
require(dates.size == 2) { "Value does not contain exactly two dates separated by |." }
return dates.map { Instant.parse(it) }.let { it[0] to it[1] }
}

/**
* Creates JPA Specification for query of payload attributes based on JSON paths. All criteria must have the same path
* and will be composed by the logical OR operator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
fun hasDueDateBefore(dueDate: Instant): Specification<TaskEntity> =
Specification { task, _, builder ->
builder.or(
builder.isNull(task.get<Instant>(TaskEntity::followUpDate.name)),
builder.isNull(task.get<Instant>(TaskEntity::dueDate.name)),
builder.lessThan(
task.get(TaskEntity::dueDate.name),
dueDate
Expand All @@ -145,14 +145,35 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
fun hasDueDateAfter(dueDate: Instant): Specification<TaskEntity> =
Specification { task, _, builder ->
builder.or(
builder.isNull(task.get<Instant>(TaskEntity::followUpDate.name)),
builder.isNull(task.get<Instant>(TaskEntity::dueDate.name)),
builder.greaterThan(
task.get(TaskEntity::dueDate.name),
dueDate
)
)
}

/**
* Specification for checking if the due date is in the specified range.
*/
fun hasDueDateBetween(range: Pair<Instant, Instant>): Specification<TaskEntity> =
Specification { task, _, builder ->
val (from, to) = range
builder.or(
builder.isNull(task.get<Instant>(TaskEntity::dueDate.name)),
builder.and(
builder.greaterThan(
task.get(TaskEntity::dueDate.name),
from
),
builder.lessThan(
task.get(TaskEntity::dueDate.name),
to
),
)
)
}

/**
* Specification for checking the follow-up date.
*/
Expand Down Expand Up @@ -192,6 +213,27 @@ interface TaskRepository : CrudRepository<TaskEntity, String>, JpaSpecificationE
)
}

/**
* Specification for checking if the follow-up date is in the specified range.
*/
fun hasFollowUpDateBetween(range: Pair<Instant, Instant>): Specification<TaskEntity> =
Specification { task, _, builder ->
val (from, to) = range
builder.or(
builder.isNull(task.get<Instant>(TaskEntity::followUpDate.name)),
builder.and(
builder.greaterThan(
task.get(TaskEntity::followUpDate.name),
from
),
builder.lessThan(
task.get(TaskEntity::followUpDate.name),
to
),
)
)
}

/**
* Specification for checking the name likeness.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.transaction.annotation.Transactional
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.function.Predicate

Expand Down Expand Up @@ -92,7 +92,8 @@ internal class JpaPolyflowViewServiceTaskITest {
businessKey = "business-1",
createTime = Date.from(Instant.now()),
candidateUsers = setOf("kermit"),
candidateGroups = setOf("muppets")
candidateGroups = setOf("muppets"),
dueDate = Date.from(now)
), metaData = MetaData.emptyInstance()
)

Expand Down Expand Up @@ -156,7 +157,8 @@ internal class JpaPolyflowViewServiceTaskITest {
businessKey = "business-3",
createTime = Date.from(Instant.now()),
candidateUsers = setOf("luffy"),
candidateGroups = setOf("strawhats")
candidateGroups = setOf("strawhats"),
dueDate = Date.from(now.plus(1, ChronoUnit.DAYS))
), metaData = MetaData.emptyInstance()
)

Expand Down Expand Up @@ -200,7 +202,8 @@ internal class JpaPolyflowViewServiceTaskITest {
businessKey = "business-4",
createTime = Date.from(Instant.now()),
candidateUsers = setOf("zoro"),
candidateGroups = setOf("strawhats")
candidateGroups = setOf("strawhats"),
dueDate = Date.from(now.plus(5, ChronoUnit.DAYS))
), metaData = MetaData.emptyInstance()
)

Expand Down Expand Up @@ -576,6 +579,26 @@ internal class JpaPolyflowViewServiceTaskITest {
assertThat(namesOSH.elements).hasSize(0)
}

@Test
fun `should find tasks by date range`() {
val range = jpaPolyflowViewService.query(
AllTasksQuery(
filters = listOf("task.dueDate[]${now.minus(1, ChronoUnit.DAYS)}|${now.plus(2, ChronoUnit.DAYS)}")
)
)
assertThat(range.elements).hasSize(2)
}

@Test
fun `should not find tasks outsides of date range`() {
val range = jpaPolyflowViewService.query(
AllTasksQuery(
filters = listOf("task.dueDate[]${now.minus(5, ChronoUnit.DAYS)}|${now.minus(2, ChronoUnit.DAYS)}")
)
)
assertThat(range.elements).isEmpty()
}

private fun captureEmittedQueryUpdates(): List<QueryUpdate<Any>> {
val queryTypeCaptor = argumentCaptor<Class<Any>>()
val predicateCaptor = argumentCaptor<Predicate<Any>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ open class TaskWithDataEntriesRepositoryExtensionImpl(
GREATER -> this.gt(it.typedValue())
LESS -> this.lt(it.typedValue())
// FIXME -> implement like
// FIXME -> implement BETWEEN
else -> throw IllegalArgumentException("Unsupported operator ${it.operator}")
}
}
Expand Down
19 changes: 17 additions & 2 deletions view/view-api/src/main/kotlin/filter/Filter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const val EQUALS = "="
const val LIKE = "%"
const val GREATER = ">"
const val LESS = "<"
const val BETWEEN = "[]"
const val TASK_PREFIX = "task."
const val DATA_PREFIX = "data."

Expand All @@ -25,7 +26,7 @@ const val DATA_PREFIX = "data."
*/
typealias CompareOperator = (Any, Any?) -> Boolean

val OPERATORS = Regex("[$EQUALS$LESS$GREATER$LIKE]")
val OPERATORS = listOf(EQUALS, LIKE, GREATER, LESS, BETWEEN)

/**
* Implemented comparison support for some data types.
Expand Down Expand Up @@ -61,6 +62,19 @@ internal fun compareOperator(sign: String): CompareOperator =
}
}

BETWEEN -> { filter, actual ->
when(actual) {
is Comparable<*> -> {
val (from, to) = filter.toString().split("|")
compareOperator(GREATER)
.invoke(from, actual).and(compareOperator(LESS)
.invoke(to, actual))
}
null -> true // match tasks where actual is null
else -> throw IllegalArgumentException("Unsupported actual type ${actual.javaClass.name} for between operator. Type must be comparable")
}
}

EQUALS -> { filter, actual -> filter.toString() == actual.toString() }

else -> throw IllegalArgumentException("Unsupported operator $sign")
Expand Down Expand Up @@ -235,7 +249,7 @@ internal fun toCriterion(filter: String): Criterion {

require(filter.isNotBlank()) { "Failed to create criteria from empty filter '$filter'." }

if (!filter.contains(OPERATORS)) {
if (!OPERATORS.any { filter.contains(it) }) {
return Criterion.EmptyCriterion
}

Expand All @@ -244,6 +258,7 @@ internal fun toCriterion(filter: String): Criterion {
filter.contains(LIKE) -> filter.split(LIKE).plus(LIKE)
filter.contains(GREATER) -> filter.split(GREATER).plus(GREATER)
filter.contains(LESS) -> filter.split(LESS).plus(LESS)
filter.contains(BETWEEN) -> filter.split(BETWEEN).plus(BETWEEN)
else -> listOf()
}.map { it.trim() }

Expand Down
24 changes: 18 additions & 6 deletions view/view-api/src/test/kotlin/filter/FilterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@ import java.time.temporal.ChronoUnit
class FilterTest {

private val filtersList = listOf("task.name${EQUALS}myName", "task.assignee${EQUALS}kermit", "dataAttr1${EQUALS}value", "dataAttr2${EQUALS}another")
private val now = Instant.now()

private val ref = ProcessReference("1", "2", "3", "4", "My Process", "myExample")

// no match: task.assignee, dataAttr1, dataAttr2
// match: task.name
private val task1 = TaskWithDataEntries(Task("id", ref, "key", name = "myName", priority = 90), listOf())
private val task1 = TaskWithDataEntries(Task("id1", ref, "key", name = "myName", priority = 90, dueDate = now), listOf())

// no match: task.name, dataAttr1, dataAttr2
// match: task.assignee
private val task2 = TaskWithDataEntries(Task("id", ref, "key", assignee = "kermit", priority = 91), listOf())
private val task2 = TaskWithDataEntries(Task("id2", ref, "key", assignee = "kermit", priority = 91, dueDate = now.minus(1, ChronoUnit.DAYS)), listOf())

// no match: task.name, task.assignee, dataAttr2
// match: dataEntries[0].payload -> dataAttr1
private val task3 = TaskWithDataEntries(
Task("id", ref, "key", name = "foo", assignee = "gonzo", priority = 80), listOf(
Task("id3", ref, "key", name = "foo", assignee = "gonzo", priority = 80, dueDate = now.plus(1, ChronoUnit.DAYS)), listOf(
DataEntry(
entryType = "type",
entryId = "4711",
Expand All @@ -43,7 +44,7 @@ class FilterTest {
// no match: task.name, task.assignee, dataAttr2
// match: dataEntries[0].payload -> dataAttr1
private val task4 = TaskWithDataEntries(
Task("id", ref, "key", name = "foo", assignee = "gonzo", priority = 78), listOf(
Task("id4", ref, "key", name = "foo", assignee = "gonzo", priority = 78, dueDate = now.plus(5, ChronoUnit.DAYS)), listOf(
DataEntry(
entryType = "type",
entryId = "4711",
Expand All @@ -58,7 +59,7 @@ class FilterTest {
// no match: task.name, task.assignee, dataAttr1
// match: dataEntries[0].payload -> dataAttr2
private val task5 = TaskWithDataEntries(
Task("id", ref, "key", name = "foo", assignee = "gonzo", priority = 80), listOf(
Task("id5", ref, "key", name = "foo", assignee = "gonzo", priority = 80, dueDate = now.minus(4, ChronoUnit.DAYS)), listOf(
DataEntry(
entryType = "type",
entryId = "4711",
Expand All @@ -74,7 +75,7 @@ class FilterTest {
// match: task.payload -> dataAttr2
private val task6 = TaskWithDataEntries(
Task(
"id", ref, "key", name = "foo", assignee = "gonzo", priority = 1,
"id6", ref, "key", name = "foo", assignee = "gonzo", priority = 1,
payload = Variables.createVariables().putValue("dataAttr2", "another").putValue("name", "myName")
), listOf()
)
Expand Down Expand Up @@ -271,5 +272,16 @@ class FilterTest {
assertThat(filtered).containsExactlyElementsOf(listOf(task2))
}

@Test
fun `should filter tasks by due date range`() {
val twoDaysAgo = now.minus(2, ChronoUnit.DAYS)
val inTwoDays = now.plus(2, ChronoUnit.DAYS)
val dueDateFilter = listOf("task.dueDate[]$twoDaysAgo|$inTwoDays")


val filtered = filter(dueDateFilter, listOf(task1, task2, task3, task4, task5, task6))
// task1, task2 and task3 are in the date range. task6 does not have a dueDate which is counted as a match
assertThat(filtered).containsExactlyElementsOf(listOf(task1, task2, task3, task6))
}
}

0 comments on commit 0269a03

Please sign in to comment.