Skip to content

Commit 6dd2c75

Browse files
committed
feat(dao): Apply a hierarchy filter when listing products
Extend the `ProductRepository.list()` function to support a `HierarchyFiler`. This allows to query a specific set of products; for instance, all those a user can access. Signed-off-by: Oliver Heger <[email protected]>
1 parent 97f0556 commit 6dd2c75

File tree

3 files changed

+138
-19
lines changed

3 files changed

+138
-19
lines changed

dao/src/main/kotlin/repositories/product/DaoProductRepository.kt

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ package org.eclipse.apoapsis.ortserver.dao.repositories.product
2121

2222
import org.eclipse.apoapsis.ortserver.dao.blockingQuery
2323
import org.eclipse.apoapsis.ortserver.dao.entityQuery
24+
import org.eclipse.apoapsis.ortserver.dao.utils.apply
2425
import org.eclipse.apoapsis.ortserver.dao.utils.applyRegex
26+
import org.eclipse.apoapsis.ortserver.dao.utils.extractIds
2527
import org.eclipse.apoapsis.ortserver.dao.utils.listQuery
28+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2629
import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository
2730
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
31+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
2832
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
2933
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
3034

3135
import org.jetbrains.exposed.sql.Database
3236
import org.jetbrains.exposed.sql.Op
37+
import org.jetbrains.exposed.sql.SqlExpressionBuilder
3338
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
3439
import org.jetbrains.exposed.sql.and
3540

