Skip to content

Commit 77d0faf

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 0479b56 commit 77d0faf

File tree

4 files changed

+191
-13
lines changed

4 files changed

+191
-13
lines changed

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,23 @@ 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.apply
2426
import org.eclipse.apoapsis.ortserver.dao.utils.applyRegex
27+
import org.eclipse.apoapsis.ortserver.dao.utils.extractIds
2528
import org.eclipse.apoapsis.ortserver.dao.utils.listQuery
29+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2630
import org.eclipse.apoapsis.ortserver.model.Hierarchy
2731
import org.eclipse.apoapsis.ortserver.model.RepositoryType
2832
import org.eclipse.apoapsis.ortserver.model.repositories.RepositoryRepository
2933
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
34+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3035
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
3136
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
3237

3338
import org.jetbrains.exposed.sql.Database
3439
import org.jetbrains.exposed.sql.Op
40+
import org.jetbrains.exposed.sql.SqlExpressionBuilder
3541
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
3642
import org.jetbrains.exposed.sql.and
3743
import org.jetbrains.exposed.sql.deleteWhere
@@ -56,17 +62,17 @@ class DaoRepositoryRepository(private val db: Database) : RepositoryRepository {
5662
Hierarchy(repository.mapToModel(), product.mapToModel(), organization.mapToModel())
5763
}
5864

59-
override fun list(parameters: ListQueryParameters, filter: FilterParameter?) =
65+
override fun list(parameters: ListQueryParameters, urlFilter: FilterParameter?, hierarchyFilter: HierarchyFilter) =
6066
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
67+
val urlCondition = urlFilter?.let {
68+
RepositoriesTable.url.applyRegex(it.value)
69+
} ?: Op.TRUE
70+
71+
val builder = hierarchyFilter.apply(urlCondition) { level, ids, _ ->
72+
generateHierarchyCondition(level, ids)
6973
}
74+
75+
RepositoryDao.listQuery(parameters, RepositoryDao::mapToModel, builder)
7076
}
7177

7278
override fun listForProduct(productId: Long, parameters: ListQueryParameters, filter: FilterParameter?) =
@@ -101,3 +107,27 @@ class DaoRepositoryRepository(private val db: Database) : RepositoryRepository {
101107
RepositoriesTable.deleteWhere { RepositoriesTable.productId eq productId }
102108
}
103109
}
110+
111+
/**
112+
* Generate a condition defined by a [HierarchyFilter] for the given [level] and [ids].
113+
*/
114+
private fun SqlExpressionBuilder.generateHierarchyCondition(
115+
level: Int,
116+
ids: List<CompoundHierarchyId>
117+
): Op<Boolean> =
118+
when (level) {
119+
CompoundHierarchyId.REPOSITORY_LEVEL ->
120+
RepositoriesTable.id inList ids.extractIds(CompoundHierarchyId.REPOSITORY_LEVEL)
121+
122+
CompoundHierarchyId.PRODUCT_LEVEL ->
123+
RepositoriesTable.productId inList ids.extractIds(CompoundHierarchyId.PRODUCT_LEVEL)
124+
125+
CompoundHierarchyId.ORGANIZATION_LEVEL -> {
126+
val subquery = ProductsTable.select(ProductsTable.id).where {
127+
ProductsTable.organizationId inList ids.extractIds(CompoundHierarchyId.ORGANIZATION_LEVEL)
128+
}
129+
RepositoriesTable.productId inSubQuery subquery
130+
}
131+
132+
else -> Op.FALSE
133+
}

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

Lines changed: 44 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,8 @@ 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.or
5863

5964
/**
6065
* Transform the given column to an [EntityID] when creating a DAO object. This can be used for foreign key columns to
@@ -80,6 +85,45 @@ fun <T : Instant?> Column<T>.transformToDatabasePrecision() =
8085
*/
8186
fun Instant.toDatabasePrecision() = minus(nanosecondsOfSecond, DateTimeUnit.NANOSECOND)
8287

