diff --git a/build.sbt b/build.sbt index 5445fdba4..a37597e22 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,7 @@ val disciplineMunitVersion = "1.0.9" val flywayVersion = "9.20.0" val fs2AwsVersion = "6.0.4" val fs2Version = "3.9.2" -val grackleVersion = "0.13.0" +val grackleVersion = "0.14.0" val http4sBlazeVersion = "0.23.14" val http4sEmberVersion = "0.23.23" val http4sJdkHttpClientVersion = "0.9.0" @@ -18,7 +18,7 @@ val log4catsVersion = "2.6.0" val lucumaCatalogVersion = "0.44.1" val lucumaItcVersion = "0.20.3" val lucumaCoreVersion = "0.88.0" -val lucumaGraphQLRoutesVersion = "0.6.6" +val lucumaGraphQLRoutesVersion = "0.7.0" val lucumaSsoVersion = "0.6.8" val munitVersion = "0.7.29" val munitCatsEffectVersion = "1.0.7" @@ -114,7 +114,7 @@ lazy val service = project "edu.gemini" %% "gsp-graphql-skunk" % grackleVersion, "edu.gemini" %% "lucuma-catalog" % lucumaCatalogVersion, "edu.gemini" %% "lucuma-ags" % lucumaCatalogVersion, - "edu.gemini" %% "lucuma-graphql-routes-grackle" % lucumaGraphQLRoutesVersion, + "edu.gemini" %% "lucuma-graphql-routes" % lucumaGraphQLRoutesVersion, "edu.gemini" %% "lucuma-sso-backend-client" % lucumaSsoVersion, "is.cir" %% "ciris" % cirisVersion, "is.cir" %% "ciris-refined" % cirisVersion, diff --git a/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql b/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql index b597c0ab3..9ab329494 100644 --- a/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql +++ b/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql @@ -1,3 +1,16 @@ + + +# These "empty" enums (they must have at least one element) will be replaced at runtime with +# definitions based on database tables. + +enum Partner { DUMMY } +enum ConditionsMeasurementSource { DUMMY } +enum SeeingTrend { DUMMY } +enum ConditionsExpectationType { DUMMY } +enum ObsAttachmentType { DUMMY } +enum FilterType { DUMMY } +enum ProposalAttachmentType { DUMMY } + "DatasetEvent creation parameters." input AddDatasetEventInput { @@ -3169,7 +3182,7 @@ type RecordGmosSouthVisitResult { Right Ascension, choose one of the available units """ input RightAscensionInput { - microarcseconds: Long @deprecated + microarcseconds: Long microseconds: Long degrees: BigDecimal hours: BigDecimal diff --git a/modules/service/src/main/scala/lucuma/odb/Main.scala b/modules/service/src/main/scala/lucuma/odb/Main.scala index 879c66585..9caf1e5bd 100644 --- a/modules/service/src/main/scala/lucuma/odb/Main.scala +++ b/modules/service/src/main/scala/lucuma/odb/Main.scala @@ -15,7 +15,7 @@ import edu.gemini.grackle.skunk.SkunkMonitor import fs2.io.net.Network import io.laserdisc.pure.s3.tagless.S3AsyncClientOp import lucuma.core.model.User -import lucuma.graphql.routes.GrackleGraphQLService +import lucuma.graphql.routes.GraphQLService import lucuma.itc.client.ItcClient import lucuma.odb.graphql.GraphQLRoutes import lucuma.odb.graphql.ObsAttachmentRoutes @@ -238,7 +238,7 @@ object FMain extends MainParams { s3ClientOps <- s3OpsResource s3Presigner <- s3PresignerResource s3FileService = S3FileService.fromS3ConfigAndClient(awsConfig, s3ClientOps, s3Presigner) - metadataService = GrackleGraphQLService(OdbMapping.forMetadata(pool, SkunkMonitor.noopMonitor[F], enums)) + metadataService = GraphQLService(OdbMapping.forMetadata(pool, SkunkMonitor.noopMonitor[F], enums)) } yield { wsb => val obsAttachmentRoutes = ObsAttachmentRoutes.apply[F](pool, s3FileService, ssoClient, awsConfig.fileUploadMaxMb) val proposalAttachmentRoutes = ProposalAttachmentRoutes[F](pool, s3FileService, ssoClient, awsConfig.fileUploadMaxMb) diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/GraphQLRoutes.scala b/modules/service/src/main/scala/lucuma/odb/graphql/GraphQLRoutes.scala index 74ac79fc5..635062e35 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/GraphQLRoutes.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/GraphQLRoutes.scala @@ -7,10 +7,11 @@ import cats.data.OptionT import cats.effect._ import cats.implicits._ import cats.kernel.Order +import edu.gemini.grackle.Operation +import edu.gemini.grackle.Result import edu.gemini.grackle.skunk.SkunkMonitor import io.circe.Json import lucuma.core.model.User -import lucuma.graphql.routes.GrackleGraphQLService import lucuma.graphql.routes.GraphQLService import lucuma.graphql.routes.HttpRouteHandler import lucuma.graphql.routes.{Routes => LucumaGraphQLRoutes} @@ -98,11 +99,11 @@ object GraphQLRoutes { _ <- OptionT.liftF(info(user, s"New service instance.")) map = OdbMapping(pool, monitor, user, topics, itcClient, commitHash, enums, ptc, httpClient) - svc = new GrackleGraphQLService(map) { - override def query(request: ParsedGraphQLRequest): F[Either[Throwable, Json]] = + svc = new GraphQLService(map) { + override def query(request: Operation): F[Result[Json]] = super.query(request).retryOnInvalidCursorName.flatTap { - case Left(t) => warn(user, s"Internal error: ${t.getClass.getSimpleName}: ${t.getMessage}") - case Right(j) => debug(user, s"Query (success).") + case Result.InternalError(t) => warn(user, s"Internal error: ${t.getClass.getSimpleName}: ${t.getMessage}") + case _ => debug(user, s"Query (success).") } } } yield svc @@ -129,8 +130,8 @@ object GraphQLRoutes { val dsl = new Http4sDsl[F]{}; import dsl._ // borrow HttpRouteHandler from lucuma-graphql-routes but hack it to return js val h = new HttpRouteHandler(service) { - override def toResponse(result: Either[Throwable, Json]): F[Response[F]] = - result match { + override def toResponse(result: Result[Json]): F[Response[F]] = + result.toEither match { case Left(err) => super.toResponse(result) case Right(json) => Ok(s"export const $name ='${json.noSpaces}'") diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedEnumBinding.scala b/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumBinding.scala similarity index 57% rename from modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedEnumBinding.scala rename to modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumBinding.scala index aa2ef20ae..6402766d0 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedEnumBinding.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumBinding.scala @@ -3,8 +3,7 @@ package lucuma.odb.graphql.binding -import edu.gemini.grackle.Value.UntypedEnumValue - -val UntypedEnumBinding: Matcher[String] = - primitiveBinding("UntypedEnum") { case UntypedEnumValue(name) => name } +import edu.gemini.grackle.Value.EnumValue +val EnumBinding: Matcher[String] = + primitiveBinding("EnumValue") { case EnumValue(name) => name } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumeratedBinding.scala b/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumeratedBinding.scala index daff282fa..cd6ed8bb6 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumeratedBinding.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/binding/EnumeratedBinding.scala @@ -8,6 +8,6 @@ import io.circe.Json import lucuma.core.util.Enumerated def enumeratedBinding[A](implicit ev: Enumerated[A]): Matcher[A] = - TypedEnumBinding.map(b => Json.fromString(b.name)).emap { j => + EnumBinding.map(n => Json.fromString(n)).emap { j => ev.decodeJson(j).leftMap(_.message) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/binding/TagBinding.scala b/modules/service/src/main/scala/lucuma/odb/graphql/binding/TagBinding.scala index 7af7113cf..4e7e1fcb8 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/binding/TagBinding.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/binding/TagBinding.scala @@ -6,4 +6,4 @@ package lucuma.odb.graphql.binding import lucuma.odb.data.Tag val TagBinding = - TypedEnumBinding.map(v => Tag(v.name.toLowerCase)) + EnumBinding.map(n => Tag(n.toLowerCase)) diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/binding/TypedEnumBinding.scala b/modules/service/src/main/scala/lucuma/odb/graphql/binding/TypedEnumBinding.scala deleted file mode 100644 index d4910f0c8..000000000 --- a/modules/service/src/main/scala/lucuma/odb/graphql/binding/TypedEnumBinding.scala +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package lucuma.odb.graphql.binding - -import edu.gemini.grackle.EnumValue -import edu.gemini.grackle.Value.TypedEnumValue - -val TypedEnumBinding: Matcher[EnumValue] = - primitiveBinding("TypedEnum") { case TypedEnumValue(value) => value } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedVariableBinding.scala b/modules/service/src/main/scala/lucuma/odb/graphql/binding/VariableRefBinding.scala similarity index 54% rename from modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedVariableBinding.scala rename to modules/service/src/main/scala/lucuma/odb/graphql/binding/VariableRefBinding.scala index a52e8310f..fc43af070 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/binding/UntypedVariableBinding.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/binding/VariableRefBinding.scala @@ -3,7 +3,7 @@ package lucuma.odb.graphql.binding -import edu.gemini.grackle.Value.UntypedVariableValue +import edu.gemini.grackle.Value.VariableRef -val UntypedVariableBinding: Matcher[String] = - primitiveBinding("UntypedVariable") { case UntypedVariableValue(name) => name } +val VariableRefBinding: Matcher[String] = + primitiveBinding("VariableRef") { case VariableRef(name) => name } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsExpectationTypeEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsExpectationTypeEnumType.scala index 86048326a..250cce7b4 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsExpectationTypeEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsExpectationTypeEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object ConditionsExpectationTypeEnumType { EnumType( "ConditionsExpectationType", Some("Enumerated type of expected conditions."), - elems.map { case (tag, desc) => EnumValue(tag.toUpperCase, Some(desc)) } + elems.map { case (tag, desc) => EnumValueDefinition(tag.toUpperCase, Some(desc), Nil) }, + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsMeasurementSourceEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsMeasurementSourceEnumType.scala index 2f5578bd2..bfe16d865 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsMeasurementSourceEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ConditionsMeasurementSourceEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object ConditionsMeasurementSourceEnumType { EnumType( "ConditionsMeasurementSource", Some("Enumerated type of sources for observed conditions."), - elems.map { case (tag, desc) => EnumValue(tag.toUpperCase, Some(desc)) } + elems.map { case (tag, desc) => EnumValueDefinition(tag.toUpperCase, Some(desc), Nil) }, + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/Enums.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/Enums.scala index 851ddc7ed..44835dc90 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/Enums.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/Enums.scala @@ -8,7 +8,7 @@ import cats.data.NonEmptyList import cats.syntax.flatMap.* import cats.syntax.functor.* import cats.syntax.traverse.* -import edu.gemini.grackle.Directive +import edu.gemini.grackle.DirectiveDef import edu.gemini.grackle.EnumType import edu.gemini.grackle.NamedType import edu.gemini.grackle.Schema @@ -88,7 +88,7 @@ final class Enums( new Schema { def pos: SourcePos = SourcePos.instance def types: List[NamedType] = enumMeta.unreferencedTypes - def directives: List[Directive] = Nil + def directives: List[DirectiveDef] = Nil } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/FilterTypeEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/FilterTypeEnumType.scala index 297b4505f..1c5063eb6 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/FilterTypeEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/FilterTypeEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object FilterTypeEnumType { EnumType( "FilterType", Some("Enumerated type of filters."), - elems.map { case (tag, desc) => EnumValue(tag, Some(desc)) } + elems.map { case (tag, desc) => EnumValueDefinition(tag, Some(desc), Nil) }, + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ObsAttachmentTypeEnum.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ObsAttachmentTypeEnum.scala index f4d046f9e..6dcf960b3 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ObsAttachmentTypeEnum.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ObsAttachmentTypeEnum.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object ObsAttachmentTypeEnumType { EnumType( "ObsAttachmentType", Some("Enumerated type of observation attachments."), - elems.map { case tag ~ desc => EnumValue(tag.toUpperCase(), Some(desc)) } + elems.map { case tag ~ desc => EnumValueDefinition(tag.toUpperCase(), Some(desc), Nil) }, + Nil ) } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/PartnerEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/PartnerEnumType.scala index de1275fa3..5f4ce2233 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/PartnerEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/PartnerEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object PartnerEnumType { EnumType( "Partner", Some("Enumerated type of partners."), - elems.map { case tag ~ desc ~ active => EnumValue(tag.toUpperCase(), Some(desc), !active) } + elems.map { case tag ~ desc ~ _ => EnumValueDefinition(tag.toUpperCase(), Some(desc), Nil) }, // TODO: deprecated directive + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ProposalAttachmentTypeEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ProposalAttachmentTypeEnumType.scala index 887a76e08..7c5f79955 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/ProposalAttachmentTypeEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/ProposalAttachmentTypeEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object ProposalAttachmentTypeEnumType { EnumType( "ProposalAttachmentType", Some("Enumerated type of proposal attachments."), - elems.map { case tag ~ desc => EnumValue(tag.toUpperCase(), Some(desc)) } + elems.map { case tag ~ desc => EnumValueDefinition(tag.toUpperCase(), Some(desc), Nil) }, + Nil ) } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/SeeingTrendEnumType.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/SeeingTrendEnumType.scala index 4467c8184..9108100ea 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/SeeingTrendEnumType.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/SeeingTrendEnumType.scala @@ -6,7 +6,7 @@ package lucuma.odb.graphql.enums import cats.Functor import cats.syntax.all._ import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import skunk._ import skunk.codec.all._ import skunk.syntax.all._ @@ -18,7 +18,8 @@ object SeeingTrendEnumType { EnumType( "SeeingTrend", Some("Enumerated type of seeing trends."), - elems.map { case (tag, desc) => EnumValue(tag.toUpperCase, Some(desc)) } + elems.map { case (tag, desc) => EnumValueDefinition(tag.toUpperCase, Some(desc), Nil) }, + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/enums/extensions.scala b/modules/service/src/main/scala/lucuma/odb/graphql/enums/extensions.scala index 6bb136d0d..d23a227cf 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/enums/extensions.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/enums/extensions.scala @@ -5,7 +5,7 @@ package lucuma.odb.graphql.enums import cats.syntax.option.* import edu.gemini.grackle.EnumType -import edu.gemini.grackle.EnumValue +import edu.gemini.grackle.EnumValueDefinition import lucuma.core.util.Enumerated extension [A](e: Enumerated[A]) { @@ -21,7 +21,8 @@ extension [A](e: Enumerated[A]) { EnumType( typeName, description.some, - e.all.map { v => EnumValue(e.tag(v).toUpperCase(), valueName(v).some) } + e.all.map { v => EnumValueDefinition(e.tag(v).toUpperCase(), valueName(v).some, Nil) }, + Nil ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/input/DecimalInput.scala b/modules/service/src/main/scala/lucuma/odb/graphql/input/DecimalInput.scala index 9c387d90e..a93abb425 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/input/DecimalInput.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/input/DecimalInput.scala @@ -25,9 +25,9 @@ import lucuma.odb.graphql.binding._ ObjectFieldsBinding.rmap { case List( BigDecimalBinding("value", rValue), - TypedEnumBinding("units", rEnum) + EnumBinding("units", rEnum) ) => - (rValue, rEnum.map(_.name)).parTupled.flatMap { + (rValue, rEnum).parTupled.flatMap { case (value, label) => handler.lift((value, label)) match { case Some(r) => r diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/AsterismGroupMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/AsterismGroupMapping.scala index 902a8d5c1..6c4ae2005 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/AsterismGroupMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/AsterismGroupMapping.scala @@ -5,9 +5,9 @@ package lucuma.odb.graphql package mapping import edu.gemini.grackle.Query +import edu.gemini.grackle.Query.Binding import edu.gemini.grackle.Query.Filter -import edu.gemini.grackle.Query.Select -import edu.gemini.grackle.Result +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.TypeRef import lucuma.odb.data.Existence import lucuma.odb.graphql.predicate.Predicates @@ -38,19 +38,13 @@ trait AsterismGroupMapping[F[_]] ) // Make sure the asterism is filtered by existence - lazy val AsterismGroupElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - AsterismGroupType -> { - case Select("asterism", Nil, child) => - Result( - Select("asterism", Nil, - Filter( - Predicates.target.existence.eql(Existence.Present), - child - ) - ) - ) + lazy val AsterismGroupElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (AsterismGroupType, "asterism", Nil) => + Elab.transformChild { child => + Filter( + Predicates.target.existence.eql(Existence.Present), + child + ) } - ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ConstraintSetGroupMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ConstraintSetGroupMapping.scala index c74186a5c..be8d45e9b 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ConstraintSetGroupMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ConstraintSetGroupMapping.scala @@ -10,7 +10,7 @@ import edu.gemini.grackle.Predicate import edu.gemini.grackle.Predicate._ import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ -import edu.gemini.grackle.Result +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping import lucuma.core.model.Observation @@ -36,30 +36,29 @@ trait ConstraintSetGroupMapping[F[_]] ) ) - lazy val ConstraintSetGroupElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - ConstraintSetGroupType -> { - case Select("observations", List( - BooleanBinding("includeDeleted", rIncludeDeleted), - ObservationIdBinding.Option("OFFSET", rOFFSET), - NonNegIntBinding.Option("LIMIT", rLIMIT), - ), child) => - (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => - val limit = lim.fold(ResultMapping.MaxLimit)(_.value) - ResultMapping.selectResult("observations", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - Predicates.observation.existence.includeDeleted(includeDeleted), - OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) - ))), - oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), - offset = None, - limit = Some(limit + 1), - q - ) - } + lazy val ConstraintSetGroupElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (ConstraintSetGroupType, "observations", List( + BooleanBinding("includeDeleted", rIncludeDeleted), + ObservationIdBinding.Option("OFFSET", rOFFSET), + NonNegIntBinding.Option("LIMIT", rLIMIT), + )) => + Elab.transformChild { child => + (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => + val limit = lim.fold(ResultMapping.MaxLimit)(_.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + Predicates.observation.existence.includeDeleted(includeDeleted), + OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) + ))), + oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), + offset = None, + limit = Some(limit + 1), + q + ) } + } } - ) + } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ExecutionMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ExecutionMapping.scala index b0dd773f3..83d13a2ba 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ExecutionMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ExecutionMapping.scala @@ -9,16 +9,15 @@ import cats.effect.Resource import cats.syntax.bifunctor.* import cats.syntax.functor.* import cats.syntax.parallel.* -import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Env import edu.gemini.grackle.Predicate.True import edu.gemini.grackle.Predicate.and import edu.gemini.grackle.Query +import edu.gemini.grackle.Query.Binding import edu.gemini.grackle.Query.EffectHandler -import edu.gemini.grackle.Query.Environment import edu.gemini.grackle.Query.FilterOrderByOffsetLimit import edu.gemini.grackle.Query.OrderSelection -import edu.gemini.grackle.Query.Select +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.TypeRef import edu.gemini.grackle.syntax.* @@ -61,41 +60,38 @@ trait ExecutionMapping[F[_]] extends ObservationEffectHandler[F] with Predicates ) ) - lazy val ExecutionElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - ExecutionType -> { - case Select("config", List( - Generator.FutureLimit.Binding.Option(FutureLimitParam, rFutureLimit) - ), child) => - rFutureLimit.map { futureLimit => - Environment( - Env(FutureLimitParam -> futureLimit.getOrElse(Generator.FutureLimit.Default)), - Select("config", Nil, child) + lazy val ExecutionElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { + case (ExecutionType, "config", List( + Generator.FutureLimit.Binding.Option(FutureLimitParam, rFutureLimit) + )) => + Elab.liftR(rFutureLimit).flatMap { futureLimit => + Elab.env(FutureLimitParam -> futureLimit.getOrElse(Generator.FutureLimit.Default)) + } + case (ExecutionType, "datasets", List( + DatasetIdBinding.Option("OFFSET", rOFFSET), + NonNegIntBinding.Option("LIMIT", rLIMIT) + )) => + Elab.transformChild { child => + (rOFFSET, rLIMIT).parTupled.flatMap { (OFFSET, LIMIT) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + OFFSET.map(Predicates.dataset.id.gtEql).getOrElse(True), + Predicates.dataset.observation.program.isVisibleTo(user), + ))), + oss = Some(List( + OrderSelection[Dataset.Id](DatasetType / "id") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q ) } - case Select("datasets", List( - DatasetIdBinding.Option("OFFSET", rOFFSET), - NonNegIntBinding.Option("LIMIT", rLIMIT) - ), child) => - (rOFFSET, rLIMIT).parTupled.flatMap { (OFFSET, LIMIT) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("datasets", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - OFFSET.map(Predicates.dataset.id.gtEql).getOrElse(True), - Predicates.dataset.observation.program.isVisibleTo(user), - ))), - oss = Some(List( - OrderSelection[Dataset.Id](DatasetType / "id") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) - } - } + } } - ) + } + extension (e: Generator.Error) { def toResult: Result[Json] = diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosNorthStaticMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosNorthStaticMapping.scala index 092185efd..0122d4c18 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosNorthStaticMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosNorthStaticMapping.scala @@ -8,7 +8,7 @@ trait GmosNorthStaticMapping[F[_]] extends GmosStaticTables[F] { - private lazy val mapping: ObjectMapping = + lazy val GmosNorthStaticMapping: ObjectMapping = ObjectMapping( tpe = GmosNorthStaticType, fieldMappings = List( @@ -19,14 +19,4 @@ ) ) - // The GmosNorthStatic type appears in the middle of a computed JSON result. - // In order to avoid finding this mapping in that case, we need to be - // explicit about when to use this mapping. - lazy val GmosNorthStaticMapping: TypeMapping = - SwitchMapping( - GmosNorthStaticType, - List( - GmosNorthVisitType / "static" -> mapping - ) - ) } \ No newline at end of file diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosSouthStaticMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosSouthStaticMapping.scala index 11f8c90aa..22e28c744 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosSouthStaticMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GmosSouthStaticMapping.scala @@ -8,7 +8,7 @@ trait GmosSouthStaticMapping[F[_]] extends GmosStaticTables[F] { - private lazy val mapping: ObjectMapping = + lazy val GmosSouthStaticMapping: ObjectMapping = ObjectMapping( tpe = GmosSouthStaticType, fieldMappings = List( @@ -19,14 +19,4 @@ ) ) - // The GmosSouthStatic type appears in the middle of a computed JSON result. - // In order to avoid finding this mapping in that case, we need to be - // explicit about when to use this mapping. - lazy val GmosSouthStaticMapping: TypeMapping = - SwitchMapping( - GmosSouthStaticType, - List( - GmosSouthVisitType / "static" -> mapping - ) - ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GroupMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GroupMapping.scala index 0a7150d9a..ce5ef2e7a 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GroupMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/GroupMapping.scala @@ -6,11 +6,11 @@ package lucuma.odb.graphql package mapping import edu.gemini.grackle.Query +import edu.gemini.grackle.Query.Binding import edu.gemini.grackle.Query.OrderBy import edu.gemini.grackle.Query.OrderSelection import edu.gemini.grackle.Query.OrderSelections -import edu.gemini.grackle.Query.Select -import edu.gemini.grackle.Result +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping import eu.timepit.refined.types.numeric.NonNegShort @@ -37,17 +37,11 @@ trait GroupMapping[F[_]] extends GroupView[F] with ProgramTable[F] with GroupEle ) ) - lazy val GroupElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - GroupType -> { - case Select("elements", Nil, child) => - Result( - Select("elements", Nil, - OrderBy(OrderSelections(List(OrderSelection[NonNegShort](GroupElementType / "parentIndex"))), child) - ) - ) + lazy val GroupElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (GroupType, "elements", Nil) => + Elab.transformChild { child => + OrderBy(OrderSelections(List(OrderSelection[NonNegShort](GroupElementType / "parentIndex"))), child) } - ) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/MutationMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/MutationMapping.scala index 7d4052171..5ae99346a 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/MutationMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/MutationMapping.scala @@ -9,13 +9,14 @@ import cats.data.NonEmptyList import cats.effect.Resource import cats.kernel.Order import cats.syntax.all.* -import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Context +import edu.gemini.grackle.Env import edu.gemini.grackle.Path import edu.gemini.grackle.Predicate import edu.gemini.grackle.Predicate.* import edu.gemini.grackle.Query import edu.gemini.grackle.Query.* +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.ResultT import edu.gemini.grackle.Term @@ -124,8 +125,8 @@ trait MutationMapping[F[_]] extends Predicates[F] { lazy val MutationMapping: ObjectMapping = ObjectMapping(tpe = MutationType, fieldMappings = mutationFields.map(_.FieldMapping)) - lazy val MutationElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - mutationFields.foldMap(mf => Map(MutationType -> mf.Elaborator)) + lazy val MutationElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + mutationFields.foldMap(_.elaborator) // Resources defined in the final cake. def services: Resource[F, Services[F]] @@ -133,26 +134,28 @@ trait MutationMapping[F[_]] extends Predicates[F] { // Convenience for constructing a SqlRoot and corresponding 1-arg elaborator. private trait MutationField { - def Elaborator: PartialFunction[Select, Result[Query]] + def elaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] def FieldMapping: RootEffect } private object MutationField { def apply[I: ClassTag: TypeName](fieldName: String, inputBinding: Matcher[I])(f: (I, Query) => F[Result[Query]]) = new MutationField { val FieldMapping = - RootEffect.computeQuery(fieldName) { (query, tpe, env) => - query match { - case Environment(x, Select(y, z, child)) => - Nested(env.getR[I]("input").flatTraverse(i => f(i, child))) - .map(q => Environment(x, Select(y, z, q))) + RootEffect.computeChild(fieldName) { (child, _, _) => + child match { + case Environment(env, child2) => + Nested(env.getR[I]("input").flatTraverse(i => f(i, child2))) + .map(child3 => Environment(env, child3)) .value case _ => - Result.failure(s"Unexpected: $query").pure[F] + Result.failure(s"Unexpected: $child").pure[F] } } - val Elaborator = - case Select(`fieldName`, List(inputBinding("input", rInput)), child) => - rInput.map(input => Environment(Env("input" -> input), Select(fieldName, Nil, child))) + val elaborator = + case (MutationType, `fieldName`, List(inputBinding("input", rInput))) => + Elab.transformChild { child => + rInput.map(input => Environment(Env("input" -> input), child)) + } } } @@ -577,8 +580,8 @@ trait MutationMapping[F[_]] extends Predicates[F] { WHERE.getOrElse(True) )) MappedQuery( - Filter(whereObservation, Select("id", Nil, Query.Empty)), - Cursor.Context(QueryType, List("observations"), List("observations"), List(ObservationType)) + Filter(whereObservation, Select("id", None, Query.Empty)), + Context(QueryType, List("observations"), List("observations"), List(ObservationType)) ).flatMap(_.fragment) } @@ -627,7 +630,7 @@ trait MutationMapping[F[_]] extends Predicates[F] { )) val idSelect: Result[AppliedFragment] = - MappedQuery(Filter(filterPredicate, Select("id", Nil, Empty)), Cursor.Context(QueryType, List("datasets"), List("datasets"), List(DatasetType))).flatMap(_.fragment) + MappedQuery(Filter(filterPredicate, Select("id", Empty)), Context(QueryType, List("datasets"), List("datasets"), List(DatasetType))).flatMap(_.fragment) idSelect.flatTraverse { which => datasetService @@ -648,8 +651,8 @@ trait MutationMapping[F[_]] extends Predicates[F] { val idSelect: Result[AppliedFragment] = MappedQuery( - Filter(filterPredicate, Select("id", Nil, Empty)), - Cursor.Context(QueryType, List("obsAttachments"), List("obsAttachments"), List(ObsAttachmentType)) + Filter(filterPredicate, Select("id", Empty)), + Context(QueryType, List("obsAttachments"), List("obsAttachments"), List(ObsAttachmentType)) ).flatMap(_.fragment) idSelect.flatTraverse { which => @@ -705,7 +708,7 @@ trait MutationMapping[F[_]] extends Predicates[F] { // An applied fragment that selects all program ids that satisfy `filterPredicate` val idSelect: Result[AppliedFragment] = - MappedQuery(Filter(filterPredicate, Select("id", Nil, Empty)), Cursor.Context(QueryType, List("programs"), List("programs"), List(ProgramType))).flatMap(_.fragment) + MappedQuery(Filter(filterPredicate, Select("id", None, Empty)), Context(QueryType, List("programs"), List("programs"), List(ProgramType))).flatMap(_.fragment) // Update the specified programs and then return a query for the affected programs. idSelect.flatTraverse { which => @@ -731,8 +734,8 @@ trait MutationMapping[F[_]] extends Predicates[F] { val typeSelect: Result[AppliedFragment] = MappedQuery( - Filter(filterPredicate, Select("attachmentType", Nil, Empty)), - Cursor.Context(QueryType, List("proposalAttachments"), List("proposalAttachments"), List(ProposalAttachmentType)) + Filter(filterPredicate, Select("attachmentType", None, Empty)), + Context(QueryType, List("proposalAttachments"), List("proposalAttachments"), List(ProposalAttachmentType)) ).flatMap(_.fragment) typeSelect.flatTraverse { which => @@ -766,7 +769,7 @@ trait MutationMapping[F[_]] extends Predicates[F] { // An applied fragment that selects all target ids that satisfy `filterPredicate` val idSelect: Result[AppliedFragment] = - MappedQuery(Filter(filterPredicate, Select("id", Nil, Empty)), Cursor.Context(QueryType, List("targets"), List("targets"), List(TargetType))).flatMap(_.fragment) + MappedQuery(Filter(filterPredicate, Select("id", None, Empty)), Context(QueryType, List("targets"), List("targets"), List(TargetType))).flatMap(_.fragment) // Update the specified targets and then return a query for the affected targets (or an error) idSelect.flatTraverse { which => @@ -801,7 +804,7 @@ trait MutationMapping[F[_]] extends Predicates[F] { // An applied fragment that selects all group ids that satisfy `filterPredicate` val idSelect: Result[AppliedFragment] = - MappedQuery(Filter(filterPredicate, Select("id", Nil, Empty)), Cursor.Context(QueryType, List("groups"), List("groups"), List(GroupType))).flatMap(_.fragment) + MappedQuery(Filter(filterPredicate, Select("id", None, Empty)), Context(QueryType, List("groups"), List("groups"), List(GroupType))).flatMap(_.fragment) // Update the specified groups and then return a query for the affected groups (or an error) idSelect.flatTraverse { which => diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationEffectHandler.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationEffectHandler.scala index bcbeace71..5db0a9e9f 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationEffectHandler.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationEffectHandler.scala @@ -9,12 +9,11 @@ import cats.syntax.applicative.* import cats.syntax.eq.* import cats.syntax.traverse.* import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Env import edu.gemini.grackle.Query import edu.gemini.grackle.Query.EffectHandler import edu.gemini.grackle.Result import edu.gemini.grackle.ResultT -import io.circe.Json import io.circe.syntax.* import lucuma.core.model.Observation import lucuma.core.model.Program @@ -40,21 +39,22 @@ trait ObservationEffectHandler[F[_]] extends ObservationView[F] { } yield (p, o, e) } - def runEffects(queries: List[(Query, Cursor)]): F[Result[List[(Query, Cursor)]]] = + def runEffects(queries: List[(Query, Cursor)]): F[Result[List[Cursor]]] = (for { ctx <- ResultT(queryContext(queries).pure[F]) - res <- ctx.distinct.traverse { case (pid, oid, env) => + obs <- ctx.distinct.traverse { case (pid, oid, env) => ResultT(calculate(pid, oid, env)).map((oid, env, _)) } - } yield - ctx - .flatMap { case (_, oid, env) => res.find(r => r._1 === oid && r._2 === env).map(_._3).toList } - .zip(queries) - .map { case (result, (child, parentCursor)) => - val json: Json = Json.fromFields(List(fieldName -> result.asJson)) - val cursor: Cursor = CirceCursor(parentCursor.context, json, Some(parentCursor), parentCursor.fullEnv) - (child, cursor) - } + res <- ResultT(ctx + .flatMap { case (_, oid, env) => obs.find(r => r._1 === oid && r._2 === env).map(_._3).toList } + .zip(queries) + .traverse { case (result, (query, parentCursor)) => + Query.childContext(parentCursor.context, query).map { childContext => + CirceCursor(childContext, result.asJson, Some(parentCursor), parentCursor.fullEnv) + } + }.pure[F] + ) + } yield res ).value } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationMapping.scala index b8744a10f..698a4f210 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ObservationMapping.scala @@ -8,10 +8,10 @@ package mapping import cats.Eq import cats.effect.Resource import cats.syntax.functor.* -import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Env import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping @@ -70,36 +70,32 @@ trait ObservationMapping[F[_]] ) ) - lazy val ObservationElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - ObservationType -> { - case Select("timingWindows", Nil, child) => - Result( - Select("timingWindows", Nil, - FilterOrderByOffsetLimit( - pred = None, - oss = Some(List( - OrderSelection[Long](TimingWindowType / "id", true, true) - )), - offset = None, - limit = None, - child - ) - ) - ) - - case Select("obsAttachments", Nil, child) => - Result( - Select("obsAttachments", Nil, - OrderBy(OrderSelections(List(OrderSelection[ObsAttachment.Id](ObsAttachmentType / "id"))), child) - ) - ) - - case Select("itc", List(BooleanBinding.Option("useCache", rUseCache)), child) => - rUseCache.map { _ => Select("itc", Nil, child) } + lazy val ObservationElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { + + case (ObservationType, "timingWindows", Nil) => + Elab.transformChild { child => + FilterOrderByOffsetLimit( + pred = None, + oss = Some(List( + OrderSelection[Long](TimingWindowType / "id", true, true) + )), + offset = None, + limit = None, + child + ) + } + case (ObservationType, "obsAttachments", Nil) => + Elab.transformChild { child => + OrderBy(OrderSelections(List(OrderSelection[ObsAttachment.Id](ObsAttachmentType / "id"))), child) } - ) + + case (ObservationType, "itc", List(BooleanBinding.Option("useCache", rUseCache))) => + Elab.transformChild { child => + rUseCache.as(child) + } + + } def itcQueryHandler: EffectHandler[F] = { val readEnv: Env => Result[Unit] = _ => ().success diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ProgramMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ProgramMapping.scala index a72a7edbf..7cb3ad509 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ProgramMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ProgramMapping.scala @@ -12,12 +12,12 @@ import edu.gemini.grackle.Predicate import edu.gemini.grackle.Predicate._ import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.ResultT import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping import eu.timepit.refined.types.numeric.NonNegShort -import io.circe.Json import io.circe.syntax.* import lucuma.core.model.Group import lucuma.core.model.ObsAttachment @@ -75,70 +75,67 @@ trait ProgramMapping[F[_]] ) ) - lazy val ProgramElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - ProgramType -> { - case Select("observations", List( - BooleanBinding("includeDeleted", rIncludeDeleted), - ObservationIdBinding.Option("OFFSET", rOFFSET), - NonNegIntBinding.Option("LIMIT", rLIMIT), - ), child) => - (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => - val limit = lim.fold(ResultMapping.MaxLimit)(_.value) - ResultMapping.selectResult("observations", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - Predicates.observation.existence.includeDeleted(includeDeleted), - OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) - ))), - oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), - offset = None, - limit = Some(limit + 1), - q - ) - } - } - case Select("groupElements", Nil, child) => - Result( - Select("groupElements", Nil, - FilterOrderByOffsetLimit( - pred = Some(Predicates.groupElement.parentGroupId.isNull(true)), - oss = Some(List(OrderSelection[NonNegShort](GroupElementType / "parentIndex", true, true))), - offset = None, - limit = None, - child - ) - ) - ) - case Select("allGroupElements", Nil, child) => - Result( - Select("allGroupElements", Nil, - FilterOrderByOffsetLimit( - pred = None, - oss = Some(List( - OrderSelection[Option[Group.Id]](GroupElementType / "parentGroupId", true, true), - OrderSelection[NonNegShort](GroupElementType / "parentIndex", true, true) - )), - offset = None, - limit = None, - child - ) - ) - ) - case Select("obsAttachments", Nil, child) => - Result( - Select("obsAttachments", Nil, - OrderBy(OrderSelections(List(OrderSelection[ObsAttachment.Id](ObsAttachmentType / "id"))), child) - ) - ) - case Select("proposalAttachments", Nil, child) => - Result( - Select("proposalAttachments", Nil, - OrderBy(OrderSelections(List(OrderSelection[Tag](ProposalAttachmentType / "attachmentType"))), child) + lazy val ProgramElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { + + case (ProgramType, "observations", List( + BooleanBinding("includeDeleted", rIncludeDeleted), + ObservationIdBinding.Option("OFFSET", rOFFSET), + NonNegIntBinding.Option("LIMIT", rLIMIT), + )) => + Elab.transformChild { child => + (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => + val limit = lim.fold(ResultMapping.MaxLimit)(_.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + Predicates.observation.existence.includeDeleted(includeDeleted), + OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) + ))), + oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), + offset = None, + limit = Some(limit + 1), + q ) - ) + } + } + } + + case (ProgramType, "groupElements", Nil) => + Elab.transformChild { child => + FilterOrderByOffsetLimit( + pred = Some(Predicates.groupElement.parentGroupId.isNull(true)), + oss = Some(List(OrderSelection[NonNegShort](GroupElementType / "parentIndex", true, true))), + offset = None, + limit = None, + child + ) + } + + case (ProgramType, "allGroupElements", Nil) => + Elab.transformChild { child => + FilterOrderByOffsetLimit( + pred = None, + oss = Some(List( + OrderSelection[Option[Group.Id]](GroupElementType / "parentGroupId", true, true), + OrderSelection[NonNegShort](GroupElementType / "parentIndex", true, true) + )), + offset = None, + limit = None, + child + ) + } + + case (ProgramType, "obsAttachments", Nil) => + Elab.transformChild { child => + OrderBy(OrderSelections(List(OrderSelection[ObsAttachment.Id](ObsAttachmentType / "id"))), child) } - ) + + case (ProgramType, "proposalAttachments", Nil) => + Elab.transformChild { child => + OrderBy(OrderSelections(List(OrderSelection[Tag](ProposalAttachmentType / "attachmentType"))), child) + } + + } lazy val plannedTimeHandler: EffectHandler[F] = new EffectHandler[F] { @@ -149,23 +146,24 @@ trait ProgramMapping[F[_]] .estimateProgram(pid) } - def runEffects(queries: List[(Query, Cursor)]): F[Result[List[(Query, Cursor)]]] = + def runEffects(queries: List[(Query, Cursor)]): F[Result[List[Cursor]]] = (for { ctx <- ResultT(queries.traverse { case (_, cursor) => cursor.fieldAs[Program.Id]("id") }.pure[F]) - res <- ctx.distinct.traverse { pid => ResultT(calculate(pid).map(Result.success)).tupleLeft(pid) } - } yield - ctx - .flatMap(pid => res.find(r => r._1 === pid).map(_._2).toList) - .zip(queries) - .map { case (result, (child, parentCursor)) => - import lucuma.odb.json.plannedtime.given - import lucuma.odb.json.time.query.given - val json: Json = Json.fromFields(List("plannedTimeRange" -> result.asJson)) - val cursor: Cursor = CirceCursor(parentCursor.context, json, Some(parentCursor), parentCursor.fullEnv) - (child, cursor) - } + prg <- ctx.distinct.traverse { pid => ResultT(calculate(pid).map(Result.success)).tupleLeft(pid) } + res <- ResultT(ctx + .flatMap(pid => prg.find(r => r._1 === pid).map(_._2).toList) + .zip(queries) + .traverse { case (result, (query, parentCursor)) => + import lucuma.odb.json.plannedtime.given + import lucuma.odb.json.time.query.given + Query.childContext(parentCursor.context, query).map { childContext => + CirceCursor(childContext, result.asJson, Some(parentCursor), parentCursor.fullEnv) + } + }.pure[F] + ) + } yield res ).value - } - + + } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/QueryMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/QueryMapping.scala index e28243a93..25a993b5d 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/QueryMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/QueryMapping.scala @@ -11,7 +11,7 @@ import edu.gemini.grackle.Path import edu.gemini.grackle.Predicate._ import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ -import edu.gemini.grackle.Result +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping import lucuma.core.model @@ -55,7 +55,7 @@ trait QueryMapping[F[_]] extends Predicates[F] { ) ) - lazy val QueryElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = + lazy val QueryElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = List( AsterismGroup, ConstraintSetGroup, @@ -72,58 +72,60 @@ trait QueryMapping[F[_]] extends Predicates[F] { Target, TargetGroup, Targets, - ).foldMap(pf => Map(QueryType -> pf)) + ).combineAll // Elaborators below - private lazy val AsterismGroup: PartialFunction[Select, Result[Query]] = + private lazy val AsterismGroup: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = val WhereObservationBinding = WhereObservation.binding(AsterismGroupType / "observations" / "matches") { - case Select("asterismGroup", List( + case (QueryType, "asterismGroup", List( ProgramIdBinding("programId", rProgramId), WhereObservationBinding.Option("WHERE", rWHERE), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("asterismGroup", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some( - and(List( - WHERE.getOrElse(True), - Predicates.asterismGroup.program.id.eql(pid), - Predicates.asterismGroup.program.existence.includeDeleted(includeDeleted), - Predicates.asterismGroup.program.isVisibleTo(user), - )) - ), - oss = None, - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some( + and(List( + WHERE.getOrElse(True), + Predicates.asterismGroup.program.id.eql(pid), + Predicates.asterismGroup.program.existence.includeDeleted(includeDeleted), + Predicates.asterismGroup.program.isVisibleTo(user), + )) + ), + oss = None, + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } } - private lazy val ObsAttachmentTypeMeta: PartialFunction[Select, Result[Query]] = - case Select("obsAttachmentTypeMeta", Nil, child) => - Result(Select("obsAttachmentTypeMeta", Nil, + private lazy val ObsAttachmentTypeMeta: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "obsAttachmentTypeMeta", Nil) => + Elab.transformChild { child => OrderBy(OrderSelections(List(OrderSelection[Tag](ObsAttachmentTypeMetaType / "tag"))), child) - )) + } - private lazy val ProposalAttachmentTypeMeta: PartialFunction[Select, Result[Query]] = - case Select("proposalAttachmentTypeMeta", Nil, child) => - Result(Select("proposalAttachmentTypeMeta", Nil, + private lazy val ProposalAttachmentTypeMeta: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "proposalAttachmentTypeMeta", Nil) => + Elab.transformChild { child => OrderBy(OrderSelections(List(OrderSelection[Tag](ProposalAttachmentTypeMetaType / "tag"))), child) - )) + } - private lazy val Dataset: PartialFunction[Select, Result[Query]] = - case Select("dataset", List( + private lazy val Dataset: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "dataset", List( DatasetIdBinding("datasetId", rDid) - ), child) => - rDid.map { did => - Select("dataset", Nil, + )) => + Elab.transformChild { child => + rDid.map { did => Unique( Filter( And( @@ -133,83 +135,87 @@ trait QueryMapping[F[_]] extends Predicates[F] { child ) ) - ) + } } - private lazy val Datasets: PartialFunction[Select, Result[Query]] = { + private lazy val Datasets: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = val WhereDatasetBinding = WhereDataset.binding(Path.from(DatasetType)) { - case Select("datasets", List( + case (QueryType, "datasets", List( WhereDatasetBinding.Option("WHERE", rWHERE), DatasetIdBinding.Option("OFFSET", rOFFSET), NonNegIntBinding.Option("LIMIT", rLIMIT) - ), child) => - (rWHERE, rOFFSET, rLIMIT).parTupled.flatMap { (WHERE, OFFSET, LIMIT) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("datasets", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - OFFSET.map(Predicates.dataset.id.gtEql).getOrElse(True), - Predicates.dataset.observation.program.isVisibleTo(user), - WHERE.getOrElse(True) - ))), - oss = Some(List( - OrderSelection[lucuma.core.model.sequence.Dataset.Id](DatasetType / "id") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rWHERE, rOFFSET, rLIMIT).parTupled.flatMap { (WHERE, OFFSET, LIMIT) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + OFFSET.map(Predicates.dataset.id.gtEql).getOrElse(True), + Predicates.dataset.observation.program.isVisibleTo(user), + WHERE.getOrElse(True) + ))), + oss = Some(List( + OrderSelection[lucuma.core.model.sequence.Dataset.Id](DatasetType / "id") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } - } - } + } + - private lazy val ConstraintSetGroup: PartialFunction[Select, Result[Query]] = + private lazy val ConstraintSetGroup: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = val WhereObservationBinding = WhereObservation.binding(ConstraintSetGroupType / "observations" / "matches") { - case Select("constraintSetGroup", List( + case (QueryType, "constraintSetGroup", List( ProgramIdBinding("programId", rProgramId), WhereObservationBinding.Option("WHERE", rWHERE), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("constraintSetGroup", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some( - and(List( - WHERE.getOrElse(True), - Predicates.constraintSetGroup.programId.eql(pid), - Predicates.constraintSetGroup.observations.matches.existence.includeDeleted(includeDeleted), - Predicates.constraintSetGroup.observations.matches.program.existence.includeDeleted(includeDeleted), - Predicates.constraintSetGroup.observations.matches.program.isVisibleTo(user), - )) - ), - oss = Some(List( - OrderSelection[String](ConstraintSetGroupType / "key") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some( + and(List( + WHERE.getOrElse(True), + Predicates.constraintSetGroup.programId.eql(pid), + Predicates.constraintSetGroup.observations.matches.existence.includeDeleted(includeDeleted), + Predicates.constraintSetGroup.observations.matches.program.existence.includeDeleted(includeDeleted), + Predicates.constraintSetGroup.observations.matches.program.isVisibleTo(user), + )) + ), + oss = Some(List( + OrderSelection[String](ConstraintSetGroupType / "key") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } - } + } - private lazy val FilterTypeMeta: PartialFunction[Select, Result[Query]] = - case Select("filterTypeMeta", Nil, child) => - Result(Select("filterTypeMeta", Nil, + private lazy val FilterTypeMeta: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "filterTypeMeta", Nil) => + Elab.transformChild { child => OrderBy(OrderSelections(List(OrderSelection[Tag](FilterTypeMetaType / "tag"))), child) - )) + } - private lazy val Observation: PartialFunction[Select, Result[Query]] = - case Select("observation", List( + private lazy val Observation: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "observation", List( ObservationIdBinding("observationId", rOid) - ), child) => - rOid.map { oid => - Select("observation", Nil, + )) => + Elab.transformChild { child => + rOid.map { oid => Unique( Filter( And( @@ -219,54 +225,56 @@ trait QueryMapping[F[_]] extends Predicates[F] { child ) ) - ) + } } - private lazy val Observations: PartialFunction[Select, Result[Query]] = { + private lazy val Observations: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { val WhereObservationBinding = WhereObservation.binding(Path.from(ObservationType)) { - case Select("observations", List( + case (QueryType, "observations", List( ProgramIdBinding.Option("programId", rPid), WhereObservationBinding.Option("WHERE", rWHERE), ObservationIdBinding.Option("OFFSET", rOFFSET), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rPid, rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, OFFSET, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("observations", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - pid.map(Predicates.observation.program.id.eql).getOrElse(True), - OFFSET.map(Predicates.observation.id.gtEql).getOrElse(True), - Predicates.observation.existence.includeDeleted(includeDeleted), - Predicates.observation.program.isVisibleTo(user), - WHERE.getOrElse(True) - ))), - oss = Some(List( - OrderSelection[lucuma.core.model.Observation.Id](ObservationType / "id") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rPid, rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, OFFSET, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + pid.map(Predicates.observation.program.id.eql).getOrElse(True), + OFFSET.map(Predicates.observation.id.gtEql).getOrElse(True), + Predicates.observation.existence.includeDeleted(includeDeleted), + Predicates.observation.program.isVisibleTo(user), + WHERE.getOrElse(True) + ))), + oss = Some(List( + OrderSelection[lucuma.core.model.Observation.Id](ObservationType / "id") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } } - } + } - private lazy val PartnerMeta: PartialFunction[Select, Result[Query]] = - case Select("partnerMeta", Nil, child) => - Result(Select("partnerMeta", Nil, + private lazy val PartnerMeta: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "partnerMeta", Nil) => + Elab.transformChild { child => OrderBy(OrderSelections(List(OrderSelection[Tag](PartnerMetaType / "tag"))), child) - )) + } - private lazy val Program: PartialFunction[Select, Result[Query]] = - case Select("program", List( + private lazy val Program: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "program", List( ProgramIdBinding("programId", rPid), - ), child) => - rPid.map { pid => - Select("program", Nil, + )) => + Elab.transformChild { child => + rPid.map { pid => Unique( Filter( And( @@ -276,48 +284,50 @@ trait QueryMapping[F[_]] extends Predicates[F] { child ) ) - ) + } } - private lazy val Programs: PartialFunction[Select, Result[Query]] = { + private lazy val Programs: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { val WhereProgramBinding = WhereProgram.binding(Path.from(ProgramType)) { - case Select("programs", List( + case (QueryType, "programs", List( WhereProgramBinding.Option("WHERE", rWHERE), ProgramIdBinding.Option("OFFSET", rOFFSET), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (WHERE, OFFSET, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("programs", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some( - and(List( - OFFSET.map(Predicates.program.id.gtEql).getOrElse(True), - Predicates.program.existence.includeDeleted(includeDeleted), - Predicates.program.isVisibleTo(user), - WHERE.getOrElse(True) - )) - ), - oss = Some(List( - OrderSelection[lucuma.core.model.Program.Id](ProgramType / "id") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (WHERE, OFFSET, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some( + and(List( + OFFSET.map(Predicates.program.id.gtEql).getOrElse(True), + Predicates.program.existence.includeDeleted(includeDeleted), + Predicates.program.isVisibleTo(user), + WHERE.getOrElse(True) + )) + ), + oss = Some(List( + OrderSelection[lucuma.core.model.Program.Id](ProgramType / "id") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } - } + } } - private lazy val Target: PartialFunction[Select, Result[Query]] = - case Select("target", List( + private lazy val Target: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + case (QueryType, "target", List( TargetIdBinding("targetId", rPid), - ), child) => - rPid.map { pid => - Select("target", Nil, + )) => + Elab.transformChild { child => + rPid.map { pid => Unique( Filter( and(List( @@ -328,73 +338,78 @@ trait QueryMapping[F[_]] extends Predicates[F] { child ) ) - ) + } } - private lazy val TargetGroup: PartialFunction[Select, Result[Query]] = + private lazy val TargetGroup: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { val WhereObservationBinding = WhereObservation.binding(TargetGroupType / "observations" / "matches") { - case Select("targetGroup", List( + case (QueryType, "targetGroup", List( ProgramIdBinding("programId", rProgramId), WhereObservationBinding.Option("WHERE", rWHERE), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("targetGroup", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some( - and(List( - WHERE.getOrElse(True), - Predicates.targetGroup.program.id.eql(pid), - Predicates.targetGroup.target.existence.includeDeleted(includeDeleted), - Predicates.targetGroup.program.isVisibleTo(user), - )) - ), - oss = Some(List( - OrderSelection[lucuma.core.model.Target.Id](TargetGroupType / "key") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rProgramId, rWHERE, rLIMIT, rIncludeDeleted).parTupled.flatMap { (pid, WHERE, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some( + and(List( + WHERE.getOrElse(True), + Predicates.targetGroup.program.id.eql(pid), + Predicates.targetGroup.target.existence.includeDeleted(includeDeleted), + Predicates.targetGroup.program.isVisibleTo(user), + )) + ), + oss = Some(List( + OrderSelection[lucuma.core.model.Target.Id](TargetGroupType / "key") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } } + } - private lazy val Targets: PartialFunction[Select, Result[Query]] = { + private lazy val Targets: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { val WhereTargetInputBinding = WhereTargetInput.binding(Path.from(TargetType)) { - case Select("targets", List( + case (QueryType, "targets", List( WhereTargetInputBinding.Option("WHERE", rWHERE), TargetIdBinding.Option("OFFSET", rOFFSET), NonNegIntBinding.Option("LIMIT", rLIMIT), BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - (rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (WHERE, OFFSET, LIMIT, includeDeleted) => - val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) - ResultMapping.selectResult("targets", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some( - and(List( - OFFSET.map(Predicates.target.id.gtEql).getOrElse(True), - Predicates.target.existence.includeDeleted(includeDeleted), - Predicates.target.program.isVisibleTo(user), - Predicates.target.hasRole(TargetRole.Science), - WHERE.getOrElse(True) - ) - )), - oss = Some(List( - OrderSelection[lucuma.core.model.Target.Id](TargetType / "id") - )), - offset = None, - limit = Some(limit + 1), // Select one extra row here. - child = q - ) + )) => + Elab.transformChild { child => + (rWHERE, rOFFSET, rLIMIT, rIncludeDeleted).parTupled.flatMap { (WHERE, OFFSET, LIMIT, includeDeleted) => + val limit = LIMIT.foldLeft(ResultMapping.MaxLimit)(_ min _.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some( + and(List( + OFFSET.map(Predicates.target.id.gtEql).getOrElse(True), + Predicates.target.existence.includeDeleted(includeDeleted), + Predicates.target.program.isVisibleTo(user), + Predicates.target.hasRole(TargetRole.Science), + WHERE.getOrElse(True) + ) + )), + oss = Some(List( + OrderSelection[lucuma.core.model.Target.Id](TargetType / "id") + )), + offset = None, + limit = Some(limit + 1), // Select one extra row here. + child = q + ) + } } } - } + } } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ResultMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ResultMapping.scala index 856d53e44..9eb73170a 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ResultMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/ResultMapping.scala @@ -5,8 +5,8 @@ package lucuma.odb.graphql.mapping import cats.syntax.all._ import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Env import edu.gemini.grackle.Cursor.ListTransformCursor +import edu.gemini.grackle.Env import edu.gemini.grackle.Query import edu.gemini.grackle.Query.* import edu.gemini.grackle.Result @@ -43,18 +43,22 @@ object ResultMapping { } extension (self: Query.type) - def mapSomeFields(query: Query)(f: PartialFunction[Query, Result[Query]]): Result[Query] = - self.mapFields(query)(f.applyOrElse(_, Result.apply)) + def mapSomeFields(query: Query)(f: PartialFunction[Query, Query]): Query = + self.mapFields(query) { q => + f.lift(q) match + case None => q + case Some(qq) => qq + } - private def result(field: Option[String], child: Query, limit: Int, collectionField: String)(transform: Query => Query): Result[Query] = { + private def result(child: Query, limit: Int, collectionField: String)(transform: Query => Query): Result[Query] = { // Find the "matches" node under the main "targets" query and add all our filtering // and whatnot down in there, wrapping with a transform that removes the last row from the // final results. See an `SelectResultMapping` to see how `hasMore` works. - def transformMatches(q: Query): Result[Query] = + def transformMatches(q: Query): Query = Query.mapSomeFields(q) { - case Select(`collectionField`, Nil, child) => - Result(Select(collectionField, Nil, TransformCursor(Take(limit), transform(child)))) + case Select(`collectionField`, alias, child) => + Select(collectionField, alias, TransformCursor(Take(limit), transform(child))) } // If we're selecting collectionField then continue by transforming the child query, otherwise @@ -62,24 +66,23 @@ object ResultMapping { if !Query.hasField(child, collectionField) then Result.failure(s"Field `$collectionField` must be selected.") // meh else - transformMatches(child).map { child => - val env = Environment( - Env( - ResultMapping.LimitKey -> limit, - ResultMapping.AliasKey -> Query.fieldAlias(child, collectionField), - ), - child - ) - field.fold(env)(Select(_, Nil, env)) + Result { + Environment( + Env( + ResultMapping.LimitKey -> limit, + ResultMapping.AliasKey -> Query.fieldAlias(child, collectionField), + ), + transformMatches(child) + ) } } - def selectResult(field: String, child: Query, limit: Int)(transform: Query => Query): Result[Query] = - result(Some(field), child, limit, "matches")(transform) + def selectResult(child: Query, limit: Int)(transform: Query => Query): Result[Query] = + result(child, limit, "matches")(transform) def mutationResult(child: Query, limit: Int, collectionField: String)(transform: Query => Query): Result[Query] = - result(None, child, limit, collectionField)(transform) + result(child, limit, collectionField)(transform) } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala index 1b7918418..5de645d7f 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala @@ -7,9 +7,10 @@ package mapping import cats.data.Nested import cats.syntax.all._ -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Env import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping @@ -46,30 +47,32 @@ trait SubscriptionMapping[F[_]] extends Predicates[F] { lazy val SubscriptionMapping = ObjectMapping(tpe = SubscriptionType, fieldMappings = subscriptionFields.map(_.FieldMapping)) - lazy val SubscriptionElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - subscriptionFields.foldMap(mf => Map(SubscriptionType -> mf.Elaborator)) + lazy val SubscriptionElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = + subscriptionFields.foldMap(_.elaborator) // Convenience for constructing a Subscription stream and corresponding 1-arg elaborator. private trait SubscriptionField { - def Elaborator: PartialFunction[Select, Result[Query]] + def elaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] def FieldMapping: RootStream } private object SubscriptionField { def apply[I: ClassTag: TypeName](fieldName: String, inputBinding: Matcher[I])(f: (I, Query) => Stream[F, Result[Query]]) = new SubscriptionField { val FieldMapping = - RootStream.computeQuery(fieldName) { (query, tpe, env) => - query match - case Environment(a, Select(b, c, q)) => - Nested(env.getR[I]("input").flatTraverse(f(_, q))) - .map(q => Environment(a, Select(b, c, q))) + RootStream.computeChild(fieldName) { (child, _, _) => + child match + case Environment(env, child2) => + Nested(env.getR[I]("input").flatTraverse(f(_, child2))) + .map(child3 => Environment(env, child3)) .value case _ => - Result.failure(s"Unexpected: $query").pure[Stream[F, *]] + Result.failure(s"Unexpected: $child").pure[Stream[F, *]] } - val Elaborator = - case Select(`fieldName`, List(inputBinding("input", rInput)), child) => - rInput.map(input => Environment(Env("input" -> input), Select(fieldName, Nil, child))) + val elaborator = + case (SubscriptionType, `fieldName`, List(inputBinding("input", rInput))) => + Elab.transformChild { child => + rInput.map(input => Environment(Env("input" -> input), child)) + } } } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEnvironmentMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEnvironmentMapping.scala index 9b58dcf69..c63d34b6f 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEnvironmentMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEnvironmentMapping.scala @@ -8,9 +8,10 @@ package mapping import cats.effect.Resource import cats.effect.Temporal import cats.syntax.all.* -import edu.gemini.grackle.Cursor.Env +import edu.gemini.grackle.Env import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.Result import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping @@ -75,35 +76,31 @@ trait TargetEnvironmentMapping[F[_]: Temporal] child = child ) - lazy val TargetEnvironmentElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - TargetEnvironmentType -> { - case Select("asterism", List( - BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - rIncludeDeleted.map { includeDeleted => - Select("asterism", Nil, asterismQuery(includeDeleted, firstOnly = false, child)) - } - - // TODO: not yet working - case Select("firstScienceTarget", List( - BooleanBinding("includeDeleted", rIncludeDeleted) - ), child) => - rIncludeDeleted.map { includeDeleted => - Select("firstScienceTarget", Nil, Unique(asterismQuery(includeDeleted, firstOnly = true, child))) - } - - case Select("guideEnvironment", List( - TimestampBinding(ObsTimeParam, rObsTime) - ), child) => - rObsTime.map { obsTime => - Environment( - Env(ObsTimeParam -> obsTime), - Select("guideEnvironment", Nil, child) - ) - } + lazy val TargetEnvironmentElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { + case (TargetEnvironmentType, "asterism", List(BooleanBinding("includeDeleted", rIncludeDeleted))) => + Elab.transformChild { child => + rIncludeDeleted.map { includeDeleted => + asterismQuery(includeDeleted, firstOnly = false, child) + } } - ) + + // TODO: not yet working + case (TargetEnvironmentType, "firstScienceTarget", List( + BooleanBinding("includeDeleted", rIncludeDeleted) + )) => + Elab.transformChild { child => + rIncludeDeleted.map { includeDeleted => + Unique(asterismQuery(includeDeleted, firstOnly = true, child)) + } + } + + case (TargetEnvironmentType, "guideEnvironment", List( + TimestampBinding(ObsTimeParam, rObsTime) + )) => + Elab.liftR(rObsTime).flatMap { obsTime => + Elab.env(ObsTimeParam -> obsTime) + } + } def guideEnvironmentQueryHandler: EffectHandler[F] = { val readEnv: Env => Result[Timestamp] = _.getR[Timestamp](ObsTimeParam) diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetGroupMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetGroupMapping.scala index b1eb425b0..5f53ceb2b 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetGroupMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetGroupMapping.scala @@ -10,7 +10,7 @@ import edu.gemini.grackle.Predicate import edu.gemini.grackle.Predicate._ import edu.gemini.grackle.Query import edu.gemini.grackle.Query._ -import edu.gemini.grackle.Result +import edu.gemini.grackle.QueryCompiler.Elab import edu.gemini.grackle.TypeRef import edu.gemini.grackle.skunk.SkunkMapping import lucuma.core.model.Observation @@ -35,30 +35,34 @@ trait TargetGroupMapping[F[_]] ) ) - lazy val TargetGroupElaborator: Map[TypeRef, PartialFunction[Select, Result[Query]]] = - Map( - TargetGroupType -> { - case Select("observations", List( - BooleanBinding("includeDeleted", rIncludeDeleted), - ObservationIdBinding.Option("OFFSET", rOFFSET), - NonNegIntBinding.Option("LIMIT", rLIMIT), - ), child) => - (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => - val limit = lim.fold(ResultMapping.MaxLimit)(_.value) - ResultMapping.selectResult("observations", child, limit) { q => - FilterOrderByOffsetLimit( - pred = Some(and(List( - Predicates.observation.existence.includeDeleted(includeDeleted), - OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) - ))), - oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), - offset = None, - limit = Some(limit + 1), - q - ) - } + lazy val TargetGroupElaborator: PartialFunction[(TypeRef, String, List[Binding]), Elab[Unit]] = { + case ( + TargetGroupType, + "observations", + List( + BooleanBinding("includeDeleted", rIncludeDeleted), + ObservationIdBinding.Option("OFFSET", rOFFSET), + NonNegIntBinding.Option("LIMIT", rLIMIT), + ) + ) => + Elab.transformChild { child => + (rIncludeDeleted, rOFFSET, rLIMIT).parTupled.flatMap { (includeDeleted, OFFSET, lim) => + val limit = lim.fold(ResultMapping.MaxLimit)(_.value) + ResultMapping.selectResult(child, limit) { q => + FilterOrderByOffsetLimit( + pred = Some(and(List( + Predicates.observation.existence.includeDeleted(includeDeleted), + OFFSET.fold[Predicate](True)(Predicates.observation.id.gtEql) + ))), + oss = Some(List(OrderSelection[Observation.Id](ObservationType / "id", true, true))), + offset = None, + limit = Some(limit + 1), + q + ) } + } } - ) + } + } diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/util/MappingExtras.scala b/modules/service/src/main/scala/lucuma/odb/graphql/util/MappingExtras.scala index c617eff83..2ee77e293 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/util/MappingExtras.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/util/MappingExtras.scala @@ -6,8 +6,8 @@ package lucuma.odb.graphql.util import cats.Eq import cats.kernel.Order import cats.syntax.all._ +import edu.gemini.grackle.Context import edu.gemini.grackle.Cursor -import edu.gemini.grackle.Cursor.Context import edu.gemini.grackle.Mapping import edu.gemini.grackle.Path import edu.gemini.grackle.Result diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/util/PrettyPrinter.scala b/modules/service/src/main/scala/lucuma/odb/graphql/util/PrettyPrinter.scala index 8bae45f92..ebc27f098 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/util/PrettyPrinter.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/util/PrettyPrinter.scala @@ -3,9 +3,11 @@ package lucuma.odb.graphql.util -import edu.gemini.grackle.Cursor.Env -import edu.gemini.grackle.Cursor.Env.EmptyEnv -import edu.gemini.grackle.Cursor.Env.NonEmptyEnv +import cats.syntax.all.* +import edu.gemini.grackle.Directive +import edu.gemini.grackle.Env +import edu.gemini.grackle.Env.EmptyEnv +import edu.gemini.grackle.Env.NonEmptyEnv import edu.gemini.grackle.PathTerm import edu.gemini.grackle.Predicate import edu.gemini.grackle.Query @@ -47,37 +49,41 @@ object PrettyPrinter { def obj(name: String, vs: Doc*)(using DummyImplicit): Doc = elems(vs.toList).tightBracketBy(text(name) + Paren.Open, Paren.Close) - def query(q: Query): Doc = - q match + def binding(b: Binding): Doc = + obj("Binding", "name" -> quoted(b.name), "value" -> str(b.value)) - case Select(name, args, child) => - var props = List("name" -> quoted(name)) - if args.nonEmpty then props :+= "args" -> elems(args.map(binding)).tightBracketBy(Bracket.Open, Bracket.Close) - if child != Query.Empty then props :+= "child" -> query(child) - if props.length == 1 then obj("Select", props.head._2) else obj("Select", props: _*) + def directives(ds: List[Directive]): Doc = + elems(ds.map { d => prop(d.name, elems(d.args.map(binding))) }) - case Group(queries) => obj("Group", queries.map(query):_*) - case Unique(child) => obj("Unique", query(child)) - case Filter(pred, child) => obj("Filter", "pred" -> predicate(pred), "child" -> query(child)) + def query(q: Query): Doc = + q match case Component(mapping, join, child) => obj("Component", "mapping" -> str(""), "join" -> str("") , "child" -> query(child)) - case Introspect(schema, child) => obj("Introspect", "schema" -> str(""), "child" -> query(child)) + case Count(child) => ??? case Effect(handler, child) => obj("Effect", "handler" -> str(""), "child" -> query(child)) + case Empty => text("Empty") case Environment(e, child) => obj("Environment", "env" -> env(e), "child" -> query(child)) - case Wrap(name, child) => obj("Wrap", "name" -> quoted(name), "child" -> query(child)) - case Rename(name, child) => obj("Rename", "name" -> quoted(name), "child" -> query(child)) - case UntypedNarrow(tpnme, child) => obj("UntypedNarrow", "tpname" -> quoted(tpnme), "child" -> query(child)) - case Narrow(subtpe, child) => obj("Narrow", "subtpe" -> str(subtpe), "child" -> query(child)) - case Skip(sense, cond, child) => obj("Skip", "sense" -> str(sense), "cond" -> str(cond), "child" -> query(child)) + case Filter(pred, child) => obj("Filter", "pred" -> predicate(pred), "child" -> query(child)) + case Group(queries) => obj("Group", queries.map(query):_*) + case Introspect(schema, child) => obj("Introspect", "schema" -> str(""), "child" -> query(child)) case Limit(num, child) => obj("Limit", "num" -> str(num), "child" -> query(child)) + case Narrow(subtpe, child) => obj("Narrow", "subtpe" -> str(subtpe), "child" -> query(child)) case Offset(num, child) => obj("Offset", "num" -> str(num), "child" -> query(child)) case OrderBy(selections, child) => obj("OrderBy", "selections" -> orderSelections(selections), "child" -> query(child)) - case Count(name, child) => obj("Limit", "name" -> quoted(name), "child" -> query(child)) + case Select(name, alias, child) => + var props = List("name" -> quoted(name)) ++ alias.foldMap(a => List("alias" -> quoted(a))) + if child != Query.Empty then props :+= "child" -> query(child) + if props.length == 1 then obj("Select", props.head._2) else obj("Select", props: _*) case TransformCursor(f, child) => obj("TransformCursor", "f" -> text(""), "child" -> query(child)) - case Skipped => text("Skipped") - case Empty => text("Empty") - - def binding(b: Binding): Doc = - obj("Binding", "name" -> quoted(b.name), "value" -> str(b.value)) + case Unique(child) => obj("Unique", query(child)) + case UntypedFragmentSpread(name, ds) => obj("UntypedFragmentSpread", "directives" -> directives(ds)) + case UntypedInlineFragment(tpnme, ds, child) => + val ps = tpnme.foldMap(n => List("tpnme" -> quoted(n))) :+ ("directives" -> directives(ds)) :+ ("child" -> query(child)) + obj("UntypedInlineFragment", ps:_*) + case UntypedSelect(name, alias, args, ds, child) => + var props = List("name" -> quoted(name)) ++ alias.foldMap(a => List("alias" -> quoted(a))) + if args.nonEmpty then props = props :+ ("args" -> elems(args.map(binding)).tightBracketBy(Bracket.Open, Bracket.Close)) + if child != Query.Empty then props :+= "child" -> query(child) + if props.length == 1 then obj("Select", props.head._2) else obj("Select", props: _*) def env(e: Env): Doc = e match diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/util/Remapper.scala b/modules/service/src/main/scala/lucuma/odb/graphql/util/Remapper.scala index 967ad9cb5..af2b0f085 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/util/Remapper.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/util/Remapper.scala @@ -3,7 +3,7 @@ package lucuma.odb.graphql.util -import edu.gemini.grackle.Directive +import edu.gemini.grackle.DirectiveDef import edu.gemini.grackle.EnumType import edu.gemini.grackle.Field import edu.gemini.grackle.InputObjectType @@ -27,7 +27,7 @@ object Remapper { new Schema { val pos: SourcePos = s.pos val types: List[NamedType] = s.types.map(new Remapper(this).remapNamedType) - val directives: List[Directive] = s.directives + val directives: List[DirectiveDef] = s.directives } } @@ -35,23 +35,23 @@ object Remapper { private class Remapper(s: Schema) { def remapInputValue: InputValue => InputValue = { - case InputValue(name, desc, tpe, defaultValue) => - InputValue(name, desc, remapType(tpe), defaultValue) + case InputValue(name, desc, tpe, defaultValue, dirs) => + InputValue(name, desc, remapType(tpe), defaultValue, dirs) } def remapField: Field => Field = { - case Field(name, desc, args, tpe, isDeprecated, deprecationReason) => - Field(name, desc, args.map(remapInputValue), remapType(tpe), isDeprecated, deprecationReason) + case Field(name, desc, args, tpe, dirs) => + Field(name, desc, args.map(remapInputValue), remapType(tpe), dirs) } def remapNamedType: NamedType => NamedType = { - case TypeRef(_, name) => TypeRef(s, name) - case ScalarType(name, desc) => ScalarType(name, desc) - case UnionType(name, desc, members) => UnionType(name, desc, members.map(remapNamedType)) - case EnumType(name, desc, enumValues) => EnumType(name, desc, enumValues) - case InputObjectType(name, desc, inputFields) => InputObjectType(name, desc, inputFields.map(remapInputValue)) - case InterfaceType(name, desc, fields, interfaces) => InterfaceType(name, desc, fields.map(remapField), interfaces.map(remapNamedType)) - case ObjectType(name, desc, fields, interfaces) => ObjectType(name, desc, fields.map(remapField), interfaces.map(remapNamedType)) + case TypeRef(_, name) => TypeRef(s, name) + case ScalarType(name, desc, dirs) => ScalarType(name, desc, dirs) + case UnionType(name, desc, members, dirs) => UnionType(name, desc, members.map(remapNamedType), dirs) + case EnumType(name, desc, enumValues, dirs) => EnumType(name, desc, enumValues, dirs) + case InputObjectType(name, desc, inputFields, dirs) => InputObjectType(name, desc, inputFields.map(remapInputValue), dirs) + case InterfaceType(name, desc, fields, interfaces, dirs) => InterfaceType(name, desc, fields.map(remapField), interfaces.map(remapNamedType), dirs) + case ObjectType(name, desc, fields, interfaces, dirs) => ObjectType(name, desc, fields.map(remapField), interfaces.map(remapNamedType), dirs) } def remapType: Type => Type = { diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/util/SchemaSemigroup.scala b/modules/service/src/main/scala/lucuma/odb/graphql/util/SchemaSemigroup.scala index 8a45d007a..3391dc63f 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/util/SchemaSemigroup.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/util/SchemaSemigroup.scala @@ -5,7 +5,8 @@ package lucuma.odb.graphql.util import cats.Semigroup import cats.syntax.all._ -import edu.gemini.grackle.Directive +import edu.gemini.grackle.DirectiveDef +import edu.gemini.grackle.EnumType import edu.gemini.grackle.Mapping import edu.gemini.grackle.NamedType import edu.gemini.grackle.ObjectType @@ -16,13 +17,14 @@ import org.tpolecat.sourcepos.SourcePos /** A mixin that provides Semigroup[Schema]. */ trait SchemaSemigroup[F[_]] extends Mapping[F] { - private implicit val SemigroupDirective: Semigroup[Directive] = (a, b) => + private implicit val SemigroupDirectiveDef: Semigroup[DirectiveDef] = (a, b) => if (a.name != b.name) a - else Directive( + else DirectiveDef( a.name, a.description orElse b.description, + (a.args ++ b.args).distinctBy(_.name), + a.isRepeatable || b.isRepeatable, (a.locations ++ b.locations).distinct, - (a.args ++ b.args).distinctBy(_.name) ) private implicit val SemigroupNamedType: Semigroup[NamedType] = { @@ -32,6 +34,14 @@ trait SchemaSemigroup[F[_]] extends Mapping[F] { a.description orElse b.description, (a.fields ++ b.fields).distinctBy(_.name), (a.interfaces ++ b.interfaces).distinctBy(_.name), + (a.directives ++ b.directives).distinctBy(_.name), + ) + case ((a: EnumType, b: EnumType)) if sameName(a, b) => + EnumType( + a.name, + a.description, + (a.enumValues ++ b.enumValues).distinctBy(_.name).filterNot(_.name == "DUMMY"), + (a.directives ++ b.directives).distinctBy(_.name) ) // todo: other named types case (a, _) => a @@ -42,7 +52,7 @@ trait SchemaSemigroup[F[_]] extends Mapping[F] { new Schema { val pos: SourcePos = a.pos val types: List[NamedType] = concatAndMergeWhen(a.types, b.types)(sameName) - val directives: List[Directive] = concatAndMergeWhen(a.directives, b.directives)(_.name == _.name) + val directives: List[DirectiveDef] = concatAndMergeWhen(a.directives, b.directives)(_.name == _.name) } } diff --git a/modules/service/src/test/scala/lucuma/odb/graphql/mutation/addConditionsEntry.scala b/modules/service/src/test/scala/lucuma/odb/graphql/mutation/addConditionsEntry.scala index 2555da6fd..189ffd3bb 100644 --- a/modules/service/src/test/scala/lucuma/odb/graphql/mutation/addConditionsEntry.scala +++ b/modules/service/src/test/scala/lucuma/odb/graphql/mutation/addConditionsEntry.scala @@ -58,7 +58,7 @@ class addConditionsEntry extends OdbSuite { } """, expected = Left(List( - "Value of type ConditionsMeasurementSource required for 'source'" + "Value of type ConditionsMeasurementSource required for 'source' in field 'addConditionsEntry' of type 'Mutation'" )) ) } diff --git a/modules/service/src/test/scala/lucuma/odb/graphql/mutation/createProgram.scala b/modules/service/src/test/scala/lucuma/odb/graphql/mutation/createProgram.scala index 50693ca8d..b12a0fd7f 100644 --- a/modules/service/src/test/scala/lucuma/odb/graphql/mutation/createProgram.scala +++ b/modules/service/src/test/scala/lucuma/odb/graphql/mutation/createProgram.scala @@ -288,7 +288,7 @@ class createProgram extends OdbSuite { "c_mod_pi_user_id" : true, "c_mod_program_id" : true, "c_new_pi_user_id" : "u-1", - "c_new_program_id" : "p-108", + "c_new_program_id" : ${pid}, "c_mod_pi_user_type" : true, "c_new_pi_user_type" : "standard", "c_mod_pts_execution" : true,