Skip to content

Commit f261957

Browse files
committed
feat(dao): Apply a hierarchy filter when listing repositories
Extend the `RepositoryRepository.list()` function to support a `HierarchyFiler`. This allows to query a specific set of repositories; for instance, all those a user can access. To make this possible, add some more helper extension functions for the `dao` module. Signed-off-by: Oliver Heger <[email protected]>
1 parent 9e11ee4 commit f261957

File tree

4 files changed

+304
-13
lines changed

4 files changed

+304
-13
lines changed

dao/src/main/kotlin/repositories/repository/DaoRepositoryRepository.kt

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,25 @@ package org.eclipse.apoapsis.ortserver.dao.repositories.repository
2121

2222
import org.eclipse.apoapsis.ortserver.dao.blockingQuery
2323
import org.eclipse.apoapsis.ortserver.dao.entityQuery
24+
import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable
25+
import org.eclipse.apoapsis.ortserver.dao.utils.andCond
26+
import org.eclipse.apoapsis.ortserver.dao.utils.apply
2427
import org.eclipse.apoapsis.ortserver.dao.utils.applyRegex
28+
import org.eclipse.apoapsis.ortserver.dao.utils.excludesCondition
29+
import org.eclipse.apoapsis.ortserver.dao.utils.extractIds
2530
import org.eclipse.apoapsis.ortserver.dao.utils.listQuery
31+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2632
import org.eclipse.apoapsis.ortserver.model.Hierarchy
2733
import org.eclipse.apoapsis.ortserver.model.RepositoryType
2834
import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository
2935
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
36+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3037
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
3138
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
3239

3340
import org.jetbrains.exposed.sql.Database
3441
import org.jetbrains.exposed.sql.Op
42+
import org.jetbrains.exposed.sql.SqlExpressionBuilder
3543
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
3644
import org.jetbrains.exposed.sql.and
3745
import org.jetbrains.exposed.sql.deleteWhere
@@ -56,17 +64,22 @@ class DaoRepositoryRepository(private val db: Database) : RepositoryRepository {
5664
Hierarchy(repository.mapToModel(), product.mapToModel(), organization.mapToModel())
5765
}
5866

59-
override fun list(parameters: ListQueryParameters, filter: FilterParameter?) =
67+
override fun list(parameters: ListQueryParameters, urlFilter: FilterParameter?, hierarchyFilter: HierarchyFilter) =
6068
db.blockingQuery {
61-
RepositoryDao.listQuery(parameters, RepositoryDao::mapToModel) {
62-
var condition: Op<Boolean> = Op.TRUE
63-
filter?.let {
64-
condition = condition and RepositoriesTable.url.applyRegex(
65-
it.value
66-
)
67-
}
68-
condition
69+
val urlCondition = urlFilter?.let {
70+
RepositoriesTable.url.applyRegex(it.value)
71+
} ?: Op.TRUE
72+
73+
val builder = hierarchyFilter.apply(
74+
urlCondition andCond hierarchyFilter.excludesCondition(
75+
RepositoriesTable.id,
76+
CompoundHierarchyId.REPOSITORY_LEVEL
77+
)
78+
) { level, ids, filter ->
79+
generateHierarchyCondition(level, ids, filter)
6980
}
81+
82+
RepositoryDao.listQuery(parameters, RepositoryDao::mapToModel, builder)
7083
}
7184

7285
override fun listForProduct(productId: Long, parameters: ListQueryParameters, filter: FilterParameter?) =
@@ -101,3 +114,29 @@ class DaoRepositoryRepository(private val db: Database) : RepositoryRepository {
101114
RepositoriesTable.deleteWhere { RepositoriesTable.productId eq productId }
102115
}
103116
}
117+
118+
/**
119+
* Generate a condition defined by a [HierarchyFilter] for the given [level], [ids], and [filter].
120+
*/
121+
private fun SqlExpressionBuilder.generateHierarchyCondition(
122+
level: Int,
123+
ids: List<CompoundHierarchyId>,
124+
filter: HierarchyFilter
125+
): Op<Boolean> =
126+
when (level) {
127+
CompoundHierarchyId.REPOSITORY_LEVEL ->
128+
RepositoriesTable.id inList ids.extractIds(CompoundHierarchyId.REPOSITORY_LEVEL)
129+
130+
CompoundHierarchyId.PRODUCT_LEVEL ->
131+
RepositoriesTable.productId inList ids.extractIds(CompoundHierarchyId.PRODUCT_LEVEL)
132+
133+
CompoundHierarchyId.ORGANIZATION_LEVEL -> {
134+
val subquery = ProductsTable.select(ProductsTable.id).where {
135+
(ProductsTable.organizationId inList ids.extractIds(CompoundHierarchyId.ORGANIZATION_LEVEL)) andCond
136+
filter.excludesCondition(ProductsTable.id, CompoundHierarchyId.PRODUCT_LEVEL)
137+
}
138+
RepositoriesTable.productId inSubQuery subquery
139+
}
140+
141+
else -> Op.FALSE
142+
}

