Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ http-client.private.env.json

# VS Code
.vscode/

# Agent files.
.github/chatmodes/
44 changes: 44 additions & 0 deletions components/search/api-model/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

plugins {
id("ort-server-kotlin-multiplatform-conventions")
id("ort-server-publication-conventions")

// Apply third-party plugins.
alias(libs.plugins.kotlinSerialization)
}

group = "org.eclipse.apoapsis.ortserver.components.search"

kotlin {
linuxX64()
macosArm64()
macosX64()
mingwX64()

sourceSets {
commonMain {
dependencies {
implementation(libs.kotlinxDatetime)
implementation(libs.kotlinxSerializationJson)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.components.search.apimodel

import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable

@Serializable
data class RunWithPackage(
val organizationId: Long,
val productId: Long,
val repositoryId: Long,
val ortRunId: Long,
val revision: String?,
val createdAt: Instant,
val packageId: String,
)
66 changes: 66 additions & 0 deletions components/search/backend/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

plugins {
id("ort-server-kotlin-component-backend-conventions")
id("ort-server-publication-conventions")
}

group = "org.eclipse.apoapsis.ortserver.components.search"

repositories {
exclusiveContent {
forRepository {
maven("https://repo.gradle.org/gradle/libs-releases/")
}
filter {
includeGroup("org.gradle")
}
}
}
Comment on lines +27 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this special repository declaration should be required at all.

Copy link
Contributor Author

@Etsija Etsija Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I copied it from plugin-manager's buildfile. I don't really understand what it means. Also infrastructure-services and admin-config components have it. Maybe @mnonnenmacher could have an answer?

Could this be something left there "historically" for the other components as well? I took the whole repositories block out: buildAllItems pass, all tests pass, manually verified that the feature also works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is only needed if either this project or projects it depends on need to retrieve artifacts from non-standard locations (in this case the Gradle-specific Maven repository).

To find out whether it's really needed we first need to double-check that the list of dependencies is as minimal as it can be, and then simply try whether everything builds also without this block or not.


dependencies {
api(libs.exposedCore)

implementation(projects.components.authorization.backend)
implementation(projects.components.search.apiModel)
implementation(projects.dao)
implementation(projects.model)

routesImplementation(projects.components.authorization.backend)
routesImplementation(projects.shared.apiModel)
routesImplementation(projects.shared.ktorUtils)

routesImplementation(ktorLibs.server.auth)
routesImplementation(ktorLibs.server.core)
routesImplementation(libs.ktorOpenApi)

testImplementation(testFixtures(projects.dao))
testImplementation(testFixtures(projects.shared.ktorUtils))

testImplementation(ktorLibs.serialization.kotlinx.json)
testImplementation(ktorLibs.server.auth)
testImplementation(ktorLibs.server.contentNegotiation)
testImplementation(ktorLibs.server.statusPages)
testImplementation(ktorLibs.server.testHost)
testImplementation(libs.kotestAssertionsKtor)
testImplementation(libs.kotestRunnerJunit5)
testImplementation(libs.kotlinxSerializationJson)
testImplementation(libs.mockk)
}
143 changes: 143 additions & 0 deletions components/search/backend/src/main/kotlin/SearchService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.components.search.backend

import org.eclipse.apoapsis.ortserver.components.authorization.rights.RepositoryPermission
import org.eclipse.apoapsis.ortserver.components.authorization.service.AuthorizationService
import org.eclipse.apoapsis.ortserver.components.search.apimodel.RunWithPackage
import org.eclipse.apoapsis.ortserver.dao.blockingQuery
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerjob.AnalyzerJobsTable
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.AnalyzerRunsTable
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.PackagesAnalyzerRunsTable
import org.eclipse.apoapsis.ortserver.dao.repositories.analyzerrun.PackagesTable
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunDao
import org.eclipse.apoapsis.ortserver.dao.repositories.ortrun.OrtRunsTable
import org.eclipse.apoapsis.ortserver.dao.repositories.product.ProductsTable
import org.eclipse.apoapsis.ortserver.dao.repositories.repository.RepositoriesTable
import org.eclipse.apoapsis.ortserver.dao.tables.shared.IdentifiersTable
import org.eclipse.apoapsis.ortserver.dao.utils.apply
import org.eclipse.apoapsis.ortserver.dao.utils.applyRegex
import org.eclipse.apoapsis.ortserver.dao.utils.extractIds
import org.eclipse.apoapsis.ortserver.model.CompoundHierarchyId
import org.eclipse.apoapsis.ortserver.model.HierarchyId
import org.eclipse.apoapsis.ortserver.model.util.HierarchyFilter

import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SqlExpressionBuilder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat
import org.jetbrains.exposed.sql.innerJoin
import org.jetbrains.exposed.sql.stringLiteral

class SearchService(
private val db: Database,
private val authorizationService: AuthorizationService
) {
/**
* Search for Analyzer runs containing the given package identifier, with optional scoping.
*
* @param identifier The package identifier regex to search for.
* @param userId The user ID performing the search.
* @param scope Optional scope to limit the search. Can be an [OrganizationId], [ProductId], or [RepositoryId].
* If null, performs a global search (requires superuser permission).
*/
suspend fun findOrtRunsByPackage(
identifier: String,
userId: String,
scope: HierarchyId? = null
): List<RunWithPackage> {
val hierarchyFilter = authorizationService.filterHierarchyIds(
userId = userId,
repositoryPermissions = setOf(RepositoryPermission.READ),
containedIn = scope
)

return db.blockingQuery {
val query = OrtRunsTable
.innerJoin(AnalyzerJobsTable, { OrtRunsTable.id }, { ortRunId })
.innerJoin(AnalyzerRunsTable, { AnalyzerJobsTable.id }, { analyzerJobId })
.innerJoin(PackagesAnalyzerRunsTable, { AnalyzerRunsTable.id }, { analyzerRunId })
.innerJoin(PackagesTable, { PackagesAnalyzerRunsTable.packageId }, { PackagesTable.id })
.innerJoin(IdentifiersTable, { PackagesTable.identifierId }, { IdentifiersTable.id })
.innerJoin(RepositoriesTable, { OrtRunsTable.repositoryId }, { RepositoriesTable.id })
.innerJoin(ProductsTable, { RepositoriesTable.productId }, { ProductsTable.id })

val concatenatedIdentifier = concat(
IdentifiersTable.type,
stringLiteral(":"),
IdentifiersTable.namespace,
stringLiteral(":"),
IdentifiersTable.name,
stringLiteral(":"),
IdentifiersTable.version
)

val identifierCondition = concatenatedIdentifier.applyRegex(identifier)

val whereClause = hierarchyFilter.apply(identifierCondition) { level, ids, filter ->
generateHierarchyCondition(level, ids, filter)
}

query.select(OrtRunsTable.columns + IdentifiersTable.columns).where(whereClause).map { row ->
val ortRun = OrtRunDao.wrapRow(row).mapToModel()
val packageId = listOf(
row[IdentifiersTable.type],
row[IdentifiersTable.namespace],
row[IdentifiersTable.name],
row[IdentifiersTable.version]
).joinToString(":")
RunWithPackage(
organizationId = ortRun.organizationId,
productId = ortRun.productId,
repositoryId = ortRun.repositoryId,
ortRunId = ortRun.id,
revision = ortRun.revision,
createdAt = ortRun.createdAt,
packageId = packageId
)
}
}
}
}

/**
* Generate a condition defined by a [HierarchyFilter] for the given [level] and [ids].
*/
private fun SqlExpressionBuilder.generateHierarchyCondition(
level: Int,
ids: List<CompoundHierarchyId>,
filter: HierarchyFilter
): Op<Boolean> =
when (level) {
CompoundHierarchyId.REPOSITORY_LEVEL ->
OrtRunsTable.repositoryId inList (
ids.extractIds(CompoundHierarchyId.REPOSITORY_LEVEL) +
filter.nonTransitiveIncludes[CompoundHierarchyId.REPOSITORY_LEVEL].orEmpty()
.extractIds(CompoundHierarchyId.REPOSITORY_LEVEL)
)

CompoundHierarchyId.PRODUCT_LEVEL ->
RepositoriesTable.productId inList ids.extractIds(CompoundHierarchyId.PRODUCT_LEVEL)

CompoundHierarchyId.ORGANIZATION_LEVEL ->
ProductsTable.organizationId inList ids.extractIds(CompoundHierarchyId.ORGANIZATION_LEVEL)

else -> Op.FALSE
}
30 changes: 30 additions & 0 deletions components/search/backend/src/routes/kotlin/Routing.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2025 The ORT Server Authors (See <https://github.com/eclipse-apoapsis/ort-server/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.eclipse.apoapsis.ortserver.components.search

import io.ktor.server.routing.Route

import org.eclipse.apoapsis.ortserver.components.search.backend.SearchService
import org.eclipse.apoapsis.ortserver.components.search.routes.getRunsWithPackage

/** Add all package-search routes. */
fun Route.searchRoutes(searchService: SearchService) {
getRunsWithPackage(searchService)
}
Loading
Loading