88+
/**
89+
* Extract the defined IDs on the specified [level] from the [CompoundHierarchyId]s in this collection as long values.
90+
*/
91+
fun Collection<CompoundHierarchyId>.extractIds(level: Int): List<Long> = mapNotNull { it[level]?.value }
92+
93+
/**
94+
* Definition of a function type for generating query conditions based on accessible hierarchy elements. The function
95+
* has access to a [SqlExpressionBuilder] to create the conditions. It is passed the level in the hierarchy to filter
96+
* by, a list with the IDs to be included together with their child elements, and the filter itself to gain access to
97+
* additional properties. The function returns an [Op] representing the condition. The conditions for the different
98+
* hierarchy levels are then combined using an `OR` operator.
99+
*/
100+
typealias HierarchyConditionGenerator = SqlExpressionBuilder.(
101+
Int,
102+
List<CompoundHierarchyId>,
103+
HierarchyFilter
104+
) -> Op<Boolean>
105+
106+
/**
107+
* Generate a condition for this [HierarchyFilter] using the provided [generator] function. The [generator] is
108+
* responsible for creating the conditions on each hierarchy level. This function combines these conditions using an
109+
* `OR` operator. The result is then combined with the optional [otherCondition] using an `AND` operator.
110+
*/
111+
fun HierarchyFilter.apply(
112+
otherCondition: Op<Boolean> = Op.TRUE,
113+
generator: HierarchyConditionGenerator
114+
): ConditionBuilder = {
115+
if (isWildcard) {
116+
otherCondition
117+
} else {
118+
val hierarchyCondition = transitiveIncludes.entries.fold(Op.FALSE as Op<Boolean>) { op, (level, ids) ->
119+
val condition = generator(this, level, ids, this@apply)
120+
op or condition
121+
}
122+
123+
otherCondition and hierarchyCondition
124+
}
125+
}
126+
83127
/**
84128
* Run the provided [query] with the given [parameters] to create a [ListQueryResult]. The entities are mapped to the
85129
* corresponding model objects using the provided [entityMapper].

dao/src/test/kotlin/repositories/repository/DaoRepositoryRepositoryTest.kt

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ import io.kotest.matchers.shouldBe
2828
import org.eclipse.apoapsis.ortserver.dao.UniqueConstraintException
2929
import org.eclipse.apoapsis.ortserver.dao.test.DatabaseTestExtension
3030
import org.eclipse.apoapsis.ortserver.dao.test.Fixtures
31+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
3132
import org.eclipse.apoapsis.ortserver.model.Hierarchy
33+
import org.eclipse.apoapsis.ortserver.model.OrganizationId
34+
import org.eclipse.apoapsis.ortserver.model.ProductId
3235
import org.eclipse.apoapsis.ortserver.model.Repository
36+
import org.eclipse.apoapsis.ortserver.model.RepositoryId
3337
import org.eclipse.apoapsis.ortserver.model.RepositoryType
3438
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
39+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3540
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
3641
import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult
3742
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
@@ -121,7 +126,7 @@ class DaoRepositoryRepositoryTest : StringSpec({
121126
fixtures.createRepository(url = "https://example.com/repo3.git")
122127
fixtures.createRepository(url = "https://example.com/repo4.git")
123128

124-
repositoryRepository.list(filter = FilterParameter("repository.git$")) shouldBe ListQueryResult(
129+
repositoryRepository.list(urlFilter = FilterParameter("repository.git$")) shouldBe ListQueryResult(
125130
data = listOf(
126131
Repository(repo1.id, orgId, repo1.productId, repo1.type, repo1.url, repo1.description),
127132
Repository(repo2.id, orgId, repo2.productId, repo2.type, repo2.url, repo2.description),
@@ -142,7 +147,7 @@ class DaoRepositoryRepositoryTest : StringSpec({
142147
url = "https://subdomain.example.com/repo.git"
143148
)
144149

145-
val result = repositoryRepository.list(filter = FilterParameter("example\\.com"))
150+
val result = repositoryRepository.list(urlFilter = FilterParameter("example\\.com"))
146151

147152
result shouldBe ListQueryResult(
148153
data = listOf(
@@ -155,6 +160,102 @@ class DaoRepositoryRepositoryTest : StringSpec({
155160
)
156161
}
157162

163+
"list should apply a hierarchy filter on repository level" {
164+
val repo1 = fixtures.createRepository(url = "https://example.com/repo1.git")
165+
val repo1Id = CompoundHierarchyId.forRepository(
166+
OrganizationId(fixtures.organization.id),
167+
ProductId(fixtures.product.id),
168+
RepositoryId(repo1.id)
169+
)
170+
val repo2 = fixtures.createRepository(url = "https://example.com/repo2.git")
171+
val repo2Id = CompoundHierarchyId.forRepository(
172+
OrganizationId(fixtures.organization.id),
173+
ProductId(fixtures.product.id),
174+
RepositoryId(repo2.id)
175+
)
176+
fixtures.createRepository(url = "https://example.com/repo3.git")
177+
178+
val hierarchyFilter = HierarchyFilter(
179+
transitiveIncludes = mapOf(CompoundHierarchyId.REPOSITORY_LEVEL to listOf(repo1Id, repo2Id)),
180+
nonTransitiveIncludes = emptyMap()
181+
)
182+
val result = repositoryRepository.list(hierarchyFilter = hierarchyFilter)
183+
184+
result shouldBe ListQueryResult(
185+
data = listOf(
186+
Repository(repo1.id, orgId, repo1.productId, repo1.type, repo1.url, repo1.description),
187+
Repository(repo2.id, orgId, repo2.productId, repo2.type, repo2.url, repo2.description),
188+
),
189+
params = ListQueryParameters.DEFAULT,
190+
totalCount = 2
191+
)
192+
}
193+
194+
"list should apply a hierarchy filter on product level" {
195+
val product1 = fixtures.createProduct(name = "product1")
196+
val product1Id = CompoundHierarchyId.forProduct(
197+
OrganizationId(fixtures.organization.id),
198+
ProductId(product1.id)
199+
)
200+
val repo1 = fixtures.createRepository(url = "https://example.com/repo1.git", productId = product1.id)
201+
val product2 = fixtures.createProduct(name = "product2")
202+
val product2Id = CompoundHierarchyId.forProduct(
203+
OrganizationId(fixtures.organization.id),
204+
ProductId(product2.id)
205+
)
206+
val repo2 = fixtures.createRepository(url = "https://example.com/repo2.git", productId = product2.id)
207+
fixtures.createRepository(url = "https://example.com/repo3.git")
208+
209+
val hierarchyFilter = HierarchyFilter(
210+
transitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(product1Id, product2Id)),
211+
nonTransitiveIncludes = emptyMap()
212+
)
213+
val result = repositoryRepository.list(hierarchyFilter = hierarchyFilter)
214+
215+
result shouldBe ListQueryResult(
216+
data = listOf(
217+
Repository(repo1.id, orgId, repo1.productId, repo1.type, repo1.url, repo1.description),
218+
Repository(repo2.id, orgId, repo2.productId, repo2.type, repo2.url, repo2.description),
219+
),
220+
params = ListQueryParameters.DEFAULT,
221+
totalCount = 2
222+
)
223+
}
224+
225+
"list should apply a hierarchy filter on organization level" {
226+
val organization1 = fixtures.createOrganization(name = "testOrganization")
227+
val organization1Id = CompoundHierarchyId.forOrganization(OrganizationId(organization1.id))
228+
val product1 = fixtures.createProduct(name = "product1", organizationId = organization1.id)
229+
val repo1 = fixtures.createRepository(url = "https://example.com/repo1.git", productId = product1.id)
230+
231+
val organization2 = fixtures.createOrganization(name = "organization2")
232+
val organization2Id = CompoundHierarchyId.forOrganization(OrganizationId(organization2.id))
233+
val product2 = fixtures.createProduct(name = "product2", organizationId = organization2.id)
234+
val repo2 = fixtures.createRepository(url = "https://example.com/repo2.git", productId = product2.id)
235+
236+
fixtures.createRepository(url = "https://example.com/repo3.git")
237+
238+
val hierarchyFilter = HierarchyFilter(
239+
transitiveIncludes = mapOf(
240+
CompoundHierarchyId.ORGANIZATION_LEVEL to listOf(
241+
organization1Id,
242+
organization2Id
243+
)
244+
),
245+
nonTransitiveIncludes = emptyMap()
246+
)
247+
val result = repositoryRepository.list(hierarchyFilter = hierarchyFilter)
248+
249+
result shouldBe ListQueryResult(
250+
data = listOf(
251+
Repository(repo1.id, organization1.id, repo1.productId, repo1.type, repo1.url, repo1.description),
252+
Repository(repo2.id, organization2.id, repo2.productId, repo2.type, repo2.url, repo2.description),
253+
),
254+
params = ListQueryParameters.DEFAULT,
255+
totalCount = 2
256+
)
257+
}
258+
158259
"listForProduct should return all repositories for a product" {
159260
val type = RepositoryType.GIT
160261

model/src/commonMain/kotlin/repositories/RepositoryRepository.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.eclipse.apoapsis.ortserver.model.Hierarchy
2323
import org.eclipse.apoapsis.ortserver.model.Repository
2424
import org.eclipse.apoapsis.ortserver.model.RepositoryType
2525
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
26+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
2627
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
2728
import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult
2829
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
@@ -48,11 +49,13 @@ interface RepositoryRepository {
4849
fun getHierarchy(id: Long): Hierarchy
4950

5051
/**
51-
* List all repositories according to the given [parameters].
52+
* List all repositories according to the given [parameters]. Optionally, a [urlFilter] on the repository URL and a
53+
* [hierarchyFilter] can be provided.
5254
*/
5355
fun list(
5456
parameters: ListQueryParameters = ListQueryParameters.DEFAULT,
55-
filter: FilterParameter? = null
57+
urlFilter: FilterParameter? = null,
58+
hierarchyFilter: HierarchyFilter = HierarchyFilter.WILDCARD
5659
): ListQueryResult<Repository>
5760

5861
/**

0 commit comments

Comments
 (0)