From 725afec3b1154e2356f6f19aa79e10deaf03735c Mon Sep 17 00:00:00 2001 From: dantb Date: Fri, 13 Oct 2023 13:24:25 +0200 Subject: [PATCH] Allow creation of a tagged resource (#4339) --- .../nexus/delta/routes/ResourcesRoutes.scala | 23 +- .../delta/routes/ResourcesRoutesSpec.scala | 86 ++++++-- .../nexus/delta/rdf/Vocabulary.scala | 1 - .../delta/sdk/directives/UriDirectives.scala | 5 + .../nexus/delta/sdk/resources/Resources.scala | 15 +- .../delta/sdk/resources/ResourcesImpl.scala | 10 +- .../sdk/resources/model/ResourceCommand.scala | 3 +- .../sdk/resources/model/ResourceEvent.scala | 15 +- .../database/resource-created-tagged.json | 70 ++++++ .../sse/resource-created-tagged.json | 27 +++ .../sdk/resources/ResourcesImplSpec.scala | 200 +++++++++--------- .../delta/sdk/resources/ResourcesSpec.scala | 14 +- .../model/ResourceSerializationSuite.scala | 18 +- 13 files changed, 327 insertions(+), 160 deletions(-) create mode 100644 delta/sdk/src/test/resources/resources/database/resource-created-tagged.json create mode 100644 delta/sdk/src/test/resources/resources/sse/resource-created-tagged.json 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 db803992b7..f8bd30a8d1 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 @@ -4,13 +4,13 @@ import akka.http.scaladsl.model.StatusCodes.{Created, OK} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._ 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.kernel.effect.migration._ 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.DeltaDirectives._ @@ -79,12 +79,15 @@ final class ResourcesRoutes( resolveProjectRef.apply { ref => concat( // Create a resource without schema nor id segment - (post & pathEndOrSingleSlash & noParameter("rev") & entity(as[NexusSource]) & indexingMode) { - (source, mode) => + (post & pathEndOrSingleSlash & noParameter("rev") & entity(as[NexusSource]) & indexingMode & tagParam) { + (source, mode, tag) => authorizeFor(ref, Write).apply { emit( Created, - resources.create(ref, resourceSchema, source.value).tapEval(indexUIO(ref, _, mode)).map(_.void) + resources + .create(ref, resourceSchema, source.value, tag) + .tapEval(indexUIO(ref, _, mode)) + .map(_.void) ) } }, @@ -92,13 +95,13 @@ final class ResourcesRoutes( val schemaOpt = underscoreToOption(schema) concat( // Create a resource with schema but without id segment - (post & pathEndOrSingleSlash & noParameter("rev")) { + (post & pathEndOrSingleSlash & noParameter("rev") & tagParam) { tag => authorizeFor(ref, Write).apply { entity(as[NexusSource]) { source => emit( Created, resources - .create(ref, schema, source.value) + .create(ref, schema, source.value, tag) .tapEval(indexUIO(ref, _, mode)) .map(_.void) .rejectWhen(wrongJsonOrNotFound) @@ -113,18 +116,18 @@ final class ResourcesRoutes( // Create or update a resource put { authorizeFor(ref, Write).apply { - (parameter("rev".as[Int].?) & pathEndOrSingleSlash & entity(as[NexusSource])) { - case (None, source) => + (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) + .create(id, ref, schema, source.value, tag) .tapEval(indexUIO(ref, _, mode)) .map(_.void) .rejectWhen(wrongJsonOrNotFound) ) - case (Some(rev), source) => + case (Some(rev), source, _) => // Update a resource emit( resources diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala index b1ef9d1f86..c40ba4cc62 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala @@ -17,7 +17,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceResolut 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.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceUris +import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegmentRef, ResourceUris} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{events, resources} import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings @@ -29,6 +29,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resources.{Resources, ResourcesConfig, import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import io.circe.{Json, Printer} @@ -61,12 +62,19 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { private val schemaSource = jsonContentOf("resources/schema.json").addContext(contexts.shacl, contexts.schemasMetadata) private val schema1 = SchemaGen.schema(nxv + "myschema", project.value.ref, schemaSource.removeKeys(keywords.id)) private val schema2 = SchemaGen.schema(schema.Person, project.value.ref, schemaSource.removeKeys(keywords.id)) - - private val myId = nxv + "myid" // Resource created against no schema with id present on the payload - private val myId2 = nxv + "myid2" // Resource created against schema1 with id present on the payload - private val myId3 = nxv + "myid3" // Resource created against no schema with id passed and present on the payload - private val myId4 = nxv + "myid4" // Resource created against schema1 with id passed and present on the payload - private val myId5 = nxv + "myid5" // Resource created against schema1 with id passed and present on the payload + private val tag = UserTag.unsafe("mytag") + + private val myId = nxv + "myid" // Resource created against no schema with id present on the payload + private val myId2 = nxv + "myid2" // Resource created against schema1 with id present on the payload + private val myId3 = nxv + "myid3" // Resource created against no schema with id passed and present on the payload + private val myId4 = nxv + "myid4" // Resource created against schema1 with id passed and present on the payload + private val myId5 = nxv + "myid5" // Resource created against schema1 with id passed and present on the payload + private val myId6 = nxv + "myid6" // Resource created and tagged against no schema with id present on the payload + private val myId7 = nxv + "myid7" // Resource created and tagged against no schema with id present on the payload + private val myId8 = + nxv + "myid8" // Resource created and tagged against no schema with id passed and present on the payload + private val myId9 = + nxv + "myid9" // Resource created and tagged against schema1 with id passed and present on the payload private val myIdEncoded = UrlUtils.encode(myId.toString) private val myId2Encoded = UrlUtils.encode(myId2.toString) private val payload = jsonContentOf("resources/resource.json", "id" -> myId) @@ -94,7 +102,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { private val fetchContext = FetchContextDummy(List(project.value), ProjectContextRejection) private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(rcr) - private def routesWithDecodingOption(implicit decodingOption: DecodingOption) = { + private def routesWithDecodingOption(implicit decodingOption: DecodingOption): (Route, Resources) = { val resources = ResourcesImpl( validator, fetchContext, @@ -102,18 +110,25 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { ResourcesConfig(eventLogConfig, decodingOption), xas ) - Route.seal( - ResourcesRoutes( - IdentitiesDummy(caller), - aclCheck, - resources, - DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)), - IndexingAction.noop - ) + ( + Route.seal( + ResourcesRoutes( + IdentitiesDummy(caller), + aclCheck, + resources, + DeltaSchemeDirectives( + fetchContext, + ioFromMap(uuid -> projectRef.organization), + ioFromMap(uuid -> projectRef) + ), + IndexingAction.noop + ) + ), + resources ) } - private lazy val routes = routesWithDecodingOption(DecodingOption.Strict) + private lazy val routes = routesWithDecodingOption(DecodingOption.Strict)._1 private val payloadUpdated = payload deepMerge json"""{"name": "Alice", "address": null}""" @@ -149,6 +164,22 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { } } + "create a tagged resource" in { + val endpoints = List( + ("/v1/resources/myorg/myproject?tag=mytag", myId6, schemas.resources), + ("/v1/resources/myorg/myproject/myschema?tag=mytag", myId7, schema1.id) + ) + val (routes, resources) = routesWithDecodingOption(DecodingOption.Strict) + forAll(endpoints) { case (endpoint, id, schema) => + val payload = jsonContentOf("resources/resource.json", "id" -> id) + Post(endpoint, payload.toEntity) ~> routes ~> check { + status shouldEqual StatusCodes.Created + response.asJson shouldEqual resourceMetadata(projectRef, id, schema, (nxv + "Custom").toString) + lookupResourceByTag(resources, id) should contain(tag) + } + } + } + "create a resource with an authenticated user and provided id" in { val endpoints = List( ("/v1/resources/myorg/myproject/_/myid3", myId3, schemas.resources), @@ -161,10 +192,29 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { response.asJson shouldEqual resourceMetadata(projectRef, id, schema, (nxv + "Custom").toString, createdBy = alice, updatedBy = alice) } + } + } + "create a tagged resource with an authenticated user and provided id" in { + val endpoints = List( + ("/v1/resources/myorg/myproject/_/myid8?tag=mytag", myId8, schemas.resources), + ("/v1/resources/myorg/myproject/myschema/myid9?tag=mytag", myId9, schema1.id) + ) + val (routes, resources) = routesWithDecodingOption(DecodingOption.Strict) + forAll(endpoints) { case (endpoint, id, schema) => + val payload = jsonContentOf("resources/resource.json", "id" -> id) + Put(endpoint, payload.toEntity) ~> asAlice ~> routes ~> check { + status shouldEqual StatusCodes.Created + response.asJson shouldEqual + resourceMetadata(projectRef, id, schema, (nxv + "Custom").toString, createdBy = alice, updatedBy = alice) + lookupResourceByTag(resources, id) should contain(tag) + } } } + def lookupResourceByTag(resources: Resources, id: Iri): List[UserTag] = + resources.fetch(IdSegmentRef(id, tag), projectRef, None).accepted.value.tags.value.keys.toList + "reject the creation of a resource which already exists" in { Put("/v1/resources/myorg/myproject/_/myid", payload.toEntity) ~> routes ~> check { status shouldEqual StatusCodes.Conflict @@ -210,7 +260,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with IOFromMap { } "succeed if underscore fields are present but the decoding is set to lenient" in { - val lenientDecodingRoutes = routesWithDecodingOption(DecodingOption.Lenient) + val lenientDecodingRoutes = routesWithDecodingOption(DecodingOption.Lenient)._1 Post("/v1/resources/myorg/myproject/_/", payloadWithUnderscoreFields.toEntity) ~> lenientDecodingRoutes ~> check { response.status shouldEqual StatusCodes.Created diff --git a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/Vocabulary.scala b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/Vocabulary.scala index 327fff5517..d69f04083a 100644 --- a/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/Vocabulary.scala +++ b/delta/rdf/src/main/scala/ch/epfl/bluebrain/nexus/delta/rdf/Vocabulary.scala @@ -160,7 +160,6 @@ object Vocabulary { val score = Metadata("score") val self = Metadata("self") val source = Metadata("source") - val tags = Metadata("tags") val tokenEndpoint = Metadata("tokenEndpoint") val total = Metadata("total") val types = Metadata("types") diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala index c8706922fa..6b2b400062 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/UriDirectives.scala @@ -170,6 +170,11 @@ trait UriDirectives extends QueryParamsUnmarshalling { ) } + /** + * Creates optional [[UserTag]] from `tag` query param. + */ + val tagParam: Directive1[Option[UserTag]] = parameter("tag".as[UserTag].?) + def timeRange(paramName: String): Directive1[TimeRange] = parameter(paramName.as[String].?).flatMap { case None => provide(TimeRange.default) case Some(value) => diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/Resources.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/Resources.scala index fa317a9f50..bd8be60d06 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/Resources.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/Resources.scala @@ -42,7 +42,8 @@ trait Resources { def create( projectRef: ProjectRef, schema: IdSegment, - source: Json + source: Json, + tag: Option[UserTag] )(implicit caller: Caller): IO[ResourceRejection, DataResource] /** @@ -61,7 +62,8 @@ trait Resources { id: IdSegment, projectRef: ProjectRef, schema: IdSegment, - source: Json + source: Json, + tag: Option[UserTag] )(implicit caller: Caller): IO[ResourceRejection, DataResource] /** @@ -261,10 +263,12 @@ object Resources { private[delta] def next(state: Option[ResourceState], event: ResourceEvent): Option[ResourceState] = { // format: off - def created(e: ResourceCreated): Option[ResourceState] = + def created(e: ResourceCreated): Option[ResourceState] = { + val tags = e.tag.fold(Tags.empty)(t => Tags(t -> e.rev)) Option.when(state.isEmpty){ - ResourceState(e.id, e.project, e.schemaProject, e.source, e.compacted, e.expanded, e.remoteContexts, e.rev, deprecated = false, e.schema, e.types, Tags.empty, e.instant, e.subject, e.instant, e.subject) + ResourceState(e.id, e.project, e.schemaProject, e.source, e.compacted, e.expanded, e.remoteContexts, e.rev, deprecated = false, e.schema, e.types, tags, e.instant, e.subject, e.instant, e.subject) } + } def updated(e: ResourceUpdated): Option[ResourceState] = state.map { _.copy(rev = e.rev, types = e.types, source = e.source, compacted = e.compacted, expanded = e.expanded, remoteContexts = e.remoteContexts, updatedAt = e.instant, updatedBy = e.subject) @@ -324,7 +328,7 @@ object Resources { for { (schemaRev, schemaProject) <- validate(c.id, expanded, c.schema, c.project, c.caller) t <- IOUtils.instant - } yield ResourceCreated(c.id, c.project, schemaRev, schemaProject, types, c.source, compacted, expanded, remoteContextRefs, 1, t, c.subject) + } yield ResourceCreated(c.id, c.project, schemaRev, schemaProject, types, c.source, compacted, expanded, remoteContextRefs, 1, t, c.subject, c.tag) // format: on case _ => IO.raiseError(ResourceAlreadyExists(c.id, c.project)) @@ -433,6 +437,7 @@ object Resources { ResourceState.serializer, Tagger[ResourceEvent]( { + case r: ResourceCreated => r.tag.flatMap(t => Some(t -> r.rev)) case r: ResourceTagAdded => Some(r.tag -> r.targetRev) case _ => None }, diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImpl.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImpl.scala index 80218f5d78..398563fc57 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImpl.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImpl.scala @@ -36,13 +36,14 @@ final class ResourcesImpl private ( override def create( projectRef: ProjectRef, schema: IdSegment, - source: Json + source: Json, + tag: Option[UserTag] )(implicit caller: Caller): IO[ResourceRejection, DataResource] = { for { projectContext <- fetchContext.onCreate(projectRef) schemeRef <- expandResourceRef(schema, projectContext) jsonld <- sourceParser(projectRef, projectContext, source) - res <- eval(CreateResource(jsonld.iri, projectRef, schemeRef, source, jsonld, caller)) + res <- eval(CreateResource(jsonld.iri, projectRef, schemeRef, source, jsonld, caller, tag)) } yield res }.span("createResource") @@ -50,14 +51,15 @@ final class ResourcesImpl private ( id: IdSegment, projectRef: ProjectRef, schema: IdSegment, - source: Json + source: Json, + tag: Option[UserTag] )(implicit caller: Caller): IO[ResourceRejection, DataResource] = { for { projectContext <- fetchContext.onCreate(projectRef) iri <- expandIri(id, projectContext) schemeRef <- expandResourceRef(schema, projectContext) jsonld <- sourceParser(projectRef, projectContext, iri, source) - res <- eval(CreateResource(iri, projectRef, schemeRef, source, jsonld, caller)) + res <- eval(CreateResource(iri, projectRef, schemeRef, source, jsonld, caller, tag)) } yield res }.span("createResource") diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceCommand.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceCommand.scala index 13a2153d7d..6347ab04ab 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceCommand.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceCommand.scala @@ -70,7 +70,8 @@ object ResourceCommand { schema: ResourceRef, source: Json, jsonld: JsonLdResult, - caller: Caller + caller: Caller, + tag: Option[UserTag] ) extends ResourceCommand { override def rev: Int = 0 diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceEvent.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceEvent.scala index 97d596de23..b9784a79e0 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceEvent.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceEvent.scala @@ -19,7 +19,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, Label, ProjectRef, ResourceRef} import io.circe.generic.extras.Configuration -import io.circe.generic.extras.semiauto.{deriveConfiguredCodec, deriveConfiguredEncoder} +import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} import io.circe.syntax._ import io.circe._ @@ -94,7 +94,8 @@ object ResourceEvent { remoteContexts: Set[RemoteContextRef] = Set.empty, rev: Int, instant: Instant, - subject: Subject + subject: Subject, + tag: Option[UserTag] ) extends ResourceEvent /** @@ -276,7 +277,9 @@ object ResourceEvent { // when deserializing an event that has none. Remove it after 1.10 migration. implicit val configuration: Configuration = Serializer.circeConfiguration.withDefaults - implicit val coder: Codec.AsObject[ResourceEvent] = deriveConfiguredCodec[ResourceEvent] + implicit val enc: Encoder.AsObject[ResourceEvent] = + deriveConfiguredEncoder[ResourceEvent].mapJsonObject(dropNullValues) + implicit val coder: Codec.AsObject[ResourceEvent] = Codec.AsObject.from(deriveConfiguredDecoder[ResourceEvent], enc) Serializer() } @@ -336,8 +339,8 @@ object ResourceEvent { implicit val subjectEncoder: Encoder[Subject] = IriEncoder.jsonEncoder[Subject] implicit val projectRefEncoder: Encoder[ProjectRef] = IriEncoder.jsonEncoder[ProjectRef] Encoder.encodeJsonObject.contramapObject { event => - deriveConfiguredEncoder[ResourceEvent] - .encodeObject(event) + val obj = deriveConfiguredEncoder[ResourceEvent].encodeObject(event) + dropNullValues(obj) .remove("compacted") .remove("expanded") .remove("remoteContexts") @@ -345,4 +348,6 @@ object ResourceEvent { } } } + + private def dropNullValues(j: JsonObject): JsonObject = j.filter { case (_, v) => !v.isNull } } diff --git a/delta/sdk/src/test/resources/resources/database/resource-created-tagged.json b/delta/sdk/src/test/resources/resources/database/resource-created-tagged.json new file mode 100644 index 0000000000..add2156d9b --- /dev/null +++ b/delta/sdk/src/test/resources/resources/database/resource-created-tagged.json @@ -0,0 +1,70 @@ +{ + "id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "project": "myorg/myproj", + "schema": "https://bluebrain.github.io/nexus/schemas/unconstrained.json?rev=1", + "schemaProject": "myorg/myproj", + "types": [ + "https://neuroshapes.org/Morphology" + ], + "source": { + "@context": [ + "https://neuroshapes.org", + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + } + ], + "@id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "@type": "Morphology", + "name": "Morphology 001" + }, + "compacted": { + "@context": [ + "https://neuroshapes.org", + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + } + ], + "@id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "@type": "Morphology", + "name": "Morphology 001" + }, + "expanded": [ + { + "@id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "@type": [ + "https://neuroshapes.org/Morphology" + ], + "https://bluebrain.github.io/nexus/vocabulary/name": [ + { + "@value": "Morphology 001" + } + ] + } + ], + "remoteContexts": [ + { + "iri": "https://bluebrain.github.io/nexus/contexts/metadata.json", + "@type": "StaticContextRef" + }, + { + "iri": "https://neuroshapes.org", + "resource": { + "id": "https://neuroshapes.org", + "project": "myorg/myproj", + "rev": 5 + }, + "@type": "ProjectRemoteContextRef" + } + ], + "rev": 1, + "tag": "mytag", + "instant": "1970-01-01T00:00:00Z", + "subject": { + "subject": "username", + "realm": "myrealm", + "@type": "User" + }, + "@type": "ResourceCreated" +} \ No newline at end of file diff --git a/delta/sdk/src/test/resources/resources/sse/resource-created-tagged.json b/delta/sdk/src/test/resources/resources/sse/resource-created-tagged.json new file mode 100644 index 0000000000..b27d3fc289 --- /dev/null +++ b/delta/sdk/src/test/resources/resources/sse/resource-created-tagged.json @@ -0,0 +1,27 @@ +{ + "@context": "https://bluebrain.github.io/nexus/contexts/metadata.json", + "@type": "ResourceCreated", + "_constrainedBy": "https://bluebrain.github.io/nexus/schemas/unconstrained.json", + "_instant": "1970-01-01T00:00:00Z", + "_project": "http://localhost/v1/projects/myorg/myproj", + "_resourceId": "https://bluebrain.github.io/nexus/vocabulary/myId", + "_rev": 1, + "tag": "mytag", + "_schemaProject": "http://localhost/v1/projects/myorg/myproj", + "_source": { + "@context": [ + "https://neuroshapes.org", + "https://bluebrain.github.io/nexus/contexts/metadata.json", + { + "@vocab": "https://bluebrain.github.io/nexus/vocabulary/" + } + ], + "@id": "https://bluebrain.github.io/nexus/vocabulary/myId", + "@type": "Morphology", + "name": "Morphology 001" + }, + "_subject": "http://localhost/v1/realms/myrealm/users/username", + "_types": [ + "https://neuroshapes.org/Morphology" + ] +} \ No newline at end of file diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala index 0851d880d6..11d235df66 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala @@ -7,7 +7,6 @@ import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema, sche import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} -import ch.epfl.bluebrain.nexus.delta.sdk.ConfigFixtures import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen, ResourceResolutionGen, SchemaGen} import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef, Tags} @@ -18,9 +17,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.{FetchReso import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{ResolverResolutionRejection, ResourceResolutionReport} import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{BlankResourceId, IncorrectRev, InvalidJsonLdFormat, InvalidResource, InvalidSchemaRejection, ProjectContextRejection, ReservedResourceId, ResourceAlreadyExists, ResourceIsDeprecated, ResourceNotFound, RevisionNotFound, SchemaIsDeprecated, TagNotFound, UnexpectedResourceId, UnexpectedResourceSchema} import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import ch.epfl.bluebrain.nexus.delta.sdk.{ConfigFixtures, DataResource} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef.{Latest, Revision} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag @@ -112,6 +113,10 @@ class ResourcesImplSpec val myId7 = nxv + "myid7" // Resource created against the resource schema with id passed explicitly and with payload without @context val myId8 = nxv + "myid8" // Resource created against the resource schema with id present on the payload and having its context pointing on metadata and myId1 and myId2 val myId9 = nxv + "myid9" // Resource created against the resource schema with id present on the payload and having its context pointing on metadata and myId8 so therefore myId1 and myId2 + val myId10 = nxv + "myid10" + val myId11 = nxv + "myid11" + val myId12 = nxv + "myid12" + val myId13 = nxv + "myid13" // format: on val resourceSchema = Latest(schemas.resources) @@ -121,17 +126,31 @@ class ResourcesImplSpec def sourceWithBlankId = source deepMerge json"""{"@id": ""}""" val tag = UserTag.unsafe("tag") + def mkResource(res: Resource): DataResource = + ResourceGen.resourceFor(res, types = types, subject = subject) + "creating a resource" should { "succeed with the id present on the payload" in { forAll(List(myId -> resourceSchema, myId2 -> Latest(schema1.id))) { case (id, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" val expectedData = ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1)) - val resource = resources.create(projectRef, schemaRef, sourceWithId).accepted - resource shouldEqual ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + val resource = resources.create(projectRef, schemaRef, sourceWithId, None).accepted + resource shouldEqual mkResource(expectedData) + } + } + + "succeed and tag with the id present on the payload" in { + forAll(List(myId10 -> resourceSchema, myId11 -> Latest(schema1.id))) { case (id, schemaRef) => + val sourceWithId = source deepMerge json"""{"@id": "$id"}""" + val expectedData = + ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1), Tags(tag -> 1)) + val expectedResource = mkResource(expectedData) + + val resource = resources.create(projectRef, schemaRef, sourceWithId, Some(tag)).accepted + + val resourceByTag = resources.fetch(IdSegmentRef(id, tag), projectRef, Some(schemaRef)).accepted + resource shouldEqual expectedResource + resourceByTag shouldEqual expectedResource } } @@ -144,12 +163,28 @@ class ResourcesImplSpec forAll(list) { case (id, schemaSegment, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" val expectedData = ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1)) - val resource = resources.create(id, projectRef, schemaSegment, sourceWithId).accepted - resource shouldEqual ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject + val resource = resources.create(id, projectRef, schemaSegment, sourceWithId, None).accepted + resource shouldEqual mkResource(expectedData) + } + } + + "succeed and tag with the id present on the payload and passed" in { + val list = + List( + (myId12, "_", resourceSchema), + (myId13, "myschema", Latest(schema1.id)) ) + forAll(list) { case (id, schemaSegment, schemaRef) => + val sourceWithId = source deepMerge json"""{"@id": "$id"}""" + val expectedData = + ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1), Tags(tag -> 1)) + val expectedResource = mkResource(expectedData) + + val resource = resources.create(id, projectRef, schemaSegment, sourceWithId, Some(tag)).accepted + + val resourceByTag = resources.fetch(IdSegmentRef(id, tag), projectRef, Some(schemaRef)).accepted + resource shouldEqual expectedResource + resourceByTag shouldEqual expectedResource } } @@ -165,12 +200,8 @@ class ResourcesImplSpec ResourceGen .resource(iri, projectRef, sourceWithId, Revision(schemaRef.iri, 1)) .copy(source = sourceWithoutId) - val resource = resources.create(segment, projectRef, schemaRef, sourceWithoutId).accepted - resource shouldEqual ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + val resource = resources.create(segment, projectRef, schemaRef, sourceWithoutId, None).accepted + resource shouldEqual mkResource(expectedData) } } @@ -182,7 +213,7 @@ class ResourcesImplSpec val expectedData = ResourceGen.resource(myId7, projectRef, payloadWithCtx, schemaRev).copy(source = payload) - resources.create(myId7, projectRef, schemas.resources, payload).accepted shouldEqual + resources.create(myId7, projectRef, schemas.resources, payload, None).accepted shouldEqual ResourceGen.resourceFor(expectedData, subject = subject) } @@ -192,12 +223,8 @@ class ResourcesImplSpec val schemaRev = Revision(resourceSchema.iri, 1) val expectedData = ResourceGen.resource(myId8, projectRef, sourceMyId8, schemaRev)(resolverContextResolution(projectRef)) - val resource = resources.create(projectRef, resourceSchema, sourceMyId8).accepted - resource shouldEqual ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + val resource = resources.create(projectRef, resourceSchema, sourceMyId8, None).accepted + resource shouldEqual mkResource(expectedData) } "succeed when pointing to another resource which itself points to other resources in its context" in { @@ -205,22 +232,18 @@ class ResourcesImplSpec val schemaRev = Revision(resourceSchema.iri, 1) val expectedData = ResourceGen.resource(myId9, projectRef, sourceMyId9, schemaRev)(resolverContextResolution(projectRef)) - val resource = resources.create(projectRef, resourceSchema, sourceMyId9).accepted - resource shouldEqual ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + val resource = resources.create(projectRef, resourceSchema, sourceMyId9, None).accepted + resource shouldEqual mkResource(expectedData) } "reject with different ids on the payload and passed" in { val otherId = nxv + "other" - resources.create(otherId, projectRef, schemas.resources, source).rejected shouldEqual + resources.create(otherId, projectRef, schemas.resources, source, None).rejected shouldEqual UnexpectedResourceId(id = otherId, payloadId = myId) } "reject if the id is blank" in { - resources.create(projectRef, schemas.resources, sourceWithBlankId).rejected shouldEqual + resources.create(projectRef, schemas.resources, sourceWithBlankId, None).rejected shouldEqual BlankResourceId } @@ -228,16 +251,16 @@ class ResourcesImplSpec forAll(List(Latest(schemas.resources), Latest(schema1.id))) { schemaRef => val myId = contexts + "some.json" val sourceWithReservedId = source deepMerge json"""{"@id": "$myId"}""" - resources.create(myId, projectRef, schemaRef, sourceWithReservedId).rejectedWith[ReservedResourceId] + resources.create(myId, projectRef, schemaRef, sourceWithReservedId, None).rejectedWith[ReservedResourceId] } } "reject if it already exists" in { - resources.create(myId, projectRef, schemas.resources, source).rejected shouldEqual + resources.create(myId, projectRef, schemas.resources, source, None).rejected shouldEqual ResourceAlreadyExists(myId, projectRef) resources - .create("nxv:myid", projectRef, schemas.resources, source) + .create("nxv:myid", projectRef, schemas.resources, source, None) .rejected shouldEqual ResourceAlreadyExists(myId, projectRef) } @@ -245,14 +268,14 @@ class ResourcesImplSpec "reject if it does not validate against its schema" in { val otherId = nxv + "other" val wrongSource = source deepMerge json"""{"@id": "$otherId", "number": "wrong"}""" - resources.create(otherId, projectRef, schema1.id, wrongSource).rejectedWith[InvalidResource] + resources.create(otherId, projectRef, schema1.id, wrongSource, None).rejectedWith[InvalidResource] } "reject if the validated schema is deprecated" in { val otherId = nxv + "other" val noIdSource = source.removeKeys(keywords.id) forAll(List[IdSegment](schema2.id, "Person")) { segment => - resources.create(otherId, projectRef, segment, noIdSource).rejected shouldEqual + resources.create(otherId, projectRef, segment, noIdSource, None).rejected shouldEqual SchemaIsDeprecated(schema2.id) } @@ -261,7 +284,7 @@ class ResourcesImplSpec "reject if the validated schema does not exists" in { val otherId = nxv + "other" val noIdSource = source.removeKeys(keywords.id) - resources.create(otherId, projectRef, "nxv:notExist", noIdSource).rejected shouldEqual + resources.create(otherId, projectRef, "nxv:notExist", noIdSource, None).rejected shouldEqual InvalidSchemaRejection( Latest(nxv + "notExist"), project.ref, @@ -276,15 +299,17 @@ class ResourcesImplSpec "reject if project does not exist" in { val projectRef = ProjectRef(org, Label.unsafe("other")) - resources.create(projectRef, schemas.resources, source).rejectedWith[ProjectContextRejection] + resources.create(projectRef, schemas.resources, source, None).rejectedWith[ProjectContextRejection] - resources.create(myId, projectRef, schemas.resources, source).rejectedWith[ProjectContextRejection] + resources.create(myId, projectRef, schemas.resources, source, None).rejectedWith[ProjectContextRejection] } "reject if project is deprecated" in { - resources.create(projectDeprecated.ref, schemas.resources, source).rejectedWith[ProjectContextRejection] + resources.create(projectDeprecated.ref, schemas.resources, source, None).rejectedWith[ProjectContextRejection] - resources.create(myId, projectDeprecated.ref, schemas.resources, source).rejectedWith[ProjectContextRejection] + resources + .create(myId, projectDeprecated.ref, schemas.resources, source, None) + .rejectedWith[ProjectContextRejection] } "reject if part of the context can't be resolved" in { @@ -292,7 +317,7 @@ class ResourcesImplSpec val unknownResource = nxv + "fail" val sourceMyIdX = source.addContext(contexts.metadata).addContext(unknownResource) deepMerge json"""{"@id": "$myIdX"}""" - resources.create(projectRef, resourceSchema, sourceMyIdX).rejectedWith[InvalidJsonLdFormat] + resources.create(projectRef, resourceSchema, sourceMyIdX, None).rejectedWith[InvalidJsonLdFormat] } "reject for an incorrect payload" in { @@ -301,7 +326,7 @@ class ResourcesImplSpec source.addContext( contexts.metadata ) deepMerge json"""{"other": {"@id": " http://nexus.example.com/myid"}}""" deepMerge json"""{"@id": "$myIdX"}""" - resources.create(projectRef, resourceSchema, sourceMyIdX).rejectedWith[InvalidJsonLdFormat] + resources.create(projectRef, resourceSchema, sourceMyIdX, None).rejectedWith[InvalidJsonLdFormat] } } @@ -311,24 +336,14 @@ class ResourcesImplSpec val updated = source.removeKeys(keywords.id) deepMerge json"""{"number": 60}""" val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1.id, 1)) resources.update(myId2, projectRef, Some(schema1.id), 1, updated).accepted shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 2 - ) + mkResource(expectedData).copy(rev = 2) } "succeed without specifying the schema" in { val updated = source.removeKeys(keywords.id) deepMerge json"""{"number": 65}""" val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1.id, 1)) resources.update("nxv:myid2", projectRef, None, 2, updated).accepted shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 3 - ) + mkResource(expectedData).copy(rev = 3) } "reject if it doesn't exists" in { @@ -380,12 +395,7 @@ class ResourcesImplSpec val expectedData = ResourceGen.resource(myId6, projectRef, source.removeKeys(keywords.id), Revision(schema1.id, 1)) resources.refresh(myId6, projectRef, Some(schema1.id)).accepted shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 2 - ) + mkResource(expectedData).copy(rev = 2) } "succeed without specifying the schema" in { @@ -436,17 +446,25 @@ class ResourcesImplSpec "tagging a resource" should { "succeed" in { - val schemaRev = Revision(resourceSchema.iri, 1) - val expectedData = ResourceGen.resource(myId, projectRef, source, schemaRev, tags = Tags(tag -> 1)) - val resource = + val schemaRev = Revision(resourceSchema.iri, 1) + val expectedData = ResourceGen.resource(myId, projectRef, source, schemaRev, tags = Tags(tag -> 1)) + val expectedLatestRev = mkResource(expectedData).copy(rev = 2) + val expectedTaggedData = expectedData.copy(tags = Tags.empty) + val expectedTaggedRev = mkResource(expectedTaggedData).copy(rev = 1) + + val resource = resources.tag(myId, projectRef, Some(schemas.resources), tag, 1, 1).accepted - resource shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 2 - ) + + // Lookup by tag should return the tagged rev but have no tags in the data + val taggedRevision = + resources.fetch(IdSegmentRef(myId, tag), projectRef, Some(schemas.resources)).accepted + // Lookup by latest revision should return rev 2 with tags in the data + val latestRevision = + resources.fetch(ResourceRef(myId), projectRef).accepted + + resource shouldEqual expectedLatestRev + latestRevision shouldEqual expectedLatestRev + taggedRevision shouldEqual expectedTaggedRev } "reject if it doesn't exists" in { @@ -492,14 +510,7 @@ class ResourcesImplSpec val sourceWithId = source deepMerge json"""{"@id": "$myId4"}""" val expectedData = ResourceGen.resource(myId4, projectRef, sourceWithId, Revision(schema1.id, 1)) val resource = resources.deprecate(myId4, projectRef, Some(schema1.id), 1).accepted - resource shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 2, - deprecated = true - ) + resource shouldEqual mkResource(expectedData).copy(rev = 2, deprecated = true) } "reject if it doesn't exists" in { @@ -540,34 +551,21 @@ class ResourcesImplSpec "succeed" in { forAll(List[Option[IdSegment]](None, Some(schemas.resources))) { schema => resources.fetch(myId, projectRef, schema).accepted shouldEqual - ResourceGen.resourceFor( - expectedDataLatest, - types = types, - subject = subject, - rev = 2 - ) + mkResource(expectedDataLatest).copy(rev = 2) } } "succeed by tag" in { forAll(List[Option[IdSegment]](None, Some(schemas.resources))) { schema => resources.fetch(IdSegmentRef("nxv:myid", tag), projectRef, schema).accepted shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + mkResource(expectedData) } } "succeed by rev" in { forAll(List[Option[IdSegment]](None, Some(schemas.resources))) { schema => resources.fetch(IdSegmentRef(myId, 1), projectRef, schema).accepted shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject - ) + mkResource(expectedData) } } @@ -615,13 +613,7 @@ class ResourcesImplSpec val expectedData = ResourceGen.resource(myId, projectRef, source, schemaRev) val resource = resources.deleteTag(myId, projectRef, Some(schemas.resources), tag, 2).accepted - resource shouldEqual - ResourceGen.resourceFor( - expectedData, - types = types, - subject = subject, - rev = 3 - ) + resource shouldEqual mkResource(expectedData).copy(rev = 3) } "reject if the resource doesn't exists" in { diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala index 2f6d865825..aa0e0fe388 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesSpec.scala @@ -48,6 +48,7 @@ class ResourcesSpec val time2 = Instant.ofEpochMilli(10L) val subject = User("myuser", Label.unsafe("myrealm")) val caller = Caller(subject, Set.empty) + val tag = UserTag.unsafe("mytag") val jsonld = JsonLdResult(myId, compacted, expanded, remoteContexts) @@ -62,7 +63,7 @@ class ResourcesSpec val schemaRev = Revision(schemaRef.iri, 1) eval( None, - CreateResource(myId, projectRef, schemaRef, source, jsonld, caller) + CreateResource(myId, projectRef, schemaRef, source, jsonld, caller, Some(tag)) ).accepted shouldEqual ResourceCreated( myId, @@ -76,7 +77,8 @@ class ResourcesSpec remoteContextRefs, 1, epoch, - subject + subject, + Some(tag) ) } } @@ -252,7 +254,8 @@ class ResourcesSpec remoteContexts, 1, epoch, - subject + subject, + Some(tag) ) ).value shouldEqual current.copy( @@ -261,7 +264,7 @@ class ResourcesSpec createdBy = subject, updatedAt = epoch, updatedBy = subject, - tags = Tags.empty + tags = Tags(tag -> schemaRev.rev) ) next( @@ -278,7 +281,8 @@ class ResourcesSpec remoteContexts, 1, time2, - subject + subject, + Some(tag) ) ) shouldEqual None } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala index 395328cab2..a023283b0c 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/model/ResourceSerializationSuite.scala @@ -23,8 +23,9 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc val instant: Instant = Instant.EPOCH val realm: Label = Label.unsafe("myrealm") val subject: Subject = User("username", realm) + val tag: UserTag = UserTag.unsafe("mytag") - private val created = + private val created = ResourceCreated( myId, projectRef, @@ -37,9 +38,11 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc remoteContextRefs, 1, instant, - subject + subject, + None ) - private val updated = + private val createdWithTag = created.copy(tag = Some(tag)) + private val updated = ResourceUpdated( myId, projectRef, @@ -54,7 +57,7 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc instant, subject ) - private val refreshed = ResourceRefreshed( + private val refreshed = ResourceRefreshed( myId, projectRef, Revision(schemas.resources, 1), @@ -67,7 +70,7 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc instant, subject ) - private val tagged = + private val tagged = ResourceTagAdded( myId, projectRef, @@ -78,7 +81,7 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc instant, subject ) - private val deprecated = + private val deprecated = ResourceDeprecated( myId, projectRef, @@ -87,7 +90,7 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc instant, subject ) - private val tagDeleted = + private val tagDeleted = ResourceTagDeleted( myId, projectRef, @@ -100,6 +103,7 @@ class ResourceSerializationSuite extends SerializationSuite with ResourceInstanc private val resourcesMapping = List( (created, loadEvents("resources", "resource-created.json"), Created), + (createdWithTag, loadEvents("resources", "resource-created-tagged.json"), Created), (updated, loadEvents("resources", "resource-updated.json"), Updated), (refreshed, loadEvents("resources", "resource-refreshed.json"), Refreshed), (tagged, loadEvents("resources", "resource-tagged.json"), Tagged),