Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a multi-fetch operation #4132

Merged
merged 6 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.directives.UriDirectives.baseUriPrefix
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.MultiFetch
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchRequest
import monix.execution.Scheduler

/**
* Route allowing to fetch multiple resources in a single request
*/
class MultiFetchRoutes(
identities: Identities,
aclCheck: AclCheck,
multiFetch: MultiFetch
)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering,
s: Scheduler
) extends AuthDirectives(identities, aclCheck)
with CirceUnmarshalling
with RdfMarshalling {

def routes: Route =
baseUriPrefix(baseUri.prefix) {
pathPrefix("multi-fetch") {
pathPrefix("resources") {
extractCaller { implicit caller =>
(get & entity(as[MultiFetchRequest])) { request =>
emit(multiFetch(request).flatMap(_.asJson))
}
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class
include(ResolversModule)
include(SchemasModule)
include(ResourcesModule)
include(MultiFetchModule)
include(IdentitiesModule)
include(VersionModule)
include(QuotasModule)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ch.epfl.bluebrain.nexus.delta.wiring

import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.routes.MultiFetchRoutes
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.MultiFetch
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchRequest
import ch.epfl.bluebrain.nexus.delta.sdk.{PriorityRoute, ResourceShifts}
import distage.ModuleDef
import izumi.distage.model.definition.Id
import monix.execution.Scheduler

object MultiFetchModule extends ModuleDef {

make[MultiFetch].from {
(
aclCheck: AclCheck,
shifts: ResourceShifts
) =>
MultiFetch(
aclCheck,
(input: MultiFetchRequest.Input) => shifts.fetch(input.id, input.project)
)
}

make[MultiFetchRoutes].from {
(
identities: Identities,
aclCheck: AclCheck,
multiFetch: MultiFetch,
baseUri: BaseUri,
rcr: RemoteContextResolution @Id("aggregate"),
jko: JsonKeyOrdering,
sc: Scheduler
) =>
new MultiFetchRoutes(identities, aclCheck, multiFetch)(baseUri, rcr, jko, sc)
}

many[PriorityRoute].add { (route: MultiFetchRoutes) =>
PriorityRoute(pluginsMaxPriority + 13, route.routes, requiresStrictEntity = true)
}

}
32 changes: 32 additions & 0 deletions delta/app/src/test/resources/multi-fetch/all-unauthorized.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"format": "compacted",
"resources": [
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"error": {
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj1"
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/not-found",
"error": {
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj1"
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/unauthorized",
"error": {
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj2"
}
]
}
52 changes: 52 additions & 0 deletions delta/app/src/test/resources/multi-fetch/compacted-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"format": "compacted",
"resources": [
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"project": "org/proj1",
"value": {
"@context": [
{
"@vocab": "https://bluebrain.github.io/nexus/vocabulary/"
},
"https://bluebrain.github.io/nexus/contexts/metadata.json"
],
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"@type": "Custom",
"bool": false,
"name": "Alex",
"number": 24,
"_constrainedBy": "https://bluebrain.github.io/nexus/schemas/unconstrained.json",
"_createdAt": "1970-01-01T00:00:00Z",
"_createdBy": "http://localhost/v1/anonymous",
"_deprecated": false,
"_incoming": "http://localhost/v1/resources/org/proj1/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fschemas%2Funconstrained.json/success/incoming",
"_outgoing": "http://localhost/v1/resources/org/proj1/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fschemas%2Funconstrained.json/success/outgoing",
"_project": "http://localhost/v1/projects/org/proj1",
"_rev": 1,
"_schemaProject": "http://localhost/v1/projects/org/proj1",
"_self": "http://localhost/v1/resources/org/proj1/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fschemas%2Funconstrained.json/success",
"_updatedAt": "1970-01-01T00:00:00Z",
"_updatedBy": "http://localhost/v1/anonymous"
}
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/not-found",
"error": {
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "NotFound",
"reason": "The resource 'https://bluebrain.github.io/nexus/vocabulary/not-found' was not found in project 'org/proj1'."
},
"project": "org/proj1"
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/unauthorized",
"error": {
"@context": "https://bluebrain.github.io/nexus/contexts/error.json",
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj2"
}
]
}
35 changes: 35 additions & 0 deletions delta/app/src/test/resources/multi-fetch/source-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"format": "source",
"resources": [
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"project": "org/proj1",
"value": {
"@context": {
"@vocab": "https://bluebrain.github.io/nexus/vocabulary/"
},
"@id": "https://bluebrain.github.io/nexus/vocabulary/success",
"@type": "Custom",
"bool": false,
"name": "Alex",
"number": 24
}
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/not-found",
"error": {
"@type": "NotFound",
"reason": "The resource 'https://bluebrain.github.io/nexus/vocabulary/not-found' was not found in project 'org/proj1'."
},
"project": "org/proj1"
},
{
"@id": "https://bluebrain.github.io/nexus/vocabulary/unauthorized",
"error": {
"@type": "AuthorizationFailed",
"reason": "The supplied authentication is not authorized to access this resource."
},
"project": "org/proj2"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.server.Route
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck
import ch.epfl.bluebrain.nexus.delta.sdk.generators.ResourceGen
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.MultiFetch
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchRequest
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.Latest
import monix.bio.UIO

class MultiFetchRoutesSpec extends BaseRouteSpec {

implicit private val caller: Caller =
Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm)))

private val asAlice = addCredentials(OAuth2BearerToken("alice"))

private val identities = IdentitiesDummy(caller)

private val project1 = ProjectRef.unsafe("org", "proj1")
private val project2 = ProjectRef.unsafe("org", "proj2")

private val permissions = Set(Permissions.resources.read)
private val aclCheck = AclSimpleCheck((alice, project1, permissions)).runSyncUnsafe()

private val successId = nxv + "success"
private val successContent =
ResourceGen.jsonLdContent(successId, project1, jsonContentOf("resources/resource.json", "id" -> successId))

private val notFoundId = nxv + "not-found"
private val unauthorizedId = nxv + "unauthorized"

private def fetchResource =
(input: MultiFetchRequest.Input) => {
input match {
case MultiFetchRequest.Input(Latest(`successId`), `project1`) =>
UIO.some(successContent)
case _ => UIO.none
}
}

private val multiFetch = MultiFetch(
aclCheck,
fetchResource
)

private val routes = Route.seal(
new MultiFetchRoutes(identities, aclCheck, multiFetch).routes
)

"The Multi fetch route" should {

val endpoint = "/v1/multi-fetch/resources"

def request(format: ResourceRepresentation) =
json"""
{
"format": "$format",
"resources": [
{ "id": "$successId", "project": "$project1" },
{ "id": "$notFoundId", "project": "$project1" },
{ "id": "$unauthorizedId", "project": "$project2" }
]
}"""

"return unauthorised results for a user with no access" in {
val entity = request(ResourceRepresentation.CompactedJsonLd).toEntity
Get(endpoint, entity) ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf("multi-fetch/all-unauthorized.json")
}
}

"return expected results as compacted json-ld for a user with limited access" in {
val entity = request(ResourceRepresentation.CompactedJsonLd).toEntity
Get(endpoint, entity) ~> asAlice ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf("multi-fetch/compacted-response.json")
}
}

"return expected results as original payloads for a user with limited access" in {
val entity = request(ResourceRepresentation.SourceJson).toEntity
Get(endpoint, entity) ~> asAlice ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf("multi-fetch/source-response.json")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import akka.util.ByteString
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveReference.{FileReference, FileSelfReference, ResourceReference}
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveRejection._
import ch.epfl.bluebrain.nexus.delta.plugins.archive.model.ArchiveResourceRepresentation._
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceRepresentation._
Copy link
Contributor Author

@imsdu imsdu Aug 4, 2023

Choose a reason for hiding this comment

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

I used the same way to chose the output format as archives so I reused (moved and renamed) the class responsible for it

import ch.epfl.bluebrain.nexus.delta.plugins.archive.model._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection
import ch.epfl.bluebrain.nexus.delta.rdf.RdfError
Expand All @@ -21,7 +21,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.Response.Complete
import ch.epfl.bluebrain.nexus.delta.sdk.error.SDKError
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources
import ch.epfl.bluebrain.nexus.delta.sdk.stream.StreamConverter
import ch.epfl.bluebrain.nexus.delta.sdk.{AkkaSource, JsonLdValue}
Expand Down Expand Up @@ -249,7 +249,7 @@ object ArchiveDownload {

private def valueToByteString[A](
value: JsonLdContent[A, _],
repr: ArchiveResourceRepresentation
repr: ResourceRepresentation
): IO[RdfError, ByteString] = {
implicit val encoder: JsonLdEncoder[A] = value.encoder
repr match {
Expand Down
Loading