dao/src/main/kotlin/utils/Extensions.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ import kotlinx.datetime.minus
2828

2929
import org.eclipse.apoapsis.ortserver.dao.ConditionBuilder
3030
import org.eclipse.apoapsis.ortserver.dao.QueryParametersException
31+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
3132
import org.eclipse.apoapsis.ortserver.model.util.ComparisonOperator
33+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3234
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
3335
import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult
3436
import org.eclipse.apoapsis.ortserver.model.util.OrderDirection
@@ -46,6 +48,7 @@ import org.jetbrains.exposed.sql.QueryParameter
4648
import org.jetbrains.exposed.sql.ResultRow
4749
import org.jetbrains.exposed.sql.SizedIterable
4850
import org.jetbrains.exposed.sql.SortOrder
51+
import org.jetbrains.exposed.sql.SqlExpressionBuilder
4952
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
5053
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
5154
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
@@ -55,6 +58,9 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
5558
import org.jetbrains.exposed.sql.SqlExpressionBuilder.neq
5659
import org.jetbrains.exposed.sql.SqlExpressionBuilder.notInList
5760
import org.jetbrains.exposed.sql.TextColumnType
61+
import org.jetbrains.exposed.sql.and
62+
import org.jetbrains.exposed.sql.not
63+
import org.jetbrains.exposed.sql.or
5864

5965
/**
6066
* Transform the given column to an [EntityID] when creating a DAO object. This can be used for foreign key columns to
@@ -80,6 +86,61 @@ fun <T : Instant?> Column<T>.transformToDatabasePrecision() =
8086
*/
8187
fun Instant.toDatabasePrecision() = minus(nanosecondsOfSecond, DateTimeUnit.NANOSECOND)
8288

89+
/**
90+
* Extract the defined IDs on the specified [level] from the [CompoundHierarchyId]s in this collection as long values.
91+
*/
92+
fun Collection<CompoundHierarchyId>.extractIds(level: Int): List<Long> = mapNotNull { it[level]?.value }
93+
94+
/**
95+
* Definition of a function type for generating query conditions based on accessible hierarchy elements. The function
96+
* has access to a [SqlExpressionBuilder] to create the conditions. It is passed the level in the hierarchy to filter
97+
* by, a list with the IDs to be included together with their child elements, and the filter itself to gain access to
98+
* additional properties. The function returns an [Op] representing the condition. The conditions for the different
99+
* hierarchy levels are then combined using an `OR` operator.
100+
*/
101+
typealias HierarchyConditionGenerator = SqlExpressionBuilder.(
102+
Int,
103+
List<CompoundHierarchyId>,
104+
HierarchyFilter
105+
) -> Op<Boolean>
106+
107+
/**
108+
* Generate a condition for this [HierarchyFilter] using the provided [generator] function. The [generator] is
109+
* responsible for creating the conditions on each hierarchy level. This function combines these conditions using an
110+
* `OR` operator. The result is then combined with the optional [otherCondition] using an `AND` operator.
111+
*/
112+
fun HierarchyFilter.apply(
113+
otherCondition: Op<Boolean> = Op.TRUE,
114+
generator: HierarchyConditionGenerator
115+
): ConditionBuilder = {
116+
if (isWildcard) {
117+
otherCondition
118+
} else {
119+
val hierarchyCondition = transitiveIncludes.entries.fold(Op.FALSE as Op<Boolean>) { op, (level, ids) ->
120+
val condition = generator(this, level, ids, this@apply)
121+
op or condition
122+
}
123+
124+
otherCondition and hierarchyCondition
125+
}
126+
}
127+
128+
/**
129+
* Generate a conditional exclusion condition for the given [filter] for the specified [column] at the given hierarchy
130+
* [level]. The function obtains the IDs to be excluded at the given [level] and constructs a `NOT IN` condition if
131+
* this list is not empty.
132+
*/
133+
fun HierarchyFilter.excludesCondition(column: Column<EntityID<Long>>, level: Int): Op<Boolean>? =
134+
excludes[level]?.takeUnless { it.isEmpty() }?.let { excludes ->
135+
not(column inList excludes.extractIds(level))
136+
}
137+
138+
/**
139+
* Construct a logic `AND` condition with an optional [op]. If [op] is null, return this condition.
140+
*/
141+
infix fun Op<Boolean>.andCond(op: Expression<Boolean>?): Op<Boolean> =
142+
op?.let { this and it } ?: this
143+
83144
/**
84145
* Run the provided [query] with the given [parameters] to create a [ListQueryResult]. The entities are mapped to the
85146
* corresponding model objects using the provided [entityMapper].

0 commit comments

Comments
 (0)