Skip to content

Latest commit

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Build status

Finch GraphQL support

Some simple wrappers around Sangria to support its use in Finch.

It is a small layer, that is reasonably opininated, which may not be to your liking. In particular:

  • We transport GraphQL queries as JSON, over HTTP. This necessitates some nasties from time to time.
  • We use Twitter classes instead of the standard library, for things like Future and Try.
  • We use Futures containing Options or Eithers instead a failing Future. Failing Futures are only used for things that we'd not reasonably expect a client to be able to handle (i.e. something catastrophic).
  • We handle variables in the form of a JSON encoded string (for example from GraphiQL), as well as a straight JSON object.
  • We do our best to give back semi-sane HTTP status codes.
  • We expect that you want strong types for things.

There are some things that need improvement, including:

  • We are hard coded to Circe, it should be fairly easy to decouple it should you so wish.
  • In the same vein, the executor returns Json, mainly because of the CirceResultMarshaller. Ideally both of these would use some form of class that represented the variables/results, and defined an InputUnmarshaller and a ResultMarshaller for them respectively. In particular, this leads to the unpleasantness with the re-parsing of the JSON returned from the underlying executor to find the status of the result.

If you like this, you might like other open source code from Redbubble:

  • rb-scala-utils - Miscellaneous utilities (common code) for building Scala-based services, using Finch (on which this project depends).
  • finch-template - A template project for Finch-based services.
  • rb-graphql-template - A template for Scala HTTP GraphQL services.
  • finagle-hawk - HTTP Holder-Of-Key Authentication Scheme for Finagle.

Setup

You will need to add something like the following to your build.sbt:

resolvers +=Resolver.jcenterRepo
libraryDependencies +="com.redbubble"%%"finch-sangria"%"0.3.8"

Usage

  1. Configure the executor:

    valschema= ... // your Sangria schemavalcontext= ... // your root contextvalerrorReporter= ... // a way to log errors, e.g. RollbarvalserverMetrics= ... // your stats receivervallogger= ... // a loggervalexecutor=GraphQlQueryExecutor.executor(
    schema, context, maxQueryDepth =10)(errorReporter, serverMetrics, logger)