@@ -44,32 +49,32 @@ class DaoProductRepository(private val db: Database) : ProductRepository {
4449

4550
override fun get(id: Long) = db.entityQuery { ProductDao[id].mapToModel() }
4651

47-
override fun list(parameters: ListQueryParameters, filter: FilterParameter?) =
52+
override fun list(parameters: ListQueryParameters, nameFilter: FilterParameter?, hierarchyFilter: HierarchyFilter) =
4853
db.blockingQuery {
49-
ProductDao.listQuery(parameters, ProductDao::mapToModel) {
50-
var condition: Op<Boolean> = Op.TRUE
51-
filter?.let {
52-
condition = condition and ProductsTable.name.applyRegex(
53-
it.value
54-
)
55-
}
56-
condition
54+
val nameCondition = nameFilter?.let {
55+
ProductsTable.name.applyRegex(it.value)
56+
} ?: Op.TRUE
57+
58+
val builder = hierarchyFilter.apply(nameCondition) { level, ids, filter ->
59+
generateHierarchyCondition(level, ids, filter)
5760
}
61+
62+
ProductDao.listQuery(parameters, ProductDao::mapToModel, builder)
5863
}
5964

6065
override fun countForOrganization(organizationId: Long) =
6166
ProductDao.count(ProductsTable.organizationId eq organizationId)
6267

6368
override fun listForOrganization(organizationId: Long, parameters: ListQueryParameters, filter: FilterParameter?) =
6469
db.blockingQuery {
65-
ProductDao.listQuery(parameters, ProductDao::mapToModel) {
66-
if (filter != null) {
67-
ProductsTable.organizationId eq organizationId and ProductsTable.name.applyRegex(filter.value)
68-
} else {
69-
ProductsTable.organizationId eq organizationId
70+
ProductDao.listQuery(parameters, ProductDao::mapToModel) {
71+
if (filter != null) {
72+
ProductsTable.organizationId eq organizationId and ProductsTable.name.applyRegex(filter.value)
73+
} else {
74+
ProductsTable.organizationId eq organizationId
75+
}
7076
}
7177
}
72-
}
7378

7479
override fun update(id: Long, name: OptionalValue<String>, description: OptionalValue<String?>) = db.blockingQuery {
7580
val product = ProductDao[id]
@@ -82,3 +87,25 @@ class DaoProductRepository(private val db: Database) : ProductRepository {
8287

8388
override fun delete(id: Long) = db.blockingQuery { ProductDao[id].delete() }
8489
}
90+
91+
/**
92+
* Generate a condition defined by a [HierarchyFilter] for the given [level] and [ids].
93+
*/
94+
private fun SqlExpressionBuilder.generateHierarchyCondition(
95+
level: Int,
96+
ids: List<CompoundHierarchyId>,
97+
filter: HierarchyFilter
98+
): Op<Boolean> =
99+
when (level) {
100+
CompoundHierarchyId.PRODUCT_LEVEL ->
101+
ProductsTable.id inList (
102+
ids.extractIds(CompoundHierarchyId.PRODUCT_LEVEL) +
103+
filter.nonTransitiveIncludes[CompoundHierarchyId.PRODUCT_LEVEL].orEmpty()
104+
.extractIds(CompoundHierarchyId.PRODUCT_LEVEL)
105+
)
106+
107+
CompoundHierarchyId.ORGANIZATION_LEVEL ->
108+
ProductsTable.organizationId inList ids.extractIds(CompoundHierarchyId.ORGANIZATION_LEVEL)
109+
110+
else -> Op.FALSE
111+
}

dao/src/test/kotlin/repositories/product/DaoProductRepositoryTest.kt

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ 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
32+
import org.eclipse.apoapsis.ortserver.model.OrganizationId
3133
import org.eclipse.apoapsis.ortserver.model.Product
34+
import org.eclipse.apoapsis.ortserver.model.ProductId
3235
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
36+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
3337
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
3438
import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult
3539
import org.eclipse.apoapsis.ortserver.model.util.OrderDirection
@@ -112,7 +116,7 @@ class DaoProductRepositoryTest : StringSpec({
112116
fixtures.createProduct("product-gateway")
113117
fixtures.createProduct("core-service")
114118

115-
productRepository.list(filter = FilterParameter("product$")) shouldBe ListQueryResult(
119+
productRepository.list(nameFilter = FilterParameter("product$")) shouldBe ListQueryResult(
116120
data = listOf(
117121
Product(prod1.id, orgId, prod1.name, prod1.description),
118122
Product(prod2.id, orgId, prod2.name, prod2.description)
@@ -128,7 +132,92 @@ class DaoProductRepositoryTest : StringSpec({
128132
fixtures.createProduct("user-product")
129133
fixtures.createProduct("name")
130134

131-
productRepository.list(filter = FilterParameter("^product")) shouldBe ListQueryResult(
135+
productRepository.list(nameFilter = FilterParameter("^product")) shouldBe ListQueryResult(
136+
data = listOf(
137+
Product(prod1.id, orgId, prod1.name, prod1.description),
138+
Product(prod2.id, orgId, prod2.name, prod2.description)
139+
),
140+
params = ListQueryParameters.DEFAULT,
141+
totalCount = 2
142+
)
143+
}
144+
145+
"list should apply a hierarchy filter on product level" {
146+
val prod1 = fixtures.createProduct("prod1")
147+
val prod1Id = CompoundHierarchyId.forProduct(
148+
OrganizationId(fixtures.organization.id),
149+
ProductId(prod1.id)
150+
)
151+
val prod2 = fixtures.createProduct("prod2")
152+
val prod2Id = CompoundHierarchyId.forProduct(
153+
OrganizationId(fixtures.organization.id),
154+
ProductId(prod2.id)
155+
)
156+
fixtures.createProduct("prod3")
157+
158+
val hierarchyFilter = HierarchyFilter(
159+
transitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod1Id, prod2Id)),
160+
nonTransitiveIncludes = emptyMap(),
161+
)
162+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
163+
164+
result shouldBe ListQueryResult(
165+
data = listOf(
166+
Product(prod1.id, orgId, prod1.name, prod1.description),
167+
Product(prod2.id, orgId, prod2.name, prod2.description)
168+
),
169+
params = ListQueryParameters.DEFAULT,
170+
totalCount = 2
171+
)
172+
}
173+
174+
"list should apply a hierarchy filter on organization level" {
175+
val org2 = fixtures.createOrganization(name = "org2")
176+
val org1Id = CompoundHierarchyId.forOrganization(OrganizationId(fixtures.organization.id))
177+
val org2Id = CompoundHierarchyId.forOrganization(OrganizationId(org2.id))
178+
179+
val prod1 = fixtures.createProduct("prod1")
180+
val prod2 = fixtures.createProduct("prod2", organizationId = org2.id)
181+
182+
val otherOrg = fixtures.createOrganization(name = "otherOrg")
183+
fixtures.createProduct("prod3", organizationId = otherOrg.id)
184+
185+
val hierarchyFilter = HierarchyFilter(
186+
transitiveIncludes = mapOf(CompoundHierarchyId.ORGANIZATION_LEVEL to listOf(org1Id, org2Id)),
187+
nonTransitiveIncludes = emptyMap(),
188+
)
189+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
190+
191+
result shouldBe ListQueryResult(
192+
data = listOf(
193+
Product(prod1.id, orgId, prod1.name, prod1.description),
194+
Product(prod2.id, org2.id, prod2.name, prod2.description)
195+
),
196+
params = ListQueryParameters.DEFAULT,
197+
totalCount = 2
198+
)
199+
}
200+
201+
"list should apply a filter with non-transitive includes" {
202+
val prod1 = fixtures.createProduct("prod1")
203+
val prod1Id = CompoundHierarchyId.forProduct(
204+
OrganizationId(fixtures.organization.id),
205+
ProductId(prod1.id)
206+
)
207+
val prod2 = fixtures.createProduct("prod2")
208+
val prod2Id = CompoundHierarchyId.forProduct(
209+
OrganizationId(fixtures.organization.id),
210+
ProductId(prod2.id)
211+
)
212+
fixtures.createProduct("prod3")
213+
214+
val hierarchyFilter = HierarchyFilter(
215+
transitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod1Id)),
216+
nonTransitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod2Id)),
217+
)
218+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
219+
220+
result shouldBe ListQueryResult(
132221
data = listOf(
133222
Product(prod1.id, orgId, prod1.name, prod1.description),
134223
Product(prod2.id, orgId, prod2.name, prod2.description)

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package org.eclipse.apoapsis.ortserver.model.repositories
2121

2222
import org.eclipse.apoapsis.ortserver.model.Product
2323
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
24+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
2425
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
2526
import org.eclipse.apoapsis.ortserver.model.util.ListQueryResult
2627
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
@@ -40,11 +41,13 @@ interface ProductRepository {
4041
fun get(id: Long): Product?
4142

4243
/**
43-
* List all products according to the given [parameters].
44+
* List all products according to the given [parameters]. Optionally, a [nameFilter] on the product name and a
45+
* [hierarchyFilter] can be provided.
4446
*/
4547
fun list(
4648
parameters: ListQueryParameters = ListQueryParameters.DEFAULT,
47-
filter: FilterParameter? = null
49+
nameFilter: FilterParameter? = null,
50+
hierarchyFilter: HierarchyFilter = HierarchyFilter.WILDCARD
4851
): ListQueryResult<Product>
4952

5053
/**

0 commit comments

Comments
 (0)