Skip to content

Commit 5ea8929

Browse files
committed
feat(search): Add SearchService
Signed-off-by: Jyrki Keisala <[email protected]>
1 parent cec7bfd commit 5ea8929

File tree

8 files changed

+511
-0
lines changed

8 files changed

+511
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
plugins {
21+
id("ort-server-kotlin-multiplatform-conventions")
22+
id("ort-server-publication-conventions")
23+
alias(libs.plugins.kotlinSerialization)
24+
}
25+
26+
group = "org.eclipse.apoapsis.ortserver.components.search"
27+
28+
kotlin {
29+
jvm()
30+
linuxX64()
31+
macosArm64()
32+
macosX64()
33+
mingwX64()
34+
35+
sourceSets {
36+
commonMain {
37+
dependencies {
38+
implementation(libs.kotlinxDatetime)
39+
implementation(libs.kotlinxSerializationJson)
40+
}
41+
}
42+
}
43+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.eclipse.apoapsis.ortserver.components.search.apimodel
21+
22+
import kotlinx.datetime.Instant
23+
import kotlinx.serialization.Serializable
24+
25+
@Serializable
26+
data class RunWithPackage(
27+
val organizationId: Long,
28+
val productId: Long,
29+
val repositoryId: Long,
30+
val ortRunId: Long,
31+
val revision: String?,
32+
val createdAt: Instant,
33+
val packageId: String,
34+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
plugins {
21+
id("ort-server-kotlin-component-backend-conventions")
22+
id("ort-server-publication-conventions")
23+
}
24+
25+
group = "org.eclipse.apoapsis.ortserver.components.search"
26+
27+
repositories {
28+
exclusiveContent {
29+
forRepository {
30+
maven("https://repo.gradle.org/gradle/libs-releases/")
31+
}
32+
filter {
33+
includeGroup("org.gradle")
34+
}
35+
}
36+
}
37+
38+
dependencies {
39+
api(libs.exposedCore)
40+
41+
implementation(projects.components.search.apiModel)
42+
implementation(projects.dao)
43+
implementation(projects.model)
44+
45+
routesImplementation(projects.components.authorizationKeycloak.backend)
46+
routesImplementation(projects.shared.apiModel)
47+
routesImplementation(projects.shared.ktorUtils)
48+
49+
routesImplementation(ktorLibs.server.auth)
50+
routesImplementation(ktorLibs.server.core)
51+
routesImplementation(libs.ktorOpenApi)
52+
53+
testImplementation(testFixtures(projects.clients.keycloak))
54+
testImplementation(testFixtures(projects.dao))
55+
testImplementation(testFixtures(projects.shared.ktorUtils))
56+
57+
testImplementation(ktorLibs.serialization.kotlinx.json)
58+
testImplementation(ktorLibs.server.auth)
59+
testImplementation(ktorLibs.server.contentNegotiation)
60+
testImplementation(ktorLibs.server.statusPages)
61+
testImplementation(ktorLibs.server.testHost)
62+
testImplementation(libs.kotestAssertionsKtor)
63+
testImplementation(libs.kotestRunnerJunit5)
64+
testImplementation(libs.kotlinxSerializationJson)
65+
testImplementation(libs.mockk)
66+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.eclipse.apoapsis.ortserver.components.search.backend
21+
22+
import org.eclipse.apoapsis.ortserver.components.search.apimodel.RunWithPackage
23+
import org.eclipse.apoapsis.ortserver.dao.blockingQuery
24+
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerjob.AnalyzerJobsTable
25+
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.AnalyzerRunsTable
26+
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.PackagesAnalyzerRunsTable
27+
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.PackagesTable
28+
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunDao
29+
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunsTable
30+
import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable
31+
import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable
32+
import org.eclipse.apoapsis.ortserver.dao.tables.shared.IdentifiersTable
33+
import org.eclipse.apoapsis.ortserver.dao.utils.applyRegex
34+
35+
import org.jetbrains.exposed.sql.Database
36+
import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat
37+
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
38+
import org.jetbrains.exposed.sql.and
39+
import org.jetbrains.exposed.sql.innerJoin
40+
import org.jetbrains.exposed.sql.stringLiteral
41+
42+
class SearchService(private val db: Database) {
43+
/**
44+
* Search for Analyzer runs containing the given package identifier, with optional scoping.
45+
* Throws IllegalArgumentException for invalid scoping hierarchy.
46+
*/
47+
@Suppress("LongMethod")
48+
fun findOrtRunsByPackage(
49+
identifier: String,
50+
organizationId: Long? = null,
51+
productId: Long? = null,
52+
repositoryId: Long? = null
53+
): List<RunWithPackage> = db.blockingQuery {
54+
validateScope(organizationId, productId, repositoryId)
55+
56+
// Build base query
57+
var query = OrtRunsTable
58+
.innerJoin(AnalyzerJobsTable, { OrtRunsTable.id }, { ortRunId })
59+
.innerJoin(AnalyzerRunsTable, { AnalyzerJobsTable.id }, { analyzerJobId })
60+
.innerJoin(PackagesAnalyzerRunsTable, { AnalyzerRunsTable.id }, { PackagesAnalyzerRunsTable.analyzerRunId })
61+
.innerJoin(PackagesTable, { PackagesAnalyzerRunsTable.packageId }, { PackagesTable.id })
62+
.innerJoin(IdentifiersTable, { PackagesTable.identifierId }, { IdentifiersTable.id })
63+
64+
// Convert Identifier to a concatenated string format for ILike comparison
65+
val concatenatedIdentifier = concat(
66+
IdentifiersTable.type,
67+
stringLiteral(":"),
68+
IdentifiersTable.namespace,
69+
stringLiteral(":"),
70+
IdentifiersTable.name,
71+
stringLiteral(":"),
72+
IdentifiersTable.version
73+
)
74+
75+
val conditions = mutableListOf(concatenatedIdentifier.applyRegex(identifier))
76+
77+
val scopeRequested = organizationId != null || productId != null || repositoryId != null
78+
if (scopeRequested) {
79+
query = query
80+
.innerJoin(RepositoriesTable, { OrtRunsTable.repositoryId }, { RepositoriesTable.id })
81+
.innerJoin(ProductsTable, { RepositoriesTable.productId }, { ProductsTable.id })
82+
}
83+
84+
organizationId?.let { conditions += ProductsTable.organizationId eq it }
85+
productId?.let { conditions += RepositoriesTable.productId eq it }
86+
repositoryId?.let { conditions += OrtRunsTable.repositoryId eq it }
87+
88+
val whereClause = conditions.reduce { acc, expression -> acc and expression }
89+
90+
val resultRows = query.select(OrtRunsTable.columns + IdentifiersTable.columns).where { whereClause }
91+
resultRows.map { row ->
92+
val ortRun = OrtRunDao.wrapRow(row).mapToModel()
93+
val packageId = listOf(
94+
row[IdentifiersTable.type],
95+
row[IdentifiersTable.namespace],
96+
row[IdentifiersTable.name],
97+
row[IdentifiersTable.version]
98+
).joinToString(":")
99+
RunWithPackage(
100+
organizationId = ortRun.organizationId,
101+
productId = ortRun.productId,
102+
repositoryId = ortRun.repositoryId,
103+
ortRunId = ortRun.id,
104+
revision = ortRun.revision,
105+
createdAt = ortRun.createdAt,
106+
packageId = packageId
107+
)
108+
}
109+
}
110+
111+
private fun validateScope(organizationId: Long?, productId: Long?, repositoryId: Long?) {
112+
require(!(repositoryId != null && (productId == null || organizationId == null))) {
113+
"If repositoryId is provided, productId and organizationId must also be provided."
114+
}
115+
require(organizationId != null || productId == null) {
116+
"If productId is provided, organizationId must also be provided."
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)