From 250ea6b90d030981933381a26d4b18e42dee26db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 11:02:04 +0200 Subject: [PATCH 1/6] feat: Add shortcode query param for `GET /admin/lists` --- .../responders/admin/ListsResponderSpec.scala | 11 ++++++-- .../responders/admin/ListsResponder.scala | 22 +++++++++++---- .../slice/admin/api/ListsEndpoints.scala | 9 ++++-- .../admin/api/ListsEndpointsHandlers.scala | 4 ++- .../admin/api/model/AdminQueryVariables.scala | 28 +++++++++++++++++++ 5 files changed, 62 insertions(+), 12 deletions(-) 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..01e69eeb20 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,6 +6,7 @@ package org.knora.webapi.responders.admin import org.apache.pekko.testkit.* +import zio.ZIO import java.util.UUID @@ -54,20 +55,24 @@ 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)))) + val actual = + UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom(imagesProjectIri)))))) actual.lists.size should be(4) } "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/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..b9bacf060a 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 @@ -10,7 +10,6 @@ import zio.ZIO import zio.ZLayer import java.util.UUID - import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.Iri.* @@ -29,6 +28,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 +63,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 +1544,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..b5841a8f59 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 @@ -12,6 +12,7 @@ import zio.ZLayer import zio.json.DeriveJsonCodec import zio.json.JsonCodec +import dsp.errors.BadRequestException import org.knora.webapi.messages.admin.responder.listsmessages.* import org.knora.webapi.slice.admin.api.Requests.ListChangeCommentsRequest import org.knora.webapi.slice.admin.api.Requests.ListChangeLabelsRequest @@ -22,6 +23,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 +34,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) + } } From 29f9263ba2de62a2fd905faf8f32a43d01f2ea68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 11:08:23 +0200 Subject: [PATCH 2/6] add test --- .../webapi/responders/admin/ListsResponderSpec.scala | 9 +++++++-- .../knora/webapi/sharedtestdata/SharedTestDataADM2.scala | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) 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 01e69eeb20..2045ada9e7 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 @@ -9,7 +9,6 @@ import org.apache.pekko.testkit.* import zio.ZIO import java.util.UUID - import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException import dsp.errors.UpdateNotPerformedException @@ -64,12 +63,18 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { actual.lists.size should be(9) } - "return all lists belonging to the images project" in { + "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) + } + "return all lists belonging to the anything project" in { val actual = UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom(anythingProjectIri)))))) 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( From 67b42cf470577bb469cd66b73ff9c96bd5c993b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 11:12:24 +0200 Subject: [PATCH 3/6] fmt --- .../org/knora/webapi/responders/admin/ListsResponderSpec.scala | 1 + .../scala/org/knora/webapi/slice/admin/api/ListsEndpoints.scala | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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 2045ada9e7..78f6728c68 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 @@ -9,6 +9,7 @@ import org.apache.pekko.testkit.* import zio.ZIO import java.util.UUID + import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException import dsp.errors.UpdateNotPerformedException 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 b5841a8f59..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 @@ -12,7 +12,6 @@ import zio.ZLayer import zio.json.DeriveJsonCodec import zio.json.JsonCodec -import dsp.errors.BadRequestException import org.knora.webapi.messages.admin.responder.listsmessages.* import org.knora.webapi.slice.admin.api.Requests.ListChangeCommentsRequest import org.knora.webapi.slice.admin.api.Requests.ListChangeLabelsRequest From c5bfd23b4d55fc51626113cadeb6be23d0d00e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 11:22:04 +0200 Subject: [PATCH 4/6] add tests --- .../responders/admin/ListsResponderSpec.scala | 20 +++++++++++++++++-- .../responders/admin/ListsResponder.scala | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) 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 78f6728c68..4d59f06eb2 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 @@ -5,11 +5,11 @@ package org.knora.webapi.responders.admin +import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA import org.apache.pekko.testkit.* import zio.ZIO import java.util.UUID - import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException import dsp.errors.UpdateNotPerformedException @@ -27,9 +27,9 @@ 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 /** * Tests [[ListsResponder]]. @@ -76,6 +76,22 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { 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[BadRequestException](exit) + } + + "getLists should fail if project by shortcode was not found" in { + val exit = + UnsafeZioRun.run( + listsResponder(_.getLists(Some(Right(Shortcode.unsafeFrom("FFFF"))))), + ) + assertFailsWithA[BadRequestException](exit) + } + "return all lists belonging to the anything project" in { val actual = UnsafeZioRun.runOrThrow(listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom(anythingProjectIri)))))) 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 b9bacf060a..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 @@ -10,6 +10,7 @@ import zio.ZIO import zio.ZLayer import java.util.UUID + import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.Iri.* From d6c83de60fd66f1174980d63f4d3bf88afecf0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 16:22:35 +0200 Subject: [PATCH 5/6] fix tests --- .../knora/webapi/responders/admin/ListsResponderSpec.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 4d59f06eb2..5c5b951056 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 @@ -12,6 +12,7 @@ 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.* @@ -81,15 +82,15 @@ class ListsResponderSpec extends CoreSpec with ImplicitSender { UnsafeZioRun.run( listsResponder(_.getLists(Some(Left(ProjectIri.unsafeFrom("http://rdfh.ch/projects/unknown"))))), ) - assertFailsWithA[BadRequestException](exit) + assertFailsWithA[NotFoundException](exit) } "getLists should fail if project by shortcode was not found" in { val exit = UnsafeZioRun.run( - listsResponder(_.getLists(Some(Right(Shortcode.unsafeFrom("FFFF"))))), + listsResponder(_.getLists(Some(Right(Shortcode.unsafeFrom("9999"))))), ) - assertFailsWithA[BadRequestException](exit) + assertFailsWithA[NotFoundException](exit) } "return all lists belonging to the anything project" in { From 6bc337019fac42b51d935e95e63863233b0e1e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Sep 2024 16:25:39 +0200 Subject: [PATCH 6/6] fmt --- .../org/knora/webapi/responders/admin/ListsResponderSpec.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 5c5b951056..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 @@ -5,11 +5,11 @@ package org.knora.webapi.responders.admin -import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA import org.apache.pekko.testkit.* import zio.ZIO import java.util.UUID + import dsp.errors.BadRequestException import dsp.errors.DuplicateValueException import dsp.errors.NotFoundException @@ -31,6 +31,7 @@ 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 /** * Tests [[ListsResponder]].