Skip to content

Commit

Permalink
Allow to return annotated payload for schemas (#5079)
Browse files Browse the repository at this point in the history
* Allow to return annotated payload for schemas

---------

Co-authored-by: Simon Dumas <[email protected]>
  • Loading branch information
imsdu and Simon Dumas authored Aug 7, 2024
1 parent ccbcdb9 commit c9521b2
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.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.ResourcesRoutes.asSourceWithMetadata
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling
Expand Down Expand Up @@ -210,22 +209,15 @@ final class ResourcesRoutes(
authorizeFor(project, Read).apply {
annotateSource { annotate =>
implicit val source: Printer = sourcePrinter
if (annotate) {
emit(
resources
.fetch(resourceRef, project, schemaOpt)
.map(asSourceWithMetadata)
.attemptNarrow[ResourceRejection]
)
} else {
emit(
resources
.fetch(resourceRef, project, schemaOpt)
.map(_.value.source)
.attemptNarrow[ResourceRejection]
.rejectOn[ResourceNotFound]
)
}
emit(
resources
.fetch(resourceRef, project, schemaOpt)
.map { resource =>
AnnotatedSource.when(annotate)(resource, resource.value.source)
}
.attemptNarrow[ResourceRejection]
.rejectOn[ResourceNotFound]
)
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaScheme
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{AnnotatedSource, RdfMarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.model.routes.Tag
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.schemas.{read => Read, write => Write}
Expand Down Expand Up @@ -72,9 +72,13 @@ final class SchemasRoutes(
private def emitMetadataOrReject(io: IO[SchemaResource]): Route =
emit(io.map(_.void).attemptNarrow[SchemaRejection].rejectOn[SchemaNotFound])

private def emitSource(io: IO[SchemaResource]): Route = {
private def emitSource(io: IO[SchemaResource], annotate: Boolean): Route = {
implicit val source: Printer = sourcePrinter
emit(io.map(_.value.source).attemptNarrow[SchemaRejection].rejectOn[SchemaNotFound])
emit(
io.map { resource => AnnotatedSource.when(annotate)(resource, resource.value.source) }
.attemptNarrow[SchemaRejection]
.rejectOn[SchemaNotFound]
)
}

private def emitTags(io: IO[SchemaResource]): Route =
Expand Down Expand Up @@ -139,10 +143,11 @@ final class SchemasRoutes(
}
},
// Fetch a schema original source
(pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id)) { id =>
authorizeFor(ref, Read).apply {
emitSource(schemas.fetch(id, ref))
}
(pathPrefix("source") & get & pathEndOrSingleSlash & idSegmentRef(id) & annotateSource) {
(id, annotate) =>
authorizeFor(ref, Read).apply {
emitSource(schemas.fetch(id, ref), annotate)
}
},
pathPrefix("tags") {
concat(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"@context": [
{
"nxv": "https://bluebrain.github.io/nexus/vocabulary/",
"schema": "http://schema.org/"
},
"https://bluebrain.github.io/nexus/contexts/metadata.json"
],
"@id": "{{id}}",
"@type": "Schema",
"shapes": [
{
"@id": "nxv:MyShape",
"@type": "NodeShape",
"nodeKind": "sh:BlankNodeOrIRI",
"property": [
{
"@id": "nxv:NameProperty",
"datatype": "xsd:string",
"minCount": 1,
"path": "nxv:name"
},
{
"@id": "nxv:NumberProperty",
"datatype": "xsd:integer",
"minCount": 1,
"path": "nxv:number"
},
{
"@id": "nxv:PathProperty",
"datatype": "xsd:boolean",
"minCount": 1,
"path": "nxv:bool"
}
],
"targetClass": "schema:Custom"
}
],
"_constrainedBy": "https://bluebrain.github.io/nexus/schemas/shacl-20170720.ttl",
"_createdAt": "1970-01-01T00:00:00Z",
"_createdBy": "http://localhost/v1/realms/wonderland/users/writer",
"_deprecated": false,
"_incoming": "http://localhost/v1/schemas/myorg/myproject/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2Fmyid2/incoming",
"_outgoing": "http://localhost/v1/schemas/myorg/myproject/https:%2F%2Fbluebrain.github.io%2Fnexus%2Fvocabulary%2Fmyid2/outgoing",
"_project": "http://localhost/v1/projects/myorg/myproject",
"_rev": {{rev}},
"_self": "{{self}}",
"_updatedAt": "1970-01-01T00:00:00Z",
"_updatedBy": "http://localhost/v1/realms/wonderland/users/writer"
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues {
}
}
}

"fetch a schema original payload by rev or tag" in {
val endpoints = List(
"/v1/schemas/myorg/myproject/myid2/source",
Expand All @@ -360,6 +361,38 @@ class SchemasRoutesSpec extends BaseRouteSpec with IOFromMap with CatsIOValues {
}
}

"fetch an annotated schema original payload" in {

Get("/v1/schemas/myorg/myproject/myid2/source?annotate=true") ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf(
"schemas/schema-payload-with-metadata.json",
"id" -> myId2,
"self" -> ResourceUris.schema(projectRef, myId2).accessUri,
"rev" -> 2
)
}
}

"fetch an annotated schema original payload by rev or tag" in {
val endpoints = List(
"/v1/schemas/myorg/myproject/myid2/source?rev=1&annotate=true",
"/v1/schemas/myorg/myproject/myid2/source?tag=mytag&annotate=true"
)

forAll(endpoints) { endpoint =>
Get(endpoint) ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
response.asJson shouldEqual jsonContentOf(
"schemas/schema-payload-with-metadata.json",
"id" -> myId2,
"self" -> ResourceUris.schema(projectRef, myId2).accessUri,
"rev" -> 1
)
}
}
}

"fetch the schema tags" in {
Get("/v1/schemas/myorg/myproject/myid2/tags?rev=1") ~> asReader ~> routes ~> check {
status shouldEqual StatusCodes.OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import io.circe.syntax.EncoderOps

object AnnotatedSource {

/**
* Merge the source with the metadata when annotation is requested or return only the source otherwise
*/
def when(annotate: Boolean)(resourceF: ResourceF[_], source: Json)(implicit baseUri: BaseUri): Json =
if (annotate) apply(resourceF, source) else source

/**
* Merges the source with the metadata of [[ResourceF]]
*/
Expand Down
2 changes: 1 addition & 1 deletion docs/src/main/paradox/docs/delta/api/resources-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ where ...

- `{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.
- `{annotate}`: Boolean - annotate the response with the resource metadata. This field only applies to standard resources. This field is optional.
- `{annotate}`: Boolean - annotate the response with the resource metadata. This field only applies to standard resources and schemas. This field is optional.

`{rev}` and `{tag}` fields cannot be simultaneously present.

Expand Down
6 changes: 5 additions & 1 deletion docs/src/main/paradox/docs/delta/api/schemas-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,19 @@ if the `Accept` header is set to `text/html`, a redirection to the fusion repres
## Fetch original payload

```
GET /v1/schemas/{org_label}/{project_label}/{schema_id}/source?rev={rev}&tag={tag}
GET /v1/schemas/{org_label}/{project_label}/{schema_id}/source?rev={rev}&tag={tag}&annotate={annotate}
```
where ...

- `{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.
- `{annotate}`: Boolean - annotate the response with the schema metadata.

`{rev}` and `{tag}` fields cannot be simultaneously present.

If `{annotate}` is set, the metadata is injected alongside with the original payload where the ones from the original payload take precedence.
The context in the original payload is also amended with the metadata context.

**Example**

Request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import akka.http.scaladsl.model.{HttpResponse, StatusCodes}
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UrlUtils
import ch.epfl.bluebrain.nexus.tests.BaseIntegrationSpec
import ch.epfl.bluebrain.nexus.tests.Identity.Anonymous
import ch.epfl.bluebrain.nexus.tests.Identity.resources.Rick
import ch.epfl.bluebrain.nexus.tests.builders.SchemaPayloads
import ch.epfl.bluebrain.nexus.tests.builders.SchemaPayloads._
Expand Down Expand Up @@ -197,6 +198,25 @@ class SchemasSpec extends BaseIntegrationSpec {
} yield succeed
}

"fetch the original payload for a user with access" in {
val id = genId()
for {
payload <- SchemaPayloads.simple(id)
_ <- deltaClient.post[Json](s"/schemas/$project", payload, Rick) { expectCreated }
// Forbidden for anonymous
_ <- deltaClient.get[Json](s"/schemas/$project/$id/source", Anonymous) { expectForbidden }
// Granted for the user with the read permission
_ <- deltaClient.get[Json](s"/schemas/$project/$id/source", Rick) { (json, response) =>
response.status shouldEqual StatusCodes.OK
json shouldEqual payload
}
_ <- deltaClient.get[Json](s"/schemas/$project/$id/source?annotate=true", Rick) { (json, response) =>
response.status shouldEqual StatusCodes.OK
json.asObject.value.keys should contain allOf ("_createdAt", "_updatedAt", "@id", "@type")
}
} yield succeed
}

"create a schema against the resource endpoint" in {
val id = genId()
val schemaSegment = UrlUtils.encode("https://bluebrain.github.io/nexus/schemas/shacl-20170720.ttl")
Expand Down

0 comments on commit c9521b2

Please sign in to comment.