Skip to content

Commit

Permalink
Add a multi-fetch operation
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Dumas committed Aug 4, 2023
1 parent 3e128a4 commit 52aec95
Show file tree
Hide file tree
Showing 34 changed files with 1,083 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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

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))
}
}
}
}
}

}

object MultiFetchRoutes {

/**
* @return
* the [[Route]] for the multi-fetch operation
*/
def apply(identities: Identities, aclCheck: AclCheck, multiFetch: MultiFetch)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering,
s: Scheduler
): Route = new MultiFetchRoutes(identities, aclCheck, multiFetch).routes

}
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(
MultiFetchRoutes(identities, aclCheck, multiFetch)
)

"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._
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

0 comments on commit 52aec95

Please sign in to comment.