All Projects → redbubble → finch-sangria

redbubble / finch-sangria

Licence: BSD-3-Clause license
A simple wrapper for using Sangria from within Finch.

Programming Languages

scala
5932 projects
shell
77523 projects

Projects that are alternatives of or similar to finch-sangria

protoc-gen-twirpql
Generate A GraphQL Layer from A Twirp Server: https://twirpql.dev
Stars: ✭ 49 (+48.48%)
Mutual labels:  graphql-server, graphiql
Altair
✨⚡️ A beautiful feature-rich GraphQL Client for all platforms.
Stars: ✭ 3,827 (+11496.97%)
Mutual labels:  graphql-server, graphiql
DotNetGraphQL
A sample demonstrating how to create a GraphQL Backend in .NET and consume it from a .NET mobile app created using Xamarin
Stars: ✭ 78 (+136.36%)
Mutual labels:  graphql-server, graphiql
graphql-remote loader
Performant remote GraphQL queries from within the resolvers of a Ruby GraphQL API.
Stars: ✭ 52 (+57.58%)
Mutual labels:  graphql-server
graphql-spring-boot-mongo
Attempt to use GraphQL java library to build GraphQL server using spring boot
Stars: ✭ 12 (-63.64%)
Mutual labels:  graphql-server
express-graphql-boilerplate-hmr
Boilerplate for Express + GraphQL with HMR
Stars: ✭ 15 (-54.55%)
Mutual labels:  graphql-server
bramble
The Movio GraphQL Gateway
Stars: ✭ 423 (+1181.82%)
Mutual labels:  graphql-server
student-api-graphql
A GraphQL Wrapper for Ellucian's Banner Student REST API
Stars: ✭ 19 (-42.42%)
Mutual labels:  graphql-server
dev-feed
Flutter-based mobile app displaying a list of daily curated content from top engineering blogs and articles. Backed by a GraphQL-based API written in Kotlin..
Stars: ✭ 20 (-39.39%)
Mutual labels:  graphql-server
finch-server
Some base classes and configuration used for making a server using finch
Stars: ✭ 23 (-30.3%)
Mutual labels:  finch
ugql
🚀GraphQL.js over HTTP with uWebSockets.js
Stars: ✭ 27 (-18.18%)
Mutual labels:  graphql-server
hapi-plugin-graphiql
HAPI plugin for GraphiQL integration
Stars: ✭ 20 (-39.39%)
Mutual labels:  graphiql
graphql-bench
A super simple tool to benchmark GraphQL queries
Stars: ✭ 222 (+572.73%)
Mutual labels:  graphql-server
apollo-express-ts-server-boilerplate
No description or website provided.
Stars: ✭ 29 (-12.12%)
Mutual labels:  graphql-server
kingdom-python-server
Modular, cohesive, transparent and fast web server template
Stars: ✭ 20 (-39.39%)
Mutual labels:  graphql-server
graphql-playground
🎮 GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration)
Stars: ✭ 8,103 (+24454.55%)
Mutual labels:  graphiql
apollo-studio-community
🎡  GraphQL developer portal featuring an IDE (Apollo Explorer), auto-documentation, metrics reporting, and more. This repo is for issues, feature requests, and preview docs. 📬
Stars: ✭ 212 (+542.42%)
Mutual labels:  graphiql
Heighliner
A GraphQL Server for NewSpring Web
Stars: ✭ 13 (-60.61%)
Mutual labels:  graphql-server
graphql-query-whitelist
GraphQL query whitelisting middleware
Stars: ✭ 17 (-48.48%)
Mutual labels:  graphql-server
graphql-modules-app
TypeScripted Apollo GraphQL Server using modules and a NextJS frontend utilising React modules with Apollo hooks. All bundled with a lot of dev friendly tools in a lerna setup..
Stars: ✭ 39 (+18.18%)
Mutual labels:  graphql-server

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:

    val schema = ...           // your Sangria schema
    val context = ...          // your root context
    val errorReporter = ...    // a way to log errors, e.g. Rollbar
    val serverMetrics = ...    // your stats receiver
    val logger = ...           // a logger
    
    val executor = 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:

    import com.redbubble.graphql.GraphQlRequestDecoders.graphQlQueryDecode
    
    object GraphQlApi {
      val stats = StatsReceiver.stats
    
      def graphQlGet: Endpoint[Json] =
        get("graphql" :: graphqlQuery) { query: GraphQlQuery =>
          executeQuery(query)
        }
    
      def graphQlPost: Endpoint[Json] =
        post("graphql" :: jsonBody[GraphQlQuery]) { query: GraphQlQuery =>
          executeQuery(query)
        }
    
      private def executeQuery(query: GraphQlQuery): Future[Output[Json]] = {
        val operationName = query.operationName.getOrElse("unnamed_operation")
        stats.counter("count", operationName).incr()
        Stat.timeFuture(stats.stat("execution_time", operationName)) {
          runQuery(query)
        }
      }
    
      private def runQuery(query: GraphQlQuery): Future[Output[Json]] = {
        val result = executor.execute(query)(globalAsyncExecutionContext)
    
        // Do our best to map the type of error back to a HTTP status code
        result.map {
          case SuccessfulGraphQlResult(json) => Output.payload(json, Status.Ok)
          case ClientErrorGraphQlResult(json, _) => Output.payload(json, Status.BadRequest)
          case BackendErrorGraphQlResult(json, _) => Output.payload(json, Status.InternalServerError)
        }
      }
    }
  2. Bring the response encoder into scope when you create your Service:

    import com.redbubble.graphql.GraphQlEncoders.graphQlResultEncode
    
    val api = GraphQlApi.graphQlGet :+: GraphQlApi.graphQlPost
    val service = 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:

    object ExploreApi {
      private val graphiQlPath = "/graphiql.html"
    
      def explore: Endpoint[Response] = get("explore") {
        classpathResource(graphiQlPath).map(fromStream) match {
          case Some(content) => asyncHtmlResponse(Status.Ok, AsyncStream.fromReader(content, chunkSize = 512.kilobytes.inBytes.toInt))
          case None => textResponse(Status.InternalServerError, Buf.Utf8(s"Unable to find GraphiQL at '$graphiQlPath'"))
        }
      }
    
        private def classpathResource(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
//

import shapeless.tag
import shapeless.tag._

trait PixelWidthTag
type PixelWidth = Int @@ PixelWidthTag
def PixelWidth(w: Int): @@[Int, PixelWidthTag] = tag[PixelWidthTag](w)

//
// Define your GraphQL type for the tagged type
//

private val widthRange = 1 to MaxImageDimension
private implicit val widthInput = new ScalarToInput[PixelWidth]

private case object WidthCoercionViolation
    extends ValueCoercionViolation(s"Width in pixels, between ${widthRange.start} and ${widthRange.end}")

private def parseWidth(i: Int) = intValueFromInt(i, widthRange, PixelWidth, () => WidthCoercionViolation)

val WidthType = intScalarType(
  "width",
  s"The width of an image, in pixels, between ${widthRange.start} and ${widthRange.end} (default $DefaultImageWidth).",
  parseWidth, () => WidthCoercionViolation)

val WidthArg: 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 type
trait PushNotificationTokenTag
type PushNotificationToken = String @@ PushNotificationTokenTag
def PushNotificationToken(t: String): @@[String, PushNotificationTokenTag] = tag[PushNotificationTokenTag](t)

// GraphQL type
private case object PushNotificationTokenCoercionViolation
    extends ValueCoercionViolation(s"Push notification token expected")

private def parseToken(s: String): Either[PushNotificationTokenCoercionViolation.type, PushNotificationToken] =
  Right(PushNotificationToken(s))

val PushNotificationTokenType =
  stringScalarType(
    "PushNotificationToken", s"An iOS push notification token.",
    parseToken, () => PushNotificationTokenCoercionViolation
  )

val PushNotificationTokenArg =
  Argument("token", PushNotificationTokenType, description = s"An iOS push notification token.")


//
// Input type for our type
//
val FieldPushNotificationToken = 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."
)

val RegisterDeviceType: InputObjectType[DefaultInput] =
  InputObjectType(
    name = "RegisterDevice",
    description = "Register device fields.",
    fields = List(FieldPushNotificationToken, FieldBundleId, FieldAppVersion, FieldOsVersion)
  )

val RegisterDeviceArg = Argument(InputFieldName, RegisterDeviceType, "Register device fields.")

//
// Let's use that type in a mutation
//

object DeviceRegistration extends InputHelper {
  def registerDevice(ctx: Context[RootContext, Unit]): Action[RootContext, RegisteredDevice] = {
    val token = ctx.inputArg(FieldPushNotificationToken).flatten
    val registeredDevice = for {
      bundleId <- ctx.inputArg(FieldBundleId)
      appVersion <- ctx.inputArg(FieldAppVersion).flatMap(fromRawVersion)
      osVersion <- ctx.inputArg(FieldOsVersion).flatMap(fromRawVersion)
    } yield {
      val device = Device.device(token, App(bundleId, appVersion), osVersion)
      ctx.ctx.registerDevice(device)
    }
    registeredDevice.getOrElse(Future.exception(graphQlError("Unable to parse device input fields"))).asScala
  }
}

val MutationType: 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.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].