diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala index 7031e7f0a5..1b05fb42a6 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResolversRoutes.scala @@ -10,6 +10,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering +import ch.epfl.bluebrain.nexus.delta.routes.ResolutionType.{AllResolversInProject, SingleResolver} import ch.epfl.bluebrain.nexus.delta.sdk._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._ @@ -87,20 +88,21 @@ final class ResolversRoutes( (baseUriPrefix(baseUri.prefix) & replaceUri("resolvers", schemas.resolvers)) { pathPrefix("resolvers") { extractCaller { implicit caller => - (resolveProjectRef & indexingMode) { (ref, mode) => - def index(resolver: ResolverResource): IO[Unit] = indexAction(resolver.value.project, resolver, mode) - val authorizeRead = authorizeFor(ref, Read) - val authorizeWrite = authorizeFor(ref, Write) + (resolveProjectRef & indexingMode) { (project, indexingMode) => + def index(resolver: ResolverResource): IO[Unit] = + indexAction(resolver.value.project, resolver, indexingMode) + val authorizeRead = authorizeFor(project, Read) + val authorizeWrite = authorizeFor(project, Write) concat( pathEndOrSingleSlash { // Create a resolver without an id segment (post & noParameter("rev") & entity(as[Json])) { payload => authorizeWrite { - emitMetadata(Created, resolvers.create(ref, payload).flatTap(index)) + emitMetadata(Created, resolvers.create(project, payload).flatTap(index)) } } }, - idSegment { id => + idSegment { resolver => concat( pathEndOrSingleSlash { concat( @@ -109,55 +111,72 @@ final class ResolversRoutes( (parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[Json])) { case (None, payload) => // Create a resolver with an id segment - emitMetadata(Created, resolvers.create(id, ref, payload).flatTap(index)) + emitMetadata(Created, resolvers.create(resolver, project, payload).flatTap(index)) case (Some(rev), payload) => // Update a resolver - emitMetadata(resolvers.update(id, ref, rev, payload).flatTap(index)) + emitMetadata(resolvers.update(resolver, project, rev, payload).flatTap(index)) } } }, (delete & parameter("rev".as[Int])) { rev => authorizeWrite { // Deprecate a resolver - emitMetadataOrReject(resolvers.deprecate(id, ref, rev).flatTap(index)) + emitMetadataOrReject(resolvers.deprecate(resolver, project, rev).flatTap(index)) } }, // Fetches a resolver - (get & idSegmentRef(id)) { id => + (get & idSegmentRef(resolver)) { resolverRef => emitOrFusionRedirect( - ref, - id, + project, + resolverRef, authorizeRead { - emitFetch(resolvers.fetch(id, ref)) + emitFetch(resolvers.fetch(resolverRef, project)) } ) } ) }, // Fetches a resolver original source - (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & authorizeRead) { id => - emitSource(resolvers.fetch(id, ref)) + (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(resolver) & authorizeRead) { + resolverRef => + emitSource(resolvers.fetch(resolverRef, project)) }, // Tags (pathPrefix("tags") & pathEndOrSingleSlash) { concat( // Fetch a resolver tags - (get & idSegmentRef(id) & authorizeRead) { id => - emitTags(resolvers.fetch(id, ref)) + (get & idSegmentRef(resolver) & authorizeRead) { resolverRef => + emitTags(resolvers.fetch(resolverRef, project)) }, // Tag a resolver (post & parameter("rev".as[Int])) { rev => authorizeWrite { entity(as[Tag]) { case Tag(tagRev, tag) => - emitMetadata(Created, resolvers.tag(id, ref, tag, tagRev, rev).flatTap(index)) + emitMetadata(Created, resolvers.tag(resolver, project, tag, tagRev, rev).flatTap(index)) } } } ) }, // Fetch a resource using a resolver - (idSegmentRef & pathEndOrSingleSlash) { resourceIdRef => - resolve(resourceIdRef, ref, underscoreToOption(id)) + (get & idSegmentRef) { resourceIdRef => + concat( + pathEndOrSingleSlash { + parameter("showReport".as[Boolean].withDefault(default = false)) { showReport => + val outputType = + if (showReport) ResolvedResourceOutputType.Report else ResolvedResourceOutputType.JsonLd + resolveResource(resourceIdRef, project, resolutionType(resolver), outputType) + } + }, + (pathPrefix("source") & pathEndOrSingleSlash) { + resolveResource( + resourceIdRef, + project, + resolutionType(resolver), + ResolvedResourceOutputType.Source + ) + } + ) } ) } @@ -167,23 +186,48 @@ final class ResolversRoutes( } } - private def resolve(resourceSegment: IdSegmentRef, projectRef: ProjectRef, resolverId: Option[IdSegment])(implicit + private def resolveResource( + resource: IdSegmentRef, + project: ProjectRef, + resolutionType: ResolutionType, + output: ResolvedResourceOutputType + )(implicit caller: Caller ): Route = - authorizeFor(projectRef, Permissions.resources.read).apply { - parameter("showReport".as[Boolean].withDefault(default = false)) { showReport => - def emitResult[R: JsonLdEncoder](io: IO[MultiResolutionResult[R]]) = - if (showReport) - emit(io.map(_.report).attemptNarrow[ResolverRejection]) - else - emit(io.map(_.value.jsonLdValue).attemptNarrow[ResolverRejection]) - - resolverId.fold(emitResult(multiResolution(resourceSegment, projectRef))) { resolverId => - emitResult(multiResolution(resourceSegment, projectRef, resolverId)) + authorizeFor(project, Permissions.resources.read).apply { + def emitResult[R: JsonLdEncoder](io: IO[MultiResolutionResult[R]]) = { + output match { + case ResolvedResourceOutputType.Report => emit(io.map(_.report).attemptNarrow[ResolverRejection]) + case ResolvedResourceOutputType.JsonLd => emit(io.map(_.value.jsonLdValue).attemptNarrow[ResolverRejection]) + case ResolvedResourceOutputType.Source => emit(io.map(_.value.source).attemptNarrow[ResolverRejection]) } } + + resolutionType match { + case ResolutionType.AllResolversInProject => emitResult(multiResolution(resource, project)) + case SingleResolver(resolver) => emitResult(multiResolution(resource, project, resolver)) + } + } + + private def resolutionType(segment: IdSegment): ResolutionType = { + underscoreToOption(segment) match { + case Some(resolver) => SingleResolver(resolver) + case None => AllResolversInProject } + } +} + +sealed trait ResolutionType +object ResolutionType { + case object AllResolversInProject extends ResolutionType + case class SingleResolver(id: IdSegment) extends ResolutionType +} +sealed trait ResolvedResourceOutputType +object ResolvedResourceOutputType { + case object Report extends ResolvedResourceOutputType + case object JsonLd extends ResolvedResourceOutputType + case object Source extends ResolvedResourceOutputType } object ResolversRoutes { diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala index 8c6674cb52..16877f1945 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutes.scala @@ -74,17 +74,17 @@ final class ResourcesRoutes( baseUriPrefix(baseUri.prefix) { pathPrefix("resources") { extractCaller { implicit caller => - resolveProjectRef.apply { ref => + resolveProjectRef.apply { project => concat( // Create a resource without schema nor id segment (post & pathEndOrSingleSlash & noParameter("rev") & entity(as[NexusSource]) & indexingMode & tagParam) { (source, mode, tag) => - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { emit( Created, resources - .create(ref, resourceSchema, source.value, tag) - .flatTap(indexUIO(ref, _, mode)) + .create(project, resourceSchema, source.value, tag) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] ) @@ -95,13 +95,13 @@ final class ResourcesRoutes( concat( // Create a resource with schema but without id segment (post & pathEndOrSingleSlash & noParameter("rev") & tagParam) { tag => - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { entity(as[NexusSource]) { source => emit( Created, resources - .create(ref, schema, source.value, tag) - .flatTap(indexUIO(ref, _, mode)) + .create(project, schema, source.value, tag) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -109,21 +109,21 @@ final class ResourcesRoutes( } } }, - idSegment { id => + idSegment { resource => concat( pathEndOrSingleSlash { concat( // Create or update a resource put { - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { (parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[NexusSource]) & tagParam) { case (None, source, tag) => // Create a resource with schema and id segments emit( Created, resources - .create(id, ref, schema, source.value, tag) - .flatTap(indexUIO(ref, _, mode)) + .create(resource, project, schema, source.value, tag) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -132,8 +132,8 @@ final class ResourcesRoutes( // Update a resource emit( resources - .update(id, ref, schemaOpt, rev, source.value, tag) - .flatTap(indexUIO(ref, _, mode)) + .update(resource, project, schemaOpt, rev, source.value, tag) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -143,11 +143,11 @@ final class ResourcesRoutes( }, // Deprecate a resource (delete & parameter("rev".as[Int])) { rev => - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { emit( resources - .deprecate(id, ref, schemaOpt, rev) - .flatTap(indexUIO(ref, _, mode)) + .deprecate(resource, project, schemaOpt, rev) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -155,14 +155,14 @@ final class ResourcesRoutes( } }, // Fetch a resource - (get & idSegmentRef(id) & varyAcceptHeaders) { id => + (get & idSegmentRef(resource) & varyAcceptHeaders) { resourceRef => emitOrFusionRedirect( - ref, - id, - authorizeFor(ref, Read).apply { + project, + resourceRef, + authorizeFor(project, Read).apply { emit( resources - .fetch(id, ref, schemaOpt) + .fetch(resourceRef, project, schemaOpt) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) ) @@ -173,11 +173,11 @@ final class ResourcesRoutes( }, // Undeprecate a resource (pathPrefix("undeprecate") & put & parameter("rev".as[Int])) { rev => - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { emit( resources - .undeprecate(id, ref, schemaOpt, rev) - .flatTap(indexUIO(ref, _, mode)) + .undeprecate(resource, project, schemaOpt, rev) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -185,14 +185,14 @@ final class ResourcesRoutes( } }, (pathPrefix("update-schema") & put & pathEndOrSingleSlash) { - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { emit( IO.fromOption(schemaOpt)(NoSchemaProvided) .flatMap { schema => resources - .updateAttachedSchema(id, ref, schema) - .flatTap(indexUIO(ref, _, mode)) + .updateAttachedSchema(resource, project, schema) + .flatTap(indexUIO(project, _, mode)) } .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -201,12 +201,12 @@ final class ResourcesRoutes( } }, (pathPrefix("refresh") & put & pathEndOrSingleSlash) { - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { emit( OK, resources - .refresh(id, ref, schemaOpt) - .flatTap(indexUIO(ref, _, mode)) + .refresh(resource, project, schemaOpt) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -214,62 +214,65 @@ final class ResourcesRoutes( } }, // Fetch a resource original source - (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & varyAcceptHeaders) { id => - authorizeFor(ref, Read).apply { - parameter("annotate".as[Boolean].withDefault(false)) { annotate => - implicit val source: Printer = sourcePrinter - if (annotate) { - emit( - resources - .fetch(id, ref, schemaOpt) - .flatMap(asSourceWithMetadata) - .attemptNarrow[ResourceRejection] - ) - } else { - emit( - resources - .fetch(id, ref, schemaOpt) - .map(_.value.source) - .attemptNarrow[ResourceRejection] - .rejectWhen(wrongJsonOrNotFound) - ) + (pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(resource) & varyAcceptHeaders) { + resourceRef => + authorizeFor(project, Read).apply { + parameter("annotate".as[Boolean].withDefault(false)) { annotate => + implicit val source: Printer = sourcePrinter + if (annotate) { + emit( + resources + .fetch(resourceRef, project, schemaOpt) + .flatMap(asSourceWithMetadata) + .attemptNarrow[ResourceRejection] + ) + } else { + emit( + resources + .fetch(resourceRef, project, schemaOpt) + .map(_.value.source) + .attemptNarrow[ResourceRejection] + .rejectWhen(wrongJsonOrNotFound) + ) + } } } - } }, // Get remote contexts pathPrefix("remote-contexts") { - (get & idSegmentRef(id) & pathEndOrSingleSlash & authorizeFor(ref, Read)) { id => - emit( - resources - .fetchState(id, ref, schemaOpt) - .map(_.remoteContexts) - .attemptNarrow[ResourceRejection] - ) + (get & idSegmentRef(resource) & pathEndOrSingleSlash & authorizeFor(project, Read)) { + resourceRef => + emit( + resources + .fetchState(resourceRef, project, schemaOpt) + .map(_.remoteContexts) + .attemptNarrow[ResourceRejection] + ) } }, // Tag a resource pathPrefix("tags") { concat( // Fetch a resource tags - (get & idSegmentRef(id) & pathEndOrSingleSlash & authorizeFor(ref, Read)) { id => - emit( - resources - .fetch(id, ref, schemaOpt) - .map(_.value.tags) - .attemptNarrow[ResourceRejection] - .rejectWhen(wrongJsonOrNotFound) - ) + (get & idSegmentRef(resource) & pathEndOrSingleSlash & authorizeFor(project, Read)) { + resourceRef => + emit( + resources + .fetch(resourceRef, project, schemaOpt) + .map(_.value.tags) + .attemptNarrow[ResourceRejection] + .rejectWhen(wrongJsonOrNotFound) + ) }, // Tag a resource (post & parameter("rev".as[Int]) & pathEndOrSingleSlash) { rev => - authorizeFor(ref, Write).apply { + authorizeFor(project, Write).apply { entity(as[Tag]) { case Tag(tagRev, tag) => emit( Created, resources - .tag(id, ref, schemaOpt, tag, tagRev, rev) - .flatTap(indexUIO(ref, _, mode)) + .tag(resource, project, schemaOpt, tag, tagRev, rev) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectWhen(wrongJsonOrNotFound) @@ -279,13 +282,13 @@ final class ResourcesRoutes( }, // Delete a tag (tagLabel & delete & parameter("rev".as[Int]) & pathEndOrSingleSlash & authorizeFor( - ref, + project, Write )) { (tag, rev) => emit( resources - .deleteTag(id, ref, schemaOpt, tag, rev) - .flatTap(indexUIO(ref, _, mode)) + .deleteTag(resource, project, schemaOpt, tag, rev) + .flatTap(indexUIO(project, _, mode)) .map(_.void) .attemptNarrow[ResourceRejection] .rejectOn[ResourceNotFound] diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala index cb575ae55d..579cc309eb 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resolvers/ResolverResolution.scala @@ -64,18 +64,18 @@ final class ResolverResolution[R]( * Attempts to resolve the resource against the given resolver and return the resource if found and a report of how * the resolution went * - * @param ref + * @param resource * the resource reference - * @param projectRef + * @param project * the project reference */ - def resolveReport(ref: ResourceRef, projectRef: ProjectRef)(implicit + def resolveReport(resource: ResourceRef, project: ProjectRef)(implicit caller: Caller ): IO[(ResourceResolutionReport, Option[R])] = { val initial: (ResourceResolutionReport, Option[R]) = ResourceResolutionReport() -> None - listResolvers(projectRef) + listResolvers(project) .flatMap { resolvers => resolvers .sortBy { r => (r.value.priority.value, r.id.toString) } @@ -85,7 +85,7 @@ final class ResolverResolution[R]( case (report, result @ Some(_)) => IO.pure(report -> result) // No resolution was successful yet, we carry on case (report, None) => - resolveReport(ref, projectRef, resolver).map { case (resolverReport, result) => + resolveReport(resource, project, resolver).map { case (resolverReport, result) => report.copy(history = report.history :+ resolverReport) -> result } } diff --git a/docs/src/main/paradox/docs/delta/api/assets/resolvers/fetch-resource-source.sh b/docs/src/main/paradox/docs/delta/api/assets/resolvers/fetch-resource-source.sh new file mode 100644 index 0000000000..76f0ed1255 --- /dev/null +++ b/docs/src/main/paradox/docs/delta/api/assets/resolvers/fetch-resource-source.sh @@ -0,0 +1 @@ +curl "http://localhost:8080/v1/resolvers/myorg/myproj/nxv:myresolver/fd8a2b32-170e-44e8-808f-44a8cbbc49b0/source" \ No newline at end of file diff --git a/docs/src/main/paradox/docs/delta/api/resolvers-api.md b/docs/src/main/paradox/docs/delta/api/resolvers-api.md index a8096f1aea..5d65159e39 100644 --- a/docs/src/main/paradox/docs/delta/api/resolvers-api.md +++ b/docs/src/main/paradox/docs/delta/api/resolvers-api.md @@ -393,6 +393,31 @@ Request Response : @@snip [fetched.json](assets/resources/fetched.json) +## Fetch original resource payload using resolvers + +Fetches the original source payload of a resource using the provided resolver. + +If the resolver segment (`{resolver_id}`) is `_` the resource is fetched from the first resolver in the requested +project (`{org_label}/{project_label}`). The resolvers are ordered by its priority field. + +``` +GET /v1/resolvers/{org_label}/{project_label}/{resolver_id}/{resource_id}/source?rev={rev}&tag={tag} +``` +where ... +- `{resource_id}`: Iri - the @id value of the resource to be retrieved. +- `{rev}`: Number - the targeted revision to be fetched. This field is optional and defaults to the latest revision. +- `{tag}`: String - the targeted tag to be fetched. This field is optional. + +`{rev}` and `{tag}` fields cannot be simultaneously present. + +**Example** + +Request +: @@snip [fetch-resource-source.sh](assets/resolvers/fetch-resource-source.sh) + +Response +: @@snip [source.json](assets/resources/payload.json) + ## Server Sent Events From Delta 1.5, it is possible to fetch SSEs for all resolvers or just resolvers diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala index c7d4537151..8a08fdf8ca 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/ResourcesSpec.scala @@ -174,6 +174,14 @@ class ResourcesSpec extends BaseIntegrationSpec { } } + "fetch the original payload through a resolver" in { + deltaClient.get[Json](s"/resolvers/$project1/_/test-resource:1/source", Morty) { (json, response) => + val expected = SimpleResource.sourcePayload(resource1Id, 5) + response.status shouldEqual StatusCodes.OK + json should equalIgnoreArrayOrder(expected) + } + } + "fetch the original payload with metadata" in { deltaClient.get[Json](s"/resources/$project1/test-schema/test-resource:1/source?annotate=true", Morty) { (json, response) =>