Set the max depth to whatever suits your schema (you'll likely need >= 10 for the introspection query).

  1. Write your endpoint:

    importcom.redbubble.graphql.GraphQlRequestDecoders.graphQlQueryDecodeobjectGraphQlApi {
    valstats=StatsReceiver.stats
    defgraphQlGet:Endpoint[Json] =
    get("graphql":: graphqlQuery) { query: GraphQlQuery=>
    executeQuery(query)
    }
    defgraphQlPost:Endpoint[Json] =
    post("graphql":: jsonBody[GraphQlQuery]) { query: GraphQlQuery=>
    executeQuery(query)
    }
    privatedefexecuteQuery(query: GraphQlQuery):Future[Output[Json]] = {
    valoperationName= query.operationName.getOrElse("unnamed_operation")
    stats.counter("count", operationName).incr()
    Stat.timeFuture(stats.stat("execution_time", operationName)) {
    runQuery(query)
    }
    }
    privatedefrunQuery(query: GraphQlQuery):Future[Output[Json]] = {
    valresult= executor.execute(query)(globalAsyncExecutionContext)
    // Do our best to map the type of error back to a HTTP status code
    result.map {
    caseSuccessfulGraphQlResult(json) =>Output.payload(json, Status.Ok)
    caseClientErrorGraphQlResult(json, _) =>Output.payload(json, Status.BadRequest)
    caseBackendErrorGraphQlResult(json, _) =>Output.payload(json, Status.InternalServerError)
    }
    }
    }
  2. Bring the response encoder into scope when you create your Service:

    importcom.redbubble.graphql.GraphQlEncoders.graphQlResultEncodevalapi=GraphQlApi.graphQlGet :+:GraphQlApi.graphQlPost
    valservice= api.toServiceAs[Application.Json]
    Http.server.serve(":8080", service)

GraphiQL

If you want to integrate GraphiQL (you should), it's pretty easy.

  1. Pull down the latest GraphiQL file.

  2. You may need to adjust the paths within the GraphiQL file if you're using versioned paths, etc.

  3. Stick it somewhere in your classpath.

  4. Write an endpoint for it:

    objectExploreApi {
    privatevalgraphiQlPath="/graphiql.html"defexplore:Endpoint[Response] = get("explore") {
    classpathResource(graphiQlPath).map(fromStream) match {
    caseSome(content) => asyncHtmlResponse(Status.Ok, AsyncStream.fromReader(content, chunkSize =512.kilobytes.inBytes.toInt))
    caseNone=> textResponse(Status.InternalServerError, Buf.Utf8(s"Unable to find GraphiQL at '$graphiQlPath'"))
    }
    }
    privatedefclasspathResource(name: String):Option[InputStream] =Option(getClass.getResourceAsStream(name))
    }

Other Fun Bits

We've added some other bits & pieces to make using Sangria easier.

Scalar types

There are various helpers that can help you define Scalar types. For example to add support for a tagged type:

//// Set up a tagged type//importshapeless.tagimportshapeless.tag._traitPixelWidthTagtypePixelWidth=Int@@PixelWidthTagdefPixelWidth(w: Int):@@[Int, PixelWidthTag] = tag[PixelWidthTag](w)
//// Define your GraphQL type for the tagged type//privatevalwidthRange=1 to MaxImageDimensionprivateimplicitvalwidthInput=newScalarToInput[PixelWidth]
privatecaseobjectWidthCoercionViolationextendsValueCoercionViolation(s"Width in pixels, between ${widthRange.start} and ${widthRange.end}")
privatedefparseWidth(i: Int) = intValueFromInt(i, widthRange, PixelWidth, () =>WidthCoercionViolation)
valWidthType= intScalarType(
"width",
s"The width of an image, in pixels, between ${widthRange.start} and ${widthRange.end} (default $DefaultImageWidth).",
parseWidth, () =>WidthCoercionViolation)
valWidthArg:Argument[PixelWidth] =Argument(
name ="width",
argumentType =OptionInputType(WidthType),
description =s"The width of an image, in pixels, between ${widthRange.start} and ${widthRange.end} (default $DefaultImageWidth).", defaultValue =DefaultImageWidth)

Input types

We've also added support for input types, in a similar way to how other types are handled, they are typesafe.

// Tagged typetraitPushNotificationTokenTagtypePushNotificationToken=String@@PushNotificationTokenTagdefPushNotificationToken(t: String):@@[String, PushNotificationTokenTag] = tag[PushNotificationTokenTag](t)
// GraphQL typeprivatecaseobjectPushNotificationTokenCoercionViolationextendsValueCoercionViolation(s"Push notification token expected")
privatedefparseToken(s: String):Either[PushNotificationTokenCoercionViolation.type, PushNotificationToken] =Right(PushNotificationToken(s))
valPushNotificationTokenType=
stringScalarType(
"PushNotificationToken", s"An iOS push notification token.",
parseToken, () =>PushNotificationTokenCoercionViolation
)
valPushNotificationTokenArg=Argument("token", PushNotificationTokenType, description =s"An iOS push notification token.")
//// Input type for our type//valFieldPushNotificationToken=InputField(
"token",
OptionInputType(PushNotificationTokenType),
"If available, the push notification token for the device. May be empty if the user has not given permission to send notifications."
)
valRegisterDeviceType:InputObjectType[DefaultInput] =InputObjectType(
name ="RegisterDevice",
description ="Register device fields.",
fields =List(FieldPushNotificationToken, FieldBundleId, FieldAppVersion, FieldOsVersion)
)
valRegisterDeviceArg=Argument(InputFieldName, RegisterDeviceType, "Register device fields.")
//// Let's use that type in a mutation//objectDeviceRegistrationextendsInputHelper {
defregisterDevice(ctx: Context[RootContext, Unit]):Action[RootContext, RegisteredDevice] = {
valtoken= ctx.inputArg(FieldPushNotificationToken).flatten
valregisteredDevice=for {
bundleId <- ctx.inputArg(FieldBundleId)
appVersion <- ctx.inputArg(FieldAppVersion).flatMap(fromRawVersion)
osVersion <- ctx.inputArg(FieldOsVersion).flatMap(fromRawVersion)
} yield {
valdevice=Device.device(token, App(bundleId, appVersion), osVersion)
ctx.ctx.registerDevice(device)
}
registeredDevice.getOrElse(Future.exception(graphQlError("Unable to parse device input fields"))).asScala
}
}
valMutationType:ObjectType[RootContext, Unit] =ObjectType(
"MutationAPI",
description ="The Redbubble iOS Mutation API.",
fields[RootContext, Unit](
Field(
name ="registerDevice",
arguments =List(RegisterDeviceArg),
fieldType =OptionType(RegisteredDeviceType),
resolve = registerDevice
)
)
)

Release

For contributors, a cheat sheet to making a new release:

$ git commit -m "New things"&& git push
$ git tag -a v0.0.3 -m "v0.0.3"
$ git push --tags
$ ./sbt publish

Contributing

Issues and pull requests are welcome. Code contributions should be aligned with the above scope to be included, and include unit tests.

About

A simple wrapper for using Sangria from within Finch.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages