From 870187a9a77b8663015221d432456f7aecbafb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 09:15:35 +0200 Subject: [PATCH 01/99] refactor(dsp-api): Align *RestService method signature and simplify *Handlers --- .../api/AuthenticationApiModule.scala | 1 + .../AuthenticationEndpointsV2Handler.scala | 123 ++---------------- .../api/AuthenticationRestService.scala | 105 +++++++++++++++ 3 files changed, 115 insertions(+), 114 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationRestService.scala diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala index 9c294950a6d..008b9d40180 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala @@ -22,5 +22,6 @@ object AuthenticationApiModule { self => AuthenticationEndpointsV2.layer, AuthenticationEndpointsV2Handler.layer, AuthenticationApiRoutes.layer, + AuthenticationRestService.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala index bdc145097ae..861f56d7d44 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala @@ -8,135 +8,30 @@ import sttp.model.headers.CookieValueWithMeta import zio.ZIO import zio.ZLayer -import java.time.Instant - -import dsp.errors.AuthenticationException -import dsp.errors.BadCredentialsException import org.knora.webapi.config.AppConfig -import org.knora.webapi.slice.admin.domain.model.Username import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.PublicEndpointHandler import org.knora.webapi.slice.common.api.SecuredEndpointHandler -import org.knora.webapi.slice.infrastructure.Jwt -import org.knora.webapi.slice.security.Authenticator -import org.knora.webapi.slice.security.Authenticator.BAD_CRED_NOT_VALID import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.CheckResponse import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginForm import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload -import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.EmailPassword -import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.IriPassword -import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.UsernamePassword import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutResponse import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse case class AuthenticationEndpointsV2Handler( appConfig: AppConfig, - authenticator: Authenticator, + restService: AuthenticationRestService, endpoints: AuthenticationEndpointsV2, mapper: HandlerMapper, ) { - val getV2Authentication = - SecuredEndpointHandler[Unit, CheckResponse]( - endpoints.getV2Authentication, - _ => _ => ZIO.succeed(CheckResponse("credentials are OK")), - ) - - val postV2Authentication = - PublicEndpointHandler[LoginPayload, (CookieValueWithMeta, TokenResponse)]( - endpoints.postV2Authentication, - (login: LoginPayload) => { - (login match { - case IriPassword(iri, password) => authenticator.authenticate(iri, password) - case UsernamePassword(username, password) => authenticator.authenticate(username, password) - case EmailPassword(email, password) => authenticator.authenticate(email, password) - }).mapBoth( - _ => BadCredentialsException(BAD_CRED_NOT_VALID), - (_, token) => setCookieAndResponse(token), - ).catchAllDefect(e => - ZIO.fail(AuthenticationException("An internal error happened during authentication", Some(e))), - ) - }, - ) - - val deleteV2Authentication = - PublicEndpointHandler[(Option[String], Option[String]), (CookieValueWithMeta, LogoutResponse)]( - endpoints.deleteV2Authentication, - (tokenFromBearer: Option[String], tokenFromCookie: Option[String]) => { - ZIO - .foreachDiscard(Set(tokenFromBearer, tokenFromCookie).flatten)(authenticator.invalidateToken) - .ignore - .as { - ( - CookieValueWithMeta.unsafeApply( - domain = Some(appConfig.cookieDomain), - expires = Some(Instant.EPOCH), - httpOnly = true, - maxAge = Some(0), - path = Some("/"), - value = "", - ), - LogoutResponse(0, "Logout OK"), - ) - } - }, - ) - - val getV2Login = PublicEndpointHandler[Unit, String]( - endpoints.getV2Login, - _ => { - val apiUrl = appConfig.knoraApi.externalKnoraApiBaseUrl - val form = - s""" - | - | - |
- |
- | - |
- |
- |

© 2015–2024 dasch.swiss

- |
- |
- | - | - """.stripMargin - ZIO.succeed(form) - }, - ) - - val postV2Login = - PublicEndpointHandler[LoginForm, (CookieValueWithMeta, TokenResponse)]( - endpoints.postV2Login, - (login: LoginForm) => { - (for { - username <- ZIO.fromEither(Username.from(login.username)) - token <- authenticator.authenticate(username, login.password) - } yield setCookieAndResponse(token._2)).orElseFail(BadCredentialsException(BAD_CRED_NOT_VALID)) - }, - ) - - private def setCookieAndResponse(token: Jwt) = - ( - CookieValueWithMeta.unsafeApply( - domain = Some(appConfig.cookieDomain), - httpOnly = true, - path = Some("/"), - value = token.jwtString, - ), - TokenResponse(token.jwtString), - ) - - private val secure = List(getV2Authentication).map(mapper.mapSecuredEndpointHandler(_)) - private val public = - List(postV2Authentication, deleteV2Authentication, getV2Login, postV2Login).map(mapper.mapPublicEndpointHandler(_)) - val allHandlers = secure ++ public + val allHandlers = List( + SecuredEndpointHandler(endpoints.getV2Authentication, _ => _ => ZIO.succeed(CheckResponse("credentials are OK"))), + ).map(mapper.mapSecuredEndpointHandler) ++ List( + PublicEndpointHandler(endpoints.postV2Authentication, restService.authenticate), + PublicEndpointHandler(endpoints.deleteV2Authentication, restService.logout), + PublicEndpointHandler(endpoints.getV2Login, restService.loginForm), + PublicEndpointHandler(endpoints.postV2Login, restService.authenticate), + ).map(mapper.mapPublicEndpointHandler) } object AuthenticationEndpointsV2Handler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationRestService.scala new file mode 100644 index 00000000000..cc66cdf54db --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationRestService.scala @@ -0,0 +1,105 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.security.api + +import sttp.model.headers.CookieValueWithMeta +import zio.* + +import java.time.Instant + +import dsp.errors.BadCredentialsException +import org.knora.webapi.config.AppConfig +import org.knora.webapi.slice.admin.domain.model.Username +import org.knora.webapi.slice.infrastructure.Jwt +import org.knora.webapi.slice.security.Authenticator +import org.knora.webapi.slice.security.Authenticator.BAD_CRED_NOT_VALID +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginForm +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.EmailPassword +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.IriPassword +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload.UsernamePassword +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutResponse +import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse + +final case class AuthenticationRestService( + private val authenticator: Authenticator, + private val appConfig: AppConfig, +) { + + def loginForm(ignored: Unit): UIO[String] = + val apiUrl = appConfig.knoraApi.externalKnoraApiBaseUrl + val form = + s""" + | + | + |
+ |
+ | + |
+ |
+ |

© 2015–2024 dasch.swiss

+ |
+ |
+ | + | + """.stripMargin + ZIO.succeed(form) + + def authenticate(login: LoginForm): IO[BadCredentialsException, (CookieValueWithMeta, TokenResponse)] = + (for { + username <- ZIO.fromEither(Username.from(login.username)) + token <- authenticator.authenticate(username, login.password) + } yield setCookieAndResponse(token._2)) + .orElseFail(BadCredentialsException(BAD_CRED_NOT_VALID)) + + def authenticate(login: LoginPayload): IO[BadCredentialsException, (CookieValueWithMeta, TokenResponse)] = + (login match { + case IriPassword(iri, password) => authenticator.authenticate(iri, password) + case UsernamePassword(username, password) => authenticator.authenticate(username, password) + case EmailPassword(email, password) => authenticator.authenticate(email, password) + }).mapBoth(_ => BadCredentialsException(BAD_CRED_NOT_VALID), (_, token) => setCookieAndResponse(token)) + + private def setCookieAndResponse(token: Jwt) = + ( + CookieValueWithMeta.unsafeApply( + domain = Some(appConfig.cookieDomain), + httpOnly = true, + path = Some("/"), + value = token.jwtString, + ), + TokenResponse(token.jwtString), + ) + + def logout(tokenFromBearer: Option[String], tokenFromCookie: Option[String]) = + ZIO + .foreachDiscard(Set(tokenFromBearer, tokenFromCookie).flatten)(authenticator.invalidateToken) + .ignore + .as { + ( + CookieValueWithMeta.unsafeApply( + domain = Some(appConfig.cookieDomain), + expires = Some(Instant.EPOCH), + httpOnly = true, + maxAge = Some(0), + path = Some("/"), + value = "", + ), + LogoutResponse(0, "Logout OK"), + ) + } + +} + +object AuthenticationRestService { + val layer = zio.ZLayer.derive[AuthenticationRestService] +} From 16bb65db06ef0deff331ed3c7e80d0fe6edf47c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 09:36:47 +0200 Subject: [PATCH 02/99] Merge order query params and move default values into endpoint spec --- .../admin/api/model/FilterAndOrder.scala | 36 +++++++++---- .../lists/api/ListsEndpointsV2Handler.scala | 5 +- .../resources/api/ResourceInfoEndpoints.scala | 8 +-- .../resources/api/ResourceInfoRoutes.scala | 24 ++------- .../resources/api/model/QueryParams.scala | 54 ------------------- .../api/service/ResourceInfoRestService.scala | 11 ++-- .../api/ResourceInfoRestServiceSpec.scala | 10 ++-- 7 files changed, 46 insertions(+), 102 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/QueryParams.scala diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/FilterAndOrder.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/FilterAndOrder.scala index eef02ac0f79..2f0e27fe419 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/FilterAndOrder.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/FilterAndOrder.scala @@ -7,14 +7,11 @@ package org.knora.webapi.slice.admin.api.model import sttp.tapir.* -import org.knora.webapi.slice.admin.api.model.Order.Asc -import org.knora.webapi.slice.admin.api.model.Order.Desc - case class FilterAndOrder(filter: Option[String], order: Order) { self => def ordering[A](using o: Ordering[A]): Ordering[A] = self.order match { - case Asc => o - case Desc => o.reverse + case Order.Asc => o + case Order.Desc => o.reverse } } @@ -23,12 +20,9 @@ object FilterAndOrder { query[Option[String]]("filter") .description("Filter the results.") .default(None) - private val orderQueryParam = query[Order]("order") - .description("Sort the results in ascending (asc) or descending (desc) order.") - .default(Order.Asc) val queryParams: EndpointInput[FilterAndOrder] = - filterQueryParam.and(orderQueryParam).mapTo[FilterAndOrder] + filterQueryParam.and(Order.queryParam).mapTo[FilterAndOrder] } enum Order { @@ -40,11 +34,35 @@ enum Order { case Desc => "DESC" } } + object Order { given Codec[String, Order, CodecFormat.TextPlain] = Codec.string.mapEither(from)(_.toString) + val queryParam: EndpointInput.Query[Order] = query[Order]("order") + .description("Sort the results in ascending (asc) or descending (desc) order.") + .default(Order.Asc) + def from(str: String): Either[String, Order] = Order.values .find(_.toString.equalsIgnoreCase(str)) .toRight(s"Invalid order: possible values are '${Order.values.map(_.toString.toLowerCase).mkString(", ")}'") } + +enum OrderBy { + case CreationDate extends OrderBy + case LastModificationDate extends OrderBy +} + +object OrderBy { + given Codec[String, OrderBy, CodecFormat.TextPlain] = Codec.string.mapEither(from)(_.toString) + + val queryParam: EndpointInput.Query[OrderBy] = query[OrderBy]("orderBy") + .description("Sort the results by the specified property.") + + def from(str: String): Either[String, OrderBy] = + OrderBy.values + .find(_.toString.equalsIgnoreCase(str)) + .toRight( + s"Invalid order by: possible values are '${OrderBy.values.map(_.toString.toLowerCase).mkString(", ")}'", + ) +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala index 004ec978cba..48e04fefd73 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala @@ -21,13 +21,10 @@ final case class ListsEndpointsV2Handler( private val listsRestService: ListsV2RestService, private val mapper: HandlerMapper, ) { - val allHandlers = List( SecuredEndpointHandler(endpoints.getV2Lists, listsRestService.getList), SecuredEndpointHandler(endpoints.getV2Node, listsRestService.getNode), - ) - .map(mapper.mapSecuredEndpointHandler) - + ).map(mapper.mapSecuredEndpointHandler) } object ListsEndpointsV2Handler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoEndpoints.scala index 0ae4261db39..8092a2babf9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoEndpoints.scala @@ -10,20 +10,20 @@ import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.* import zio.ZLayer +import org.knora.webapi.slice.admin.api.model.Order +import org.knora.webapi.slice.admin.api.model.OrderBy import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints import org.knora.webapi.slice.resources.api.model.ListResponseDto -import org.knora.webapi.slice.resources.api.model.QueryParams.Order -import org.knora.webapi.slice.resources.api.model.QueryParams.OrderBy final case class ResourceInfoEndpoints(baseEndpoints: BaseEndpoints) { val getResourcesInfo = baseEndpoints.publicEndpoint.get .in("v2" / "resources" / "info") .in(header[ProjectIri](ApiV2.Headers.xKnoraAcceptProject)) .in(query[String]("resourceClass")) - .in(query[Option[Order]](Order.queryParamKey)) - .in(query[Option[OrderBy]](OrderBy.queryParamKey)) + .in(Order.queryParam) + .in(OrderBy.queryParam.default(OrderBy.LastModificationDate)) .out(jsonBody[ListResponseDto]) val endpoints: Seq[AnyEndpoint] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala index cc1caa3d0dd..7bf3dd39115 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala @@ -13,10 +13,6 @@ import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.PublicEndpointHandler import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.resources.api.model.ListResponseDto -import org.knora.webapi.slice.resources.api.model.QueryParams.Asc -import org.knora.webapi.slice.resources.api.model.QueryParams.LastModificationDate -import org.knora.webapi.slice.resources.api.model.QueryParams.Order -import org.knora.webapi.slice.resources.api.model.QueryParams.OrderBy import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService final case class ResourceInfoRoutes( @@ -26,22 +22,10 @@ final case class ResourceInfoRoutes( interpreter: TapirToPekkoInterpreter, ) { - val getResourcesInfoHandler = - PublicEndpointHandler[(ProjectIri, String, Option[Order], Option[OrderBy]), ListResponseDto]( - endpoints.getResourcesInfo, - { case (projectIri: ProjectIri, resourceClass: String, order: Option[Order], orderBy: Option[OrderBy]) => - resourceInfoService.findByProjectAndResourceClass( - projectIri, - resourceClass, - order.getOrElse(Asc), - orderBy.getOrElse(LastModificationDate), - ) - }, - ) - - val routes: Seq[Route] = List(getResourcesInfoHandler) - .map(it => mapper.mapPublicEndpointHandler(it)) - .map(interpreter.toRoute(_)) + val routes: Seq[Route] = + List(PublicEndpointHandler(endpoints.getResourcesInfo, resourceInfoService.findByProjectAndResourceClass)) + .map(mapper.mapPublicEndpointHandler) + .map(interpreter.toRoute(_)) } object ResourceInfoRoutes { val layer = ZLayer.derive[ResourceInfoRoutes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/QueryParams.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/QueryParams.scala deleted file mode 100644 index 773981bb8d9..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/model/QueryParams.scala +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.resources.api.model - -import sttp.tapir.Codec -import sttp.tapir.CodecFormat.TextPlain -import sttp.tapir.DecodeResult - -import dsp.errors.BadRequestException - -object QueryParams { - - sealed trait WithUrlParam { self => - def urlParam: String = self.getClass.getSimpleName.stripSuffix("$") - } - - private def decode[A <: WithUrlParam](value: String, allValues: List[A]): DecodeResult[A] = - allValues - .find(_.urlParam.equalsIgnoreCase(value)) - .fold[DecodeResult[A]]( - DecodeResult.Error(value, BadRequestException(s"Expected one of ${allValues.map(_.urlParam.mkString)}")), - )(DecodeResult.Value(_)) - - sealed trait OrderBy extends WithUrlParam - case object CreationDate extends OrderBy - case object LastModificationDate extends OrderBy - - object OrderBy { - - val queryParamKey = "orderBy" - - private val allValues = List(CreationDate, LastModificationDate) - - implicit val tapirCodec: Codec[String, OrderBy, TextPlain] = - Codec.string.mapDecode(decode[OrderBy](_, allValues))(_.urlParam) - } - - sealed trait Order extends WithUrlParam - case object Asc extends Order - case object Desc extends Order - - object Order { - - val queryParamKey = "order" - - private val allValues = List(Asc, Desc) - - implicit val tapirCodec: Codec[String, Order, TextPlain] = - Codec.string.mapDecode(decode[Order](_, allValues))(_.urlParam) - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourceInfoRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourceInfoRestService.scala index 6bbb2e22410..cf94cac452a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourceInfoRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/service/ResourceInfoRestService.scala @@ -11,10 +11,11 @@ import java.time.Instant import dsp.errors.BadRequestException import org.knora.webapi.IRI +import org.knora.webapi.slice.admin.api.model.Order +import org.knora.webapi.slice.admin.api.model.OrderBy import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.resources.api.model.ListResponseDto -import org.knora.webapi.slice.resources.api.model.QueryParams.* import org.knora.webapi.slice.resources.api.model.ResourceInfoDto import org.knora.webapi.slice.resources.domain.ResourceInfoRepo @@ -28,13 +29,13 @@ final case class ResourceInfoRestService(repo: ResourceInfoRepo, iriConverter: I private def instant(order: Order)(one: Instant, two: Instant) = order match { - case Asc => two.compareTo(one) > 0 - case Desc => one.compareTo(two) > 0 + case Order.Asc => two.compareTo(one) > 0 + case Order.Desc => one.compareTo(two) > 0 } private def sort(resources: List[ResourceInfoDto], order: Order, orderBy: OrderBy) = (orderBy, order) match { - case (LastModificationDate, order) => resources.sortWith(lastModificationDateSort(order)) - case (CreationDate, order) => resources.sortWith(creationDateSort(order)) + case (OrderBy.LastModificationDate, order) => resources.sortWith(lastModificationDateSort(order)) + case (OrderBy.CreationDate, order) => resources.sortWith(creationDateSort(order)) } /** diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resources/api/ResourceInfoRestServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/resources/api/ResourceInfoRestServiceSpec.scala index f3348ad2fe4..2f67d91a505 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/resources/api/ResourceInfoRestServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/resources/api/ResourceInfoRestServiceSpec.scala @@ -16,15 +16,13 @@ import java.util.UUID.randomUUID import dsp.errors.BadRequestException import org.knora.webapi.IRI import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.admin.api.model.Order +import org.knora.webapi.slice.admin.api.model.Order.* +import org.knora.webapi.slice.admin.api.model.OrderBy +import org.knora.webapi.slice.admin.api.model.OrderBy.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.resources.api.model.ListResponseDto -import org.knora.webapi.slice.resources.api.model.QueryParams.Asc -import org.knora.webapi.slice.resources.api.model.QueryParams.CreationDate -import org.knora.webapi.slice.resources.api.model.QueryParams.Desc -import org.knora.webapi.slice.resources.api.model.QueryParams.LastModificationDate -import org.knora.webapi.slice.resources.api.model.QueryParams.Order -import org.knora.webapi.slice.resources.api.model.QueryParams.OrderBy import org.knora.webapi.slice.resources.api.model.ResourceInfoDto import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService import org.knora.webapi.slice.resources.domain.ResourceInfo From 41b9e83ff5ec57e2f25926817990c14bc7ef9263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 09:45:37 +0200 Subject: [PATCH 03/99] cleanup --- .../slice/resources/api/MetadataServerEndpoints.scala | 6 ++---- .../slice/resources/api/ResourcesEndpointsHandler.scala | 2 +- .../slice/resources/api/StandoffEndpointsHandler.scala | 7 ++----- .../slice/resources/api/ValuesEndpointsHandler.scala | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala index e0eec4c7ca2..507df567cc0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala @@ -16,11 +16,9 @@ final case class MetadataServerEndpoints( private val mapper: HandlerMapper, ) { val allHandlers = - Seq( - SecuredEndpointHandler(endpoints.getResourcesMetadata, resourcesRestService.getResourcesMetadata), - ).map(mapper.mapSecuredEndpointHandler) + Seq(SecuredEndpointHandler(endpoints.getResourcesMetadata, resourcesRestService.getResourcesMetadata)) + .map(mapper.mapSecuredEndpointHandler) } - object MetadataServerEndpoints { val layer = ZLayer.derive[MetadataServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index 65b5ad7778d..fc6d9946456 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -47,7 +47,7 @@ final class ResourcesEndpointsHandler( SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), SecuredEndpointHandler(resourcesEndpoints.postResources, resourcesRestService.createResource), SecuredEndpointHandler(resourcesEndpoints.putResources, resourcesRestService.updateResourceMetadata), - ).map(mapper.mapSecuredEndpointHandler(_)) + ).map(mapper.mapSecuredEndpointHandler) } object ResourcesEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala index 3fbb5d2e71a..0c932b8d8dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala @@ -16,11 +16,8 @@ final case class StandoffEndpointsHandler( standoffRestService: StandoffRestService, mapper: HandlerMapper, ) { - - val allHandlers = - Seq( - SecuredEndpointHandler(endpoints.postMapping, standoffRestService.createMapping), - ).map(mapper.mapSecuredEndpointHandler(_)) + val allHandlers = Seq(SecuredEndpointHandler(endpoints.postMapping, standoffRestService.createMapping)) + .map(mapper.mapSecuredEndpointHandler) } object StandoffEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala index ced5ad2409a..b1875964f06 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala @@ -29,7 +29,7 @@ final class ValuesEndpointsHandler( SecuredEndpointHandler(endpoints.deleteValues, valuesRestService.deleteValue), SecuredEndpointHandler(endpoints.postValuesErase, valuesRestService.eraseValue), SecuredEndpointHandler(endpoints.postValuesErasehistory, valuesRestService.eraseValueHistory), - ).map(mapper.mapSecuredEndpointHandler(_)) + ).map(mapper.mapSecuredEndpointHandler) } object ValuesEndpointsHandler { From 8fa280e53fc99bb661bbd84e8821706bfb97f62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 09:49:03 +0200 Subject: [PATCH 04/99] Move SearchApiRoutes and SearchRestService to own files --- .../slice/search/api/SearchApiRoutes.scala | 154 ++++++++ .../slice/search/api/SearchEndpoints.scala | 336 ------------------ .../slice/search/api/SearchRestService.scala | 212 +++++++++++ 3 files changed, 366 insertions(+), 336 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala new file mode 100644 index 00000000000..d60f84b2b27 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala @@ -0,0 +1,154 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.search.api +import org.apache.pekko.http.scaladsl.server.Route +import sttp.model.MediaType +import zio.* + +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.common.api.SecuredEndpointHandler +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import org.knora.webapi.slice.common.service.IriConverter +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset + +final case class SearchApiRoutes( + searchEndpoints: SearchEndpoints, + searchRestService: SearchRestService, + mapper: HandlerMapper, + tapirToPekko: TapirToPekkoInterpreter, + iriConverter: IriConverter, +) { + private type GravsearchQuery = String + + private val postGravsearch = + SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.postGravsearch, + user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, + ) + + private val getGravsearch = + SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getGravsearch, + user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, + ) + + private val postGravsearchCount = + SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.postGravsearchCount, + user => { case (query, opts, limitToProject) => + searchRestService.gravsearchCount(query, opts, user, limitToProject) + }, + ) + + private val getGravsearchCount = + SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getGravsearchCount, + user => { case (query, opts, limitToProject) => + searchRestService.gravsearchCount(query, opts, user, limitToProject) + }, + ) + + private val getSearchIncomingLinks = + SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getSearchIncomingLinks, + user => { case (resourceIri, offset, opts, limitToProject) => + searchRestService.searchIncomingLinks(resourceIri.value, offset, opts, user, limitToProject) + }, + ) + + private val getSearchStillImageRepresentations = + SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getSearchStillImageRepresentations, + user => { case (resourceIri, offset, opts, limitToProject) => + searchRestService.getSearchStillImageRepresentations(resourceIri.value, offset, opts, user, limitToProject) + }, + ) + + private val getSearchStillImageRepresentationsCount = + SecuredEndpointHandler[(InputIri, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getSearchStillImageRepresentationsCount, + user => { case (resourceIri, opts, limitToProject) => + searchRestService.getSearchStillImageRepresentationsCount(resourceIri.value, opts, user, limitToProject) + }, + ) + + private val getSearchIncomingRegions = + SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( + searchEndpoints.getSearchIncomingRegions, + user => { case (resourceIri, offset, opts, limitToProject) => + searchRestService.searchIncomingRegions(resourceIri.value, offset, opts, user, limitToProject) + }, + ) + + private val getSearchByLabel = + SecuredEndpointHandler[ + (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri]), + (RenderedResponse, MediaType), + ]( + searchEndpoints.getSearchByLabel, + user => { case (query, opts, offset, project, resourceClass) => + searchRestService.searchResourcesByLabelV2(query, opts, offset, project, resourceClass, user) + }, + ) + + private val getSearchByLabelCount = + SecuredEndpointHandler[ + (String, FormatOptions, Option[ProjectIri], Option[InputIri]), + (RenderedResponse, MediaType), + ]( + searchEndpoints.getSearchByLabelCount, + _ => { case (query, opts, project, resourceClass) => + searchRestService.searchResourcesByLabelCountV2(query, opts, project, resourceClass) + }, + ) + + private val getFullTextSearch = + SecuredEndpointHandler[ + (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri], Option[InputIri], Boolean), + (RenderedResponse, MediaType), + ]( + searchEndpoints.getFullTextSearch, + user => { case (query, opts, offset, project, resourceClass, standoffClass, returnFiles) => + searchRestService.fullTextSearch(query, opts, offset, project, resourceClass, standoffClass, returnFiles, user) + }, + ) + + private val getFullTextSearchCount = + SecuredEndpointHandler[ + (String, FormatOptions, Option[ProjectIri], Option[InputIri], Option[InputIri]), + (RenderedResponse, MediaType), + ]( + searchEndpoints.getFullTextSearchCount, + _ => { case (query, opts, project, resourceClass, standoffClass) => + searchRestService.fullTextSearchCount(query, opts, project, resourceClass, standoffClass) + }, + ) + + val routes: Seq[Route] = + Seq( + getFullTextSearch, + getFullTextSearchCount, + getSearchByLabel, + getSearchByLabelCount, + postGravsearch, + getGravsearch, + postGravsearchCount, + getGravsearchCount, + getSearchIncomingLinks, + getSearchStillImageRepresentations, + getSearchStillImageRepresentationsCount, + getSearchIncomingRegions, + ) + .map(it => mapper.mapSecuredEndpointHandler(it)) + .map(it => tapirToPekko.toRoute(it)) +} +object SearchApiRoutes { + val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 33d4cd02f20..0fe9307dac4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -8,21 +8,14 @@ package org.knora.webapi.slice.search.api import eu.timepit.refined.api.Refined import eu.timepit.refined.api.RefinedTypeOps import eu.timepit.refined.numeric.Greater -import io.opentelemetry.api.common.Attributes -import io.sentry.Sentry -import io.sentry.SentryLevel -import org.apache.pekko.http.scaladsl.server.Route import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import sttp.tapir.codec.refined.* -import zio.Task import zio.ZIO import zio.ZLayer -import zio.telemetry.opentelemetry.tracing.Tracing import dsp.valueobjects.Iri -import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.slice.admin.api.Codecs.TapirCodec.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.User @@ -30,10 +23,7 @@ import org.knora.webapi.slice.common.StringValueCompanion import org.knora.webapi.slice.common.Value.StringValue import org.knora.webapi.slice.common.api.* import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions -import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse -import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri -import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset object SearchEndpointsInputs { @@ -217,329 +207,3 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { object SearchEndpoints { val layer = ZLayer.derive[SearchEndpoints] } - -final case class SearchApiRoutes( - searchEndpoints: SearchEndpoints, - searchRestService: SearchRestService, - mapper: HandlerMapper, - tapirToPekko: TapirToPekkoInterpreter, - iriConverter: IriConverter, -) { - private type GravsearchQuery = String - - private val postGravsearch = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.postGravsearch, - user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, - ) - - private val getGravsearch = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getGravsearch, - user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, - ) - - private val postGravsearchCount = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.postGravsearchCount, - user => { case (query, opts, limitToProject) => - searchRestService.gravsearchCount(query, opts, user, limitToProject) - }, - ) - - private val getGravsearchCount = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getGravsearchCount, - user => { case (query, opts, limitToProject) => - searchRestService.gravsearchCount(query, opts, user, limitToProject) - }, - ) - - private val getSearchIncomingLinks = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchIncomingLinks, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.searchIncomingLinks(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchStillImageRepresentations = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchStillImageRepresentations, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.getSearchStillImageRepresentations(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchStillImageRepresentationsCount = - SecuredEndpointHandler[(InputIri, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchStillImageRepresentationsCount, - user => { case (resourceIri, opts, limitToProject) => - searchRestService.getSearchStillImageRepresentationsCount(resourceIri.value, opts, user, limitToProject) - }, - ) - - private val getSearchIncomingRegions = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchIncomingRegions, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.searchIncomingRegions(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchByLabel = - SecuredEndpointHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getSearchByLabel, - user => { case (query, opts, offset, project, resourceClass) => - searchRestService.searchResourcesByLabelV2(query, opts, offset, project, resourceClass, user) - }, - ) - - private val getSearchByLabelCount = - SecuredEndpointHandler[ - (String, FormatOptions, Option[ProjectIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getSearchByLabelCount, - _ => { case (query, opts, project, resourceClass) => - searchRestService.searchResourcesByLabelCountV2(query, opts, project, resourceClass) - }, - ) - - private val getFullTextSearch = - SecuredEndpointHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri], Option[InputIri], Boolean), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getFullTextSearch, - user => { case (query, opts, offset, project, resourceClass, standoffClass, returnFiles) => - searchRestService.fullTextSearch(query, opts, offset, project, resourceClass, standoffClass, returnFiles, user) - }, - ) - - private val getFullTextSearchCount = - SecuredEndpointHandler[ - (String, FormatOptions, Option[ProjectIri], Option[InputIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getFullTextSearchCount, - _ => { case (query, opts, project, resourceClass, standoffClass) => - searchRestService.fullTextSearchCount(query, opts, project, resourceClass, standoffClass) - }, - ) - - val routes: Seq[Route] = - Seq( - getFullTextSearch, - getFullTextSearchCount, - getSearchByLabel, - getSearchByLabelCount, - postGravsearch, - getGravsearch, - postGravsearchCount, - getGravsearchCount, - getSearchIncomingLinks, - getSearchStillImageRepresentations, - getSearchStillImageRepresentationsCount, - getSearchIncomingRegions, - ) - .map(it => mapper.mapSecuredEndpointHandler(it)) - .map(it => tapirToPekko.toRoute(it)) -} - -object SearchApiRoutes { - val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] -} - -final case class SearchRestService( - searchResponderV2: SearchResponderV2, - renderer: KnoraResponseRenderer, - iriConverter: IriConverter, - tracing: Tracing, -) { - - def searchResourcesByLabelV2( - query: String, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - limitByResourceClass: Option[InputIri], - user: User, - ): Task[(RenderedResponse, MediaType)] = for { - resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) - searchResult <- - searchResponderV2.searchResourcesByLabelV2(query, offset.value, project, resourceClass, opts.schema, user) - response <- renderer.render(searchResult, opts) - } yield response - - def searchResourcesByLabelCountV2( - query: String, - opts: FormatOptions, - project: Option[ProjectIri], - limitByResourceClass: Option[InputIri], - ): Task[(RenderedResponse, MediaType)] = for { - resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) - searchResult <- - searchResponderV2.searchResourcesByLabelCountV2(query, project, resourceClass) - response <- renderer.render(searchResult, opts) - } yield response - - def gravsearch( - query: String, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = for { - searchResult <- searchResponderV2.gravsearchV2(query, opts.schemaRendering, user, limitToProject) - response <- renderer.render(searchResult, opts) - } yield response - - def gravsearchCount( - query: String, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = for { - searchResult <- searchResponderV2.gravsearchCountV2(query, user, limitToProject) - response <- renderer.render(searchResult, opts) - } yield response - - def searchIncomingLinks( - resourceIri: String, - offset: Offset, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = - (for { - searchResult <- - searchResponderV2.searchIncomingLinksV2(resourceIri, offset.value, opts.schemaRendering, user, limitToProject) - @@ tracing.aspects.span("query") - response <- renderer.render(searchResult, opts) @@ tracing.aspects.span("render") - _ <- ZIO.succeed(Sentry.captureMessage("searchIncomingLinks", SentryLevel.INFO)) - } yield response) @@ tracing.aspects.root( - spanName = "searchIncomingLinks", - attributes = Attributes - .builder() - .put("resourceIri", resourceIri) - .put("offset", offset.value) - .put("limitToProject", limitToProject.map(_.value).getOrElse("None")) - .build(), - ) - - def getSearchStillImageRepresentations( - resourceIri: String, - offset: Offset, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = - for { - response <- - tracing.root("searchStillImageRepresentations") { - for { - result <- - searchResponderV2.searchStillImageRepresentationsV2( - resourceIri, - offset.value, - opts.schemaRendering, - user, - limitToProject, - ) - rr <- renderer.render(result, opts) - } yield rr - } - } yield response - - def getSearchStillImageRepresentationsCount( - resourceIri: String, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = - for { - response <- - tracing.root("searchStillImageRepresentationsCount") { - for { - result <- - searchResponderV2.searchStillImageRepresentationsCountV2( - resourceIri, - user, - limitToProject, - ) - rr <- renderer.render(result, opts) - } yield rr - } - } yield response - - def searchIncomingRegions( - resourceIri: String, - offset: Offset, - opts: FormatOptions, - user: User, - limitToProject: Option[ProjectIri], - ): Task[(RenderedResponse, MediaType)] = - for { - response <- - tracing.root("searchIncomingRegions") { - for { - searchResult <- - searchResponderV2.searchIncomingRegionsV2( - resourceIri, - offset.value, - opts.schemaRendering, - user, - limitToProject, - ) - response <- renderer.render(searchResult, opts) - } yield response - } - } yield response - - def fullTextSearch( - query: RenderedResponse, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - resourceClass: Option[InputIri], - standoffClass: Option[InputIri], - returnFiles: Boolean, - user: User, - ): Task[(RenderedResponse, MediaType)] = for { - resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) - standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) - searchResult <- searchResponderV2.fulltextSearchV2( - query, - offset.value, - project, - resourceClass, - standoffClass, - returnFiles, - opts.schemaRendering, - user, - ) - response <- renderer.render(searchResult, opts) - } yield response - - def fullTextSearchCount( - query: RenderedResponse, - opts: FormatOptions, - project: Option[ProjectIri], - resourceClass: Option[InputIri], - standoffClass: Option[InputIri], - ): zio.Task[ - (_root_.org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse, _root_.sttp.model.MediaType), - ] = for { - resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) - standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) - searchResult <- searchResponderV2.fulltextSearchCountV2(query, project, resourceClass, standoffClass) - response <- renderer.render(searchResult, opts) - } yield response -} - -object SearchRestService { - val layer = ZLayer.derive[SearchRestService] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala new file mode 100644 index 00000000000..d1278523e73 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala @@ -0,0 +1,212 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.search.api + +import io.opentelemetry.api.common.Attributes +import io.sentry.Sentry +import io.sentry.SentryLevel +import sttp.model.MediaType +import zio.* +import zio.telemetry.opentelemetry.tracing.Tracing + +import org.knora.webapi.responders.v2.SearchResponderV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.api.KnoraResponseRenderer +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.common.service.IriConverter +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset + +final case class SearchRestService( + searchResponderV2: SearchResponderV2, + renderer: KnoraResponseRenderer, + iriConverter: IriConverter, + tracing: Tracing, +) { + + def searchResourcesByLabelV2( + query: String, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + limitByResourceClass: Option[InputIri], + user: User, + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- + searchResponderV2.searchResourcesByLabelV2(query, offset.value, project, resourceClass, opts.schema, user) + response <- renderer.render(searchResult, opts) + } yield response + + def searchResourcesByLabelCountV2( + query: String, + opts: FormatOptions, + project: Option[ProjectIri], + limitByResourceClass: Option[InputIri], + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- + searchResponderV2.searchResourcesByLabelCountV2(query, project, resourceClass) + response <- renderer.render(searchResult, opts) + } yield response + + def gravsearch( + query: String, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = for { + searchResult <- searchResponderV2.gravsearchV2(query, opts.schemaRendering, user, limitToProject) + response <- renderer.render(searchResult, opts) + } yield response + + def gravsearchCount( + query: String, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = for { + searchResult <- searchResponderV2.gravsearchCountV2(query, user, limitToProject) + response <- renderer.render(searchResult, opts) + } yield response + + def searchIncomingLinks( + resourceIri: String, + offset: Offset, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = + (for { + searchResult <- + searchResponderV2.searchIncomingLinksV2(resourceIri, offset.value, opts.schemaRendering, user, limitToProject) + @@ tracing.aspects.span("query") + response <- renderer.render(searchResult, opts) @@ tracing.aspects.span("render") + _ <- ZIO.succeed(Sentry.captureMessage("searchIncomingLinks", SentryLevel.INFO)) + } yield response) @@ tracing.aspects.root( + spanName = "searchIncomingLinks", + attributes = Attributes + .builder() + .put("resourceIri", resourceIri) + .put("offset", offset.value) + .put("limitToProject", limitToProject.map(_.value).getOrElse("None")) + .build(), + ) + + def getSearchStillImageRepresentations( + resourceIri: String, + offset: Offset, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = + for { + response <- + tracing.root("searchStillImageRepresentations") { + for { + result <- + searchResponderV2.searchStillImageRepresentationsV2( + resourceIri, + offset.value, + opts.schemaRendering, + user, + limitToProject, + ) + rr <- renderer.render(result, opts) + } yield rr + } + } yield response + + def getSearchStillImageRepresentationsCount( + resourceIri: String, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = + for { + response <- + tracing.root("searchStillImageRepresentationsCount") { + for { + result <- + searchResponderV2.searchStillImageRepresentationsCountV2( + resourceIri, + user, + limitToProject, + ) + rr <- renderer.render(result, opts) + } yield rr + } + } yield response + + def searchIncomingRegions( + resourceIri: String, + offset: Offset, + opts: FormatOptions, + user: User, + limitToProject: Option[ProjectIri], + ): Task[(RenderedResponse, MediaType)] = + for { + response <- + tracing.root("searchIncomingRegions") { + for { + searchResult <- + searchResponderV2.searchIncomingRegionsV2( + resourceIri, + offset.value, + opts.schemaRendering, + user, + limitToProject, + ) + response <- renderer.render(searchResult, opts) + } yield response + } + } yield response + + def fullTextSearch( + query: RenderedResponse, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri], + returnFiles: Boolean, + user: User, + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) + standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- searchResponderV2.fulltextSearchV2( + query, + offset.value, + project, + resourceClass, + standoffClass, + returnFiles, + opts.schemaRendering, + user, + ) + response <- renderer.render(searchResult, opts) + } yield response + + def fullTextSearchCount( + query: RenderedResponse, + opts: FormatOptions, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri], + ): Task[ + (KnoraResponseRenderer.RenderedResponse, MediaType), + ] = for { + resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) + standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- searchResponderV2.fulltextSearchCountV2(query, project, resourceClass, standoffClass) + response <- renderer.render(searchResult, opts) + } yield response +} +object SearchRestService { + val layer = ZLayer.derive[SearchRestService] +} From 356797d36458997574314dd03adf4305933f546a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 09:58:20 +0200 Subject: [PATCH 05/99] align SearchRestService methods and inline handlers in SearchApiRoutes --- .../slice/search/api/SearchApiRoutes.scala | 139 +++--------------- .../slice/search/api/SearchRestService.scala | 53 ++++--- 2 files changed, 44 insertions(+), 148 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala index d60f84b2b27..bb089d3f77f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala @@ -25,129 +25,28 @@ final case class SearchApiRoutes( tapirToPekko: TapirToPekkoInterpreter, iriConverter: IriConverter, ) { - private type GravsearchQuery = String - - private val postGravsearch = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.postGravsearch, - user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, - ) - - private val getGravsearch = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getGravsearch, - user => { case (query, opts, limitToProject) => searchRestService.gravsearch(query, opts, user, limitToProject) }, - ) - - private val postGravsearchCount = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.postGravsearchCount, - user => { case (query, opts, limitToProject) => - searchRestService.gravsearchCount(query, opts, user, limitToProject) - }, - ) - - private val getGravsearchCount = - SecuredEndpointHandler[(GravsearchQuery, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getGravsearchCount, - user => { case (query, opts, limitToProject) => - searchRestService.gravsearchCount(query, opts, user, limitToProject) - }, - ) - - private val getSearchIncomingLinks = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchIncomingLinks, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.searchIncomingLinks(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchStillImageRepresentations = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchStillImageRepresentations, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.getSearchStillImageRepresentations(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchStillImageRepresentationsCount = - SecuredEndpointHandler[(InputIri, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchStillImageRepresentationsCount, - user => { case (resourceIri, opts, limitToProject) => - searchRestService.getSearchStillImageRepresentationsCount(resourceIri.value, opts, user, limitToProject) - }, - ) - - private val getSearchIncomingRegions = - SecuredEndpointHandler[(InputIri, Offset, FormatOptions, Option[ProjectIri]), (RenderedResponse, MediaType)]( - searchEndpoints.getSearchIncomingRegions, - user => { case (resourceIri, offset, opts, limitToProject) => - searchRestService.searchIncomingRegions(resourceIri.value, offset, opts, user, limitToProject) - }, - ) - - private val getSearchByLabel = - SecuredEndpointHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getSearchByLabel, - user => { case (query, opts, offset, project, resourceClass) => - searchRestService.searchResourcesByLabelV2(query, opts, offset, project, resourceClass, user) - }, - ) - - private val getSearchByLabelCount = - SecuredEndpointHandler[ - (String, FormatOptions, Option[ProjectIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getSearchByLabelCount, - _ => { case (query, opts, project, resourceClass) => - searchRestService.searchResourcesByLabelCountV2(query, opts, project, resourceClass) - }, - ) - - private val getFullTextSearch = - SecuredEndpointHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri], Option[InputIri], Boolean), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getFullTextSearch, - user => { case (query, opts, offset, project, resourceClass, standoffClass, returnFiles) => - searchRestService.fullTextSearch(query, opts, offset, project, resourceClass, standoffClass, returnFiles, user) - }, - ) - - private val getFullTextSearchCount = - SecuredEndpointHandler[ - (String, FormatOptions, Option[ProjectIri], Option[InputIri], Option[InputIri]), - (RenderedResponse, MediaType), - ]( - searchEndpoints.getFullTextSearchCount, - _ => { case (query, opts, project, resourceClass, standoffClass) => - searchRestService.fullTextSearchCount(query, opts, project, resourceClass, standoffClass) - }, - ) val routes: Seq[Route] = Seq( - getFullTextSearch, - getFullTextSearchCount, - getSearchByLabel, - getSearchByLabelCount, - postGravsearch, - getGravsearch, - postGravsearchCount, - getGravsearchCount, - getSearchIncomingLinks, - getSearchStillImageRepresentations, - getSearchStillImageRepresentationsCount, - getSearchIncomingRegions, - ) - .map(it => mapper.mapSecuredEndpointHandler(it)) - .map(it => tapirToPekko.toRoute(it)) + SecuredEndpointHandler(searchEndpoints.getFullTextSearch, searchRestService.fullTextSearch), + SecuredEndpointHandler(searchEndpoints.getFullTextSearchCount, searchRestService.fullTextSearchCount), + SecuredEndpointHandler(searchEndpoints.getSearchByLabel, searchRestService.searchResourcesByLabelV2), + SecuredEndpointHandler(searchEndpoints.getSearchByLabelCount, searchRestService.searchResourcesByLabelCountV2), + SecuredEndpointHandler(searchEndpoints.postGravsearch, searchRestService.gravsearch), + SecuredEndpointHandler(searchEndpoints.getGravsearch, searchRestService.gravsearch), + SecuredEndpointHandler(searchEndpoints.postGravsearchCount, searchRestService.gravsearchCount), + SecuredEndpointHandler(searchEndpoints.getGravsearchCount, searchRestService.gravsearchCount), + SecuredEndpointHandler(searchEndpoints.getSearchIncomingLinks, searchRestService.searchIncomingLinks), + SecuredEndpointHandler( + searchEndpoints.getSearchStillImageRepresentations, + searchRestService.getSearchStillImageRepresentations, + ), + SecuredEndpointHandler( + searchEndpoints.getSearchStillImageRepresentationsCount, + searchRestService.getSearchStillImageRepresentationsCount, + ), + SecuredEndpointHandler(searchEndpoints.getSearchIncomingRegions, searchRestService.searchIncomingRegions), + ).map(mapper.mapSecuredEndpointHandler).map(tapirToPekko.toRoute) } object SearchApiRoutes { val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala index d1278523e73..dc0755e9cb8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchRestService.scala @@ -29,13 +29,12 @@ final case class SearchRestService( tracing: Tracing, ) { - def searchResourcesByLabelV2( + def searchResourcesByLabelV2(user: User)( query: String, opts: FormatOptions, offset: Offset, project: Option[ProjectIri], limitByResourceClass: Option[InputIri], - user: User, ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) searchResult <- @@ -43,7 +42,7 @@ final case class SearchRestService( response <- renderer.render(searchResult, opts) } yield response - def searchResourcesByLabelCountV2( + def searchResourcesByLabelCountV2(ignored: User)( query: String, opts: FormatOptions, project: Option[ProjectIri], @@ -55,54 +54,55 @@ final case class SearchRestService( response <- renderer.render(searchResult, opts) } yield response - def gravsearch( + def gravsearch(user: User)( query: String, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = for { searchResult <- searchResponderV2.gravsearchV2(query, opts.schemaRendering, user, limitToProject) response <- renderer.render(searchResult, opts) } yield response - def gravsearchCount( + def gravsearchCount(user: User)( query: String, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = for { searchResult <- searchResponderV2.gravsearchCountV2(query, user, limitToProject) response <- renderer.render(searchResult, opts) } yield response - def searchIncomingLinks( - resourceIri: String, + def searchIncomingLinks(user: User)( + resourceIri: InputIri, offset: Offset, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = (for { searchResult <- - searchResponderV2.searchIncomingLinksV2(resourceIri, offset.value, opts.schemaRendering, user, limitToProject) - @@ tracing.aspects.span("query") + searchResponderV2.searchIncomingLinksV2( + resourceIri.value, + offset.value, + opts.schemaRendering, + user, + limitToProject, + ) @@ tracing.aspects.span("query") response <- renderer.render(searchResult, opts) @@ tracing.aspects.span("render") _ <- ZIO.succeed(Sentry.captureMessage("searchIncomingLinks", SentryLevel.INFO)) } yield response) @@ tracing.aspects.root( spanName = "searchIncomingLinks", attributes = Attributes .builder() - .put("resourceIri", resourceIri) + .put("resourceIri", resourceIri.value) .put("offset", offset.value) .put("limitToProject", limitToProject.map(_.value).getOrElse("None")) .build(), ) - def getSearchStillImageRepresentations( - resourceIri: String, + def getSearchStillImageRepresentations(user: User)( + resourceIri: InputIri, offset: Offset, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = for { @@ -111,7 +111,7 @@ final case class SearchRestService( for { result <- searchResponderV2.searchStillImageRepresentationsV2( - resourceIri, + resourceIri.value, offset.value, opts.schemaRendering, user, @@ -122,10 +122,9 @@ final case class SearchRestService( } } yield response - def getSearchStillImageRepresentationsCount( - resourceIri: String, + def getSearchStillImageRepresentationsCount(user: User)( + resourceIri: InputIri, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = for { @@ -134,7 +133,7 @@ final case class SearchRestService( for { result <- searchResponderV2.searchStillImageRepresentationsCountV2( - resourceIri, + resourceIri.value, user, limitToProject, ) @@ -143,11 +142,10 @@ final case class SearchRestService( } } yield response - def searchIncomingRegions( - resourceIri: String, + def searchIncomingRegions(user: User)( + resourceIri: InputIri, offset: Offset, opts: FormatOptions, - user: User, limitToProject: Option[ProjectIri], ): Task[(RenderedResponse, MediaType)] = for { @@ -156,7 +154,7 @@ final case class SearchRestService( for { searchResult <- searchResponderV2.searchIncomingRegionsV2( - resourceIri, + resourceIri.value, offset.value, opts.schemaRendering, user, @@ -167,7 +165,7 @@ final case class SearchRestService( } } yield response - def fullTextSearch( + def fullTextSearch(user: User)( query: RenderedResponse, opts: FormatOptions, offset: Offset, @@ -175,7 +173,6 @@ final case class SearchRestService( resourceClass: Option[InputIri], standoffClass: Option[InputIri], returnFiles: Boolean, - user: User, ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) @@ -192,7 +189,7 @@ final case class SearchRestService( response <- renderer.render(searchResult, opts) } yield response - def fullTextSearchCount( + def fullTextSearchCount(ignored: User)( query: RenderedResponse, opts: FormatOptions, project: Option[ProjectIri], From fb1ab7f0d7c4458a2b6afc667a63f1c3265f8cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 10:04:16 +0200 Subject: [PATCH 06/99] Move ManagementEndpoints to own file and cleanup --- .../slice/admin/api/AdminApiRoutes.scala | 2 +- .../api/ManagementEndpoints.scala | 54 ------------------- .../slice/lists/api/ListsApiV2Routes.scala | 3 +- .../resources/api/ResourceInfoRoutes.scala | 8 +-- .../slice/search/api/SearchApiRoutes.scala | 10 ++-- .../api/AuthenticationApiRoutes.scala | 7 ++- .../slice/shacl/api/ShaclApiRoutes.scala | 8 +-- .../shacl/api/ShaclEndpointsHandler.scala | 14 ++--- 8 files changed, 26 insertions(+), 80 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala index dbd622f7e8e..1d813404632 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala @@ -11,9 +11,9 @@ import zio.ZLayer import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter final case class AdminApiRoutes( + private val adminLists: AdminListsEndpointsHandlers, private val filesEndpoints: FilesEndpointsHandler, private val groups: GroupsEndpointsHandler, - private val adminLists: AdminListsEndpointsHandlers, private val maintenance: MaintenanceEndpointsHandlers, private val permissions: PermissionsEndpointsHandlers, private val project: ProjectsEndpointsHandler, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementEndpoints.scala index 86792a666ea..a086c8e343c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementEndpoints.scala @@ -9,21 +9,13 @@ import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.jsonBody -import zio.UIO import zio.ZIO import zio.json.DeriveJsonCodec import zio.json.JsonCodec -import org.knora.webapi.core.State import org.knora.webapi.core.domain.AppState import org.knora.webapi.http.version.BuildInfo -import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter -import org.knora.webapi.store.triplestore.api.TriplestoreService final case class VersionResponse( webapi: String, @@ -95,49 +87,3 @@ final case class ManagementEndpoints(baseEndpoints: BaseEndpoints) { object ManagementEndpoints { val layer = zio.ZLayer.derive[ManagementEndpoints] } - -final case class ManagementRoutes( - endpoint: ManagementEndpoints, - state: State, - mapper: HandlerMapper, - tapirToPekko: TapirToPekkoInterpreter, - triplestore: TriplestoreService, - auth: AuthorizationRestService, -) { - - private val versionEndpointHandler = - PublicEndpointHandler[Unit, VersionResponse](endpoint.getVersion, _ => ZIO.succeed(VersionResponse.current)) - - private val healthEndpointHandler = - PublicEndpointHandler[Unit, (HealthResponse, StatusCode)](endpoint.getHealth, _ => createHealthResponse) - - private val createHealthResponse: UIO[(HealthResponse, StatusCode)] = - state.getAppState.map { s => - val response = HealthResponse.from(s) - (response, if (response.status) StatusCode.Ok else StatusCode.ServiceUnavailable) - } - - private val startCompactionHandler = - SecuredEndpointHandler[Unit, (String, StatusCode)]( - endpoint.postStartCompaction, - user => - _ => - for { - _ <- auth.ensureSystemAdmin(user) - success <- triplestore.compact() - } yield if (success) ("ok", StatusCode.Ok) else ("forbidden", StatusCode.Forbidden), - ) - - val routes = ( - List(versionEndpointHandler, healthEndpointHandler) - .map(mapper.mapPublicEndpointHandler(_)) - ++ - List(startCompactionHandler) - .map(mapper.mapSecuredEndpointHandler(_)) - ) - .map(tapirToPekko.toRoute) -} - -object ManagementRoutes { - val layer = zio.ZLayer.derive[ManagementRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala index f2fa6860a25..92cf82f9a9c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala @@ -14,8 +14,7 @@ final case class ListsApiV2Routes( private val listsEndpointsV2: ListsEndpointsV2Handler, private val tapirToPekko: TapirToPekkoInterpreter, ) { - private val handlers = listsEndpointsV2.allHandlers - val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) + val routes: Seq[Route] = listsEndpointsV2.allHandlers.map(tapirToPekko.toRoute(_)) } object ListsApiV2Routes { val layer = ZLayer.derive[ListsApiV2Routes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala index 7bf3dd39115..d6cd2b0883a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala @@ -16,10 +16,10 @@ import org.knora.webapi.slice.resources.api.model.ListResponseDto import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService final case class ResourceInfoRoutes( - endpoints: ResourceInfoEndpoints, - resourceInfoService: ResourceInfoRestService, - mapper: HandlerMapper, - interpreter: TapirToPekkoInterpreter, + private val endpoints: ResourceInfoEndpoints, + private val interpreter: TapirToPekkoInterpreter, + private val mapper: HandlerMapper, + private val resourceInfoService: ResourceInfoRestService, ) { val routes: Seq[Route] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala index bb089d3f77f..2487a424c76 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala @@ -19,11 +19,11 @@ import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset final case class SearchApiRoutes( - searchEndpoints: SearchEndpoints, - searchRestService: SearchRestService, - mapper: HandlerMapper, - tapirToPekko: TapirToPekkoInterpreter, - iriConverter: IriConverter, + private val iriConverter: IriConverter, + private val mapper: HandlerMapper, + private val searchEndpoints: SearchEndpoints, + private val searchRestService: SearchRestService, + private val tapirToPekko: TapirToPekkoInterpreter, ) { val routes: Seq[Route] = diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala index f2e42cf292e..085fcd935b6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala @@ -9,8 +9,11 @@ import zio.ZLayer import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter -final class AuthenticationApiRoutes(handler: AuthenticationEndpointsV2Handler, tapirToPekko: TapirToPekkoInterpreter) { - val routes = handler.allHandlers.map(tapirToPekko.toRoute(_)) +final class AuthenticationApiRoutes( + private val handler: AuthenticationEndpointsV2Handler, + private val tapirToPekko: TapirToPekkoInterpreter, +) { + val routes = handler.allHandlers.map(tapirToPekko.toRoute) } object AuthenticationApiRoutes { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala index 2ff4328968b..b66cebc7256 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala @@ -10,9 +10,11 @@ import zio.* import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter -final case class ShaclApiRoutes(shaclEndpointsHandler: ShaclEndpointsHandler, tapirToPekko: TapirToPekkoInterpreter) { - private val handlers = shaclEndpointsHandler.allHandlers - val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) +final case class ShaclApiRoutes( + private val shaclEndpointsHandler: ShaclEndpointsHandler, + private val tapirToPekko: TapirToPekkoInterpreter, +) { + val routes: Seq[Route] = shaclEndpointsHandler.allHandlers.map(tapirToPekko.toRoute(_)) } object ShaclApiRoutes { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala index f0ecdc65af9..46944ff5b26 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala @@ -11,17 +11,13 @@ import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.PublicEndpointHandler case class ShaclEndpointsHandler( - shaclEndpoints: ShaclEndpoints, - shaclApiService: ShaclApiService, - mapper: HandlerMapper, + private val shaclEndpoints: ShaclEndpoints, + private val shaclApiService: ShaclApiService, + private val mapper: HandlerMapper, ) { - val validate = PublicEndpointHandler( - shaclEndpoints.validate, - shaclApiService.validate, - ) - - val allHandlers = List(validate).map(mapper.mapPublicEndpointHandler) + val allHandlers = + List(PublicEndpointHandler(shaclEndpoints.validate, shaclApiService.validate)).map(mapper.mapPublicEndpointHandler) } object ShaclEndpointsHandler { From 1ce0fecb483a5db8181815f3ef6368085c1600ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 10:32:16 +0200 Subject: [PATCH 07/99] Introduce ManagementRestService --- .../org/knora/webapi/core/LayersLive.scala | 2 ++ .../api/ManagementRestService.scala | 35 +++++++++++++++++++ .../infrastructure/api/ManagementRoutes.scala | 35 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRestService.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index a3de9248650..21b66e313b2 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -44,6 +44,7 @@ import org.knora.webapi.slice.infrastructure.JwtService import org.knora.webapi.slice.infrastructure.MetricsServer.MetricsServerEnv import org.knora.webapi.slice.infrastructure.OpenTelemetry import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints +import org.knora.webapi.slice.infrastructure.api.ManagementRestService import org.knora.webapi.slice.infrastructure.api.ManagementRoutes import org.knora.webapi.slice.lists.api.ListsApiModule import org.knora.webapi.slice.ontology.OntologyModule @@ -204,6 +205,7 @@ object LayersLive { self => ListsApiModule.layer, ListsResponder.layer, ManagementEndpoints.layer, + ManagementRestService.layer, ManagementRoutes.layer, MessageRelayLive.layer, OntologyApiModule.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRestService.scala new file mode 100644 index 00000000000..e5b66b92d6d --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRestService.scala @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.infrastructure.api + +import sttp.model.StatusCode +import zio.* + +import org.knora.webapi.core.State +import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.common.api.AuthorizationRestService +import org.knora.webapi.store.triplestore.api.TriplestoreService + +final case class ManagementRestService( + private val auth: AuthorizationRestService, + private val state: State, + private val triplestore: TriplestoreService, +) { + + def healthCheck: UIO[(HealthResponse, StatusCode)] = + state.getAppState.map { s => + val response = HealthResponse.from(s) + (response, if (response.status) StatusCode.Ok else StatusCode.ServiceUnavailable) + } + + def startCompaction(user: User)(ignored: Unit): Task[(String, StatusCode)] = + auth.ensureSystemAdmin(user) *> + triplestore.compact().map(success => if (success) ("ok", StatusCode.Ok) else ("forbidden", StatusCode.Forbidden)) +} + +object ManagementRestService { + val layer = ZLayer.derive[ManagementRestService] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala new file mode 100644 index 00000000000..ae75d4bc33d --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.infrastructure.api + +import sttp.model.StatusCode +import zio.* + +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.PublicEndpointHandler +import org.knora.webapi.slice.common.api.SecuredEndpointHandler +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter + +final case class ManagementRoutes( + private val endpoint: ManagementEndpoints, + private val restService: ManagementRestService, + private val mapper: HandlerMapper, + private val tapirToPekko: TapirToPekkoInterpreter, +) { + + val routes = ( + List( + PublicEndpointHandler(endpoint.getVersion, _ => ZIO.succeed(VersionResponse.current)), + PublicEndpointHandler(endpoint.getHealth, _ => restService.healthCheck), + ).map(mapper.mapPublicEndpointHandler) + ++ + List(SecuredEndpointHandler(endpoint.postStartCompaction, restService.startCompaction)) + .map(mapper.mapSecuredEndpointHandler) + ).map(tapirToPekko.toRoute) +} +object ManagementRoutes { + val layer = zio.ZLayer.derive[ManagementRoutes] +} From 5df0270cbf6392a93bdddd6799abb04a18dd4b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 10:51:23 +0200 Subject: [PATCH 08/99] add flaky test aspect to "/maintenance/needs-top-left-correction should" test --- .../test/scala/swiss/dasch/api/MaintenanceEndpointsSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/src/test/scala/swiss/dasch/api/MaintenanceEndpointsSpec.scala b/ingest/src/test/scala/swiss/dasch/api/MaintenanceEndpointsSpec.scala index 967854967d6..52d63526f4f 100644 --- a/ingest/src/test/scala/swiss/dasch/api/MaintenanceEndpointsSpec.scala +++ b/ingest/src/test/scala/swiss/dasch/api/MaintenanceEndpointsSpec.scala @@ -53,7 +53,7 @@ object MaintenanceEndpointsSpec extends ZIOSpecDefault { } } }, - ) @@ TestAspect.withLiveClock + ) @@ TestAspect.withLiveClock @@ TestAspect.flaky val spec = suite("MaintenanceEndpoint")(needsTopleftCorrectionSuite) .provide( From 022090c0d5a7783f5ad272c2bfba6fc55084be97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 11:12:56 +0200 Subject: [PATCH 09/99] (wip) refactor: Replace pekko http with zio http server --- AGENTS.md | 4 +- CLAUDE.md | 4 +- build.sbt | 2 - docs/03-endpoints/api-util/version.md | 1 - .../04-publishing-deployment/configuration.md | 7 +- .../design/api-v2/how-to-add-a-route.md | 47 ---- mkdocs.yml | 1 - .../src/main/scala/RdfMediaTypes.scala | 38 --- .../org/knora/webapi/core/LayersTest.scala | 1 - project/Dependencies.scala | 14 - webapi/src/main/resources/application.conf | 246 +----------------- .../org/knora/webapi/core/HttpServer.scala | 58 ++--- .../org/knora/webapi/core/LayersLive.scala | 3 - .../knora/webapi/core/PekkoActorSystem.scala | 33 --- .../webapi/http/version/ServerVersion.scala | 19 +- .../responders/admin/ListsResponder.scala | 2 +- .../org/knora/webapi/routing/ApiRoutes.scala | 41 +-- .../slice/admin/api/AdminApiModule.scala | 4 +- ...es.scala => AdminApiServerEndpoints.scala} | 15 +- .../api/AdminListsEndpointsHandlers.scala | 21 +- .../admin/api/FilesEndpointsHandler.scala | 18 +- .../admin/api/GroupsEndpointsHandler.scala | 29 +-- .../api/MaintenanceEndpointsHandlers.scala | 12 +- .../ProjectsLegalInfoEntpointsHandler.scala | 1 - .../admin/api/service/GroupRestService.scala | 4 +- .../slice/common/api/BaseEndpoints.scala | 70 ++--- .../slice/common/api/HandlerMapper.scala | 64 ----- .../common/api/TapirToPekkoInterpreter.scala | 51 ---- .../http/version/ServerVersionSpec.scala | 21 -- 29 files changed, 108 insertions(+), 723 deletions(-) delete mode 100644 docs/05-internals/design/api-v2/how-to-add-a-route.md delete mode 100644 modules/testkit/src/main/scala/RdfMediaTypes.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/core/PekkoActorSystem.scala rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{AdminApiRoutes.scala => AdminApiServerEndpoints.scala} (76%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala delete mode 100644 webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala diff --git a/AGENTS.md b/AGENTS.md index 25e5abfdaf0..61a872ad2f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ When context is insufficient, prefer asking 1–3 focused questions rather than ## Project Overview DSP-API is the Digital Humanities Service Platform API — a Scala-based REST API for managing semantic data and digital -assets in the humanities. The project uses ZIO for functional programming, Pekko HTTP (Apache Pekko) for the API, and +assets in the humanities. The project uses ZIO for functional programming, zio-http and tapir for the API, and integrates with Apache Jena Fuseki triplestore and the Sipi media server. ## Build System & Commands @@ -74,7 +74,7 @@ Essential commands: - Technology - Language: Scala 3.3.x - FP: ZIO 2.x - - HTTP: Pekko HTTP + Tapir + - HTTP: ZIO HTTP + Tapir - Store: Apache Jena Fuseki (RDF) - Media: Sipi - JSON: ZIO JSON diff --git a/CLAUDE.md b/CLAUDE.md index a9fbcf98952..b1af088e7b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview DSP-API is the Digital Humanities Service Platform API - a Scala-based REST API for managing semantic data and digital assets in the humanities. -The project uses ZIO for functional programming, Pekko HTTP (Apache Pekko) for the API, and integrates with Apache Jena Fuseki triplestore and Sipi media server. +The project uses ZIO for functional programming, zio-http as HTTP server and tapir for defining endpoints of the API, and integrates with Apache Jena Fuseki triplestore and Sipi media server. ## Build System & Commands @@ -164,4 +164,4 @@ Each slice typically contains: ### Debugging - Use `make stack-logs` to view all service logs - Check `make stack-health` for API health status -- Use `make stack-status` to see container status \ No newline at end of file +- Use `make stack-status` to see container status diff --git a/build.sbt b/build.sbt index aa1eb17100d..fa2e9c0556f 100644 --- a/build.sbt +++ b/build.sbt @@ -310,7 +310,6 @@ lazy val it: Project = Project(id = "test-it", base = file("modules/test-it")) Test / fork := true, Test / testForkedParallel := false, Test / parallelExecution := false, - Test / javaOptions += "-Dkey=" + sys.props.getOrElse("key", "pekko"), Test / testOptions += Tests.Argument("-oDF"), // full stack traces and durations Test / baseDirectory := (ThisBuild / baseDirectory).value, libraryDependencies ++= Dependencies.webapiDependencies ++ Dependencies.webapiTestDependencies ++ Dependencies.integrationTestDependencies, @@ -342,7 +341,6 @@ lazy val e2e: Project = Project(id = "test-e2e", base = file("modules/test-e2e") Test / fork := true, Test / testForkedParallel := false, Test / parallelExecution := false, - Test / javaOptions += "-Dkey=" + sys.props.getOrElse("key", "pekko"), Test / testOptions += Tests.Argument("-oDF"), Test / baseDirectory := (ThisBuild / baseDirectory).value, libraryDependencies ++= Dependencies.webapiDependencies ++ Dependencies.webapiTestDependencies ++ Dependencies.integrationTestDependencies, diff --git a/docs/03-endpoints/api-util/version.md b/docs/03-endpoints/api-util/version.md index ffe6bc86652..009795453d4 100644 --- a/docs/03-endpoints/api-util/version.md +++ b/docs/03-endpoints/api-util/version.md @@ -19,7 +19,6 @@ Server: webapi/v30.9.0 "buildCommit": "bbb0e65c7", "buildTime": "2024-03-11T17:40:17.322491Z", "fuseki": "2.1.5", - "pekkoHttp": "1.0.1", "scala": "2.13.13", "sipi": "3.9.0", "webapi": "v30.9.0" diff --git a/docs/04-publishing-deployment/configuration.md b/docs/04-publishing-deployment/configuration.md index b5b10aa1262..4eeed0bcec4 100644 --- a/docs/04-publishing-deployment/configuration.md +++ b/docs/04-publishing-deployment/configuration.md @@ -1,14 +1,12 @@ # Configuration -All configuration for Knora is done in `application.conf`. Besides the Knora application -specific configuration, there we can also find configuration for the underlying Pekko library. +All configuration for Knora is done in `application.conf`. For optimal performance it is important to tune the configuration to the hardware used, mainly to the number of CPUs and cores per CPU. The relevant sections for tuning are: -- `pekko.actor.deployment` - `knora-actor-dispatcher` - `knora-blocking-dispatcher` @@ -18,9 +16,6 @@ A number of core settings is additionally configurable through system environmen | key in application.conf | environment variable | default value | |----------------------------------------|-------------------------------------------------|-------------------------| -| pekko.log-config-on-start | KNORA_AKKA_LOG_CONFIG_ON_START | off | -| pekko.loglevel | KNORA_AKKA_LOGLEVEL | INFO | -| pekko.stdout-loglevel | KNORA_AKKA_STDOUT_LOGLEVEL | INFO | | app.print-extended-config | KNORA_WEBAPI_PRINT_EXTENDED_CONFIG | false | | app.bcrypt-password-strength | KNORA_WEBAPI_BCRYPT_PASSWORD_STRENGTH | 12 | | app.jwt.secret | KNORA_WEBAPI_JWT_SECRET_KEY | super-secret-key | diff --git a/docs/05-internals/design/api-v2/how-to-add-a-route.md b/docs/05-internals/design/api-v2/how-to-add-a-route.md deleted file mode 100644 index 3c7d4396f5a..00000000000 --- a/docs/05-internals/design/api-v2/how-to-add-a-route.md +++ /dev/null @@ -1,47 +0,0 @@ -# How to Add an API v2 Route - -## Write SPARQL templates - -Add any SPARQL templates you need to `src/main/twirl/queries/sparql/v2`, -using the [Twirl](https://github.com/playframework/twirl) template -engine. - -## Write Responder Request and Response Messages - -Add a file to the `org.knora.webapi.messages.v2.responder` -package, containing case classes for your responder's request and -response messages. Add a trait that the responder's request messages -extend. Each request message type should contain a `UserADM`. - -Request and response messages should be designed following the patterns described -in [JSON-LD Parsing and Formatting](json-ld.md). Each responder's -request messages should extend a responder-specific trait, so that -`ResponderManager` will know which responder to route those messages to. - -## Write a Responder - -Write a Pekko actor class that extends `org.knora.webapi.responders.Responder`, -and add it to the `org.knora.webapi.responders.v2` package. - -Give your responder a `receive(msg: YourCustomType)` method that handles each of your -request message types by generating a `Future` containing a response message. - -Add the path of your responder to the `org.knora.webapi.responders` package object, -and add code to `ResponderManager` to instantiate the new responder. Then add a `case` to -the `receive` method in `ResponderManager`, to match messages that extend your request -message trait, and pass them to that responder's receive method. -The responder's resulting `Future` must be passed to the `ActorUtil.future2Message`. -See [Error Handling](../principles/design-overview.md#error-handling) for details. - -## Write a Route - -Add a class to the `org.knora.webapi.routing.v2` package for your -route, using the Pekko HTTP [Routing DSL](https://pekko.apache.org/docs/pekko-http/current/routing-dsl/index.html). -See the routes in that package for examples. Typically, each route -route will construct a responder request message and pass it to -`RouteUtilV2.runRdfRouteWithFuture` to handle the request. - -Finally, add your route's `knoraApiPath` function to the `apiRoutes` member -variable in `KnoraService`. Any exception thrown inside the route will -be handled by the `KnoraExceptionHandler`, so that the correct client -response (including the HTTP status code) will be returned. diff --git a/mkdocs.yml b/mkdocs.yml index 3c73a901575..6c0a283eb66 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,7 +73,6 @@ nav: - Ontology Schemas: 05-internals/design/api-v2/ontology-schemas.md - Smart IRIs: 05-internals/design/api-v2/smart-iris.md - Content Wrappers: 05-internals/design/api-v2/content-wrappers.md - - How to Add an API v2 Route: 05-internals/design/api-v2/how-to-add-a-route.md - JSON-LD Parsing and Formatting: 05-internals/design/api-v2/json-ld.md - Ontology Management: 05-internals/design/api-v2/ontology-management.md - DSP-API and Sipi: 05-internals/design/api-v2/sipi.md diff --git a/modules/testkit/src/main/scala/RdfMediaTypes.scala b/modules/testkit/src/main/scala/RdfMediaTypes.scala deleted file mode 100644 index 96aa6a038d7..00000000000 --- a/modules/testkit/src/main/scala/RdfMediaTypes.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi - -import org.apache.pekko.http.scaladsl.model.HttpCharsets -import org.apache.pekko.http.scaladsl.model.MediaType - -object RdfMediaTypes { - val `application/ld+json`: MediaType.WithFixedCharset = MediaType.customWithFixedCharset( - mainType = "application", - subType = "ld+json", - charset = HttpCharsets.`UTF-8`, - fileExtensions = List("jsonld"), - ) - - val `text/turtle`: MediaType.WithFixedCharset = MediaType.customWithFixedCharset( - mainType = "text", - subType = "turtle", - charset = HttpCharsets.`UTF-8`, - fileExtensions = List("ttl"), - ) - - val `application/rdf+xml`: MediaType.WithOpenCharset = MediaType.customWithOpenCharset( - mainType = "application", - subType = "rdf+xml", - fileExtensions = List("rdf"), - ) - - val `application/sparql-query`: MediaType.WithFixedCharset = MediaType.customWithFixedCharset( - mainType = "application", - subType = "sparql-query", - charset = HttpCharsets.`UTF-8`, - fileExtensions = List("rq"), - ) -} diff --git a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala index cc2ed54a0f6..5622f6f6ef8 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala @@ -5,7 +5,6 @@ package org.knora.webapi.core -import org.apache.pekko.actor.ActorSystem import zio.* import org.knora.webapi.config.* diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c0284685113..3983dd49c9a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -16,9 +16,6 @@ object Dependencies { val ScalaVersion = "3.3.6" - val PekkoActorVersion = "1.2.0" - val PekkoHttpVersion = "1.2.0" - val MonocleVersion = "3.3.0" val Rdf4jVersion = "5.1.5" @@ -90,12 +87,6 @@ object Dependencies { val zioTest = "dev.zio" %% "zio-test" % ZioVersion val zioTestSbt = "dev.zio" %% "zio-test-sbt" % ZioVersion - // pekko - val pekkoHttp = "org.apache.pekko" %% "pekko-http" % PekkoHttpVersion - val pekkoHttpCors = "org.apache.pekko" %% "pekko-http-cors" % PekkoHttpVersion - val pekkoSlf4j = "org.apache.pekko" %% "pekko-slf4j" % PekkoActorVersion - val pekkoStream = "org.apache.pekko" %% "pekko-stream" % PekkoActorVersion - // rdf and graph libraries val jenaCore = "org.apache.jena" % "jena-core" % JenaVersion val jenaText = "org.apache.jena" % "jena-text" % JenaVersion @@ -149,7 +140,6 @@ object Dependencies { val tapirVersion = "1.11.44" val tapir = Seq( - "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion, @@ -179,10 +169,6 @@ object Dependencies { val webapiTestDependencies = Seq(zioTest, zioTestSbt, wiremock).map(_ % Test) val webapiDependencies = monocle ++ refined ++ Seq( - pekkoHttp, - pekkoHttpCors, - pekkoSlf4j, - pekkoStream, bouncyCastle, commonsLang3, commonsValidator, diff --git a/webapi/src/main/resources/application.conf b/webapi/src/main/resources/application.conf index db166711619..b69ccba2a65 100644 --- a/webapi/src/main/resources/application.conf +++ b/webapi/src/main/resources/application.conf @@ -1,247 +1,11 @@ -pekko { - log-config-on-start = off - log-config-on-start = ${?KNORA_AKKA_LOG_CONFIG_ON_START} - loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"] - loglevel = "ERROR" - loglevel = ${?KNORA_AKKA_LOGLEVEL} - stdout-loglevel = "ERROR" - stdout-loglevel = ${?KNORA_AKKA_STDOUT_LOGLEVEL} - logging-filter = "org.apache.pekko.event.slf4j.Slf4jLoggingFilter" - log-dead-letters = off - log-dead-letters-during-shutdown = off - logger-startup-timeout = 30s - - // pekko-http configuration - http { - server { - # The requested maximum length of the queue of incoming connections. - # If the server is busy and the backlog is full the OS will start dropping - # SYN-packets and connection attempts may fail. Note, that the backlog - # size is usually only a maximum size hint for the OS and the OS can - # restrict the number further based on global limits. - backlog = 1024 - - # The time after which an idle connection will be automatically closed. - # Set to `infinite` to completely disable idle connection timeouts. - # - # Must be larger then request-timeout - idle-timeout = 120 minutes - - # Defines the default time period within which the application has to - # produce an HttpResponse for any given HttpRequest it received. - # The timeout begins to run when the *end* of the request has been - # received, so even potentially long uploads can have a short timeout. - # Set to `infinite` to completely disable request timeout checking. - # - # If this setting is not `infinite` the HTTP server layer attaches a - # `Timeout-Access` header to the request, which enables programmatic - # customization of the timeout period and timeout response for each - # request individually. - # - # Must be smaller then idle-timeout - request-timeout = 120 minutes - - # The time period within which the TCP binding process must be completed. - bind-timeout = 5 seconds - - # The maximum number of concurrently accepted connections when using the - # `Http().bindAndHandle` methods. - # - # This setting doesn't apply to the `Http().bind` method which will still - # deliver an unlimited backpressured stream of incoming connections. - # - # Note, that this setting limits the number of the connections on a best-effort basis. - # It does *not* strictly guarantee that the number of established TCP connections will never - # exceed the limit (but it will be approximately correct) because connection termination happens - # asynchronously. It also does *not* guarantee that the number of concurrently active handler - # flow materializations will never exceed the limit for the reason that it is impossible to reliably - # detect when a materialization has ended. - max-connections = 1024 - - # The maximum number of requests that are accepted (and dispatched to - # the application) on one single connection before the first request - # has to be completed. - # Incoming requests that would cause the pipelining limit to be exceeded - # are not read from the connections socket so as to build up "back-pressure" - # to the client via TCP flow control. - # A setting of 1 disables HTTP pipelining, since only one request per - # connection can be "open" (i.e. being processed by the application) at any - # time. Set to higher values to enable HTTP pipelining. - # This value must be > 0 and <= 1024. - pipelining-limit = 1 - - parsing { - max-content-length = 512M - - # Defines the maximum length of the URL - # Set to 10k because Sparql queries for the extended search v2 are submitted as segments of the URL - max-uri-length = 10k - } - } - - client { - # The time period within which the TCP connecting process must be completed. - connecting-timeout = 479999 ms // 480 s - - # The time after which an idle connection will be automatically closed. - # Set to `infinite` to completely disable idle timeouts. - idle-timeout = 479999 ms // 480 s - - parsing { - max-chunk-size = 2m - max-response-reason-length = 1024 - } - } - - host-connection-pool { - # The maximum number of parallel connections that a connection pool to a - # single host endpoint is allowed to establish. Must be greater than zero. - max-connections = 15 - - # The minimum number of parallel connections that a pool should keep alive ("hot"). - # If the number of connections is falling below the given threshold, new ones are being spawned. - # You can use this setting to build a hot pool of "always on" connections. - # Default is 0, meaning there might be no active connection at given moment. - # Keep in mind that `min-connections` should be smaller than `max-connections` or equal - min-connections = 0 - - # The maximum number of times failed requests are attempted again, - # (if the request can be safely retried) before giving up and returning an error. - # Set to zero to completely disable request retries. - max-retries = 3 - - # The maximum number of open requests accepted into the pool across all - # materializations of any of its client flows. - # Protects against (accidentally) overloading a single pool with too many client flow materializations. - # Note that with N concurrent materializations the max number of open request in the pool - # will never exceed N * max-connections * pipelining-limit. - # Must be a power of 2 and > 0! - max-open-requests = 64 - - # The maximum number of requests that are dispatched to the target host in - # batch-mode across a single connection (HTTP pipelining). - # A setting of 1 disables HTTP pipelining, since only one request per - # connection can be "in flight" at any time. - # Set to higher values to enable HTTP pipelining. - # This value must be > 0. - # (Note that, independently of this setting, pipelining will never be done - # on a connection that still has a non-idempotent request in flight. - # - # Before increasing this value, make sure you understand the effects of head-of-line blocking. - # Using a connection pool, a request may be issued on a connection where a previous - # long-running request hasn't finished yet. The response to the pipelined requests may then be stuck - # behind the response of the long-running previous requests on the server. This may introduce an - # unwanted "coupling" of run time between otherwise unrelated requests. - # - # See http://tools.ietf.org/html/rfc7230#section-6.3.2 for more info.) - pipelining-limit = 1 - - # The time after which an idle connection pool (without pending requests) - # will automatically terminate itself. Set to `infinite` to completely disable idle timeouts. - idle-timeout = 30 s - - # Modify to tweak client settings for host connection pools only. - # - # IMPORTANT: - # Please note that this section mirrors `pekko.http.client` however is used only for pool-based APIs, - # such as `Http().superPool` or `Http().singleRequest`. - client = { - # no overrides, see `pekko.http.client` for used values - } - } - } -} - - -// all responder actors should run on this dispatcher -knora-actor-dispatcher { - type = Dispatcher - - executor = "fork-join-executor" - # Configuration for the fork join pool - fork-join-executor { - parallelism-min = 8 - parallelism-factor = 2.0 - parallelism-max = 32 - } - - throughput = 5 -} - -// any futures or blocking code should run on this dispatcher -knora-blocking-dispatcher { - type = Dispatcher - - executor = "thread-pool-executor" - - thread-pool-executor { - core-pool-size-min = 8 - core-pool-size-factor = 2.0 - core-pool-size-max = 32 - } - - throughput = 1 -} - -pekko-http-cors { - - # If enabled, allow generic requests (that are outside the scope of the specification) - # to pass through the directive. Else, strict CORS filtering is applied and any - # invalid request will be rejected. - allow-generic-http-requests = yes - - # Indicates whether the resource supports user credentials. If enabled, the header - # `Access-Control-Allow-Credentials` is set in the response, indicating that the - # actual request can include user credentials. Examples of user credentials are: - # cookies, HTTP authentication or client-side certificates. - allow-credentials = yes - - # List of origins that the CORS filter must allow. Can also be set to `*` to allow - # access to the resource from any origin. Controls the content of the - # `Access-Control-Allow-Origin` response header: if parameter is `*` and credentials - # are not allowed, a `*` is set in `Access-Control-Allow-Origin`. Otherwise, the - # origins given in the `Origin` request header are echoed. - # - # Hostname starting with `*.` will match any sub-domain. - # The scheme and the port are always strictly matched. - # - # The actual or preflight request is rejected if any of the origins from the request - # is not allowed. - allowed-origins = "*" - - # List of request headers that can be used when making an actual request. Controls - # the content of the `Access-Control-Allow-Headers` header in a preflight response: - # if parameter is `*`, the headers from `Access-Control-Request-Headers` are echoed. - # Otherwise the parameter list is returned as part of the header. - allowed-headers = "*" - - # List of methods that can be used when making an actual request. The list is - # returned as part of the `Access-Control-Allow-Methods` preflight response header. - # - # The preflight request will be rejected if the `Access-Control-Request-Method` - # header's method is not part of the list. - allowed-methods = ["GET", "PUT", "POST", "DELETE", "PATCH", "HEAD", "OPTIONS"] - - # List of headers (other than simple response headers) that browsers are allowed to access. - # If not empty, this list is returned as part of the `Access-Control-Expose-Headers` - # header in the actual response. - exposed-headers = ["Server"] - - # When set, the amount of seconds the browser is allowed to cache the results of a preflight request. - # This value is returned as part of the `Access-Control-Max-Age` preflight response header. - # If `null`, the header is not added to the preflight response. - max-age = 1800 seconds -} - app { print-extended-config = false // If true, an extended list of configuration parameters will be printed out at startup. print-extended-config = ${?KNORA_WEBAPI_PRINT_EXTENDED_CONFIG} - // default ask timeout. can be same or lower then pekko.http.server.request-timeout. + // default ask timeout. default-timeout = 120 minutes // a timeout here should never happen - // If true, log all messages sent from and received by routes. Since messages are logged at DEBUG level, you will - // need to set loglevel = "DEBUG" in the pekko section of this file, and in logback.xml. + // If true, log all messages sent from and received. dump-messages = false show-internal-errors = true // If true, clients will see error messages from internal errors. Useful for debugging. If false, those error messages will appear only in the log. @@ -410,13 +174,13 @@ app { host = "localhost" host = ${?KNORA_WEBAPI_TRIPLESTORE_HOST} - // timeout for triplestore queries. can be same or lower then pekko.http.server.request-timeout. + // timeout for triplestore queries. query-timeout = 20 seconds - // timeout for triplestore queries for maintenance actions. can be same or lower then pekko.http.server.request-timeout. + // timeout for triplestore queries for maintenance actions. maintenance-timeout = 120 seconds - // timeout for Gravsearch queries. can be same or lower then pekko.http.server.request-timeout. + // timeout for Gravsearch queries. gravsearch-timeout = 120 seconds fuseki { diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index 1cee558ceba..b405da16ddc 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -5,52 +5,28 @@ package org.knora.webapi.core -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.scaladsl.Http import zio.* - +import zio.http.* import org.knora.webapi.config.AppConfig import org.knora.webapi.routing.ApiRoutes - -/** - * The Akka based HTTP server - */ -trait HttpServer { - val serverBinding: Http.ServerBinding -} +import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin +import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} +import sttp.tapir.server.metrics.zio.ZioMetrics +import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions} +import sttp.tapir.ztapir.RIOMonadError object HttpServer { - val layer: ZLayer[ActorSystem & AppConfig & ApiRoutes, Nothing, HttpServer] = - ZLayer.scoped { - for { - as <- ZIO.service[ActorSystem] - config <- ZIO.service[AppConfig] - apiRoutes <- ZIO.service[ApiRoutes] - binding <- { - implicit val system: ActorSystem = as - ZIO.acquireRelease { - ZIO - .fromFuture(_ => - Http().newServerAt(config.knoraApi.internalHost, config.knoraApi.internalPort).bind(apiRoutes.routes), - ) - .zipLeft(ZIO.logInfo(">>> Acquire HTTP Server <<<")) - .orDie - } { serverBinding => - ZIO - .fromFuture(_ => - serverBinding.terminate( - new scala.concurrent.duration.FiniteDuration(1, scala.concurrent.duration.MILLISECONDS), - ), - ) - .zipLeft(ZIO.logInfo(">>> Release HTTP Server <<<")) - .orDie - } - } - } yield HttpServerImpl(binding) - } + private def options: ZioHttpServerOptions[Any] = ZioHttpServerOptions.default + + val layer: ZLayer[AppConfig & ApiRoutes, Nothing, Unit] = ZLayer.scoped(createServer.orDie) - private final case class HttpServerImpl(binding: Http.ServerBinding) extends HttpServer { self => - val serverBinding: Http.ServerBinding = self.binding - } + private def createServer: ZIO[ApiRoutes & AppConfig, Throwable, Unit] = + for { + config <- ZIO.service[AppConfig] + endpoints <- ZIO.service[ApiRoutes] + httpApp = ZioHttpInterpreter(options).toHttp(endpoints.endpoints) + _ <- Server.install(httpApp).provide(Server.defaultWithPort(config.knoraApi.externalPort)) + _ <- Console.printLine(s"Go to http://localhost:$config.knoraApi.externalPort/docs to open SwaggerUI") + } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 21b66e313b2..123bcbd2e77 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -5,7 +5,6 @@ package org.knora.webapi.core -import org.apache.pekko.actor.ActorSystem import zio.* import org.knora.webapi.config.AppConfig @@ -211,7 +210,6 @@ object LayersLive { self => OntologyApiModule.layer, OntologyResponderV2.layer, OpenTelemetry.layer, - PekkoActorSystem.layer, PermissionUtilADMLive.layer, PermissionsResponder.layer, ProjectExportServiceLive.layer, @@ -233,7 +231,6 @@ object LayersLive { self => StandoffResponderV2.layer, StandoffTagUtilV2Live.layer, State.layer, - TapirToPekkoInterpreter.layer, ValuesResponderV2.layer, // ZLayer.Debug.mermaid, ) diff --git a/webapi/src/main/scala/org/knora/webapi/core/PekkoActorSystem.scala b/webapi/src/main/scala/org/knora/webapi/core/PekkoActorSystem.scala deleted file mode 100644 index 70c30c2459e..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/core/PekkoActorSystem.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.core - -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.actor.Terminated -import zio.* - -import scala.concurrent.ExecutionContext - -object PekkoActorSystem { - - private def acquire(executionContext: ExecutionContext): UIO[ActorSystem] = - ZIO - .attempt(ActorSystem("webapi", None, None, Some(executionContext))) - .retry(Schedule.exponential(1.second) && Schedule.recurs(3)) - .tapError(error => ZIO.logError(s"Failed to initialize Actor System: ${error.getMessage}")) - .orDie <* - ZIO.logInfo(">>> Acquire Actor System <<<") - - private def release(system: ActorSystem): UIO[Terminated] = - ZIO.fromFuture(_ => system.terminate()).orDie <* ZIO.logInfo(">>> Release Actor System <<<") - - val layer: ULayer[ActorSystem] = ZLayer.scoped( - for { - context <- ZIO.executor.map(_.asExecutionContext) - system <- ZIO.acquireRelease(acquire(context))(release) - } yield system, - ) -} diff --git a/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala b/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala index 55f42e6b6af..39a3c5dab5d 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/version/ServerVersion.scala @@ -5,23 +5,6 @@ package org.knora.webapi.http.version -import org.apache.pekko - -import pekko.http.scaladsl.model.headers.Server -import pekko.http.scaladsl.server.Directives.respondWithHeader -import pekko.http.scaladsl.server.Route - -/** - * This object provides methods that can be used to add the [[Server]] header - * to an [[pekko.http.scaladsl.model.HttpResponse]]. - */ object ServerVersion { - - private val ApiNameAndVersion = s"${BuildInfo.name}/${BuildInfo.version}" - - def serverVersionHeader: Server = Server(products = ApiNameAndVersion) - - def addServerHeader(route: Route): Route = respondWithHeader(serverVersionHeader) { - route - } + val ApiNameAndVersion = s"${BuildInfo.name}/${BuildInfo.version}" } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala index b0b2ea8cdaa..d5e59f73aad 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala @@ -143,7 +143,7 @@ final case class ListsResponder( * @param nodeIri the Iri if the required node. * @return a [[ListItemGetResponseADM]]. */ - def listGetRequestADM(nodeIri: IRI): Task[ListItemGetResponseADM] = { + def listGetRequestADM(nodeIri: ListIri): Task[ListItemGetResponseADM] = { def getNodeADM(childNode: ListChildNodeADM): Task[ListNodeGetResponseADM] = for { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index 90e1f8e858f..6eb39a30bc9 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -5,17 +5,11 @@ package org.knora.webapi.routing -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.cors.scaladsl.CorsDirectives -import org.apache.pekko.http.cors.scaladsl.settings.CorsSettings -import org.apache.pekko.http.scaladsl.model.HttpMethods.* -import org.apache.pekko.http.scaladsl.server.Directives.* -import org.apache.pekko.http.scaladsl.server.Route import zio.* - import org.knora.webapi.http.version.ServerVersion import org.knora.webapi.routing import org.knora.webapi.slice.admin.api.AdminApiRoutes +import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementRoutes import org.knora.webapi.slice.lists.api.ListsApiV2Routes import org.knora.webapi.slice.ontology.api.OntologiesApiRoutes @@ -24,16 +18,14 @@ import org.knora.webapi.slice.resources.api.ResourcesApiRoutes import org.knora.webapi.slice.search.api.SearchApiRoutes import org.knora.webapi.slice.security.api.AuthenticationApiRoutes import org.knora.webapi.slice.shacl.api.ShaclApiRoutes +import sttp.tapir.ztapir.ZServerEndpoint /** - * All routes composed together and CORS activated based on the - * the configuration in application.conf (pekko-http-cors). - * * ALL requests go through each of the routes in ORDER. * The FIRST matching route is used for handling a request. */ final case class ApiRoutes( - adminApiRoutes: AdminApiRoutes, + adminApiRoutes: AdminApiServerEndpoints, authenticationApiRoutes: AuthenticationApiRoutes, listsApiV2Routes: ListsApiV2Routes, resourceInfoRoutes: ResourceInfoRoutes, @@ -42,24 +34,17 @@ final case class ApiRoutes( shaclApiRoutes: ShaclApiRoutes, managementRoutes: ManagementRoutes, ontologiesRoutes: OntologiesApiRoutes, - system: ActorSystem, ) { - val routes: Route = - ServerVersion.addServerHeader { - CorsDirectives.cors( - CorsSettings(system).withAllowedMethods(List(GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS)), - ) { - (adminApiRoutes.routes ++ - authenticationApiRoutes.routes ++ - listsApiV2Routes.routes ++ - managementRoutes.routes ++ - ontologiesRoutes.routes ++ - resourceInfoRoutes.routes ++ - resourcesApiRoutes.routes ++ - searchApiRoutes.routes ++ - shaclApiRoutes.routes).reduce(_ ~ _) - } - } + val endpoints: List[ZServerEndpoint[Any, Any]] = + (adminApiRoutes.endpoints ++ + authenticationApiRoutes.routes ++ + listsApiV2Routes.routes ++ + managementRoutes.routes ++ + ontologiesRoutes.routes ++ + resourceInfoRoutes.routes ++ + resourcesApiRoutes.routes ++ + searchApiRoutes.routes ++ + shaclApiRoutes.routes) } object ApiRoutes { val layer = ZLayer.derive[ApiRoutes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala index 2afff367492..6179ca11bcd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala @@ -51,7 +51,6 @@ object AdminApiModule { self => CacheManager & Features & GroupService & - HandlerMapper & KnoraGroupService & KnoraProjectService & KnoraResponseRenderer & @@ -67,7 +66,6 @@ object AdminApiModule { self => ProjectExportService & ProjectImportService & ProjectService & - TapirToPekkoInterpreter & TriplestoreService & UserService // format: on @@ -86,7 +84,7 @@ object AdminApiModule { self => val layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( AdminApiEndpoints.layer, - AdminApiRoutes.layer, + AdminApiServerEndpoints.layer, AdminListRestService.layer, AdminListsEndpoints.layer, AdminListsEndpointsHandlers.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala similarity index 76% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index 1d813404632..2da58341654 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -6,11 +6,11 @@ package org.knora.webapi.slice.admin.api import org.apache.pekko.http.scaladsl.server.Route -import zio.ZLayer +import zio.* -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import sttp.tapir.ztapir.ZServerEndpoint -final case class AdminApiRoutes( +final case class AdminApiServerEndpoints( private val adminLists: AdminListsEndpointsHandlers, private val filesEndpoints: FilesEndpointsHandler, private val groups: GroupsEndpointsHandler, @@ -19,11 +19,10 @@ final case class AdminApiRoutes( private val project: ProjectsEndpointsHandler, private val projectLegalInfo: ProjectsLegalInfoEndpointsHandler, private val storeEndpoints: StoreEndpointsHandler, - private val tapirToPekko: TapirToPekkoInterpreter, private val users: UsersEndpointsHandler, ) { - private val handlers = + private val endpoints: List[ZServerEndpoint[Any, Any]] = filesEndpoints.allHandlers ++ groups.allHandlers ++ adminLists.allHandlers ++ @@ -33,10 +32,8 @@ final case class AdminApiRoutes( project.allHanders ++ storeEndpoints.allHandlers ++ users.allHanders - - val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) } -object AdminApiRoutes { - val layer = ZLayer.derive[AdminApiRoutes] +object AdminApiServerEndpoints { + val layer = ZLayer.derive[AdminApiServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala index 7bf74c6b189..42619ef491f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala @@ -5,9 +5,11 @@ package org.knora.webapi.slice.admin.api -import zio.ZIO +import zio.* import zio.ZLayer +import sttp.tapir.ztapir.* + import dsp.errors.BadRequestException import org.knora.webapi.messages.admin.responder.listsmessages.CanDeleteListResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM @@ -23,22 +25,13 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler final case class AdminListsEndpointsHandlers( private val adminListsEndpoints: AdminListsEndpoints, private val adminListsRestService: AdminListRestService, private val listsResponder: ListsResponder, - private val mapper: HandlerMapper, ) { - private val getListsQueryByProjectIriHandler = PublicEndpointHandler( - adminListsEndpoints.getListsQueryByProjectIriOption, - (iriShortcode: Option[Either[ProjectIri, Shortcode]]) => listsResponder.getLists(iriShortcode), - ) - private val getListsByIriHandler = PublicEndpointHandler( adminListsEndpoints.getListsByIri, (iri: ListIri) => listsResponder.listGetRequestADM(iri.value), @@ -135,12 +128,12 @@ final case class AdminListsEndpointsHandlers( private val public = List( getListsByIriHandler, - getListsQueryByProjectIriHandler, + adminListsEndpoints.getListsQueryByProjectIriOption.zServerLogic(listsResponder.getLists), getListsByIriInfoHandler, getListsInfosByIriHandler, getListsNodesByIriHandler, getListsCanDeleteByIriHandler, - ).map(mapper.mapPublicEndpointHandler(_)) + ) private val secured = List( postListsCreateRootNodeHandler, @@ -152,9 +145,9 @@ final case class AdminListsEndpointsHandlers( putListsByIriHandler, deleteListsByIriHandler, deleteListsCommentHandler, - ).map(mapper.mapSecuredEndpointHandler(_)) + ) - val allHandlers = public ++ secured + val allHandlers: List[ZServerEndpoint[Any, Any]] = public ++ secured } object AdminListsEndpointsHandlers { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala index 3daed742389..908355655ff 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala @@ -5,29 +5,27 @@ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* + +import sttp.tapir.ztapir.* import org.knora.webapi.responders.admin.AssetPermissionsResponder import org.knora.webapi.slice.admin.api.model.PermissionCodeAndProjectRestrictedViewSettings import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.common.domain.SparqlEncodedString final case class FilesEndpointsHandler( filesEndpoints: FilesEndpoints, assetPermissionsResponder: AssetPermissionsResponder, - mapper: HandlerMapper, ) { - private val getAdminFilesShortcodeFileIri = - SecuredEndpointHandler( - filesEndpoints.getAdminFilesShortcodeFileIri, - assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings, + val allHandlers: List[ZServerEndpoint[Any, Any]] = + List( + filesEndpoints.getAdminFilesShortcodeFileIri.serverLogic( + assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings, + ), ) - - val allHandlers = List(getAdminFilesShortcodeFileIri).map(mapper.mapSecuredEndpointHandler) } object FilesEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala index 6dc4aa649db..481bedc8d0e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala @@ -5,35 +5,30 @@ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.messages.admin.responder.groupsmessages.GroupGetResponseADM import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest import org.knora.webapi.slice.admin.api.service.GroupRestService import org.knora.webapi.slice.admin.domain.model.GroupIri -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler case class GroupsEndpointsHandler( endpoints: GroupsEndpoints, restService: GroupRestService, - mapper: HandlerMapper, ) { - val allHandlers = - List( - PublicEndpointHandler(endpoints.getGroups, (_: Unit) => restService.getGroups), - PublicEndpointHandler(endpoints.getGroupByIri, restService.getGroupByIri), - ).map(mapper.mapPublicEndpointHandler) ++ - List( - SecuredEndpointHandler(endpoints.getGroupMembers, restService.getGroupMembers), - SecuredEndpointHandler(endpoints.postGroup, restService.postGroup), - SecuredEndpointHandler(endpoints.putGroup, restService.putGroup), - SecuredEndpointHandler(endpoints.putGroupStatus, restService.putGroupStatus), - SecuredEndpointHandler(endpoints.deleteGroup, restService.deleteGroup), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers: ZServerEndpoint[Any, Any] = + Seq( + endpoints.getGroups.zServerLogic(_ => restService.getGroups), + endpoints.getGroupByIri.zServerLogic(restService.getGroupByIri), + endpoints.getGroupMembers.serverLogic(restService.getGroupMembers), + endpoints.postGroup.serverLogic(restService.postGroup), + endpoints.putGroup.serverLogic(restService.putGroup), + endpoints.putGroupStatus.serverLogic(restService.putGroupStatus), + endpoints.deleteGroup.serverLogic(restService.deleteGroup), + ) } object GroupsEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala index bf0a7a59588..670151bc731 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala @@ -5,24 +5,20 @@ package org.knora.webapi.slice.admin.api -import zio.ZIO -import zio.ZLayer +import zio.* import zio.json.ast.Json +import sttp.tapir.ztapir.* + import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler final case class MaintenanceEndpointsHandlers( endpoints: MaintenanceEndpoints, restService: MaintenanceRestService, - mapper: HandlerMapper, ) { - val allHandlers = List( - SecuredEndpointHandler(endpoints.postMaintenance, restService.executeMaintenanceAction), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers: ZServerEndpoint[Any, Any] = Seq(endpoints.postMaintenance.serverLogic(restService.executeMaintenanceAction)) } object MaintenanceEndpointsHandlers { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala index b3cbae4e8a3..1fc567fbb62 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala @@ -14,7 +14,6 @@ import org.knora.webapi.slice.common.api.SecuredEndpointHandler final class ProjectsLegalInfoEndpointsHandler( endpoints: ProjectsLegalInfoEndpoints, restService: ProjectsLegalInfoRestService, - mapper: HandlerMapper, ) { val allHandlers = List( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala index 04b98ed86cb..0694d867e89 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala @@ -6,9 +6,9 @@ package org.knora.webapi.slice.admin.api.service import zio.* - import dsp.errors.BadRequestException import dsp.errors.NotFoundException +import dsp.errors.RequestRejectedException import org.knora.webapi.messages.admin.responder.groupsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.GroupMembersGetResponseADM import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest @@ -34,7 +34,7 @@ final case class GroupRestService( ) { def getGroups: Task[GroupsGetResponseADM] = for { - internal <- groupService.findAllRegularGroups + internal <- groupService.findAllRegularGroups.orDie external <- format.toExternal(GroupsGetResponseADM(internal)) } yield external diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala index f1aefd0d616..354db9b12cd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -7,17 +7,11 @@ package org.knora.webapi.slice.common.api import sttp.model.StatusCode import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir.Endpoint -import sttp.tapir.EndpointOutput -import sttp.tapir.auth -import sttp.tapir.cookie -import sttp.tapir.endpoint +import sttp.tapir.ztapir.* +import sttp.tapir.{EndpointOutput, PublicEndpoint, Validator} import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.model.UsernamePassword -import sttp.tapir.oneOf -import sttp.tapir.oneOfVariant -import sttp.tapir.statusCode import zio.ZIO import zio.ZLayer @@ -25,15 +19,14 @@ import scala.concurrent.Future import dsp.errors.* import org.knora.webapi.messages.util.KnoraSystemInstances.Users.AnonymousUser -import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.security.Authenticator -final case class BaseEndpoints(authenticator: Authenticator)(implicit val r: zio.Runtime[Any]) { +final case class BaseEndpoints(authenticator: Authenticator) { - private val errorOutputs: EndpointOutput.OneOf[RequestRejectedException, RequestRejectedException] = - oneOf[RequestRejectedException]( + private val errorOutputs = + oneOf[Throwable]( // default oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException])), @@ -53,56 +46,45 @@ final case class BaseEndpoints(authenticator: Authenticator)(implicit val r: zio val publicEndpoint = endpoint.errorOut(errorOutputs) - private val endpointWithBearerCookieBasicAuthOptional - : Endpoint[(Option[String], Option[String], Option[UsernamePassword]), Unit, RequestRejectedException, Unit, Any] = + private val endpointWithBearerCookieBasicAuthOptional = endpoint .errorOut(errorOutputs) .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) .securityIn(cookie[Option[String]](authenticator.calculateCookieName())) .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic("realm"))) - val securedEndpoint = endpointWithBearerCookieBasicAuthOptional.serverSecurityLogic { + val securedEndpoint = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) case (_, Some(cookie), _) => authenticateJwt(cookie) case (_, _, Some(basic)) => authenticateBasic(basic) - case _ => Future.successful(Left(BadCredentialsException("No credentials provided."))) + case _ => ZIO.fail(BadCredentialsException("No credentials provided.")) } - val withUserEndpoint = endpointWithBearerCookieBasicAuthOptional.serverSecurityLogic { + val withUserEndpoint = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) case (_, Some(cookie), _) => authenticateJwt(cookie) case (_, _, Some(basic)) => authenticateBasic(basic) - - case _ => Future.successful(Right(AnonymousUser)) + case _ => ZIO.succeed(AnonymousUser) } - private def authenticateJwt(jwtToken: String): Future[Either[RequestRejectedException, User]] = - UnsafeZioRun.runToFuture( - authenticator.authenticate(jwtToken).orElseFail(BadCredentialsException("Invalid credentials.")).either, - ) + private def authenticateJwt(jwtToken: String): IO[BadCredentialsException, User] = + authenticator.authenticate(jwtToken).orElseFail(BadCredentialsException("Invalid credentials.")) - private def authenticateBasic(basic: UsernamePassword): Future[Either[RequestRejectedException, User]] = - UnsafeZioRun.runToFuture( - (for { - email <- ZIO - .fromEither(Email.from(basic.username)) - .orElseFail(BadCredentialsException("Invalid credentials, email address expected.")) - password <- ZIO - .fromOption(basic.password) - .orElseFail(BadCredentialsException("Invalid credentials, missing password.")) - userAndJwt <- authenticator - .authenticate(email, password) - .orElseFail(BadCredentialsException("Invalid credentials.")) - (user, _) = userAndJwt - } yield user).either, - ) + private def authenticateBasic(basic: UsernamePassword): IO[BadCredentialsException, User] = + for { + email <- ZIO + .fromEither(Email.from(basic.username)) + .orElseFail(BadCredentialsException("Invalid credentials, email address expected.")) + password <- ZIO + .fromOption(basic.password) + .orElseFail(BadCredentialsException("Invalid credentials, missing password.")) + userAndJwt <- authenticator + .authenticate(email, password) + .orElseFail(BadCredentialsException("Invalid credentials.")) + (user, _) = userAndJwt + } yield user } object BaseEndpoints { - val layer = ZLayer.fromZIO( - for { - auth <- ZIO.service[Authenticator] - r <- ZIO.runtime[Any] - } yield BaseEndpoints(auth)(r), - ) + val layer = ZLayer.derive[BaseEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala deleted file mode 100644 index 34ad1bb4e1f..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.common.api - -import sttp.capabilities.pekko.PekkoStreams -import sttp.tapir.Endpoint -import sttp.tapir.model.UsernamePassword -import sttp.tapir.server.PartialServerEndpoint -import sttp.tapir.server.ServerEndpoint.Full -import zio.Task -import zio.ZIO -import zio.ZLayer - -import scala.concurrent.Future - -import dsp.errors.RequestRejectedException -import org.knora.webapi.routing.UnsafeZioRun -import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.InputType.SecurityIn - -object InputType { - type SecurityIn = (Option[String], Option[String], Option[UsernamePassword]) -} - -case class PublicEndpointHandler[INPUT, OUTPUT]( - endpoint: Endpoint[Unit, INPUT, RequestRejectedException, OUTPUT, PekkoStreams], - handler: INPUT => Task[OUTPUT], -) - -case class SecuredEndpointHandler[INPUT, OUTPUT]( - endpoint: PartialServerEndpoint[ - SecurityIn, - User, - INPUT, - RequestRejectedException, - OUTPUT, - Any, - Future, - ], - handler: User => INPUT => Task[OUTPUT], -) - -final case class HandlerMapper()(implicit val r: zio.Runtime[Any]) { - - def mapSecuredEndpointHandler[INPUT, OUTPUT]( - handlerAndEndpoint: SecuredEndpointHandler[INPUT, OUTPUT], - ): Full[SecurityIn, User, INPUT, RequestRejectedException, OUTPUT, Any, Future] = - handlerAndEndpoint.endpoint.serverLogic(user => in => runToFuture(handlerAndEndpoint.handler(user)(in))) - - def mapPublicEndpointHandler[INPUT, OUTPUT]( - handlerAndEndpoint: PublicEndpointHandler[INPUT, OUTPUT], - ): Full[Unit, Unit, INPUT, RequestRejectedException, OUTPUT, PekkoStreams, Future] = - handlerAndEndpoint.endpoint.serverLogic[Future](in => runToFuture(handlerAndEndpoint.handler(in))) - - def runToFuture[OUTPUT](zio: Task[OUTPUT]): Future[Either[RequestRejectedException, OUTPUT]] = - UnsafeZioRun.runToFuture(zio.refineOrDie { case e: RequestRejectedException => e }.either) -} - -object HandlerMapper { - val layer = ZLayer.fromZIO(ZIO.runtime[Any].map(HandlerMapper()(_))) -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala deleted file mode 100644 index 64ae7448a87..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.common.api - -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.scaladsl.server.Route -import sttp.capabilities.WebSockets -import sttp.capabilities.pekko.PekkoStreams -import sttp.tapir.generic.auto.* -import sttp.tapir.json.zio.jsonBody -import sttp.tapir.server.ServerEndpoint -import sttp.tapir.server.metrics.zio.ZioMetrics -import sttp.tapir.server.model.ValuedEndpointOutput -import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter -import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions -import zio.ZLayer -import zio.json.DeriveJsonCodec -import zio.json.JsonCodec - -import scala.concurrent.ExecutionContext -import scala.concurrent.Future - -final case class TapirToPekkoInterpreter()(system: ActorSystem) { - implicit val executionContext: ExecutionContext = system.dispatcher - private case class GenericErrorResponse(error: String) - private object GenericErrorResponse { - implicit val codec: JsonCodec[GenericErrorResponse] = DeriveJsonCodec.gen[GenericErrorResponse] - } - - private def customizedErrorResponse(m: String): ValuedEndpointOutput[?] = - ValuedEndpointOutput(jsonBody[GenericErrorResponse], GenericErrorResponse(m)) - - private val serverOptions = - PekkoHttpServerOptions.customiseInterceptors - .defaultHandlers(customizedErrorResponse) - .metricsInterceptor(ZioMetrics.default[Future]().metricsInterceptor()) - .notAcceptableInterceptor(None) - .options - - private val interpreter: PekkoHttpServerInterpreter = PekkoHttpServerInterpreter(serverOptions) - - def toRoute(endpoint: ServerEndpoint[PekkoStreams & WebSockets, Future]): Route = - interpreter.toRoute(endpoint) -} - -object TapirToPekkoInterpreter { - val layer = ZLayer.derive[TapirToPekkoInterpreter] -} diff --git a/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala b/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala deleted file mode 100644 index aff466ed241..00000000000 --- a/webapi/src/test/scala/org/knora/webapi/http/version/ServerVersionSpec.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.http.version - -import org.apache.pekko.http.scaladsl.model.headers.Server -import zio.test.Spec -import zio.test.ZIOSpecDefault -import zio.test.assertTrue - -object ServerVersionSpec extends ZIOSpecDefault { - - val spec: Spec[Any, Nothing] = suite("ServerVersionSpec")( - test("The server version header") { - val header: Server = ServerVersion.serverVersionHeader - assertTrue(header.toString.contains("webapi/")) - }, - ) -} From 2c0f8486be9406753ccb55bbd0326aaa5829ef27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 12:21:49 +0200 Subject: [PATCH 10/99] refactor(dsp-api): Align *RestService method signature and simplify *Handlers (pt.3) --- ...rvice.scala => AdminListRestService.scala} | 40 +++-- .../api/AdminListsEndpointsHandlers.scala | 139 +++--------------- 2 files changed, 49 insertions(+), 130 deletions(-) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{ListRestService.scala => AdminListRestService.scala} (63%) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala similarity index 63% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListRestService.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala index 4b717cae48f..d17677ff9a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala @@ -11,13 +11,19 @@ import zio.ZIO import zio.ZLayer import dsp.errors.BadRequestException +import org.knora.webapi.messages.admin.responder.listsmessages.CanDeleteListResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListItemDeleteResponseADM +import org.knora.webapi.messages.admin.responder.listsmessages.ListItemGetResponseADM +import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCommentsDeleteResponseADM +import org.knora.webapi.messages.admin.responder.listsmessages.ListsGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.NodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.NodePositionChangeResponseADM import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.slice.admin.api.Requests.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.admin.domain.service.KnoraProjectService @@ -28,7 +34,16 @@ final case class AdminListRestService( private val knoraProjectService: KnoraProjectService, private val listsResponder: ListsResponder, ) { - def listChange(iri: ListIri, request: ListChangeRequest, user: User): Task[NodeInfoGetResponseADM] = for { + + def getLists(projectIriOpt: Option[Either[ProjectIri, Shortcode]]): Task[ListsGetResponseADM] = + listsResponder.getLists(projectIriOpt) + + def listGetRequestADM(iri: ListIri): Task[ListItemGetResponseADM] = listsResponder.listGetRequestADM(iri.value) + + def listNodeInfoGetRequestADM(iri: ListIri): Task[NodeInfoGetResponseADM] = + listsResponder.listNodeInfoGetRequestADM(iri.value) + + def listChange(user: User)(iri: ListIri, request: ListChangeRequest): Task[NodeInfoGetResponseADM] = for { _ <- ZIO.fail(BadRequestException("List IRI in path and body must match")).when(iri != request.listIri) project <- knoraProjectService .findById(request.projectIri) @@ -38,42 +53,41 @@ final case class AdminListRestService( response <- listsResponder.nodeInfoChangeRequest(request, uuid) } yield response - def listChangeName(iri: ListIri, request: ListChangeNameRequest, user: User): Task[NodeInfoGetResponseADM] = for { + def listChangeName(user: User)(iri: ListIri, request: ListChangeNameRequest): Task[NodeInfoGetResponseADM] = for { // authorization is currently done in the responder uuid <- Random.nextUUID response <- listsResponder.nodeNameChangeRequest(iri, request, user, uuid) } yield response - def listChangeLabels(iri: ListIri, request: ListChangeLabelsRequest, user: User): Task[NodeInfoGetResponseADM] = for { + def listChangeLabels(user: User)(iri: ListIri, request: ListChangeLabelsRequest): Task[NodeInfoGetResponseADM] = for { // authorization is currently done in the responder uuid <- Random.nextUUID response <- listsResponder.nodeLabelsChangeRequest(iri, request, user, uuid) } yield response - def listChangeComments(iri: ListIri, request: ListChangeCommentsRequest, user: User): Task[NodeInfoGetResponseADM] = + def listChangeComments(user: User)(iri: ListIri, request: ListChangeCommentsRequest): Task[NodeInfoGetResponseADM] = for { // authorization is currently done in the responder uuid <- Random.nextUUID response <- listsResponder.nodeCommentsChangeRequest(iri, request, user, uuid) } yield response - def nodePositionChangeRequest( + def nodePositionChangeRequest(user: User)( iri: ListIri, request: ListChangePositionRequest, - user: User, ): Task[NodePositionChangeResponseADM] = for { // authorization is currently done in the responder uuid <- Random.nextUUID response <- listsResponder.nodePositionChangeRequest(iri, request, user, uuid) } yield response - def deleteListItemRequestADM(iri: ListIri, user: User): Task[ListItemDeleteResponseADM] = for { + def deleteListItemRequestADM(user: User)(iri: ListIri): Task[ListItemDeleteResponseADM] = for { // authorization is currently done in the responder uuid <- Random.nextUUID response <- listsResponder.deleteListItemRequestADM(iri, user, uuid) } yield response - def listCreateRootNode(req: ListCreateRootNodeRequest, user: User): Task[ListGetResponseADM] = for { + def listCreateRootNode(user: User)(req: ListCreateRootNodeRequest): Task[ListGetResponseADM] = for { project <- knoraProjectService .findById(req.projectIri) .someOrFail(BadRequestException("Project not found")) @@ -82,7 +96,10 @@ final case class AdminListRestService( response <- listsResponder.listCreateRootNode(req, uuid) } yield response - def listCreateChildNode(req: ListCreateChildNodeRequest, user: User): Task[ChildNodeInfoGetResponseADM] = for { + def listCreateChildNode( + user: User, + )(iri: ListIri, req: ListCreateChildNodeRequest): Task[ChildNodeInfoGetResponseADM] = for { + _ <- ZIO.fail(BadRequestException("Route and payload parentNodeIri mismatch.")).when(iri != req.parentNodeIri) project <- knoraProjectService .findById(req.projectIri) .someOrFail(BadRequestException("Project not found")) @@ -91,6 +108,11 @@ final case class AdminListRestService( response <- listsResponder.listCreateChildNode(req, uuid) } yield response + def canDeleteListRequestADM(iri: ListIri): Task[CanDeleteListResponseADM] = + listsResponder.canDeleteListRequestADM(iri) + + def deleteListNodeCommentsADM(ignored: User)(iri: ListIri): Task[ListNodeCommentsDeleteResponseADM] = + listsResponder.deleteListNodeCommentsADM(iri) } object AdminListRestService { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala index 7bf74c6b189..7ac74769d1f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.admin.api import zio.ZIO import zio.ZLayer -import dsp.errors.BadRequestException import org.knora.webapi.messages.admin.responder.listsmessages.CanDeleteListResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM @@ -16,9 +15,7 @@ import org.knora.webapi.messages.admin.responder.listsmessages.ListItemDeleteRes import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCommentsDeleteResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.NodeInfoGetResponseADM import org.knora.webapi.messages.admin.responder.listsmessages.NodePositionChangeResponseADM -import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.slice.admin.api.Requests.* -import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri @@ -29,130 +26,30 @@ import org.knora.webapi.slice.common.api.SecuredEndpointHandler final case class AdminListsEndpointsHandlers( private val adminListsEndpoints: AdminListsEndpoints, - private val adminListsRestService: AdminListRestService, - private val listsResponder: ListsResponder, + private val restService: AdminListRestService, private val mapper: HandlerMapper, ) { - private val getListsQueryByProjectIriHandler = PublicEndpointHandler( - adminListsEndpoints.getListsQueryByProjectIriOption, - (iriShortcode: Option[Either[ProjectIri, Shortcode]]) => listsResponder.getLists(iriShortcode), - ) - - private val getListsByIriHandler = PublicEndpointHandler( - adminListsEndpoints.getListsByIri, - (iri: ListIri) => listsResponder.listGetRequestADM(iri.value), - ) - - private val getListsByIriInfoHandler = PublicEndpointHandler( - adminListsEndpoints.getListsByIriInfo, - (iri: ListIri) => listsResponder.listNodeInfoGetRequestADM(iri.value), - ) - - private val getListsInfosByIriHandler = PublicEndpointHandler( - adminListsEndpoints.getListsInfosByIri, - (iri: ListIri) => listsResponder.listNodeInfoGetRequestADM(iri.value), - ) - - private val getListsNodesByIriHandler = PublicEndpointHandler( - adminListsEndpoints.getListsNodesByIri, - (iri: ListIri) => listsResponder.listNodeInfoGetRequestADM(iri.value), - ) - - // Creates - private val postListsCreateRootNodeHandler = - SecuredEndpointHandler[ListCreateRootNodeRequest, ListGetResponseADM]( - adminListsEndpoints.postLists, - user => req => adminListsRestService.listCreateRootNode(req, user), - ) - - private val postListsCreateChildNodeHandler = - SecuredEndpointHandler[(ListIri, ListCreateChildNodeRequest), ChildNodeInfoGetResponseADM]( - adminListsEndpoints.postListsChild, - user => { case (iri: ListIri, req: ListCreateChildNodeRequest) => - ZIO - .fail(BadRequestException("Route and payload parentNodeIri mismatch.")) - .when(iri != req.parentNodeIri) *> - adminListsRestService.listCreateChildNode(req, user) - }, - ) - - // Updates - private val putListsByIriNameHandler = - SecuredEndpointHandler[(ListIri, ListChangeNameRequest), NodeInfoGetResponseADM]( - adminListsEndpoints.putListsByIriName, - (user: User) => { case (iri: ListIri, newName: ListChangeNameRequest) => - adminListsRestService.listChangeName(iri, newName, user) - }, - ) - - private val putListsByIriLabelsHandler = - SecuredEndpointHandler[(ListIri, ListChangeLabelsRequest), NodeInfoGetResponseADM]( - adminListsEndpoints.putListsByIriLabels, - (user: User) => { case (iri: ListIri, request: ListChangeLabelsRequest) => - adminListsRestService.listChangeLabels(iri, request, user) - }, - ) - - private val putListsByIriCommentsHandler = - SecuredEndpointHandler[(ListIri, ListChangeCommentsRequest), NodeInfoGetResponseADM]( - adminListsEndpoints.putListsByIriComments, - (user: User) => { case (iri: ListIri, request: ListChangeCommentsRequest) => - adminListsRestService.listChangeComments(iri, request, user) - }, - ) - - private val putListsByIriPositionHandler = - SecuredEndpointHandler[(ListIri, ListChangePositionRequest), NodePositionChangeResponseADM]( - adminListsEndpoints.putListsByIriPosition, - (user: User) => { case (iri: ListIri, request: ListChangePositionRequest) => - adminListsRestService.nodePositionChangeRequest(iri, request, user) - }, - ) - - private val putListsByIriHandler = SecuredEndpointHandler[(ListIri, ListChangeRequest), NodeInfoGetResponseADM]( - adminListsEndpoints.putListsByIri, - (user: User) => { case (iri: ListIri, request: ListChangeRequest) => - adminListsRestService.listChange(iri, request, user) - }, - ) - - // Deletes - private val deleteListsByIriHandler = SecuredEndpointHandler[ListIri, ListItemDeleteResponseADM]( - adminListsEndpoints.deleteListsByIri, - user => listIri => adminListsRestService.deleteListItemRequestADM(listIri, user), - ) - - private val getListsCanDeleteByIriHandler = PublicEndpointHandler[ListIri, CanDeleteListResponseADM]( - adminListsEndpoints.getListsCanDeleteByIri, - listsResponder.canDeleteListRequestADM, - ) - - private val deleteListsCommentHandler = SecuredEndpointHandler[ListIri, ListNodeCommentsDeleteResponseADM]( - adminListsEndpoints.deleteListsComment, - _ => listIri => listsResponder.deleteListNodeCommentsADM(listIri), - ) - private val public = List( - getListsByIriHandler, - getListsQueryByProjectIriHandler, - getListsByIriInfoHandler, - getListsInfosByIriHandler, - getListsNodesByIriHandler, - getListsCanDeleteByIriHandler, - ).map(mapper.mapPublicEndpointHandler(_)) + PublicEndpointHandler(adminListsEndpoints.getListsByIri, restService.listGetRequestADM), + PublicEndpointHandler(adminListsEndpoints.getListsQueryByProjectIriOption, restService.getLists), + PublicEndpointHandler(adminListsEndpoints.getListsByIriInfo, restService.listNodeInfoGetRequestADM), + PublicEndpointHandler(adminListsEndpoints.getListsInfosByIri, restService.listNodeInfoGetRequestADM), + PublicEndpointHandler(adminListsEndpoints.getListsNodesByIri, restService.listNodeInfoGetRequestADM), + PublicEndpointHandler(adminListsEndpoints.getListsCanDeleteByIri, restService.canDeleteListRequestADM), + ).map(mapper.mapPublicEndpointHandler) private val secured = List( - postListsCreateRootNodeHandler, - postListsCreateChildNodeHandler, - putListsByIriNameHandler, - putListsByIriLabelsHandler, - putListsByIriCommentsHandler, - putListsByIriPositionHandler, - putListsByIriHandler, - deleteListsByIriHandler, - deleteListsCommentHandler, - ).map(mapper.mapSecuredEndpointHandler(_)) + SecuredEndpointHandler(adminListsEndpoints.postLists, restService.listCreateRootNode), + SecuredEndpointHandler(adminListsEndpoints.postListsChild, restService.listCreateChildNode), + SecuredEndpointHandler(adminListsEndpoints.putListsByIriName, restService.listChangeName), + SecuredEndpointHandler(adminListsEndpoints.putListsByIriLabels, restService.listChangeLabels), + SecuredEndpointHandler(adminListsEndpoints.putListsByIriComments, restService.listChangeComments), + SecuredEndpointHandler(adminListsEndpoints.putListsByIriPosition, restService.nodePositionChangeRequest), + SecuredEndpointHandler(adminListsEndpoints.putListsByIri, restService.listChange), + SecuredEndpointHandler(adminListsEndpoints.deleteListsByIri, restService.deleteListItemRequestADM), + SecuredEndpointHandler(adminListsEndpoints.deleteListsComment, restService.deleteListNodeCommentsADM), + ).map(mapper.mapSecuredEndpointHandler) val allHandlers = public ++ secured } From 18239395b1469800155cf220b16f22da6a7cc07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 13:16:42 +0200 Subject: [PATCH 11/99] remove Public and SecuredHandlers --- .../api/AdminListsEndpointsHandlers.scala | 20 +-- .../api/MaintenanceEndpointsHandlers.scala | 6 +- .../api/PermissionsEndpointsHandlers.scala | 46 ++---- .../admin/api/ProjectsEndpointsHandler.scala | 149 ++++++------------ .../ProjectsLegalInfoEntpointsHandler.scala | 30 ++-- .../admin/api/StoreEndpointsHandler.scala | 17 +- .../admin/api/UsersEndpointsHandler.scala | 60 +++---- .../infrastructure/api/ManagementRoutes.scala | 26 +-- .../lists/api/ListsEndpointsV2Handler.scala | 21 +-- .../api/OntologiesEndpointsHandler.scala | 74 ++++----- .../api/MetadataServerEndpoints.scala | 13 +- .../resources/api/ResourceInfoRoutes.scala | 15 +- .../api/ResourcesEndpointsHandler.scala | 53 +++---- .../api/StandoffEndpointsHandler.scala | 12 +- .../api/ValuesEndpointsHandler.scala | 25 ++- .../slice/search/api/SearchApiRoutes.scala | 44 +++--- .../AuthenticationEndpointsV2Handler.scala | 28 ++-- .../shacl/api/ShaclEndpointsHandler.scala | 11 +- 18 files changed, 240 insertions(+), 410 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala index e1e3e58512d..ebb35a727c2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala @@ -28,22 +28,22 @@ final case class AdminListsEndpointsHandlers( private val adminListsEndpoints: AdminListsEndpoints, private val restService: AdminListRestService, ) { - val allHandlers: Seq[ZServerEndpoint[Any, Any]] + val allHandlers: Seq[ZServerEndpoint[Any, Any]] = Seq( adminListsEndpoints.getListsQueryByProjectIriOption.zServerLogic(restService.getLists), adminListsEndpoints.getListsByIri.zServerLogic(restService.listGetRequestADM), adminListsEndpoints.getListsByIriInfo.zServerLogic(restService.listNodeInfoGetRequestADM), adminListsEndpoints.getListsInfosByIri.zServerLogic(restService.listNodeInfoGetRequestADM), adminListsEndpoints.getListsNodesByIri.zServerLogic(restService.listNodeInfoGetRequestADM), adminListsEndpoints.getListsCanDeleteByIri.zServerLogic(restService.canDeleteListRequestADM), - adminListsEndpoints.postLists.serverLogic( restService.listCreateRootNode), - adminListsEndpoints.postListsChild.serverLogic( restService.listCreateChildNode), - adminListsEndpoints.putListsByIriName.serverLogic( restService.listChangeName), - adminListsEndpoints.putListsByIriLabels.serverLogic( restService.listChangeLabels), - adminListsEndpoints.putListsByIriComments.serverLogic( restService.listChangeComments), - adminListsEndpoints.putListsByIriPosition.serverLogic( restService.nodePositionChangeRequest), - adminListsEndpoints.putListsByIri.serverLogic( restService.listChange), - adminListsEndpoints.deleteListsByIri.serverLogic( restService.deleteListItemRequestADM), - adminListsEndpoints.deleteListsComment.serverLogic( restService.deleteListNodeCommentsADM), + adminListsEndpoints.postLists.serverLogic(restService.listCreateRootNode), + adminListsEndpoints.postListsChild.serverLogic(restService.listCreateChildNode), + adminListsEndpoints.putListsByIriName.serverLogic(restService.listChangeName), + adminListsEndpoints.putListsByIriLabels.serverLogic(restService.listChangeLabels), + adminListsEndpoints.putListsByIriComments.serverLogic(restService.listChangeComments), + adminListsEndpoints.putListsByIriPosition.serverLogic(restService.nodePositionChangeRequest), + adminListsEndpoints.putListsByIri.serverLogic(restService.listChange), + adminListsEndpoints.deleteListsByIri.serverLogic(restService.deleteListItemRequestADM), + adminListsEndpoints.deleteListsComment.serverLogic(restService.deleteListNodeCommentsADM), ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala index 670151bc731..53ab37fbcb2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala @@ -17,10 +17,10 @@ final case class MaintenanceEndpointsHandlers( endpoints: MaintenanceEndpoints, restService: MaintenanceRestService, ) { - - val allHandlers: ZServerEndpoint[Any, Any] = Seq(endpoints.postMaintenance.serverLogic(restService.executeMaintenanceAction)) + val allHandlers: ZServerEndpoint[Any, Any] = Seq( + endpoints.postMaintenance.serverLogic(restService.executeMaintenanceAction), + ) } - object MaintenanceEndpointsHandlers { val layer = ZLayer.derive[MaintenanceEndpointsHandlers] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala index 79675cc2f8a..f2f9910ce70 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala @@ -5,7 +5,8 @@ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionCreateResponseADM import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionGetResponseADM @@ -27,44 +28,29 @@ import org.knora.webapi.slice.admin.api.service.PermissionRestService import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.PermissionIri -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler final case class PermissionsEndpointsHandlers( permissionsEndpoints: PermissionsEndpoints, restService: PermissionRestService, - mapper: HandlerMapper, ) { - val allHanders = List( - SecuredEndpointHandler(permissionsEndpoints.postPermissionsAp, restService.createAdministrativePermission), - SecuredEndpointHandler(permissionsEndpoints.getPermissionsApByProjectIri, restService.getPermissionsApByProjectIri), - SecuredEndpointHandler( - permissionsEndpoints.getPermissionsApByProjectAndGroupIri, + val allHanders = Seq( + permissionsEndpoints.postPermissionsAp.serverLogic(restService.createAdministrativePermission), + permissionsEndpoints.getPermissionsApByProjectIri.serverLogic(restService.getPermissionsApByProjectIri), + permissionsEndpoints.getPermissionsApByProjectAndGroupIri.serverLogic( restService.getPermissionsApByProjectAndGroupIri, ), - SecuredEndpointHandler( - permissionsEndpoints.getPermissionsDoapByProjectIri, - restService.getPermissionsDaopByProjectIri, - ), - SecuredEndpointHandler(permissionsEndpoints.getPermissionsByProjectIri, restService.getPermissionsByProjectIri), - SecuredEndpointHandler(permissionsEndpoints.putPermissionsDoapForWhat, restService.updateDoapForWhat), - SecuredEndpointHandler(permissionsEndpoints.putPermissionsProjectIriGroup, restService.updatePermissionGroup), - SecuredEndpointHandler( - permissionsEndpoints.putPerrmissionsHasPermissions, - restService.updatePermissionHasPermissions, - ), - SecuredEndpointHandler(permissionsEndpoints.putPermissionsProperty, restService.updatePermissionProperty), - SecuredEndpointHandler( - permissionsEndpoints.putPermisssionsResourceClass, - restService.updatePermissionResourceClass, - ), - SecuredEndpointHandler(permissionsEndpoints.deletePermission, restService.deletePermission), - SecuredEndpointHandler(permissionsEndpoints.postPermissionsDoap, restService.createDefaultObjectAccessPermission), - ).map(mapper.mapSecuredEndpointHandler) + permissionsEndpoints.getPermissionsDoapByProjectIri.serverLogic(restService.getPermissionsDaopByProjectIri), + permissionsEndpoints.getPermissionsByProjectIri.serverLogic(restService.getPermissionsByProjectIri), + permissionsEndpoints.putPermissionsDoapForWhat.serverLogic(restService.updateDoapForWhat), + permissionsEndpoints.putPermissionsProjectIriGroup.serverLogic(restService.updatePermissionGroup), + permissionsEndpoints.putPerrmissionsHasPermissions.serverLogic(restService.updatePermissionHasPermissions), + permissionsEndpoints.putPermissionsProperty.serverLogic(restService.updatePermissionProperty), + permissionsEndpoints.putPermisssionsResourceClass.serverLogic(restService.updatePermissionResourceClass), + permissionsEndpoints.deletePermission.serverLogic(restService.deletePermission), + permissionsEndpoints.postPermissionsDoap.serverLogic(restService.createDefaultObjectAccessPermission), + ) } - object PermissionsEndpointsHandlers { - val layer = ZLayer.derive[PermissionsEndpointsHandlers] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala index 9940a43b825..af5d211ba8e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala @@ -5,8 +5,9 @@ package org.knora.webapi.slice.admin.api -import org.apache.pekko.stream.scaladsl.FileIO -import zio.ZLayer +import sttp.tapir.ztapir.* +import zio.* +import zio.stream.* import java.nio.file.Files import scala.concurrent.ExecutionContext @@ -21,114 +22,62 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler final case class ProjectsEndpointsHandler( projectsEndpoints: ProjectsEndpoints, restService: ProjectRestService, - mapper: HandlerMapper, ) { - val getAdminProjectsByIriAllDataHandler = { - implicit val ec: ExecutionContext = ExecutionContext.global + private val getAdminProjectsByIriAllDataHandler = projectsEndpoints.Secured.getAdminProjectsByIriAllData.serverLogic((user: User) => (iri: ProjectIri) => - // Future[Either[RequestRejectedException, (String, String, PekkoStreams.BinaryStream]] - mapper.runToFuture( - restService - .getAllProjectData(user)(iri) - .map { result => - val path = result.projectDataFile -// On Pekko use pekko-streams to stream the file, but when running on zio-http we use ZStream: -// val stream = ZStream -// .fromPath(path) -// .ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) - val stream = FileIO - .fromPath(path) - .watchTermination() { case (_, result) => result.onComplete(_ => Files.deleteIfExists(path)) } - (s"attachment; filename=project-data.trig", "application/octet-stream", stream) - }, - ), + restService + .getAllProjectData(user)(iri) + .map { result => + val path = result.projectDataFile + val stream = ZStream.fromPath(path).ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) + (s"attachment; filename=project-data.trig", "application/octet-stream", stream) + }, ) - } - private val handlers = - List( - PublicEndpointHandler(projectsEndpoints.Public.getAdminProjects, restService.listAllProjects), - PublicEndpointHandler(projectsEndpoints.Public.getAdminProjectsKeywords, restService.listAllKeywords), - PublicEndpointHandler(projectsEndpoints.Public.getAdminProjectsByProjectIri, restService.findById), - PublicEndpointHandler(projectsEndpoints.Public.getAdminProjectsByProjectShortcode, restService.findByShortcode), - PublicEndpointHandler(projectsEndpoints.Public.getAdminProjectsByProjectShortname, restService.findByShortname), - PublicEndpointHandler( - projectsEndpoints.Public.getAdminProjectsKeywordsByProjectIri, - restService.getKeywordsByProjectIri, - ), - PublicEndpointHandler( - projectsEndpoints.Public.getAdminProjectsByProjectIriRestrictedViewSettings, - restService.getProjectRestrictedViewSettingsById, - ), - PublicEndpointHandler( - projectsEndpoints.Public.getAdminProjectsByProjectShortcodeRestrictedViewSettings, - restService.getProjectRestrictedViewSettingsByShortcode, - ), - PublicEndpointHandler( - projectsEndpoints.Public.getAdminProjectsByProjectShortnameRestrictedViewSettings, - restService.getProjectRestrictedViewSettingsByShortname, - ), - ).map(mapper.mapPublicEndpointHandler(_)) - - private val secureHandlers = getAdminProjectsByIriAllDataHandler :: List( - SecuredEndpointHandler( - projectsEndpoints.Secured.postAdminProjectsByProjectIriRestrictedViewSettings, - restService.updateProjectRestrictedViewSettingsById, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.postAdminProjectsByProjectShortcodeRestrictedViewSettings, - restService.updateProjectRestrictedViewSettingsByShortcode, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectIriMembers, - restService.getProjectMembersById, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeMembers, - restService.getProjectMembersByShortcode, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectShortnameMembers, - restService.getProjectMembersByShortname, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectIriAdminMembers, - restService.getProjectAdminMembersById, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeAdminMembers, - restService.getProjectAdminMembersByShortcode, - ), - SecuredEndpointHandler( - projectsEndpoints.Secured.getAdminProjectsByProjectShortnameAdminMembers, - restService.getProjectAdminMembersByShortname, - ), - SecuredEndpointHandler(projectsEndpoints.Secured.deleteAdminProjectsByIri, restService.deleteProject), - SecuredEndpointHandler( - projectsEndpoints.Secured.deleteAdminProjectsByProjectShortcodeErase, - restService.eraseProject, - ), - SecuredEndpointHandler(projectsEndpoints.Secured.getAdminProjectsExports, restService.listExports), - SecuredEndpointHandler(projectsEndpoints.Secured.postAdminProjectsByShortcodeExport, restService.exportProject), - SecuredEndpointHandler( - projectsEndpoints.Secured.postAdminProjectsByShortcodeExportAwaiting, - restService.exportProjectAwaiting, - ), - SecuredEndpointHandler(projectsEndpoints.Secured.postAdminProjectsByShortcodeImport, restService.importProject), - SecuredEndpointHandler(projectsEndpoints.Secured.postAdminProjects, restService.createProject), - SecuredEndpointHandler(projectsEndpoints.Secured.putAdminProjectsByIri, restService.updateProject), - ).map(mapper.mapSecuredEndpointHandler) - - val allHanders = handlers ++ secureHandlers + val allHanders = Seq( + projectsEndpoints.Public.getAdminProjects.zServerLogic(restService.listAllProjects), + projectsEndpoints.Public.getAdminProjectsKeywords.zServerLogic(restService.listAllKeywords), + projectsEndpoints.Public.getAdminProjectsByProjectIri.zServerLogic(restService.findById), + projectsEndpoints.Public.getAdminProjectsByProjectShortcode.zServerLogic(restService.findByShortcode), + projectsEndpoints.Public.getAdminProjectsByProjectShortname.zServerLogic(restService.findByShortname), + projectsEndpoints.Public.getAdminProjectsKeywordsByProjectIri.zServerLogic(restService.getKeywordsByProjectIri), + projectsEndpoints.Public.getAdminProjectsByProjectIriRestrictedViewSettings + .zServerLogic(restService.getProjectRestrictedViewSettingsById), + projectsEndpoints.Public.getAdminProjectsByProjectShortcodeRestrictedViewSettings + .zServerLogic(restService.getProjectRestrictedViewSettingsByShortcode), + projectsEndpoints.Public.getAdminProjectsByProjectShortnameRestrictedViewSettings + .zServerLogic(restService.getProjectRestrictedViewSettingsByShortname), + getAdminProjectsByIriAllDataHandler, + projectsEndpoints.Secured.postAdminProjectsByProjectIriRestrictedViewSettings + .serverLogic(restService.updateProjectRestrictedViewSettingsById), + projectsEndpoints.Secured.postAdminProjectsByProjectShortcodeRestrictedViewSettings + .serverLogic(restService.updateProjectRestrictedViewSettingsByShortcode), + projectsEndpoints.Secured.getAdminProjectsByProjectIriMembers.serverLogic(restService.getProjectMembersById), + projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeMembers + .serverLogic(restService.getProjectMembersByShortcode), + projectsEndpoints.Secured.getAdminProjectsByProjectShortnameMembers + .serverLogic(restService.getProjectMembersByShortname), + projectsEndpoints.Secured.getAdminProjectsByProjectIriAdminMembers + .serverLogic(restService.getProjectAdminMembersById), + projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeAdminMembers + .serverLogic(restService.getProjectAdminMembersByShortcode), + projectsEndpoints.Secured.getAdminProjectsByProjectShortnameAdminMembers + .serverLogic(restService.getProjectAdminMembersByShortname), + projectsEndpoints.Secured.deleteAdminProjectsByIri.serverLogic(restService.deleteProject), + projectsEndpoints.Secured.deleteAdminProjectsByProjectShortcodeErase.serverLogic(restService.eraseProject), + projectsEndpoints.Secured.getAdminProjectsExports.serverLogic(restService.listExports), + projectsEndpoints.Secured.postAdminProjectsByShortcodeExport.serverLogic(restService.exportProject), + projectsEndpoints.Secured.postAdminProjectsByShortcodeExportAwaiting.serverLogic(restService.exportProjectAwaiting), + projectsEndpoints.Secured.postAdminProjectsByShortcodeImport.serverLogic(restService.importProject), + projectsEndpoints.Secured.postAdminProjects.serverLogic(restService.createProject), + projectsEndpoints.Secured.putAdminProjectsByIri.serverLogic(restService.updateProject), + ) } object ProjectsEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala index 1fc567fbb62..3e17f920c93 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala @@ -4,31 +4,25 @@ */ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.api.service.ProjectsLegalInfoRestService -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler final class ProjectsLegalInfoEndpointsHandler( endpoints: ProjectsLegalInfoEndpoints, restService: ProjectsLegalInfoRestService, ) { - val allHandlers = - List( - PublicEndpointHandler(endpoints.getProjectLicenses, restService.findLicenses), - PublicEndpointHandler(endpoints.getProjectLicensesIri, restService.findAvailableLicenseByIdAndShortcode), - ) - .map(mapper.mapPublicEndpointHandler) ++ - List( - SecuredEndpointHandler(endpoints.getProjectAuthorships, restService.findAuthorships), - SecuredEndpointHandler(endpoints.putProjectLicensesEnable, restService.enableLicense), - SecuredEndpointHandler(endpoints.putProjectLicensesDisable, restService.disableLicense), - SecuredEndpointHandler(endpoints.getProjectCopyrightHolders, restService.findCopyrightHolders), - SecuredEndpointHandler(endpoints.postProjectCopyrightHolders, restService.addCopyrightHolders), - SecuredEndpointHandler(endpoints.putProjectCopyrightHolders, restService.replaceCopyrightHolder), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq( + endpoints.getProjectLicenses.zServerLogic(restService.findLicenses), + endpoints.getProjectLicensesIri.zServerLogic(restService.findAvailableLicenseByIdAndShortcode), + endpoints.getProjectAuthorships.serverLogic(restService.findAuthorships), + endpoints.putProjectLicensesEnable.serverLogic(restService.enableLicense), + endpoints.putProjectLicensesDisable.serverLogic(restService.disableLicense), + endpoints.getProjectCopyrightHolders.serverLogic(restService.findCopyrightHolders), + endpoints.postProjectCopyrightHolders.serverLogic(restService.addCopyrightHolders), + endpoints.putProjectCopyrightHolders.serverLogic(restService.replaceCopyrightHolder), + ) } object ProjectsLegalInfoEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala index bf115e9978b..7fe88092e34 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala @@ -5,28 +5,23 @@ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.slice.admin.api.service.StoreRestService -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler final case class StoreEndpointsHandler( private val appConfig: AppConfig, private val endpoints: StoreEndpoints, - private val mapper: HandlerMapper, private val restService: StoreRestService, ) { - val allHandlers = { - val handlerIfConfigured = - if (appConfig.allowReloadOverHttp) - Seq(PublicEndpointHandler(endpoints.postStoreResetTriplestoreContent, restService.resetTriplestoreContent)) - else Seq.empty - handlerIfConfigured.map(mapper.mapPublicEndpointHandler) - } + val allHandlers = + if (appConfig.allowReloadOverHttp) + Seq(endpoints.postStoreResetTriplestoreContent.zServerLogic(restService.resetTriplestoreContent)) + else Seq.empty } object StoreEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala index 9f592f23069..cf3115c6164 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala @@ -5,7 +5,8 @@ package org.knora.webapi.slice.admin.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.BasicUserInformationChangeRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.PasswordChangeRequest @@ -19,48 +20,33 @@ import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.Username -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler case class UsersEndpointsHandler( usersEndpoints: UsersEndpoints, restService: UserRestService, - mapper: HandlerMapper, ) { - private val public = List( - PublicEndpointHandler(usersEndpoints.get.usersByIriProjectMemberShips, restService.getProjectMemberShipsByUserIri), - PublicEndpointHandler( - usersEndpoints.get.usersByIriProjectAdminMemberShips, - restService.getProjectAdminMemberShipsByUserIri, - ), - PublicEndpointHandler(usersEndpoints.get.usersByIriGroupMemberships, restService.getGroupMemberShipsByIri), - ).map(mapper.mapPublicEndpointHandler(_)) - - private val secured = List( - SecuredEndpointHandler(usersEndpoints.get.users, user => _ => restService.getAllUsers(user)), - SecuredEndpointHandler(usersEndpoints.get.userByIri, restService.getUserByIri), - SecuredEndpointHandler(usersEndpoints.get.userByEmail, restService.getUserByEmail), - SecuredEndpointHandler(usersEndpoints.get.userByUsername, restService.getUserByUsername), - SecuredEndpointHandler(usersEndpoints.post.users, restService.createUser), - SecuredEndpointHandler(usersEndpoints.post.usersByIriProjectMemberShips, restService.addUserToProject), - SecuredEndpointHandler(usersEndpoints.post.usersByIriProjectAdminMemberShips, restService.addUserToProjectAsAdmin), - SecuredEndpointHandler(usersEndpoints.post.usersByIriGroupMemberShips, restService.addUserToGroup), - SecuredEndpointHandler(usersEndpoints.put.usersIriBasicInformation, restService.updateUser), - SecuredEndpointHandler(usersEndpoints.put.usersIriPassword, restService.changePassword), - SecuredEndpointHandler(usersEndpoints.put.usersIriStatus, restService.changeStatus), - SecuredEndpointHandler(usersEndpoints.put.usersIriSystemAdmin, restService.changeSystemAdmin), - SecuredEndpointHandler(usersEndpoints.delete.deleteUser, restService.deleteUser), - SecuredEndpointHandler(usersEndpoints.delete.usersByIriProjectMemberShips, restService.removeUserFromProject), - SecuredEndpointHandler( - usersEndpoints.delete.usersByIriProjectAdminMemberShips, - restService.removeUserFromProjectAsAdmin, - ), - SecuredEndpointHandler(usersEndpoints.delete.usersByIriGroupMemberShips, restService.removeUserFromGroup), - ).map(mapper.mapSecuredEndpointHandler(_)) - - val allHanders = public ++ secured + val allHanders = Seq( + usersEndpoints.get.usersByIriProjectMemberShips.zServerLogic(restService.getProjectMemberShipsByUserIri), + usersEndpoints.get.usersByIriProjectAdminMemberShips.zServerLogic(restService.getProjectAdminMemberShipsByUserIri), + usersEndpoints.get.usersByIriGroupMemberships.zServerLogic(restService.getGroupMemberShipsByIri), + usersEndpoints.get.users.serverLogic(user => _ => restService.getAllUsers(user)), + usersEndpoints.get.userByIri.serverLogic(restService.getUserByIri), + usersEndpoints.get.userByEmail.serverLogic(restService.getUserByEmail), + usersEndpoints.get.userByUsername.serverLogic(restService.getUserByUsername), + usersEndpoints.post.users.serverLogic(restService.createUser), + usersEndpoints.post.usersByIriProjectMemberShips.serverLogic(restService.addUserToProject), + usersEndpoints.post.usersByIriProjectAdminMemberShips.serverLogic(restService.addUserToProjectAsAdmin), + usersEndpoints.post.usersByIriGroupMemberShips.serverLogic(restService.addUserToGroup), + usersEndpoints.put.usersIriBasicInformation.serverLogic(restService.updateUser), + usersEndpoints.put.usersIriPassword.serverLogic(restService.changePassword), + usersEndpoints.put.usersIriStatus.serverLogic(restService.changeStatus), + usersEndpoints.put.usersIriSystemAdmin.serverLogic(restService.changeSystemAdmin), + usersEndpoints.delete.deleteUser.serverLogic(restService.deleteUser), + usersEndpoints.delete.usersByIriProjectMemberShips.serverLogic(restService.removeUserFromProject), + usersEndpoints.delete.usersByIriProjectAdminMemberShips.serverLogic(restService.removeUserFromProjectAsAdmin), + usersEndpoints.delete.usersByIriGroupMemberShips.serverLogic(restService.removeUserFromGroup), + ) } object UsersEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala index ae75d4bc33d..b19ee76804a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala @@ -5,31 +5,21 @@ package org.knora.webapi.slice.infrastructure.api -import sttp.model.StatusCode +import sttp.tapir.ztapir.* import zio.* -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - final case class ManagementRoutes( private val endpoint: ManagementEndpoints, private val restService: ManagementRestService, - private val mapper: HandlerMapper, - private val tapirToPekko: TapirToPekkoInterpreter, ) { - val routes = ( - List( - PublicEndpointHandler(endpoint.getVersion, _ => ZIO.succeed(VersionResponse.current)), - PublicEndpointHandler(endpoint.getHealth, _ => restService.healthCheck), - ).map(mapper.mapPublicEndpointHandler) - ++ - List(SecuredEndpointHandler(endpoint.postStartCompaction, restService.startCompaction)) - .map(mapper.mapSecuredEndpointHandler) - ).map(tapirToPekko.toRoute) + val allHandlers = Seq( + endpoint.getVersion.zServerLogic(_ => ZIO.succeed(VersionResponse.current)), + endpoint.getHealth.zServerLogic(_ => restService.healthCheck), + endpoint.postStartCompaction.serverLogic(restService.startCompaction), + ) + } object ManagementRoutes { - val layer = zio.ZLayer.derive[ManagementRoutes] + val layer = ZLayer.derive[ManagementRoutes] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala index 48e04fefd73..ca4643e8525 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala @@ -4,27 +4,20 @@ */ package org.knora.webapi.slice.lists.api -import sttp.model.MediaType + import zio.* +import sttp.tapir.ztapir.* -import org.knora.webapi.config.AppConfig -import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri -import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.lists.api.service.ListsV2RestService final case class ListsEndpointsV2Handler( - private val appConfig: AppConfig, private val endpoints: ListsEndpointsV2, - private val listsRestService: ListsV2RestService, - private val mapper: HandlerMapper, + private val restService: ListsV2RestService, ) { - val allHandlers = List( - SecuredEndpointHandler(endpoints.getV2Lists, listsRestService.getList), - SecuredEndpointHandler(endpoints.getV2Node, listsRestService.getNode), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq( + endpoints.getV2Lists.serverLogic(restService.getList), + endpoints.getV2Node.serverLogic(restService.getNode), + ) } object ListsEndpointsV2Handler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala index a4fad72d746..03fd8b3db0d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala @@ -4,57 +4,49 @@ */ package org.knora.webapi.slice.ontology.api -import zio.ZLayer + +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.domain.model.User -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.ontology.api.service.OntologiesRestService final class OntologiesEndpointsHandler( private val endpoints: OntologiesEndpoints, private val restService: OntologiesRestService, - private val mapper: HandlerMapper, ) { - private val publicHandlers = Seq( - PublicEndpointHandler(endpoints.getOntologiesMetadataProject, restService.getOntologyMetadataByProjectOption), - PublicEndpointHandler(endpoints.getOntologiesMetadataProjects, restService.getOntologyMetadataByProjects), - ).map(mapper.mapPublicEndpointHandler) - - private val secureHandlers = Seq( - SecuredEndpointHandler(endpoints.getOntologyPathSegments, restService.dereferenceOntologyIri), - SecuredEndpointHandler(endpoints.putOntologiesMetadata, restService.changeOntologyMetadata), - SecuredEndpointHandler(endpoints.getOntologiesAllentities, restService.getOntologyEntities), - SecuredEndpointHandler(endpoints.postOntologiesClasses, restService.createClass), - SecuredEndpointHandler(endpoints.putOntologiesClasses, restService.changeClassLabelsOrComments), - SecuredEndpointHandler(endpoints.deleteOntologiesClassesComment, restService.deleteClassComment), - SecuredEndpointHandler(endpoints.postOntologiesCardinalities, restService.addCardinalities), - SecuredEndpointHandler(endpoints.getOntologiesCanreplacecardinalities, restService.canChangeCardinality), - SecuredEndpointHandler(endpoints.putOntologiesCardinalities, restService.replaceCardinalities), - SecuredEndpointHandler(endpoints.postOntologiesCandeletecardinalities, restService.canDeleteCardinalitiesFromClass), - SecuredEndpointHandler(endpoints.patchOntologiesCardinalities, restService.deleteCardinalitiesFromClass), - SecuredEndpointHandler(endpoints.putOntologiesGuiorder, restService.changeGuiOrder), - SecuredEndpointHandler(endpoints.getOntologiesClassesIris, restService.getClasses), - SecuredEndpointHandler(endpoints.getOntologiesCandeleteclass, restService.canDeleteClass), - SecuredEndpointHandler(endpoints.deleteOntologiesClasses, restService.deleteClass), - SecuredEndpointHandler(endpoints.deleteOntologiesComment, restService.deleteOntologyComment), - SecuredEndpointHandler(endpoints.postOntologiesProperties, restService.createProperty), - SecuredEndpointHandler(endpoints.putOntologiesProperties, restService.changePropertyLabelsOrComments), - SecuredEndpointHandler(endpoints.deletePropertiesComment, restService.deletePropertyComment), - SecuredEndpointHandler(endpoints.putOntologiesPropertiesGuielement, restService.changePropertyGuiElement), - SecuredEndpointHandler(endpoints.getOntologiesProperties, restService.getProperties), - SecuredEndpointHandler(endpoints.getOntologiesCandeleteproperty, restService.canDeleteProperty), - SecuredEndpointHandler(endpoints.deleteOntologiesProperty, restService.deleteProperty), - SecuredEndpointHandler(endpoints.postOntologies, restService.createOntology), - SecuredEndpointHandler(endpoints.getOntologiesCandeleteontology, restService.canDeleteOntology), - SecuredEndpointHandler(endpoints.deleteOntologies, restService.deleteOntology), - ).map(mapper.mapSecuredEndpointHandler) - - val allHandlers = publicHandlers ++ secureHandlers + val allHandlers = Seq( + endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), + endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjects), + endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), + endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), + endpoints.getOntologiesAllentities.serverLogic(restService.getOntologyEntities), + endpoints.postOntologiesClasses.serverLogic(restService.createClass), + endpoints.putOntologiesClasses.serverLogic(restService.changeClassLabelsOrComments), + endpoints.deleteOntologiesClassesComment.serverLogic(restService.deleteClassComment), + endpoints.postOntologiesCardinalities.serverLogic(restService.addCardinalities), + endpoints.getOntologiesCanreplacecardinalities.serverLogic(restService.canChangeCardinality), + endpoints.putOntologiesCardinalities.serverLogic(restService.replaceCardinalities), + endpoints.postOntologiesCandeletecardinalities.serverLogic(restService.canDeleteCardinalitiesFromClass), + endpoints.patchOntologiesCardinalities.serverLogic(restService.deleteCardinalitiesFromClass), + endpoints.putOntologiesGuiorder.serverLogic(restService.changeGuiOrder), + endpoints.getOntologiesClassesIris.serverLogic(restService.getClasses), + endpoints.getOntologiesCandeleteclass.serverLogic(restService.canDeleteClass), + endpoints.deleteOntologiesClasses.serverLogic(restService.deleteClass), + endpoints.deleteOntologiesComment.serverLogic(restService.deleteOntologyComment), + endpoints.postOntologiesProperties.serverLogic(restService.createProperty), + endpoints.putOntologiesProperties.serverLogic(restService.changePropertyLabelsOrComments), + endpoints.deletePropertiesComment.serverLogic(restService.deletePropertyComment), + endpoints.putOntologiesPropertiesGuielement.serverLogic(restService.changePropertyGuiElement), + endpoints.getOntologiesProperties.serverLogic(restService.getProperties), + endpoints.getOntologiesCandeleteproperty.serverLogic(restService.canDeleteProperty), + endpoints.deleteOntologiesProperty.serverLogic(restService.deleteProperty), + endpoints.postOntologies.serverLogic(restService.createOntology), + endpoints.getOntologiesCandeleteontology.serverLogic(restService.canDeleteOntology), + endpoints.deleteOntologies.serverLogic(restService.deleteOntology), + ) } - object OntologiesEndpointsHandler { val layer = ZLayer.derive[OntologiesEndpointsHandler] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala index 507df567cc0..54b4eb68a35 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala @@ -4,20 +4,17 @@ */ package org.knora.webapi.slice.resources.api -import zio.ZLayer -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler +import zio.* +import sttp.tapir.ztapir.* + import org.knora.webapi.slice.resources.api.service.MetadataRestService final case class MetadataServerEndpoints( private val endpoints: MetadataEndpoints, - private val resourcesRestService: MetadataRestService, - private val mapper: HandlerMapper, + private val restService: MetadataRestService, ) { - val allHandlers = - Seq(SecuredEndpointHandler(endpoints.getResourcesMetadata, resourcesRestService.getResourcesMetadata)) - .map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq(endpoints.getResourcesMetadata.serverLogic(restService.getResourcesMetadata)) } object MetadataServerEndpoints { val layer = ZLayer.derive[MetadataServerEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala index d6cd2b0883a..a2056d1491c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala @@ -5,27 +5,18 @@ package org.knora.webapi.slice.resources.api -import org.apache.pekko.http.scaladsl.server.Route -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.resources.api.model.ListResponseDto import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService final case class ResourceInfoRoutes( private val endpoints: ResourceInfoEndpoints, - private val interpreter: TapirToPekkoInterpreter, - private val mapper: HandlerMapper, private val resourceInfoService: ResourceInfoRestService, ) { - - val routes: Seq[Route] = - List(PublicEndpointHandler(endpoints.getResourcesInfo, resourceInfoService.findByProjectAndResourceClass)) - .map(mapper.mapPublicEndpointHandler) - .map(interpreter.toRoute(_)) + val allHandler = Seq(endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass)) } object ResourceInfoRoutes { val layer = ZLayer.derive[ResourceInfoRoutes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala index fc6d9946456..1fd20e324c4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala @@ -6,48 +6,31 @@ package org.knora.webapi.slice.resources.api import zio.* -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.resources.api.service.ResourcesRestService final class ResourcesEndpointsHandler( private val resourcesEndpoints: ResourcesEndpoints, private val resourcesRestService: ResourcesRestService, - private val mapper: HandlerMapper, ) { - val allHandlers = - Seq( - SecuredEndpointHandler(resourcesEndpoints.getResourcesCanDelete, resourcesRestService.canDeleteResource), - SecuredEndpointHandler(resourcesEndpoints.getResourcesGraph, resourcesRestService.getResourcesGraph), - SecuredEndpointHandler( - resourcesEndpoints.getResourcesIiifManifest, - resourcesRestService.getResourcesIiifManifest, - ), - SecuredEndpointHandler( - resourcesEndpoints.getResourcesPreview, - resourcesRestService.getResourcesPreview, - ), - SecuredEndpointHandler( - resourcesEndpoints.getResourcesProjectHistoryEvents, - resourcesRestService.getResourcesProjectHistoryEvents, - ), - SecuredEndpointHandler( - resourcesEndpoints.getResourcesHistoryEvents, - resourcesRestService.getResourcesHistoryEvents, - ), - SecuredEndpointHandler(resourcesEndpoints.getResourcesHistory, resourcesRestService.getResourceHistory), - SecuredEndpointHandler( - resourcesEndpoints.getResourcesParams, - resourcesRestService.searchResourcesByProjectAndClass, - ), - SecuredEndpointHandler(resourcesEndpoints.getResources, resourcesRestService.getResources), - SecuredEndpointHandler(resourcesEndpoints.getResourcesTei, resourcesRestService.getResourceAsTeiV2), - SecuredEndpointHandler(resourcesEndpoints.postResourcesErase, resourcesRestService.eraseResource), - SecuredEndpointHandler(resourcesEndpoints.postResourcesDelete, resourcesRestService.deleteResource), - SecuredEndpointHandler(resourcesEndpoints.postResources, resourcesRestService.createResource), - SecuredEndpointHandler(resourcesEndpoints.putResources, resourcesRestService.updateResourceMetadata), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq( + resourcesEndpoints.getResourcesCanDelete.serverLogic(resourcesRestService.canDeleteResource), + resourcesEndpoints.getResourcesGraph.serverLogic(resourcesRestService.getResourcesGraph), + resourcesEndpoints.getResourcesIiifManifest.serverLogic(resourcesRestService.getResourcesIiifManifest), + resourcesEndpoints.getResourcesPreview.serverLogic(resourcesRestService.getResourcesPreview), + resourcesEndpoints.getResourcesProjectHistoryEvents.serverLogic( + resourcesRestService.getResourcesProjectHistoryEvents, + ), + resourcesEndpoints.getResourcesHistoryEvents.serverLogic(resourcesRestService.getResourcesHistoryEvents), + resourcesEndpoints.getResourcesHistory.serverLogic(resourcesRestService.getResourceHistory), + resourcesEndpoints.getResourcesParams.serverLogic(resourcesRestService.searchResourcesByProjectAndClass), + resourcesEndpoints.getResources.serverLogic(resourcesRestService.getResources), + resourcesEndpoints.getResourcesTei.serverLogic(resourcesRestService.getResourceAsTeiV2), + resourcesEndpoints.postResourcesErase.serverLogic(resourcesRestService.eraseResource), + resourcesEndpoints.postResourcesDelete.serverLogic(resourcesRestService.deleteResource), + resourcesEndpoints.postResources.serverLogic(resourcesRestService.createResource), + resourcesEndpoints.putResources.serverLogic(resourcesRestService.updateResourceMetadata), + ) } object ResourcesEndpointsHandler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala index 0c932b8d8dc..3127f1a6b94 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala @@ -5,21 +5,17 @@ package org.knora.webapi.slice.resources.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.resources.api.service.StandoffRestService final case class StandoffEndpointsHandler( endpoints: StandoffEndpoints, - standoffRestService: StandoffRestService, - mapper: HandlerMapper, + restService: StandoffRestService, ) { - val allHandlers = Seq(SecuredEndpointHandler(endpoints.postMapping, standoffRestService.createMapping)) - .map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq(endpoints.postMapping.serverLogic(restService.createMapping)) } - object StandoffEndpointsHandler { val layer = ZLayer.derive[StandoffEndpointsHandler] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala index b1875964f06..170d6d704c0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala @@ -6,11 +6,11 @@ package org.knora.webapi.slice.resources.api import sttp.model.MediaType -import zio.ZLayer -import org.knora.webapi.slice.common.api.HandlerMapper +import zio.* +import sttp.tapir.ztapir.* + import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.resources.api.model.ValueUuid import org.knora.webapi.slice.resources.api.model.VersionDate import org.knora.webapi.slice.resources.api.service.ValuesRestService @@ -18,20 +18,17 @@ import org.knora.webapi.slice.resources.api.service.ValuesRestService final class ValuesEndpointsHandler( endpoints: ValuesEndpoints, valuesRestService: ValuesRestService, - mapper: HandlerMapper, ) { - val allHandlers = - Seq( - SecuredEndpointHandler(endpoints.getValue, valuesRestService.getValue), - SecuredEndpointHandler(endpoints.postValues, valuesRestService.createValue), - SecuredEndpointHandler(endpoints.putValues, valuesRestService.updateValue), - SecuredEndpointHandler(endpoints.deleteValues, valuesRestService.deleteValue), - SecuredEndpointHandler(endpoints.postValuesErase, valuesRestService.eraseValue), - SecuredEndpointHandler(endpoints.postValuesErasehistory, valuesRestService.eraseValueHistory), - ).map(mapper.mapSecuredEndpointHandler) + val allHandlers = Seq( + endpoints.getValue.serverLogic(valuesRestService.getValue), + endpoints.postValues.serverLogic(valuesRestService.createValue), + endpoints.putValues.serverLogic(valuesRestService.updateValue), + endpoints.deleteValues.serverLogic(valuesRestService.deleteValue), + endpoints.postValuesErase.serverLogic(valuesRestService.eraseValue), + endpoints.postValuesErasehistory.serverLogic(valuesRestService.eraseValueHistory), + ) } - object ValuesEndpointsHandler { val layer = ZLayer.derive[ValuesEndpointsHandler] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala index 2487a424c76..346614f21b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala @@ -12,41 +12,33 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse -import org.knora.webapi.slice.common.api.SecuredEndpointHandler -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset final case class SearchApiRoutes( - private val iriConverter: IriConverter, - private val mapper: HandlerMapper, private val searchEndpoints: SearchEndpoints, private val searchRestService: SearchRestService, - private val tapirToPekko: TapirToPekkoInterpreter, ) { - val routes: Seq[Route] = - Seq( - SecuredEndpointHandler(searchEndpoints.getFullTextSearch, searchRestService.fullTextSearch), - SecuredEndpointHandler(searchEndpoints.getFullTextSearchCount, searchRestService.fullTextSearchCount), - SecuredEndpointHandler(searchEndpoints.getSearchByLabel, searchRestService.searchResourcesByLabelV2), - SecuredEndpointHandler(searchEndpoints.getSearchByLabelCount, searchRestService.searchResourcesByLabelCountV2), - SecuredEndpointHandler(searchEndpoints.postGravsearch, searchRestService.gravsearch), - SecuredEndpointHandler(searchEndpoints.getGravsearch, searchRestService.gravsearch), - SecuredEndpointHandler(searchEndpoints.postGravsearchCount, searchRestService.gravsearchCount), - SecuredEndpointHandler(searchEndpoints.getGravsearchCount, searchRestService.gravsearchCount), - SecuredEndpointHandler(searchEndpoints.getSearchIncomingLinks, searchRestService.searchIncomingLinks), - SecuredEndpointHandler( - searchEndpoints.getSearchStillImageRepresentations, - searchRestService.getSearchStillImageRepresentations, - ), - SecuredEndpointHandler( - searchEndpoints.getSearchStillImageRepresentationsCount, - searchRestService.getSearchStillImageRepresentationsCount, - ), - SecuredEndpointHandler(searchEndpoints.getSearchIncomingRegions, searchRestService.searchIncomingRegions), - ).map(mapper.mapSecuredEndpointHandler).map(tapirToPekko.toRoute) + val allHandler = Seq( + searchEndpoints.getFullTextSearch.serverLogic(searchRestService.fullTextSearch), + searchEndpoints.getFullTextSearchCount.serverLogic(searchRestService.fullTextSearchCount), + searchEndpoints.getSearchByLabel.serverLogic(searchRestService.searchResourcesByLabelV2), + searchEndpoints.getSearchByLabelCount.serverLogic(searchRestService.searchResourcesByLabelCountV2), + searchEndpoints.postGravsearch.serverLogic(searchRestService.gravsearch), + searchEndpoints.getGravsearch.serverLogic(searchRestService.gravsearch), + searchEndpoints.postGravsearchCount.serverLogic(searchRestService.gravsearchCount), + searchEndpoints.getGravsearchCount.serverLogic(searchRestService.gravsearchCount), + searchEndpoints.getSearchIncomingLinks.serverLogic(searchRestService.searchIncomingLinks), + searchEndpoints.getSearchStillImageRepresentations.serverLogic( + searchRestService.getSearchStillImageRepresentations, + ), + searchEndpoints.getSearchStillImageRepresentationsCount.serverLogic( + searchRestService.getSearchStillImageRepresentationsCount, + ), + searchEndpoints.getSearchIncomingRegions.serverLogic(searchRestService.searchIncomingRegions), + ) } object SearchApiRoutes { val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala index 861f56d7d44..fb481a246a9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala @@ -5,13 +5,10 @@ package org.knora.webapi.slice.security.api import sttp.model.headers.CookieValueWithMeta -import zio.ZIO -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* import org.knora.webapi.config.AppConfig -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler -import org.knora.webapi.slice.common.api.SecuredEndpointHandler import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.CheckResponse import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginForm import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload @@ -19,19 +16,16 @@ import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutRespo import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse case class AuthenticationEndpointsV2Handler( - appConfig: AppConfig, - restService: AuthenticationRestService, - endpoints: AuthenticationEndpointsV2, - mapper: HandlerMapper, + private val restService: AuthenticationRestService, + private val endpoints: AuthenticationEndpointsV2, ) { - val allHandlers = List( - SecuredEndpointHandler(endpoints.getV2Authentication, _ => _ => ZIO.succeed(CheckResponse("credentials are OK"))), - ).map(mapper.mapSecuredEndpointHandler) ++ List( - PublicEndpointHandler(endpoints.postV2Authentication, restService.authenticate), - PublicEndpointHandler(endpoints.deleteV2Authentication, restService.logout), - PublicEndpointHandler(endpoints.getV2Login, restService.loginForm), - PublicEndpointHandler(endpoints.postV2Login, restService.authenticate), - ).map(mapper.mapPublicEndpointHandler) + val allHandlers = Seq( + endpoints.getV2Authentication.serverLogic(_ => _ => ZIO.succeed(CheckResponse("credentials are OK"))), + endpoints.postV2Authentication.zServerLogic(restService.authenticate), + endpoints.deleteV2Authentication.zServerLogic(restService.logout), + endpoints.getV2Login.zServerLogic(restService.loginForm), + endpoints.postV2Login.zServerLogic(restService.authenticate), + ) } object AuthenticationEndpointsV2Handler { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala index 46944ff5b26..4900bc3bbc9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala @@ -5,19 +5,14 @@ package org.knora.webapi.slice.shacl.api -import zio.ZLayer - -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.PublicEndpointHandler +import zio.* +import sttp.tapir.ztapir.* case class ShaclEndpointsHandler( private val shaclEndpoints: ShaclEndpoints, private val shaclApiService: ShaclApiService, - private val mapper: HandlerMapper, ) { - - val allHandlers = - List(PublicEndpointHandler(shaclEndpoints.validate, shaclApiService.validate)).map(mapper.mapPublicEndpointHandler) + val allHandlers = Seq(shaclEndpoints.validate.zServerLogic(shaclApiService.validate)) } object ShaclEndpointsHandler { From 702d825c5273f91522081c832f348c4795cf5ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 14:30:48 +0200 Subject: [PATCH 12/99] remove Public and SecuredHandlers --- AGENTS.md | 7 ++- CLAUDE.md | 8 +-- .../org/knora/webapi/core/LayersTest.scala | 1 - .../org/knora/webapi/core/HttpServer.scala | 21 ++++---- .../org/knora/webapi/core/LayersLive.scala | 23 ++++----- .../responders/admin/ListsResponder.scala | 14 ++--- .../org/knora/webapi/routing/ApiRoutes.scala | 51 ------------------- .../routing/CompleteApiServerEndpoints.scala | 49 ++++++++++++++++++ .../slice/admin/api/AdminApiModule.scala | 20 ++++---- .../admin/api/AdminApiServerEndpoints.scala | 38 +++++++------- ....scala => AdminListsServerEndpoints.scala} | 8 +-- ...ndler.scala => FilesServerEndpoints.scala} | 22 ++++---- .../admin/api/GroupsEndpointsHandler.scala | 36 ------------- .../admin/api/GroupsServerEndpoints.scala | 33 ++++++++++++ ...scala => MaintenanceServerEndpoints.scala} | 12 ++--- ...scala => PermissionsServerEndpoints.scala} | 12 ++--- ...=> ProjectsLegalInfoServerEndpoints.scala} | 12 ++--- ...er.scala => ProjectsServerEndpoints.scala} | 12 ++--- ...ndler.scala => StoreServerEndpoints.scala} | 8 +-- .../admin/api/UsersEndpointsHandler.scala | 12 ++--- .../slice/common/api/BaseEndpoints.scala | 34 +++++++++---- ....scala => ManagementServerEndpoints.scala} | 10 ++-- .../slice/lists/api/ListsApiModule.scala | 12 ++--- .../slice/lists/api/ListsApiV2Routes.scala | 21 -------- ...ler.scala => ListsV2ServerEndpoints.scala} | 8 +-- ....scala => OntologiesServerEndpoints.scala} | 8 +-- .../ontology/api/OntologyApiModule.scala | 11 ++-- .../ontology/api/OntologyApiRoutes.scala | 20 -------- .../resources/api/MetadataEndpoints.scala | 5 +- .../api/MetadataServerEndpoints.scala | 2 +- ...cala => ResourceInfoServerEndpoints.scala} | 8 +-- .../resources/api/ResourcesApiModule.scala | 39 +++++++------- .../resources/api/ResourcesApiRoutes.scala | 28 ---------- .../api/ResourcesApiServerEndpoints.scala | 22 ++++++++ ...r.scala => ResourcesServerEndpoints.scala} | 8 +-- ...er.scala => StandoffServerEndpoints.scala} | 12 ++--- ...dler.scala => ValuesServerEndpoints.scala} | 12 ++--- ...utes.scala => SearchServerEndpoints.scala} | 10 ++-- .../api/AuthenticationApiModule.scala | 9 ++-- .../api/AuthenticationApiRoutes.scala | 21 -------- ...la => AuthenticationServerEndpoints.scala} | 8 +-- .../slice/shacl/api/ShaclApiModule.scala | 18 +++---- .../slice/shacl/api/ShaclApiRoutes.scala | 22 -------- ...ndler.scala => ShaclServerEndpoints.scala} | 8 +-- 44 files changed, 324 insertions(+), 431 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{AdminListsEndpointsHandlers.scala => AdminListsServerEndpoints.scala} (93%) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{FilesEndpointsHandler.scala => FilesServerEndpoints.scala} (58%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{MaintenanceEndpointsHandlers.scala => MaintenanceServerEndpoints.scala} (65%) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{PermissionsEndpointsHandlers.scala => PermissionsServerEndpoints.scala} (92%) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{ProjectsLegalInfoEntpointsHandler.scala => ProjectsLegalInfoServerEndpoints.scala} (78%) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{ProjectsEndpointsHandler.scala => ProjectsServerEndpoints.scala} (94%) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{StoreEndpointsHandler.scala => StoreServerEndpoints.scala} (83%) rename webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/{ManagementRoutes.scala => ManagementServerEndpoints.scala} (78%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala rename webapi/src/main/scala/org/knora/webapi/slice/lists/api/{ListsEndpointsV2Handler.scala => ListsV2ServerEndpoints.scala} (78%) rename webapi/src/main/scala/org/knora/webapi/slice/ontology/api/{OntologiesEndpointsHandler.scala => OntologiesServerEndpoints.scala} (94%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiRoutes.scala rename webapi/src/main/scala/org/knora/webapi/slice/resources/api/{ResourceInfoRoutes.scala => ResourceInfoServerEndpoints.scala} (69%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala rename webapi/src/main/scala/org/knora/webapi/slice/resources/api/{ResourcesEndpointsHandler.scala => ResourcesServerEndpoints.scala} (92%) rename webapi/src/main/scala/org/knora/webapi/slice/resources/api/{StandoffEndpointsHandler.scala => StandoffServerEndpoints.scala} (52%) rename webapi/src/main/scala/org/knora/webapi/slice/resources/api/{ValuesEndpointsHandler.scala => ValuesServerEndpoints.scala} (80%) rename webapi/src/main/scala/org/knora/webapi/slice/search/api/{SearchApiRoutes.scala => SearchServerEndpoints.scala} (90%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala rename webapi/src/main/scala/org/knora/webapi/slice/security/api/{AuthenticationEndpointsV2Handler.scala => AuthenticationServerEndpoints.scala} (87%) delete mode 100644 webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala rename webapi/src/main/scala/org/knora/webapi/slice/shacl/api/{ShaclEndpointsHandler.scala => ShaclServerEndpoints.scala} (63%) diff --git a/AGENTS.md b/AGENTS.md index 61a872ad2f2..2699f9b29cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,8 +98,7 @@ Essential commands: ## API Structure - Endpoints: defined via Tapir in `*Endpoints.scala` -- Handlers: in `*EndpointsHandler.scala` -- Routes: in `*Routes.scala` and wired through `ApiRoutes.scala` +- ServerEndpoints: in `*ServerEndpoints.scala` - API areas: Admin API, API v2 (main), Management (health/metrics) - Auth: JWT; scoped authorization; session management @@ -107,8 +106,8 @@ Essential commands: - Add a new endpoint 1. Define endpoint in the appropriate `*Endpoints.scala` - 2. Implement handler in `*EndpointsHandler.scala` - 3. Add route in `*Routes.scala` and register in `ApiRoutes.scala` + 2. Connect endpoint definition with server logic in `*ServerEndpoints.scala` + 3. Register in `CompleteApiServerEndpoints.scala` 4. Add unit/integration tests mirroring the main structure - Code style and patterns - Use Scalafmt; prefer ZIO effects over side effects diff --git a/CLAUDE.md b/CLAUDE.md index b1af088e7b8..f134a69df41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,10 +143,10 @@ Each slice typically contains: ## Common Development Tasks ### Adding New Endpoints -1. Define endpoint in appropriate `*Endpoints.scala` -2. Implement handler in `*EndpointsHandler.scala` -3. Add route in `*Routes.scala` -4. Add to main router in `ApiRoutes.scala` +1. Define endpoint in the appropriate `*Endpoints.scala` +2. Connect endpoint definition with server logic in `*ServerEndpoints.scala` +3. Register in `CompleteApiServerEndpoints.scala` +4. Add unit/integration tests mirroring the main structure ### Code Style - Use Scalafmt for formatting diff --git a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala index 5622f6f6ef8..f2459e2c854 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala @@ -21,7 +21,6 @@ import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.api.* import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.resources.repo.service.ResourcesRepo -import org.knora.webapi.slice.search.api.SearchApiRoutes import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index b405da16ddc..46424c5b6c5 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -8,25 +8,24 @@ package org.knora.webapi.core import zio.* import zio.http.* import org.knora.webapi.config.AppConfig -import org.knora.webapi.routing.ApiRoutes +import org.knora.webapi.routing.CompleteApiServerEndpoints import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} import sttp.tapir.server.metrics.zio.ZioMetrics -import sttp.tapir.server.ziohttp.{ZioHttpInterpreter, ZioHttpServerOptions} +import sttp.tapir.server.ziohttp.{ZioHttpServerOptions, ZioHttpInterpreter} import sttp.tapir.ztapir.RIOMonadError object HttpServer { private def options: ZioHttpServerOptions[Any] = ZioHttpServerOptions.default - val layer: ZLayer[AppConfig & ApiRoutes, Nothing, Unit] = ZLayer.scoped(createServer.orDie) + val layer = ZLayer.scoped(createServer) - private def createServer: ZIO[ApiRoutes & AppConfig, Throwable, Unit] = - for { - config <- ZIO.service[AppConfig] - endpoints <- ZIO.service[ApiRoutes] - httpApp = ZioHttpInterpreter(options).toHttp(endpoints.endpoints) - _ <- Server.install(httpApp).provide(Server.defaultWithPort(config.knoraApi.externalPort)) - _ <- Console.printLine(s"Go to http://localhost:$config.knoraApi.externalPort/docs to open SwaggerUI") - } yield () + private def createServer = for { + config <- ZIO.service[AppConfig] + endpoints <- ZIO.service[CompleteApiServerEndpoints].map(_.serverEndpoints) + httpApp = ZioHttpInterpreter(options).toHttp(endpoints) + _ <- Server.install(httpApp).provide(Server.defaultWithPort(config.knoraApi.externalPort)) + _ <- Console.printLine(s"Go to http://localhost:$config.knoraApi.externalPort/docs to open SwaggerUI") + } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 123bcbd2e77..193bd84178d 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -6,7 +6,6 @@ package org.knora.webapi.core import zio.* - import org.knora.webapi.config.AppConfig import org.knora.webapi.config.DspIngestConfig import org.knora.webapi.config.Features @@ -44,7 +43,7 @@ import org.knora.webapi.slice.infrastructure.MetricsServer.MetricsServerEnv import org.knora.webapi.slice.infrastructure.OpenTelemetry import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementRestService -import org.knora.webapi.slice.infrastructure.api.ManagementRoutes +import org.knora.webapi.slice.infrastructure.api.ManagementServerEndpoints import org.knora.webapi.slice.lists.api.ListsApiModule import org.knora.webapi.slice.ontology.OntologyModule import org.knora.webapi.slice.ontology.OntologyModule.Provided @@ -54,16 +53,15 @@ import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.slice.resources.ResourcesModule import org.knora.webapi.slice.resources.api.ResourcesApiModule -import org.knora.webapi.slice.resources.api.ResourcesApiRoutes +import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.slice.resources.repo.service.ResourcesRepoLive -import org.knora.webapi.slice.search.api.SearchApiRoutes +import org.knora.webapi.slice.search.api.SearchServerEndpoints import org.knora.webapi.slice.search.api.SearchEndpoints import org.knora.webapi.slice.security.SecurityModule import org.knora.webapi.slice.security.api.AuthenticationApiModule import org.knora.webapi.slice.shacl.ShaclModule import org.knora.webapi.slice.shacl.api.ShaclApiModule -import org.knora.webapi.slice.shacl.api.ShaclApiRoutes import org.knora.webapi.slice.shacl.api.ShaclEndpoints import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive @@ -132,12 +130,11 @@ object LayersLive { self => */ type Environment = // format: off - ActorSystem & AdminApiEndpoints & AdminApiModule.Provided & AdminModule.Provided & ApiComplexV2JsonLdRequestParser & - ApiRoutes & + CompleteApiServerEndpoints & ApiV2Endpoints & AssetPermissionsResponder & AuthenticationApiModule.Provided & @@ -147,7 +144,6 @@ object LayersLive { self => ConstructResponseUtilV2 & OntologyModule.Provided & DefaultObjectAccessPermissionService & - HttpServer & IIIFRequestMessageHandler & InfrastructureModule.Provided & ListsApiModule.Provided & @@ -163,11 +159,11 @@ object LayersLive { self => ProjectImportService & RepositoryUpdater & ResourceUtilV2 & - ResourcesApiRoutes & + ResourcesApiServerEndpoints & ResourcesResponderV2 & ResourcesRepo & SecurityModule.Provided & - SearchApiRoutes & + SearchServerEndpoints & SearchResponderV2Module.Provided & SecurityModule.Provided & ShaclApiModule.Provided & @@ -189,7 +185,7 @@ object LayersLive { self => ]( AdminApiModule.layer, ApiComplexV2JsonLdRequestParser.layer, - ApiRoutes.layer, + CompleteApiServerEndpoints.layer, ApiV2Endpoints.layer, AssetPermissionsResponder.layer, AuthenticationApiModule.layer, @@ -197,7 +193,6 @@ object LayersLive { self => BaseEndpoints.layer, CardinalityHandler.layer, ConstructResponseUtilV2.layer, - HandlerMapper.layer, HttpServer.layer, IIIFRequestMessageHandlerLive.layer, KnoraResponseRenderer.layer, @@ -205,7 +200,7 @@ object LayersLive { self => ListsResponder.layer, ManagementEndpoints.layer, ManagementRestService.layer, - ManagementRoutes.layer, + ManagementServerEndpoints.layer, MessageRelayLive.layer, OntologyApiModule.layer, OntologyResponderV2.layer, @@ -221,7 +216,7 @@ object LayersLive { self => ResourcesModule.layer, ResourcesRepoLive.layer, ResourcesResponderV2.layer, - SearchApiRoutes.layer, + SearchServerEndpoints.layer, SearchEndpoints.layer, SearchResponderV2Module.layer, SecurityModule.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala index d5e59f73aad..ad2a24ae2c0 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponder.scala @@ -60,8 +60,8 @@ final case class ListsResponder( * (as lists can be very large), we only return the head of the list, i.e. the root node without * any children. * - * @param projectIri [[Some(ProjectIri)]] if the project for which lists are to be queried. - * [[None]] if all lists are to be queried. + * @param iriShortcode [[Some(ProjectIri|Shortcode)]] if the project for which lists are to be queried. + * [[None]] if all lists are to be queried. * @return a [[ListsGetResponseADM]]. */ def getLists(iriShortcode: Option[Either[ProjectIri, Shortcode]]): Task[ListsGetResponseADM] = @@ -147,17 +147,19 @@ final case class ListsResponder( def getNodeADM(childNode: ListChildNodeADM): Task[ListNodeGetResponseADM] = for { - maybeNodeInfo <- listNodeInfoGetADM(nodeIri = nodeIri) + maybeNodeInfo <- listNodeInfoGetADM(nodeIri.value) nodeInfo <- maybeNodeInfo match { case Some(childNodeInfo: ListChildNodeInfoADM) => ZIO.succeed(childNodeInfo) case _ => ZIO.fail(NotFoundException(s"Information not found for node '$nodeIri'")) } } yield ListNodeGetResponseADM(NodeADM(nodeInfo, childNode.children)) - ZIO.ifZIO(rootNodeByIriExists(nodeIri))( - listGetADM(nodeIri).someOrFail(NotFoundException(s"List '$nodeIri' not found")).map(ListGetResponseADM.apply), + ZIO.ifZIO(rootNodeByIriExists(nodeIri.value))( + listGetADM(nodeIri.value) + .someOrFail(NotFoundException(s"List '$nodeIri' not found")) + .map(ListGetResponseADM.apply), for { - maybeNode <- listNodeGetADM(nodeIri, shallow = true) + maybeNode <- listNodeGetADM(nodeIri.value, shallow = true) entireNode <- maybeNode match { // make sure that it is a child node diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala deleted file mode 100644 index 6eb39a30bc9..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.routing - -import zio.* -import org.knora.webapi.http.version.ServerVersion -import org.knora.webapi.routing -import org.knora.webapi.slice.admin.api.AdminApiRoutes -import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints -import org.knora.webapi.slice.infrastructure.api.ManagementRoutes -import org.knora.webapi.slice.lists.api.ListsApiV2Routes -import org.knora.webapi.slice.ontology.api.OntologiesApiRoutes -import org.knora.webapi.slice.resources.api.ResourceInfoRoutes -import org.knora.webapi.slice.resources.api.ResourcesApiRoutes -import org.knora.webapi.slice.search.api.SearchApiRoutes -import org.knora.webapi.slice.security.api.AuthenticationApiRoutes -import org.knora.webapi.slice.shacl.api.ShaclApiRoutes -import sttp.tapir.ztapir.ZServerEndpoint - -/** - * ALL requests go through each of the routes in ORDER. - * The FIRST matching route is used for handling a request. - */ -final case class ApiRoutes( - adminApiRoutes: AdminApiServerEndpoints, - authenticationApiRoutes: AuthenticationApiRoutes, - listsApiV2Routes: ListsApiV2Routes, - resourceInfoRoutes: ResourceInfoRoutes, - resourcesApiRoutes: ResourcesApiRoutes, - searchApiRoutes: SearchApiRoutes, - shaclApiRoutes: ShaclApiRoutes, - managementRoutes: ManagementRoutes, - ontologiesRoutes: OntologiesApiRoutes, -) { - val endpoints: List[ZServerEndpoint[Any, Any]] = - (adminApiRoutes.endpoints ++ - authenticationApiRoutes.routes ++ - listsApiV2Routes.routes ++ - managementRoutes.routes ++ - ontologiesRoutes.routes ++ - resourceInfoRoutes.routes ++ - resourcesApiRoutes.routes ++ - searchApiRoutes.routes ++ - shaclApiRoutes.routes) -} -object ApiRoutes { - val layer = ZLayer.derive[ApiRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala new file mode 100644 index 00000000000..aae4db249af --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.routing + +import zio.* +import org.knora.webapi.http.version.ServerVersion +import org.knora.webapi.routing +import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints +import org.knora.webapi.slice.infrastructure.api.ManagementServerEndpoints +import org.knora.webapi.slice.lists.api.ListsV2ServerEndpoints +import org.knora.webapi.slice.ontology.api.OntologiesServerEndpoints +import org.knora.webapi.slice.resources.api.ResourceInfoServerEndpoints +import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints +import org.knora.webapi.slice.search.api.SearchServerEndpoints +import org.knora.webapi.slice.security.api.AuthenticationServerEndpoints +import org.knora.webapi.slice.shacl.api.ShaclServerEndpoints +import sttp.tapir.ztapir.* + +/** + * ALL requests go through each of the routes in ORDER. + * The FIRST matching route is used for handling a request. + */ +final case class CompleteApiServerEndpoints( + adminApiServerEndpoints: AdminApiServerEndpoints, + authenticationServerEndpoints: AuthenticationServerEndpoints, + listsV2ServerEndpoints: ListsV2ServerEndpoints, + resourceInfoServerEndpoints: ResourceInfoServerEndpoints, + resourcesApiServerEndpoints: ResourcesApiServerEndpoints, + searchServerEndpoints: SearchServerEndpoints, + shaclServerEndpoints: ShaclServerEndpoints, + managementServerEndpoints: ManagementServerEndpoints, + ontologiesServerEndpoints: OntologiesServerEndpoints, +) { + val serverEndpoints = adminApiServerEndpoints.serverEndpoints ++ + authenticationServerEndpoints.serverEndpoints ++ + listsV2ServerEndpoints.serverEndpoints ++ + managementServerEndpoints.serverEndpoints ++ + ontologiesServerEndpoints.serverEndpoints ++ + resourceInfoServerEndpoints.serverEndpoints ++ + resourcesApiServerEndpoints.serverEndpoints ++ + searchServerEndpoints.serverEndpoints ++ + shaclServerEndpoints.serverEndpoints +} +object CompleteApiServerEndpoints { + val layer = ZLayer.derive[CompleteApiServerEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala index 6179ca11bcd..a104c191c77 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiModule.scala @@ -73,7 +73,7 @@ object AdminApiModule { self => type Provided = // format: off AdminApiEndpoints & - AdminApiRoutes & + AdminApiServerEndpoints & // the `*RestService`s are only exposed for the integration tests GroupRestService & PermissionRestService & @@ -87,29 +87,29 @@ object AdminApiModule { self => AdminApiServerEndpoints.layer, AdminListRestService.layer, AdminListsEndpoints.layer, - AdminListsEndpointsHandlers.layer, + AdminListsServerEndpoints.layer, FilesEndpoints.layer, - FilesEndpointsHandler.layer, + FilesServerEndpoints.layer, GroupRestService.layer, GroupsEndpoints.layer, - GroupsEndpointsHandler.layer, + GroupsServerEndpoints.layer, MaintenanceEndpoints.layer, - MaintenanceEndpointsHandlers.layer, MaintenanceRestService.layer, + MaintenanceServerEndpoints.layer, PermissionRestService.layer, PermissionsEndpoints.layer, - PermissionsEndpointsHandlers.layer, + PermissionsServerEndpoints.layer, ProjectRestService.layer, ProjectsEndpoints.layer, - ProjectsEndpointsHandler.layer, ProjectsLegalInfoEndpoints.layer, - ProjectsLegalInfoEndpointsHandler.layer, ProjectsLegalInfoRestService.layer, + ProjectsLegalInfoServerEndpoints.layer, + ProjectsServerEndpoints.layer, StoreEndpoints.layer, - StoreEndpointsHandler.layer, StoreRestService.layer, + StoreServerEndpoints.layer, UserRestService.layer, UsersEndpoints.layer, - UsersEndpointsHandler.layer, + UsersServerEndpoints.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index 2da58341654..3d44ea49957 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -11,27 +11,27 @@ import zio.* import sttp.tapir.ztapir.ZServerEndpoint final case class AdminApiServerEndpoints( - private val adminLists: AdminListsEndpointsHandlers, - private val filesEndpoints: FilesEndpointsHandler, - private val groups: GroupsEndpointsHandler, - private val maintenance: MaintenanceEndpointsHandlers, - private val permissions: PermissionsEndpointsHandlers, - private val project: ProjectsEndpointsHandler, - private val projectLegalInfo: ProjectsLegalInfoEndpointsHandler, - private val storeEndpoints: StoreEndpointsHandler, - private val users: UsersEndpointsHandler, + private val adminListsServerEndpoints: AdminListsServerEndpoints, + private val filesServerEndpoints: FilesServerEndpoints, + private val groupsServerEndpoints: GroupsServerEndpoints, + private val maintenanceServerEndpoints: MaintenanceServerEndpoints, + private val permissionsServerEndpoints: PermissionsServerEndpoints, + private val projectsServerEndpoints: ProjectsServerEndpoints, + private val projectsLegalInfoServerEndpoints: ProjectsLegalInfoServerEndpoints, + private val storeServerEndpoints: StoreServerEndpoints, + private val usersServerEndpoints: UsersServerEndpoints, ) { - private val endpoints: List[ZServerEndpoint[Any, Any]] = - filesEndpoints.allHandlers ++ - groups.allHandlers ++ - adminLists.allHandlers ++ - maintenance.allHandlers ++ - permissions.allHanders ++ - projectLegalInfo.allHandlers ++ - project.allHanders ++ - storeEndpoints.allHandlers ++ - users.allHanders + val serverEndpoints = + filesServerEndpoints.serverEndpoints ++ + groupsServerEndpoints.serverEndpoints ++ + adminListsServerEndpoints.serverEndpoints ++ + maintenanceServerEndpoints.serverEndpoints ++ + permissionsServerEndpoints.serverEndpoints ++ + projectsLegalInfoServerEndpoints.serverEndpoints ++ + projectsServerEndpoints.serverEndpoints ++ + storeServerEndpoints.serverEndpoints ++ + usersServerEndpoints.serverEndpoints } object AdminApiServerEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala similarity index 93% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala index ebb35a727c2..b42612bca34 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala @@ -24,11 +24,11 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.ListProperties.ListIri import org.knora.webapi.slice.admin.domain.model.User -final case class AdminListsEndpointsHandlers( +final case class AdminListsServerEndpoints( private val adminListsEndpoints: AdminListsEndpoints, private val restService: AdminListRestService, ) { - val allHandlers: Seq[ZServerEndpoint[Any, Any]] = Seq( + val serverEndpoints = Seq( adminListsEndpoints.getListsQueryByProjectIriOption.zServerLogic(restService.getLists), adminListsEndpoints.getListsByIri.zServerLogic(restService.listGetRequestADM), adminListsEndpoints.getListsByIriInfo.zServerLogic(restService.listNodeInfoGetRequestADM), @@ -47,6 +47,6 @@ final case class AdminListsEndpointsHandlers( ) } -object AdminListsEndpointsHandlers { - val layer = ZLayer.derive[AdminListsEndpointsHandlers] +object AdminListsServerEndpoints { + val layer = ZLayer.derive[AdminListsServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala similarity index 58% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala index 908355655ff..e62703d9c7c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala @@ -15,19 +15,17 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.domain.SparqlEncodedString -final case class FilesEndpointsHandler( - filesEndpoints: FilesEndpoints, - assetPermissionsResponder: AssetPermissionsResponder, +final case class FilesServerEndpoints( + private val filesEndpoints: FilesEndpoints, + private val assetPermissionsResponder: AssetPermissionsResponder, ) { - - val allHandlers: List[ZServerEndpoint[Any, Any]] = - List( - filesEndpoints.getAdminFilesShortcodeFileIri.serverLogic( - assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings, - ), - ) + val serverEndpoints = Seq( + filesEndpoints.getAdminFilesShortcodeFileIri.serverLogic( + assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings, + ), + ) } -object FilesEndpointsHandler { - val layer = ZLayer.derive[FilesEndpointsHandler] +object FilesServerEndpoints { + val layer = ZLayer.derive[FilesServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala deleted file mode 100644 index 481bedc8d0e..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsEndpointsHandler.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.admin.api - -import zio.* -import sttp.tapir.ztapir.* - -import org.knora.webapi.messages.admin.responder.groupsmessages.GroupGetResponseADM -import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest -import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest -import org.knora.webapi.slice.admin.api.service.GroupRestService -import org.knora.webapi.slice.admin.domain.model.GroupIri - -case class GroupsEndpointsHandler( - endpoints: GroupsEndpoints, - restService: GroupRestService, -) { - - val allHandlers: ZServerEndpoint[Any, Any] = - Seq( - endpoints.getGroups.zServerLogic(_ => restService.getGroups), - endpoints.getGroupByIri.zServerLogic(restService.getGroupByIri), - endpoints.getGroupMembers.serverLogic(restService.getGroupMembers), - endpoints.postGroup.serverLogic(restService.postGroup), - endpoints.putGroup.serverLogic(restService.putGroup), - endpoints.putGroupStatus.serverLogic(restService.putGroupStatus), - endpoints.deleteGroup.serverLogic(restService.deleteGroup), - ) -} - -object GroupsEndpointsHandler { - val layer = ZLayer.derive[GroupsEndpointsHandler] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala new file mode 100644 index 00000000000..aa33f6e652e --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala @@ -0,0 +1,33 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.admin.api + +import zio.* +import sttp.tapir.ztapir.* + +import org.knora.webapi.messages.admin.responder.groupsmessages.GroupGetResponseADM +import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest +import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest +import org.knora.webapi.slice.admin.api.service.GroupRestService +import org.knora.webapi.slice.admin.domain.model.GroupIri + +case class GroupsServerEndpoints( + private val endpoints: GroupsEndpoints, + private val restService: GroupRestService, +) { + val serverEndpoints = Seq( + endpoints.getGroups.zServerLogic(_ => restService.getGroups), + endpoints.getGroupByIri.zServerLogic(restService.getGroupByIri), + endpoints.getGroupMembers.serverLogic(restService.getGroupMembers), + endpoints.postGroup.serverLogic(restService.postGroup), + endpoints.putGroup.serverLogic(restService.putGroup), + endpoints.putGroupStatus.serverLogic(restService.putGroupStatus), + endpoints.deleteGroup.serverLogic(restService.deleteGroup), + ) +} +object GroupsServerEndpoints { + val layer = ZLayer.derive[GroupsServerEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala similarity index 65% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala index 53ab37fbcb2..a3848a649b8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala @@ -13,14 +13,14 @@ import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.domain.model.User -final case class MaintenanceEndpointsHandlers( - endpoints: MaintenanceEndpoints, - restService: MaintenanceRestService, +final case class MaintenanceServerEndpoints( + private val endpoints: MaintenanceEndpoints, + private val restService: MaintenanceRestService, ) { - val allHandlers: ZServerEndpoint[Any, Any] = Seq( + val serverEndpoints = Seq( endpoints.postMaintenance.serverLogic(restService.executeMaintenanceAction), ) } -object MaintenanceEndpointsHandlers { - val layer = ZLayer.derive[MaintenanceEndpointsHandlers] +object MaintenanceServerEndpoints { + val layer = ZLayer.derive[MaintenanceServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala similarity index 92% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala index f2f9910ce70..69e4902c5b2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala @@ -29,12 +29,12 @@ import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.PermissionIri -final case class PermissionsEndpointsHandlers( - permissionsEndpoints: PermissionsEndpoints, - restService: PermissionRestService, +final case class PermissionsServerEndpoints( + private val permissionsEndpoints: PermissionsEndpoints, + private val restService: PermissionRestService, ) { - val allHanders = Seq( + val serverEndpoints = Seq( permissionsEndpoints.postPermissionsAp.serverLogic(restService.createAdministrativePermission), permissionsEndpoints.getPermissionsApByProjectIri.serverLogic(restService.getPermissionsApByProjectIri), permissionsEndpoints.getPermissionsApByProjectAndGroupIri.serverLogic( @@ -51,6 +51,6 @@ final case class PermissionsEndpointsHandlers( permissionsEndpoints.postPermissionsDoap.serverLogic(restService.createDefaultObjectAccessPermission), ) } -object PermissionsEndpointsHandlers { - val layer = ZLayer.derive[PermissionsEndpointsHandlers] +object PermissionsServerEndpoints { + val layer = ZLayer.derive[PermissionsServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala similarity index 78% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala index 3e17f920c93..4ef9709bd0d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoEntpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala @@ -9,11 +9,11 @@ import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.api.service.ProjectsLegalInfoRestService -final class ProjectsLegalInfoEndpointsHandler( - endpoints: ProjectsLegalInfoEndpoints, - restService: ProjectsLegalInfoRestService, +final class ProjectsLegalInfoServerEndpoints( + private val endpoints: ProjectsLegalInfoEndpoints, + private val restService: ProjectsLegalInfoRestService, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( endpoints.getProjectLicenses.zServerLogic(restService.findLicenses), endpoints.getProjectLicensesIri.zServerLogic(restService.findAvailableLicenseByIdAndShortcode), endpoints.getProjectAuthorships.serverLogic(restService.findAuthorships), @@ -25,6 +25,6 @@ final class ProjectsLegalInfoEndpointsHandler( ) } -object ProjectsLegalInfoEndpointsHandler { - val layer = ZLayer.derive[ProjectsLegalInfoEndpointsHandler] +object ProjectsLegalInfoServerEndpoints { + val layer = ZLayer.derive[ProjectsLegalInfoServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala similarity index 94% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala index af5d211ba8e..91de1d0d5c6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala @@ -23,9 +23,9 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.User -final case class ProjectsEndpointsHandler( - projectsEndpoints: ProjectsEndpoints, - restService: ProjectRestService, +final case class ProjectsServerEndpoints( + private val projectsEndpoints: ProjectsEndpoints, + private val restService: ProjectRestService, ) { private val getAdminProjectsByIriAllDataHandler = @@ -40,7 +40,7 @@ final case class ProjectsEndpointsHandler( }, ) - val allHanders = Seq( + val serverEndpoints = Seq( projectsEndpoints.Public.getAdminProjects.zServerLogic(restService.listAllProjects), projectsEndpoints.Public.getAdminProjectsKeywords.zServerLogic(restService.listAllKeywords), projectsEndpoints.Public.getAdminProjectsByProjectIri.zServerLogic(restService.findById), @@ -80,6 +80,6 @@ final case class ProjectsEndpointsHandler( ) } -object ProjectsEndpointsHandler { - val layer = ZLayer.derive[ProjectsEndpointsHandler] +object ProjectsServerEndpoints { + val layer = ZLayer.derive[ProjectsServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala similarity index 83% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala index 7fe88092e34..3f686ca9c57 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala @@ -12,18 +12,18 @@ import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.slice.admin.api.service.StoreRestService -final case class StoreEndpointsHandler( +final case class StoreServerEndpoints( private val appConfig: AppConfig, private val endpoints: StoreEndpoints, private val restService: StoreRestService, ) { - val allHandlers = + val serverEndpoints = if (appConfig.allowReloadOverHttp) Seq(endpoints.postStoreResetTriplestoreContent.zServerLogic(restService.resetTriplestoreContent)) else Seq.empty } -object StoreEndpointsHandler { - val layer = ZLayer.derive[StoreEndpointsHandler] +object StoreServerEndpoints { + val layer = ZLayer.derive[StoreServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala index cf3115c6164..09825e5fd1f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala @@ -21,12 +21,12 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.UserIri import org.knora.webapi.slice.admin.domain.model.Username -case class UsersEndpointsHandler( - usersEndpoints: UsersEndpoints, - restService: UserRestService, +case class UsersServerEndpoints( + private val usersEndpoints: UsersEndpoints, + private val restService: UserRestService, ) { - val allHanders = Seq( + val serverEndpoints = Seq( usersEndpoints.get.usersByIriProjectMemberShips.zServerLogic(restService.getProjectMemberShipsByUserIri), usersEndpoints.get.usersByIriProjectAdminMemberShips.zServerLogic(restService.getProjectAdminMemberShipsByUserIri), usersEndpoints.get.usersByIriGroupMemberships.zServerLogic(restService.getGroupMemberShipsByIri), @@ -49,6 +49,6 @@ case class UsersEndpointsHandler( ) } -object UsersEndpointsHandler { - val layer = ZLayer.derive[UsersEndpointsHandler] +object UsersServerEndpoints { + val layer = ZLayer.derive[UsersServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala index 354db9b12cd..b647ac6eae5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -8,24 +8,23 @@ package org.knora.webapi.slice.common.api import sttp.model.StatusCode import sttp.model.headers.WWWAuthenticateChallenge import sttp.tapir.ztapir.* -import sttp.tapir.{EndpointOutput, PublicEndpoint, Validator} +import sttp.tapir.{PublicEndpoint, EndpointOutput, Validator} import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.model.UsernamePassword -import zio.ZIO -import zio.ZLayer +import zio.* import scala.concurrent.Future - import dsp.errors.* import org.knora.webapi.messages.util.KnoraSystemInstances.Users.AnonymousUser import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.security.Authenticator +import sttp.tapir.Endpoint final case class BaseEndpoints(authenticator: Authenticator) { - private val errorOutputs = + private val errorOutputs: EndpointOutput.OneOf[Throwable, Throwable] = oneOf[Throwable]( // default oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), @@ -44,23 +43,40 @@ final case class BaseEndpoints(authenticator: Authenticator) { oneOfVariant[ForbiddenException](statusCode(StatusCode.Forbidden).and(jsonBody[ForbiddenException])), ) - val publicEndpoint = endpoint.errorOut(errorOutputs) + val publicEndpoint: Endpoint[Unit, Unit, Throwable, Unit, Any] = endpoint.errorOut(errorOutputs) - private val endpointWithBearerCookieBasicAuthOptional = + private val endpointWithBearerCookieBasicAuthOptional + : Endpoint[(Option[String], Option[String], Option[UsernamePassword]), Unit, Throwable, Unit, Any] = endpoint .errorOut(errorOutputs) .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) .securityIn(cookie[Option[String]](authenticator.calculateCookieName())) .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic("realm"))) - val securedEndpoint = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { + val securedEndpoint: ZPartialServerEndpoint[ + Nothing, + (Option[String], Option[String], Option[UsernamePassword]), + User, + Unit, + Throwable, + Unit, + Any, + ] = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) case (_, Some(cookie), _) => authenticateJwt(cookie) case (_, _, Some(basic)) => authenticateBasic(basic) case _ => ZIO.fail(BadCredentialsException("No credentials provided.")) } - val withUserEndpoint = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { + val withUserEndpoint: ZPartialServerEndpoint[ + Nothing, + (Option[String], Option[String], Option[UsernamePassword]), + User, + Unit, + Throwable, + Unit, + Any, + ] = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) case (_, Some(cookie), _) => authenticateJwt(cookie) case (_, _, Some(basic)) => authenticateBasic(basic) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala similarity index 78% rename from webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala rename to webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala index b19ee76804a..196043040d1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala @@ -8,18 +8,16 @@ package org.knora.webapi.slice.infrastructure.api import sttp.tapir.ztapir.* import zio.* -final case class ManagementRoutes( +final case class ManagementServerEndpoints( private val endpoint: ManagementEndpoints, private val restService: ManagementRestService, ) { - - val allHandlers = Seq( + val serverEndpoints = Seq( endpoint.getVersion.zServerLogic(_ => ZIO.succeed(VersionResponse.current)), endpoint.getHealth.zServerLogic(_ => restService.healthCheck), endpoint.postStartCompaction.serverLogic(restService.startCompaction), ) - } -object ManagementRoutes { - val layer = ZLayer.derive[ManagementRoutes] +object ManagementServerEndpoints { + val layer = ZLayer.derive[ManagementServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiModule.scala index b158884dadd..25aede1950f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiModule.scala @@ -4,15 +4,14 @@ */ package org.knora.webapi.slice.lists.api + import zio.URLayer import zio.ZLayer import org.knora.webapi.config.AppConfig import org.knora.webapi.responders.admin.ListsResponder import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.lists.api.service.ListsV2RestService object ListsApiModule { self => @@ -20,18 +19,15 @@ object ListsApiModule { self => // format: off AppConfig & BaseEndpoints & - HandlerMapper & KnoraResponseRenderer & - ListsResponder & - TapirToPekkoInterpreter + ListsResponder // format: on - type Provided = ListsApiV2Routes & ListsEndpointsV2 + type Provided = ListsV2ServerEndpoints & ListsEndpointsV2 val layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( - ListsApiV2Routes.layer, + ListsV2ServerEndpoints.layer, ListsEndpointsV2.layer, - ListsEndpointsV2Handler.layer, ListsV2RestService.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala deleted file mode 100644 index 92cf82f9a9c..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsApiV2Routes.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.lists.api - -import org.apache.pekko.http.scaladsl.server.Route -import zio.ZLayer - -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - -final case class ListsApiV2Routes( - private val listsEndpointsV2: ListsEndpointsV2Handler, - private val tapirToPekko: TapirToPekkoInterpreter, -) { - val routes: Seq[Route] = listsEndpointsV2.allHandlers.map(tapirToPekko.toRoute(_)) -} -object ListsApiV2Routes { - val layer = ZLayer.derive[ListsApiV2Routes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala similarity index 78% rename from webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala index ca4643e8525..52ceda73565 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala @@ -10,16 +10,16 @@ import sttp.tapir.ztapir.* import org.knora.webapi.slice.lists.api.service.ListsV2RestService -final case class ListsEndpointsV2Handler( +final case class ListsV2ServerEndpoints( private val endpoints: ListsEndpointsV2, private val restService: ListsV2RestService, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( endpoints.getV2Lists.serverLogic(restService.getList), endpoints.getV2Node.serverLogic(restService.getNode), ) } -object ListsEndpointsV2Handler { - val layer = ZLayer.derive[ListsEndpointsV2Handler] +object ListsV2ServerEndpoints { + val layer = ZLayer.derive[ListsV2ServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala similarity index 94% rename from webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 03fd8b3db0d..b529efc2235 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -11,12 +11,12 @@ import sttp.tapir.ztapir.* import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.ontology.api.service.OntologiesRestService -final class OntologiesEndpointsHandler( +final class OntologiesServerEndpoints( private val endpoints: OntologiesEndpoints, private val restService: OntologiesRestService, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjects), endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), @@ -47,6 +47,6 @@ final class OntologiesEndpointsHandler( endpoints.deleteOntologies.serverLogic(restService.deleteOntology), ) } -object OntologiesEndpointsHandler { - val layer = ZLayer.derive[OntologiesEndpointsHandler] +object OntologiesServerEndpoints { + val layer = ZLayer.derive[OntologiesServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala index 2746a654436..5da17f6930e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiModule.scala @@ -11,9 +11,7 @@ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.responders.v2.OntologyResponderV2 import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.ontology.api.service.OntologiesRestService import org.knora.webapi.slice.ontology.api.service.RestCardinalityService @@ -29,25 +27,22 @@ object OntologyApiModule { self => AppConfig & BaseEndpoints & CardinalityService & - HandlerMapper & IriConverter & KnoraResponseRenderer & OntologyCacheHelpers & OntologyRepo & OntologyResponderV2 & - StringFormatter & - TapirToPekkoInterpreter + StringFormatter // format: on - type Provided = OntologiesApiRoutes & OntologiesEndpoints & OntologyV2RequestParser + type Provided = OntologiesServerEndpoints & OntologiesEndpoints & OntologyV2RequestParser val layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( RestCardinalityService.layer, OntologiesRestService.layer, - OntologiesEndpointsHandler.layer, + OntologiesServerEndpoints.layer, OntologiesEndpoints.layer, OntologyV2RequestParser.layer, - OntologiesApiRoutes.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiRoutes.scala deleted file mode 100644 index ed5d7e2e441..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologyApiRoutes.scala +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.ontology.api -import zio.ZLayer - -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - -final case class OntologiesApiRoutes( - private val handler: OntologiesEndpointsHandler, - private val tapirToPekko: TapirToPekkoInterpreter, -) { - val routes = handler.allHandlers.map(tapirToPekko.toRoute(_)) -} - -object OntologiesApiRoutes { - val layer = ZLayer.derive[OntologiesApiRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala index ed017e6f92b..e39df136596 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala @@ -16,7 +16,6 @@ import zio.json.JsonCodec import java.time.Instant import scala.concurrent.Future -import dsp.errors.RequestRejectedException import org.knora.webapi.slice.admin.api.AdminPathVariables import org.knora.webapi.slice.admin.api.AdminPathVariables.projectShortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode @@ -106,9 +105,7 @@ final case class MetadataEndpoints(private val baseEndpoints: BaseEndpoints) { "This endpoint is only available for system and project admins.", ) - val endpoints: Seq[AnyEndpoint] = (Seq( - getResourcesMetadata, - ).map(_.endpoint)).map(_.tag("V2 Metadata")) + val endpoints: Seq[AnyEndpoint] = Seq(getResourcesMetadata).map(_.endpoint).map(_.tag("V2 Metadata")) } object MetadataEndpoints { val layer = ZLayer.derive[MetadataEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala index 54b4eb68a35..b3b89b1a303 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala @@ -14,7 +14,7 @@ final case class MetadataServerEndpoints( private val endpoints: MetadataEndpoints, private val restService: MetadataRestService, ) { - val allHandlers = Seq(endpoints.getResourcesMetadata.serverLogic(restService.getResourcesMetadata)) + val serverEndpoints = Seq(endpoints.getResourcesMetadata.serverLogic(restService.getResourcesMetadata)) } object MetadataServerEndpoints { val layer = ZLayer.derive[MetadataServerEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala similarity index 69% rename from webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala rename to webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala index a2056d1491c..80c01e417f5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala @@ -12,12 +12,12 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.resources.api.model.ListResponseDto import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService -final case class ResourceInfoRoutes( +final case class ResourceInfoServerEndpoints( private val endpoints: ResourceInfoEndpoints, private val resourceInfoService: ResourceInfoRestService, ) { - val allHandler = Seq(endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass)) + val serverEndpoints = Seq(endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass)) } -object ResourceInfoRoutes { - val layer = ZLayer.derive[ResourceInfoRoutes] +object ResourceInfoServerEndpoints { + val layer = ZLayer.derive[ResourceInfoServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala index fc4736e1a35..9bc37507a1e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala @@ -17,9 +17,7 @@ import org.knora.webapi.slice.admin.domain.service.KnoraProjectService import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.infrastructure.CsvService import org.knora.webapi.slice.resources.api.service.MetadataRestService @@ -38,7 +36,6 @@ object ResourcesApiModule { self => BaseEndpoints & CsvService & GraphRoute & - HandlerMapper & IriConverter & KnoraProjectService & KnoraResponseRenderer & @@ -47,30 +44,36 @@ object ResourcesApiModule { self => ResourcesResponderV2 & SearchResponderV2 & StandoffResponderV2 & - TapirToPekkoInterpreter & ValuesResponderV2 //format: on - type Provided = MetadataEndpoints & ResourceInfoEndpoints & ResourceInfoRoutes & ResourcesApiRoutes & - ResourcesEndpoints & StandoffEndpoints & ValuesEndpoints + type Provided = + // format: off + MetadataEndpoints & + ResourceInfoEndpoints & + ResourceInfoServerEndpoints & + ResourcesApiServerEndpoints & + ResourcesEndpoints & + StandoffEndpoints & + ValuesEndpoints + //format: on def layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( - ValuesEndpointsHandler.layer, - ValuesEndpoints.layer, - ValuesRestService.layer, - ResourcesEndpoints.layer, - ResourcesEndpointsHandler.layer, - ResourcesRestService.layer, - ResourcesApiRoutes.layer, MetadataEndpoints.layer, - MetadataServerEndpoints.layer, MetadataRestService.layer, + MetadataServerEndpoints.layer, + ResourceInfoEndpoints.layer, + ResourceInfoRestService.layer, + ResourceInfoServerEndpoints.layer, + ResourcesEndpoints.layer, + ResourcesRestService.layer, + ResourcesServerEndpoints.layer, StandoffEndpoints.layer, - StandoffEndpointsHandler.layer, StandoffRestService.layer, - ResourceInfoRestService.layer, - ResourceInfoEndpoints.layer, - ResourceInfoRoutes.layer, + StandoffServerEndpoints.layer, + ValuesEndpoints.layer, + ValuesRestService.layer, + ValuesServerEndpoints.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala deleted file mode 100644 index 825dfdb399b..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiRoutes.scala +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.resources.api -import org.apache.pekko.http.scaladsl.server.Route -import zio.ZLayer - -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - -final case class ResourcesApiRoutes( - private val metadataEndpoints: MetadataServerEndpoints, - private val resourcesEndpoints: ResourcesEndpointsHandler, - private val standoffEndpoints: StandoffEndpointsHandler, - private val valuesEndpoints: ValuesEndpointsHandler, - private val tapirToPekko: TapirToPekkoInterpreter, -) { - - private val handlers = - valuesEndpoints.allHandlers ++ resourcesEndpoints.allHandlers ++ standoffEndpoints.allHandlers ++ metadataEndpoints.allHandlers - - val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) -} - -object ResourcesApiRoutes { - val layer = ZLayer.derive[ResourcesApiRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala new file mode 100644 index 00000000000..f335aa3ed8b --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala @@ -0,0 +1,22 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.resources.api +import zio.ZLayer + +final case class ResourcesApiServerEndpoints( + private val metadataServerEndpoints: MetadataServerEndpoints, + private val resourcesServerEndpoints: ResourcesServerEndpoints, + private val standoffServerEndpoints: StandoffServerEndpoints, + private val valuesServerEndpoints: ValuesServerEndpoints, +) { + val serverEndpoints = valuesServerEndpoints.serverEndpoints ++ + resourcesServerEndpoints.serverEndpoints ++ + standoffServerEndpoints.serverEndpoints ++ + metadataServerEndpoints.serverEndpoints +} +object ResourcesApiServerEndpoints { + val layer = ZLayer.derive[ResourcesApiServerEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala similarity index 92% rename from webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala index 1fd20e324c4..fda370ffff4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala @@ -8,12 +8,12 @@ import zio.* import org.knora.webapi.slice.resources.api.service.ResourcesRestService -final class ResourcesEndpointsHandler( +final class ResourcesServerEndpoints( private val resourcesEndpoints: ResourcesEndpoints, private val resourcesRestService: ResourcesRestService, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( resourcesEndpoints.getResourcesCanDelete.serverLogic(resourcesRestService.canDeleteResource), resourcesEndpoints.getResourcesGraph.serverLogic(resourcesRestService.getResourcesGraph), resourcesEndpoints.getResourcesIiifManifest.serverLogic(resourcesRestService.getResourcesIiifManifest), @@ -33,6 +33,6 @@ final class ResourcesEndpointsHandler( ) } -object ResourcesEndpointsHandler { - val layer = ZLayer.derive[ResourcesEndpointsHandler] +object ResourcesServerEndpoints { + val layer = ZLayer.derive[ResourcesServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala similarity index 52% rename from webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala index 3127f1a6b94..11da5d2d804 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala @@ -10,12 +10,12 @@ import sttp.tapir.ztapir.* import org.knora.webapi.slice.resources.api.service.StandoffRestService -final case class StandoffEndpointsHandler( - endpoints: StandoffEndpoints, - restService: StandoffRestService, +final case class StandoffServerEndpoints( + private val endpoints: StandoffEndpoints, + private val restService: StandoffRestService, ) { - val allHandlers = Seq(endpoints.postMapping.serverLogic(restService.createMapping)) + val serverEndpoints = Seq(endpoints.postMapping.serverLogic(restService.createMapping)) } -object StandoffEndpointsHandler { - val layer = ZLayer.derive[StandoffEndpointsHandler] +object StandoffServerEndpoints { + val layer = ZLayer.derive[StandoffServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala similarity index 80% rename from webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala index 170d6d704c0..6b985d58639 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala @@ -15,12 +15,12 @@ import org.knora.webapi.slice.resources.api.model.ValueUuid import org.knora.webapi.slice.resources.api.model.VersionDate import org.knora.webapi.slice.resources.api.service.ValuesRestService -final class ValuesEndpointsHandler( - endpoints: ValuesEndpoints, - valuesRestService: ValuesRestService, +final class ValuesServerEndpoints( + private val endpoints: ValuesEndpoints, + private val valuesRestService: ValuesRestService, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( endpoints.getValue.serverLogic(valuesRestService.getValue), endpoints.postValues.serverLogic(valuesRestService.createValue), endpoints.putValues.serverLogic(valuesRestService.updateValue), @@ -29,6 +29,6 @@ final class ValuesEndpointsHandler( endpoints.postValuesErasehistory.serverLogic(valuesRestService.eraseValueHistory), ) } -object ValuesEndpointsHandler { - val layer = ZLayer.derive[ValuesEndpointsHandler] +object ValuesServerEndpoints { + val layer = ZLayer.derive[ValuesServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala similarity index 90% rename from webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala rename to webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala index 346614f21b4..6898de84db4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala @@ -4,24 +4,22 @@ */ package org.knora.webapi.slice.search.api -import org.apache.pekko.http.scaladsl.server.Route import sttp.model.MediaType import zio.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset -final case class SearchApiRoutes( +final case class SearchServerEndpoints( private val searchEndpoints: SearchEndpoints, private val searchRestService: SearchRestService, ) { - val allHandler = Seq( + val serverEndpoints = Seq( searchEndpoints.getFullTextSearch.serverLogic(searchRestService.fullTextSearch), searchEndpoints.getFullTextSearchCount.serverLogic(searchRestService.fullTextSearchCount), searchEndpoints.getSearchByLabel.serverLogic(searchRestService.searchResourcesByLabelV2), @@ -40,6 +38,6 @@ final case class SearchApiRoutes( searchEndpoints.getSearchIncomingRegions.serverLogic(searchRestService.searchIncomingRegions), ) } -object SearchApiRoutes { - val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] +object SearchServerEndpoints { + val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala index 008b9d40180..a62cc11466c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiModule.scala @@ -10,18 +10,15 @@ import zio.ZLayer import org.knora.webapi.config.AppConfig import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.security.Authenticator object AuthenticationApiModule { self => - type Dependencies = AppConfig & Authenticator & BaseEndpoints & HandlerMapper & TapirToPekkoInterpreter - type Provided = AuthenticationApiRoutes & AuthenticationEndpointsV2 + type Dependencies = AppConfig & Authenticator & BaseEndpoints + type Provided = AuthenticationServerEndpoints & AuthenticationEndpointsV2 val layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( AuthenticationEndpointsV2.layer, - AuthenticationEndpointsV2Handler.layer, - AuthenticationApiRoutes.layer, + AuthenticationServerEndpoints.layer, AuthenticationRestService.layer, ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala deleted file mode 100644 index 085fcd935b6..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationApiRoutes.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.security.api - -import zio.ZLayer - -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - -final class AuthenticationApiRoutes( - private val handler: AuthenticationEndpointsV2Handler, - private val tapirToPekko: TapirToPekkoInterpreter, -) { - val routes = handler.allHandlers.map(tapirToPekko.toRoute) -} - -object AuthenticationApiRoutes { - val layer = ZLayer.derive[AuthenticationApiRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala similarity index 87% rename from webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala index fb481a246a9..b03622dc42d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2Handler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala @@ -15,11 +15,11 @@ import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayloa import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutResponse import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse -case class AuthenticationEndpointsV2Handler( +case class AuthenticationServerEndpoints( private val restService: AuthenticationRestService, private val endpoints: AuthenticationEndpointsV2, ) { - val allHandlers = Seq( + val serverEndpoints = Seq( endpoints.getV2Authentication.serverLogic(_ => _ => ZIO.succeed(CheckResponse("credentials are OK"))), endpoints.postV2Authentication.zServerLogic(restService.authenticate), endpoints.deleteV2Authentication.zServerLogic(restService.logout), @@ -28,6 +28,6 @@ case class AuthenticationEndpointsV2Handler( ) } -object AuthenticationEndpointsV2Handler { - val layer = ZLayer.derive[AuthenticationEndpointsV2Handler] +object AuthenticationServerEndpoints { + val layer = ZLayer.derive[AuthenticationServerEndpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiModule.scala index 47282ab9d2f..dcee102b317 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiModule.scala @@ -8,18 +8,14 @@ package org.knora.webapi.slice.shacl.api import zio.* import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.shacl.domain.ShaclValidator object ShaclApiModule { self => - type Dependencies = BaseEndpoints & HandlerMapper & ShaclValidator & TapirToPekkoInterpreter - type Provided = ShaclApiRoutes & ShaclEndpoints - val layer: URLayer[self.Dependencies, self.Provided] = - ZLayer.makeSome[self.Dependencies, self.Provided]( - ShaclApiRoutes.layer, - ShaclEndpoints.layer, - ShaclEndpointsHandler.layer, - ShaclApiService.layer, - ) + type Dependencies = BaseEndpoints & ShaclValidator + type Provided = ShaclServerEndpoints & ShaclEndpoints + val layer: URLayer[self.Dependencies, self.Provided] = ZLayer.makeSome[self.Dependencies, self.Provided]( + ShaclEndpoints.layer, + ShaclServerEndpoints.layer, + ShaclApiService.layer, + ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala deleted file mode 100644 index b66cebc7256..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiRoutes.scala +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.slice.shacl.api - -import org.apache.pekko.http.scaladsl.server.Route -import zio.* - -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter - -final case class ShaclApiRoutes( - private val shaclEndpointsHandler: ShaclEndpointsHandler, - private val tapirToPekko: TapirToPekkoInterpreter, -) { - val routes: Seq[Route] = shaclEndpointsHandler.allHandlers.map(tapirToPekko.toRoute(_)) -} - -object ShaclApiRoutes { - val layer = ZLayer.derive[ShaclApiRoutes] -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala similarity index 63% rename from webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala index 4900bc3bbc9..59a02cae178 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala @@ -8,13 +8,13 @@ package org.knora.webapi.slice.shacl.api import zio.* import sttp.tapir.ztapir.* -case class ShaclEndpointsHandler( +case class ShaclServerEndpoints( private val shaclEndpoints: ShaclEndpoints, private val shaclApiService: ShaclApiService, ) { - val allHandlers = Seq(shaclEndpoints.validate.zServerLogic(shaclApiService.validate)) + val serverEndpoints = Seq(shaclEndpoints.validate.zServerLogic(shaclApiService.validate)) } -object ShaclEndpointsHandler { - val layer = ZLayer.derive[ShaclEndpointsHandler] +object ShaclServerEndpoints { + val layer = ZLayer.derive[ShaclServerEndpoints] } From f234fec1bcc992d67780f44dd87fc8b9c4007b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 14:53:04 +0200 Subject: [PATCH 13/99] remove Public and SecuredHandlers --- .../org/knora/webapi/core/HttpServer.scala | 11 +++--- .../admin/api/AdminApiServerEndpoints.scala | 3 +- .../admin/api/AdminListRestService.scala | 2 +- .../slice/admin/api/ProjectsEndpoints.scala | 4 +- .../api/service/ListsV2RestService.scala | 2 +- .../api/OntologiesServerEndpoints.scala | 1 - .../api/service/OntologiesRestService.scala | 9 ++--- .../slice/shacl/api/ShaclApiService.scala | 21 +++++----- .../slice/shacl/api/ShaclEndpoints.scala | 6 +-- .../slice/shacl/api/ShaclApiServiceSpec.scala | 38 +++++-------------- 10 files changed, 37 insertions(+), 60 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index 46424c5b6c5..4289beaff29 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -8,24 +8,25 @@ package org.knora.webapi.core import zio.* import zio.http.* import org.knora.webapi.config.AppConfig +import org.knora.webapi.config.KnoraApi import org.knora.webapi.routing.CompleteApiServerEndpoints import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} import sttp.tapir.server.metrics.zio.ZioMetrics import sttp.tapir.server.ziohttp.{ZioHttpServerOptions, ZioHttpInterpreter} -import sttp.tapir.ztapir.RIOMonadError +import sttp.tapir.ztapir.* object HttpServer { - private def options: ZioHttpServerOptions[Any] = ZioHttpServerOptions.default + private def options = ZioHttpServerOptions.default val layer = ZLayer.scoped(createServer) private def createServer = for { - config <- ZIO.service[AppConfig] - endpoints <- ZIO.service[CompleteApiServerEndpoints].map(_.serverEndpoints) + port <- ZIO.serviceWith[KnoraApi](_.internalPort) + endpoints <- ZIO.serviceWith[CompleteApiServerEndpoints](_.serverEndpoints) httpApp = ZioHttpInterpreter(options).toHttp(endpoints) - _ <- Server.install(httpApp).provide(Server.defaultWithPort(config.knoraApi.externalPort)) + _ <- Server.install(httpApp).provide(Server.defaultWithPort(port)) _ <- Console.printLine(s"Go to http://localhost:$config.knoraApi.externalPort/docs to open SwaggerUI") } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index 3d44ea49957..e6919a607aa 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -5,10 +5,9 @@ package org.knora.webapi.slice.admin.api -import org.apache.pekko.http.scaladsl.server.Route import zio.* -import sttp.tapir.ztapir.ZServerEndpoint +import sttp.tapir.ztapir.* final case class AdminApiServerEndpoints( private val adminListsServerEndpoints: AdminListsServerEndpoints, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala index d17677ff9a4..30ceb83f90d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListRestService.scala @@ -38,7 +38,7 @@ final case class AdminListRestService( def getLists(projectIriOpt: Option[Either[ProjectIri, Shortcode]]): Task[ListsGetResponseADM] = listsResponder.getLists(projectIriOpt) - def listGetRequestADM(iri: ListIri): Task[ListItemGetResponseADM] = listsResponder.listGetRequestADM(iri.value) + def listGetRequestADM(iri: ListIri): Task[ListItemGetResponseADM] = listsResponder.listGetRequestADM(iri) def listNodeInfoGetRequestADM(iri: ListIri): Task[NodeInfoGetResponseADM] = listsResponder.listNodeInfoGetRequestADM(iri.value) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala index 1b26c9a5396..20754defebe 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala @@ -5,7 +5,7 @@ package org.knora.webapi.slice.admin.api -import sttp.capabilities.pekko.PekkoStreams +import sttp.capabilities.zio.ZioStreams import sttp.model.StatusCode import sttp.tapir.* import sttp.tapir.generic.auto.* @@ -210,7 +210,7 @@ final case class ProjectsEndpoints( .in(projectsByIri / "AllData") .out(header[String]("Content-Disposition")) .out(header[String]("Content-Type")) - .out(streamBinaryBody(PekkoStreams)(CodecFormat.OctetStream())) + .out(streamBinaryBody(ZioStreams)(CodecFormat.OctetStream())) .description("Returns all ontologies, data, and configuration belonging to a project identified by the IRI.") } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/service/ListsV2RestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/service/ListsV2RestService.scala index bb199020117..6d6b747649f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/service/ListsV2RestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/service/ListsV2RestService.scala @@ -37,7 +37,7 @@ final case class ListsV2RestService( */ def getList(user: User)(listIri: ListIri, opts: FormatOptions): Task[(RenderedResponse, MediaType)] = listsResponder - .listGetRequestADM(listIri.value) + .listGetRequestADM(listIri) .flatMap(r => ZIO.getOrFailWith(NotFoundException(s"List $listIri not found."))(r.asOpt[ListGetResponseADM])) .map(_.list) .map(ListGetResponseV2(_, user.lang, appConfig.fallbackLanguage)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index b529efc2235..3a57f86480a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -18,7 +18,6 @@ final class OntologiesServerEndpoints( val serverEndpoints = Seq( endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), - endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjects), endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), endpoints.getOntologiesAllentities.serverLogic(restService.getOntologyEntities), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala index 71b599e64c9..a743606af5b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala @@ -91,13 +91,10 @@ final case class OntologiesRestService( getOntologyMetadataBy(projectIri.toSet, formatOptions) def getOntologyMetadataByProjects( - projectIris: List[String], + projectIris: List[ProjectIri], formatOptions: FormatOptions, - ): Task[(RenderedResponse, MediaType)] = ZIO - .foreach(projectIris.toSet)(iri => - ZIO.fromEither(ProjectIri.from(iri)).orElseFail(BadRequestException(s"Invalid project IRI $iri")), - ) - .flatMap(getOntologyMetadataBy(_, formatOptions)) + ): Task[(RenderedResponse, MediaType)] = + getOntologyMetadataBy(projectIris.toSet, formatOptions) private def getOntologyMetadataBy(projectIris: Set[ProjectIri], formatOptions: FormatOptions) = for { result <- ontologyResponder.getOntologyMetadataForProjects(projectIris) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala index 5eff8f3dc19..7f018438fd9 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala @@ -7,22 +7,23 @@ package org.knora.webapi.slice.shacl.api import org.apache.jena.riot.RDFDataMgr import org.apache.jena.riot.RDFFormat -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.stream.scaladsl.StreamConverters -import org.apache.pekko.util.ByteString import zio.* +import zio.stream.* import java.io.FileInputStream import java.io.OutputStream import java.io.PipedInputStream import java.io.PipedOutputStream - import org.knora.webapi.slice.shacl.domain.ShaclValidator import org.knora.webapi.slice.shacl.domain.ValidationOptions -final case class ShaclApiService(validator: ShaclValidator) { +import java.io.IOException + +final case class ShaclApiService(private val validator: ShaclValidator) { + + private type ValidationStream = ZStream[Any, IOException, Byte] - def validate(formData: ValidationFormData): Task[Source[ByteString, Any]] = { + def validate(formData: ValidationFormData): Task[ValidationStream] = { val options = ValidationOptions( formData.validateShapes.getOrElse(ValidationOptions.default.validateShapes), formData.reportDetails.getOrElse(ValidationOptions.default.reportDetails), @@ -42,10 +43,10 @@ final case class ShaclApiService(validator: ShaclValidator) { }.as(src) } - private def makeOutputStreamAndSource(): (OutputStream, Source[ByteString, _]) = { - val outputStream = new PipedOutputStream() - val inputStream = new PipedInputStream(outputStream) - val source = StreamConverters.fromInputStream(() => inputStream) + private def makeOutputStreamAndSource(): (OutputStream, ValidationStream) = { + val outputStream = new PipedOutputStream() + val inputStream = new PipedInputStream(outputStream) + val source: ValidationStream = ZStream.fromInputStream(() => inputStream) (outputStream, source) } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala index 75da7363b1e..ec8528c2c1e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala @@ -5,9 +5,7 @@ package org.knora.webapi.slice.shacl.api -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.util.ByteString -import sttp.capabilities.pekko.PekkoStreams +import sttp.capabilities.zio.ZioStreams import sttp.model.MediaType import sttp.tapir.* import sttp.tapir.Schema.annotations.description @@ -39,7 +37,7 @@ case class ShaclEndpoints(baseEndpoints: BaseEndpoints) { .in("shacl" / "validate") .description("foo") .in(multipartBody[ValidationFormData]) - .out(streamTextBody(PekkoStreams)(new CodecFormat { + .out(streamTextBody(ZioStreams)(new CodecFormat { override val mediaType: MediaType = MediaType("text", "turtle") }).description(""" |The validation report in Turtle format. diff --git a/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala index b0871097a08..c5403d69287 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala @@ -5,45 +5,27 @@ package org.knora.webapi.slice.shacl.api -import org.apache.pekko.actor.* -import org.apache.pekko.stream.Materializer -import org.apache.pekko.stream.scaladsl.Sink -import org.apache.pekko.stream.scaladsl.Source -import org.apache.pekko.util.ByteString import zio.* import zio.nio.file.Files import zio.test.* - -import scala.concurrent.Await -import scala.concurrent.ExecutionContextExecutor -import scala.concurrent.Future - +import zio.stream.ZStream import org.knora.webapi.slice.shacl.domain.ShaclValidator +import java.io.IOException + object ShaclApiServiceSpec extends ZIOSpecDefault { private val shaclApiService = ZIO.serviceWithZIO[ShaclApiService] - val spec: Spec[TestEnvironment & zio.Scope, Any] = suite("ShaclApiService")( + val spec = suite("ShaclApiService")( test("validate") { for { - data <- Files.createTempFile("data.ttl", None, Seq.empty) - shacl <- Files.createTempFile("shacl.ttl", None, Seq.empty) - formData = ValidationFormData(data.toFile, shacl.toFile, None, None, None) - result <- shaclApiService(_.validate(formData)) - } yield assertTrue(reportConforms(result)) + data <- Files.createTempFile("data.ttl", None, Seq.empty) + shacl <- Files.createTempFile("shacl.ttl", None, Seq.empty) + formData = ValidationFormData(data.toFile, shacl.toFile, None, None, None) + result <- shaclApiService(_.validate(formData)) + conforms <- result.runCollect.map(bytes => new String(bytes.toArray)).map(_.contains("sh:conforms true")) + } yield assertTrue(conforms) }, ).provide(ShaclApiService.layer, ShaclValidator.layer) - - private def reportConforms(result: Source[ByteString, Any]): Boolean = { - implicit val system: ActorSystem = ActorSystem.create() - implicit val ec: ExecutionContextExecutor = system.dispatcher - implicit val mat: Materializer = Materializer(system) - - val str: Future[String] = result - .runWith(Sink.fold(ByteString.empty)(_ ++ _)) // Accumulate ByteString - .map(_.utf8String) - val reportStr = Await.result(str, 5.seconds.asScala) - reportStr.contains("sh:conforms true") - } } From e2010668cf18ac826d086ecff964687e83ed1cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 14:55:05 +0200 Subject: [PATCH 14/99] remove Public and SecuredHandlers --- .../org/knora/webapi/slice/shacl/api/ShaclApiService.scala | 4 ++-- .../org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala index 7f018438fd9..26e91349013 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala @@ -16,13 +16,12 @@ import java.io.PipedInputStream import java.io.PipedOutputStream import org.knora.webapi.slice.shacl.domain.ShaclValidator import org.knora.webapi.slice.shacl.domain.ValidationOptions +import org.knora.webapi.slice.shacl.api.ShaclApiService.ValidationStream import java.io.IOException final case class ShaclApiService(private val validator: ShaclValidator) { - private type ValidationStream = ZStream[Any, IOException, Byte] - def validate(formData: ValidationFormData): Task[ValidationStream] = { val options = ValidationOptions( formData.validateShapes.getOrElse(ValidationOptions.default.validateShapes), @@ -52,5 +51,6 @@ final case class ShaclApiService(private val validator: ShaclValidator) { } object ShaclApiService { + type ValidationStream = ZStream[Any, IOException, Byte] val layer = ZLayer.derive[ShaclApiService] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala index ec8528c2c1e..3b8bd1d730c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala @@ -17,6 +17,8 @@ import java.io.File import dsp.errors.RequestRejectedException import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.shacl.api.ShaclApiService.ValidationStream + case class ValidationFormData( @description("The data to be validated.") `data.ttl`: File, @@ -32,7 +34,7 @@ case class ValidationFormData( case class ShaclEndpoints(baseEndpoints: BaseEndpoints) { - val validate: Endpoint[Unit, ValidationFormData, RequestRejectedException, Source[ByteString, Any], PekkoStreams] = + val validate: Endpoint[Unit, ValidationFormData, RequestRejectedException, ValidationStream, ZioStreams] = baseEndpoints.publicEndpoint.post .in("shacl" / "validate") .description("foo") From 653725de6fa3e746de72ec3db33b8fa78931a829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:03:50 +0200 Subject: [PATCH 15/99] remove Public and SecuredHandlers --- .../slice/shacl/api/ShaclApiService.scala | 24 ++++--------------- .../slice/shacl/api/ShaclEndpoints.scala | 10 ++++---- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala index 26e91349013..40527a32a10 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala @@ -11,46 +11,30 @@ import zio.* import zio.stream.* import java.io.FileInputStream -import java.io.OutputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream import org.knora.webapi.slice.shacl.domain.ShaclValidator import org.knora.webapi.slice.shacl.domain.ValidationOptions -import org.knora.webapi.slice.shacl.api.ShaclApiService.ValidationStream import java.io.IOException final case class ShaclApiService(private val validator: ShaclValidator) { - def validate(formData: ValidationFormData): Task[ValidationStream] = { + def validate(formData: ValidationFormData): Task[ZStream[Any, Throwable, Byte]] = { val options = ValidationOptions( formData.validateShapes.getOrElse(ValidationOptions.default.validateShapes), formData.reportDetails.getOrElse(ValidationOptions.default.reportDetails), formData.addBlankNodes.getOrElse(ValidationOptions.default.addBlankNodes), ) - val (out, src) = makeOutputStreamAndSource() ZIO.scoped { for { dataStream <- ZIO.fromAutoCloseable(ZIO.succeed(new FileInputStream(formData.`data.ttl`))) shaclStream <- ZIO.fromAutoCloseable(ZIO.succeed(new FileInputStream(formData.`shacl.ttl`))) report <- validator.validate(dataStream, shaclStream, options) - _ <- ZIO.attemptBlockingIO { - try { RDFDataMgr.write(out, report.getModel, RDFFormat.TURTLE) } - finally { out.close() } - }.forkDaemon - } yield () - }.as(src) - } - - private def makeOutputStreamAndSource(): (OutputStream, ValidationStream) = { - val outputStream = new PipedOutputStream() - val inputStream = new PipedInputStream(outputStream) - val source: ValidationStream = ZStream.fromInputStream(() => inputStream) - (outputStream, source) + out = ZStream.fromOutputStreamWriter(RDFDataMgr.write(_, report.getModel, RDFFormat.TURTLE)) + } yield out + } } } object ShaclApiService { - type ValidationStream = ZStream[Any, IOException, Byte] val layer = ZLayer.derive[ShaclApiService] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala index 3b8bd1d730c..d657a380e1d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala @@ -10,15 +10,14 @@ import sttp.model.MediaType import sttp.tapir.* import sttp.tapir.Schema.annotations.description import sttp.tapir.generic.auto.* -import zio.ZLayer +import zio.* +import zio.stream.ZStream import java.io.File import dsp.errors.RequestRejectedException import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.shacl.api.ShaclApiService.ValidationStream - case class ValidationFormData( @description("The data to be validated.") `data.ttl`: File, @@ -34,7 +33,7 @@ case class ValidationFormData( case class ShaclEndpoints(baseEndpoints: BaseEndpoints) { - val validate: Endpoint[Unit, ValidationFormData, RequestRejectedException, ValidationStream, ZioStreams] = + val validate = baseEndpoints.publicEndpoint.post .in("shacl" / "validate") .description("foo") @@ -54,8 +53,7 @@ case class ShaclEndpoints(baseEndpoints: BaseEndpoints) { |``` |""".stripMargin)) - val endpoints: Seq[AnyEndpoint] = - Seq(validate).map(_.tag("Shacl")) + val endpoints = Seq(validate).map(_.tag("Shacl")) } object ShaclEndpoints { From 86bcacaa99c673bb12bc32efb18d9354cd68215d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:07:02 +0200 Subject: [PATCH 16/99] remove Public and SecuredHandlers --- .../src/main/scala/org/knora/webapi/core/HttpServer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index 4289beaff29..1ea1ea3e4a5 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -23,10 +23,10 @@ object HttpServer { val layer = ZLayer.scoped(createServer) private def createServer = for { - port <- ZIO.serviceWith[KnoraApi](_.internalPort) + apiConfig <- ZIO.service[KnoraApi] endpoints <- ZIO.serviceWith[CompleteApiServerEndpoints](_.serverEndpoints) httpApp = ZioHttpInterpreter(options).toHttp(endpoints) - _ <- Server.install(httpApp).provide(Server.defaultWithPort(port)) - _ <- Console.printLine(s"Go to http://localhost:$config.knoraApi.externalPort/docs to open SwaggerUI") + _ <- Server.install(httpApp).provide(Server.defaultWithPort(apiConfig.internalPort)) + _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") } yield () } From ea7c7179182c4f99475fd153d2557233e0b0b274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:31:20 +0200 Subject: [PATCH 17/99] remove Public and SecuredHandlers --- .../api/MaintenanceEndpointsHandler.scala | 6 +- .../dasch/api/ProjectsEndpointsHandler.scala | 26 ++++---- .../dasch/api/ReportEndpointsHandler.scala | 2 +- .../org/knora/webapi/core/HttpServer.scala | 4 +- .../org/knora/webapi/core/LayersLive.scala | 16 ++--- .../routing/CompleteApiServerEndpoints.scala | 25 ++++---- .../admin/api/AdminApiServerEndpoints.scala | 2 +- .../admin/api/AdminListsServerEndpoints.scala | 2 +- .../admin/api/FilesServerEndpoints.scala | 2 +- .../admin/api/GroupsServerEndpoints.scala | 2 +- .../api/MaintenanceServerEndpoints.scala | 2 +- .../api/PermissionsServerEndpoints.scala | 2 +- .../ProjectsLegalInfoServerEndpoints.scala | 2 +- .../admin/api/ProjectsServerEndpoints.scala | 2 +- ...ndler.scala => UsersServerEndpoints.scala} | 2 +- .../slice/common/api/BaseEndpoints.scala | 61 +++++++------------ .../api/ManagementServerEndpoints.scala | 4 +- .../lists/api/ListsV2ServerEndpoints.scala | 2 +- .../api/OntologiesServerEndpoints.scala | 2 +- .../api/MetadataServerEndpoints.scala | 4 +- .../api/ResourceInfoServerEndpoints.scala | 4 +- .../api/ResourcesApiServerEndpoints.scala | 2 +- .../api/ResourcesServerEndpoints.scala | 4 +- .../api/StandoffServerEndpoints.scala | 4 +- .../resources/api/ValuesServerEndpoints.scala | 2 +- .../search/api/SearchServerEndpoints.scala | 3 +- .../api/AuthenticationServerEndpoints.scala | 2 +- .../shacl/api/ShaclServerEndpoints.scala | 4 +- 28 files changed, 96 insertions(+), 99 deletions(-) rename webapi/src/main/scala/org/knora/webapi/slice/admin/api/{UsersEndpointsHandler.scala => UsersServerEndpoints.scala} (98%) diff --git a/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala index 43170faa42f..db7fb8dceea 100644 --- a/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala @@ -25,7 +25,7 @@ final case class MaintenanceEndpointsHandler( private val postMaintenanceEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.postMaintenanceActionEndpoint - .serverLogic(userSession => { case (action, shortcodes) => + .zServerLogic(userSession => { case (action, shortcodes) => for { _ <- authorizationHandler.ensureAdminScope(userSession) paths <- @@ -46,7 +46,7 @@ final case class MaintenanceEndpointsHandler( val needsTopLeftCorrectionEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.needsTopLeftCorrectionEndpoint - .serverLogic(userSession => + .zServerLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> maintenanceActions @@ -58,7 +58,7 @@ final case class MaintenanceEndpointsHandler( val wasTopLeftCorrectionAppliedEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.wasTopLeftCorrectionAppliedEndpoint - .serverLogic(userSession => + .zServerLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> maintenanceActions diff --git a/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala index 1d5d654c336..de49af27373 100644 --- a/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala @@ -42,7 +42,7 @@ final case class ProjectsEndpointsHandler( ) extends HandlerFunctions { val getProjectsEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectsEndpoint - .serverLogic(userSession => + .zServerLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> projectService @@ -55,7 +55,7 @@ final case class ProjectsEndpointsHandler( ) val getProjectByShortcodeEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectByShortcodeEndpoint - .serverLogic(userSession => + .zServerLogic(userSession => shortcode => authorizationHandler.ensureProjectReadable(userSession, shortcode) *> projectService @@ -68,7 +68,7 @@ final case class ProjectsEndpointsHandler( ) private val getProjectChecksumReportEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectsChecksumReport - .serverLogic(userSession => + .zServerLogic(userSession => shortcode => authorizationHandler.ensureProjectReadable(userSession, shortcode) *> reportService @@ -81,7 +81,7 @@ final case class ProjectsEndpointsHandler( ) private val deleteProjectsEraseEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.deleteProjectsErase - .serverLogic(userSession => + .zServerLogic(userSession => shortcode => authorizationHandler.ensureAdminScope(userSession) *> projectService.findProject(shortcode).some.mapError(projectNotFoundOrServerError(_, shortcode)) *> { @@ -99,7 +99,7 @@ final case class ProjectsEndpointsHandler( ) private val getProjectsAssetsInfoEndpoint: ZServerEndpoint[Any, Any] = - projectEndpoints.getProjectsAssetsInfo.serverLogic { userSession => (shortcode, assetId) => + projectEndpoints.getProjectsAssetsInfo.zServerLogic { userSession => (shortcode, assetId) => val ref = AssetRef(assetId, shortcode) authorizationHandler.ensureProjectReadable(userSession, shortcode) *> assetInfoService @@ -113,7 +113,7 @@ final case class ProjectsEndpointsHandler( private val getProjectsAssetsOriginalEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.getProjectsAssetsOriginal - .serverLogic(userSession => + .zServerLogic(userSession => (shortcode, assetId) => for { ref <- ZIO.succeed(AssetRef(assetId, shortcode)) @@ -132,7 +132,7 @@ final case class ProjectsEndpointsHandler( private val ChunkSize = 64 * 1024 // larger chunk size; better for larger files private val postProjectAssetEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postProjectAsset - .serverLogic(principal => { case (shortcode, filename, stream) => + .zServerLogic(principal => { case (shortcode, filename, stream) => authorizationHandler.ensureProjectWritable(principal, shortcode) *> ZIO.scoped { for { @@ -149,7 +149,7 @@ final case class ProjectsEndpointsHandler( }) private val postBulkIngestEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.postBulkIngest - .serverLogic(userSession => + .zServerLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -164,7 +164,7 @@ final case class ProjectsEndpointsHandler( ) private val postBulkIngestEndpointFinalize: ZServerEndpoint[Any, Any] = projectEndpoints.postBulkIngestFinalize - .serverLogic(userSession => + .zServerLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -180,7 +180,7 @@ final case class ProjectsEndpointsHandler( private val getBulkIngestMappingCsvEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getBulkIngestMappingCsv - .serverLogic(userSession => + .zServerLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -193,7 +193,7 @@ final case class ProjectsEndpointsHandler( ) private val postBulkIngestUploadEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postBulkIngestUpload - .serverLogic(principal => { case (shortcode, filename, stream) => + .zServerLogic(principal => { case (shortcode, filename, stream) => for { _ <- authorizationHandler.ensureProjectWritable(principal, shortcode) s <- bulkIngestService.uploadSingleFile(shortcode, filename, stream).mapError { @@ -206,7 +206,7 @@ final case class ProjectsEndpointsHandler( Conflict(s"A bulk ingest is currently in progress for project ${code.value}.") private val postExportEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postExport - .serverLogic(userSession => + .zServerLogic(userSession => shortcode => authorizationHandler.ensureAdminScope(userSession) *> projectService @@ -224,7 +224,7 @@ final case class ProjectsEndpointsHandler( ) private val getImportEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.getImport - .serverLogic(userSession => + .zServerLogic(userSession => (shortcode, stream) => authorizationHandler.ensureAdminScope(userSession) *> importService diff --git a/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala index 70bdef89508..84016ec15fd 100644 --- a/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala @@ -18,7 +18,7 @@ final class ReportEndpointsHandler( ) { private val postAssetOverviewReportHandler: ZServerEndpoint[Any, Any] = - reportEndpoints.postAssetOverviewReport.serverLogic(_ => + reportEndpoints.postAssetOverviewReport.zServerLogic(_ => _ => createAssetOverReports.forkDaemon.logError.as("work in progress"), ) diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index 1ea1ea3e4a5..f30cea19409 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -9,7 +9,7 @@ import zio.* import zio.http.* import org.knora.webapi.config.AppConfig import org.knora.webapi.config.KnoraApi -import org.knora.webapi.routing.CompleteApiServerEndpoints +import org.knora.webapi.routing.Endpoints import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} import sttp.tapir.server.metrics.zio.ZioMetrics @@ -24,7 +24,7 @@ object HttpServer { private def createServer = for { apiConfig <- ZIO.service[KnoraApi] - endpoints <- ZIO.serviceWith[CompleteApiServerEndpoints](_.serverEndpoints) + endpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) httpApp = ZioHttpInterpreter(options).toHttp(endpoints) _ <- Server.install(httpApp).provide(Server.defaultWithPort(apiConfig.internalPort)) _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 193bd84178d..8229b6db09e 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -134,7 +134,6 @@ object LayersLive { self => AdminApiModule.Provided & AdminModule.Provided & ApiComplexV2JsonLdRequestParser & - CompleteApiServerEndpoints & ApiV2Endpoints & AssetPermissionsResponder & AuthenticationApiModule.Provided & @@ -142,8 +141,8 @@ object LayersLive { self => CardinalityHandler & CommonModule.Provided & ConstructResponseUtilV2 & - OntologyModule.Provided & DefaultObjectAccessPermissionService & + Endpoints & IIIFRequestMessageHandler & InfrastructureModule.Provided & ListsApiModule.Provided & @@ -151,6 +150,7 @@ object LayersLive { self => MessageRelay & OntologyApiModule.Provided & OntologyInferencer & + OntologyModule.Provided & OntologyResponderV2 & PermissionUtilADM & PermissionsResponder & @@ -160,15 +160,15 @@ object LayersLive { self => RepositoryUpdater & ResourceUtilV2 & ResourcesApiServerEndpoints & - ResourcesResponderV2 & ResourcesRepo & - SecurityModule.Provided & - SearchServerEndpoints & + ResourcesResponderV2 & SearchResponderV2Module.Provided & + SearchServerEndpoints & + SecurityModule.Provided & SecurityModule.Provided & ShaclApiModule.Provided & - ShaclModule.Provided & ShaclEndpoints & + ShaclModule.Provided & SipiService & StandoffResponderV2 & StandoffTagUtilV2 & @@ -185,7 +185,6 @@ object LayersLive { self => ]( AdminApiModule.layer, ApiComplexV2JsonLdRequestParser.layer, - CompleteApiServerEndpoints.layer, ApiV2Endpoints.layer, AssetPermissionsResponder.layer, AuthenticationApiModule.layer, @@ -193,6 +192,7 @@ object LayersLive { self => BaseEndpoints.layer, CardinalityHandler.layer, ConstructResponseUtilV2.layer, + Endpoints.layer, HttpServer.layer, IIIFRequestMessageHandlerLive.layer, KnoraResponseRenderer.layer, @@ -216,9 +216,9 @@ object LayersLive { self => ResourcesModule.layer, ResourcesRepoLive.layer, ResourcesResponderV2.layer, - SearchServerEndpoints.layer, SearchEndpoints.layer, SearchResponderV2Module.layer, + SearchServerEndpoints.layer, SecurityModule.layer, ShaclApiModule.layer, ShaclModule.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala index aae4db249af..e188d78c48b 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala @@ -23,7 +23,7 @@ import sttp.tapir.ztapir.* * ALL requests go through each of the routes in ORDER. * The FIRST matching route is used for handling a request. */ -final case class CompleteApiServerEndpoints( +final case class Endpoints( adminApiServerEndpoints: AdminApiServerEndpoints, authenticationServerEndpoints: AuthenticationServerEndpoints, listsV2ServerEndpoints: ListsV2ServerEndpoints, @@ -34,16 +34,17 @@ final case class CompleteApiServerEndpoints( managementServerEndpoints: ManagementServerEndpoints, ontologiesServerEndpoints: OntologiesServerEndpoints, ) { - val serverEndpoints = adminApiServerEndpoints.serverEndpoints ++ - authenticationServerEndpoints.serverEndpoints ++ - listsV2ServerEndpoints.serverEndpoints ++ - managementServerEndpoints.serverEndpoints ++ - ontologiesServerEndpoints.serverEndpoints ++ - resourceInfoServerEndpoints.serverEndpoints ++ - resourcesApiServerEndpoints.serverEndpoints ++ - searchServerEndpoints.serverEndpoints ++ - shaclServerEndpoints.serverEndpoints + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = + adminApiServerEndpoints.serverEndpoints ++ + authenticationServerEndpoints.serverEndpoints ++ + listsV2ServerEndpoints.serverEndpoints ++ + managementServerEndpoints.serverEndpoints ++ + ontologiesServerEndpoints.serverEndpoints ++ + resourceInfoServerEndpoints.serverEndpoints ++ + resourcesApiServerEndpoints.serverEndpoints ++ + searchServerEndpoints.serverEndpoints ++ + shaclServerEndpoints.serverEndpoints } -object CompleteApiServerEndpoints { - val layer = ZLayer.derive[CompleteApiServerEndpoints] +object Endpoints { + val layer = ZLayer.derive[Endpoints] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index e6919a607aa..2111d2ad130 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -21,7 +21,7 @@ final case class AdminApiServerEndpoints( private val usersServerEndpoints: UsersServerEndpoints, ) { - val serverEndpoints = + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = filesServerEndpoints.serverEndpoints ++ groupsServerEndpoints.serverEndpoints ++ adminListsServerEndpoints.serverEndpoints ++ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala index b42612bca34..c16681e1376 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala @@ -28,7 +28,7 @@ final case class AdminListsServerEndpoints( private val adminListsEndpoints: AdminListsEndpoints, private val restService: AdminListRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( adminListsEndpoints.getListsQueryByProjectIriOption.zServerLogic(restService.getLists), adminListsEndpoints.getListsByIri.zServerLogic(restService.listGetRequestADM), adminListsEndpoints.getListsByIriInfo.zServerLogic(restService.listNodeInfoGetRequestADM), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala index e62703d9c7c..1ace4e4e726 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala @@ -19,7 +19,7 @@ final case class FilesServerEndpoints( private val filesEndpoints: FilesEndpoints, private val assetPermissionsResponder: AssetPermissionsResponder, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( filesEndpoints.getAdminFilesShortcodeFileIri.serverLogic( assetPermissionsResponder.getPermissionCodeAndProjectRestrictedViewSettings, ), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala index aa33f6e652e..3590607192a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala @@ -18,7 +18,7 @@ case class GroupsServerEndpoints( private val endpoints: GroupsEndpoints, private val restService: GroupRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getGroups.zServerLogic(_ => restService.getGroups), endpoints.getGroupByIri.zServerLogic(restService.getGroupByIri), endpoints.getGroupMembers.serverLogic(restService.getGroupMembers), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala index a3848a649b8..2fc3e8a9b48 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala @@ -17,7 +17,7 @@ final case class MaintenanceServerEndpoints( private val endpoints: MaintenanceEndpoints, private val restService: MaintenanceRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.postMaintenance.serverLogic(restService.executeMaintenanceAction), ) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala index 69e4902c5b2..0dfc6a55751 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala @@ -34,7 +34,7 @@ final case class PermissionsServerEndpoints( private val restService: PermissionRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( permissionsEndpoints.postPermissionsAp.serverLogic(restService.createAdministrativePermission), permissionsEndpoints.getPermissionsApByProjectIri.serverLogic(restService.getPermissionsApByProjectIri), permissionsEndpoints.getPermissionsApByProjectAndGroupIri.serverLogic( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala index 4ef9709bd0d..e93fc250c20 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala @@ -13,7 +13,7 @@ final class ProjectsLegalInfoServerEndpoints( private val endpoints: ProjectsLegalInfoEndpoints, private val restService: ProjectsLegalInfoRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getProjectLicenses.zServerLogic(restService.findLicenses), endpoints.getProjectLicensesIri.zServerLogic(restService.findAvailableLicenseByIdAndShortcode), endpoints.getProjectAuthorships.serverLogic(restService.findAuthorships), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala index 91de1d0d5c6..c5f45704be6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala @@ -40,7 +40,7 @@ final case class ProjectsServerEndpoints( }, ) - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( projectsEndpoints.Public.getAdminProjects.zServerLogic(restService.listAllProjects), projectsEndpoints.Public.getAdminProjectsKeywords.zServerLogic(restService.listAllKeywords), projectsEndpoints.Public.getAdminProjectsByProjectIri.zServerLogic(restService.findById), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala similarity index 98% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala index 09825e5fd1f..15c96ac9009 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala @@ -26,7 +26,7 @@ case class UsersServerEndpoints( private val restService: UserRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( usersEndpoints.get.usersByIriProjectMemberShips.zServerLogic(restService.getProjectMemberShipsByUserIri), usersEndpoints.get.usersByIriProjectAdminMemberShips.zServerLogic(restService.getProjectAdminMemberShipsByUserIri), usersEndpoints.get.usersByIriGroupMemberships.zServerLogic(restService.getGroupMemberShipsByIri), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala index b647ac6eae5..90f39f4b82c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -43,48 +43,33 @@ final case class BaseEndpoints(authenticator: Authenticator) { oneOfVariant[ForbiddenException](statusCode(StatusCode.Forbidden).and(jsonBody[ForbiddenException])), ) - val publicEndpoint: Endpoint[Unit, Unit, Throwable, Unit, Any] = endpoint.errorOut(errorOutputs) + val publicEndpoint: PublicEndpoint[Unit, Throwable, Unit, Any] = endpoint.errorOut(errorOutputs) - private val endpointWithBearerCookieBasicAuthOptional - : Endpoint[(Option[String], Option[String], Option[UsernamePassword]), Unit, Throwable, Unit, Any] = - endpoint - .errorOut(errorOutputs) - .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) - .securityIn(cookie[Option[String]](authenticator.calculateCookieName())) - .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic("realm"))) + private type SecurityIn = (Option[String], Option[String], Option[UsernamePassword]) + private val endpointWithBearerCookieBasicAuthOptional = endpoint + .errorOut(errorOutputs) + .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) + .securityIn(cookie[Option[String]](authenticator.calculateCookieName())) + .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic("realm"))) - val securedEndpoint: ZPartialServerEndpoint[ - Nothing, - (Option[String], Option[String], Option[UsernamePassword]), - User, - Unit, - Throwable, - Unit, - Any, - ] = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { - case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) - case (_, Some(cookie), _) => authenticateJwt(cookie) - case (_, _, Some(basic)) => authenticateBasic(basic) - case _ => ZIO.fail(BadCredentialsException("No credentials provided.")) - } + val securedEndpoint: ZPartialServerEndpoint[Any, SecurityIn, User, Unit, Throwable, Unit, Any] = + endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { + case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) + case (_, Some(cookie), _) => authenticateJwt(cookie) + case (_, _, Some(basic)) => authenticateBasic(basic) + case _ => ZIO.fail(BadCredentialsException("No credentials provided.")) + } - val withUserEndpoint: ZPartialServerEndpoint[ - Nothing, - (Option[String], Option[String], Option[UsernamePassword]), - User, - Unit, - Throwable, - Unit, - Any, - ] = endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { - case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) - case (_, Some(cookie), _) => authenticateJwt(cookie) - case (_, _, Some(basic)) => authenticateBasic(basic) - case _ => ZIO.succeed(AnonymousUser) - } + val withUserEndpoint: ZPartialServerEndpoint[Any, SecurityIn, User, Unit, Throwable, Unit, Any] = + endpointWithBearerCookieBasicAuthOptional.zServerSecurityLogic { + case (Some(bearer), _, _) => authenticateJwt(bearer) + case (_, Some(cookie), _) => authenticateJwt(cookie) + case (_, _, Some(basic)) => authenticateBasic(basic) + case _ => ZIO.succeed(AnonymousUser) + } - private def authenticateJwt(jwtToken: String): IO[BadCredentialsException, User] = - authenticator.authenticate(jwtToken).orElseFail(BadCredentialsException("Invalid credentials.")) + private def authenticateJwt(token: String): IO[BadCredentialsException, User] = + authenticator.authenticate(token).orElseFail(BadCredentialsException("Invalid credentials.")) private def authenticateBasic(basic: UsernamePassword): IO[BadCredentialsException, User] = for { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala index 196043040d1..d62ad1bcfca 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala @@ -12,10 +12,10 @@ final case class ManagementServerEndpoints( private val endpoint: ManagementEndpoints, private val restService: ManagementRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( endpoint.getVersion.zServerLogic(_ => ZIO.succeed(VersionResponse.current)), endpoint.getHealth.zServerLogic(_ => restService.healthCheck), - endpoint.postStartCompaction.serverLogic(restService.startCompaction), + endpoint.postStartCompaction.zServerLogic(restService.startCompaction), ) } object ManagementServerEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala index 52ceda73565..79edf6801ad 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala @@ -14,7 +14,7 @@ final case class ListsV2ServerEndpoints( private val endpoints: ListsEndpointsV2, private val restService: ListsV2RestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getV2Lists.serverLogic(restService.getList), endpoints.getV2Node.serverLogic(restService.getNode), ) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 3a57f86480a..223c5b9e26e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -16,7 +16,7 @@ final class OntologiesServerEndpoints( private val restService: OntologiesRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala index b3b89b1a303..c22551e9399 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala @@ -14,7 +14,9 @@ final case class MetadataServerEndpoints( private val endpoints: MetadataEndpoints, private val restService: MetadataRestService, ) { - val serverEndpoints = Seq(endpoints.getResourcesMetadata.serverLogic(restService.getResourcesMetadata)) + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( + endpoints.getResourcesMetadata.serverLogic(restService.getResourcesMetadata), + ) } object MetadataServerEndpoints { val layer = ZLayer.derive[MetadataServerEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala index 80c01e417f5..2ee691f3727 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala @@ -16,7 +16,9 @@ final case class ResourceInfoServerEndpoints( private val endpoints: ResourceInfoEndpoints, private val resourceInfoService: ResourceInfoRestService, ) { - val serverEndpoints = Seq(endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass)) + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( + endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass), + ) } object ResourceInfoServerEndpoints { val layer = ZLayer.derive[ResourceInfoServerEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala index f335aa3ed8b..21515f9951f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala @@ -12,7 +12,7 @@ final case class ResourcesApiServerEndpoints( private val standoffServerEndpoints: StandoffServerEndpoints, private val valuesServerEndpoints: ValuesServerEndpoints, ) { - val serverEndpoints = valuesServerEndpoints.serverEndpoints ++ + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = valuesServerEndpoints.serverEndpoints ++ resourcesServerEndpoints.serverEndpoints ++ standoffServerEndpoints.serverEndpoints ++ metadataServerEndpoints.serverEndpoints diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala index fda370ffff4..5cba6d667d1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesServerEndpoints.scala @@ -4,6 +4,8 @@ */ package org.knora.webapi.slice.resources.api + +import sttp.tapir.ztapir.* import zio.* import org.knora.webapi.slice.resources.api.service.ResourcesRestService @@ -13,7 +15,7 @@ final class ResourcesServerEndpoints( private val resourcesRestService: ResourcesRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( resourcesEndpoints.getResourcesCanDelete.serverLogic(resourcesRestService.canDeleteResource), resourcesEndpoints.getResourcesGraph.serverLogic(resourcesRestService.getResourcesGraph), resourcesEndpoints.getResourcesIiifManifest.serverLogic(resourcesRestService.getResourcesIiifManifest), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala index 11da5d2d804..d2a17371321 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala @@ -14,7 +14,9 @@ final case class StandoffServerEndpoints( private val endpoints: StandoffEndpoints, private val restService: StandoffRestService, ) { - val serverEndpoints = Seq(endpoints.postMapping.serverLogic(restService.createMapping)) + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( + endpoints.postMapping.serverLogic(restService.createMapping), + ) } object StandoffServerEndpoints { val layer = ZLayer.derive[StandoffServerEndpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala index 6b985d58639..a5769795062 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala @@ -20,7 +20,7 @@ final class ValuesServerEndpoints( private val valuesRestService: ValuesRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getValue.serverLogic(valuesRestService.getValue), endpoints.postValues.serverLogic(valuesRestService.createValue), endpoints.putValues.serverLogic(valuesRestService.updateValue), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala index 6898de84db4..2b86bd0e107 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala @@ -5,6 +5,7 @@ package org.knora.webapi.slice.search.api import sttp.model.MediaType +import sttp.tapir.ztapir.* import zio.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri @@ -19,7 +20,7 @@ final case class SearchServerEndpoints( private val searchRestService: SearchRestService, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( searchEndpoints.getFullTextSearch.serverLogic(searchRestService.fullTextSearch), searchEndpoints.getFullTextSearchCount.serverLogic(searchRestService.fullTextSearchCount), searchEndpoints.getSearchByLabel.serverLogic(searchRestService.searchResourcesByLabelV2), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala index b03622dc42d..9ea3351481d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala @@ -19,7 +19,7 @@ case class AuthenticationServerEndpoints( private val restService: AuthenticationRestService, private val endpoints: AuthenticationEndpointsV2, ) { - val serverEndpoints = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getV2Authentication.serverLogic(_ => _ => ZIO.succeed(CheckResponse("credentials are OK"))), endpoints.postV2Authentication.zServerLogic(restService.authenticate), endpoints.deleteV2Authentication.zServerLogic(restService.logout), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala index 59a02cae178..86ec1b9071f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala @@ -12,7 +12,9 @@ case class ShaclServerEndpoints( private val shaclEndpoints: ShaclEndpoints, private val shaclApiService: ShaclApiService, ) { - val serverEndpoints = Seq(shaclEndpoints.validate.zServerLogic(shaclApiService.validate)) + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( + shaclEndpoints.validate.zServerLogic(shaclApiService.validate), + ) } object ShaclServerEndpoints { From 5964b2d627ec62c470b12afa7f4c73674199e9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:32:49 +0200 Subject: [PATCH 18/99] remove Public and SecuredHandlers --- .../dasch/api/MaintenanceEndpointsHandler.scala | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala index db7fb8dceea..19f4f68f91a 100644 --- a/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/MaintenanceEndpointsHandler.scala @@ -25,7 +25,7 @@ final case class MaintenanceEndpointsHandler( private val postMaintenanceEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.postMaintenanceActionEndpoint - .zServerLogic(userSession => { case (action, shortcodes) => + .serverLogic(userSession => { case (action, shortcodes) => for { _ <- authorizationHandler.ensureAdminScope(userSession) paths <- @@ -46,7 +46,7 @@ final case class MaintenanceEndpointsHandler( val needsTopLeftCorrectionEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.needsTopLeftCorrectionEndpoint - .zServerLogic(userSession => + .serverLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> maintenanceActions @@ -58,7 +58,7 @@ final case class MaintenanceEndpointsHandler( val wasTopLeftCorrectionAppliedEndpoint: ZServerEndpoint[Any, Any] = maintenanceEndpoints.wasTopLeftCorrectionAppliedEndpoint - .zServerLogic(userSession => + .serverLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> maintenanceActions @@ -68,12 +68,11 @@ final case class MaintenanceEndpointsHandler( .as("work in progress"), ) - val endpoints: List[ZServerEndpoint[Any, Any]] = - List( - postMaintenanceEndpoint, - needsTopLeftCorrectionEndpoint, - wasTopLeftCorrectionAppliedEndpoint, - ) + val endpoints: List[ZServerEndpoint[Any, Any]] = List( + postMaintenanceEndpoint, + needsTopLeftCorrectionEndpoint, + wasTopLeftCorrectionAppliedEndpoint, + ) } object MaintenanceEndpointsHandler { From 52dec9ddadd3b45a0c5e0c939604bcb7fafc33a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:33:18 +0200 Subject: [PATCH 19/99] remove Public and SecuredHandlers --- .../org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala index 86ec1b9071f..fb1670209fa 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala @@ -12,7 +12,7 @@ case class ShaclServerEndpoints( private val shaclEndpoints: ShaclEndpoints, private val shaclApiService: ShaclApiService, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( shaclEndpoints.validate.zServerLogic(shaclApiService.validate), ) } From a9f4cae65ac6e259ff469a8d5d21ca53d3c0bf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:34:20 +0200 Subject: [PATCH 20/99] remove Public and SecuredHandlers --- .../knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala index fb1670209fa..47e0f7a6674 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala @@ -5,6 +5,7 @@ package org.knora.webapi.slice.shacl.api +import sttp.capabilities.zio.ZioStreams import zio.* import sttp.tapir.ztapir.* @@ -12,7 +13,7 @@ case class ShaclServerEndpoints( private val shaclEndpoints: ShaclEndpoints, private val shaclApiService: ShaclApiService, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( + val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = List( shaclEndpoints.validate.zServerLogic(shaclApiService.validate), ) } From f1173570bf6449e2a70d318108bb1bef2d96ba4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:35:06 +0200 Subject: [PATCH 21/99] remove Public and SecuredHandlers --- .../slice/resources/api/ResourceInfoServerEndpoints.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala index 2ee691f3727..1e98f4dec2c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala @@ -16,7 +16,7 @@ final case class ResourceInfoServerEndpoints( private val endpoints: ResourceInfoEndpoints, private val resourceInfoService: ResourceInfoRestService, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getResourcesInfo.zServerLogic(resourceInfoService.findByProjectAndResourceClass), ) } From 79ac657165c2b8c80b3c8e49ebe14dac44666f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:35:38 +0200 Subject: [PATCH 22/99] remove Public and SecuredHandlers --- .../slice/infrastructure/api/ManagementServerEndpoints.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala index d62ad1bcfca..8d658d0910b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/api/ManagementServerEndpoints.scala @@ -12,10 +12,10 @@ final case class ManagementServerEndpoints( private val endpoint: ManagementEndpoints, private val restService: ManagementRestService, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = Seq( + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoint.getVersion.zServerLogic(_ => ZIO.succeed(VersionResponse.current)), endpoint.getHealth.zServerLogic(_ => restService.healthCheck), - endpoint.postStartCompaction.zServerLogic(restService.startCompaction), + endpoint.postStartCompaction.serverLogic(restService.startCompaction), ) } object ManagementServerEndpoints { From 69990dd593971ac862d8f1e4df67ff1e3a3dca14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:36:05 +0200 Subject: [PATCH 23/99] remove Public and SecuredHandlers --- .../src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala index 84016ec15fd..70bdef89508 100644 --- a/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/ReportEndpointsHandler.scala @@ -18,7 +18,7 @@ final class ReportEndpointsHandler( ) { private val postAssetOverviewReportHandler: ZServerEndpoint[Any, Any] = - reportEndpoints.postAssetOverviewReport.zServerLogic(_ => + reportEndpoints.postAssetOverviewReport.serverLogic(_ => _ => createAssetOverReports.forkDaemon.logError.as("work in progress"), ) From 11f416be9e789f2e90c2fcb3b7d25fb31e2decbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:38:05 +0200 Subject: [PATCH 24/99] remove Public and SecuredHandlers --- .../dasch/api/ProjectsEndpointsHandler.scala | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala b/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala index de49af27373..1d5d654c336 100644 --- a/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala +++ b/ingest/src/main/scala/swiss/dasch/api/ProjectsEndpointsHandler.scala @@ -42,7 +42,7 @@ final case class ProjectsEndpointsHandler( ) extends HandlerFunctions { val getProjectsEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectsEndpoint - .zServerLogic(userSession => + .serverLogic(userSession => _ => authorizationHandler.ensureAdminScope(userSession) *> projectService @@ -55,7 +55,7 @@ final case class ProjectsEndpointsHandler( ) val getProjectByShortcodeEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectByShortcodeEndpoint - .zServerLogic(userSession => + .serverLogic(userSession => shortcode => authorizationHandler.ensureProjectReadable(userSession, shortcode) *> projectService @@ -68,7 +68,7 @@ final case class ProjectsEndpointsHandler( ) private val getProjectChecksumReportEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getProjectsChecksumReport - .zServerLogic(userSession => + .serverLogic(userSession => shortcode => authorizationHandler.ensureProjectReadable(userSession, shortcode) *> reportService @@ -81,7 +81,7 @@ final case class ProjectsEndpointsHandler( ) private val deleteProjectsEraseEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.deleteProjectsErase - .zServerLogic(userSession => + .serverLogic(userSession => shortcode => authorizationHandler.ensureAdminScope(userSession) *> projectService.findProject(shortcode).some.mapError(projectNotFoundOrServerError(_, shortcode)) *> { @@ -99,7 +99,7 @@ final case class ProjectsEndpointsHandler( ) private val getProjectsAssetsInfoEndpoint: ZServerEndpoint[Any, Any] = - projectEndpoints.getProjectsAssetsInfo.zServerLogic { userSession => (shortcode, assetId) => + projectEndpoints.getProjectsAssetsInfo.serverLogic { userSession => (shortcode, assetId) => val ref = AssetRef(assetId, shortcode) authorizationHandler.ensureProjectReadable(userSession, shortcode) *> assetInfoService @@ -113,7 +113,7 @@ final case class ProjectsEndpointsHandler( private val getProjectsAssetsOriginalEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.getProjectsAssetsOriginal - .zServerLogic(userSession => + .serverLogic(userSession => (shortcode, assetId) => for { ref <- ZIO.succeed(AssetRef(assetId, shortcode)) @@ -132,7 +132,7 @@ final case class ProjectsEndpointsHandler( private val ChunkSize = 64 * 1024 // larger chunk size; better for larger files private val postProjectAssetEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postProjectAsset - .zServerLogic(principal => { case (shortcode, filename, stream) => + .serverLogic(principal => { case (shortcode, filename, stream) => authorizationHandler.ensureProjectWritable(principal, shortcode) *> ZIO.scoped { for { @@ -149,7 +149,7 @@ final case class ProjectsEndpointsHandler( }) private val postBulkIngestEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.postBulkIngest - .zServerLogic(userSession => + .serverLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -164,7 +164,7 @@ final case class ProjectsEndpointsHandler( ) private val postBulkIngestEndpointFinalize: ZServerEndpoint[Any, Any] = projectEndpoints.postBulkIngestFinalize - .zServerLogic(userSession => + .serverLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -180,7 +180,7 @@ final case class ProjectsEndpointsHandler( private val getBulkIngestMappingCsvEndpoint: ZServerEndpoint[Any, Any] = projectEndpoints.getBulkIngestMappingCsv - .zServerLogic(userSession => + .serverLogic(userSession => code => authorizationHandler.ensureProjectWritable(userSession, code) *> bulkIngestService @@ -193,7 +193,7 @@ final case class ProjectsEndpointsHandler( ) private val postBulkIngestUploadEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postBulkIngestUpload - .zServerLogic(principal => { case (shortcode, filename, stream) => + .serverLogic(principal => { case (shortcode, filename, stream) => for { _ <- authorizationHandler.ensureProjectWritable(principal, shortcode) s <- bulkIngestService.uploadSingleFile(shortcode, filename, stream).mapError { @@ -206,7 +206,7 @@ final case class ProjectsEndpointsHandler( Conflict(s"A bulk ingest is currently in progress for project ${code.value}.") private val postExportEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.postExport - .zServerLogic(userSession => + .serverLogic(userSession => shortcode => authorizationHandler.ensureAdminScope(userSession) *> projectService @@ -224,7 +224,7 @@ final case class ProjectsEndpointsHandler( ) private val getImportEndpoint: ZServerEndpoint[Any, ZioStreams] = projectEndpoints.getImport - .zServerLogic(userSession => + .serverLogic(userSession => (shortcode, stream) => authorizationHandler.ensureAdminScope(userSession) *> importService From 2160952d58b3904ddb31c51d52757f8298eda2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 15:41:21 +0200 Subject: [PATCH 25/99] remove Public and SecuredHandlers --- .../{CompleteApiServerEndpoints.scala => Endpoints.scala} | 7 ++----- .../webapi/slice/admin/api/AdminApiServerEndpoints.scala | 3 ++- .../slice/resources/api/ResourcesApiServerEndpoints.scala | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) rename webapi/src/main/scala/org/knora/webapi/routing/{CompleteApiServerEndpoints.scala => Endpoints.scala} (91%) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala similarity index 91% rename from webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala rename to webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala index e188d78c48b..5bcebf4c445 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/CompleteApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala @@ -18,11 +18,8 @@ import org.knora.webapi.slice.search.api.SearchServerEndpoints import org.knora.webapi.slice.security.api.AuthenticationServerEndpoints import org.knora.webapi.slice.shacl.api.ShaclServerEndpoints import sttp.tapir.ztapir.* +import sttp.capabilities.zio.ZioStreams -/** - * ALL requests go through each of the routes in ORDER. - * The FIRST matching route is used for handling a request. - */ final case class Endpoints( adminApiServerEndpoints: AdminApiServerEndpoints, authenticationServerEndpoints: AuthenticationServerEndpoints, @@ -34,7 +31,7 @@ final case class Endpoints( managementServerEndpoints: ManagementServerEndpoints, ontologiesServerEndpoints: OntologiesServerEndpoints, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = + val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = adminApiServerEndpoints.serverEndpoints ++ authenticationServerEndpoints.serverEndpoints ++ listsV2ServerEndpoints.serverEndpoints ++ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index 2111d2ad130..0ef02ae2e3e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -8,6 +8,7 @@ package org.knora.webapi.slice.admin.api import zio.* import sttp.tapir.ztapir.* +import sttp.capabilities.zio.ZioStreams final case class AdminApiServerEndpoints( private val adminListsServerEndpoints: AdminListsServerEndpoints, @@ -21,7 +22,7 @@ final case class AdminApiServerEndpoints( private val usersServerEndpoints: UsersServerEndpoints, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = + val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = filesServerEndpoints.serverEndpoints ++ groupsServerEndpoints.serverEndpoints ++ adminListsServerEndpoints.serverEndpoints ++ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala index 21515f9951f..472b1a81b78 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala @@ -4,7 +4,8 @@ */ package org.knora.webapi.slice.resources.api -import zio.ZLayer +import zio.* +import sttp.tapir.ztapir.* final case class ResourcesApiServerEndpoints( private val metadataServerEndpoints: MetadataServerEndpoints, From 38afd791701a593f00e16cc93696308cbdab94f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 16:01:17 +0200 Subject: [PATCH 26/99] remove Public and SecuredHandlers --- .../admin/api/ProjectsServerEndpoints.scala | 18 +++--------------- .../admin/api/service/ProjectRestService.scala | 11 +++++++---- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala index c5f45704be6..b7f316cfcda 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala @@ -11,7 +11,6 @@ import zio.stream.* import java.nio.file.Files import scala.concurrent.ExecutionContext - import org.knora.webapi.slice.admin.api.model.* import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest @@ -22,25 +21,14 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.User +import sttp.capabilities.zio.ZioStreams final case class ProjectsServerEndpoints( private val projectsEndpoints: ProjectsEndpoints, private val restService: ProjectRestService, ) { - private val getAdminProjectsByIriAllDataHandler = - projectsEndpoints.Secured.getAdminProjectsByIriAllData.serverLogic((user: User) => - (iri: ProjectIri) => - restService - .getAllProjectData(user)(iri) - .map { result => - val path = result.projectDataFile - val stream = ZStream.fromPath(path).ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) - (s"attachment; filename=project-data.trig", "application/octet-stream", stream) - }, - ) - - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( + val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = List( projectsEndpoints.Public.getAdminProjects.zServerLogic(restService.listAllProjects), projectsEndpoints.Public.getAdminProjectsKeywords.zServerLogic(restService.listAllKeywords), projectsEndpoints.Public.getAdminProjectsByProjectIri.zServerLogic(restService.findById), @@ -53,7 +41,7 @@ final case class ProjectsServerEndpoints( .zServerLogic(restService.getProjectRestrictedViewSettingsByShortcode), projectsEndpoints.Public.getAdminProjectsByProjectShortnameRestrictedViewSettings .zServerLogic(restService.getProjectRestrictedViewSettingsByShortname), - getAdminProjectsByIriAllDataHandler, + projectsEndpoints.Secured.getAdminProjectsByIriAllData.serverLogic(restService.getAllProjectData), projectsEndpoints.Secured.postAdminProjectsByProjectIriRestrictedViewSettings .serverLogic(restService.updateProjectRestrictedViewSettingsById), projectsEndpoints.Secured.postAdminProjectsByProjectShortcodeRestrictedViewSettings diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala index 193995e2c21..8b6b710fa74 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala @@ -5,6 +5,7 @@ package org.knora.webapi.slice.admin.api.service +import zio.stream.ZStream import zio.* import dsp.errors.BadRequestException @@ -34,6 +35,7 @@ import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.store.triplestore.api.TriplestoreService +import zio.nio.file.Files final case class ProjectRestService( private val format: KnoraResponseRenderer, @@ -177,16 +179,17 @@ final case class ProjectRestService( * @param id the [[IriIdentifier]] of the project * @param user the [[User]] making the request * @return - * '''success''': data of the project as [[ProjectDataGetResponseADM]] + * '''success''': data of the project for download as a stream of bytes, along with a suggested filename and a MIME type * * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[IriIdentifier]] can be found * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ - def getAllProjectData(user: User)(id: ProjectIri): Task[ProjectDataGetResponseADM] = + def getAllProjectData(user: User)(id: ProjectIri): Task[(String, String, ZStream[Any, Throwable, Byte])] = for { project <- auth.ensureSystemAdminOrProjectAdminById(user, id) - result <- projectExportService.exportProjectTriples(project).map(_.toFile.toPath) - } yield ProjectDataGetResponseADM(result) + path <- projectExportService.exportProjectTriples(project).map(_.toFile.toPath) + stream = ZStream.fromPath(path).ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) + } yield (s"attachment; filename=project-data.trig", "application/octet-stream", stream) def getProjectMembersById(user: User)(id: ProjectIri): Task[ProjectMembersGetResponseADM] = auth.ensureSystemAdminOrProjectAdminById(user, id).flatMap(findProjectMembers) From 7ec9d6ac717c5fa48829855200d77077e31e0577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 16:03:46 +0200 Subject: [PATCH 27/99] remove Public and SecuredHandlers --- .../knora/webapi/slice/admin/api/StoreServerEndpoints.scala | 6 +++--- .../webapi/slice/admin/api/service/ProjectRestService.scala | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala index 3f686ca9c57..da79eb0811f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala @@ -18,10 +18,10 @@ final case class StoreServerEndpoints( private val restService: StoreRestService, ) { - val serverEndpoints = + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = if (appConfig.allowReloadOverHttp) - Seq(endpoints.postStoreResetTriplestoreContent.zServerLogic(restService.resetTriplestoreContent)) - else Seq.empty + List(endpoints.postStoreResetTriplestoreContent.zServerLogic(restService.resetTriplestoreContent)) + else List.empty } object StoreServerEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala index 8b6b710fa74..234e21b5703 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala @@ -187,8 +187,8 @@ final case class ProjectRestService( def getAllProjectData(user: User)(id: ProjectIri): Task[(String, String, ZStream[Any, Throwable, Byte])] = for { project <- auth.ensureSystemAdminOrProjectAdminById(user, id) - path <- projectExportService.exportProjectTriples(project).map(_.toFile.toPath) - stream = ZStream.fromPath(path).ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) + path <- projectExportService.exportProjectTriples(project) + stream = ZStream.fromPath(path.toFile.toPath).ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) } yield (s"attachment; filename=project-data.trig", "application/octet-stream", stream) def getProjectMembersById(user: User)(id: ProjectIri): Task[ProjectMembersGetResponseADM] = From 43d7c549bd3409c40faa44c39891f5a3047dc1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 17:02:48 +0200 Subject: [PATCH 28/99] fix layers --- .../responders/admin/ListsResponderSpec.scala | 6 +- .../org/knora/webapi/core/LayersTest.scala | 22 +----- .../main/scala/org/knora/webapi/Main.scala | 8 +- .../org/knora/webapi/config/AppConfig.scala | 5 +- .../org/knora/webapi/core/HttpServer.scala | 16 ++-- .../org/knora/webapi/core/LayersLive.scala | 79 +++++-------------- .../org/knora/webapi/routing/Endpoints.scala | 6 +- .../admin/api/AdminApiServerEndpoints.scala | 5 +- .../admin/api/AdminListsServerEndpoints.scala | 12 +-- .../admin/api/FilesServerEndpoints.scala | 3 +- .../admin/api/GroupsServerEndpoints.scala | 3 +- .../api/MaintenanceServerEndpoints.scala | 3 +- .../api/PermissionsServerEndpoints.scala | 11 +-- .../ProjectsLegalInfoServerEndpoints.scala | 2 +- .../admin/api/ProjectsServerEndpoints.scala | 7 +- .../admin/api/StoreServerEndpoints.scala | 2 +- .../admin/api/UsersServerEndpoints.scala | 3 +- .../admin/api/service/GroupRestService.scala | 2 +- .../api/service/ProjectRestService.scala | 4 +- .../slice/common/api/BaseEndpoints.scala | 8 +- .../lists/api/ListsV2ServerEndpoints.scala | 2 +- .../api/OntologiesServerEndpoints.scala | 2 +- .../api/MetadataServerEndpoints.scala | 2 +- .../api/ResourceInfoServerEndpoints.scala | 3 +- .../resources/api/ResourcesApiModule.scala | 1 + .../api/ResourcesApiServerEndpoints.scala | 2 +- .../api/StandoffServerEndpoints.scala | 2 +- .../resources/api/ValuesServerEndpoints.scala | 4 +- .../api/AuthenticationServerEndpoints.scala | 6 +- .../slice/shacl/api/ShaclApiService.scala | 3 +- .../shacl/api/ShaclServerEndpoints.scala | 2 +- .../slice/shacl/api/ShaclApiServiceSpec.scala | 4 +- 32 files changed, 69 insertions(+), 171 deletions(-) diff --git a/modules/test-it/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala b/modules/test-it/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala index ca02b5a88bd..51f910c8e7b 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala @@ -100,7 +100,7 @@ object ListsResponderSpec extends E2EZSpec { .map(actual => assertTrue(actual.nodeinfo.sorted == summerNodeInfo.sorted)) }, test("return a full list response") { - listsResponder(_.listGetRequestADM("http://rdfh.ch/lists/0001/treeList")) + listsResponder(_.listGetRequestADM(ListIri.unsafeFrom("http://rdfh.ch/lists/0001/treeList"))) .flatMap(expectType[ListGetResponseADM]) .map(actual => assertTrue( @@ -423,7 +423,7 @@ object ListsResponderSpec extends E2EZSpec { UUID.randomUUID, ), ).map(_.node) - val actual = listsResponder(_.listGetRequestADM(oldParentIri.value)).flatMap(expectType[ListNodeGetResponseADM]) + val actual = listsResponder(_.listGetRequestADM(oldParentIri)).flatMap(expectType[ListNodeGetResponseADM]) (parentNode <*> actual).map { (parentNode: ListNodeADM, actual: ListNodeGetResponseADM) => @@ -471,7 +471,7 @@ object ListsResponderSpec extends E2EZSpec { ), ).map(_.node) - val actual = listsResponder(_.listGetRequestADM(oldParentIri.value)) + val actual = listsResponder(_.listGetRequestADM(oldParentIri)) .flatMap(expectType[ListNodeGetResponseADM]) (parentNode <*> actual).map { (parentNode: ListNodeADM, actual: ListNodeGetResponseADM) => diff --git a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala index f2459e2c854..2c44a765ee9 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala @@ -7,11 +7,8 @@ package org.knora.webapi.core import zio.* -import org.knora.webapi.config.* -import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.messages.util.* import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 -import org.knora.webapi.responders.IriService import org.knora.webapi.responders.admin.* import org.knora.webapi.responders.v2.* import org.knora.webapi.responders.v2.ontology.CardinalityHandler @@ -19,14 +16,10 @@ import org.knora.webapi.routing.* import org.knora.webapi.slice.admin.domain.service.* import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.api.* -import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater -import org.knora.webapi.testcontainers.DspIngestTestContainer -import org.knora.webapi.testcontainers.FusekiTestContainer -import org.knora.webapi.testcontainers.SipiTestContainer import org.knora.webapi.testservices.TestClientsModule object LayersTest { self => @@ -42,18 +35,7 @@ object LayersTest { self => * Provides a layer for integration tests which depend on Fuseki and Sipi as Testcontainers. * @return a [[ULayer]] with the [[DefaultTestEnvironmentWithSipi]] */ - val layer: ULayer[self.Environment] = { - // Custom bootstrap for tests that uses TestContainer configs instead of LayersLive.bootstrap - val testBootstrap = - TestContainerLayers.all >+> - LayersLive.intermediateLayers1 >+> - LayersLive.intermediateLayers2 >+> - LayersLive.intermediateLayers3 >+> - LayersLive.remainingLayer + val layer: ULayer[self.Environment] = + TestContainerLayers.all >+> LayersLive.remainingLayer >+> TestClientsModule.layer - ZLayer.make[self.Environment]( - testBootstrap, - TestClientsModule.layer, - ) - } } diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index 0a874a8503a..d4066248eea 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -7,10 +7,10 @@ package org.knora.webapi import zio.* +import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.core.* -import org.knora.webapi.core.Db.DbInitEnv +import org.knora.webapi.core.LayersLive.Environment import org.knora.webapi.slice.infrastructure.MetricsServer -import org.knora.webapi.slice.infrastructure.MetricsServer.MetricsServerEnv object Main extends ZIOApp { @@ -19,12 +19,12 @@ object Main extends ZIOApp { /** * The `Environment` that we require to exist at startup. */ - override type Environment = LayersLive.ApplicationEnvironment + override type Environment = AppConfigurations & LayersLive.Environment /** * The layers provided to the application. */ - override def bootstrap: ZLayer[Any, Nothing, Environment] = LayersLive.bootstrap + override def bootstrap: ULayer[Environment] = LayersLive.bootstrap /** * Entrypoint of our Application diff --git a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala index 8b7a7638054..2b61bfac35f 100644 --- a/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala +++ b/webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala @@ -13,8 +13,6 @@ import zio.config.typesafe.* import java.time.Duration -import org.knora.webapi.core.LayersLive - /** * Represents the configuration as defined in application.conf. */ @@ -187,7 +185,8 @@ final case class OpenTelemetryConfig( ) object AppConfig { - type AppConfigurations = LayersLive.Config & InstrumentationServerConfig & JwtConfig & DspIngestConfig + type AppConfigurations = AppConfig & DspIngestConfig & InstrumentationServerConfig & KnoraApi & Sipi & Triplestore & + Features & GraphRoute & JwtConfig & OpenTelemetryConfig val parseConfig: UIO[AppConfig] = { val descriptor = deriveConfig[AppConfig].mapKey(toKebabCase) diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index f30cea19409..a71ede078a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -5,28 +5,26 @@ package org.knora.webapi.core +import sttp.tapir.server.ziohttp.ZioHttpInterpreter +import sttp.tapir.server.ziohttp.ZioHttpServerOptions import zio.* import zio.http.* -import org.knora.webapi.config.AppConfig + import org.knora.webapi.config.KnoraApi import org.knora.webapi.routing.Endpoints -import sttp.tapir.server.interceptor.cors.CORSConfig.AllowedOrigin -import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor} -import sttp.tapir.server.metrics.zio.ZioMetrics -import sttp.tapir.server.ziohttp.{ZioHttpServerOptions, ZioHttpInterpreter} -import sttp.tapir.ztapir.* object HttpServer { private def options = ZioHttpServerOptions.default - val layer = ZLayer.scoped(createServer) + val layer = ZLayer.scoped(createServer).orDie private def createServer = for { apiConfig <- ZIO.service[KnoraApi] endpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) httpApp = ZioHttpInterpreter(options).toHttp(endpoints) - _ <- Server.install(httpApp).provide(Server.defaultWithPort(apiConfig.internalPort)) - _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") +// _ <- Server.install(httpApp).provide(Server.defaultWithPort(apiConfig.internalPort)) + _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") + _ <- ZIO.never } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 8229b6db09e..f7427d9ea0a 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -6,7 +6,9 @@ package org.knora.webapi.core import zio.* + import org.knora.webapi.config.AppConfig +import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.config.DspIngestConfig import org.knora.webapi.config.Features import org.knora.webapi.config.GraphRoute @@ -15,7 +17,6 @@ import org.knora.webapi.config.KnoraApi import org.knora.webapi.config.OpenTelemetryConfig import org.knora.webapi.config.Sipi import org.knora.webapi.config.Triplestore -import org.knora.webapi.core.Db.DbInitEnv import org.knora.webapi.messages.util.* import org.knora.webapi.messages.util.search.QueryTraverser import org.knora.webapi.messages.util.search.gravsearch.transformers.OntologyInferencer @@ -39,7 +40,6 @@ import org.knora.webapi.slice.common.service.IriConverter import org.knora.webapi.slice.infrastructure.InfrastructureModule import org.knora.webapi.slice.infrastructure.InfrastructureModule.Provided import org.knora.webapi.slice.infrastructure.JwtService -import org.knora.webapi.slice.infrastructure.MetricsServer.MetricsServerEnv import org.knora.webapi.slice.infrastructure.OpenTelemetry import org.knora.webapi.slice.infrastructure.api.ManagementEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementRestService @@ -56,8 +56,8 @@ import org.knora.webapi.slice.resources.api.ResourcesApiModule import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints import org.knora.webapi.slice.resources.repo.service.ResourcesRepo import org.knora.webapi.slice.resources.repo.service.ResourcesRepoLive -import org.knora.webapi.slice.search.api.SearchServerEndpoints import org.knora.webapi.slice.search.api.SearchEndpoints +import org.knora.webapi.slice.search.api.SearchServerEndpoints import org.knora.webapi.slice.security.SecurityModule import org.knora.webapi.slice.security.api.AuthenticationApiModule import org.knora.webapi.slice.shacl.ShaclModule @@ -72,59 +72,6 @@ import org.knora.webapi.util.Logger object LayersLive { self => - /* - * This layer composition is done really happhazardly. - * This can certainly be improved a lot. - * However, this brings the layers ependency graph from ~12.7k to ~2.9k nodes. - * (By that I'm refering to the text output one gets when enabling `ZLayer.Debug.mermaid`.) - * It is unclear how closely this relates to the actual graph construction code, - * but I expect it will prevent the `class too big` error that we have seen in the past. - * It seems to have little or no impact on compile time. - */ - - private type ConfigDependencies = Any - private type ConfigProvided = AppConfig.AppConfigurations - - private type IntermediateDependencies1 = AppConfig & Triplestore & Features & DspIngestConfig & JwtConfig - private type IntermediateProvided1 = CommonModule.Provided & IriService & OntologyModule.Provided & - InfrastructureModule.Provided - - private type IntermediateDependencies3 = IntermediateProvided1 & Features & DspIngestConfig & JwtConfig & - DspIngestClient & PredicateObjectMapper & AppConfig - private type IntermediateProvided3 = AdminModule.Provided & IntermediateProvided1 - - type ApplicationEnvironment = DbInitEnv & MetricsServerEnv - - private val configLayer: ZLayer[self.ConfigDependencies, Nothing, self.ConfigProvided] = - Logger.fromEnv() >>> AppConfig.layer - - val intermediateLayers1: ZLayer[self.IntermediateDependencies1, Nothing, self.IntermediateProvided1] = - ZLayer.makeSome[self.IntermediateDependencies1, self.IntermediateProvided1]( - CommonModule.layer, - IriService.layer, - OntologyModule.layer, - InfrastructureModule.layer, - ) - - val intermediateLayers2 - : ZLayer[JwtService & DspIngestConfig & IriConverter, Nothing, DspIngestClient & PredicateObjectMapper] = - ZLayer.makeSome[JwtService & DspIngestConfig & IriConverter, DspIngestClient & PredicateObjectMapper]( - DspIngestClientLive.layer, - PredicateObjectMapper.layer, - ) - - val intermediateLayers3 - : ZLayer[self.IntermediateDependencies3, Nothing, self.IntermediateProvided3 & DspIngestConfig & JwtConfig] = - ZLayer.makeSome[self.IntermediateDependencies3, self.IntermediateProvided3 & DspIngestConfig & JwtConfig]( - AdminModule.layer, - ) - - private val intermediateLayersAll = - configLayer >+> intermediateLayers1 >+> intermediateLayers2 >+> intermediateLayers3 - - val bootstrap: ZLayer[Any, Nothing, ApplicationEnvironment] = - intermediateLayersAll >+> LayersLive.remainingLayer - /** * The `Environment` that we require to exist at startup. */ @@ -176,14 +123,15 @@ object LayersLive { self => ValuesResponderV2 // format: on - type Config = AppConfig & Features & GraphRoute & KnoraApi & OpenTelemetryConfig & Sipi & Triplestore - - val remainingLayer = + val remainingLayer: URLayer[AppConfigurations, Environment] = ZLayer.makeSome[ - Config & IntermediateProvided1 & IntermediateProvided3 & DspIngestClient & PredicateObjectMapper, + AppConfig & DspIngestConfig & KnoraApi & Sipi & Triplestore & Features & GraphRoute & JwtConfig & + OpenTelemetryConfig, self.Environment, ]( + // ZLayer.Debug.mermaid, AdminApiModule.layer, + AdminModule.layer, ApiComplexV2JsonLdRequestParser.layer, ApiV2Endpoints.layer, AssetPermissionsResponder.layer, @@ -191,10 +139,14 @@ object LayersLive { self => AuthorizationRestService.layer, BaseEndpoints.layer, CardinalityHandler.layer, + CommonModule.layer, ConstructResponseUtilV2.layer, + DspIngestClientLive.layer, Endpoints.layer, HttpServer.layer, IIIFRequestMessageHandlerLive.layer, + InfrastructureModule.layer, + IriService.layer, KnoraResponseRenderer.layer, ListsApiModule.layer, ListsResponder.layer, @@ -203,10 +155,12 @@ object LayersLive { self => ManagementServerEndpoints.layer, MessageRelayLive.layer, OntologyApiModule.layer, + OntologyModule.layer, OntologyResponderV2.layer, OpenTelemetry.layer, PermissionUtilADMLive.layer, PermissionsResponder.layer, + PredicateObjectMapper.layer, ProjectExportServiceLive.layer, ProjectExportStorageServiceLive.layer, ProjectImportService.layer, @@ -227,6 +181,9 @@ object LayersLive { self => StandoffTagUtilV2Live.layer, State.layer, ValuesResponderV2.layer, - // ZLayer.Debug.mermaid, ) + + private val loggerAndConfig: ULayer[AppConfigurations] = Logger.fromEnv() >>> AppConfig.layer + + val bootstrap: ULayer[AppConfigurations & Environment] = loggerAndConfig >+> LayersLive.remainingLayer } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala index 5bcebf4c445..0d9a255047e 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala @@ -5,8 +5,10 @@ package org.knora.webapi.routing +import sttp.capabilities.zio.ZioStreams +import sttp.tapir.ztapir.* import zio.* -import org.knora.webapi.http.version.ServerVersion + import org.knora.webapi.routing import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementServerEndpoints @@ -17,8 +19,6 @@ import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints import org.knora.webapi.slice.search.api.SearchServerEndpoints import org.knora.webapi.slice.security.api.AuthenticationServerEndpoints import org.knora.webapi.slice.shacl.api.ShaclServerEndpoints -import sttp.tapir.ztapir.* -import sttp.capabilities.zio.ZioStreams final case class Endpoints( adminApiServerEndpoints: AdminApiServerEndpoints, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala index 0ef02ae2e3e..1d62685f497 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiServerEndpoints.scala @@ -5,10 +5,9 @@ package org.knora.webapi.slice.admin.api -import zio.* - -import sttp.tapir.ztapir.* import sttp.capabilities.zio.ZioStreams +import sttp.tapir.ztapir.* +import zio.* final case class AdminApiServerEndpoints( private val adminListsServerEndpoints: AdminListsServerEndpoints, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala index c16681e1376..f269e4766de 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminListsServerEndpoints.scala @@ -5,19 +5,9 @@ package org.knora.webapi.slice.admin.api -import zio.* -import zio.ZLayer - import sttp.tapir.ztapir.* +import zio.* -import dsp.errors.BadRequestException -import org.knora.webapi.messages.admin.responder.listsmessages.CanDeleteListResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ChildNodeInfoGetResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListGetResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListItemDeleteResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.ListNodeCommentsDeleteResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.NodeInfoGetResponseADM -import org.knora.webapi.messages.admin.responder.listsmessages.NodePositionChangeResponseADM import org.knora.webapi.slice.admin.api.Requests.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala index 1ace4e4e726..3a42d366dd0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/FilesServerEndpoints.scala @@ -5,9 +5,8 @@ package org.knora.webapi.slice.admin.api -import zio.* - import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.responders.admin.AssetPermissionsResponder import org.knora.webapi.slice.admin.api.model.PermissionCodeAndProjectRestrictedViewSettings diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala index 3590607192a..17d1fc4cd89 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/GroupsServerEndpoints.scala @@ -5,10 +5,9 @@ package org.knora.webapi.slice.admin.api -import zio.* import sttp.tapir.ztapir.* +import zio.* -import org.knora.webapi.messages.admin.responder.groupsmessages.GroupGetResponseADM import org.knora.webapi.slice.admin.api.GroupsRequests.GroupStatusUpdateRequest import org.knora.webapi.slice.admin.api.GroupsRequests.GroupUpdateRequest import org.knora.webapi.slice.admin.api.service.GroupRestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala index 2fc3e8a9b48..6d6f133dbb5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceServerEndpoints.scala @@ -5,11 +5,10 @@ package org.knora.webapi.slice.admin.api +import sttp.tapir.ztapir.* import zio.* import zio.json.ast.Json -import sttp.tapir.ztapir.* - import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.domain.model.User diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala index 0dfc6a55751..7940323f834 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/PermissionsServerEndpoints.scala @@ -5,24 +5,15 @@ package org.knora.webapi.slice.admin.api -import zio.* import sttp.tapir.ztapir.* +import zio.* -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionCreateResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.AdministrativePermissionsForProjectGetResponseADM import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionGroupApiRequestADM import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionHasPermissionsApiRequestADM import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionPropertyApiRequestADM import org.knora.webapi.messages.admin.responder.permissionsmessages.ChangePermissionResourceClassApiRequestADM import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateAdministrativePermissionAPIRequestADM import org.knora.webapi.messages.admin.responder.permissionsmessages.CreateDefaultObjectAccessPermissionAPIRequestADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionCreateResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.DefaultObjectAccessPermissionsForProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionDeleteResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionGetResponseADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsForProjectGetResponseADM import org.knora.webapi.slice.admin.api.PermissionEndpointsRequests.ChangeDoapRequest import org.knora.webapi.slice.admin.api.service.PermissionRestService import org.knora.webapi.slice.admin.domain.model.GroupIri diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala index e93fc250c20..7354df5f950 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsLegalInfoServerEndpoints.scala @@ -4,8 +4,8 @@ */ package org.knora.webapi.slice.admin.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.admin.api.service.ProjectsLegalInfoRestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala index b7f316cfcda..78a1e4cd6ab 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsServerEndpoints.scala @@ -5,23 +5,18 @@ package org.knora.webapi.slice.admin.api +import sttp.capabilities.zio.ZioStreams import sttp.tapir.ztapir.* import zio.* -import zio.stream.* -import java.nio.file.Files -import scala.concurrent.ExecutionContext -import org.knora.webapi.slice.admin.api.model.* import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectUpdateRequest -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.RestrictedViewResponse import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.SetRestrictedViewRequest import org.knora.webapi.slice.admin.api.service.ProjectRestService import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname import org.knora.webapi.slice.admin.domain.model.User -import sttp.capabilities.zio.ZioStreams final case class ProjectsServerEndpoints( private val projectsEndpoints: ProjectsEndpoints, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala index da79eb0811f..b291fd31296 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/StoreServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.admin.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala index 15c96ac9009..6d02a0ff4a1 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/UsersServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.admin.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.BasicUserInformationChangeRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.PasswordChangeRequest @@ -14,7 +14,6 @@ import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.StatusChangeRequ import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.SystemAdminChangeRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest import org.knora.webapi.slice.admin.api.service.UserRestService -import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.GroupIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala index 0694d867e89..91eb8402d5d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/GroupRestService.scala @@ -6,9 +6,9 @@ package org.knora.webapi.slice.admin.api.service import zio.* + import dsp.errors.BadRequestException import dsp.errors.NotFoundException -import dsp.errors.RequestRejectedException import org.knora.webapi.messages.admin.responder.groupsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.GroupMembersGetResponseADM import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala index 234e21b5703..88ca8f9284f 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectRestService.scala @@ -5,8 +5,9 @@ package org.knora.webapi.slice.admin.api.service -import zio.stream.ZStream import zio.* +import zio.nio.file.Files +import zio.stream.ZStream import dsp.errors.BadRequestException import dsp.errors.ForbiddenException @@ -35,7 +36,6 @@ import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.store.triplestore.api.TriplestoreService -import zio.nio.file.Files final case class ProjectRestService( private val format: KnoraResponseRenderer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala index 90f39f4b82c..21daa911494 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -7,20 +7,20 @@ package org.knora.webapi.slice.common.api import sttp.model.StatusCode import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir.ztapir.* -import sttp.tapir.{PublicEndpoint, EndpointOutput, Validator} +import sttp.tapir.EndpointOutput +import sttp.tapir.PublicEndpoint +import sttp.tapir.Validator import sttp.tapir.generic.auto.* import sttp.tapir.json.zio.jsonBody import sttp.tapir.model.UsernamePassword +import sttp.tapir.ztapir.* import zio.* -import scala.concurrent.Future import dsp.errors.* import org.knora.webapi.messages.util.KnoraSystemInstances.Users.AnonymousUser import org.knora.webapi.slice.admin.domain.model.Email import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.security.Authenticator -import sttp.tapir.Endpoint final case class BaseEndpoints(authenticator: Authenticator) { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala index 79edf6801ad..ebb335701a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsV2ServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.lists.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.lists.api.service.ListsV2RestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 223c5b9e26e..9d1c65b38a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.ontology.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.ontology.api.service.OntologiesRestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala index c22551e9399..c5c5243fd2a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.resources.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.resources.api.service.MetadataRestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala index 1e98f4dec2c..cff1f2c6aff 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourceInfoServerEndpoints.scala @@ -5,11 +5,10 @@ package org.knora.webapi.slice.resources.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.resources.api.model.ListResponseDto import org.knora.webapi.slice.resources.api.service.ResourceInfoRestService final case class ResourceInfoServerEndpoints( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala index 9bc37507a1e..b0e3f66c24e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiModule.scala @@ -69,6 +69,7 @@ object ResourcesApiModule { self => ResourcesEndpoints.layer, ResourcesRestService.layer, ResourcesServerEndpoints.layer, + ResourcesApiServerEndpoints.layer, StandoffEndpoints.layer, StandoffRestService.layer, StandoffServerEndpoints.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala index 472b1a81b78..afdaabbe2a4 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala @@ -4,8 +4,8 @@ */ package org.knora.webapi.slice.resources.api -import zio.* import sttp.tapir.ztapir.* +import zio.* final case class ResourcesApiServerEndpoints( private val metadataServerEndpoints: MetadataServerEndpoints, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala index d2a17371321..52659751be5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffServerEndpoints.scala @@ -5,8 +5,8 @@ package org.knora.webapi.slice.resources.api -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.resources.api.service.StandoffRestService diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala index a5769795062..06f88c09409 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesServerEndpoints.scala @@ -5,10 +5,8 @@ package org.knora.webapi.slice.resources.api -import sttp.model.MediaType - -import zio.* import sttp.tapir.ztapir.* +import zio.* import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.resources.api.model.ValueUuid diff --git a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala index 9ea3351481d..3a6aae45c40 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/security/api/AuthenticationServerEndpoints.scala @@ -4,16 +4,12 @@ */ package org.knora.webapi.slice.security.api -import sttp.model.headers.CookieValueWithMeta -import zio.* import sttp.tapir.ztapir.* +import zio.* -import org.knora.webapi.config.AppConfig import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.CheckResponse import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginForm import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload -import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutResponse -import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse case class AuthenticationServerEndpoints( private val restService: AuthenticationRestService, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala index 40527a32a10..7f8685971ff 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclApiService.scala @@ -11,11 +11,10 @@ import zio.* import zio.stream.* import java.io.FileInputStream + import org.knora.webapi.slice.shacl.domain.ShaclValidator import org.knora.webapi.slice.shacl.domain.ValidationOptions -import java.io.IOException - final case class ShaclApiService(private val validator: ShaclValidator) { def validate(formData: ValidationFormData): Task[ZStream[Any, Throwable, Byte]] = { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala index 47e0f7a6674..e67d6d17833 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclServerEndpoints.scala @@ -6,8 +6,8 @@ package org.knora.webapi.slice.shacl.api import sttp.capabilities.zio.ZioStreams -import zio.* import sttp.tapir.ztapir.* +import zio.* case class ShaclServerEndpoints( private val shaclEndpoints: ShaclEndpoints, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala index c5403d69287..8aa824f85e4 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/shacl/api/ShaclApiServiceSpec.scala @@ -8,10 +8,8 @@ package org.knora.webapi.slice.shacl.api import zio.* import zio.nio.file.Files import zio.test.* -import zio.stream.ZStream -import org.knora.webapi.slice.shacl.domain.ShaclValidator -import java.io.IOException +import org.knora.webapi.slice.shacl.domain.ShaclValidator object ShaclApiServiceSpec extends ZIOSpecDefault { From 0794fb0d73bbe96eb7572b2f7b6aeab600686d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 17:19:07 +0200 Subject: [PATCH 29/99] fix server --- .../scala/org/knora/webapi/core/HttpServer.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala index a71ede078a4..744dfcf829d 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala @@ -15,16 +15,16 @@ import org.knora.webapi.routing.Endpoints object HttpServer { - private def options = ZioHttpServerOptions.default + private def options: ZioHttpServerOptions[Any] = ZioHttpServerOptions.default - val layer = ZLayer.scoped(createServer).orDie + val layer = ZLayer.scoped(apiServer).orDie - private def createServer = for { + private def apiServer: ZIO[Endpoints & KnoraApi, Throwable, Unit] = for { apiConfig <- ZIO.service[KnoraApi] endpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) - httpApp = ZioHttpInterpreter(options).toHttp(endpoints) -// _ <- Server.install(httpApp).provide(Server.defaultWithPort(apiConfig.internalPort)) - _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") - _ <- ZIO.never + routes = ZioHttpInterpreter(options).toHttp(endpoints) + _ <- Server.install(routes).provide(Server.defaultWithPort(apiConfig.internalPort)): @annotation.nowarn + _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") + _ <- ZIO.never.unit } yield () } From ffde02e2762a8b1acc0d09bd020b0599545ff86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 17:23:51 +0200 Subject: [PATCH 30/99] organize imports --- .../knora/webapi/slice/resources/api/MetadataEndpoints.scala | 2 -- .../webapi/slice/resources/api/ResourcesEndpoints.scala | 5 ----- .../webapi/slice/search/api/SearchServerEndpoints.scala | 3 --- .../org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala | 1 - 4 files changed, 11 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala index e39df136596..eaca227a9dc 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/MetadataEndpoints.scala @@ -8,13 +8,11 @@ package org.knora.webapi.slice.resources.api import sttp.model.MediaType import sttp.tapir.* import sttp.tapir.Codec.PlainCodec -import sttp.tapir.server.PartialServerEndpoint import zio.ZLayer import zio.json.DeriveJsonCodec import zio.json.JsonCodec import java.time.Instant -import scala.concurrent.Future import org.knora.webapi.slice.admin.api.AdminPathVariables import org.knora.webapi.slice.admin.api.AdminPathVariables.projectShortcode diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index ee8195259d6..8deb0f0cd66 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -8,15 +8,10 @@ package org.knora.webapi.slice.resources.api import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* -import sttp.tapir.server.PartialServerEndpoint import zio.ZLayer -import scala.concurrent.Future - -import dsp.errors.RequestRejectedException import org.knora.webapi.config.GraphRoute import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.ApiV2 import org.knora.webapi.slice.common.api.BaseEndpoints import org.knora.webapi.slice.resources.api.model.GraphDirection diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala index 2b86bd0e107..61196558a5b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchServerEndpoints.scala @@ -4,15 +4,12 @@ */ package org.knora.webapi.slice.search.api -import sttp.model.MediaType import sttp.tapir.ztapir.* import zio.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse -import org.knora.webapi.slice.common.service.IriConverter -import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset final case class SearchServerEndpoints( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala index d657a380e1d..f0971f4abaa 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/shacl/api/ShaclEndpoints.scala @@ -15,7 +15,6 @@ import zio.stream.ZStream import java.io.File -import dsp.errors.RequestRejectedException import org.knora.webapi.slice.common.api.BaseEndpoints case class ValidationFormData( From 47b03043b1bd05ca4106c7c8cb59df89fb4a85bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 23 Sep 2025 22:49:43 +0200 Subject: [PATCH 31/99] start server in main --- .../main/scala/org/knora/webapi/Main.scala | 21 ++++++----- .../org/knora/webapi/core/DspApiServer.scala | 35 +++++++++++++++++++ .../org/knora/webapi/core/HttpServer.scala | 30 ---------------- .../org/knora/webapi/core/LayersLive.scala | 1 - 4 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index d4066248eea..9f620134494 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -6,11 +6,12 @@ package org.knora.webapi import zio.* - +import zio.http.Server import org.knora.webapi.config.AppConfig.AppConfigurations +import org.knora.webapi.config.KnoraApi import org.knora.webapi.core.* import org.knora.webapi.core.LayersLive.Environment -import org.knora.webapi.slice.infrastructure.MetricsServer +//import org.knora.webapi.slice.infrastructure.MetricsServer object Main extends ZIOApp { @@ -29,10 +30,14 @@ object Main extends ZIOApp { /** * Entrypoint of our Application */ - override def run: ZIO[Environment & ZIOAppArgs & Scope, Any, Any] = app - - /** - * The application logic. - */ - def app: ZIO[Environment, Throwable, Unit] = Db.init *> MetricsServer.make + override def run: ZIO[Environment & ZIOAppArgs & Scope, Any, Any] = + (Db.init *> + DspApiServer.startup() *> + ZIO.never) + .provideSomeAuto( + ZLayer + .service[KnoraApi] + .flatMap(c => Server.defaultWith(_.binding(c.get.internalHost, c.get.internalPort).enableRequestStreaming)), + ) +// MetricsServer.make } diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala new file mode 100644 index 00000000000..73d83cff95b --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.core + +import sttp.tapir.server.ziohttp.ZioHttpInterpreter +import sttp.tapir.server.ziohttp.ZioHttpServerOptions +import zio.* +import zio.http.* +import org.knora.webapi.config.KnoraApi +import org.knora.webapi.routing.Endpoints +import sttp.tapir.server.interceptor.cors.CORSConfig +import sttp.tapir.server.interceptor.cors.CORSInterceptor +import sttp.tapir.server.metrics.zio.ZioMetrics + +object DspApiServer { + + private def options: ZioHttpServerOptions[Any] = + ZioHttpServerOptions.customiseInterceptors + .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) + .corsInterceptor( + CORSInterceptor.customOrThrow(CORSConfig.default.allowAllMethods.allowAllOrigins.exposeAllHeaders), + ) + .options + + def startup(): ZIO[Endpoints & KnoraApi & Server, Throwable, Unit] = + for { + c <- ZIO.service[KnoraApi] + app <- ZIO.serviceWith[Endpoints](_.serverEndpoints).map(ZioHttpInterpreter(options).toHttp(_)) + _ <- ZIO.serviceWithZIO[Server](_.install(app)): @annotation.nowarn + _ <- ZIO.logInfo(s"http://localhost:${c.internalPort}/version ") + } yield () +} diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala deleted file mode 100644 index 744dfcf829d..00000000000 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServer.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.core - -import sttp.tapir.server.ziohttp.ZioHttpInterpreter -import sttp.tapir.server.ziohttp.ZioHttpServerOptions -import zio.* -import zio.http.* - -import org.knora.webapi.config.KnoraApi -import org.knora.webapi.routing.Endpoints - -object HttpServer { - - private def options: ZioHttpServerOptions[Any] = ZioHttpServerOptions.default - - val layer = ZLayer.scoped(apiServer).orDie - - private def apiServer: ZIO[Endpoints & KnoraApi, Throwable, Unit] = for { - apiConfig <- ZIO.service[KnoraApi] - endpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) - routes = ZioHttpInterpreter(options).toHttp(endpoints) - _ <- Server.install(routes).provide(Server.defaultWithPort(apiConfig.internalPort)): @annotation.nowarn - _ <- Console.printLine(s"Go to http://localhost:${apiConfig.externalPort}/docs to open SwaggerUI") - _ <- ZIO.never.unit - } yield () -} diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index f7427d9ea0a..7be86e21fa8 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -143,7 +143,6 @@ object LayersLive { self => ConstructResponseUtilV2.layer, DspIngestClientLive.layer, Endpoints.layer, - HttpServer.layer, IIIFRequestMessageHandlerLive.layer, InfrastructureModule.layer, IriService.layer, From e098a299e51105f1505fd9c0fb6325b38b35fe9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 09:25:18 +0200 Subject: [PATCH 32/99] start server in main --- .../main/scala/org/knora/webapi/Main.scala | 17 +++++------ .../org/knora/webapi/core/DspApiServer.scala | 29 +++++++++++++------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index 9f620134494..fc08ffd8c2d 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -6,12 +6,11 @@ package org.knora.webapi import zio.* -import zio.http.Server import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.config.KnoraApi import org.knora.webapi.core.* import org.knora.webapi.core.LayersLive.Environment -//import org.knora.webapi.slice.infrastructure.MetricsServer +import org.knora.webapi.slice.infrastructure.MetricsServer object Main extends ZIOApp { @@ -32,12 +31,10 @@ object Main extends ZIOApp { */ override def run: ZIO[Environment & ZIOAppArgs & Scope, Any, Any] = (Db.init *> - DspApiServer.startup() *> - ZIO.never) - .provideSomeAuto( - ZLayer - .service[KnoraApi] - .flatMap(c => Server.defaultWith(_.binding(c.get.internalHost, c.get.internalPort).enableRequestStreaming)), - ) -// MetricsServer.make + DspApiServer.startup *> + MetricsServer.make *> + logStarted *> + ZIO.never).provideSomeAuto(DspApiServer.layer) + + private def logStarted = ZIO.logInfo(s"${BuildInfo.name} ${BuildInfo.version} started.") } diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 73d83cff95b..f37d4acc2c8 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -15,9 +15,9 @@ import sttp.tapir.server.interceptor.cors.CORSConfig import sttp.tapir.server.interceptor.cors.CORSInterceptor import sttp.tapir.server.metrics.zio.ZioMetrics -object DspApiServer { +final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) { - private def options: ZioHttpServerOptions[Any] = + private val serverOptions: ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) .corsInterceptor( @@ -25,11 +25,22 @@ object DspApiServer { ) .options - def startup(): ZIO[Endpoints & KnoraApi & Server, Throwable, Unit] = - for { - c <- ZIO.service[KnoraApi] - app <- ZIO.serviceWith[Endpoints](_.serverEndpoints).map(ZioHttpInterpreter(options).toHttp(_)) - _ <- ZIO.serviceWithZIO[Server](_.install(app)): @annotation.nowarn - _ <- ZIO.logInfo(s"http://localhost:${c.internalPort}/version ") - } yield () + def startup(): UIO[Unit] = for { + _ <- ZIO.logInfo(s"Starting ${BuildInfo.name} (${BuildInfo.version}") + app = ZioHttpInterpreter(serverOptions).toHttp(endpoints.serverEndpoints) + actualPort <- Server.install(app).provide(ZLayer.succeed(server)): @annotation.nowarn + _ <- ZIO.logInfo(s"API available at http://${c.externalHost}:$actualPort/version") + } yield () +} + +object DspApiServer { + + def startup: RIO[DspApiServer, Unit] = ZIO.serviceWithZIO[DspApiServer](_.startup()) + + private val serverLayer = ZLayer + .service[KnoraApi] + .flatMap(cfg => Server.defaultWith(_.binding(cfg.get.internalHost, cfg.get.internalPort).enableRequestStreaming)) + .orDie + + val layer = serverLayer >>> ZLayer.derive[DspApiServer] } From 2506eb582a78bf22f0cb7bc70dfb79854133e0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 10:33:05 +0200 Subject: [PATCH 33/99] start server in main --- webapi/src/main/scala/org/knora/webapi/Main.scala | 7 +------ .../scala/org/knora/webapi/core/DspApiServer.scala | 2 +- .../webapi/slice/infrastructure/MetricsServer.scala | 11 +++++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index fc08ffd8c2d..530fc985b74 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -30,11 +30,6 @@ object Main extends ZIOApp { * Entrypoint of our Application */ override def run: ZIO[Environment & ZIOAppArgs & Scope, Any, Any] = - (Db.init *> - DspApiServer.startup *> - MetricsServer.make *> - logStarted *> - ZIO.never).provideSomeAuto(DspApiServer.layer) + (Db.init *> DspApiServer.startup *> MetricsServer.make).provideSomeAuto(DspApiServer.layer) - private def logStarted = ZIO.logInfo(s"${BuildInfo.name} ${BuildInfo.version} started.") } diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index f37d4acc2c8..298886094ac 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -26,7 +26,7 @@ final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) .options def startup(): UIO[Unit] = for { - _ <- ZIO.logInfo(s"Starting ${BuildInfo.name} (${BuildInfo.version}") + _ <- ZIO.logInfo("Starting DSP API server...") app = ZioHttpInterpreter(serverOptions).toHttp(endpoints.serverEndpoints) actualPort <- Server.install(app).provide(ZLayer.succeed(server)): @annotation.nowarn _ <- ZIO.logInfo(s"API available at http://${c.externalHost}:$actualPort/version") diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala index 56a9e2595e1..48a749388c6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala @@ -41,6 +41,7 @@ object MetricsServer { val make: ZIO[MetricsServerEnv, Throwable, Unit] = for { + _ <- ZIO.logInfo("Starting metrics and docs server...") knoraApiConfig <- ZIO.service[KnoraApi] apiV2Endpoints <- ZIO.service[ApiV2Endpoints] adminApiEndpoints <- ZIO.service[AdminApiEndpoints] @@ -49,10 +50,12 @@ object MetricsServer { port = config.port interval = config.interval metricsConfig = MetricsConfig(interval) - _ <- ZIO.logInfo( - s"Starting api on ${knoraApiConfig.externalKnoraApiBaseUrl}, " + - s"find docs on ${knoraApiConfig.externalProtocol}://${knoraApiConfig.externalHost}:$port/docs", - ) + _ <- + ZIO.logInfo( + s"Docs and metrics available at " + + s"${knoraApiConfig.externalProtocol}://${knoraApiConfig.externalHost}:$port/docs & " + + s"${knoraApiConfig.externalProtocol}://${knoraApiConfig.externalHost}:$port/metrics", + ) _ <- metricsServer.provide( ZLayer.succeed(knoraApiConfig), ZLayer.succeed(adminApiEndpoints), From 61fceff08686acef6e88c04c84d55c350f3ae323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 11:02:47 +0200 Subject: [PATCH 34/99] start server in main --- .../main/scala/org/knora/webapi/core/DspApiServer.scala | 7 +++++-- .../slice/ontology/api/OntologiesServerEndpoints.scala | 1 + .../slice/ontology/api/service/OntologiesRestService.scala | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 298886094ac..3af6824d2b2 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -14,15 +14,18 @@ import org.knora.webapi.routing.Endpoints import sttp.tapir.server.interceptor.cors.CORSConfig import sttp.tapir.server.interceptor.cors.CORSInterceptor import sttp.tapir.server.metrics.zio.ZioMetrics +import sttp.model.Method.* final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) { private val serverOptions: ZioHttpServerOptions[Any] = ZioHttpServerOptions.customiseInterceptors - .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) .corsInterceptor( - CORSInterceptor.customOrThrow(CORSConfig.default.allowAllMethods.allowAllOrigins.exposeAllHeaders), + CORSInterceptor.customOrThrow( + CORSConfig.default.allowAllMethods.allowAllOrigins.exposeAllHeaders, + ), ) + .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) .options def startup(): UIO[Unit] = for { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 9d1c65b38a4..117d546ea9b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -18,6 +18,7 @@ final class OntologiesServerEndpoints( val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), + endpoints.getOntologiesMetadataProjects.zServerLogic(restService.getOntologyMetadataByProjects), endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), endpoints.getOntologiesAllentities.serverLogic(restService.getOntologyEntities), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala index a743606af5b..ae218f917a3 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala @@ -91,10 +91,11 @@ final case class OntologiesRestService( getOntologyMetadataBy(projectIri.toSet, formatOptions) def getOntologyMetadataByProjects( - projectIris: List[ProjectIri], + projectIris: List[String], formatOptions: FormatOptions, - ): Task[(RenderedResponse, MediaType)] = - getOntologyMetadataBy(projectIris.toSet, formatOptions) + ): Task[(RenderedResponse, MediaType)] = ZIO + .foreach(projectIris.toSet)(iri => ZIO.fromEither(ProjectIri.from(iri)).mapError(BadRequestException.apply)) + .flatMap(projectIris => getOntologyMetadataBy(projectIris, formatOptions)) private def getOntologyMetadataBy(projectIris: Set[ProjectIri], formatOptions: FormatOptions) = for { result <- ontologyResponder.getOntologyMetadataForProjects(projectIris) From 206d66ab37947db6df546738dee4aef0698a027f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 11:50:20 +0200 Subject: [PATCH 35/99] cors --- .../src/main/scala/org/knora/webapi/core/DspApiServer.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 3af6824d2b2..51e5a983a6f 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -22,7 +22,10 @@ final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) ZioHttpServerOptions.customiseInterceptors .corsInterceptor( CORSInterceptor.customOrThrow( - CORSConfig.default.allowAllMethods.allowAllOrigins.exposeAllHeaders, + CORSConfig.default.allowCredentials + .allowMethods(GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH) + .allowMatchingOrigins(_ => true) + .exposeAllHeaders, ), ) .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) From 47951a766ac09f8575844428b1ec6be78e2cdb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 11:58:50 +0200 Subject: [PATCH 36/99] rm rootUser from E2EZSpec --- .../src/test/scala/org/knora/webapi/ProjectEraseIT.scala | 1 + .../webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala | 1 + .../scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala | 2 +- .../scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala | 2 +- .../security/api/AuthenticationEndpointsV2E2ESpec.scala | 2 +- .../src/main/scala/{ => org/knora/webapi}/E2EZSpec.scala | 6 +----- webapi/src/main/scala/org/knora/webapi/Main.scala | 1 + .../main/scala/org/knora/webapi/core/DspApiServer.scala | 9 +++++---- 8 files changed, 12 insertions(+), 12 deletions(-) rename modules/testkit/src/main/scala/{ => org/knora/webapi}/E2EZSpec.scala (86%) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/ProjectEraseIT.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/ProjectEraseIT.scala index 3247af6a757..6a7199985ef 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/ProjectEraseIT.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/ProjectEraseIT.scala @@ -20,6 +20,7 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.messages.v2.responder.ontologymessages.CreateOntologyRequestV2 import org.knora.webapi.responders.v2.OntologyResponderV2 +import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.slice.admin.api.GroupsRequests.GroupCreateRequest import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala index 11d3fc599a5..fe736eb10f2 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/CardinalitiesV2E2ESpec.scala @@ -16,6 +16,7 @@ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.messages.util.rdf.JsonLDKeywords import org.knora.webapi.messages.util.rdf.JsonLDUtil +import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequestsAndResponses.ProjectCreateRequest import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.admin.domain.service.KnoraProjectService diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala index c3957c409da..236caa624e4 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala @@ -14,10 +14,10 @@ import zio.test.* import org.knora.webapi.E2EZSpec import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient - object SearchE2EZSpec extends E2EZSpec { override def rdfDataObjects: List[RdfDataObject] = List( diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala index b3e9972e58d..6c630153465 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala @@ -16,10 +16,10 @@ import org.knora.webapi.e2ez.KnoraBaseJsonModels.ResourceResponses.AudioSegmentR import org.knora.webapi.e2ez.KnoraBaseJsonModels.ResourceResponses.ResourcePreviewResponse import org.knora.webapi.e2ez.KnoraBaseJsonModels.ResourceResponses.VideoSegmentResourceResponse import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient import org.knora.webapi.testservices.TestOntologyApiClient - object SegmentE2EZSpec extends E2EZSpec { override def rdfDataObjects: List[RdfDataObject] = List( diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala index 58744e9bd9a..f295b73a40d 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala @@ -12,6 +12,7 @@ import zio.json.JsonDecoder import zio.test.* import org.knora.webapi.E2EZSpec +import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.slice.security.Authenticator import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.CheckResponse @@ -20,7 +21,6 @@ import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutRespo import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient - object AuthenticationEndpointsV2E2ESpec extends E2EZSpec { private val validPassword = "test" diff --git a/modules/testkit/src/main/scala/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala similarity index 86% rename from modules/testkit/src/main/scala/E2EZSpec.scala rename to modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 7ff7aaf2716..f8111cee3d2 100644 --- a/modules/testkit/src/main/scala/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -17,18 +17,14 @@ import org.knora.webapi.core.LayersTest import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject -import org.knora.webapi.sharedtestdata.SharedTestDataADM -import org.knora.webapi.slice.admin.domain.model.User abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { implicit val sf: StringFormatter = StringFormatter.getInitializedTestInstance - // test data - val rootUser: User = SharedTestDataADM.rootUser private val testLayers = org.knora.webapi.util.Logger.testSafe() >>> LayersTest.layer - def rdfDataObjects: List[RdfDataObject] = List.empty[RdfDataObject] + def rdfDataObjects: List[RdfDataObject] = List.empty type env = LayersTest.Environment with Client with Scope diff --git a/webapi/src/main/scala/org/knora/webapi/Main.scala b/webapi/src/main/scala/org/knora/webapi/Main.scala index 530fc985b74..4d64c12b85d 100644 --- a/webapi/src/main/scala/org/knora/webapi/Main.scala +++ b/webapi/src/main/scala/org/knora/webapi/Main.scala @@ -6,6 +6,7 @@ package org.knora.webapi import zio.* + import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.config.KnoraApi import org.knora.webapi.core.* diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 51e5a983a6f..641fa6e6ca8 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -5,16 +5,17 @@ package org.knora.webapi.core +import sttp.model.Method.* +import sttp.tapir.server.interceptor.cors.CORSConfig +import sttp.tapir.server.interceptor.cors.CORSInterceptor +import sttp.tapir.server.metrics.zio.ZioMetrics import sttp.tapir.server.ziohttp.ZioHttpInterpreter import sttp.tapir.server.ziohttp.ZioHttpServerOptions import zio.* import zio.http.* + import org.knora.webapi.config.KnoraApi import org.knora.webapi.routing.Endpoints -import sttp.tapir.server.interceptor.cors.CORSConfig -import sttp.tapir.server.interceptor.cors.CORSInterceptor -import sttp.tapir.server.metrics.zio.ZioMetrics -import sttp.model.Method.* final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) { From 54fb7e795f6d6cf0c7ecec870d350d7f71b2d99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 13:29:04 +0200 Subject: [PATCH 37/99] cors spec --- .../knora/webapi/e2e/CORSSupportE2ESpec.scala | 5 ++--- .../main/scala/org/knora/webapi/E2EZSpec.scala | 18 ++++++++++++++++-- .../org/knora/webapi/core/DspApiServer.scala | 3 ++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala index c63164cf9c6..f149daa59ea 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/CORSSupportE2ESpec.scala @@ -50,7 +50,7 @@ object CORSSupportE2ESpec extends E2EZSpec { corsClient(_.options(uri"/admin/projects", Header(AccessControlRequestMethod, "GET"))) .map(response => assertTrue( - response.code == StatusCode.Ok, + response.code.isSuccess, response.header(AccessControlAllowOrigin).contains("http://example.com"), response.header(AccessControlAllowMethods).exists(_.contains("GET")), response.header(AccessControlAllowMethods).exists(_.contains("PUT")), @@ -63,14 +63,13 @@ object CORSSupportE2ESpec extends E2EZSpec { response.header(AccessControlAllowCredentials).contains("true"), ), ) - }, test("send `Access-Control-Allow-Origin` header when the Knora resource is found ") { val resourceIri = "http://rdfh.ch/0001/55UrkgTKR2SEQgnsLWI9mg" corsClient(_.get(uri"/v2/resources/$resourceIri")) .map(response => assertTrue( - response.code == StatusCode.Ok, + response.code.isSuccess, response.header(AccessControlAllowOrigin).contains("http://example.com"), ), ) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index f8111cee3d2..c741a3e109a 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -9,14 +9,19 @@ import zio.* import zio.http.* import zio.test.* import zio.test.Assertion.* +import zio.json.ast.Json import scala.reflect.ClassTag - import org.knora.webapi.core.Db +import org.knora.webapi.core.DspApiServer import org.knora.webapi.core.LayersTest import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.testservices.TestApiClient +import sttp.client4.Response +import sttp.client4.UriContext +import sttp.model.StatusCode abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { @@ -28,7 +33,16 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { type env = LayersTest.Environment with Client with Scope - private def prepare = Db.initWithTestData(rdfDataObjects) + private def prepare = for { + _ <- Db.initWithTestData(rdfDataObjects) + _ <- (DspApiServer.startup *> ZIO.never).provideSomeAuto(DspApiServer.layer).fork + // wait max 5 seconds until api is ready + _ <- TestApiClient + .getJson[Json](uri"/version") + .repeatWhile(_.code != StatusCode.Ok) + .retry(Schedule.duration(5.seconds)) + _ <- ZIO.logInfo("API is ready") + } yield () def e2eSpec: Spec[env, Any] diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 641fa6e6ca8..5eaea26d240 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -26,7 +26,8 @@ final case class DspApiServer(server: Server, endpoints: Endpoints, c: KnoraApi) CORSConfig.default.allowCredentials .allowMethods(GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH) .allowMatchingOrigins(_ => true) - .exposeAllHeaders, + .exposeAllHeaders + .maxAge(30.minutes.asScala), ), ) .metricsInterceptor(ZioMetrics.default[Task]().metricsInterceptor()) From 67920b8b0d39cee702d76e640030416fd3c50f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 13:31:43 +0200 Subject: [PATCH 38/99] fmt --- .../scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala | 1 + .../src/main/scala/org/knora/webapi/E2EZSpec.scala | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala index 236caa624e4..9e7f898e2cb 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SearchE2EZSpec.scala @@ -18,6 +18,7 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient + object SearchE2EZSpec extends E2EZSpec { override def rdfDataObjects: List[RdfDataObject] = List( diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index c741a3e109a..052cb405dd0 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -5,13 +5,17 @@ package org.knora.webapi +import sttp.client4.Response +import sttp.client4.UriContext +import sttp.model.StatusCode import zio.* import zio.http.* +import zio.json.ast.Json import zio.test.* import zio.test.Assertion.* -import zio.json.ast.Json import scala.reflect.ClassTag + import org.knora.webapi.core.Db import org.knora.webapi.core.DspApiServer import org.knora.webapi.core.LayersTest @@ -19,9 +23,6 @@ import org.knora.webapi.core.TestStartupUtils import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.testservices.TestApiClient -import sttp.client4.Response -import sttp.client4.UriContext -import sttp.model.StatusCode abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { From 9f23daa082e9234eba424e836b7eb73f6be2dac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 14:08:32 +0200 Subject: [PATCH 39/99] cleanup --- .../responders/v2/ontology/AddCardinalitiesToClassSpec.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala index e3b2c45c8f7..82735f26f2b 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala @@ -15,7 +15,6 @@ import org.knora.webapi.E2EZSpec import org.knora.webapi.messages.IriConversions.* import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.store.triplestoremessages.SmartIriLiteralV2 import org.knora.webapi.messages.v2.responder.ontologymessages.* @@ -37,8 +36,7 @@ import org.knora.webapi.testservices.TestOntologyApiClient */ object AddCardinalitiesToClassSpec extends E2EZSpec { - private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val ontologyResponder = ZIO.serviceWithZIO[OntologyResponderV2] + private val ontologyResponder = ZIO.serviceWithZIO[OntologyResponderV2] override val rdfDataObjects: List[RdfDataObject] = List(freetestRdfOntology) From f848cde771ff0c0b3594019d35829a0afb95bc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 14:53:46 +0200 Subject: [PATCH 40/99] fix SipiIT to be able to run while api server has not shutdown yet --- .../src/test/scala/org/knora/sipi/SipiIT.scala | 17 ++++++++++------- .../testcontainers/SipiTestContainer.scala | 9 ++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala b/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala index 4e70857a4bc..4343ea93b6f 100644 --- a/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala +++ b/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala @@ -23,8 +23,8 @@ import zio.test.* import scala.util.Failure import scala.util.Success import scala.util.Try - import dsp.valueobjects.UuidUtil +import org.apache.commons.codec.binary.Base32 import org.knora.sipi.MockDspApiServer.verify.* import org.knora.webapi.slice.admin.api.model.PermissionCodeAndProjectRestrictedViewSettings import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode @@ -48,7 +48,7 @@ object SipiIT extends ZIOSpecDefault { uuid <- Random.nextUUID exp = now.plusSeconds(3600) claim = JwtClaim( - issuer = Some("0.0.0.0:3333"), + issuer = Some(s"0.0.0.0:9999"), subject = Some("someUser"), audience = Some(Set("Knora", "Sipi")), issuedAt = Some(now.getEpochSecond), @@ -62,6 +62,9 @@ object SipiIT extends ZIOSpecDefault { JwtAlgorithm.HS256, ) + private val cookieName: String = + "KnoraAuthentication" + new Base32('9'.toByte).encodeAsString("0.0.0.0:9999".getBytes) + private val cookiesSuite = suite("Given a request is authorized using cookies")( test( @@ -81,7 +84,7 @@ object SipiIT extends ZIOSpecDefault { s"KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999aSecondCookie", "anotherValueShouldBeIgnored", ), - Cookie.Request("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999", jwt), + Cookie.Request(cookieName, jwt), ), ), ) @@ -99,7 +102,7 @@ object SipiIT extends ZIOSpecDefault { jwt <- createJwt(AuthScope.admin) response <- requestGet( Path.root / prefix / imageTestfile / "file", - Header.Cookie(NonEmptyChunk(Cookie.Request("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999", jwt))), + Header.Cookie(NonEmptyChunk(Cookie.Request(cookieName, jwt))), ) requestToDspApiContainsJwt <- MockDspApiServer.verifyAuthBearerTokenReceived(jwt) } yield assertTrue(response.status == Status.Ok, requestToDspApiContainsJwt) @@ -114,7 +117,7 @@ object SipiIT extends ZIOSpecDefault { jwt <- createJwt(AuthScope.write(Shortcode.unsafeFrom(prefix))) response <- requestGet( Path.root / prefix / imageTestfile / "full" / "max" / "0" / "default.jpg", - Header.Cookie(NonEmptyChunk(Cookie.Request("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999", jwt))), + Header.Cookie(NonEmptyChunk(Cookie.Request(cookieName, jwt))), ) noInteraction <- MockDspApiServer.verifyNoInteraction } yield assertTrue(response.status == Status.Ok, noInteraction) @@ -296,7 +299,7 @@ object SipiIT extends ZIOSpecDefault { }, ) .provideSomeLayerShared[Scope & Client & WireMockServer]( - SharedVolumes.Images.layer >+> SipiTestContainer.layer, + SharedVolumes.Images.layer >+> SipiTestContainer.layer(9999), ) .provideSomeLayerShared[Scope & Client](MockDspApiServer.layer) .provideSomeLayer[Scope](Client.default) @@ TestAspect.sequential @@ TestAspect.withLiveClock @@ -379,7 +382,7 @@ object MockDspApiServer { } private def acquireWireMockServer: Task[WireMockServer] = ZIO.attempt { - val server = new WireMockServer(options().port(3333)); // No-args constructor will start on port 8080, no HTTPS + val server = new WireMockServer(options().port(9999)); // No-args constructor will start on port 8080, no HTTPS server.start() server } diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala index a60e23cdc4f..0ff1cdb5835 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala @@ -59,16 +59,16 @@ object SipiTestContainer { def resolveUrl(path: http.Path): URIO[SipiTestContainer, URL] = ZIO.serviceWith[SipiTestContainer](_.sipiBaseUrl.path(path)) - def make(imagesVolume: SharedVolumes.Images): SipiTestContainer = + def make(imagesVolume: SharedVolumes.Images, apiPort: Int = 3333): SipiTestContainer = new SipiTestContainer() .withExposedPorts(1024) .withEnv("KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST", "0.0.0.0") - .withEnv("KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT", "3333") + .withEnv("KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT", s"$apiPort") .withEnv("SIPI_EXTERNAL_PROTOCOL", "http") .withEnv("SIPI_EXTERNAL_HOSTNAME", "0.0.0.0") .withEnv("SIPI_EXTERNAL_PORT", "1024") .withEnv("SIPI_WEBAPI_HOSTNAME", SipiTestContainer.localHostAddress) - .withEnv("SIPI_WEBAPI_PORT", "3333") + .withEnv("SIPI_WEBAPI_PORT", s"$apiPort") .withEnv("CLEAN_TMP_DIR_USER", "clean_tmp_dir_user") .withEnv("CLEAN_TMP_DIR_PW", "clean_tmp_dir_pw") .withCommand("--config=/sipi/config/sipi.docker-config.lua") @@ -91,6 +91,9 @@ object SipiTestContainer { } yield container, ) + def layer(apiPort: Int = 3333): URLayer[SharedVolumes.Images, SipiTestContainer] = + (ZLayer.scoped(ZIO.serviceWithZIO[SharedVolumes.Images](make(_, apiPort).toZio)) >>> initSipi).orDie + val layer: URLayer[SharedVolumes.Images, SipiTestContainer] = { val container = ZLayer.scoped(ZIO.serviceWithZIO[SharedVolumes.Images](make(_).toZio)) (container >>> initSipi).orDie From fca003adfb51dc3dea0d566ba2696e05d1980fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 14:57:05 +0200 Subject: [PATCH 41/99] finetuning --- .../testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 052cb405dd0..e35e1049500 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -41,7 +41,9 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { _ <- TestApiClient .getJson[Json](uri"/version") .repeatWhile(_.code != StatusCode.Ok) - .retry(Schedule.duration(5.seconds)) + .retry(Schedule.exponential(10.milli)) + .timeout(5.seconds) + .orDie _ <- ZIO.logInfo("API is ready") } yield () From 746297fdadc1b98e5c836aecd51d83f602590015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 15:27:11 +0200 Subject: [PATCH 42/99] fixup --- .../src/test/scala/org/knora/sipi/SipiIT.scala | 3 ++- .../ontology/AddCardinalitiesToClassSpec.scala | 6 +++--- .../knora/webapi/util/OntologyTestHelper.scala | 16 ++++++++++++++++ .../main/scala/org/knora/webapi/E2EZSpec.scala | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala diff --git a/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala b/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala index 4343ea93b6f..c94adf359c4 100644 --- a/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala +++ b/modules/test-it/src/test/scala/org/knora/sipi/SipiIT.scala @@ -11,6 +11,7 @@ import com.github.tomakehurst.wiremock.client.WireMock.* import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.newRequestPattern +import org.apache.commons.codec.binary.Base32 import pdi.jwt.JwtAlgorithm import pdi.jwt.JwtClaim import pdi.jwt.JwtZIOJson @@ -23,8 +24,8 @@ import zio.test.* import scala.util.Failure import scala.util.Success import scala.util.Try + import dsp.valueobjects.UuidUtil -import org.apache.commons.codec.binary.Base32 import org.knora.sipi.MockDspApiServer.verify.* import org.knora.webapi.slice.admin.api.model.PermissionCodeAndProjectRestrictedViewSettings import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode diff --git a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala index 82735f26f2b..5f6f1938194 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/ontology/AddCardinalitiesToClassSpec.scala @@ -27,9 +27,10 @@ import org.knora.webapi.slice.common.KnoraIris.PropertyIri import org.knora.webapi.slice.common.KnoraIris.ResourceClassIri import org.knora.webapi.slice.ontology.api.AddCardinalitiesToClassRequestV2 import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne +import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select -import org.knora.webapi.testservices.TestOntologyApiClient +import org.knora.webapi.util.OntologyTestHelper /** * This spec is used to test [[org.knora.webapi.responders.v2.ontology.Cardinalities]]. @@ -67,12 +68,11 @@ object AddCardinalitiesToClassSpec extends E2EZSpec { val newPropertyIri = freetestOntologyIri.makeProperty("hasName") for { - ontologyLastModificationDate <- TestOntologyApiClient.getLastModificationDate(freetestOntologyIri) - // assert that the cardinality for `:hasAuthor` is only once in the triplestore countInitial <- getCardinalityCountFromTriplestore(classIri, propertyIri) // add additional cardinality to the class + ontologyLastModificationDate <- OntologyTestHelper.lastModificationDate(freetestOntologyIri) _ <- ontologyResponder( _.addCardinalitiesToClass( AddCardinalitiesToClassRequestV2( diff --git a/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala new file mode 100644 index 00000000000..7c6b3e0b28b --- /dev/null +++ b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala @@ -0,0 +1,16 @@ +package org.knora.webapi.util +import zio.* + +import java.time.Instant + +import org.knora.webapi.slice.common.KnoraIris.OntologyIri +import org.knora.webapi.slice.ontology.domain.service.OntologyRepo + +object OntologyTestHelper { + def lastModificationDate(ontologyIri: OntologyIri): RIO[OntologyRepo, Instant] = ZIO + .serviceWithZIO[OntologyRepo](_.findById(ontologyIri).orDie) + .map(_.flatMap(_.ontologyMetadata.lastModificationDate)) + .someOrElseZIO( + ZIO.die(IllegalStateException(s"Could not find the last modification date of the ontology $ontologyIri.")), + ) +} diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index e35e1049500..923d6adcab2 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -44,7 +44,7 @@ abstract class E2EZSpec extends ZIOSpecDefault with TestStartupUtils { .retry(Schedule.exponential(10.milli)) .timeout(5.seconds) .orDie - _ <- ZIO.logInfo("API is ready") + _ <- ZIO.logInfo("API is ready, start running tests...") } yield () def e2eSpec: Spec[env, Any] From 734284079778b778ecaabcf00660974118661a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 15:37:56 +0200 Subject: [PATCH 43/99] use OntologyTestHelper --- .../knora/webapi/e2ez/SegmentE2EZSpec.scala | 1 + .../v2/OntologyResponderV2Spec.scala | 14 +++------- .../webapi/util/OntologyTestHelper.scala | 28 +++++++++++++++---- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala index 6c630153465..fb252473432 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2ez/SegmentE2EZSpec.scala @@ -20,6 +20,7 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM.rootUser import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient import org.knora.webapi.testservices.TestOntologyApiClient + object SegmentE2EZSpec extends E2EZSpec { override def rdfDataObjects: List[RdfDataObject] = List( diff --git a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala index 8a77b3abdf5..60be2755346 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala @@ -51,6 +51,7 @@ import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select import org.knora.webapi.util.MutableTestIri +import org.knora.webapi.util.OntologyTestHelper object OntologyResponderV2Spec extends E2EZSpec { self => @@ -182,13 +183,6 @@ object OntologyResponderV2Spec extends E2EZSpec { self => ontologySchema = ApiV2Complex, ) - private def getLastModificationDate(r: ReadOntologyMetadataV2, ontologyIri: OntologyIri): Instant = - r.toOntologySchema(ApiV2Complex) - .ontologies - .find(_.ontologyIri == ontologyIri.toComplexSchema) - .flatMap(_.lastModificationDate) - .getOrElse(throw AssertionException(s"$ontologyIri has no last modification date")) - override val e2eSpec = suite("The ontology responder v2")( test("create an empty ontology called 'foo' with a project code") { val createReq = @@ -403,7 +397,7 @@ object OntologyResponderV2Spec extends E2EZSpec { self => test("not delete the 'anything' ontology, because it is used in data and in the 'something' ontology") { for { metadataResponse <- ontologyResponder(_.getOntologyMetadataForProject(anythingProjectIri)) - anythingLastModDate = getLastModificationDate(metadataResponse, anythingOntologyIri) + anythingLastModDate = OntologyTestHelper.lastModificationDate(metadataResponse, anythingOntologyIri) exit <- ontologyResponder(_.deleteOntology(anythingOntologyIri, anythingLastModDate, randomUUID)).exit } yield assertTrue(metadataResponse.ontologies.size == 3) && assert(exit)( @@ -608,7 +602,7 @@ object OntologyResponderV2Spec extends E2EZSpec { self => ) { for { metadataResponse <- ontologyResponder(_.getOntologyMetadataForProject(anythingProjectIri)) - anythingLastModDate = getLastModificationDate(metadataResponse, anythingOntologyIri) + anythingLastModDate = OntologyTestHelper.lastModificationDate(metadataResponse, anythingOntologyIri) propertyIri = anythingOntologyIri.makeEntityIri("hasInterestingThing") propertyInfoContent = PropertyInfoContentV2( propertyIri = propertyIri, @@ -4860,7 +4854,7 @@ object OntologyResponderV2Spec extends E2EZSpec { self => for { metadataResponse <- ontologyResponder(_.getOntologyMetadataForProject(anythingProjectIri)) - anythingLastModDate = getLastModificationDate(metadataResponse, anythingOntologyIri) + anythingLastModDate = OntologyTestHelper.lastModificationDate(metadataResponse, anythingOntologyIri) // create the property anything:hasFoafName propertyIri: SmartIri = anythingOntologyIri.makeEntityIri("hasFoafName") diff --git a/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala index 7c6b3e0b28b..c17dc422306 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala @@ -3,14 +3,32 @@ import zio.* import java.time.Instant +import org.knora.webapi.ApiV2Complex +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyMetadataV2 +import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 import org.knora.webapi.slice.common.KnoraIris.OntologyIri import org.knora.webapi.slice.ontology.domain.service.OntologyRepo object OntologyTestHelper { - def lastModificationDate(ontologyIri: OntologyIri): RIO[OntologyRepo, Instant] = ZIO - .serviceWithZIO[OntologyRepo](_.findById(ontologyIri).orDie) - .map(_.flatMap(_.ontologyMetadata.lastModificationDate)) - .someOrElseZIO( - ZIO.die(IllegalStateException(s"Could not find the last modification date of the ontology $ontologyIri.")), + private val ontologyRepo = ZIO.serviceWithZIO[OntologyRepo] + def lastModificationDate(ontologyIri: OntologyIri): RIO[OntologyRepo, Instant] = + ontologyRepo( + _.findById(ontologyIri) + .someOrFail(IllegalStateException(s"Ontology $ontologyIri not found")) + .map(lastModificationDate), + ).orDie + + @throws[IllegalStateException] + def lastModificationDate(r: ReadOntologyV2): Instant = + r.ontologyMetadata.lastModificationDate.getOrElse( + throw IllegalStateException(s"${r.ontologyIri} has no last modification date"), ) + + @throws[IllegalStateException] + def lastModificationDate(r: ReadOntologyMetadataV2, ontologyIri: OntologyIri): Instant = + r.toOntologySchema(ApiV2Complex) + .ontologies + .find(_.ontologyIri == ontologyIri.toComplexSchema) + .flatMap(_.lastModificationDate) + .getOrElse(throw IllegalStateException(s"$ontologyIri has no last modification date")) } From 09feeb366de1c56bc59ce8b628240435da61db3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 16:46:38 +0200 Subject: [PATCH 44/99] introduce api.v2 slice --- .../webapi/util/OntologyTestHelper.scala | 5 +++ .../org/knora/webapi/core/LayersLive.scala | 2 + .../org/knora/webapi/routing/Endpoints.scala | 23 ++---------- .../webapi/slice/api/v2/ApiV2Module.scala | 11 ++++++ .../slice/api/v2/ApiV2ServerEndpoints.scala | 37 +++++++++++++++++++ .../api/ResourcesApiServerEndpoints.scala | 9 +++-- 6 files changed, 64 insertions(+), 23 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2Module.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2ServerEndpoints.scala diff --git a/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala index c17dc422306..d9527666ed9 100644 --- a/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala +++ b/modules/test-it/src/test/scala/org/knora/webapi/util/OntologyTestHelper.scala @@ -1,3 +1,8 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + package org.knora.webapi.util import zio.* diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 7be86e21fa8..56b88be563f 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -31,6 +31,7 @@ import org.knora.webapi.routing.* import org.knora.webapi.slice.admin.AdminModule import org.knora.webapi.slice.admin.api.* import org.knora.webapi.slice.admin.domain.service.* +import org.knora.webapi.slice.api.v2.ApiV2ServerEndpoints import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.CommonModule import org.knora.webapi.slice.common.CommonModule.Provided @@ -134,6 +135,7 @@ object LayersLive { self => AdminModule.layer, ApiComplexV2JsonLdRequestParser.layer, ApiV2Endpoints.layer, + ApiV2ServerEndpoints.layer, AssetPermissionsResponder.layer, AuthenticationApiModule.layer, AuthorizationRestService.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala index 0d9a255047e..ed4a9f00ddd 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala @@ -11,35 +11,20 @@ import zio.* import org.knora.webapi.routing import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints +import org.knora.webapi.slice.api.v2.ApiV2ServerEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementServerEndpoints -import org.knora.webapi.slice.lists.api.ListsV2ServerEndpoints -import org.knora.webapi.slice.ontology.api.OntologiesServerEndpoints -import org.knora.webapi.slice.resources.api.ResourceInfoServerEndpoints -import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints -import org.knora.webapi.slice.search.api.SearchServerEndpoints -import org.knora.webapi.slice.security.api.AuthenticationServerEndpoints import org.knora.webapi.slice.shacl.api.ShaclServerEndpoints final case class Endpoints( - adminApiServerEndpoints: AdminApiServerEndpoints, - authenticationServerEndpoints: AuthenticationServerEndpoints, - listsV2ServerEndpoints: ListsV2ServerEndpoints, - resourceInfoServerEndpoints: ResourceInfoServerEndpoints, - resourcesApiServerEndpoints: ResourcesApiServerEndpoints, - searchServerEndpoints: SearchServerEndpoints, + adminApiServerEndpoints: AdminApiServerEndpoints, // admin api + apiV2ServerEndpoints: ApiV2ServerEndpoints, shaclServerEndpoints: ShaclServerEndpoints, managementServerEndpoints: ManagementServerEndpoints, - ontologiesServerEndpoints: OntologiesServerEndpoints, ) { val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = adminApiServerEndpoints.serverEndpoints ++ - authenticationServerEndpoints.serverEndpoints ++ - listsV2ServerEndpoints.serverEndpoints ++ + apiV2ServerEndpoints.serverEndpoints ++ managementServerEndpoints.serverEndpoints ++ - ontologiesServerEndpoints.serverEndpoints ++ - resourceInfoServerEndpoints.serverEndpoints ++ - resourcesApiServerEndpoints.serverEndpoints ++ - searchServerEndpoints.serverEndpoints ++ shaclServerEndpoints.serverEndpoints } object Endpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2Module.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2Module.scala new file mode 100644 index 00000000000..13a255b161f --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2Module.scala @@ -0,0 +1,11 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v2 + +object ApiV2Module { + + val layer = ApiV2ServerEndpoints.layer +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2ServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2ServerEndpoints.scala new file mode 100644 index 00000000000..89f84be1837 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v2/ApiV2ServerEndpoints.scala @@ -0,0 +1,37 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v2 + +import sttp.tapir.ztapir.ZServerEndpoint +import zio.ZLayer + +import org.knora.webapi.slice.lists.api.ListsV2ServerEndpoints +import org.knora.webapi.slice.ontology.api.OntologiesServerEndpoints +import org.knora.webapi.slice.resources.api.ResourceInfoServerEndpoints +import org.knora.webapi.slice.resources.api.ResourcesApiServerEndpoints +import org.knora.webapi.slice.search.api.SearchServerEndpoints +import org.knora.webapi.slice.security.api.AuthenticationServerEndpoints + +class ApiV2ServerEndpoints( + private val authenticationServerEndpoints: AuthenticationServerEndpoints, + private val listsV2ServerEndpoints: ListsV2ServerEndpoints, + private val ontologiesServerEndpoints: OntologiesServerEndpoints, + private val resourceInfoServerEndpoints: ResourceInfoServerEndpoints, + private val resourcesApiServerEndpoints: ResourcesApiServerEndpoints, + private val searchServerEndpoints: SearchServerEndpoints, +) { + + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = + authenticationServerEndpoints.serverEndpoints ++ + listsV2ServerEndpoints.serverEndpoints ++ + resourceInfoServerEndpoints.serverEndpoints ++ + resourcesApiServerEndpoints.serverEndpoints ++ + searchServerEndpoints.serverEndpoints ++ + ontologiesServerEndpoints.serverEndpoints +} +object ApiV2ServerEndpoints { + val layer = ZLayer.derive[ApiV2ServerEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala index afdaabbe2a4..717ae7a1e73 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesApiServerEndpoints.scala @@ -13,10 +13,11 @@ final case class ResourcesApiServerEndpoints( private val standoffServerEndpoints: StandoffServerEndpoints, private val valuesServerEndpoints: ValuesServerEndpoints, ) { - val serverEndpoints: List[ZServerEndpoint[Any, Any]] = valuesServerEndpoints.serverEndpoints ++ - resourcesServerEndpoints.serverEndpoints ++ - standoffServerEndpoints.serverEndpoints ++ - metadataServerEndpoints.serverEndpoints + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = + valuesServerEndpoints.serverEndpoints ++ + resourcesServerEndpoints.serverEndpoints ++ + standoffServerEndpoints.serverEndpoints ++ + metadataServerEndpoints.serverEndpoints } object ResourcesApiServerEndpoints { val layer = ZLayer.derive[ResourcesApiServerEndpoints] From 9d38cbb26c767bc45d89fb191362a79fbd529082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 24 Sep 2025 16:48:09 +0200 Subject: [PATCH 45/99] git ignore claude flow stuff --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index eb7f0e2ecf6..afaf738dc70 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,8 @@ ingest/docs/openapi/*.yml # Claude Code .claude/settings.local.json /.claude/tmp +.claude-flow/* +.swarm/* # ingest localdev/storage/ From ca03b3d1340a6f3b0320d7973bc809707138f031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 25 Sep 2025 14:39:16 +0200 Subject: [PATCH 46/99] fix content negotiation --- .../messages/util/rdf/RdfFormatUtil.scala | 5 +- .../knora/webapi/slice/common/api/ApiV2.scala | 27 +++-- .../slice/lists/api/ListsEndpointsV2.scala | 5 +- .../ontology/api/OntologiesEndpoints.scala | 109 +++++++++--------- .../api/OntologiesServerEndpoints.scala | 37 +++--- .../resources/api/ResourcesEndpoints.scala | 57 +++++---- .../resources/api/StandoffEndpoints.scala | 3 +- .../slice/resources/api/ValuesEndpoints.scala | 13 +-- .../slice/search/api/SearchEndpoints.scala | 49 ++++---- 9 files changed, 155 insertions(+), 150 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala index a5aebce692b..b073dd736b7 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala @@ -7,6 +7,7 @@ package org.knora.webapi.messages.util.rdf import org.apache.jena import sttp.model.MediaType +import sttp.tapir.CodecFormat import java.io.BufferedInputStream import java.io.BufferedOutputStream @@ -28,9 +29,7 @@ import org.knora.webapi.SchemaOptions /** * A trait for supported RDF formats. */ -sealed trait RdfFormat { - def mediaType: MediaType -} +sealed trait RdfFormat extends CodecFormat /** * A trait for formats other than JSON-LD. diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala index 146acd5f3eb..3fc60232e91 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala @@ -7,20 +7,15 @@ package org.knora.webapi.slice.common.api import sttp.model.ContentTypeRange import sttp.model.MediaType -import sttp.tapir.Codec -import sttp.tapir.CodecFormat -import sttp.tapir.DecodeResult -import sttp.tapir.EndpointIO -import sttp.tapir.EndpointInput -import sttp.tapir.extractFromRequest -import sttp.tapir.header -import sttp.tapir.query +import sttp.tapir.* + +import java.nio.charset.StandardCharsets import org.knora.webapi.ApiV2Schema import org.knora.webapi.JsonLdRendering import org.knora.webapi.MarkupRendering import org.knora.webapi.Rendering -import org.knora.webapi.messages.util.rdf.RdfFormat +import org.knora.webapi.messages.util.rdf.* import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions object ApiV2 { @@ -132,7 +127,7 @@ object ApiV2 { private val rdfFormat: EndpointInput.ExtractFromRequest[RdfFormat] = extractFromRequest(_.acceptsContentTypes) .description( s"""With the Accept header the RDF format used for the response can be specified. - |Valid values are: ${RdfFormat.values}. + |Valid values are: ${RdfFormat.values.map(_.mediaType.toString())}. |If not specified or unknown, the fallback RDF format ${RdfFormat.default} will be used.""".stripMargin, ) .mapDecode(s => @@ -153,6 +148,17 @@ object ApiV2 { ) } + object Outputs { + val stringBodyFormatted: EndpointIO.OneOfBody[String, String] = oneOfBody( + stringBodyAnyFormat(Codec.string.format(JsonLD), StandardCharsets.UTF_8), + stringBodyAnyFormat(Codec.string.format(Turtle), StandardCharsets.UTF_8), + stringBodyAnyFormat(Codec.string.format(TriG), StandardCharsets.UTF_8), + stringBodyAnyFormat(Codec.string.format(RdfXml), StandardCharsets.UTF_8), + stringBodyAnyFormat(Codec.string.format(NQuads), StandardCharsets.UTF_8), + ) + val contentTypeHeader: EndpointIO.Header[MediaType] = header[MediaType]("Content-Type") + } + private object Codecs { // Codec for ApiV2Schema implicit val apiV2SchemaListCodec: Codec[List[String], Option[ApiV2Schema], CodecFormat.TextPlain] = @@ -164,4 +170,5 @@ object ApiV2 { implicit val markupRenderingListCode: Codec[List[String], Option[MarkupRendering], CodecFormat.TextPlain] = Codec.listHeadOption(Codec.string.mapEither(MarkupRendering.from)(_.name)) } + } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala index 16d7f093288..162f668cc9e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.lists.api -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import zio.* @@ -39,14 +38,14 @@ final case class ListsEndpointsV2(private val base: BaseEndpoints) { .in("v2" / "lists" / listIri) .in(ApiV2.Inputs.formatOptions) .out(stringBody.example(Examples.listGetResponseV2.format(FormatOptions.default, Examples.appConfig))) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description("Returns a list (a graph with all list nodes).") val getV2Node = base.withUserEndpoint.get .in("v2" / "node" / listIri) .in(ApiV2.Inputs.formatOptions) .out(stringBody.example(Examples.nodeGetResponseV2.format(FormatOptions.default, Examples.appConfig))) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description("Returns a list node.") val endpoints: Seq[AnyEndpoint] = Seq( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala index e0f7b2c36b3..39925a18fda 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.ontology.api -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import zio.ZLayer @@ -46,8 +45,8 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .in(allLanguages) .in(ApiV2.Inputs.formatOptions) .in(extractFromRequest(_.uri)) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description( "This is the route used to dereference an actual ontology IRI. " + "If the URL path looks like it belongs to a built-in API ontology (which has to contain \"knora-api\"), prefix it with http://api.knora.org to get the ontology IRI. " + @@ -58,54 +57,54 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .in(base / "metadata") .in(header[Option[ProjectIri]](ApiV2.Headers.xKnoraAcceptProject)) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Get the metadata of an ontology") val putOntologiesMetadata = baseEndpoints.securedEndpoint.put .in(base / "metadata") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Change the metadata of an ontology") val getOntologiesMetadataProjects = baseEndpoints.publicEndpoint.get .in(base / "metadata" / paths.description("projectIris")) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesAllentities = baseEndpoints.withUserEndpoint.get .in(base / "allentities" / ontologyIriPath) .in(allLanguages) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Get all entities of an ontology") val postOntologiesClasses = baseEndpoints.withUserEndpoint.post .in(base / "classes") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Create a new class") val putOntologiesClasses = baseEndpoints.withUserEndpoint.put .in(base / "classes") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Change the labels or comments of a class") val deleteOntologiesClassesComment = baseEndpoints.withUserEndpoint.delete .in(base / "classes" / "comment" / resourceClassIriPath) .in(lastModificationDate) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Delete the comment of a class definition.") val postOntologiesCardinalities = baseEndpoints.withUserEndpoint.post @@ -146,7 +145,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { |""".stripMargin, ), ) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description( "Add cardinalities to a class. " + "For more info check out the documentation.", @@ -170,7 +169,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { | "${KnoraApiV2Complex.CannotDoReason}": "The new cardinality is not included in the cardinality of a super-class.", |} |""".stripMargin)) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description( "If only a class IRI is provided, this endpoint checks if any cardinality of any of the class properties can " + "be replaced. " + @@ -184,124 +183,124 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .in(base / "cardinalities") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postOntologiesCandeletecardinalities = baseEndpoints.withUserEndpoint.post .in(base / "candeletecardinalities") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val patchOntologiesCardinalities = baseEndpoints.withUserEndpoint.patch .in(base / "cardinalities") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val putOntologiesGuiorder = baseEndpoints.withUserEndpoint.put .in(base / "guiorder") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesClassesIris = baseEndpoints.withUserEndpoint.get .in(base / "classes" / paths) .in(allLanguages) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesCandeleteclass = baseEndpoints.withUserEndpoint.get .in(base / "candeleteclass" / resourceClassIriPath) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val deleteOntologiesClasses = baseEndpoints.withUserEndpoint.delete .in(base / "classes" / resourceClassIriPath) .in(lastModificationDate) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val deleteOntologiesComment = baseEndpoints.withUserEndpoint.delete .in(base / "comment" / ontologyIriPath) .in(lastModificationDate) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postOntologiesProperties = baseEndpoints.withUserEndpoint.post .in(base / "properties") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val putOntologiesProperties = baseEndpoints.withUserEndpoint.put .in(base / "properties") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val deletePropertiesComment = baseEndpoints.withUserEndpoint.delete .in(base / "properties" / "comment" / propertyIriPath) .in(lastModificationDate) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val putOntologiesPropertiesGuielement = baseEndpoints.withUserEndpoint.put .in(base / "properties" / "guielement") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesProperties = baseEndpoints.withUserEndpoint.get .in(base / "properties" / paths) .in(allLanguages) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesCandeleteproperty = baseEndpoints.withUserEndpoint.get .in(base / "candeleteproperty" / propertyIriPath) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val deleteOntologiesProperty = baseEndpoints.securedEndpoint.delete .in(base / "properties" / propertyIriPath) .in(ApiV2.Inputs.formatOptions) .in(lastModificationDate) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postOntologies = baseEndpoints.securedEndpoint.post .in(base) .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesCandeleteontology = baseEndpoints.securedEndpoint .in(base / "candeleteontology" / ontologyIriPath) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val deleteOntologies = baseEndpoints.securedEndpoint.delete .in(base / ontologyIriPath) .in(ApiV2.Inputs.formatOptions) .in(lastModificationDate) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val endpoints = ( Seq( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 117d546ea9b..d1f4ae9c271 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -17,34 +17,39 @@ final class OntologiesServerEndpoints( ) { val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List( + // GET endpoints.getOntologiesMetadataProject.zServerLogic(restService.getOntologyMetadataByProjectOption), endpoints.getOntologiesMetadataProjects.zServerLogic(restService.getOntologyMetadataByProjects), endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), - endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), endpoints.getOntologiesAllentities.serverLogic(restService.getOntologyEntities), - endpoints.postOntologiesClasses.serverLogic(restService.createClass), - endpoints.putOntologiesClasses.serverLogic(restService.changeClassLabelsOrComments), - endpoints.deleteOntologiesClassesComment.serverLogic(restService.deleteClassComment), - endpoints.postOntologiesCardinalities.serverLogic(restService.addCardinalities), endpoints.getOntologiesCanreplacecardinalities.serverLogic(restService.canChangeCardinality), - endpoints.putOntologiesCardinalities.serverLogic(restService.replaceCardinalities), - endpoints.postOntologiesCandeletecardinalities.serverLogic(restService.canDeleteCardinalitiesFromClass), - endpoints.patchOntologiesCardinalities.serverLogic(restService.deleteCardinalitiesFromClass), - endpoints.putOntologiesGuiorder.serverLogic(restService.changeGuiOrder), endpoints.getOntologiesClassesIris.serverLogic(restService.getClasses), endpoints.getOntologiesCandeleteclass.serverLogic(restService.canDeleteClass), + endpoints.getOntologiesProperties.serverLogic(restService.getProperties), // CAUSING 405 with postOntologiesProperties + endpoints.getOntologiesCandeleteproperty.serverLogic(restService.canDeleteProperty), + endpoints.getOntologiesCandeleteontology.serverLogic(restService.canDeleteOntology), + // DELETE + endpoints.deleteOntologiesClassesComment.serverLogic(restService.deleteClassComment), endpoints.deleteOntologiesClasses.serverLogic(restService.deleteClass), endpoints.deleteOntologiesComment.serverLogic(restService.deleteOntologyComment), - endpoints.postOntologiesProperties.serverLogic(restService.createProperty), - endpoints.putOntologiesProperties.serverLogic(restService.changePropertyLabelsOrComments), endpoints.deletePropertiesComment.serverLogic(restService.deletePropertyComment), - endpoints.putOntologiesPropertiesGuielement.serverLogic(restService.changePropertyGuiElement), - endpoints.getOntologiesProperties.serverLogic(restService.getProperties), - endpoints.getOntologiesCandeleteproperty.serverLogic(restService.canDeleteProperty), endpoints.deleteOntologiesProperty.serverLogic(restService.deleteProperty), - endpoints.postOntologies.serverLogic(restService.createOntology), - endpoints.getOntologiesCandeleteontology.serverLogic(restService.canDeleteOntology), endpoints.deleteOntologies.serverLogic(restService.deleteOntology), + // PATCH + endpoints.patchOntologiesCardinalities.serverLogic(restService.deleteCardinalitiesFromClass), + // POST + endpoints.postOntologiesClasses.serverLogic(restService.createClass), + endpoints.postOntologiesCardinalities.serverLogic(restService.addCardinalities), + endpoints.postOntologiesCandeletecardinalities.serverLogic(restService.canDeleteCardinalitiesFromClass), + endpoints.postOntologiesProperties.serverLogic(restService.createProperty), + endpoints.postOntologies.serverLogic(restService.createOntology), + // PUT + endpoints.putOntologiesMetadata.serverLogic(restService.changeOntologyMetadata), + endpoints.putOntologiesClasses.serverLogic(restService.changeClassLabelsOrComments), + endpoints.putOntologiesCardinalities.serverLogic(restService.replaceCardinalities), + endpoints.putOntologiesGuiorder.serverLogic(restService.changeGuiOrder), + endpoints.putOntologiesProperties.serverLogic(restService.changePropertyLabelsOrComments), + endpoints.putOntologiesPropertiesGuielement.serverLogic(restService.changePropertyGuiElement), ) } object OntologiesServerEndpoints { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala index 8deb0f0cd66..ec801814e9b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ResourcesEndpoints.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.resources.api -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import zio.ZLayer @@ -52,41 +51,41 @@ final case class ResourcesEndpoints( val getResourcesPreview = baseEndpoints.withUserEndpoint.get .in("v2" / "resourcespreview" / paths) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesIiifManifest = baseEndpoints.withUserEndpoint.get .in(base / "iiifmanifest" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesProjectHistoryEvents = baseEndpoints.withUserEndpoint.get .in(base / "projectHistoryEvents" / path[ProjectIri].name("projectIri")) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesHistoryEvents = baseEndpoints.withUserEndpoint.get .in(base / "resourceHistoryEvents" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesHistory = baseEndpoints.withUserEndpoint.get .in(base / "history" / path[IriDto].name("resourceIri")) .in(ApiV2.Inputs.formatOptions) .in(startDateQuery) .in(endDateQuery) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResources = baseEndpoints.withUserEndpoint.get .in(base / paths) .in(ApiV2.Inputs.formatOptions) .in(versionQuery) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesParams = baseEndpoints.withUserEndpoint.get .in(base) @@ -95,8 +94,8 @@ final case class ResourcesEndpoints( .in(query[Int]("page").validate(Validator.min(0))) .in(header[ProjectIri](ApiV2.Headers.xKnoraAcceptProject)) .in(ApiV2.Inputs.formatOptions) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesGraph = baseEndpoints.withUserEndpoint.get .in("v2" / "graph" / path[IriDto].name("resourceIri")) @@ -109,8 +108,8 @@ final case class ResourcesEndpoints( ) .in(query[GraphDirection]("direction").default(GraphDirection.default)) .in(query[Option[IriDto]]("excludeProperty")) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesTei = baseEndpoints.withUserEndpoint.get .in("v2" / "tei" / path[IriDto].name("resourceIri")) @@ -118,43 +117,43 @@ final case class ResourcesEndpoints( .in(query[IriDto]("textProperty")) .in(query[Option[IriDto]]("gravsearchTemplateIri")) .in(query[Option[IriDto]]("headerXSLTIri")) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postResourcesErase = baseEndpoints.withUserEndpoint.post .in(base / "erase") .in(ApiV2.Inputs.formatOptions) .in(stringJsonBody) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val getResourcesCanDelete = baseEndpoints.withUserEndpoint.get .in(base / "candelete") .in(ApiV2.Inputs.formatOptions) .in(query[String]("jsonLd")) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postResourcesDelete = baseEndpoints.withUserEndpoint.post .in(base / "delete") .in(ApiV2.Inputs.formatOptions) .in(stringJsonBody) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val postResources = baseEndpoints.withUserEndpoint.post .in(base) .in(ApiV2.Inputs.formatOptions) .in(stringJsonBody) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val putResources = baseEndpoints.withUserEndpoint.put .in(base) .in(ApiV2.Inputs.formatOptions) .in(stringJsonBody) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) val endpoints: Seq[AnyEndpoint] = Seq( getResourcesIiifManifest, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpoints.scala index 6cb1bcc1476..4b820a6557b 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/StandoffEndpoints.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.resources.api -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import zio.ZLayer @@ -26,7 +25,7 @@ final case class StandoffEndpoints(baseEndpoints: BaseEndpoints) { .in(multipartBody[CreateStandoffMappingForm]) .in(ApiV2.Inputs.formatOptions) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) val endpoints: Seq[AnyEndpoint] = Seq(postMapping).map(_.endpoint.tag("V2 Standoff")) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala index 56dd0359e18..7f835d53039 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resources/api/ValuesEndpoints.scala @@ -5,7 +5,6 @@ package org.knora.webapi.slice.resources.api -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import zio.ZLayer @@ -31,7 +30,7 @@ final case class ValuesEndpoints(baseEndpoint: BaseEndpoints) { .in(version) .in(ApiV2.Inputs.formatOptions) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description(linkToValuesDocumentation) val postValues = baseEndpoint.withUserEndpoint.post @@ -42,7 +41,7 @@ final case class ValuesEndpoints(baseEndpoint: BaseEndpoints) { ), ) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description(linkToValuesDocumentation) val putValues = baseEndpoint.withUserEndpoint.put @@ -53,28 +52,28 @@ final case class ValuesEndpoints(baseEndpoint: BaseEndpoints) { ), ) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description(linkToValuesDocumentation) val deleteValues = baseEndpoint.withUserEndpoint.post .in(base / "delete") .in(stringJsonBody.example(ValuesEndpoints.Examples.deleteValue)) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description(linkToValuesDocumentation) val postValuesErase = baseEndpoint.securedEndpoint.post .in(base / "erase") .in(stringJsonBody.example(ValuesEndpoints.Examples.deleteValue)) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description(s"Erase a Value and all of its old versions from the database completely. $linkToValuesDocumentation") val postValuesErasehistory = baseEndpoint.securedEndpoint.post .in(base / "erasehistory") .in(stringJsonBody.example(ValuesEndpoints.Examples.deleteValue)) .out(stringJsonBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.contentTypeHeader) .description( s"Erase all old versions of a Value from the database completely and keep only the latest version. $linkToValuesDocumentation", ) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 0fe9307dac4..496780fe3ed 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.search.api import eu.timepit.refined.api.Refined import eu.timepit.refined.api.RefinedTypeOps import eu.timepit.refined.numeric.Greater -import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* import sttp.tapir.codec.refined.* @@ -71,16 +70,16 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(stringBody.description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources using a Gravsearch query.") val getGravsearch = baseEndpoints.withUserEndpoint.get .in("v2" / "searchextended" / path[String].description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources using a Gravsearch query.") val postGravsearchCount = baseEndpoints.withUserEndpoint.post @@ -88,16 +87,16 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(stringBody.description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Count resources using a Gravsearch query.") val getGravsearchCount = baseEndpoints.withUserEndpoint.get .in("v2" / "searchextended" / "count" / path[String].description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Count resources using a Gravsearch query.") val getSearchIncomingLinks = baseEndpoints.withUserEndpoint.get @@ -105,8 +104,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.offset) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for incoming links using a Gravsearch query with an offset.") val getSearchStillImageRepresentations = baseEndpoints.withUserEndpoint.get @@ -118,8 +117,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.offset) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for StillImageRepresentations using a Gravsearch query with an offset.") val getSearchStillImageRepresentationsCount = baseEndpoints.withUserEndpoint.get @@ -130,8 +129,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { ) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Count SearchStillImageRepresentations using a Gravsearch query.") val getSearchIncomingRegions = baseEndpoints.withUserEndpoint.get @@ -142,8 +141,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.offset) .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for incoming regions using a Gravsearch query with an offset.") val getSearchByLabel = baseEndpoints.withUserEndpoint.get @@ -152,8 +151,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.offset) .in(SearchEndpointsInputs.limitToProject) .in(SearchEndpointsInputs.limitToResourceClass) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources by label.") val getSearchByLabelCount = baseEndpoints.withUserEndpoint.get @@ -161,8 +160,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(ApiV2.Inputs.formatOptions) .in(SearchEndpointsInputs.limitToProject) .in(SearchEndpointsInputs.limitToResourceClass) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources by label.") val getFullTextSearch = baseEndpoints.withUserEndpoint.get @@ -173,8 +172,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.limitToResourceClass) .in(SearchEndpointsInputs.limitToStandoffClass) .in(SearchEndpointsInputs.returnFiles) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources by label.") val getFullTextSearchCount = baseEndpoints.withUserEndpoint.get @@ -183,8 +182,8 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .in(SearchEndpointsInputs.limitToProject) .in(SearchEndpointsInputs.limitToResourceClass) .in(SearchEndpointsInputs.limitToStandoffClass) - .out(stringBody) - .out(header[MediaType](HeaderNames.ContentType)) + .out(ApiV2.Outputs.stringBodyFormatted) + .out(ApiV2.Outputs.contentTypeHeader) .description("Search for resources by label.") val endpoints: Seq[AnyEndpoint] = From c517273cb9b68caee1f739a5fb016e900d43817a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 25 Sep 2025 14:56:04 +0200 Subject: [PATCH 47/99] cleanup --- .../org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala index f5e2784f8ab..dd7671f4504 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/it/v2/KnoraSipiIntegrationV2ITSpec.scala @@ -39,7 +39,6 @@ import org.knora.webapi.util.MutableTestIri * Tests interaction between Knora and Sipi using Knora API v2. */ object KnoraSipiIntegrationV2ITSpec extends E2EZSpec { - private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance private val stillImageResourceIri = new MutableTestIri private val stillImageFileValueIri = new MutableTestIri From 48f3e50302380fa59fa67f4d0487c77a66cbd174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 25 Sep 2025 15:02:08 +0200 Subject: [PATCH 48/99] cleanup --- .../slice/lists/api/ListsEndpointsV2.scala | 4 +- .../ontology/api/OntologiesEndpoints.scala | 43 +------------------ 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala index 162f668cc9e..fea38ea16b0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/lists/api/ListsEndpointsV2.scala @@ -37,14 +37,14 @@ final case class ListsEndpointsV2(private val base: BaseEndpoints) { val getV2Lists = base.withUserEndpoint.get .in("v2" / "lists" / listIri) .in(ApiV2.Inputs.formatOptions) - .out(stringBody.example(Examples.listGetResponseV2.format(FormatOptions.default, Examples.appConfig))) + .out(ApiV2.Outputs.stringBodyFormatted) .out(ApiV2.Outputs.contentTypeHeader) .description("Returns a list (a graph with all list nodes).") val getV2Node = base.withUserEndpoint.get .in("v2" / "node" / listIri) .in(ApiV2.Inputs.formatOptions) - .out(stringBody.example(Examples.nodeGetResponseV2.format(FormatOptions.default, Examples.appConfig))) + .out(ApiV2.Outputs.stringBodyFormatted) .out(ApiV2.Outputs.contentTypeHeader) .description("Returns a list node.") diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala index 39925a18fda..1ddc6fe2f53 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala @@ -11,7 +11,6 @@ import zio.ZLayer import java.time.Instant -import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.slice.admin.api.Codecs.TapirCodec import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri @@ -111,40 +110,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .in(base / "cardinalities") .in(stringJsonBody) .in(ApiV2.Inputs.formatOptions) - .out( - stringBody - .example( - s""" - |{ - | "@id" : "ONTOLOGY_IRI", - | "@type" : "owl:Ontology", - | "knora-api:lastModificationDate" : { - | "@type" : "xsd:dateTimeStamp", - | "@value" : "ONTOLOGY_LAST_MODIFICATION_DATE" - | }, - | "@graph" : [ - | { - | "@id" : "CLASS_IRI", - | "@type" : "owl:Class", - | "rdfs:subClassOf" : { - | "@type": "owl:Restriction", - | "OWL_CARDINALITY_PREDICATE": "OWL_CARDINALITY_VALUE", - | "owl:onProperty": { - | "@id" : "PROPERTY_IRI" - | } - | } - | } - | ], - | "@context" : { - | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", - | "owl" : "http://www.w3.org/2002/07/owl#", - | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", - | "xsd" : "http://www.w3.org/2001/XMLSchema#" - | } - |} - |""".stripMargin, - ), - ) + .out(ApiV2.Outputs.stringBodyFormatted) .out(ApiV2.Outputs.contentTypeHeader) .description( "Add cardinalities to a class. " + @@ -163,12 +129,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .example(Some(Cardinality.AtLeastOne.toString)), ) .in(ApiV2.Inputs.formatOptions) - .out(stringBody.example(s""" - |{ - | "${KnoraApiV2Complex.CanDo}": false, - | "${KnoraApiV2Complex.CannotDoReason}": "The new cardinality is not included in the cardinality of a super-class.", - |} - |""".stripMargin)) + .out(ApiV2.Outputs.stringBodyFormatted) .out(ApiV2.Outputs.contentTypeHeader) .description( "If only a class IRI is provided, this endpoint checks if any cardinality of any of the class properties can " + From dd56cc35168e087ea14f4a9f4e6013207c404ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 25 Sep 2025 15:07:23 +0200 Subject: [PATCH 49/99] fmt --- .../slice/security/api/AuthenticationEndpointsV2E2ESpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala index f295b73a40d..a1776b78808 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/security/api/AuthenticationEndpointsV2E2ESpec.scala @@ -21,6 +21,7 @@ import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LogoutRespo import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse import org.knora.webapi.testservices.ResponseOps.* import org.knora.webapi.testservices.TestApiClient + object AuthenticationEndpointsV2E2ESpec extends E2EZSpec { private val validPassword = "test" From bb9911af0e03827a769efef9560388f151b752c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 25 Sep 2025 17:17:19 +0200 Subject: [PATCH 50/99] fmt --- .../webapi/slice/ontology/api/OntologiesServerEndpoints.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index d1f4ae9c271..3519355cfb7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -25,7 +25,9 @@ final class OntologiesServerEndpoints( endpoints.getOntologiesCanreplacecardinalities.serverLogic(restService.canChangeCardinality), endpoints.getOntologiesClassesIris.serverLogic(restService.getClasses), endpoints.getOntologiesCandeleteclass.serverLogic(restService.canDeleteClass), - endpoints.getOntologiesProperties.serverLogic(restService.getProperties), // CAUSING 405 with postOntologiesProperties + endpoints.getOntologiesProperties.serverLogic( + restService.getProperties, + ), // CAUSING 405 with postOntologiesProperties endpoints.getOntologiesCandeleteproperty.serverLogic(restService.canDeleteProperty), endpoints.getOntologiesCandeleteontology.serverLogic(restService.canDeleteOntology), // DELETE From 3385a1b5755fc5fdf44819b06689ecab40a0db68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 26 Sep 2025 11:28:08 +0200 Subject: [PATCH 51/99] fix Method not allowed by only allowing a single iri for class and property on the ontology api, BREAKS backware compatibility --- .../responders/v2/OntologyResponderV2.scala | 10 ++++- .../knora/webapi/slice/common/KnoraIris.scala | 2 + .../ontology/api/OntologiesEndpoints.scala | 5 ++- .../api/OntologiesServerEndpoints.scala | 6 +-- .../api/service/OntologiesRestService.scala | 45 +++++++------------ .../domain/service/OntologyCacheHelpers.scala | 8 +--- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala index f5ae07947e0..a29b3001f2a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala @@ -346,6 +346,13 @@ final case class OntologyResponderV2( userLang = Some(requestingUser.lang).filter(_ => !allLanguages) } yield ontology.copy(userLang = userLang) + def getPropertyFromOntologyV2( + propertyIri: PropertyIri, + allLanguages: Boolean, + requestingUser: User, + ): Task[ReadOntologyV2] = + getPropertyDefinitionsFromOntologyV2(Set(propertyIri.smartIri), allLanguages, requestingUser) + /** * Requests information about properties in a single ontology. * @@ -1979,8 +1986,7 @@ final case class OntologyResponderV2( .someOrFail(NotFoundException(s"Class ${classIri.toComplexSchema} not found")) hasComment = classToUpdate.entityInfoContent.predicates.contains(OntologyConstants.Rdfs.Comment.toSmartIri) _ <- IriLocker.runWithIriLock(apiRequestID, ONTOLOGY_CACHE_LOCK_IRI)(deleteCommentTask).when(hasComment) - response <- - ontologyCacheHelpers.getClasses(classIris = Seq(classIri), allLanguages = true, requestingUser = requestingUser) + response <- ontologyCacheHelpers.getClassAsReadOntologyV2(classIri, allLanguages = true, requestingUser) } yield response } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala index 39326124a72..49ac7b0d1ad 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/KnoraIris.scala @@ -30,6 +30,8 @@ object KnoraIris { final def toInternalSchema: SmartIri = self.smartIri.toInternalSchema final def toOntologySchema(s: OntologySchema): SmartIri = self.smartIri.toOntologySchema(s) + final def ontologySchema: Option[OntologySchema] = smartIri.getOntologySchema + override def toString: String = self.smartIri.toString def toShortString = s"${self.smartIri.getOntologyName}:${self.smartIri.getEntityName}" diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala index 1ddc6fe2f53..08f75212457 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesEndpoints.scala @@ -36,6 +36,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { private val ontologyIriPath = path[IriDto].name("ontologyIri") private val propertyIriPath = path[IriDto].name("propertyIri") private val resourceClassIriPath = path[IriDto].name("resourceClassIri") + private val classIriPath = path[IriDto].name("classIri") private val lastModificationDate = query[LastModificationDate]("lastModificationDate") private val allLanguages = query[Boolean]("allLanguages").default(false) @@ -169,7 +170,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesClassesIris = baseEndpoints.withUserEndpoint.get - .in(base / "classes" / paths) + .in(base / "classes" / classIriPath) .in(allLanguages) .in(ApiV2.Inputs.formatOptions) .out(ApiV2.Outputs.stringBodyFormatted) @@ -224,7 +225,7 @@ final case class OntologiesEndpoints(baseEndpoints: BaseEndpoints) { .out(ApiV2.Outputs.contentTypeHeader) val getOntologiesProperties = baseEndpoints.withUserEndpoint.get - .in(base / "properties" / paths) + .in(base / "properties" / propertyIriPath) .in(allLanguages) .in(ApiV2.Inputs.formatOptions) .out(ApiV2.Outputs.stringBodyFormatted) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala index 3519355cfb7..4aadc3ecf88 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/OntologiesServerEndpoints.scala @@ -23,11 +23,9 @@ final class OntologiesServerEndpoints( endpoints.getOntologyPathSegments.serverLogic(restService.dereferenceOntologyIri), endpoints.getOntologiesAllentities.serverLogic(restService.getOntologyEntities), endpoints.getOntologiesCanreplacecardinalities.serverLogic(restService.canChangeCardinality), - endpoints.getOntologiesClassesIris.serverLogic(restService.getClasses), + endpoints.getOntologiesClassesIris.serverLogic(restService.findClassByIri), endpoints.getOntologiesCandeleteclass.serverLogic(restService.canDeleteClass), - endpoints.getOntologiesProperties.serverLogic( - restService.getProperties, - ), // CAUSING 405 with postOntologiesProperties + endpoints.getOntologiesProperties.serverLogic(restService.findPropertyByIri), endpoints.getOntologiesCandeleteproperty.serverLogic(restService.canDeleteProperty), endpoints.getOntologiesCandeleteontology.serverLogic(restService.canDeleteOntology), // DELETE diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala index ae218f917a3..99c1db619b5 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala @@ -7,10 +7,10 @@ package org.knora.webapi.slice.ontology.api.service import sttp.model.MediaType import zio.* - import dsp.errors.BadRequestException import dsp.errors.NotFoundException import org.knora.webapi.ApiV2Schema +import org.knora.webapi.OntologySchema import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.StringFormatter @@ -212,26 +212,16 @@ final case class OntologiesRestService( response <- renderer.render(result, formatOptions) } yield response - def getClasses(user: User)( - classIris: List[String], + def findClassByIri(user: User)( + classIriDto: IriDto, allLanguages: Boolean, formatOptions: FormatOptions, ) = for { - classIris <- ZIO.foreach(classIris)(iriConverter.asResourceClassIri(_).mapError(BadRequestException.apply)) - classIrisWithoutSchema = classIris.filter(_.smartIri.getOntologySchema.isEmpty) - _ <- ZIO.fail { - BadRequestException(s"Class IRIs found without ontology schema: ${classIrisWithoutSchema.mkString(", ")}") - }.unless(classIrisWithoutSchema.isEmpty) - ontologyIris = classIris.map(_.ontologyIri).toSet - _ <- ontologyIris.size match - case 1 => - ZIO.fail(BadRequestException("Only external ontologies may be queried")).when(ontologyIris.head.isInternal) - case _ => ZIO.fail(BadRequestException("Only one ontology may be queried at once")) - schemas = ontologyIris.flatMap(_.smartIri.getOntologySchema).collect { case schema: ApiV2Schema => schema } - schema <- schemas.size match - case 1 => ZIO.succeed(schemas.head) - case _ => ZIO.fail(BadRequestException("Only one ontology schema may be queried at once")) - result <- ontologyCacheHelpers.getClasses(classIris, allLanguages, user) + classIri <- iriConverter.asResourceClassIri(classIriDto.value).mapError(BadRequestException.apply) + schema <- ZIO + .fromOption(classIri.ontologySchema.collect { case s: ApiV2Schema => s }) + .orElseFail(BadRequestException(s"Class IRI must have an API V2 schema: $classIriDto")) + result <- ontologyCacheHelpers.getClassAsReadOntologyV2(classIri, allLanguages, user) response <- renderer.render(result, formatOptions.copy(schema = schema)) } yield response @@ -315,21 +305,16 @@ final case class OntologiesRestService( response <- renderer.render(result, formatOptions) } yield response - def getProperties(user: User)( - propertyIris: List[String], + def findPropertyByIri(user: User)( + propertyIri: IriDto, allLanguages: Boolean, formatOptions: FormatOptions, ): Task[(RenderedResponse, MediaType)] = for { - propertyIris <- ZIO.foreach(propertyIris.toSet)(iriConverter.asPropertyIri).mapError(BadRequestException.apply) - _ <- ZIO - .fail(BadRequestException(s"Only one ontology may be queried at once")) - .when(propertyIris.map(_.ontologyIri).size != 1) - schemasFromProperties = propertyIris.flatMap(_.smartIri.getOntologySchema) - formatOptions <- ZIO - .fail(BadRequestException(s"Only one ontology schema may be queried at once")) - .when(schemasFromProperties.size != 1) - .as(formatOptions.copy(schema = schemasFromProperties.head.asInstanceOf[ApiV2Schema])) - result <- ontologyResponder.getPropertiesFromOntologyV2(propertyIris, allLanguages, user) + propertyIri <- iriConverter.asPropertyIri(propertyIri.value).mapError(BadRequestException.apply) + schema <- ZIO + .fromOption(propertyIri.ontologySchema.collect { case s: ApiV2Schema => s }) + .orElseFail(BadRequestException(s"Property IRI must have an API V2 schema: $propertyIri")) + result <- ontologyResponder.getPropertyFromOntologyV2(propertyIri, allLanguages, user) response <- renderer.render(result, formatOptions) } yield response diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyCacheHelpers.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyCacheHelpers.scala index 7d24629ca99..ce2b59847a2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyCacheHelpers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyCacheHelpers.scala @@ -25,12 +25,8 @@ import org.knora.webapi.slice.ontology.repo.service.OntologyCache final case class OntologyCacheHelpers(ontologyCache: OntologyCache, ontologyRepo: OntologyRepo) { - def getClasses( - classIris: Seq[ResourceClassIri], - allLanguages: Boolean, - requestingUser: User, - ): Task[ReadOntologyV2] = - getClassDefinitionsFromOntologyV2(classIris.map(_.smartIri).toSet, allLanguages, requestingUser) + def getClassAsReadOntologyV2(classIri: ResourceClassIri, allLanguages: Boolean, requestingUser: User): Task[ReadOntologyV2] = + getClassDefinitionsFromOntologyV2(Set(classIri.smartIri), allLanguages, requestingUser) /** * Requests information about OWL classes in a single ontology. From 30acc7e302224d32d62d29c905dd54afdb93dac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 11:50:33 +0200 Subject: [PATCH 52/99] inline LayersTest --- .../scala/org/knora/webapi/E2EZSpec.scala | 23 ++++++++--- .../org/knora/webapi/core/LayersTest.scala | 41 ------------------- 2 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 39925389b9d..ccd1df78799 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -12,27 +12,32 @@ import zio.* import zio.json.ast.Json import zio.test.* import zio.test.Assertion.* - import scala.reflect.ClassTag import org.knora.webapi.core.Db import org.knora.webapi.core.DspApiServer -import org.knora.webapi.core.LayersTest +import org.knora.webapi.core.LayersLive +import org.knora.webapi.core.TestContainerLayers import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.testservices.TestApiClient +import org.knora.webapi.testservices.TestClientsModule import org.knora.webapi.util.Logger -abstract class E2EZSpec extends ZIOSpec[LayersTest.Environment] { +abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { implicit val sf: StringFormatter = StringFormatter.getInitializedTestInstance - override val bootstrap: ULayer[LayersTest.Environment] = Logger.text >>> LayersTest.layer + override val bootstrap: ULayer[E2EZSpec.Environment] = + Logger.text >>> + TestContainerLayers.all >+> + LayersLive.remainingLayer >+> + TestClientsModule.layer def rdfDataObjects: List[RdfDataObject] = List.empty - type env = LayersTest.Environment with Scope + type env = E2EZSpec.Environment with Scope private def prepare = for { _ <- Db.initWithTestData(rdfDataObjects) @@ -57,6 +62,14 @@ abstract class E2EZSpec extends ZIOSpec[LayersTest.Environment] { } object E2EZSpec { + + type Environment = + // format: off + LayersLive.Environment & + TestClientsModule.Provided & + TestContainerLayers.Environment + // format: on + def failsWithMessageEqualTo[A <: Throwable](messsage: String)(implicit tag: ClassTag[A], ): Assertion[Exit[Any, Any]] = diff --git a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala b/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala deleted file mode 100644 index 2c44a765ee9..00000000000 --- a/modules/testkit/src/main/scala/org/knora/webapi/core/LayersTest.scala +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.core - -import zio.* - -import org.knora.webapi.messages.util.* -import org.knora.webapi.messages.util.standoff.StandoffTagUtilV2 -import org.knora.webapi.responders.admin.* -import org.knora.webapi.responders.v2.* -import org.knora.webapi.responders.v2.ontology.CardinalityHandler -import org.knora.webapi.routing.* -import org.knora.webapi.slice.admin.domain.service.* -import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser -import org.knora.webapi.slice.common.api.* -import org.knora.webapi.slice.resources.repo.service.ResourcesRepo -import org.knora.webapi.store.iiif.IIIFRequestMessageHandler -import org.knora.webapi.store.iiif.api.SipiService -import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater -import org.knora.webapi.testservices.TestClientsModule - -object LayersTest { self => - - type Environment = - // format: off - LayersLive.Environment & - TestClientsModule.Provided & - TestContainerLayers.Environment - // format: on - - /** - * Provides a layer for integration tests which depend on Fuseki and Sipi as Testcontainers. - * @return a [[ULayer]] with the [[DefaultTestEnvironmentWithSipi]] - */ - val layer: ULayer[self.Environment] = - TestContainerLayers.all >+> LayersLive.remainingLayer >+> TestClientsModule.layer - -} From 5123859afe1ac6d5e798a350ba0c6e58a0bfc922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 11:52:34 +0200 Subject: [PATCH 53/99] typo --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index ccd1df78799..4fa5351c37d 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -37,7 +37,7 @@ abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { def rdfDataObjects: List[RdfDataObject] = List.empty - type env = E2EZSpec.Environment with Scope + type env = E2EZSpec.Environment & Scope private def prepare = for { _ <- Db.initWithTestData(rdfDataObjects) From 2b28c54bb78e8bc92fe3475d04e7e3590b1a3205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 11:52:52 +0200 Subject: [PATCH 54/99] fmt --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 4fa5351c37d..d4c7aa41467 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -52,6 +52,7 @@ abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { .orDie _ <- ZIO.logInfo("API is ready, start running tests...") } yield () + def e2eSpec: Spec[env, Any] final override def spec: Spec[env, Any] = From 31ea939b57273026ed2176cf0b523537cd3f6d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 11:57:11 +0200 Subject: [PATCH 55/99] finetuning --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index d4c7aa41467..33aeef0d0d4 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -47,7 +47,7 @@ abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { _ <- TestApiClient .getJson[Json](uri"/version") .repeatWhile(_.code != StatusCode.Ok) - .retry(Schedule.exponential(10.milli)) + .retry(Schedule.fixed(10.milli)) .timeout(5.seconds) .orDie _ <- ZIO.logInfo("API is ready, start running tests...") From 08fb001a76c9d040484ef8954d0376bdb34f62b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 12:13:07 +0200 Subject: [PATCH 56/99] fmt --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 33aeef0d0d4..e48824f6103 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -12,6 +12,7 @@ import zio.* import zio.json.ast.Json import zio.test.* import zio.test.Assertion.* + import scala.reflect.ClassTag import org.knora.webapi.core.Db From c76379b64c8eaec6ec08a7fe5a5e5596d67cebc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 13:35:25 +0200 Subject: [PATCH 57/99] load a file only once into the triplestore --- .../webapi/store/triplestore/impl/TriplestoreServiceLive.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceLive.scala index 76523002131..3bf317fee21 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/triplestore/impl/TriplestoreServiceLive.scala @@ -224,7 +224,7 @@ case class TriplestoreServiceLive( for { _ <- ZIO.logDebug("==>> Loading Data Start") objects = DefaultRdfData.data.toList.filter(_ => prependDefaults) ++ rdfDataObjects - _ <- ZIO.foreachDiscard(objects)(insertObjectIntoTriplestore) + _ <- ZIO.foreachDiscard(objects.toSet)(insertObjectIntoTriplestore) _ <- ZIO.logDebug("==>> Loading Data End") } yield () From 23ac67cda67baac9fcfd834d1dea5b42529e018a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 13:39:26 +0200 Subject: [PATCH 58/99] rm unused TestStartupUtils.scala --- .../knora/webapi/core/TestStartupUtils.scala | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 modules/testkit/src/main/scala/org/knora/webapi/core/TestStartupUtils.scala diff --git a/modules/testkit/src/main/scala/org/knora/webapi/core/TestStartupUtils.scala b/modules/testkit/src/main/scala/org/knora/webapi/core/TestStartupUtils.scala deleted file mode 100644 index 48053d6d057..00000000000 --- a/modules/testkit/src/main/scala/org/knora/webapi/core/TestStartupUtils.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.core - -import zio.* - -import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject -import org.knora.webapi.slice.ontology.repo.service.OntologyCache -import org.knora.webapi.store.triplestore.api.TriplestoreService - -/** - * This trait is only used for testing. It is necessary so that E2E tests will only start - * after the KnoraService is ready. - */ -trait TestStartupUtils { - - /** - * Load the test data and caches - * - * @param rdfDataObjects a list of [[RdfDataObject]] - */ - def prepareRepository( - rdfDataObjects: List[RdfDataObject], - ): ZIO[TriplestoreService with OntologyCache, Throwable, Unit] = - for { - _ <- ZIO.logInfo(s"Loading test data: ${rdfDataObjects.map(_.name).mkString}") - tss <- ZIO.service[TriplestoreService] - _ <- tss.resetTripleStoreContent(rdfDataObjects).timeout(480.seconds) - _ <- ZIO.logInfo("... loading test data done.") - _ <- ZIO.serviceWithZIO[OntologyCache](_.refreshCache()).orDie - } yield () - -} From a67086086926b18b18ba1cd1a0166df28200bba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 15:08:20 +0200 Subject: [PATCH 59/99] fix deriving schema from provided property IRI --- .../e2e/v2/OntologyFormatsE2ESpec.scala | 45 +++++++++---------- .../webapi/testservices/RequestsUpdates.scala | 7 +-- .../api/service/OntologiesRestService.scala | 2 +- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala index a08134e30e6..af1555ae08e 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala @@ -15,13 +15,14 @@ import zio.test.* import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths - import org.knora.webapi.* import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Simple import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject +import org.knora.webapi.messages.util.rdf.JsonLD import org.knora.webapi.messages.util.rdf.RdfFormatUtil import org.knora.webapi.messages.util.rdf.RdfModel +import org.knora.webapi.messages.util.rdf.RdfXml import org.knora.webapi.messages.util.rdf.Turtle import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -78,15 +79,11 @@ object OntologyFormatsE2ESpec extends E2EZSpec { } } - private val mediaTypeJsonLd: MediaType = MediaType.unsafeParse("application/ld+json") - private val mediaTypeTurtle: MediaType = MediaType.unsafeParse("text/turtle") - private val mediaTypeRdfXml: MediaType = MediaType.unsafeParse("application/rdf+xml") - private def checkTestCase(httpGetTest: HttpGetTest) = for { - responseJsonLd <- getResponse(httpGetTest.uri, mediaTypeJsonLd) - responseTtl <- getResponse(httpGetTest.uri, mediaTypeTurtle) - responseRdfXml <- getResponse(httpGetTest.uri, mediaTypeRdfXml) + responseJsonLd <- getResponse(httpGetTest.uri, JsonLD.mediaType) + responseTtl <- getResponse(httpGetTest.uri, Turtle.mediaType) + responseRdfXml <- getResponse(httpGetTest.uri, RdfXml.mediaType) _ = if (!httpGetTest.fileExists) { httpGetTest.writeReceived(responseJsonLd) throw new AssertionError(s"File not found ${httpGetTest.makeFile().toAbsolutePath}") @@ -229,9 +226,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { for { allEntities <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Simple.KnoraApiOntologyIri}", - mediaTypeJsonLd, + JsonLD.mediaType, ) - knoraApi <- getResponse(uri"/ontology/knora-api/simple/v2", mediaTypeJsonLd) + knoraApi <- getResponse(uri"/ontology/knora-api/simple/v2", JsonLD.mediaType) } yield assertTrue( RdfModel.fromJsonLD(allEntities) == RdfModel.fromJsonLD(knoraApi), ) @@ -241,9 +238,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseTurtle <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Simple.KnoraApiOntologyIri}", - mediaTypeTurtle, + Turtle.mediaType, ) - knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/simple/v2", mediaTypeTurtle) + knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/simple/v2", Turtle.mediaType) } yield assertTrue( RdfModel.fromTurtle(ontologyAllEntitiesResponseTurtle) == RdfModel.fromTurtle(knoraApiResponseTurtle), ) @@ -253,9 +250,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseRdfXml <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Simple.KnoraApiOntologyIri}", - mediaTypeRdfXml, + RdfXml.mediaType, ) - knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/simple/v2", mediaTypeRdfXml) + knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/simple/v2", RdfXml.mediaType) } yield assertTrue( RdfModel.fromRdfXml(ontologyAllEntitiesResponseRdfXml) == RdfModel.fromRdfXml(knoraApiResponseRdfXml), ) @@ -263,8 +260,8 @@ object OntologyFormatsE2ESpec extends E2EZSpec { test("serve the knora-api in the complex schema on two separate endpoints JSON-LD") { for { ontologyAllEntitiesResponseJson <- - getResponse(uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", mediaTypeJsonLd) - knoraApiResponseJson <- getResponse(uri"/ontology/knora-api/v2", mediaTypeJsonLd) + getResponse(uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", JsonLD.mediaType) + knoraApiResponseJson <- getResponse(uri"/ontology/knora-api/v2", JsonLD.mediaType) } yield assertTrue( RdfModel.fromJsonLD(ontologyAllEntitiesResponseJson) == RdfModel.fromJsonLD(knoraApiResponseJson), ) @@ -274,9 +271,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseTurtle <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", - mediaTypeTurtle, + Turtle.mediaType, ) - knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/v2", mediaTypeTurtle) + knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/v2", Turtle.mediaType) } yield assertTrue( RdfModel.fromTurtle(ontologyAllEntitiesResponseTurtle) == RdfModel.fromTurtle(knoraApiResponseTurtle), ) @@ -286,9 +283,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseRdfXml <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", - mediaTypeRdfXml, + RdfXml.mediaType, ) - knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/v2", mediaTypeRdfXml) + knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/v2", RdfXml.mediaType) } yield assertTrue( RdfModel.fromRdfXml(ontologyAllEntitiesResponseRdfXml) == RdfModel.fromRdfXml(knoraApiResponseRdfXml), ) @@ -298,9 +295,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseTurtle <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", - mediaTypeTurtle, + Turtle.mediaType, ) - knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/v2", mediaTypeTurtle) + knoraApiResponseTurtle <- getResponse(uri"/ontology/knora-api/v2", Turtle.mediaType) } yield assertTrue( RdfModel.fromTurtle(ontologyAllEntitiesResponseTurtle) == RdfModel.fromTurtle(knoraApiResponseTurtle), ) @@ -310,9 +307,9 @@ object OntologyFormatsE2ESpec extends E2EZSpec { ontologyAllEntitiesResponseRdfXml <- getResponse( uri"/v2/ontologies/allentities/${KnoraApiV2Complex.KnoraApiOntologyIri}", - mediaTypeRdfXml, + RdfXml.mediaType, ) - knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/v2", mediaTypeRdfXml) + knoraApiResponseRdfXml <- getResponse(uri"/ontology/knora-api/v2", RdfXml.mediaType) } yield assertTrue( RdfModel.fromRdfXml(ontologyAllEntitiesResponseRdfXml) == RdfModel.fromRdfXml(knoraApiResponseRdfXml), ) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testservices/RequestsUpdates.scala b/modules/testkit/src/main/scala/org/knora/webapi/testservices/RequestsUpdates.scala index c1bc0240e89..384138b75e3 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testservices/RequestsUpdates.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testservices/RequestsUpdates.scala @@ -5,6 +5,7 @@ package org.knora.webapi.testservices import sttp.client4.Request +import sttp.model.HeaderNames import sttp.model.MediaType object RequestsUpdates { @@ -28,9 +29,9 @@ object RequestsUpdates { def addAcceptHeader[A](accept: MediaType): RequestUpdate[A] = addAcceptHeader(accept.toString) def addAcceptHeaderTurtle[A]: RequestUpdate[A] = - _.header("Accept", "text/turtle") + _.header(HeaderNames.Accept, "text/turtle") def addAcceptHeaderRdfXml[A]: RequestUpdate[A] = - _.header("Accept", "application/rdf+xml") + _.header(HeaderNames.Accept, "application/rdf+xml") def addAcceptHeader[A](accept: String): RequestUpdate[A] = - _.header("Accept", accept) + _.header(HeaderNames.Accept, accept) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala index b4800eb2f92..c10e54bfc0e 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/api/service/OntologiesRestService.scala @@ -316,7 +316,7 @@ final case class OntologiesRestService( .fromOption(propertyIri.ontologySchema.collect { case s: ApiV2Schema => s }) .orElseFail(BadRequestException(s"Property IRI must have an API V2 schema: $propertyIri")) result <- ontologyResponder.getPropertyFromOntologyV2(propertyIri, allLanguages, user) - response <- renderer.render(result, formatOptions) + response <- renderer.render(result, formatOptions.copy(schema = schema)) } yield response def canDeleteProperty( From bb6f277e176f9a9483f4620f827d939d3266d0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 15:30:03 +0200 Subject: [PATCH 60/99] fmt --- .../scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala index af1555ae08e..d439e884646 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/OntologyFormatsE2ESpec.scala @@ -15,6 +15,7 @@ import zio.test.* import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths + import org.knora.webapi.* import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Complex import org.knora.webapi.messages.OntologyConstants.KnoraApiV2Simple From 77944bc9b1e7f3e9aa20b0f72b1d080126bf47db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 30 Sep 2025 17:13:21 +0200 Subject: [PATCH 61/99] Migrate InputOntologyV2Spec to E2ESpec --- .../knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala index 776ae457525..4536801bdd9 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala @@ -13,9 +13,9 @@ import java.time.Instant import dsp.errors.BadRequestException import org.knora.webapi.ApiV2Complex +import org.knora.webapi.E2EZSpec import org.knora.webapi.e2e.v2.ontology import org.knora.webapi.messages.IriConversions.* -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.SmartIriLiteralV2 import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.util.rdf.JsonLDUtil @@ -29,8 +29,7 @@ import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne /** * Tests [[InputOntologyV2]]. */ -object InputOntologyV2Spec extends ZIOSpecDefault { - private implicit val stringFormatter: StringFormatter = StringFormatter.getInitializedTestInstance +object InputOntologyV2Spec extends E2EZSpec { private val PropertyDef = InputOntologyV2( ontologyMetadata = OntologyMetadataV2( @@ -112,7 +111,7 @@ object InputOntologyV2Spec extends ZIOSpecDefault { properties = Map(), ) - val spec = suite("InputOntologyV2")( + override val e2eSpec = suite("InputOntologyV2")( test("parse a property definition") { val params = From f774b16b05d38151a6c6945227bdcdd70e3e00ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 13:39:40 +0200 Subject: [PATCH 62/99] Migrate ResponseCheckerV2Spec to E2EZSpec --- .../webapi/e2e/v2/ResponseCheckerV2Spec.scala | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ResponseCheckerV2Spec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ResponseCheckerV2Spec.scala index efbdb341fdb..ec95071ea76 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ResponseCheckerV2Spec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ResponseCheckerV2Spec.scala @@ -5,44 +5,47 @@ package org.knora.webapi.e2e.v2 -import org.scalatest.matchers.should.Matchers -import org.scalatest.wordspec.AnyWordSpec +import zio.* +import zio.test.* +import zio.test.Assertion.* import java.nio.file.Paths +import org.knora.webapi.E2EZSpec import org.knora.webapi.util.FileUtil -/** - * Tests [[ResponseCheckerV2]]. - */ -class ResponseCheckerV2Spec extends AnyWordSpec with Matchers { +object ResponseCheckerV2Spec extends E2EZSpec { - "ResponseCheckerV2" should { - "not throw an exception if received and expected resource responses are the same" in { + override val e2eSpec = suite("ResponseCheckerV2")( + test("not throw an exception if received and expected resource responses are the same") { val expectedAnswerJSONLD = - FileUtil.readTextFile( - Paths.get("test_data/generated_test_data/resourcesR2RV2/ThingWithLinkComplex.jsonld"), - ) + FileUtil.readTextFile(Paths.get("test_data/generated_test_data/resourcesR2RV2/ThingWithLinkComplex.jsonld")) - ResponseCheckerV2.compareJSONLDForResourcesResponse( - expectedJSONLD = expectedAnswerJSONLD, - receivedJSONLD = expectedAnswerJSONLD, - ) - } - - "not throw an exception if received and expected mapping responses are the same" in { + ZIO + .attempt( + ResponseCheckerV2.compareJSONLDForResourcesResponse( + expectedJSONLD = expectedAnswerJSONLD, + receivedJSONLD = expectedAnswerJSONLD, + ), + ) + .as(assertCompletes) + }, + test("not throw an exception if received and expected mapping responses are the same") { val expectedAnswerJSONLD = FileUtil.readTextFile( Paths.get("test_data/generated_test_data/standoffR2RV2/mappingCreationResponse.jsonld"), ) - ResponseCheckerV2.compareJSONLDForMappingCreationResponse( - expectedJSONLD = expectedAnswerJSONLD, - receivedJSONLD = expectedAnswerJSONLD, - ) - } - - "throw an exception if received and expected resource responses are different" in { + ZIO + .attempt( + ResponseCheckerV2.compareJSONLDForMappingCreationResponse( + expectedJSONLD = expectedAnswerJSONLD, + receivedJSONLD = expectedAnswerJSONLD, + ), + ) + .as(assertCompletes) + }, + test("throw an exception if received and expected resource responses are different") { val expectedAnswerJSONLD = FileUtil.readTextFile( Paths.get("test_data/generated_test_data/resourcesR2RV2/ThingWithLinkComplex.jsonld"), @@ -50,15 +53,14 @@ class ResponseCheckerV2Spec extends AnyWordSpec with Matchers { val receivedAnswerJSONLD = FileUtil.readTextFile(Paths.get("test_data/generated_test_data/resourcesR2RV2/ThingWithListValue.jsonld")) - assertThrows[AssertionError] { + ZIO.attempt { ResponseCheckerV2.compareJSONLDForResourcesResponse( expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = receivedAnswerJSONLD, ) - } - } - - "throw an exception if the values of the received and expected resource responses are different" in { + }.exit.flatMap(exit => assert(exit)(failsWithA[AssertionError])) + }, + test("throw an exception if the values of the received and expected resource responses are different") { val expectedAnswerJSONLD = FileUtil.readTextFile( Paths.get("test_data/generated_test_data/resourcesR2RV2/BookReiseInsHeiligeLand.jsonld"), @@ -68,15 +70,14 @@ class ResponseCheckerV2Spec extends AnyWordSpec with Matchers { Paths.get("test_data/generated_test_data/resourcesR2RV2/BookReiseInsHeiligeLandPreview.jsonld"), ) - assertThrows[AssertionError] { + ZIO.attempt { ResponseCheckerV2.compareJSONLDForResourcesResponse( expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = receivedAnswerJSONLD, ) - } - } - - "throw an exception if the number of values of the received and expected resource responses are different" in { + }.exit.flatMap(exit => assert(exit)(failsWithA[AssertionError])) + }, + test("throw an exception if the number of values of the received and expected resource responses are different") { val expectedAnswerJSONLD = FileUtil.readTextFile( Paths.get("test_data/generated_test_data/resourcesR2RV2/NarrenschiffFirstPage.jsonld"), @@ -87,15 +88,14 @@ class ResponseCheckerV2Spec extends AnyWordSpec with Matchers { Paths.get("test_data/generated_test_data/responseCheckerR2RV2/NarrenschiffFirstPageWrong.jsonld"), ) - assertThrows[AssertionError] { + ZIO.attempt { ResponseCheckerV2.compareJSONLDForResourcesResponse( expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = receivedAnswerJSONLD, ) - } - } - - "throw an exception if received and expected mapping responses are different" in { + }.exit.flatMap(exit => assert(exit)(failsWithA[AssertionError])) + }, + test("throw an exception if received and expected mapping responses are different") { val expectedAnswerJSONLD = FileUtil.readTextFile( Paths.get("test_data/generated_test_data/standoffR2RV2/mappingCreationResponse.jsonld"), @@ -107,12 +107,12 @@ class ResponseCheckerV2Spec extends AnyWordSpec with Matchers { ), ) - assertThrows[AssertionError] { + ZIO.attempt { ResponseCheckerV2.compareJSONLDForMappingCreationResponse( expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = receivedAnswerJSONLD, ) - } - } - } + }.exit.flatMap(exit => assert(exit)(failsWithA[AssertionError])) + }, + ) } From 330a155e9c763b74a52c347d956188f0ee70b426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 14:10:18 +0200 Subject: [PATCH 63/99] fmt --- .../knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala index 4536801bdd9..96f05f3e56a 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/e2e/v2/ontology/InputOntologyV2Spec.scala @@ -26,9 +26,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.PredicateInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.PropertyInfoContentV2 import org.knora.webapi.slice.ontology.domain.model.Cardinality.ZeroOrOne -/** - * Tests [[InputOntologyV2]]. - */ object InputOntologyV2Spec extends E2EZSpec { private val PropertyDef = InputOntologyV2( @@ -113,7 +110,6 @@ object InputOntologyV2Spec extends E2EZSpec { override val e2eSpec = suite("InputOntologyV2")( test("parse a property definition") { - val params = """ |{ @@ -212,7 +208,6 @@ object InputOntologyV2Spec extends E2EZSpec { assertTrue(actual == ClassDef) }, test("reject an entity definition with an invalid IRI") { - val params = s""" |{ From d13dceffdfa20ecbd14febc62bff13af6803aa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 14:10:33 +0200 Subject: [PATCH 64/99] try without closing the eh cache --- .../slice/infrastructure/CacheManager.scala | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala index ab3564de930..f4c31be2669 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala @@ -9,11 +9,7 @@ import org.ehcache.config.CacheConfiguration import org.ehcache.config.builders.CacheConfigurationBuilder import org.ehcache.config.builders.CacheManagerBuilder import org.ehcache.config.builders.ResourcePoolsBuilder -import zio.Ref -import zio.UIO -import zio.ULayer -import zio.ZIO -import zio.ZLayer +import zio.* import scala.reflect.ClassTag @@ -51,11 +47,8 @@ object CacheManager { private def getClassOf[A: ClassTag]: Class[A] = implicitly[ClassTag[A]].runtimeClass.asInstanceOf[Class[A]] - val layer: ULayer[CacheManager] = ZLayer.scoped { - val acquire = ZIO.succeed(CacheManagerBuilder.newCacheManagerBuilder().build(true)) - val release = (cm: org.ehcache.CacheManager) => ZIO.succeed(cm.close()) - ZIO - .acquireRelease(acquire)(release) - .flatMap(mgr => Ref.make(Set.empty[EhCache[_, _]]).map(CacheManager(mgr, _))) + val layer: ULayer[CacheManager] = ZLayer.fromZIO { + val mgr = CacheManagerBuilder.newCacheManagerBuilder().build(true) + Ref.make(Set.empty[EhCache[_, _]]).map(CacheManager(mgr, _)) } } From 2ef59f041361b181243c4f3405fff6935890342a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 16:29:04 +0200 Subject: [PATCH 65/99] add spec for CacheManager --- .../infrastructure/CacheManagerSpec.scala | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/infrastructure/CacheManagerSpec.scala diff --git a/webapi/src/test/scala/org/knora/webapi/slice/infrastructure/CacheManagerSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/infrastructure/CacheManagerSpec.scala new file mode 100644 index 00000000000..916304ffb80 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/infrastructure/CacheManagerSpec.scala @@ -0,0 +1,38 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.infrastructure + +import zio.* +import zio.test.* +import zio.test.Assertion.* + +object CacheManagerSpec extends ZIOSpecDefault { + + private val cacheManager = ZIO.serviceWithZIO[CacheManager] + + val spec = suite("CacheManagerSpec")( + test("create and use a cache") { + for { + cache <- cacheManager(_.createCache[String, String]("testCache")) + _ <- ZIO.succeed(cache.put("one", "1")) + value <- ZIO.succeed(cache.get("one")) + } yield assertTrue(value.contains("1")) + }, + test("fail to create a cache twice") { + for { + _ <- cacheManager(_.createCache[String, String]("testCache")) + exit <- cacheManager(_.createCache[String, String]("testCache")).exit + } yield assert(exit)(diesWithA[IllegalArgumentException]) + }, + test("when clearing all caches, the entries are removed") { + for { + cache <- cacheManager(_.createCache[String, String]("testCache")) + _ = cache.put("one", "1") + _ <- cacheManager(_.clearAll()) + } yield assertTrue(cache.get("one").isEmpty) + }, + ).provide(CacheManager.layer) +} From 665a88fe05ca93fd43c9f708eba460747cd7d9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 16:29:33 +0200 Subject: [PATCH 66/99] readd closing the CacheManager and logging --- .../webapi/slice/infrastructure/CacheManager.scala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala index f4c31be2669..6f1f51206d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/CacheManager.scala @@ -47,8 +47,15 @@ object CacheManager { private def getClassOf[A: ClassTag]: Class[A] = implicitly[ClassTag[A]].runtimeClass.asInstanceOf[Class[A]] - val layer: ULayer[CacheManager] = ZLayer.fromZIO { - val mgr = CacheManagerBuilder.newCacheManagerBuilder().build(true) - Ref.make(Set.empty[EhCache[_, _]]).map(CacheManager(mgr, _)) + val layer: ULayer[CacheManager] = ZLayer.scoped { + val acquire = ZIO + .succeed(CacheManagerBuilder.newCacheManagerBuilder().build(true)) + .tap(mgr => ZIO.logInfo("CacheManager created and initialized")) + + val release = (mgr: org.ehcache.CacheManager) => ZIO.succeed(mgr.close()) *> ZIO.logInfo("CacheManager closed") + + ZIO + .acquireRelease(acquire)(release) + .flatMap(mgr => Ref.make(Set.empty[EhCache[_, _]]).map(CacheManager(mgr, _))) } } From 7f8fb34ca78e12e94b155a45100f41e0206c7c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Wed, 1 Oct 2025 16:58:26 +0200 Subject: [PATCH 67/99] Remove unused config --- modules/test-e2e/src/test/resources/test.conf | 29 ------------------- modules/test-it/src/test/resources/test.conf | 29 ------------------- 2 files changed, 58 deletions(-) delete mode 100644 modules/test-e2e/src/test/resources/test.conf delete mode 100644 modules/test-it/src/test/resources/test.conf diff --git a/modules/test-e2e/src/test/resources/test.conf b/modules/test-e2e/src/test/resources/test.conf deleted file mode 100644 index b8aecaf3877..00000000000 --- a/modules/test-e2e/src/test/resources/test.conf +++ /dev/null @@ -1,29 +0,0 @@ -akka { - log-config-on-start = false - loggers = ["akka.event.slf4j.Slf4jLogger"] - logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" - loglevel = "ERROR" - stdout-loglevel = "ERROR" - log-dead-letters = off - log-dead-letters-during-shutdown = off - - actor { - default-dispatcher { - executor = "fork-join-executor" - fork-join-executor { - parallelism-min = 8 - parallelism-factor = 2.0 - parallelism-max = 8 - } - } - } - - http.host-connection-pool.response-entity-subscription-timeout = 10 seconds - - # The time period within which the TCP binding process must be completed. - http.server.bind-timeout = 15 seconds -} - -app { - allow-reload-over-http = true -} diff --git a/modules/test-it/src/test/resources/test.conf b/modules/test-it/src/test/resources/test.conf deleted file mode 100644 index b8aecaf3877..00000000000 --- a/modules/test-it/src/test/resources/test.conf +++ /dev/null @@ -1,29 +0,0 @@ -akka { - log-config-on-start = false - loggers = ["akka.event.slf4j.Slf4jLogger"] - logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" - loglevel = "ERROR" - stdout-loglevel = "ERROR" - log-dead-letters = off - log-dead-letters-during-shutdown = off - - actor { - default-dispatcher { - executor = "fork-join-executor" - fork-join-executor { - parallelism-min = 8 - parallelism-factor = 2.0 - parallelism-max = 8 - } - } - } - - http.host-connection-pool.response-entity-subscription-timeout = 10 seconds - - # The time period within which the TCP binding process must be completed. - http.server.bind-timeout = 15 seconds -} - -app { - allow-reload-over-http = true -} From 0200246ad7cc7986d89dfc35340204eeca89cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 09:08:05 +0200 Subject: [PATCH 68/99] update buildjet to buildjet-8vcpu-ubuntu-2204 --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d8914f9d4bb..93f2c5fb491 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -73,7 +73,7 @@ jobs: test-e2e: name: Build and E2E test - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: buildjet-8vcpu-ubuntu-2204 concurrency: group: ${{ github.ref }}-e2e cancel-in-progress: true From 35c1557f20f0cc3622f104664972e0a112e4c851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 14:47:44 +0200 Subject: [PATCH 69/99] try fix test on CI by using different api client and improve implementation --- .../api/AdminUsersEndpointsE2ESpec.scala | 439 +++++++++++------- 1 file changed, 269 insertions(+), 170 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala index b58626f25cc..7a805b2a3bf 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala @@ -8,9 +8,13 @@ package org.knora.webapi.slice.admin.api import sttp.client4.UriContext import sttp.model.StatusCode import zio.ZIO +import zio.json.ast.Json import zio.test.* import org.knora.webapi.* +import org.knora.webapi.messages.admin.responder.usersmessages.UserGroupMembershipsGetResponseADM +import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectAdminMembershipsGetResponseADM +import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectMembershipsGetResponseADM import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.* @@ -18,13 +22,13 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM2 import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.* import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse +import org.knora.webapi.slice.admin.api.service.UserRestService.UsersResponse import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.slice.common.domain.LanguageCode import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.LoginPayload import org.knora.webapi.slice.security.api.AuthenticationEndpointsV2.TokenResponse import org.knora.webapi.testservices.ResponseOps import org.knora.webapi.testservices.ResponseOps.assert200 -import org.knora.webapi.testservices.TestAdminApiClient import org.knora.webapi.testservices.TestApiClient import org.knora.webapi.util.MutableTestIri @@ -44,40 +48,40 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { val e2eSpec = suite("The Users Route ('admin/users')")( suite("used to query user information [FUNCTIONALITY]")( test("return all users") { - TestAdminApiClient - .getAllUsers(rootUser) + TestApiClient + .getJson[UsersResponse](uri"/admin/users", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return a single user profile identified by iri") { - TestAdminApiClient - .getUser(rootUser.userIri, rootUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/${rootUser.id}", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return a single user profile identified by email") { - TestAdminApiClient - .getUserByEmail(rootUser.getEmail, rootUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/email/${rootUser.email}", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return a single user profile identified by username") { - TestAdminApiClient - .getUserByUsername(rootUser.getUsername, rootUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/username/${rootUser.username}", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, ), suite("used to query user information [PERMISSIONS]")( test("return single user for SystemAdmin") { - TestAdminApiClient - .getUser(normalUser.userIri, rootUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/${normalUser.userIri}", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return single user for itself") { - TestAdminApiClient - .getUser(normalUser.userIri, normalUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/${normalUser.userIri}", normalUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return only public information for single user for non SystemAdmin and self") { - TestAdminApiClient - .getUser(normalUser.userIri, projectAdminUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/${normalUser.userIri}", projectAdminUser) .flatMap(_.assert200) .map(_.user) .flatMap(user => @@ -91,8 +95,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ) }, test("return only public information for single user with anonymous access") { - TestAdminApiClient - .getUser(normalUser.userIri) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/${normalUser.userIri}") .flatMap(_.assert200) .map(_.user) .map(user => @@ -106,18 +110,18 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ) }, test("return all users for SystemAdmin") { - TestAdminApiClient - .getAllUsers(rootUser) + TestApiClient + .getJson[UsersResponse](uri"/admin/users/", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return all users for ProjectAdmin") { - TestAdminApiClient - .getAllUsers(projectAdminUser) + TestApiClient + .getJson[UsersResponse](uri"/admin/users/", projectAdminUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("return 'Forbidden' for all users for normal user") { - TestAdminApiClient - .getAllUsers(normalUser) + TestApiClient + .getJson[UsersResponse](uri"/admin/users/", normalUser) .map(response => assertTrue(response.code == StatusCode.Forbidden)) }, ), @@ -150,8 +154,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .flatMap(_.assert200) .map(result => assertTrue(result.user.id == customUserIri.value)) }, @@ -167,8 +171,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, ), @@ -185,8 +189,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .flatMap(_.assert200) .flatMap(result => assertTrue( @@ -201,8 +205,12 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { givenName = Some(GivenName.unsafeFrom("Updated\tGivenName")), familyName = Some(FamilyName.unsafeFrom("Updated\"FamilyName")), ) - TestAdminApiClient - .updateUserBasicInfo(otherCustomUserIri, updateUserRequest, rootUser) + TestApiClient + .putJson[UserResponse, BasicUserInformationChangeRequest]( + uri"/admin/users/iri/$otherCustomUserIri/BasicUserInformation", + updateUserRequest, + rootUser, + ) .flatMap(_.assert200) .map(result => assertTrue( @@ -214,8 +222,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { test( "return the special characters correctly when getting a user with special characters in givenName and familyName", ) { - TestAdminApiClient - .getUser(otherCustomUserIri, rootUser) + TestApiClient + .getJson[UserResponse](uri"/admin/users/iri/$otherCustomUserIri", rootUser) .flatMap(_.assert200) .map(result => assertTrue( @@ -237,8 +245,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, projectAdminUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, projectAdminUser) .map(response => assertTrue(response.code == StatusCode.Forbidden)) }, test("create the user if the supplied email and username are unique") { @@ -252,8 +260,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .flatMap(_.assert200) .tap(result => ZIO.succeed(donaldIri.set(result.user.id))) .map(result => @@ -278,8 +286,8 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("return a 'BadRequest' if the supplied email is not unique") { @@ -293,16 +301,13 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { lang = LanguageCode.EN, systemAdmin = SystemAdmin.IsNotSystemAdmin, ) - TestAdminApiClient - .createUser(createUserRequest, rootUser) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", createUserRequest, rootUser) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("authenticate the newly created user using HttpBasicAuth") { TestApiClient - .getJson[zio.json.ast.Json]( - uri"/v2/authentication", - _.auth.basic("donald.duck@example.org", "test"), - ) + .getJson[Json](uri"/v2/authentication", _.auth.basic("donald.duck@example.org", "test")) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("authenticate the newly created user during login") { @@ -323,8 +328,12 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { familyName = Some(FamilyName.unsafeFrom("Duckmann")), lang = Some(LanguageCode.DE), ) - TestAdminApiClient - .updateUserBasicInfo(donaldIri.asUserIri, updateUserRequest, rootUser) + TestApiClient + .putJson[UserResponse, BasicUserInformationChangeRequest]( + uri"/admin/users/iri/$donaldIri/BasicUserInformation", + updateUserRequest, + rootUser, + ) .flatMap(_.assert200) .map(result => assertTrue( @@ -341,8 +350,12 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { requesterPassword = Password.unsafeFrom("test"), newPassword = Password.unsafeFrom("will-be-ignored"), ) - TestAdminApiClient - .updateUserPassword(customUserIri, changeUserPasswordRequest, normalUser) + TestApiClient + .putJson[UserResponse, PasswordChangeRequest]( + uri"/admin/users/iri/$customUserIri/Password", + changeUserPasswordRequest, + normalUser, + ) .map(response => assertTrue(response.code == StatusCode.Forbidden)) }, test("update the user's password (by himself)") { @@ -351,11 +364,15 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { newPassword = Password.unsafeFrom("test123456"), ) for { - _ <- TestAdminApiClient - .updateUserPassword(normalUser.userIri, changeUserPasswordRequest, normalUser) + _ <- TestApiClient + .putJson[UserResponse, PasswordChangeRequest]( + uri"/admin/users/iri/${normalUser.id}/Password", + changeUserPasswordRequest, + normalUser, + ) .flatMap(_.assert200) // check if the password was changed, i.e. if the new one is accepted - response <- TestApiClient.getJson[zio.json.ast.Json]( + response <- TestApiClient.getJson[Json]( uri"/v2/authentication", _.auth.basic(normalUser.email, "test123456"), ) @@ -367,11 +384,15 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { newPassword = Password.unsafeFrom("test654321"), ) for { - _ <- TestAdminApiClient - .updateUserPassword(normalUser.userIri, changeUserPasswordRequest, rootUser) + _ <- TestApiClient + .putJson[UserResponse, PasswordChangeRequest]( + uri"/admin/users/iri/${normalUser.id}/Password", + changeUserPasswordRequest, + rootUser, + ) .flatMap(_.assert200) // check if the password was changed, i.e. if the new one is accepted - response <- TestApiClient.getJson[zio.json.ast.Json]( + response <- TestApiClient.getJson[Json]( uri"/v2/authentication", _.auth.basic(normalUser.email, "test654321"), ) @@ -383,13 +404,13 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { | "requesterPassword": "test" |}""".stripMargin for { - response1 <- TestApiClient.putJson[zio.json.ast.Json, String]( - uri"/admin/users/iri/${normalUser.userIri}/Password", + response1 <- TestApiClient.putJson[Json, String]( + uri"/admin/users/iri/${normalUser.id}/Password", changeUserPasswordRequest, normalUser, ) // check that the password was not changed, i.e. the old one is still accepted - response2 <- TestApiClient.getJson[zio.json.ast.Json]( + response2 <- TestApiClient.getJson[Json]( uri"/v2/authentication", _.auth.basic(normalUser.email, "test654321"), ) @@ -399,19 +420,28 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ) }, test("change user's status") { - TestAdminApiClient - .updateUserStatus(donaldIri.asUserIri, StatusChangeRequest(UserStatus.Inactive), rootUser) + TestApiClient + .putJson[UserResponse, StatusChangeRequest]( + uri"/admin/users/iri/$donaldIri/Status", + StatusChangeRequest(UserStatus.Inactive), + rootUser, + ) .flatMap(_.assert200) .map(result => assertTrue(!result.user.status)) }, test("update the user's system admin membership status") { val changeReq = SystemAdminChangeRequest(SystemAdmin.IsSystemAdmin) for { - response <- TestAdminApiClient.updateUserSystemAdmin(donaldIri.asUserIri, changeReq, rootUser) - result <- response.assert200 + result <- TestApiClient + .putJson[UserResponse, SystemAdminChangeRequest]( + uri"/admin/users/iri/$donaldIri/SystemAdmin", + changeReq, + rootUser, + ) + .flatMap(_.assert200) // Throw BadRequest exception if user is built-in user - badResponse <- TestAdminApiClient.updateUserSystemAdmin( - KnoraSystemInstances.Users.SystemUser.userIri, + badResponse <- TestApiClient.putJson[UserResponse, SystemAdminChangeRequest]( + uri"/admin/users/iri/${KnoraSystemInstances.Users.SystemUser.userIri}/SystemAdmin", changeReq, rootUser, ) @@ -423,52 +453,55 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ) }, test("not allow updating the system user's system admin membership status") { - TestAdminApiClient - .updateUserSystemAdmin( - KnoraSystemInstances.Users.SystemUser.userIri, + TestApiClient + .putJson[UserResponse, SystemAdminChangeRequest]( + uri"/admin/users/iri/${KnoraSystemInstances.Users.SystemUser.id}/SystemAdmin", SystemAdminChangeRequest(SystemAdmin.IsSystemAdmin), rootUser, ) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("not allow changing the system user's status") { - TestAdminApiClient - .updateUserStatus( - KnoraSystemInstances.Users.SystemUser.userIri, + TestApiClient + .putJson[UserResponse, StatusChangeRequest]( + uri"/admin/users/iri/${KnoraSystemInstances.Users.SystemUser.id}/Status", StatusChangeRequest(UserStatus.Inactive), rootUser, ) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("not allow changing the anonymous user's status") { - TestAdminApiClient - .updateUserStatus( - UserIri.unsafeFrom(KnoraSystemInstances.Users.AnonymousUser.id), + TestApiClient + .putJson[UserResponse, StatusChangeRequest]( + uri"/admin/users/iri/${KnoraSystemInstances.Users.AnonymousUser.id}/Status", StatusChangeRequest(UserStatus.Inactive), rootUser, ) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("delete a user") { - TestAdminApiClient - .deleteUser(customUserIri, rootUser) + TestApiClient + .deleteJson[UserResponse](uri"/admin/users/iri/$customUserIri", rootUser) .map(response => assertTrue(response.code == StatusCode.Ok)) }, test("not allow deleting the system user") { - TestAdminApiClient - .deleteUser(KnoraSystemInstances.Users.SystemUser.userIri, rootUser) + TestApiClient + .deleteJson[UserResponse](uri"/admin/users/iri/${KnoraSystemInstances.Users.SystemUser.id}", rootUser) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, test("not allow deleting the anonymous user") { - TestAdminApiClient - .deleteUser(UserIri.unsafeFrom(KnoraSystemInstances.Users.AnonymousUser.id), rootUser) + TestApiClient + .deleteJson[UserResponse](uri"/admin/users/iri/${KnoraSystemInstances.Users.AnonymousUser.id}", rootUser) .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, ), suite("used to query project memberships")( test("return all projects the user is a member of") { - TestAdminApiClient - .getUserProjectMemberships(multiUserIri, rootUser) + TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/$multiUserIri/project-memberships", + rootUser, + ) .flatMap(_.assert200) .map(result => assertTrue( @@ -481,17 +514,35 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ), suite("used to modify project membership")( test("NOT add a user to project if the requesting user is not a SystemAdmin or ProjectAdmin") { - TestAdminApiClient - .addUserToProject(normalUser.userIri, imagesProjectIri, normalUser) + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + normalUser, + ) .map(response => assertTrue(response.code == StatusCode.Forbidden)) }, test("add user to project") { for { - beforeResponse <- TestAdminApiClient.getUserProjectMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - beforeResult <- beforeResponse.assert200 - _ <- TestAdminApiClient.addUserToProject(normalUser.userIri, imagesProjectIri, rootUser).flatMap(_.assert200) - afterResponse <- TestAdminApiClient.getUserProjectMemberships(normalUser.userIri, rootUser) - afterResult <- afterResponse.assert200 + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + _ <- TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) } yield assertTrue( beforeResult.projects == Seq.empty, afterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), @@ -499,11 +550,23 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { }, test("don't add user to project if user is already a member") { for { - beforeResponse <- TestAdminApiClient.getUserProjectMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - beforeResult <- beforeResponse.assert200 - response <- TestAdminApiClient.addUserToProject(normalUser.userIri, imagesProjectIri, rootUser) - afterResponse <- TestAdminApiClient.getUserProjectMemberships(normalUser.userIri, rootUser) - afterResult <- afterResponse.assert200 + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) } yield assertTrue( response.code == StatusCode.BadRequest, afterResult.projects == beforeResult.projects, @@ -511,11 +574,22 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { }, test("remove user from project") { for { - beforeResponse <- TestAdminApiClient.getUserProjectMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - beforeResult <- beforeResponse.assert200 - response <- TestAdminApiClient.removeUserFromProject(normalUser.userIri, imagesProjectIri, rootUser) - afterResponse <- TestAdminApiClient.getUserProjectMemberships(normalUser.userIri, rootUser) - afterResult <- afterResponse.assert200 + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + rootUser, + ) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) } yield assertTrue( beforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), response.code == StatusCode.Ok, @@ -525,14 +599,17 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { ), suite("used to query project admin group memberships")( test("return all projects the user is a member of the project admin group") { - TestAdminApiClient - .getUserProjectAdminMemberships(multiUserIri, rootUser) + TestApiClient + .getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/$multiUserIri/project-admin-memberships", + rootUser, + ) .flatMap(_.assert200) .map(result => assertTrue( - result.projects.contains(imagesProjectExternal) && - result.projects.contains(incunabulaProjectExternal) && - result.projects.contains(anythingProjectExternal), + result.projects.contains(imagesProjectExternal), + result.projects.contains(incunabulaProjectExternal), + result.projects.contains(anythingProjectExternal), ), ) }, @@ -541,30 +618,35 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { test("add user to project admin group only if he is already member of that project") { for { // add user as project admin to images project - returns a BadRequest because user is not member of the project - responseWithoutBeingMember <- TestAdminApiClient.addUserToProjectAdmin( - normalUser.userIri, - imagesProjectIri, - rootUser, - ) + responseWithoutBeingMember <- + TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", + rootUser, + ) // add user as member to images project - responseAddUserToProject <- TestAdminApiClient.addUserToProject( - normalUser.userIri, - imagesProjectIri, + responseAddUserToProject <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", rootUser, ) // verify that user is not yet project admin in images project - membershipsBeforeResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) + membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) membershipsBeforeResult <- membershipsBeforeResponse.assert200 // add user as project admin to images project - response <- TestAdminApiClient.addUserToProjectAdmin( - normalUser.userIri, - imagesProjectIri, + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", rootUser, ) // verify that user has been added as project admin to images project - membershipsAfterResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) + membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) membershipsAfterResult <- membershipsAfterResponse.assert200 } yield assertTrue( responseWithoutBeingMember.code == StatusCode.BadRequest, @@ -576,57 +658,65 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { }, test("remove user from project admin group") { for { - membershipsBeforeResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) + membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestAdminApiClient.removeUserFromProjectAdmin( - normalUser.userIri, - imagesProjectIri, + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", rootUser, ) - membershipsAfterResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) + membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) membershipsAfterResult <- membershipsAfterResponse.assert200 } yield assertTrue( membershipsBeforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), response.code == StatusCode.Ok, - membershipsAfterResult.projects == Seq.empty[Project], + membershipsAfterResult.projects == Seq.empty, ) }, test("remove user from project which also removes him from project admin group") { for { // add user as project admin to images project - responseAddUserAsProjectAdmin <- TestAdminApiClient.addUserToProjectAdmin( - normalUser.userIri, - imagesProjectIri, - rootUser, - ) - // verify that user has been added as project admin to images project - membershipsBeforeResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 + _ <- TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) *> + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) + // remove user as project member from images project - response <- TestAdminApiClient.removeUserFromProject( - normalUser.userIri, - imagesProjectIri, - rootUser, - ) + _ <- TestApiClient + .deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + rootUser, + ) + .flatMap(_.assert200) // verify that user has also been removed as project admin from images project - projectAdminMembershipsAfterResponse <- - TestAdminApiClient.getUserProjectAdminMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - projectAdminMembershipsAfterResult <- projectAdminMembershipsAfterResponse.assert200 - } yield assertTrue( - responseAddUserAsProjectAdmin.code == StatusCode.Ok, - membershipsBeforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - response.code == StatusCode.Ok, - projectAdminMembershipsAfterResult.projects == Seq(), - ) + projectAdminMembershipsAfterResult <- TestApiClient + .getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) }, ), suite("used to query group memberships")( test("return all groups the user is a member of") { - TestAdminApiClient - .getUserGroupMemberships(multiUserIri, rootUser) + TestApiClient + .getJson[UserGroupMembershipsGetResponseADM](uri"/admin/users/iri/$multiUserIri/group-memberships", rootUser) .flatMap(_.assert200) .map(result => assertTrue( @@ -638,34 +728,43 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { suite("used to modify group membership")( test("add user to group") { for { - membershipsBeforeResponse <- - TestAdminApiClient.getUserGroupMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestAdminApiClient.addUserToGroup( - normalUser.userIri, - imagesReviewerGroup.groupIri, + _ <- TestApiClient + .getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + .flatMap(_.assert200) + .filterOrFail(_.groups.isEmpty)(IllegalStateException("User is already member of a group")) + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", + "", rootUser, ) - membershipsAfterResponse <- TestAdminApiClient.getUserGroupMemberships(normalUser.userIri, rootUser) - membershipsAfterResult <- membershipsAfterResponse.assert200 - } yield assertTrue( - membershipsBeforeResult.groups == Seq.empty[Group], - response.code == StatusCode.Ok, - membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal), - ) + membershipsAfterResult <- TestApiClient + .getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue(membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal)) }, test("remove user from group") { for { membershipsBeforeResponse <- - TestAdminApiClient.getUserGroupMemberships(UserIri.unsafeFrom(normalUser.id), rootUser) + TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestAdminApiClient.removeUserFromGroup( - normalUser.userIri, - imagesReviewerGroup.groupIri, + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", rootUser, ) - membershipsAfterResponse <- TestAdminApiClient.getUserGroupMemberships(normalUser.userIri, rootUser) - membershipsAfterResult <- membershipsAfterResponse.assert200 + membershipsAfterResponse <- TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + membershipsAfterResult <- membershipsAfterResponse.assert200 } yield assertTrue( membershipsBeforeResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal), response.code == StatusCode.Ok, From 6a8cbb000af31d1fd8eb4495257adba64f87ea8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 15:26:29 +0200 Subject: [PATCH 70/99] run e2e using sbt, do not report coverage --- .github/workflows/build-and-test.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 93f2c5fb491..6ef0f4f1b52 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: CI-build-and-test on: pull_request: - types: [ opened, reopened, synchronize ] + types: [opened, reopened, synchronize] branches-ignore: - "**/graphite-base/**" @@ -83,7 +83,7 @@ jobs: with: java-version: ${{ env.JAVA_VERSION }} - name: Run end-to-end tests - run: make test-e2e + run: ./sbtx "test-e2e/test" - name: WebApi E2E Test Report uses: dorny/test-reporter@v1 if: success() || failure() @@ -91,15 +91,6 @@ jobs: name: WebApi E2E Test Results path: ./modules/test-e2e/target/test-reports/TEST-*.xml reporter: java-junit - - name: Upload coverage data to codacy - uses: codacy/codacy-coverage-reporter-action@v1 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ./target/scala-3.3.6/coverage-report/cobertura.xml - - name: Upload coverage data to codecov - uses: codecov/codecov-action@v3 - with: - files: ./target/scala-3.3.6/coverage-report/cobertura.xml test-ingest-integration: runs-on: ubuntu-latest From d603f7f162f87df28d92bd2e26440c1d8eae9143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 15:31:10 +0200 Subject: [PATCH 71/99] use latest SipiTestContainer --- .../org/knora/webapi/testcontainers/SipiTestContainer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala index 9c162066aad..9c5efa44c33 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala @@ -21,7 +21,7 @@ import org.knora.webapi.http.version.BuildInfo import org.knora.webapi.testcontainers.TestContainerOps.toZio final class SipiTestContainer - extends GenericContainer[SipiTestContainer](s"daschswiss/knora-sipi:${SipiTestContainer.imageVersion}") { + extends GenericContainer[SipiTestContainer](s"daschswiss/knora-sipi:latest") { def sipiBaseUrl: URL = { val urlString = s"http://${SipiTestContainer.localHostAddress}:$getFirstMappedPort" From 3be3595cdb64ce62704bd425db10056f51cc55d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 15:36:49 +0200 Subject: [PATCH 72/99] fmt --- .../org/knora/webapi/testcontainers/SipiTestContainer.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala index 9c5efa44c33..710879f5dde 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala @@ -20,8 +20,7 @@ import java.net.InetAddress import org.knora.webapi.http.version.BuildInfo import org.knora.webapi.testcontainers.TestContainerOps.toZio -final class SipiTestContainer - extends GenericContainer[SipiTestContainer](s"daschswiss/knora-sipi:latest") { +final class SipiTestContainer extends GenericContainer[SipiTestContainer](s"daschswiss/knora-sipi:latest") { def sipiBaseUrl: URL = { val urlString = s"http://${SipiTestContainer.localHostAddress}:$getFirstMappedPort" From 9f19da1afc2a887092a4f523a965b247d11337b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 2 Oct 2025 16:36:43 +0200 Subject: [PATCH 73/99] split test --- .../api/AdminUsersEndpointsE2ESpec.scala | 283 ------------------ ...sersGroupMembershipsEndpointsE2ESpec.scala | 90 ++++++ ...rsProjectMemberShipsEndpointsE2ESpec.scala | 251 ++++++++++++++++ 3 files changed, 341 insertions(+), 283 deletions(-) create mode 100644 modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala create mode 100644 modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala index 7a805b2a3bf..ad8ad318a46 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersEndpointsE2ESpec.scala @@ -12,15 +12,10 @@ import zio.json.ast.Json import zio.test.* import org.knora.webapi.* -import org.knora.webapi.messages.admin.responder.usersmessages.UserGroupMembershipsGetResponseADM -import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectAdminMembershipsGetResponseADM -import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectMembershipsGetResponseADM import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.* -import org.knora.webapi.sharedtestdata.SharedTestDataADM2 import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.* -import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse import org.knora.webapi.slice.admin.api.service.UserRestService.UsersResponse import org.knora.webapi.slice.admin.domain.model.* @@ -38,7 +33,6 @@ import org.knora.webapi.util.MutableTestIri object AdminUsersEndpointsE2ESpec extends E2EZSpec { private val projectAdminUser = imagesUser01 - private val multiUserIri = UserIri.unsafeFrom(SharedTestDataADM2.multiuserUser.userData.user_id.get) private val customUserIri = UserIri.unsafeFrom("http://rdfh.ch/users/14pxW-LAQIaGcCRiNCPJcQ") private val otherCustomUserIri = UserIri.unsafeFrom("http://rdfh.ch/users/v8_12VcJRlGNFCjYzqJ5cA") @@ -495,282 +489,5 @@ object AdminUsersEndpointsE2ESpec extends E2EZSpec { .map(response => assertTrue(response.code == StatusCode.BadRequest)) }, ), - suite("used to query project memberships")( - test("return all projects the user is a member of") { - TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/$multiUserIri/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - .map(result => - assertTrue( - result.projects.contains(imagesProjectExternal), - result.projects.contains(incunabulaProjectExternal), - result.projects.contains(anythingProjectExternal), - ), - ) - }, - ), - suite("used to modify project membership")( - test("NOT add a user to project if the requesting user is not a SystemAdmin or ProjectAdmin") { - TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - normalUser, - ) - .map(response => assertTrue(response.code == StatusCode.Forbidden)) - }, - test("add user to project") { - for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - _ <- TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue( - beforeResult.projects == Seq.empty, - afterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - ) - }, - test("don't add user to project if user is already a member") { - for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue( - response.code == StatusCode.BadRequest, - afterResult.projects == beforeResult.projects, - ) - }, - test("remove user from project") { - for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue( - beforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - response.code == StatusCode.Ok, - afterResult.projects == Seq.empty[Project], - ) - }, - ), - suite("used to query project admin group memberships")( - test("return all projects the user is a member of the project admin group") { - TestApiClient - .getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/$multiUserIri/project-admin-memberships", - rootUser, - ) - .flatMap(_.assert200) - .map(result => - assertTrue( - result.projects.contains(imagesProjectExternal), - result.projects.contains(incunabulaProjectExternal), - result.projects.contains(anythingProjectExternal), - ), - ) - }, - ), - suite("used to modify project admin group membership")( - test("add user to project admin group only if he is already member of that project") { - for { - // add user as project admin to images project - returns a BadRequest because user is not member of the project - responseWithoutBeingMember <- - TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) - // add user as member to images project - responseAddUserToProject <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - // verify that user is not yet project admin in images project - membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - // add user as project admin to images project - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) - // verify that user has been added as project admin to images project - membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 - } yield assertTrue( - responseWithoutBeingMember.code == StatusCode.BadRequest, - responseAddUserToProject.code == StatusCode.Ok, - membershipsBeforeResult.projects == Seq(), - response.code == StatusCode.Ok, - membershipsAfterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - ) - }, - test("remove user from project admin group") { - for { - membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 - } yield assertTrue( - membershipsBeforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - response.code == StatusCode.Ok, - membershipsAfterResult.projects == Seq.empty, - ) - }, - test("remove user from project which also removes him from project admin group") { - for { - // add user as project admin to images project - _ <- TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) *> - TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) - - // remove user as project member from images project - _ <- TestApiClient - .deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - .flatMap(_.assert200) - // verify that user has also been removed as project admin from images project - projectAdminMembershipsAfterResult <- TestApiClient - .getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) - }, - ), - suite("used to query group memberships")( - test("return all groups the user is a member of") { - TestApiClient - .getJson[UserGroupMembershipsGetResponseADM](uri"/admin/users/iri/$multiUserIri/group-memberships", rootUser) - .flatMap(_.assert200) - .map(result => - assertTrue( - result.groups.contains(SharedTestDataADM.imagesReviewerGroupExternal), - ), - ) - }, - ), - suite("used to modify group membership")( - test("add user to group") { - for { - _ <- TestApiClient - .getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - .flatMap(_.assert200) - .filterOrFail(_.groups.isEmpty)(IllegalStateException("User is already member of a group")) - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", - "", - rootUser, - ) - membershipsAfterResult <- TestApiClient - .getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue(membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal)) - }, - test("remove user from group") { - for { - membershipsBeforeResponse <- - TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", - rootUser, - ) - membershipsAfterResponse <- TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 - } yield assertTrue( - membershipsBeforeResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal), - response.code == StatusCode.Ok, - membershipsAfterResult.groups == Seq.empty[Group], - ) - }, - ), ) } diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala new file mode 100644 index 00000000000..4655763717d --- /dev/null +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala @@ -0,0 +1,90 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.admin.api + +import sttp.client4.UriContext +import sttp.model.StatusCode +import zio.test.* + +import org.knora.webapi.* +import org.knora.webapi.messages.admin.responder.usersmessages.UserGroupMembershipsGetResponseADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM.* +import org.knora.webapi.sharedtestdata.SharedTestDataADM2 +import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse +import org.knora.webapi.slice.admin.domain.model.* +import org.knora.webapi.testservices.ResponseOps +import org.knora.webapi.testservices.ResponseOps.assert200 +import org.knora.webapi.testservices.TestApiClient + +object AdminUsersGroupMembershipsEndpointsE2ESpec extends E2EZSpec { + + private val multiUserIri = UserIri.unsafeFrom(SharedTestDataADM2.multiuserUser.userData.user_id.get) + + override val e2eSpec = suite( + "The Users Route ('admin/users/iri/:userIri/group-member-ships') ", + )( + suite("used to query group memberships")( + test("return all groups the user is a member of") { + TestApiClient + .getJson[UserGroupMembershipsGetResponseADM](uri"/admin/users/iri/$multiUserIri/group-memberships", rootUser) + .flatMap(_.assert200) + .map(result => + assertTrue( + result.groups.contains(SharedTestDataADM.imagesReviewerGroupExternal), + ), + ) + }, + ), + suite("used to modify group membership")( + test("add user to group") { + for { + _ <- TestApiClient + .getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + .flatMap(_.assert200) + .filterOrFail(_.groups.isEmpty)(IllegalStateException("User is already member of a group")) + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", + "", + rootUser, + ) + membershipsAfterResult <- TestApiClient + .getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue(membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal)) + }, + test("remove user from group") { + for { + membershipsBeforeResponse <- + TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + membershipsBeforeResult <- membershipsBeforeResponse.assert200 + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", + rootUser, + ) + membershipsAfterResponse <- TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/group-memberships", + rootUser, + ) + membershipsAfterResult <- membershipsAfterResponse.assert200 + } yield assertTrue( + membershipsBeforeResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal), + response.code == StatusCode.Ok, + membershipsAfterResult.groups == Seq.empty[Group], + ) + }, + ), + ) +} diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala new file mode 100644 index 00000000000..caf749266db --- /dev/null +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -0,0 +1,251 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.admin.api + +import sttp.client4.UriContext +import sttp.model.StatusCode +import zio.test.* + +import org.knora.webapi.* +import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectAdminMembershipsGetResponseADM +import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectMembershipsGetResponseADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM.* +import org.knora.webapi.sharedtestdata.SharedTestDataADM2 +import org.knora.webapi.slice.admin.api.model.Project +import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse +import org.knora.webapi.slice.admin.domain.model.* +import org.knora.webapi.testservices.ResponseOps +import org.knora.webapi.testservices.ResponseOps.assert200 +import org.knora.webapi.testservices.TestApiClient + +object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { + + private val multiUserIri = UserIri.unsafeFrom(SharedTestDataADM2.multiuserUser.userData.user_id.get) + + override val e2eSpec = suite( + "The Users Routes ('admin/users/iri/:userIri/project-memberships', 'admin/users/iri/:userIri/project-admin-memberships') ", + )( + suite("used to query project memberships")( + test("return all projects the user is a member of") { + TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/$multiUserIri/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + .map(result => + assertTrue( + result.projects.contains(imagesProjectExternal), + result.projects.contains(incunabulaProjectExternal), + result.projects.contains(anythingProjectExternal), + ), + ) + }, + ), + suite("used to modify project membership")( + test("NOT add a user to project if the requesting user is not a SystemAdmin or ProjectAdmin") { + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + normalUser, + ) + .map(response => assertTrue(response.code == StatusCode.Forbidden)) + }, + test("add user to project") { + for { + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + _ <- TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue( + beforeResult.projects == Seq.empty, + afterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + ) + }, + test("don't add user to project if user is already a member") { + for { + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue( + response.code == StatusCode.BadRequest, + afterResult.projects == beforeResult.projects, + ) + }, + test("remove user from project") { + for { + beforeResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + rootUser, + ) + afterResult <- TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue( + beforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + response.code == StatusCode.Ok, + afterResult.projects == Seq.empty[Project], + ) + }, + ), + suite("used to query project admin group memberships")( + test("return all projects the user is a member of the project admin group") { + TestApiClient + .getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/$multiUserIri/project-admin-memberships", + rootUser, + ) + .flatMap(_.assert200) + .map(result => + assertTrue( + result.projects.contains(imagesProjectExternal), + result.projects.contains(incunabulaProjectExternal), + result.projects.contains(anythingProjectExternal), + ), + ) + }, + ), + suite("used to modify project admin group membership")( + test("add user to project admin group only if he is already member of that project") { + for { + // add user as project admin to images project - returns a BadRequest because user is not member of the project + responseWithoutBeingMember <- + TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", + rootUser, + ) + // add user as member to images project + responseAddUserToProject <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + // verify that user is not yet project admin in images project + membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + membershipsBeforeResult <- membershipsBeforeResponse.assert200 + // add user as project admin to images project + response <- TestApiClient.postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", + rootUser, + ) + // verify that user has been added as project admin to images project + membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + membershipsAfterResult <- membershipsAfterResponse.assert200 + } yield assertTrue( + responseWithoutBeingMember.code == StatusCode.BadRequest, + responseAddUserToProject.code == StatusCode.Ok, + membershipsBeforeResult.projects == Seq(), + response.code == StatusCode.Ok, + membershipsAfterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + ) + }, + test("remove user from project admin group") { + for { + membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + membershipsBeforeResult <- membershipsBeforeResponse.assert200 + response <- TestApiClient.deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + rootUser, + ) + membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + membershipsAfterResult <- membershipsAfterResponse.assert200 + } yield assertTrue( + membershipsBeforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + response.code == StatusCode.Ok, + membershipsAfterResult.projects == Seq.empty, + ) + }, + test("remove user from project which also removes him from project admin group") { + for { + // add user as project admin to images project + _ <- TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) *> + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", + "", + rootUser, + ) + .flatMap(_.assert200) + + // remove user as project member from images project + _ <- TestApiClient + .deleteJson[UserResponse]( + uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", + rootUser, + ) + .flatMap(_.assert200) + // verify that user has also been removed as project admin from images project + projectAdminMembershipsAfterResult <- TestApiClient + .getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", + rootUser, + ) + .flatMap(_.assert200) + } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) + }, + ), + ) +} From 69d9f5f7890b955b046657fd74932609c9441a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 10:37:04 +0200 Subject: [PATCH 74/99] add logging timing --- ...rsProjectMemberShipsEndpointsE2ESpec.scala | 242 +++++++----------- .../admin/api/service/UserRestService.scala | 30 ++- 2 files changed, 109 insertions(+), 163 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index caf749266db..367939ac57a 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -7,6 +7,7 @@ package org.knora.webapi.slice.admin.api import sttp.client4.UriContext import sttp.model.StatusCode +import zio.* import zio.test.* import org.knora.webapi.* @@ -18,8 +19,10 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM2 import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse import org.knora.webapi.slice.admin.domain.model.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.testservices.ResponseOps import org.knora.webapi.testservices.ResponseOps.assert200 +import org.knora.webapi.testservices.ResponseOps.assert400 import org.knora.webapi.testservices.TestApiClient object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { @@ -31,11 +34,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { )( suite("used to query project memberships")( test("return all projects the user is a member of") { - TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/$multiUserIri/project-memberships", - rootUser, - ) + getProjectMemberships(multiUserIri) .flatMap(_.assert200) .map(result => assertTrue( @@ -48,96 +47,40 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { ), suite("used to modify project membership")( test("NOT add a user to project if the requesting user is not a SystemAdmin or ProjectAdmin") { - TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - normalUser, - ) + addUserToProject(normalUser.userIri, imagesProjectExternal.id, normalUser) .map(response => assertTrue(response.code == StatusCode.Forbidden)) }, test("add user to project") { for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - _ <- TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) + beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) + _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue( beforeResult.projects == Seq.empty, - afterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + afterResult.projects == Seq(imagesProjectExternal), ) }, test("don't add user to project if user is already a member") { for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue( - response.code == StatusCode.BadRequest, - afterResult.projects == beforeResult.projects, - ) + beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) + _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert400) + afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) + } yield assertTrue(afterResult.projects == beforeResult.projects) }, test("remove user from project") { for { - beforeResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - afterResult <- TestApiClient - .getJson[UserProjectMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-memberships", - rootUser, - ) - .flatMap(_.assert200) + beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) + _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue( - beforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - response.code == StatusCode.Ok, - afterResult.projects == Seq.empty[Project], + beforeResult.projects == Seq(imagesProjectExternal), + afterResult.projects == Seq.empty, ) }, ), suite("used to query project admin group memberships")( test("return all projects the user is a member of the project admin group") { - TestApiClient - .getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/$multiUserIri/project-admin-memberships", - rootUser, - ) + getProjectAdminMemberships(multiUserIri) .flatMap(_.assert200) .map(result => assertTrue( @@ -151,101 +94,94 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { suite("used to modify project admin group membership")( test("add user to project admin group only if he is already member of that project") { for { - // add user as project admin to images project - returns a BadRequest because user is not member of the project - responseWithoutBeingMember <- - TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) - // add user as member to images project - responseAddUserToProject <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) + // add user as project admin to images project - should return BadRequest because user is not member of the project + _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert400) + // add user as member to images project, must succeed + _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) // verify that user is not yet project admin in images project - membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 + membershipsBeforeResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) // add user as project admin to images project - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) + _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) // verify that user has been added as project admin to images project - membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 + membershipsAfterResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue( - responseWithoutBeingMember.code == StatusCode.BadRequest, - responseAddUserToProject.code == StatusCode.Ok, - membershipsBeforeResult.projects == Seq(), - response.code == StatusCode.Ok, - membershipsAfterResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), + membershipsBeforeResult.projects == Seq.empty, + membershipsAfterResult.projects == Seq(imagesProjectExternal), ) }, test("remove user from project admin group") { for { - membershipsBeforeResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - membershipsAfterResponse <- TestApiClient.getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 + membershipsBefore <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) + _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + membershipsAfter <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue( - membershipsBeforeResult.projects == Seq(SharedTestDataADM.imagesProjectExternal), - response.code == StatusCode.Ok, - membershipsAfterResult.projects == Seq.empty, + membershipsBefore.projects == Seq(imagesProjectExternal), + membershipsAfter.projects == Seq.empty, ) }, test("remove user from project which also removes him from project admin group") { for { // add user as project admin to images project - _ <- TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) *> - TestApiClient - .postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships/$imagesProjectIri", - "", - rootUser, - ) - .flatMap(_.assert200) - + _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) // remove user as project member from images project - _ <- TestApiClient - .deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/project-memberships/$imagesProjectIri", - rootUser, - ) - .flatMap(_.assert200) + _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) // verify that user has also been removed as project admin from images project - projectAdminMembershipsAfterResult <- TestApiClient - .getJson[UserProjectAdminMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/project-admin-memberships", - rootUser, - ) - .flatMap(_.assert200) + projectAdminMembershipsAfterResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) }, ), ) + + private def getProjectMemberships(userIri: UserIri, requestingUser: User = rootUser) = + addLogTiming("GET project-memberships") { + TestApiClient + .getJson[UserProjectMembershipsGetResponseADM]( + uri"/admin/users/iri/$userIri/project-memberships", + requestingUser, + ) + } + + private def getProjectAdminMemberships(userIri: UserIri, requestingUser: User = rootUser) = + addLogTiming("GET project-admin-memberships") { + TestApiClient + .getJson[UserProjectAdminMembershipsGetResponseADM]( + uri"/admin/users/iri/$userIri/project-admin-memberships", + requestingUser, + ) + } + + private def addUserToProject(userIri: UserIri, projectIri: ProjectIri, requestingUser: User = rootUser) = + addLogTiming("POST project-memberships") { + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/$userIri/project-memberships/$projectIri", + "", + requestingUser, + ) + } + + private def addUserToProjectAsAdmin(userIri: UserIri, projectIri: ProjectIri, requestingUser: User = rootUser) = + addLogTiming("POST project-admin-memberships") { + TestApiClient + .postJson[UserResponse, String]( + uri"/admin/users/iri/$userIri/project-admin-memberships/$projectIri", + "", + requestingUser, + ) + } + + private def removeUserFromProject(userIri: UserIri, projectIri: ProjectIri, requestingUser: User = rootUser) = + addLogTiming("DELETE project-memberships") { + TestApiClient + .deleteJson[UserResponse]( + uri"/admin/users/iri/$userIri/project-memberships/$projectIri", + requestingUser, + ) + } + + private def addLogTiming[R, E, A](msg: String)(zio: ZIO[R, E, A]): ZIO[R, E, A] = + zio.timed.flatMap { case (duration, res) => + ZIO.logWarning(s"$msg took: ${duration.toMillis} ms").as(res) + } } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala index 832f47a10f6..8353653d8de 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala @@ -92,21 +92,25 @@ final case class UserRestService( } yield external def getProjectMemberShipsByUserIri(userIri: UserIri): Task[UserProjectMembershipsGetResponseADM] = - for { + (for { kUser <- getKnoraUserOrNotFound(userIri) projects <- projectService.findByIds(kUser.isInProject) external <- format.toExternal(UserProjectMembershipsGetResponseADM(projects)) - } yield external + } yield external).timed.flatMap { case (duration, result) => + ZIO.logWarning(s"getProjectMemberShipsByUserIri took ${duration.toMillis} ms").as(result) + } private def getKnoraUserOrNotFound(userIri: UserIri) = knoraUserService.findById(userIri).someOrFail(NotFoundException(s"User with iri ${userIri.value} not found.")) def getProjectAdminMemberShipsByUserIri(userIri: UserIri): Task[UserProjectAdminMembershipsGetResponseADM] = - for { + (for { kUser <- getKnoraUserOrNotFound(userIri) projects <- projectService.findByIds(kUser.isInProjectAdminGroup) external <- format.toExternal(UserProjectAdminMembershipsGetResponseADM(projects)) - } yield external + } yield external).timed.flatMap { case (duration, result) => + ZIO.logWarning(s"getProjectAdminMemberShipsByUserIri took ${duration.toMillis} ms").as(result) + } def getUserByUsername(requestingUser: User)(username: Username): Task[UserResponse] = for { user <- userService @@ -115,10 +119,12 @@ final case class UserRestService( external <- asExternalUserResponse(requestingUser, user) } yield external - def getUserByIri(requestingUser: User)(userIri: UserIri): Task[UserResponse] = for { + def getUserByIri(requestingUser: User)(userIri: UserIri): Task[UserResponse] = (for { internal <- userService.findUserByIri(userIri).someOrFail(NotFoundException(s"User '${userIri.value}' not found")) external <- asExternalUserResponse(requestingUser, internal) - } yield external + } yield external).timed.flatMap { case (duration, result) => + ZIO.logWarning(s"getUserByIri took ${duration.toMillis} ms").as(result) + } private def ensureSelfUpdateOrSystemAdmin(userIri: UserIri, requestingUser: User) = ZIO.when(userIri != requestingUser.userIri)(auth.ensureSystemAdmin(requestingUser)) @@ -192,14 +198,16 @@ final case class UserRestService( userIri: UserIri, projectIri: ProjectIri, ): Task[UserResponse] = - for { + (for { _ <- ensureNotABuiltInUser(userIri) _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) kUser <- getKnoraUserOrNotFound(userIri) project <- getProjectADMOrBadRequest(projectIri) updatedUser <- knoraUserService.addUserToProject(kUser, project).mapError(BadRequestException.apply) external <- asExternalUserResponse(requestingUser, updatedUser) - } yield external + } yield external).timed.flatMap { case (duration, result) => + ZIO.logWarning(s"addUserToProject took ${duration.toMillis} ms").as(result) + } private def getProjectADMOrBadRequest(projectIri: ProjectIri) = projectService @@ -229,14 +237,16 @@ final case class UserRestService( userIri: UserIri, projectIri: ProjectIri, ): Task[UserResponse] = - for { + (for { _ <- ensureNotABuiltInUser(userIri) _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) user <- getKnoraUserOrNotFound(userIri) project <- getProjectADMOrBadRequest(projectIri) updatedUser <- knoraUserService.addUserToProjectAsAdmin(user, project).mapError(BadRequestException.apply) external <- asExternalUserResponse(requestingUser, updatedUser) - } yield external + } yield external).timed.flatMap { case (duration, result) => + ZIO.logWarning(s"addUserToProjectAsAdmin took ${duration.toMillis} ms").as(result) + } def removeUserFromProject(requestingUser: User)( userIri: UserIri, From bef546379b8c44345a0d01c6ad98f25b81075542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 12:56:58 +0200 Subject: [PATCH 75/99] try removing caching from KnoraUserRepoLive --- .../webapi/slice/admin/repo/service/KnoraUserRepoLive.scala | 6 ++---- .../admin/api/service/AuthorizationRestServiceSpec.scala | 2 -- .../slice/admin/domain/service/GroupServiceSpec.scala | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala index 8ac3da4008c..ed7a25adbd7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala @@ -41,8 +41,7 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService final case class KnoraUserRepoLive( private val triplestore: TriplestoreService, private val mapper: RdfEntityMapper[KnoraUser], - private val entityCache: EntityCache[UserIri, KnoraUser], -) extends CachingEntityRepo[KnoraUser, UserIri](triplestore, mapper, entityCache) +) extends AbstractEntityRepo[KnoraUser, UserIri](triplestore, mapper) with KnoraUserRepo { override protected val resourceClass: ParsedIRI = ParsedIRI.create(KnoraAdmin.User) @@ -134,6 +133,5 @@ object KnoraUserRepoLive { .andHas(isInProjectAdminGroup, u.isInProjectAdminGroup.map(p => Rdf.iri(p.value)).toList: _*) } - val layer = - (ZLayer.succeed(mapper) >+> EntityCache.layer[UserIri, KnoraUser]("knoraUser")) >>> ZLayer.derive[KnoraUserRepoLive] + val layer = ZLayer.succeed(mapper) >>> ZLayer.derive[KnoraUserRepoLive] } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala index d69729a5bfb..a7528550191 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala @@ -29,7 +29,6 @@ import org.knora.webapi.slice.admin.repo.service.KnoraGroupRepoInMemory import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.service.IriConverter -import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.slice.ontology.repo.service.OntologyRepoInMemory import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.store.triplestore.impl.TriplestoreServiceLive @@ -139,7 +138,6 @@ object AuthorizationRestServiceSpec extends ZIOSpecDefault { ).provide( AppConfig.layer, AuthorizationRestService.layer, - CacheManager.layer, IriConverter.layer, IriService.layer, KnoraGroupRepoInMemory.layer, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala index 5d74b0e8a60..cf350185ec7 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala @@ -25,7 +25,6 @@ import org.knora.webapi.slice.admin.repo.LicenseRepo import org.knora.webapi.slice.admin.repo.service.KnoraGroupRepoInMemory import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive import org.knora.webapi.slice.common.service.IriConverter -import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.slice.ontology.repo.service.OntologyCacheLive import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory @@ -64,7 +63,6 @@ object GroupServiceSpec extends ZIOSpecDefault { }, ).provide( AppConfig.layer, - CacheManager.layer, GroupService.layer, IriConverter.layer, IriService.layer, From 461f9fb76c1775ee13f96a0317f66b703071622a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 13:14:12 +0200 Subject: [PATCH 76/99] Revert "try removing caching from KnoraUserRepoLive" This reverts commit bef546379b8c44345a0d01c6ad98f25b81075542. --- .../webapi/slice/admin/repo/service/KnoraUserRepoLive.scala | 6 ++++-- .../admin/api/service/AuthorizationRestServiceSpec.scala | 2 ++ .../slice/admin/domain/service/GroupServiceSpec.scala | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala index ed7a25adbd7..8ac3da4008c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraUserRepoLive.scala @@ -41,7 +41,8 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService final case class KnoraUserRepoLive( private val triplestore: TriplestoreService, private val mapper: RdfEntityMapper[KnoraUser], -) extends AbstractEntityRepo[KnoraUser, UserIri](triplestore, mapper) + private val entityCache: EntityCache[UserIri, KnoraUser], +) extends CachingEntityRepo[KnoraUser, UserIri](triplestore, mapper, entityCache) with KnoraUserRepo { override protected val resourceClass: ParsedIRI = ParsedIRI.create(KnoraAdmin.User) @@ -133,5 +134,6 @@ object KnoraUserRepoLive { .andHas(isInProjectAdminGroup, u.isInProjectAdminGroup.map(p => Rdf.iri(p.value)).toList: _*) } - val layer = ZLayer.succeed(mapper) >>> ZLayer.derive[KnoraUserRepoLive] + val layer = + (ZLayer.succeed(mapper) >+> EntityCache.layer[UserIri, KnoraUser]("knoraUser")) >>> ZLayer.derive[KnoraUserRepoLive] } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala index a7528550191..d69729a5bfb 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/AuthorizationRestServiceSpec.scala @@ -29,6 +29,7 @@ import org.knora.webapi.slice.admin.repo.service.KnoraGroupRepoInMemory import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive import org.knora.webapi.slice.common.api.AuthorizationRestService import org.knora.webapi.slice.common.service.IriConverter +import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.slice.ontology.repo.service.OntologyRepoInMemory import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.store.triplestore.impl.TriplestoreServiceLive @@ -138,6 +139,7 @@ object AuthorizationRestServiceSpec extends ZIOSpecDefault { ).provide( AppConfig.layer, AuthorizationRestService.layer, + CacheManager.layer, IriConverter.layer, IriService.layer, KnoraGroupRepoInMemory.layer, diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala index cf350185ec7..5d74b0e8a60 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/GroupServiceSpec.scala @@ -25,6 +25,7 @@ import org.knora.webapi.slice.admin.repo.LicenseRepo import org.knora.webapi.slice.admin.repo.service.KnoraGroupRepoInMemory import org.knora.webapi.slice.admin.repo.service.KnoraUserRepoLive import org.knora.webapi.slice.common.service.IriConverter +import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.slice.ontology.repo.service.OntologyCacheLive import org.knora.webapi.slice.ontology.repo.service.OntologyRepoLive import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory @@ -63,6 +64,7 @@ object GroupServiceSpec extends ZIOSpecDefault { }, ).provide( AppConfig.layer, + CacheManager.layer, GroupService.layer, IriConverter.layer, IriService.layer, From 30a8b83376164f4056dc1cf30c071c497e2208bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 14:31:31 +0200 Subject: [PATCH 77/99] activate response compression --- .../main/scala/org/knora/webapi/core/DspApiServer.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala index 5eaea26d240..900297bb354 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/DspApiServer.scala @@ -13,6 +13,7 @@ import sttp.tapir.server.ziohttp.ZioHttpInterpreter import sttp.tapir.server.ziohttp.ZioHttpServerOptions import zio.* import zio.http.* +import zio.http.Server.Config.ResponseCompressionConfig import org.knora.webapi.config.KnoraApi import org.knora.webapi.routing.Endpoints @@ -47,7 +48,13 @@ object DspApiServer { private val serverLayer = ZLayer .service[KnoraApi] - .flatMap(cfg => Server.defaultWith(_.binding(cfg.get.internalHost, cfg.get.internalPort).enableRequestStreaming)) + .flatMap(cfg => + Server + .defaultWith( + _.binding(cfg.get.internalHost, cfg.get.internalPort).enableRequestStreaming + .responseCompression(ResponseCompressionConfig.default), + ), + ) .orDie val layer = serverLayer >>> ZLayer.derive[DspApiServer] From ff29d5046a360b5038f398b464e548af654ce844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 15:25:52 +0200 Subject: [PATCH 78/99] refactor TestApiClient, central method for sendRequest --- .../webapi/testservices/TestApiClient.scala | 149 +++++++++--------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala index 675d76a210f..f81b8092c1f 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala @@ -13,7 +13,6 @@ import sttp.model.Part import sttp.model.Uri import zio.* import zio.json.* - import org.knora.webapi.config.KnoraApi import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -46,9 +45,22 @@ final case class TestApiClient( val request: Request[Either[String, A]] = basicRequest .delete(relativeUri) .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - f(request).send(backend) + sendRequest(f(request)) } + private def sendRequest[A](request: Request[Either[String, A]], user: User): Task[Response[Either[String, A]]] = + sendRequest(request, Some(user)) + + private def sendRequest[A]( + request: Request[Either[String, A]], + user: Option[User] = None, + ): Task[Response[Either[String, A]]] = + addAuthIfNeeded(user, request).flatMap { req => + req +// .copy(options = req.options.copy(readTimeout = 5.seconds.asScala)) + .send(backend) + } + def deleteJsonLd( relativeUri: Uri, user: Option[User], @@ -57,7 +69,7 @@ final case class TestApiClient( val request = update(basicRequest.delete(relativeUri)) .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asString) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def deleteJsonLdDocument( @@ -71,15 +83,14 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asJsonLdDocument), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def getAsString( relativeUri: Uri, f: Request[Either[String, String]] => Request[Either[String, String]], ): Task[Response[Either[String, String]]] = - val request: Request[Either[String, String]] = f(basicRequest.get(relativeUri).response(asString)) - request.send(backend) + sendRequest(f(basicRequest.get(relativeUri).response(asString))) def getJson[A: JsonDecoder](relativeUri: Uri): Task[Response[Either[String, A]]] = getJson(relativeUri, (r: Request[Either[String, A]]) => r) @@ -87,12 +98,9 @@ final case class TestApiClient( def getJson[A: JsonDecoder]( relativeUri: Uri, f: Request[Either[String, A]] => Request[Either[String, A]], - ): Task[Response[Either[String, A]]] = { - val request: Request[Either[String, A]] = basicRequest - .get(relativeUri) - .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.getMessage)) - f(request).send(backend) - } + ): Task[Response[Either[String, A]]] = sendRequest( + f(basicRequest.get(relativeUri).response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.getMessage))), + ) def getJson[A: JsonDecoder](relativeUri: Uri, user: User): Task[Response[Either[String, A]]] = jwtFor(user).flatMap(jwt => getJson(relativeUri, _.auth.bearer(jwt))) @@ -108,7 +116,7 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asString), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def getJsonLdDocument( @@ -122,7 +130,7 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asJsonLdDocument), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } private def addAuthIfNeeded[A](user: Option[User], request: Request[Either[String, A]]) = user match { @@ -134,41 +142,36 @@ final case class TestApiClient( relativeUri: Uri, body: B, user: User, - ): Task[Response[Either[String, A]]] = - jwtFor(user).flatMap { jwt => - basicRequest - .post(relativeUri) - .body(body.toJson) - .contentType(MediaType.ApplicationJson) - .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - .auth - .bearer(jwt) - .send(backend) - } + ): Task[Response[Either[String, A]]] = { + val request = basicRequest + .post(relativeUri) + .body(body.toJson) + .contentType(MediaType.ApplicationJson) + .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) + sendRequest(request, Some(user)) + } - def postJson[A: JsonDecoder, B: JsonEncoder](relativeUri: Uri, body: B): Task[Response[Either[String, A]]] = - basicRequest + def postJson[A: JsonDecoder, B: JsonEncoder](relativeUri: Uri, body: B): Task[Response[Either[String, A]]] = { + val request = basicRequest .post(relativeUri) .body(body.toJson) .contentType(MediaType.ApplicationJson) .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.getMessage)) - .send(backend) + sendRequest(request) + } def postJsonLd( relativeUri: Uri, jsonLdBody: String, user: User, - ): ZIO[Any, Throwable, Response[Either[String, String]]] = - jwtFor(user).flatMap { jwt => - basicRequest - .post(relativeUri) - .body(jsonLdBody) - .contentType(MediaType.unsafeApply("application", "ld+json")) - .auth - .bearer(jwt) - .response(asString) - .send(backend) - } + ): Task[Response[Either[String, String]]] = { + val request = basicRequest + .post(relativeUri) + .body(jsonLdBody) + .contentType(MediaType.unsafeApply("application", "ld+json")) + .response(asString) + sendRequest(request, user) + } def postJsonLdDocument( relativeUri: Uri, @@ -183,23 +186,20 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asJsonLdDocument), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def postMultiPart[A: JsonDecoder]( relativeUri: Uri, body: Seq[Part[BasicBodyPart]], user: User, - ): Task[Response[Either[String, A]]] = - jwtFor(user).flatMap { jwt => - basicRequest - .post(relativeUri) - .multipartBody(body) - .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - .auth - .bearer(jwt) - .send(backend) - } + ): Task[Response[Either[String, A]]] = { + val request = basicRequest + .post(relativeUri) + .multipartBody(body) + .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) + sendRequest(request, user) + } def patchJsonLdDocument( relativeUri: Uri, @@ -214,40 +214,34 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asJsonLdDocument), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def putJson[A: JsonDecoder, B: JsonEncoder]( relativeUri: Uri, body: B, user: User, - ): Task[Response[Either[String, A]]] = - jwtFor(user).flatMap { jwt => - basicRequest - .put(relativeUri) - .body(body.toJson) - .contentType(MediaType.ApplicationJson) - .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - .auth - .bearer(jwt) - .send(backend) - } + ): Task[Response[Either[String, A]]] = { + val request = basicRequest + .put(relativeUri) + .body(body.toJson) + .contentType(MediaType.ApplicationJson) + .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) + sendRequest(request, user) + } def putJsonLd( relativeUri: Uri, jsonLdBody: String, user: User, - ): ZIO[Any, Throwable, Response[Either[String, String]]] = - jwtFor(user).flatMap { jwt => - basicRequest - .put(relativeUri) - .body(jsonLdBody) - .contentType(MediaType.unsafeApply("application", "ld+json")) - .auth - .bearer(jwt) - .response(asString) - .send(backend) - } + ): ZIO[Any, Throwable, Response[Either[String, String]]] = { + val request = basicRequest + .put(relativeUri) + .body(jsonLdBody) + .contentType(MediaType.unsafeApply("application", "ld+json")) + .response(asString) + sendRequest(request, user) + } def putJsonLdDocument( relativeUri: Uri, @@ -262,18 +256,19 @@ final case class TestApiClient( .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asJsonLdDocument), ) - addAuthIfNeeded(user, request).flatMap(_.send(backend)) + sendRequest(request, user) } def postSparql( relativeUri: Uri, sparqlQuery: String, f: RequestUpdate[String], - ): Task[Response[Either[String, String]]] = - f(basicRequest.post(relativeUri).body(sparqlQuery)) + ): Task[Response[Either[String, String]]] = { + val request = f(basicRequest.post(relativeUri).body(sparqlQuery)) .contentType(MediaType.unsafeApply("application", "sparql-query")) .response(asString) - .send(backend) + sendRequest(request) + } def postSparql( relativeUri: Uri, From 203637950f68e8356426c627efc100bcc85ee527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 15:32:56 +0200 Subject: [PATCH 79/99] fmt --- .../main/scala/org/knora/webapi/testservices/TestApiClient.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala index f81b8092c1f..ec1110b30f8 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala @@ -13,6 +13,7 @@ import sttp.model.Part import sttp.model.Uri import zio.* import zio.json.* + import org.knora.webapi.config.KnoraApi import org.knora.webapi.messages.util.rdf.JsonLDDocument import org.knora.webapi.sharedtestdata.SharedTestDataADM From e76f8324d38ad9f5852a90398b92c3028ede1fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 15:50:29 +0200 Subject: [PATCH 80/99] try biggest buildjet instance --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6ef0f4f1b52..f5d0f0b4392 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -73,7 +73,7 @@ jobs: test-e2e: name: Build and E2E test - runs-on: buildjet-8vcpu-ubuntu-2204 + runs-on: buildjet-32vcpu-ubuntu-2204 concurrency: group: ${{ github.ref }}-e2e cancel-in-progress: true From 12d8a4ca23a2d8b3bd0786d0ca7927dd8cf60991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 16:30:30 +0200 Subject: [PATCH 81/99] update fuseki --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3b6a7e68d24..786b5623e16 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,7 +10,7 @@ import sbt._ object Dependencies { // should be the same version as in docker-compose.yml, // make sure to use the same version in ops-deploy repository when deploying new DSP releases! - val fusekiImage = "daschswiss/apache-jena-fuseki:5.5.0-1" + val fusekiImage = "daschswiss/apache-jena-fuseki:5.5.0-2" // base image the knora-sipi image is created from val sipiImage = "daschswiss/sipi:v3.16.3" From 282b045644a0689bbf3d737692263efced39bd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 3 Oct 2025 17:01:04 +0200 Subject: [PATCH 82/99] debug logging --- webapi/src/main/scala/org/knora/webapi/util/Logger.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala index a1998ea88cf..2c1b91abfb8 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala @@ -19,9 +19,10 @@ object Logger { private val logFilter = LogFilter.LogLevelByNameConfig( rootLogLevel, - ("org.apache.jena", LogLevel.Info), + ("org.apache.jena", LogLevel.Debug), ("io.netty", LogLevel.Info), ("org.ehcache", LogLevel.Info), + ("zio.http.*", LogLevel.Debug), // Uncomment the following lines to change the log level for specific loggers: // ("zio.logging.slf4j", LogLevel.Debug) // ("SLF4J-LOGGER", LogLevel.Warning) @@ -53,5 +54,8 @@ object Logger { def json(): ULayer[Unit] = Runtime.removeDefaultLoggers >>> jsonLogger >+> Slf4jBridge.initialize - val text: ULayer[Unit] = Runtime.removeDefaultLoggers >>> textLogger >+> Slf4jBridge.initialize + val text: ULayer[Unit] = Runtime.removeDefaultLoggers >>> consoleLogger( + config = ConsoleLoggerConfig.default + .copy(format = logFormatText, filter = ConsoleLoggerConfig.default.filter.withRootLevel(LogLevel.Debug)), + ) } From 5e7ce6431e993610725386e73d4a8bb3efccc89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 10:44:08 +0200 Subject: [PATCH 83/99] extract addLogTiming --- .../AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 6 +----- .../src/main/scala/org/knora/webapi/util/ZioHelper.scala | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 367939ac57a..bf6363ab6f8 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -24,6 +24,7 @@ import org.knora.webapi.testservices.ResponseOps import org.knora.webapi.testservices.ResponseOps.assert200 import org.knora.webapi.testservices.ResponseOps.assert400 import org.knora.webapi.testservices.TestApiClient +import org.knora.webapi.util.ZioHelper.addLogTiming object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { @@ -179,9 +180,4 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { requestingUser, ) } - - private def addLogTiming[R, E, A](msg: String)(zio: ZIO[R, E, A]): ZIO[R, E, A] = - zio.timed.flatMap { case (duration, res) => - ZIO.logWarning(s"$msg took: ${duration.toMillis} ms").as(res) - } } diff --git a/webapi/src/main/scala/org/knora/webapi/util/ZioHelper.scala b/webapi/src/main/scala/org/knora/webapi/util/ZioHelper.scala index 9bf4ac5430f..744c3432974 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/ZioHelper.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/ZioHelper.scala @@ -5,6 +5,7 @@ package org.knora.webapi.util +import zio.LogLevel import zio.Task import zio.ZIO @@ -16,4 +17,11 @@ object ZioHelper { def sequence[A](x: Seq[Task[A]]): Task[List[A]] = x.map(_.map(x => List[A](x))) .fold(ZIO.succeed(List.empty[A]))((x, y) => x.flatMap(a => y.map(b => a ++ b))) + + def addLogTiming[R, E, A](msg: String, logLevel: LogLevel = LogLevel.Debug)(zio: ZIO[R, E, A]): ZIO[R, E, A] = + ZIO.logLevel(logLevel) { + zio.timed.flatMap { case (duration, res) => + ZIO.log(s"$msg took: ${duration.toMillis} ms").as(res) + } + } } From 7ea481d855a3dd2a75e8ce1d0049a387b5e43ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 10:44:28 +0200 Subject: [PATCH 84/99] add flaky to AdminUsersProjectMemberShipsEndpointsE2ESpec --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index bf6363ab6f8..4333678e1f2 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -132,7 +132,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) }, ), - ) + ) @@ TestAspect.flaky private def getProjectMemberships(userIri: UserIri, requestingUser: User = rootUser) = addLogTiming("GET project-memberships") { From afaabfdeebb0504334dc6a105b997366280b9212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 12:24:07 +0200 Subject: [PATCH 85/99] add timeout --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 4333678e1f2..bdae6ec0f4f 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -132,7 +132,8 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) }, ), - ) @@ TestAspect.flaky + ) @@ TestAspect.timeout(2.seconds) + @@ TestAspect.flaky private def getProjectMemberships(userIri: UserIri, requestingUser: User = rootUser) = addLogTiming("GET project-memberships") { From bc7813669ba192ff07194db52113c070493d7d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 14:17:52 +0200 Subject: [PATCH 86/99] make tests independent of one another by creating a new user each time --- build.sbt | 1 + ...rsProjectMemberShipsEndpointsE2ESpec.scala | 136 +++++++++++------- .../scala/org/knora/webapi/E2EZSpec.scala | 2 + project/Dependencies.scala | 2 + .../slice/admin/api/model/UserDto.scala | 5 +- .../slice/admin/domain/model/User.scala | 2 +- .../admin/domain/model/UsernameSpec.scala | 3 +- 7 files changed, 94 insertions(+), 57 deletions(-) diff --git a/build.sbt b/build.sbt index fa2e9c0556f..30e68f487b3 100644 --- a/build.sbt +++ b/build.sbt @@ -285,6 +285,7 @@ lazy val testkit: Project = Project(id = "testkit", base = file("modules/testkit Dependencies.zioTest, Dependencies.testcontainers, Dependencies.wiremock, + Dependencies.dataFaker, ), publish / skip := true, name := "testkit", diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index bdae6ec0f4f..aff672db579 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -8,6 +8,7 @@ package org.knora.webapi.slice.admin.api import sttp.client4.UriContext import sttp.model.StatusCode import zio.* +import zio.json.* import zio.test.* import org.knora.webapi.* @@ -16,13 +17,15 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectMember import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.* import org.knora.webapi.sharedtestdata.SharedTestDataADM2 +import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse import org.knora.webapi.slice.admin.domain.model.* import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.common.domain.LanguageCode +import org.knora.webapi.slice.common.domain.LanguageCode.DE import org.knora.webapi.testservices.ResponseOps import org.knora.webapi.testservices.ResponseOps.assert200 -import org.knora.webapi.testservices.ResponseOps.assert400 import org.knora.webapi.testservices.TestApiClient import org.knora.webapi.util.ZioHelper.addLogTiming @@ -30,6 +33,25 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { private val multiUserIri = UserIri.unsafeFrom(SharedTestDataADM2.multiuserUser.userData.user_id.get) + private def createNewUser = + val firstName = faker.name().firstName() + val lastName = faker.name().lastName() + val req = UserCreateRequest( + id = None, + Username.unsafeFrom(s"$firstName.$lastName"), + Email.unsafeFrom(s"$firstName.$lastName@example.org"), + GivenName.unsafeFrom(firstName), + FamilyName.unsafeFrom(lastName), + Password.unsafeFrom(faker.credentials().password(8, 16)), + UserStatus.Active, + DE, + SystemAdmin.IsNotSystemAdmin, + ) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", req, rootUser) + .flatMap(_.assert200) + .map(_.user) + override val e2eSpec = suite( "The Users Routes ('admin/users/iri/:userIri/project-memberships', 'admin/users/iri/:userIri/project-admin-memberships') ", )( @@ -48,35 +70,40 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { ), suite("used to modify project membership")( test("NOT add a user to project if the requesting user is not a SystemAdmin or ProjectAdmin") { - addUserToProject(normalUser.userIri, imagesProjectExternal.id, normalUser) - .map(response => assertTrue(response.code == StatusCode.Forbidden)) + for { + newUser <- createNewUser + + response <- addUserToProject(newUser.userIri, imagesProjectExternal.id, normalUser) + + } yield assertTrue(response.code == StatusCode.Forbidden) }, test("add user to project") { for { - beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue( - beforeResult.projects == Seq.empty, - afterResult.projects == Seq(imagesProjectExternal), - ) + newUser <- createNewUser + + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + projectMemberships <- getProjectMemberships(newUser.userIri).flatMap(_.assert200) + } yield assertTrue(projectMemberships.projects == Seq(imagesProjectExternal)) }, test("don't add user to project if user is already a member") { for { - beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert400) - afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue(afterResult.projects == beforeResult.projects) + newUser <- createNewUser + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + response <- addUserToProject(newUser.userIri, imagesProjectExternal.id) + + } yield assertTrue(response.code == StatusCode.BadRequest) }, test("remove user from project") { for { - beforeResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - afterResult <- getProjectMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue( - beforeResult.projects == Seq(imagesProjectExternal), - afterResult.projects == Seq.empty, - ) + newUser <- createNewUser + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + _ <- removeUserFromProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + projectMemberships <- getProjectMemberships(newUser.userIri).flatMap(_.assert200) + } yield assertTrue(projectMemberships.projects == Seq.empty) }, ), suite("used to query project admin group memberships")( @@ -93,47 +120,48 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { }, ), suite("used to modify project admin group membership")( - test("add user to project admin group only if he is already member of that project") { + test("do NOT add user to project admin group if not member of that project") { for { - // add user as project admin to images project - should return BadRequest because user is not member of the project - _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert400) - // add user as member to images project, must succeed - _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - // verify that user is not yet project admin in images project - membershipsBeforeResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) - // add user as project admin to images project - _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - // verify that user has been added as project admin to images project - membershipsAfterResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue( - membershipsBeforeResult.projects == Seq.empty, - membershipsAfterResult.projects == Seq(imagesProjectExternal), - ) + newUser <- createNewUser + + response <- addUserToProjectAsAdmin(newUser.userIri, imagesProjectExternal.id) + + } yield assertTrue(response.code == StatusCode.BadRequest) + }, + test("add user to project admin group if member of that project") { + for { + newUser <- createNewUser + + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + _ <- addUserToProjectAsAdmin(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) + } yield assertTrue(adminMemberships.projects == Seq(imagesProjectExternal)) }, test("remove user from project admin group") { for { - membershipsBefore <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) - _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - membershipsAfter <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue( - membershipsBefore.projects == Seq(imagesProjectExternal), - membershipsAfter.projects == Seq.empty, - ) + newUser <- createNewUser + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + _ <- addUserToProjectAsAdmin(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + _ <- removeUserFromProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) + } yield assertTrue(adminMemberships.projects == Seq.empty) }, - test("remove user from project which also removes him from project admin group") { + test("remove user from project which also removes them from project admin group") { for { - // add user as project admin to images project - _ <- addUserToProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - _ <- addUserToProjectAsAdmin(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - // remove user as project member from images project - _ <- removeUserFromProject(normalUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) - // verify that user has also been removed as project admin from images project - projectAdminMembershipsAfterResult <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) - } yield assertTrue(projectAdminMembershipsAfterResult.projects == Seq.empty) + newUser <- createNewUser + _ <- addUserToProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + _ <- addUserToProjectAsAdmin(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + _ <- removeUserFromProject(newUser.userIri, imagesProjectExternal.id).flatMap(_.assert200) + + adminMemberships <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) + } yield assertTrue(adminMemberships.projects == Seq.empty) }, ), - ) @@ TestAspect.timeout(2.seconds) - @@ TestAspect.flaky + ) private def getProjectMemberships(userIri: UserIri, requestingUser: User = rootUser) = addLogTiming("GET project-memberships") { diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index e48824f6103..cd8655c6ead 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -5,6 +5,7 @@ package org.knora.webapi +import net.datafaker.Faker import sttp.client4.Response import sttp.client4.UriContext import sttp.model.StatusCode @@ -29,6 +30,7 @@ import org.knora.webapi.util.Logger abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { implicit val sf: StringFormatter = StringFormatter.getInitializedTestInstance + val faker: Faker = new Faker() override val bootstrap: ULayer[E2EZSpec.Environment] = Logger.text >>> diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 786b5623e16..9b54aed8e92 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -128,6 +128,8 @@ object Dependencies { val scalaCsv = "com.github.tototoshi" %% "scala-csv" % "2.0.0" // test + val dataFaker = "net.datafaker" % "datafaker" % "2.5.1" + val scalaTest = "org.scalatest" %% "scalatest" % "3.2.19" val testcontainers = "org.testcontainers" % "testcontainers" % "1.21.3" diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/UserDto.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/UserDto.scala index 9008336cb8d..a409a9b04cd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/UserDto.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/UserDto.scala @@ -10,6 +10,7 @@ import zio.json.JsonCodec import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM import org.knora.webapi.slice.admin.domain.model.Group import org.knora.webapi.slice.admin.domain.model.User +import org.knora.webapi.slice.admin.domain.model.UserIri final case class UserDto( id: String, @@ -22,7 +23,9 @@ final case class UserDto( groups: Seq[Group], projects: Seq[Project], permissions: PermissionsDataADM, -) +) { + def userIri: UserIri = UserIri.unsafeFrom(id) +} object UserDto { given JsonCodec[UserDto] = DeriveJsonCodec.gen[UserDto] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala index 70c67bdc2be..3d8324e6f40 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/User.scala @@ -210,7 +210,7 @@ object Username extends StringValueCompanion[Username] { } else { UsernameRegex.findFirstIn(value) match { case Some(value) => Right(Username(value)) - case None => Left(UserErrorMessages.UsernameInvalid) + case None => Left(UserErrorMessages.UsernameInvalid + s" $value") } } } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala index 3b681ae61c8..f918a68bb79 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala @@ -18,6 +18,7 @@ object UsernameSpec extends ZIOSpecDefault { "user-123", "user.123", "use", + "Mose.Dooley", ) private val invalidNames = Seq( "_username", // (starts with underscore) @@ -41,7 +42,7 @@ object UsernameSpec extends ZIOSpecDefault { check(Gen.fromIterable(validNames))(it => assertTrue(Username.from(it).map(_.value) == Right(it))) }, test("should reject invalid names") { - check(Gen.fromIterable(invalidNames))(it => assertTrue(Username.from(it) == Left("Username is invalid."))) + check(Gen.fromIterable(invalidNames))(it => assertTrue(Username.from(it) == Left(s"Username is invalid. $it"))) }, ) } From 2fb6c181b22cc10d2bd2003f96829be2faa9a518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 14:27:34 +0200 Subject: [PATCH 87/99] lastname may contain characters not allowed in username, use positive number instead --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 3 ++- .../knora/webapi/slice/admin/domain/model/UsernameSpec.scala | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index aff672db579..e0278958ed5 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -17,6 +17,7 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserProjectMember import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.* import org.knora.webapi.sharedtestdata.SharedTestDataADM2 +import org.knora.webapi.slice.admin.api.AdminUsersProjectMemberShipsEndpointsE2ESpec.faker import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest import org.knora.webapi.slice.admin.api.model.Project import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse @@ -38,7 +39,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { val lastName = faker.name().lastName() val req = UserCreateRequest( id = None, - Username.unsafeFrom(s"$firstName.$lastName"), + Username.unsafeFrom(s"$firstName.${faker.number().positive()}"), Email.unsafeFrom(s"$firstName.$lastName@example.org"), GivenName.unsafeFrom(firstName), FamilyName.unsafeFrom(lastName), diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala index f918a68bb79..b1284bf1d84 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/UsernameSpec.scala @@ -19,6 +19,9 @@ object UsernameSpec extends ZIOSpecDefault { "user.123", "use", "Mose.Dooley", + "Wayne.1576803472", + "Stacey.1811737105", + "a".repeat(50), // (50 characters) ) private val invalidNames = Seq( "_username", // (starts with underscore) From b5ab7dcca69dd5a3199e40935513d91548294d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 14:49:04 +0200 Subject: [PATCH 88/99] rm extra logging and cleanup --- .../src/test/resources/application.conf | 2 - .../src/test/resources/application.conf | 2 - .../scala/org/knora/webapi/E2EZSpec.scala | 11 ++- .../admin/api/service/UserRestService.scala | 78 ++++++++----------- .../scala/org/knora/webapi/util/Logger.scala | 5 +- 5 files changed, 42 insertions(+), 56 deletions(-) diff --git a/modules/test-e2e/src/test/resources/application.conf b/modules/test-e2e/src/test/resources/application.conf index e3ba0c82aeb..c707248e0c1 100644 --- a/modules/test-e2e/src/test/resources/application.conf +++ b/modules/test-e2e/src/test/resources/application.conf @@ -1,5 +1,3 @@ -include "test" - app { triplestore { dbtype = "fuseki" diff --git a/modules/test-it/src/test/resources/application.conf b/modules/test-it/src/test/resources/application.conf index e3ba0c82aeb..c707248e0c1 100644 --- a/modules/test-it/src/test/resources/application.conf +++ b/modules/test-it/src/test/resources/application.conf @@ -1,5 +1,3 @@ -include "test" - app { triplestore { dbtype = "fuseki" diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index cd8655c6ead..af3c2094acf 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -13,6 +13,7 @@ import zio.* import zio.json.ast.Json import zio.test.* import zio.test.Assertion.* +import zio.logging.* import scala.reflect.ClassTag @@ -25,15 +26,21 @@ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.slice.infrastructure.CacheManager import org.knora.webapi.testservices.TestApiClient import org.knora.webapi.testservices.TestClientsModule -import org.knora.webapi.util.Logger abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { implicit val sf: StringFormatter = StringFormatter.getInitializedTestInstance val faker: Faker = new Faker() + private val testLogger: ULayer[Unit] = Runtime.removeDefaultLoggers >>> consoleLogger( + config = { + val default = ConsoleLoggerConfig.default + default.copy(filter = default.filter.withRootLevel(LogLevel.Error)) + }, + ) + override val bootstrap: ULayer[E2EZSpec.Environment] = - Logger.text >>> + testLogger >>> TestContainerLayers.all >+> LayersLive.remainingLayer >+> TestClientsModule.layer diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala index 8353653d8de..71facd736b0 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/UserRestService.scala @@ -83,34 +83,27 @@ final case class UserRestService( .map(UserGroupMembershipsGetResponseADM.apply) .flatMap(format.toExternal) - def createUser(requestingUser: User)(userCreateRequest: Requests.UserCreateRequest): Task[UserResponse] = - for { - _ <- if (userCreateRequest.systemAdmin.value) { auth.ensureSystemAdmin(requestingUser) } - else { auth.ensureSystemAdminOrProjectAdminInAnyProject(requestingUser) } - internal <- knoraUserService.createNewUser(userCreateRequest) - external <- asExternalUserResponse(requestingUser, internal) - } yield external + def createUser(requestingUser: User)(userCreateRequest: Requests.UserCreateRequest): Task[UserResponse] = for { + _ <- if (userCreateRequest.systemAdmin.value) { auth.ensureSystemAdmin(requestingUser) } + else { auth.ensureSystemAdminOrProjectAdminInAnyProject(requestingUser) } + internal <- knoraUserService.createNewUser(userCreateRequest) + external <- asExternalUserResponse(requestingUser, internal) + } yield external - def getProjectMemberShipsByUserIri(userIri: UserIri): Task[UserProjectMembershipsGetResponseADM] = - (for { - kUser <- getKnoraUserOrNotFound(userIri) - projects <- projectService.findByIds(kUser.isInProject) - external <- format.toExternal(UserProjectMembershipsGetResponseADM(projects)) - } yield external).timed.flatMap { case (duration, result) => - ZIO.logWarning(s"getProjectMemberShipsByUserIri took ${duration.toMillis} ms").as(result) - } + def getProjectMemberShipsByUserIri(userIri: UserIri): Task[UserProjectMembershipsGetResponseADM] = for { + kUser <- getKnoraUserOrNotFound(userIri) + projects <- projectService.findByIds(kUser.isInProject) + external <- format.toExternal(UserProjectMembershipsGetResponseADM(projects)) + } yield external private def getKnoraUserOrNotFound(userIri: UserIri) = knoraUserService.findById(userIri).someOrFail(NotFoundException(s"User with iri ${userIri.value} not found.")) - def getProjectAdminMemberShipsByUserIri(userIri: UserIri): Task[UserProjectAdminMembershipsGetResponseADM] = - (for { - kUser <- getKnoraUserOrNotFound(userIri) - projects <- projectService.findByIds(kUser.isInProjectAdminGroup) - external <- format.toExternal(UserProjectAdminMembershipsGetResponseADM(projects)) - } yield external).timed.flatMap { case (duration, result) => - ZIO.logWarning(s"getProjectAdminMemberShipsByUserIri took ${duration.toMillis} ms").as(result) - } + def getProjectAdminMemberShipsByUserIri(userIri: UserIri): Task[UserProjectAdminMembershipsGetResponseADM] = for { + kUser <- getKnoraUserOrNotFound(userIri) + projects <- projectService.findByIds(kUser.isInProjectAdminGroup) + external <- format.toExternal(UserProjectAdminMembershipsGetResponseADM(projects)) + } yield external def getUserByUsername(requestingUser: User)(username: Username): Task[UserResponse] = for { user <- userService @@ -119,12 +112,10 @@ final case class UserRestService( external <- asExternalUserResponse(requestingUser, user) } yield external - def getUserByIri(requestingUser: User)(userIri: UserIri): Task[UserResponse] = (for { + def getUserByIri(requestingUser: User)(userIri: UserIri): Task[UserResponse] = for { internal <- userService.findUserByIri(userIri).someOrFail(NotFoundException(s"User '${userIri.value}' not found")) external <- asExternalUserResponse(requestingUser, internal) - } yield external).timed.flatMap { case (duration, result) => - ZIO.logWarning(s"getUserByIri took ${duration.toMillis} ms").as(result) - } + } yield external private def ensureSelfUpdateOrSystemAdmin(userIri: UserIri, requestingUser: User) = ZIO.when(userIri != requestingUser.userIri)(auth.ensureSystemAdmin(requestingUser)) @@ -197,19 +188,16 @@ final case class UserRestService( def addUserToProject(requestingUser: User)( userIri: UserIri, projectIri: ProjectIri, - ): Task[UserResponse] = - (for { - _ <- ensureNotABuiltInUser(userIri) - _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) - kUser <- getKnoraUserOrNotFound(userIri) - project <- getProjectADMOrBadRequest(projectIri) - updatedUser <- knoraUserService.addUserToProject(kUser, project).mapError(BadRequestException.apply) - external <- asExternalUserResponse(requestingUser, updatedUser) - } yield external).timed.flatMap { case (duration, result) => - ZIO.logWarning(s"addUserToProject took ${duration.toMillis} ms").as(result) - } + ): Task[UserResponse] = for { + _ <- ensureNotABuiltInUser(userIri) + _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) + kUser <- getKnoraUserOrNotFound(userIri) + project <- getProjectOrBadRequest(projectIri) + updatedUser <- knoraUserService.addUserToProject(kUser, project).mapError(BadRequestException.apply) + external <- asExternalUserResponse(requestingUser, updatedUser) + } yield external - private def getProjectADMOrBadRequest(projectIri: ProjectIri) = + private def getProjectOrBadRequest(projectIri: ProjectIri) = projectService .findById(projectIri) .someOrFail(BadRequestException(s"Project with iri ${projectIri.value} not found.")) @@ -237,16 +225,14 @@ final case class UserRestService( userIri: UserIri, projectIri: ProjectIri, ): Task[UserResponse] = - (for { + for { _ <- ensureNotABuiltInUser(userIri) _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) user <- getKnoraUserOrNotFound(userIri) - project <- getProjectADMOrBadRequest(projectIri) + project <- getProjectOrBadRequest(projectIri) updatedUser <- knoraUserService.addUserToProjectAsAdmin(user, project).mapError(BadRequestException.apply) external <- asExternalUserResponse(requestingUser, updatedUser) - } yield external).timed.flatMap { case (duration, result) => - ZIO.logWarning(s"addUserToProjectAsAdmin took ${duration.toMillis} ms").as(result) - } + } yield external def removeUserFromProject(requestingUser: User)( userIri: UserIri, @@ -256,7 +242,7 @@ final case class UserRestService( _ <- ensureNotABuiltInUser(userIri) _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) user <- getKnoraUserOrNotFound(userIri) - project <- getProjectADMOrBadRequest(projectIri) + project <- getProjectOrBadRequest(projectIri) updateUser <- knoraUserService.removeUserFromProject(user, project).mapError(BadRequestException.apply) response <- asExternalUserResponse(requestingUser, updateUser) } yield response @@ -269,7 +255,7 @@ final case class UserRestService( _ <- ensureNotABuiltInUser(userIri) _ <- auth.ensureSystemAdminOrProjectAdminById(requestingUser, projectIri) user <- getKnoraUserOrNotFound(userIri) - project <- getProjectADMOrBadRequest(projectIri) + project <- getProjectOrBadRequest(projectIri) updatedUser <- knoraUserService .removeUserFromProjectAsAdmin(user, project) .mapError(BadRequestException.apply) diff --git a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala index 2c1b91abfb8..e756952cc18 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala @@ -4,6 +4,7 @@ */ package org.knora.webapi.util + import zio.* import zio.logging.* import zio.logging.LogFormat.* @@ -54,8 +55,4 @@ object Logger { def json(): ULayer[Unit] = Runtime.removeDefaultLoggers >>> jsonLogger >+> Slf4jBridge.initialize - val text: ULayer[Unit] = Runtime.removeDefaultLoggers >>> consoleLogger( - config = ConsoleLoggerConfig.default - .copy(format = logFormatText, filter = ConsoleLoggerConfig.default.filter.withRootLevel(LogLevel.Debug)), - ) } From 345969344a244191befd54786ec6d82166bb120c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 14:50:51 +0200 Subject: [PATCH 89/99] fmt --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index af3c2094acf..4b1e899da5b 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -11,9 +11,9 @@ import sttp.client4.UriContext import sttp.model.StatusCode import zio.* import zio.json.ast.Json +import zio.logging.* import zio.test.* import zio.test.Assertion.* -import zio.logging.* import scala.reflect.ClassTag From 5a228760c8a4af62dd25cdfc78a74e1ce55cbb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 16:01:33 +0200 Subject: [PATCH 90/99] use TestAspect.flaky --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index e0278958ed5..30831eaec35 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -86,7 +86,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { projectMemberships <- getProjectMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(projectMemberships.projects == Seq(imagesProjectExternal)) - }, + } @@ TestAspect.flaky, test("don't add user to project if user is already a member") { for { newUser <- createNewUser @@ -138,7 +138,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(adminMemberships.projects == Seq(imagesProjectExternal)) - }, + } @@ TestAspect.flaky, test("remove user from project admin group") { for { newUser <- createNewUser From 1e1956214f2df3024d3cca733d0ef90121ab32b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 9 Oct 2025 16:16:08 +0200 Subject: [PATCH 91/99] use TestAspect.flaky and timeout --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 30831eaec35..6c3fa14f488 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -86,7 +86,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { projectMemberships <- getProjectMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(projectMemberships.projects == Seq(imagesProjectExternal)) - } @@ TestAspect.flaky, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, test("don't add user to project if user is already a member") { for { newUser <- createNewUser @@ -138,7 +138,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(adminMemberships.projects == Seq(imagesProjectExternal)) - } @@ TestAspect.flaky, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, test("remove user from project admin group") { for { newUser <- createNewUser From d67b1f6714a4bb88cba5908d174ad1c12b9d561b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 09:40:17 +0200 Subject: [PATCH 92/99] remove comment and inline addAuthIfNeeded --- .../webapi/testservices/TestApiClient.scala | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala index ec1110b30f8..b0200c1d9da 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/testservices/TestApiClient.scala @@ -49,18 +49,14 @@ final case class TestApiClient( sendRequest(f(request)) } - private def sendRequest[A](request: Request[Either[String, A]], user: User): Task[Response[Either[String, A]]] = - sendRequest(request, Some(user)) - private def sendRequest[A]( request: Request[Either[String, A]], user: Option[User] = None, ): Task[Response[Either[String, A]]] = - addAuthIfNeeded(user, request).flatMap { req => - req -// .copy(options = req.options.copy(readTimeout = 5.seconds.asScala)) - .send(backend) - } + (user match { + case Some(u) => jwtFor(u).map(jwt => request.auth.bearer(jwt)) + case None => ZIO.succeed(request) + }).flatMap(_.send(backend)) def deleteJsonLd( relativeUri: Uri, @@ -134,11 +130,6 @@ final case class TestApiClient( sendRequest(request, user) } - private def addAuthIfNeeded[A](user: Option[User], request: Request[Either[String, A]]) = user match { - case Some(u) => jwtFor(u).map(jwt => request.auth.bearer(jwt)) - case None => ZIO.succeed(request) - } - def postJson[A: JsonDecoder, B: JsonEncoder]( relativeUri: Uri, body: B, @@ -171,7 +162,7 @@ final case class TestApiClient( .body(jsonLdBody) .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asString) - sendRequest(request, user) + sendRequest(request, Some(user)) } def postJsonLdDocument( @@ -199,7 +190,7 @@ final case class TestApiClient( .post(relativeUri) .multipartBody(body) .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - sendRequest(request, user) + sendRequest(request, Some(user)) } def patchJsonLdDocument( @@ -228,7 +219,7 @@ final case class TestApiClient( .body(body.toJson) .contentType(MediaType.ApplicationJson) .response(asJsonAlways[A].mapLeft((e: DeserializationException) => e.body)) - sendRequest(request, user) + sendRequest(request, Some(user)) } def putJsonLd( @@ -241,7 +232,7 @@ final case class TestApiClient( .body(jsonLdBody) .contentType(MediaType.unsafeApply("application", "ld+json")) .response(asString) - sendRequest(request, user) + sendRequest(request, Some(user)) } def putJsonLdDocument( From be78c6f50144c221edd4c1853c5fc524fc0ba703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 09:41:58 +0200 Subject: [PATCH 93/99] rm unused jsonLogger() --- webapi/src/main/scala/org/knora/webapi/util/Logger.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala index e756952cc18..c9181267351 100644 --- a/webapi/src/main/scala/org/knora/webapi/util/Logger.scala +++ b/webapi/src/main/scala/org/knora/webapi/util/Logger.scala @@ -52,7 +52,4 @@ object Logger { private val logger: ULayer[Unit] = if (useJsonLogger) jsonLogger else textLogger def fromEnv(): ULayer[Unit] = Runtime.removeDefaultLoggers >>> logger >+> Slf4jBridge.initialize - - def json(): ULayer[Unit] = Runtime.removeDefaultLoggers >>> jsonLogger >+> Slf4jBridge.initialize - } From bf9411f25a6c49a69873bbd879c19375f3c86684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 10:01:23 +0200 Subject: [PATCH 94/99] Mark tests as flaky --- .../admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala | 3 ++- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala index 4655763717d..3c7bc09ede1 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala @@ -7,6 +7,7 @@ package org.knora.webapi.slice.admin.api import sttp.client4.UriContext import sttp.model.StatusCode +import zio.* import zio.test.* import org.knora.webapi.* @@ -61,7 +62,7 @@ object AdminUsersGroupMembershipsEndpointsE2ESpec extends E2EZSpec { ) .flatMap(_.assert200) } yield assertTrue(membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal)) - }, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, test("remove user from group") { for { membershipsBeforeResponse <- diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 6c3fa14f488..09fc16dba09 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -105,7 +105,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { projectMemberships <- getProjectMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(projectMemberships.projects == Seq.empty) - }, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, ), suite("used to query project admin group memberships")( test("return all projects the user is a member of the project admin group") { From ea0aff680421ac1e6f8598ba1db8c945aeb5b37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 11:07:49 +0200 Subject: [PATCH 95/99] mark test as flaky --- modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala index 4b1e899da5b..4ef69ee8a5b 100644 --- a/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala +++ b/modules/testkit/src/main/scala/org/knora/webapi/E2EZSpec.scala @@ -66,7 +66,7 @@ abstract class E2EZSpec extends ZIOSpec[E2EZSpec.Environment] { def e2eSpec: Spec[env, Any] final override def spec: Spec[env, Any] = - e2eSpec.provideSomeAuto(Scope.default) + e2eSpec @@ TestAspect.beforeAll(prepare) @@ TestAspect.sequential @@ TestAspect.withLiveEnvironment From cb49db251015a7f05fed84b081f376fb16ee78c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 11:08:01 +0200 Subject: [PATCH 96/99] mark test as flaky --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 09fc16dba09..4e1bef6cefe 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -149,7 +149,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(adminMemberships.projects == Seq.empty) - }, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky,, test("remove user from project which also removes them from project admin group") { for { newUser <- createNewUser From 73c87ee5c65e8706bc584debd08d5d9b3bb0338c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 11:15:31 +0200 Subject: [PATCH 97/99] fmt --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 4e1bef6cefe..4e3d27bbcb5 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -149,7 +149,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { adminMemberships <- getProjectAdminMemberships(newUser.userIri).flatMap(_.assert200) } yield assertTrue(adminMemberships.projects == Seq.empty) - } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky,, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, test("remove user from project which also removes them from project admin group") { for { newUser <- createNewUser From b4ee6a0c5d39a1d29f601b1f11ac5d11b8ef3ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 11:30:34 +0200 Subject: [PATCH 98/99] mark tests as flaky --- .../api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala index 4e3d27bbcb5..f31180700b4 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersProjectMemberShipsEndpointsE2ESpec.scala @@ -160,7 +160,7 @@ object AdminUsersProjectMemberShipsEndpointsE2ESpec extends E2EZSpec { adminMemberships <- getProjectAdminMemberships(normalUser.userIri).flatMap(_.assert200) } yield assertTrue(adminMemberships.projects == Seq.empty) - }, + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, ), ) From 0bbe546b7327017fcf18e5baa1b08fa9462170a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 11:57:43 +0200 Subject: [PATCH 99/99] mark as flaky and refactor for readability --- ...sersGroupMembershipsEndpointsE2ESpec.scala | 98 +++++++++++-------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala index 3c7bc09ede1..436f596212b 100644 --- a/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala +++ b/modules/test-e2e/src/test/scala/org/knora/webapi/slice/admin/api/AdminUsersGroupMembershipsEndpointsE2ESpec.scala @@ -6,7 +6,6 @@ package org.knora.webapi.slice.admin.api import sttp.client4.UriContext -import sttp.model.StatusCode import zio.* import zio.test.* @@ -15,8 +14,11 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserGroupMembersh import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.* import org.knora.webapi.sharedtestdata.SharedTestDataADM2 +import org.knora.webapi.slice.admin.api.UsersEndpoints.Requests.UserCreateRequest +import org.knora.webapi.slice.admin.api.model.UserDto import org.knora.webapi.slice.admin.api.service.UserRestService.UserResponse import org.knora.webapi.slice.admin.domain.model.* +import org.knora.webapi.slice.common.domain.LanguageCode.DE import org.knora.webapi.testservices.ResponseOps import org.knora.webapi.testservices.ResponseOps.assert200 import org.knora.webapi.testservices.TestApiClient @@ -25,6 +27,25 @@ object AdminUsersGroupMembershipsEndpointsE2ESpec extends E2EZSpec { private val multiUserIri = UserIri.unsafeFrom(SharedTestDataADM2.multiuserUser.userData.user_id.get) + private def createNewUser = + val firstName = faker.name().firstName() + val lastName = faker.name().lastName() + val req = UserCreateRequest( + id = None, + Username.unsafeFrom(s"$firstName.${faker.number().positive()}"), + Email.unsafeFrom(s"$firstName.$lastName@example.org"), + GivenName.unsafeFrom(firstName), + FamilyName.unsafeFrom(lastName), + Password.unsafeFrom(faker.credentials().password(8, 16)), + UserStatus.Active, + DE, + SystemAdmin.IsNotSystemAdmin, + ) + TestApiClient + .postJson[UserResponse, UserCreateRequest](uri"/admin/users", req, rootUser) + .flatMap(_.assert200) + .map(_.user) + override val e2eSpec = suite( "The Users Route ('admin/users/iri/:userIri/group-member-ships') ", )( @@ -43,49 +64,44 @@ object AdminUsersGroupMembershipsEndpointsE2ESpec extends E2EZSpec { suite("used to modify group membership")( test("add user to group") { for { - _ <- TestApiClient - .getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - .flatMap(_.assert200) - .filterOrFail(_.groups.isEmpty)(IllegalStateException("User is already member of a group")) - response <- TestApiClient.postJson[UserResponse, String]( - uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", - "", - rootUser, - ) - membershipsAfterResult <- TestApiClient - .getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - .flatMap(_.assert200) - } yield assertTrue(membershipsAfterResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal)) + newUser <- createNewUser + _ <- addUserToGroup(newUser, imagesReviewerGroupExternal) + actualMemberships <- getGroupMemberships(newUser) + } yield assertTrue(actualMemberships.groups == Seq(imagesReviewerGroupExternal)) } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, test("remove user from group") { for { - membershipsBeforeResponse <- - TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - membershipsBeforeResult <- membershipsBeforeResponse.assert200 - response <- TestApiClient.deleteJson[UserResponse]( - uri"/admin/users/iri/${normalUser.id}/group-memberships/${imagesReviewerGroup.groupIri}", - rootUser, - ) - membershipsAfterResponse <- TestApiClient.getJson[UserGroupMembershipsGetResponseADM]( - uri"/admin/users/iri/${normalUser.id}/group-memberships", - rootUser, - ) - membershipsAfterResult <- membershipsAfterResponse.assert200 - } yield assertTrue( - membershipsBeforeResult.groups == Seq(SharedTestDataADM.imagesReviewerGroupExternal), - response.code == StatusCode.Ok, - membershipsAfterResult.groups == Seq.empty[Group], - ) - }, + newUser <- createNewUser + _ <- addUserToGroup(newUser, imagesReviewerGroupExternal) + _ <- removeUserFromGroup(newUser, imagesReviewerGroupExternal) + actualMemberships <- getGroupMemberships(newUser) + } yield assertTrue(actualMemberships.groups == Seq.empty) + } @@ TestAspect.timeout(5.seconds) @@ TestAspect.flaky, ), ) + + private def removeUserFromGroup(user: UserDto, group: Group) = + TestApiClient + .deleteJson[UserResponse]( + uri"/admin/users/iri/${user.id}/group-memberships/${group.groupIri}", + rootUser, + ) + .flatMap(_.assert200) + + private def getGroupMemberships(user: UserDto) = + TestApiClient + .getJson[UserGroupMembershipsGetResponseADM]( + uri"/admin/users/iri/${user.id}/group-memberships", + rootUser, + ) + .flatMap(_.assert200) + + private def addUserToGroup(user: UserDto, group: Group) = + TestApiClient + .postJson[UserResponse, IRI]( + uri"/admin/users/iri/${user.id}/group-memberships/${group.groupIri}", + "", + rootUser, + ) + .flatMap(_.assert200) }