Skip to content

Commit ce357da

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 f261957 commit ce357da

File tree

3 files changed

+175
-19
lines changed

3 files changed

+175
-19
lines changed

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

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@ 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.andCond
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.excludesCondition
28+
import org.eclipse.apoapsis.ortserver.dao.utils.extractIds
2529
import org.eclipse.apoapsis.ortserver.dao.utils.listQuery
30+
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
2631
import org.eclipse.apoapsis.ortserver.model.repositories.ProductRepository
2732
import org.eclipse.apoapsis.ortserver.model.util.FilterParameter
33+
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter
2834
import org.eclipse.apoapsis.ortserver.model.util.ListQueryParameters
2935
import org.eclipse.apoapsis.ortserver.model.util.OptionalValue
3036

3137
import org.jetbrains.exposed.sql.Database
3238
import org.jetbrains.exposed.sql.Op
39+
import org.jetbrains.exposed.sql.SqlExpressionBuilder
3340
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
3441
import org.jetbrains.exposed.sql.and
3542

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

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

47-
override fun list(parameters: ListQueryParameters, filter: FilterParameter?) =
54+
override fun list(parameters: ListQueryParameters, nameFilter: FilterParameter?, hierarchyFilter: HierarchyFilter) =
4855
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
56+
val nameCondition = nameFilter?.let {
57+
ProductsTable.name.applyRegex(it.value)
58+
} ?: Op.TRUE
59+
60+
val builder = hierarchyFilter.apply(nameCondition) { level, ids, filter ->
61+
generateHierarchyCondition(level, ids, filter)
5762
}
63+
64+
ProductDao.listQuery(parameters, ProductDao::mapToModel, builder)
5865
}
5966

6067
override fun countForOrganization(organizationId: Long) =
6168
ProductDao.count(ProductsTable.organizationId eq organizationId)
6269

6370
override fun listForOrganization(organizationId: Long, parameters: ListQueryParameters, filter: FilterParameter?) =
6471
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
72+
ProductDao.listQuery(parameters, ProductDao::mapToModel) {
73+
if (filter != null) {
74+
ProductsTable.organizationId eq organizationId and ProductsTable.name.applyRegex(filter.value)
75+
} else {
76+
ProductsTable.organizationId eq organizationId
77+
}
7078
}
7179
}
72-
}
7380

7481
override fun update(id: Long, name: OptionalValue<String>, description: OptionalValue<String?>) = db.blockingQuery {
7582
val product = ProductDao[id]
@@ -82,3 +89,26 @@ class DaoProductRepository(private val db: Database) : ProductRepository {
8289

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

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

Lines changed: 125 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,126 @@ 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+
excludes = emptyMap()
162+
)
163+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
164+
165+
result shouldBe ListQueryResult(
166+
data = listOf(
167+
Product(prod1.id, orgId, prod1.name, prod1.description),
168+
Product(prod2.id, orgId, prod2.name, prod2.description)
169+
),
170+
params = ListQueryParameters.DEFAULT,
171+
totalCount = 2
172+
)
173+
}
174+
175+
"list should apply a hierarchy filter on organization level" {
176+
val org2 = fixtures.createOrganization(name = "org2")
177+
val org1Id = CompoundHierarchyId.forOrganization(OrganizationId(fixtures.organization.id))
178+
val org2Id = CompoundHierarchyId.forOrganization(OrganizationId(org2.id))
179+
180+
val prod1 = fixtures.createProduct("prod1")
181+
val prod2 = fixtures.createProduct("prod2", organizationId = org2.id)
182+
183+
val otherOrg = fixtures.createOrganization(name = "otherOrg")
184+
fixtures.createProduct("prod3", organizationId = otherOrg.id)
185+
186+
val hierarchyFilter = HierarchyFilter(
187+
transitiveIncludes = mapOf(CompoundHierarchyId.ORGANIZATION_LEVEL to listOf(org1Id, org2Id)),
188+
nonTransitiveIncludes = emptyMap(),
189+
excludes = emptyMap()
190+
)
191+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
192+
193+
result shouldBe ListQueryResult(
194+
data = listOf(
195+
Product(prod1.id, orgId, prod1.name, prod1.description),
196+
Product(prod2.id, org2.id, prod2.name, prod2.description)
197+
),
198+
params = ListQueryParameters.DEFAULT,
199+
totalCount = 2
200+
)
201+
}
202+
203+
"list should apply a hierarchy filter on organization level with excludes" {
204+
val org2 = fixtures.createOrganization(name = "org2")
205+
val org1Id = CompoundHierarchyId.forOrganization(OrganizationId(fixtures.organization.id))
206+
val org2Id = CompoundHierarchyId.forOrganization(OrganizationId(org2.id))
207+
208+
val prod1 = fixtures.createProduct("prod1")
209+
val prod2 = fixtures.createProduct("prod2", organizationId = org2.id)
210+
val prod2Id = CompoundHierarchyId.forProduct(
211+
OrganizationId(org2.id),
212+
ProductId(prod2.id)
213+
)
214+
215+
val otherOrg = fixtures.createOrganization(name = "otherOrg")
216+
fixtures.createProduct("prod3", organizationId = otherOrg.id)
217+
218+
val hierarchyFilter = HierarchyFilter(
219+
transitiveIncludes = mapOf(CompoundHierarchyId.ORGANIZATION_LEVEL to listOf(org1Id, org2Id)),
220+
nonTransitiveIncludes = emptyMap(),
221+
excludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod2Id))
222+
)
223+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
224+
225+
result shouldBe ListQueryResult(
226+
data = listOf(
227+
Product(prod1.id, orgId, prod1.name, prod1.description)
228+
),
229+
params = ListQueryParameters.DEFAULT,
230+
totalCount = 1
231+
)
232+
}
233+
234+
"list should apply a filter with non-transitive includes" {
235+
val prod1 = fixtures.createProduct("prod1")
236+
val prod1Id = CompoundHierarchyId.forProduct(
237+
OrganizationId(fixtures.organization.id),
238+
ProductId(prod1.id)
239+
)
240+
val prod2 = fixtures.createProduct("prod2")
241+
val prod2Id = CompoundHierarchyId.forProduct(
242+
OrganizationId(fixtures.organization.id),
243+
ProductId(prod2.id)
244+
)
245+
fixtures.createProduct("prod3")
246+
247+
val hierarchyFilter = HierarchyFilter(
248+
transitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod1Id)),
249+
nonTransitiveIncludes = mapOf(CompoundHierarchyId.PRODUCT_LEVEL to listOf(prod2Id)),
250+
excludes = emptyMap()
251+
)
252+
val result = productRepository.list(hierarchyFilter = hierarchyFilter)
253+
254+
result shouldBe ListQueryResult(
132255
data = listOf(
133256
Product(prod1.id, orgId, prod1.name, prod1.description),
134257
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)