diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala index c7add548b7..62bd6b257a 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderSpec.scala @@ -6,11 +6,13 @@ package org.knora.webapi.responders.admin import org.apache.pekko.testkit.* +import zio.ZIO import java.util.UUID import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException +import dsp.errors.NotFoundException import dsp.errors.UpdateNotPerformedException import dsp.valueobjects.Iri import org.knora.webapi.* @@ -26,6 +28,7 @@ import org.knora.webapi.slice.admin.api.Requests.ListChangeRequest import org.knora.webapi.slice.admin.api.Requests.ListCreateChildNodeRequest import org.knora.webapi.slice.admin.api.Requests.ListCreateRootNodeRequest 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.* import org.knora.webapi.util.MutableTestIri import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA @@ -54,20 +57,46 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { private val treeListChildNodes: Seq[ListNodeADM] = SharedListsTestDataADM.treeListChildNodes + private val listsResponder = ZIO.serviceWithZIO[ListsResponder] + "The Lists Responder" when { "used to query information about lists" should { "return all lists" in { - val actual = UnsafeZioRun.runOrThrow(ListsResponder.getLists(None)) + val actual = UnsafeZioRun.runOrThrow(listsResponder(_.getLists(None))) actual.lists.size should be(9) } - "return all lists belonging to the images project" in { - val actual = UnsafeZioRun.runOrThrow(ListsResponder.getLists(Some(ProjectIri.unsafeFrom(imagesProjectIri)))) + "return all lists belonging to the images project by iri" in { + val actual = + UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom(imagesProjectIri)))))) + actual.lists.size should be(4) + } + + "return all lists belonging to the images project by shortcode" in { + val actual = + UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Right(imagesProjectShortcode))))) actual.lists.size should be(4) } + "getLists should fail if project by iri was not found" in { + val exit = + UnsafeZioRun.run( + listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom("http://rdfh.ch/projects/unknown"))))), + ) + assertFailsWithA[NotFoundException](exit) + } + + "getLists should fail if project by shortcode was not found" in { + val exit = + UnsafeZioRun.run( + listsResponder(_.getLists(Some(Right(Shortcode.unsafeFrom("9999"))))), + ) + assertFailsWithA[NotFoundException](exit) + } + "return all lists belonging to the anything project" in { - val actual = UnsafeZioRun.runOrThrow(ListsResponder.getLists(Some(ProjectIri.unsafeFrom(anythingProjectIri)))) + val actual = + UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom(anythingProjectIri)))))) actual.lists.size should be(4) } diff --git a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM2.scala b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM2.scala index 049fc5b275..934d9dc9fa 100644 --- a/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM2.scala +++ b/integration/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM2.scala @@ -10,6 +10,7 @@ import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionA import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM import org.knora.webapi.sharedtestdata import org.knora.webapi.sharedtestdata.SharedOntologyTestDataADM.IMAGES_ONTOLOGY_IRI +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.Permission import org.knora.webapi.slice.admin.domain.service.KnoraGroupRepo @@ -88,7 +89,8 @@ object SharedTestDataADM2 { /** * ********************************** */ - val imagesProjectIri = "http://rdfh.ch/projects/00FF" + val imagesProjectIri = "http://rdfh.ch/projects/00FF" + val imagesProjectShortcode = Shortcode.unsafeFrom("00FF") /* represents 'user01' as found in admin-data.ttl */ def imagesUser01 = sharedtestdata.UserProfile( 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 d08a69a52a..51f170557a 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 @@ -29,6 +29,7 @@ import org.knora.webapi.responders.admin.ListsResponder.Queries 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 import org.knora.webapi.slice.admin.domain.model.ListProperties.ListName import org.knora.webapi.slice.admin.domain.model.User @@ -63,10 +64,23 @@ final case class ListsResponder( * [[None]] if all lists are to be queried. * @return a [[ListsGetResponseADM]]. */ - def getLists(projectIri: Option[ProjectIri]): Task[ListsGetResponseADM] = + def getLists(iriShortcode: Option[Either[ProjectIri, Shortcode]]): Task[ListsGetResponseADM] = for { + project <- iriShortcode match { + case Some(Left(iri)) => + knoraProjectService + .findById(iri) + .someOrFail(NotFoundException(s"Project with IRI '$iri' not found")) + .asSome + case Some(Right(code)) => + knoraProjectService + .findByShortcode(code) + .someOrFail(NotFoundException(s"Project with shortcode '$code' not found")) + .asSome + case None => ZIO.none + } statements <- - triplestore.query(Construct(Queries.getListsQuery(projectIri))).flatMap(_.asExtended).map(_.statements) + triplestore.query(Construct(Queries.getListsQuery(project.map(_.id)))).flatMap(_.asExtended).map(_.statements) lists <- ZIO.foreach(statements.toList) { case (listIri: SubjectV2, objs: ConstructPredicateObjects) => for { @@ -1531,9 +1545,6 @@ object ListsResponder { |}""".stripMargin } - def getLists(projectIri: Option[ProjectIri]): ZIO[ListsResponder, Throwable, ListsGetResponseADM] = - ZIO.serviceWithZIO[ListsResponder](_.getLists(projectIri)) - def listGetRequestADM(nodeIri: IRI): ZIO[ListsResponder, Throwable, ListItemGetResponseADM] = ZIO.serviceWithZIO[ListsResponder](_.listGetRequestADM(nodeIri)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala index 79eaa68529..a91ce4f195 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala @@ -22,6 +22,7 @@ import org.knora.webapi.slice.admin.api.Requests.ListCreateChildNodeRequest import org.knora.webapi.slice.admin.api.Requests.ListCreateRootNodeRequest import org.knora.webapi.slice.admin.api.model.AdminQueryVariables 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.* import org.knora.webapi.slice.common.api.BaseEndpoints @@ -32,9 +33,12 @@ case class ListsEndpoints(baseEndpoints: BaseEndpoints) { val getListsQueryByProjectIriOption = baseEndpoints.publicEndpoint.get .in(base) - .in(AdminQueryVariables.projectIriOption) + .in(AdminQueryVariables.projectIriOrShortcodeQueryOption) .out(jsonBody[ListsGetResponseADM].description("Contains the list of all root nodes of each found list.")) - .description("Get all lists or all lists belonging to a project.") + .description( + "Get all lists or all lists belonging to a project. " + + "Note that you can provide either a project IRI or a project shortcode.", + ) private val listIriPathVar = path[ListIri].description("The IRI of the list.") val getListsByIri = baseEndpoints.publicEndpoint.get diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala index 372934a008..47308c2460 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ListsEndpointsHandlers.scala @@ -18,7 +18,9 @@ import org.knora.webapi.messages.admin.responder.listsmessages.NodeInfoGetRespon 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 import org.knora.webapi.slice.admin.domain.model.User import org.knora.webapi.slice.common.api.HandlerMapper @@ -34,7 +36,7 @@ final case class ListsEndpointsHandlers( private val getListsQueryByProjectIriHandler = PublicEndpointHandler( listsEndpoints.getListsQueryByProjectIriOption, - (iri: Option[ProjectIri]) => listsResponder.getLists(iri), + (iriShortcode: Option[Either[ProjectIri, Shortcode]]) => listsResponder.getLists(iriShortcode), ) private val getListsByIriHandler = PublicEndpointHandler( diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala index 800d18f308..5cd3c28d6d 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/AdminQueryVariables.scala @@ -7,18 +7,46 @@ package org.knora.webapi.slice.admin.api.model import sttp.tapir.Codec import sttp.tapir.CodecFormat +import sttp.tapir.DecodeResult import sttp.tapir.EndpointInput import sttp.tapir.query +import dsp.errors.BadRequestException 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.KnoraProject.Shortcode object AdminQueryVariables { private implicit val projectIriOptionCodec: Codec[List[String], Option[ProjectIri], CodecFormat.TextPlain] = Codec.listHeadOption(TapirCodec.projectIri) + private implicit val shortcodeOptionCodec: Codec[List[String], Option[Shortcode], CodecFormat.TextPlain] = + Codec.listHeadOption(TapirCodec.shortcode) + val projectIriOption: EndpointInput.Query[Option[ProjectIri]] = query[Option[ProjectIri]]("projectIri") .description("The (optional) IRI of the project.") .example(Some(ProjectIri.unsafeFrom("http://rdfh.ch/projects/0042"))) + + val projectShortcodeOption: EndpointInput.Query[Option[Shortcode]] = query[Option[Shortcode]]("projectShortcode") + .description("The (optional) shortcode of the project.") + .example(Some(Shortcode.unsafeFrom("0042"))) + + val projectIriOrShortcodeQueryOption = + projectIriOption + .and(projectShortcodeOption) + .mapDecode[Option[Either[ProjectIri, Shortcode]]] { + case (Some(iri), None) => DecodeResult.Value(Some(Left(iri))) + case (None, Some(shortcode)) => DecodeResult.Value(Some(Right(shortcode))) + case (Some(_), Some(_)) => + DecodeResult.Error( + "Query params project IRI and shortcode are mutually exclusive", + BadRequestException("Provide either a project IRI or a project shortcode"), + ) + case _ => DecodeResult.Value(None) + } { + case Some(Left(iri)) => (Some(iri), None) + case Some(Right(shortcode)) => (None, Some(shortcode)); + case None => (None, None) + } }