From 9afe55ac084f3ad22dc5e8eee87dc95d394adeab Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Thu, 6 Jun 2024 16:03:05 +0200 Subject: [PATCH] Return user details for creator Encoders for `SearchEntity` have been changed due self-recursion issues when referencing `User` from a `Project`. - With the default borer encoders, the `User` is encoded without the `type` property, because the encoder is not summoned from the parent `SearchEntity`. But we want the `type` property to be included - when using `SearchEntity` as the type for `creatorDetails`, it ends up in a dead lock when the magnolia derived tapir schemas are instantiated - Using a scala3 union type would work for tapir, but not for borers json encoder For this reason, the encoders for `SearchEntity` have been defined with the discriminator on each concrete branch of the ADT using our little `EncoderSupport` utility. --- .../search/api/data/EntityConverter.scala | 5 +- .../renku/search/api/data/SearchEntity.scala | 26 ++++++++--- .../io/renku/search/api/tapir/ApiSchema.scala | 46 ++++++++++++------- .../io/renku/search/api/SearchApiSpec.scala | 15 ++++-- .../perftests/RandommerIoDocsCreator.scala | 4 +- .../search/provision/events/Projects.scala | 4 +- .../group/GroupMemberAddedSpec.scala | 4 +- .../group/GroupMemberRemovedSpec.scala | 4 +- .../group/GroupMemberUpdatedSpec.scala | 4 +- .../group/GroupRemovedProcessSpec.scala | 2 +- .../group/GroupUpdatedProvisioningSpec.scala | 2 +- .../AuthorizationAddedProvisioningSpec.scala | 2 +- ...AuthorizationRemovedProvisioningSpec.scala | 2 +- ...AuthorizationUpdatedProvisioningSpec.scala | 2 +- .../ProjectCreatedProvisioningSpec.scala | 2 +- .../ProjectUpdatedProvisioningSpec.scala | 4 +- .../user/UserRemovedProcessSpec.scala | 2 +- .../solr/client/SearchSolrClientImpl.scala | 10 ++++ .../solr/documents/EntityDocument.scala | 2 + .../solr/client/SearchSolrClientSpec.scala | 10 +++- .../solr/client/SolrDocumentGenerators.scala | 18 ++++++-- .../search/solr/documents/EntityOps.scala | 6 +++ .../search/solr/query/AuthTestData.scala | 1 + .../io/renku/solr/client/ResponseBody.scala | 3 ++ 24 files changed, 133 insertions(+), 47 deletions(-) diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala index 8313eb5c..f2fb0db8 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/EntityConverter.scala @@ -35,7 +35,10 @@ trait EntityConverter: repositories = p.repositories, visibility = p.visibility, description = p.description, - createdBy = SearchEntity.UserId(p.createdBy), + createdBy = p.creatorDetails + .flatMap(_.docs.headOption) + .map(user) + .orElse(Some(SearchEntity.User(p.createdBy))), creationDate = p.creationDate, keywords = p.keywords, score = p.score diff --git a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala index 781f665c..dd95754a 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/data/SearchEntity.scala @@ -20,17 +20,18 @@ package io.renku.search.api.data import io.bullet.borer.* import io.bullet.borer.NullOptions.given -import io.bullet.borer.derivation.MapBasedCodecs.{deriveAllCodecs, deriveCodec} +import io.bullet.borer.derivation.MapBasedCodecs +import io.renku.json.EncoderSupport import io.renku.search.model.* sealed trait SearchEntity: def id: Id - def score: Option[Double] object SearchEntity: private[api] val discriminatorField = "type" given AdtEncodingStrategy = AdtEncodingStrategy.flat(discriminatorField) - given Codec[SearchEntity] = deriveAllCodecs[SearchEntity] + given Decoder[SearchEntity] = MapBasedCodecs.deriveDecoder[SearchEntity] + given Encoder[SearchEntity] = EncoderSupport.derive[SearchEntity] final case class Project( id: Id, @@ -40,15 +41,17 @@ object SearchEntity: repositories: Seq[Repository], visibility: Visibility, description: Option[Description] = None, - createdBy: UserId, + createdBy: Option[User], creationDate: CreationDate, keywords: List[Keyword] = Nil, score: Option[Double] = None ) extends SearchEntity - final case class UserId(id: Id) - object UserId: - given Codec[UserId] = deriveCodec[UserId] + object Project: + given Encoder[Project] = + EncoderSupport.deriveWithDiscriminator[Project](discriminatorField) + given Decoder[Project] = MapBasedCodecs.deriveDecoder + end Project final case class User( id: Id, @@ -58,6 +61,11 @@ object SearchEntity: score: Option[Double] = None ) extends SearchEntity + object User: + given Encoder[User] = EncoderSupport.deriveWithDiscriminator(discriminatorField) + given Decoder[User] = MapBasedCodecs.deriveDecoder + end User + final case class Group( id: Id, name: Name, @@ -65,3 +73,7 @@ object SearchEntity: description: Option[Description] = None, score: Option[Double] = None ) extends SearchEntity + object Group: + given Encoder[Group] = EncoderSupport.deriveWithDiscriminator(discriminatorField) + given Decoder[Group] = MapBasedCodecs.deriveDecoder + end Group diff --git a/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala index f1086966..41c5e813 100644 --- a/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala +++ b/modules/search-api/src/main/scala/io/renku/search/api/tapir/ApiSchema.scala @@ -39,14 +39,26 @@ trait ApiSchema extends ApiSchema.Primitives: .derived[Group] .jsonExample(ApiSchema.exampleGroup) - given Schema[Project] = Schema + given (using userSchema: Schema[User]): Schema[Project] = Schema .derived[Project] + .modify(_.createdBy) { schemaOptUser => + // this is necessary to include the `type` property into the schema of the createdBy property + // It is not added automagically, because we use the concrete type `User` and not `SearchEntity` + // (the sealed trait). + // Using `SearchEntity` results in a deadlock when evaluating the magnolia macros from tapir. I + // tried to make all components lazy, but didn't manage to solve it + val userType = userSchema.schemaType.asInstanceOf[SProduct[User]] + val df = SProductField[User, String]( + FieldName("type"), + Schema.string, + _ => Some("User") + ) + val nextUserSchema: Schema[User] = + userSchema.copy(schemaType = userType.copy(fields = df :: userType.fields)) + schemaOptUser.copy(schemaType = SOption(nextUserSchema)(identity)) + } .jsonExample(ApiSchema.exampleProject) - given Schema[UserId] = Schema - .derived[UserId] - .jsonExample(UserId(Id("01HRA7AZ2Q234CDQWGA052F8MK"))) - given (using projectSchema: Schema[Project], userSchema: Schema[User], @@ -54,7 +66,7 @@ trait ApiSchema extends ApiSchema.Primitives: ): Schema[SearchEntity] = { val derived = Schema.derived[SearchEntity] derived.schemaType match { - case s: SCoproduct[_] => + case s: SCoproduct[?] => derived.copy(schemaType = s.addDiscriminatorField( FieldName(SearchEntity.discriminatorField), @@ -107,16 +119,16 @@ object ApiSchema: ) val exampleProject: SearchEntity = Project( - Id("01HRA7AZ2Q234CDQWGA052F8MK"), - Name("renku"), - Slug("renku"), - Some(Namespace("renku/renku")), - Seq(Repository("https://github.com/renku")), - Visibility.Public, - Some(Description("Renku project")), - UserId(Id("bla")), - CreationDate(Instant.now), - List(Keyword("data"), Keyword("science")), - Some(1.0) + id = Id("01HRA7AZ2Q234CDQWGA052F8MK"), + name = Name("renku"), + slug = Slug("renku"), + namespace = Some(Namespace("renku/renku")), + repositories = Seq(Repository("https://github.com/renku")), + visibility = Visibility.Public, + description = Some(Description("Renku project")), + createdBy = Some(exampleUser.asInstanceOf[User]), + creationDate = CreationDate(Instant.now), + keywords = List(Keyword("data"), Keyword("science")), + score = Some(1.0) ) end ApiSchema diff --git a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala index ecd85269..061cd7ad 100644 --- a/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala +++ b/modules/search-api/src/test/scala/io/renku/search/api/SearchApiSpec.scala @@ -29,6 +29,7 @@ import io.renku.search.solr.client.SearchSolrSuite import io.renku.search.solr.client.SolrDocumentGenerators.* import io.renku.search.solr.documents.{EntityDocument, User as SolrUser} import io.renku.solr.client.DocVersion +import io.renku.solr.client.ResponseBody import munit.CatsEffectSuite import org.scalacheck.Gen import scribe.Scribe @@ -43,11 +44,13 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: val project1 = projectDocumentGen( "matching", "matching description", + Gen.const(None), Gen.const(Visibility.Public) ).generateOne val project2 = projectDocumentGen( "disparate", "disparate description", + Gen.const(None), Gen.const(Visibility.Public) ).generateOne for { @@ -66,13 +69,14 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: ) test("return Project and User entities"): + val userId = ModelGenerators.idGen.generateOne + val user = SolrUser(userId, DocVersion.NotExists, FirstName("exclusive").some) val project = projectDocumentGen( "exclusive", "exclusive description", + Gen.const(None), Gen.const(Visibility.Public) - ).generateOne - val user = - SolrUser(project.createdBy, DocVersion.NotExists, FirstName("exclusive").some) + ).generateOne.copy(createdBy = userId) for { client <- IO(searchSolrClient()) searchApi = new SearchApiImpl[IO](client) @@ -81,7 +85,10 @@ class SearchApiSpec extends CatsEffectSuite with SearchSolrSuite: .query(AuthContext.anonymous)(mkQuery("exclusive")) .map(_.fold(err => fail(s"Calling Search API failed with $err"), identity)) - expected = toApiEntities(project, user).toSet + expected = toApiEntities( + project.copy(creatorDetails = ResponseBody.single(user).some), + user + ).toSet obtained = results.items.map(scoreToNone).toSet } yield assert( expected.diff(obtained).isEmpty, diff --git a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala index 78ba9b35..0e2db81f 100644 --- a/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala +++ b/modules/search-cli/src/main/scala/io/renku/search/cli/perftests/RandommerIoDocsCreator.scala @@ -29,6 +29,7 @@ import io.renku.search.http.borer.BorerEntityJsonCodec.given import io.renku.search.model.* import io.renku.search.solr.documents.{Project, User} import io.renku.solr.client.DocVersion +import io.renku.solr.client.ResponseBody import org.http4s.* import org.http4s.MediaType.application import org.http4s.Method.{GET, POST} @@ -101,7 +102,8 @@ private class RandommerIoDocsCreator[F[_]: Async: ModelTypesGenerators]( description = Some(desc), keywords = keywords, createdBy = user.id, - creationDate = creationDate + creationDate = creationDate, + creatorDetails = Some(ResponseBody.single(user)) ) -> List(user) } diff --git a/modules/search-provision/src/main/scala/io/renku/search/provision/events/Projects.scala b/modules/search-provision/src/main/scala/io/renku/search/provision/events/Projects.scala index 5a1f997a..2933c1fc 100644 --- a/modules/search-provision/src/main/scala/io/renku/search/provision/events/Projects.scala +++ b/modules/search-provision/src/main/scala/io/renku/search/provision/events/Projects.scala @@ -53,6 +53,7 @@ trait Projects: description = pc.description.map(_.toDescription), keywords = pc.keywords.map(_.toKeyword).toList, createdBy = pc.createdBy.toId, + creatorDetails = None, creationDate = pc.creationDate.toCreationDate ) @@ -68,7 +69,8 @@ trait Projects: description = pc.description.map(_.toDescription), keywords = pc.keywords.map(_.toKeyword).toList, createdBy = pc.createdBy.toId, - creationDate = pc.creationDate.toCreationDate + creationDate = pc.creationDate.toCreationDate, + creatorDetails = None ) def fromProjectUpdated(pu: v1.ProjectUpdated, orig: ProjectDocument): ProjectDocument = diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberAddedSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberAddedSpec.scala index 3bc4c9df..1b027cc1 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberAddedSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberAddedSpec.scala @@ -99,7 +99,9 @@ object GroupMemberAddedSpec: .map(_.setMembers(groupMembers)) projects <- Gen .choose(1, 6) - .flatMap(n => Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGen)) + .flatMap(n => + Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGenForInsert) + ) .map( _.map( _.copy(namespace = Some(group.namespace)) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberRemovedSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberRemovedSpec.scala index e5db5fcc..227acfd3 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberRemovedSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberRemovedSpec.scala @@ -107,7 +107,9 @@ object GroupMemberRemovedSpec: .map(_.setMembers(groupMembers)) projects <- Gen .choose(1, 6) - .flatMap(n => Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGen)) + .flatMap(n => + Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGenForInsert) + ) .map( _.map( _.copy(namespace = Some(group.namespace)) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberUpdatedSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberUpdatedSpec.scala index de8a4992..7f055478 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberUpdatedSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupMemberUpdatedSpec.scala @@ -99,7 +99,9 @@ object GroupMemberUpdatedSpec: .map(_.setMembers(groupMembers)) projects <- Gen .choose(1, 6) - .flatMap(n => Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGen)) + .flatMap(n => + Gen.listOfN(n, SolrDocumentGenerators.projectDocumentGenForInsert) + ) .map( _.map( _.copy(namespace = Some(group.namespace)) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala index a575a975..b5ce2c0b 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupRemovedProcessSpec.scala @@ -113,5 +113,5 @@ object GroupRemovedProcessSpec: def create: Gen[DbState] = for group <- SolrDocumentGenerators.groupDocumentGen - projects <- SolrDocumentGenerators.projectDocumentGen.asListOfN() + projects <- SolrDocumentGenerators.projectDocumentGenForInsert.asListOfN() yield DbState(group, projects.toSet.map(_.copy(namespace = group.namespace.some))) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupUpdatedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupUpdatedProvisioningSpec.scala index b0bd321d..0722dda7 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupUpdatedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/group/GroupUpdatedProvisioningSpec.scala @@ -147,7 +147,7 @@ object GroupUpdatedProvisioningSpec: val pgroup = SolrDocumentGenerators.partialGroupGen.generateOne val upd = EventsGenerators.groupUpdatedGen("group-update").generateOne val group2 = SolrDocumentGenerators.groupDocumentGen.generateOne - val projects = SolrDocumentGenerators.projectDocumentGen + val projects = SolrDocumentGenerators.projectDocumentGenForInsert .asListOfN(1, 6) .map( _.map(_.copy(namespace = Some(group2.namespace), version = DocVersion.NotExists)) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala index 7302efac..bacb339e 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationAddedProvisioningSpec.scala @@ -111,7 +111,7 @@ object AuthorizationAddedProvisioningSpec: val testCases = for { role <- MemberRole.valuesV1.toList - proj = SolrDocumentGenerators.projectDocumentGen.generateOne + proj = SolrDocumentGenerators.projectDocumentGenForInsert.generateOne pproj = SolrDocumentGenerators.partialProjectGen.generateOne dbState <- List(DbState.Empty, DbState.Project(proj), DbState.PartialProject(pproj)) userId = ModelGenerators.idGen.generateOne diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationRemovedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationRemovedProvisioningSpec.scala index ca5e4557..8f088cf4 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationRemovedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationRemovedProvisioningSpec.scala @@ -108,7 +108,7 @@ object AuthorizationRemovedProvisioningSpec: val userId = ModelGenerators.idGen.generateOne val userRole = ModelGenerators.memberRoleGen.generateOne val proj = - SolrDocumentGenerators.projectDocumentGen.generateOne.modifyEntityMembers( + SolrDocumentGenerators.projectDocumentGenForInsert.generateOne.modifyEntityMembers( _.addMember(userId, userRole) ) val pproj = diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationUpdatedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationUpdatedProvisioningSpec.scala index 855c9e18..3cbb1686 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationUpdatedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/AuthorizationUpdatedProvisioningSpec.scala @@ -115,7 +115,7 @@ object AuthorizationUpdatedProvisioningSpec: val testCases = for { role <- MemberRole.valuesV1.toList - proj = SolrDocumentGenerators.projectDocumentGen.generateOne + proj = SolrDocumentGenerators.projectDocumentGenForInsert.generateOne pproj = SolrDocumentGenerators.partialProjectGen.generateOne dbState <- List(DbState.Empty, DbState.Project(proj), DbState.PartialProject(pproj)) userId = ModelGenerators.idGen.generateOne diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectCreatedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectCreatedProvisioningSpec.scala index 3a279610..430b5dce 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectCreatedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectCreatedProvisioningSpec.scala @@ -197,7 +197,7 @@ object ProjectCreatedProvisioningSpec: override def toString = s"ProjectCreated: ${projectId.value.take(6)}… db=$dbState" val testCases = - val proj = SolrDocumentGenerators.projectDocumentGen.generateOne + val proj = SolrDocumentGenerators.projectDocumentGenForInsert.generateOne val pproj = SolrDocumentGenerators.partialProjectGen.generateOne val group = SolrDocumentGenerators.entityMembersGen .flatMap(em => SolrDocumentGenerators.groupDocumentGen.map(g => g.setMembers(em))) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectUpdatedProvisioningSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectUpdatedProvisioningSpec.scala index a6a0352a..d6df0a73 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectUpdatedProvisioningSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/project/ProjectUpdatedProvisioningSpec.scala @@ -105,7 +105,9 @@ object ProjectUpdatedProvisioningSpec: val testCases = val em = SolrDocumentGenerators.entityMembersGen val proj = em - .flatMap(gm => SolrDocumentGenerators.projectDocumentGen.map(_.setGroupMembers(gm))) + .flatMap(gm => + SolrDocumentGenerators.projectDocumentGenForInsert.map(_.setGroupMembers(gm)) + ) .generateOne val pproj = em .flatMap(gm => SolrDocumentGenerators.partialProjectGen.map(_.setGroupMembers(gm))) diff --git a/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserRemovedProcessSpec.scala b/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserRemovedProcessSpec.scala index 45a35694..2463b539 100644 --- a/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserRemovedProcessSpec.scala +++ b/modules/search-provision/src/test/scala/io/renku/search/provision/user/UserRemovedProcessSpec.scala @@ -137,7 +137,7 @@ object UserRemovedProcessSpec: val testCases: List[TestCase] = val role = ModelGenerators.memberRoleGen.generateOne val user = SolrDocumentGenerators.userDocumentGen.generateOne - val proj = SolrDocumentGenerators.projectDocumentGen + val proj = SolrDocumentGenerators.projectDocumentGenForInsert .asListOfN(1, 3) .map(_.map(_.modifyEntityMembers(_.addMember(user.id, role)))) .generateOne diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala index 3f1b1bda..dcdb098d 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/client/SearchSolrClientImpl.scala @@ -40,6 +40,8 @@ private class SearchSolrClientImpl[F[_]: Async](solrClient: SolrClient[F]) private val logger = scribe.cats.effect[F] private val interpreter = LuceneQueryInterpreter.forSync[F] + private val creatorDetails: FieldName = FieldName("creatorDetails") + private val typeTerms = Facet.Terms( EntityDocumentSchema.Fields.entityType, EntityDocumentSchema.Fields.entityType @@ -69,6 +71,14 @@ private class SearchSolrClientImpl[F[_]: Async](solrClient: SolrClient[F]) .withSort(solrQuery.sort) .withFacet(Facets(typeTerms)) .withFields(FieldName.all, FieldName.score) + .addSubQuery( + creatorDetails, + SubQuery( + "{!terms f=id v=$row.createdBy}", + "{!terms f=_kind v=fullentity}", + 1 + ).withFields(FieldName.all) + ) ) } yield res diff --git a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala index 5e1872ba..730593ee 100644 --- a/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala +++ b/modules/search-solr-client/src/main/scala/io/renku/search/solr/documents/EntityDocument.scala @@ -26,6 +26,7 @@ import io.renku.search.model.* import io.renku.search.model.MemberRole.* import io.renku.search.solr.schema.EntityDocumentSchema.Fields import io.renku.solr.client.DocVersion +import io.renku.solr.client.ResponseBody sealed trait EntityDocument extends SolrDocument: val score: Option[Double] @@ -49,6 +50,7 @@ final case class Project( visibility: Visibility, description: Option[Description] = None, createdBy: Id, + creatorDetails: Option[ResponseBody[User]] = None, creationDate: CreationDate, owners: Set[Id] = Set.empty, editors: Set[Id] = Set.empty, diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala index 583d5c48..49a774a2 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SearchSolrClientSpec.scala @@ -34,6 +34,7 @@ import io.renku.search.solr.schema.EntityDocumentSchema.Fields import io.renku.solr.client.DocVersion import io.renku.solr.client.QueryData import munit.CatsEffectSuite +import org.scalacheck.Gen class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: override def munitFixtures: Seq[munit.AnyFixture[?]] = @@ -41,7 +42,11 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: test("be able to insert and fetch a Project document"): val project = - projectDocumentGen("solr-project", "solr project description").generateOne + projectDocumentGen( + "solr-project", + "solr project description", + Gen.const(None) + ).generateOne for { client <- IO(searchSolrClient()) _ <- client.upsert(Seq(project.widen)) @@ -54,6 +59,7 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: _ = assert( qr.responseBody.docs.map( _.noneScore + .setCreatedBy(None) .assertVersionNot(DocVersion.NotExists) .setVersion(DocVersion.NotExists) ) contains project @@ -117,7 +123,7 @@ class SearchSolrClientSpec extends CatsEffectSuite with SearchSolrSuite: client <- IO(searchSolrClient()) entityMembers <- IO(entityMembersGen.suchThat(_.nonEmpty).generateOne) project <- IO( - projectDocumentGen + projectDocumentGenForInsert .map(p => p.setMembers(entityMembers).copy(visibility = Visibility.Private)) .generateOne ) diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala index 839a7c9b..b81bdfd5 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/client/SolrDocumentGenerators.scala @@ -26,6 +26,7 @@ import io.renku.search.model.ModelGenerators.* import io.renku.search.model.Visibility import io.renku.search.solr.documents.* import io.renku.solr.client.DocVersion +import io.renku.solr.client.ResponseBody import org.scalacheck.Gen import org.scalacheck.Gen.const import org.scalacheck.cats.implicits.* @@ -47,16 +48,26 @@ trait SolrDocumentGenerators: val differentiator = nameGen.generateOne projectDocumentGen( s"proj-$differentiator", - s"proj desc $differentiator" + s"proj desc $differentiator", + userDocumentGen.asOption + ) + + val projectDocumentGenForInsert: Gen[Project] = + val differentiator = nameGen.generateOne + projectDocumentGen( + s"proj-$differentiator", + s"proj desc $differentiator", + Gen.const(None) ) def projectDocumentGen( name: String, desc: String, + creatorGen: Gen[Option[User]], visibilityGen: Gen[Visibility] = visibilityGen ): Gen[Project] = - (idGen, idGen, visibilityGen, creationDateGen) - .mapN((projectId, creatorId, visibility, creationDate) => + (idGen, idGen, visibilityGen, creationDateGen, creatorGen) + .mapN((projectId, creatorId, visibility, creationDate, creator) => Project( projectId, DocVersion.NotExists, @@ -66,6 +77,7 @@ trait SolrDocumentGenerators: visibility, Option(Description(desc)), creatorId, + creator.map(_.copy(id = creatorId)).map(ResponseBody.single), creationDate ) ) diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala index bd50f3d0..2dbf2010 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/documents/EntityOps.scala @@ -19,6 +19,7 @@ package io.renku.search.solr.documents import io.renku.solr.client.DocVersion +import io.renku.solr.client.ResponseBody import munit.Assertions.assert object EntityOps extends EntityOps @@ -32,6 +33,11 @@ trait EntityOps: case e: Group => e.copy(score = None) } + def setCreatedBy(user: Option[ResponseBody[User]]): EntityDocument = + entity match + case e: Project => e.copy(creatorDetails = user) + case e => e + def assertVersionNot(v: DocVersion): EntityDocument = assert(entity.version != v) entity diff --git a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala index f791cc53..351400de 100644 --- a/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala +++ b/modules/search-solr-client/src/test/scala/io/renku/search/solr/query/AuthTestData.scala @@ -108,6 +108,7 @@ object AuthTestData: .projectDocumentGen( s"user${user.id}-${vis.name}-proj", "description", + SolrDocumentGenerators.userDocumentGen.asOption, Gen.const(vis) ) .map(p => (user.id, vis) -> p.copy(owners = Set(user.id))) diff --git a/modules/solr-client/src/main/scala/io/renku/solr/client/ResponseBody.scala b/modules/solr-client/src/main/scala/io/renku/solr/client/ResponseBody.scala index 631e0397..2559b33d 100644 --- a/modules/solr-client/src/main/scala/io/renku/solr/client/ResponseBody.scala +++ b/modules/solr-client/src/main/scala/io/renku/solr/client/ResponseBody.scala @@ -33,3 +33,6 @@ final case class ResponseBody[A]( object ResponseBody: given [A](using Decoder[A]): Decoder[ResponseBody[A]] = MapBasedCodecs.deriveDecoder given [A](using Encoder[A]): Encoder[ResponseBody[A]] = MapBasedCodecs.deriveEncoder + + def single[A](value: A): ResponseBody[A] = + ResponseBody(1, 0, true, Seq(value))