Giter Site home page Giter Site logo

graphql-api's Introduction

graphql-api

CircleCI Documentation Status

graphql-api helps you implement a robust GraphQL API in Haskell. By the time a query makes it to your handler you are dealing with strong, static types that make sense for your problem domain. All your handlers are normal Haskell functions because we derive their type signature from the schema. If you have used servant, this will sound familiar.

The library provides type combinators to create a GraphQL schema, and functions to parse and evaluate queries against the schema.

You can find the latest release on hackage.

We implement the GraphQL specification as best as we can in Haskell. We figure they know what they're doing. Even if an alternative API or behaviour looks nicer, we will defer to the spec.

Tutorial

A simple graphql-api tutorial can be read at readthedocs.io.

To follow along and get your hands dirty, clone this repository, enter the graphql-api root directory, and run:

stack repl tutorial

Example

Say we have a simple GraphQL schema like:

type Hello {
  greeting(who: String!): String!
}

which defines a single top-level type Hello which contains a single field, greeting, that takes a single, required argument who.

We can define this schema in Haskell and implement a simple handler like so:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

import Data.Text (Text)
import Data.Monoid ((<>))

import GraphQL
import GraphQL.API
import GraphQL.Resolver (Handler, returns)

type Hello = Object "Hello" '[]
  '[ Argument "who" Text :> Field "greeting" Text ]

hello :: Handler IO Hello
hello = pure (\who -> returns ("Hello " <> who))

run :: Text -> IO Response
run = interpretAnonymousQuery @Hello hello

We require GHC 8.0.2 or later for features like the @Hello type application, and for certain bug fixes. We also support GHC 8.2.

With the code above we can now run a query:

run "{ greeting(who: \"mort\") }"

Which will produce the following GraphQL response:

{
  "data": {
    "greeting": "Hello mort"
  }
}

Status

Our current goal is to gather feedback. We have learned a lot about GraphQL in the course of making this library, but we don't know what a good GraphQL library looks like in Haskell. Please let us know what you think. We won't mind if you file a bug telling us how good the library is.

Because we're still learning, we make no guarantees about API stability, or anything at all really.

We are tracking open problems, missing features & wishlist items in GitHub's issue tracker.

Roadmap

  • Near future:
    • Better error messages (this is really important to us)
    • Full support for recursive data types
    • Close off loose ends in current implementation & gather feedback
  • Medium future:
    • Full schema validation
    • Schema introspection
    • Stabilize public API
  • Long term:
    • Derive client implementations from types
    • Allow users to implement their own type combinators

References

Copyright

All files Copyright (c) 2016-2017 Thomas E. Hunger & Jonathan M. Lange, except:

  • src/GraphQL/Internal/Syntax/AST.hs
  • src/GraphQL/Internal/Syntax/Encoder.hs
  • src/GraphQL/Internal/Syntax/Parser.hs

for which see LICENSE.BSD3 in this repository.

graphql-api's People

Contributors

harendra-kumar avatar jamesdabbs avatar jml avatar kquick avatar marcosh avatar shou avatar sunwukonga avatar teh avatar theobat avatar thiagorp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-api's Issues

Homogenize various type class names

From discussion in #92 (comment)

we don't have super consistent naming so we could do better:

$ gg -h "^class " | sort | uniq
class BuildFieldResolver m fieldResolverType where
class Defaultable a where
class FromValue a where
class GenericAnnotatedInputType (f :: Type -> Type) where
class GenericEnumValues (f :: Type -> Type) where
class GenericFromValue (f :: Type -> Type) where
class GenericInputObjectFieldDefinitions (f :: Type -> Type) where
class GraphQLEnum a where
class GraphQLError e where
class HasAnnotatedInputType a where
class HasAnnotatedType a where
class HasArgumentDefinition a where
class HasFieldDefinition a where
class HasFieldDefinitions a where
class HasInterfaceDefinition a where
class HasInterfaceDefinitions a where
class HasName a where
class HasObjectDefinition a where
class HasResolver m a where
class RunFields m a where
class RunUnion m union objects where
class ToValue a where
class UnionTypeObjectTypeDefinitionList a where

List is too general

GraphQL says lists are homogeneous. All elements should be the same type.

Our type is currently:

newtype List = List [Value] deriving (Eq, Ord, Show)

We could change it to:

data List
  = IntList [Int32]
  | FloatList [Double]
  -- ... and so forth

I don't know if this is a good idea or not, but worth experimenting after other bits are more coherent.

Have an InputObject combinator in the same spirit as Enum

Right now enums in our schema are marked explictly as such:

data DogCommandEnum = Sit | Down | Heel deriving (Show, Eq, Ord, Generic)
instance GraphQLEnum DogCommandEnum

type  D = Argument "dogCommand" ( Enum "DogCommand" DogCommandEnum) :> Field "doesKnowCommand" Bool

it might make sense to have an InputObject (name :: Symbol) (t :: Type) as well to mark input objects. This would be more consistent with Enum, and normal Object. It would allow us to require a specific ReadInputObject type class instead of FromValue but whether that's desirable is an open question.

Not a super clear design choice overall.

Rename 'Values' to 'Const'

Or "Constant". Or "Constants". We talked a bit about this in #19. The thing about our "Values" module is that it only refers to constants, not variables. This is a super useful distinction that becomes more obvious when looking at validation code.

Accompanying change should consider changing ToValue and FromValue to also use "constant" (or whatever) in the name.

Resolver lacks user-facing documentation

According to #92 (comment), and because it's not in Internal, HasResolver is a public API. However, the module has no doc comment, and HasResolver has no documentation.

If we intend people to provide implementations of HasResolver, we should document the class and its methods, and say what the contract of the implementations is.

If not, we should shift it to Internal.

Add derive-Generic input reader for record types to support input objects

We have several constraints:

  1. Should be easy to use - this points to using a Haskell data type over e.g. a Map Text Value.
  2. Input must be validated against a definition: This allows both a type-class checking against Object name i f and a derive Generic record.
  3. Definition itself must be valid in the GraphQL sense. This can be derived both from an Object name i f and a derive Generic record.

Would be nice but not necessary:

  • Symmetry with handler definitions, i.e. using Object n i f to define input objects as well.
  • Make defaults work somehow, but we have this issue already, and I'm not even sure how it's meant to work when reading e.g. graphql/graphql-js#385

I think 1 in particular points to a derive Generic style input. We could add that to FromValue.

Support union types

This is a tricky one: To support union types we need type-safe, open sum types that can combine both the correct handler for an Object with the field selection from an inline fragment - i.e. only evaluate the inline fragment if the server returned a specific handler.

I have some parts of the puzzle working (the open sum type) but I'm not entirely sure yet how the end-user code will work.

Result in Resolver is a bit cumbersome

Neither @jml nor @teh like Result in Resolver very much.

jml thinks we should have something like a writer monad, since error messages are more-or-less orthogonal to the output.

teh liked the tuple we had before.

Filing as bug because this means a public API change.

Set up readthedocs

I've created haskell-graphql-api.readthedocs.org.

Still to do:

  • Give @teh maintainer access (teh needs a readthedocs username)
  • Add badge & link to README.md
  • Confirm that webhook actually works (NB: I didn't go for the automated process because I'm not comfortable giving other services admin access)

Ugly Show output appearing in error messages

We should have a type class for formatting values for clients, implement that for things that we're validating etc., and then use it when generating error messages.

Show should just be for us debugging Haskell library / REPL use.

Rename 'buildResolver'

Naming something to imply that it'll get used partially sometimes is not very Haskell.

e.g. it's map, not buildMapper. it's filter, not buildFilterer.

I suggest execute or resolve, or something similarly direct.

Implement default input values

default values are used for input arguments, e.g. type Greet { say(word: String = "hello"): String }

This poses a problem for us because the API is specified as a type, and Haskell does not support encoding terms as types in a straight-forward manner, so we can't make the default value part of the type signature.

We do however need to be able to access defaults independently of running a query for features such as introspection, and validation (input type coercion).

What we can do instead is use a unique type (e.g. newtypeing) and a class instance to return the default value, thus mapping from the type world to the value world, and allowing a type in the signature.

This can be done on a per-type level, e.g.

type Query = Object "Query" '[]
  '[ Argument "age" NewtypedInt :> Field "root" Text ]
instance Defaultable NewtypedInt where defaultFor _ = Just 33

or we could use Symbol kinds to look up defaults. This could be slightly more fragile due to colliding instances, e.g.

type Query = Object "Query" '[]
  '[ ArgumentWithDefault "age" Int "defaultAge" :> Field "root" Text ]
instance Defaultable "defaultAge" where defaultFor = Just 33

The solution above could be made slightly safer by using the whole argument as the instance class variable, though this too can suffer from accidental collisions in large APIs. There may be a unique type combination for indexing but it's not immediately obvious what that would be. Example:

instance Defaultable (Argument "age" Int) where defaultFor = Just 33

An alternative is to have a handler to handle requests, and a parallel handler to return defaults where required. E.g.

buildResolver @Query queryHandler defaultHandler

IMO that imposes a lot of work for handlers that don't have defaults (which seems to be most of them AFAICT).

The solutions so far are all conforming to the spec. We could also compromise conformance and decide to e.g. validate default arguments only in query resolution, and to not export them for introspection.

Make a backwards compatible API

Extensions like TypeApplications cause problems for other Haskell implementations like Eta. The way around this is to use Proxy a arguments when you want to send type arguments to a particular function.

A good solution that can stay compatible with other Haskell implementations as well is to:

  • Add a CPP constant called HAS_TYPE_APPLICATIONS that is automatically turned on for GHC 8 via a Cabal conditional.
  • All source code uses of type applications should be #ifdef'd using the defined constant above.

I'd be happy to take this up if this sounds like a good approach.

Ugly `Show` output appearing in error messages

We should have a type class for formatting values for clients, implement that for things that we're validating etc., and then use it when generating error messages.

Show should just be for us debugging Haskell library / REPL use.

Validate schema before using it for resolution

In #42, @teh and I both agreed that we should ideally not be checking for schema validity while we are resolving values.

The difficulty is that we need to construct values from the type-level schema in order to do the work of resolution.

The solution, as I see it, is to construct a value-level representation of the schema before resolving and to pass that as a parameter to buildResolver. As we traverse down the selection set that represents the user query, we would also traverse down the value-level schema, shucking off the top-most layer.

Question: mapping over a Handler?

This is a reproduction of an open Stack Overflow question, which I think might get more visibility here.

I have created a simple API for testing purposes:

type Query = Object "Query" '[]
  '[ Argument "id" Text :> Field "test" (Maybe Foo) ]

type Foo = Object "Foo" '[]
  '[ Field "name" Text ]

I also have a Haskell type that represents the Foo resource, as well as a function for retrieving it from the database:

data ServerFoo = ServerFoo
  { name :: Text
  } deriving (Eq, Show)

lookupFoo :: Text -> IO (Maybe ServerFoo)

Now, I want to implement a Handler for Query. To start, I implemented a Handler for Foo:

viewFoo :: ServerFoo -> Handler IO Foo
viewFoo ServerFoo { name } = pure $ pure name

Then I went to implement the root handler, but I realized I am not sure how to use my viewFoo function when I end up with with a Maybe ServerFoo. I tried this:

handler :: Handler IO Query
handler = pure $ \fooId -> do
  foo <- lookupFoo fooId
  sequence $ fmap viewFoo foo

However, this does not work. It produces the following type error:

• Couldn't match type ‘IO Text’
                 with ‘Object "Foo" '[] '[Field "name" Text]’
  Expected type: ServerFoo
                 -> IO (Object "Foo" '[] '[Field "name" Text])
    Actual type: ServerFoo -> Handler IO Foo
• In the first argument of ‘fmap’, namely ‘viewFoo’
  In the second argument of ‘($)’, namely ‘fmap viewFoo foo’
  In a stmt of a 'do' block: (sequence $ fmap viewFoo foo)

This seems a bit odd to me, given that the examples I’ve read would seem to imply that Handlers are designed to be compositional. Indeed, they appear to be, since making a few tweaks to remove the Maybe makes this typecheck:

type Query = Object "Query" '[]
  '[ Argument "id" Text :> Field "test" Foo ]

handler :: Handler IO Query
handler = pure $ \fooId -> do
  Just foo <- lookupFoo fooId
  viewFoo foo

Obviously, however, this involves a partial pattern match, so it’s not an okay solution.

My conclusion is that either Handler IO is not a functor or Handler m (Maybe a) is stranger than I’m anticipating. Whatever the reason, this seems like a fairly straightforward use case, so I would imagine there has to be some solution.

Add benchmark suite

Sooner we get a framework for this in place, the less terribly slow the library will be.

HasResolver for enum is not monadic

In all our other HasResolver instances the Handler type is m x, for for enum it's not.

instance forall m ksN enum. (Applicative m, API.GraphQLEnum enum) => HasResolver m (API.Enum ksN enum) where
  type Handler m (API.Enum ksN enum) = enum

should probably be

  type Handler m (API.Enum ksN enum) = m enum

Backwards incompatible change though, so we'd need an 0.2 release

Sort out error handling

We're currently using MonadThrow, which throws in the base monad in ExceptT and thus might not be what we want.

Figure out something better to use.

Support interfaces

We have some support for interfaces, but it's not at all clear how you'd implement a handler for something like:

type Query {
  dog: Dog
} 

type Dog {
  name: Text!
  owner: Sentient
}

interface Sentient {
  name: Text!
}

type Human implements Sentient {
  name: Text!
}

What would the Haskell implementation for dog.owner look like?

Add doctest to CI

Support directives

We currently have absolutely no support for this. We should have some.

Specifically for @skip and @include

Recursion doesn't play nicely with unions

I tried modelling a tree with recursive entries, but if try to model this:

type TreeEntryQ = Union "TreeEntryQ" '[FileQ, RecTreeQ, SymlinkQ]

I get:

    • Invalid TypeIndex. Must be Object but got: RecTreeQ

because RecTreeQ is a newtype

"Input" API for parsing, validating and normalizing graphql queries

The following bit of code:

import qualified Data.GraphQL.AST as AST
import Data.GraphQL.Parser (document)
import GraphQL.Value (Value)
import Data.Attoparsec.Text (parseOnly, endOfInput)

query :: Text -> AST.SelectionSet
query q =
  let Right (AST.Document [AST.DefinitionOperation (AST.Query (AST.Node _ _ _ selectionSet))]) =
       parseOnly (document <* endOfInput) q
  in selectionSet

could got into a function that returns a CanonicalQuery (whose type we can refine at a later stage).

Cannot define recursive schema

I discovered this last night while trying to build good tests for #92.

I tried to change our example schema by:

  • making dog's owner a Sentient
  • giving humans two new fields:
    • pets: [Pet]
    • catsAndDogs: [CatOrDog]

However, this will not compile, since it means the type definition of Dog needs to include the type definition of CatOrDog which needs to include Dog which… is circular.

I don't know what the right answer to this is. Presumably either newtypes, a language extension, or abandoning large chunks of the type-based approach.

doctest doesn't work for 8.2

I get

Preprocessing test suite 'graphql-api-doctests' for graphql-api-0.2.0..
Building test suite 'graphql-api-doctests' for graphql-api-0.2.0..

tests/Spec.hs:1:8: error:
    File name does not match module name:
    Saw: ‘Main’
    Expected: ‘Spec’
  |
1 | module Main
  |        ^^^^

for

$ ghc --version 
The Glorious Glasgow Haskell Compilation System, version 8.2.1

QuickCheck tests for value correctness

All the stuff in GraphQL.Value should have generators and quickcheck tests. It's pretty fundamental, but a lot of work.

@teh Do you have any opinion about whether we should put Arbitrary instances in the library? My current feeling is custom generators in the tests, e.g.

validName :: Gen Name
validName = Name <$> arbitrary

validObject :: Gen Object
-- and so forth ...

Start a changelog

It's very important to me that we have a changelog. It makes it easier to announce releases, and makes it easier for users to upgrade.

We should link this changelog from hackage.

Add haddock to CI

Haddock has syntactic requirements and can fail. Add it to the build process (ideally pushing docs somewhere browseable), so that any improvements we make will stick.

I try to install with cabal install graphql-api, but I have an issue

Building library for graphql-api-0.1.2..
[ 1 of 18] Compiling GraphQL.Internal.Arbitrary ( src/GraphQL/Internal/Arbitrary.hs, dist/build/GraphQL/Internal/Arbitrary.o )
[ 2 of 18] Compiling GraphQL.Internal.OrderedMap ( src/GraphQL/Internal/OrderedMap.hs, dist/build/GraphQL/Internal/OrderedMap.o )
[ 3 of 18] Compiling GraphQL.Internal.Syntax.Tokens ( src/GraphQL/Internal/Syntax/Tokens.hs, dist/build/GraphQL/Internal/Syntax/Tokens.o )
[ 4 of 18] Compiling GraphQL.Internal.Syntax.AST ( src/GraphQL/Internal/Syntax/AST.hs, dist/build/GraphQL/Internal/Syntax/AST.o )

src/GraphQL/Internal/Syntax/AST.hs:55:1: warning: [-Wdodgy-imports]
Module ‘Protolude’ does not export ‘Type’
|
55 | import Protolude hiding (Type)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[ 5 of 18] Compiling GraphQL.Internal.Syntax.Parser ( src/GraphQL/Internal/Syntax/Parser.hs, dist/build/GraphQL/Internal/Syntax/Parser.o )

src/GraphQL/Internal/Syntax/Parser.hs:86:23: error:
Ambiguous occurrence ‘option’
It could refer to either ‘Protolude.option’,
imported from ‘Protolude’ at src/GraphQL/Internal/Syntax/Parser.hs:8:1-41
(and originally defined in ‘Data.Semigroup’)
or ‘Data.Attoparsec.Text.option’,
imported from ‘Data.Attoparsec.Text’ at src/GraphQL/Internal/Syntax/Parser.hs:23:5-10
(and originally defined in ‘Data.Attoparsec.Combinator’)
|
86 | field = AST.Field <$> option empty (pure <$> alias)
| ^^^^^^

src/GraphQL/Internal/Syntax/Parser.hs:333:12: error:
Ambiguous occurrence ‘option’
It could refer to either ‘Protolude.option’,
imported from ‘Protolude’ at src/GraphQL/Internal/Syntax/Parser.hs:8:1-41
(and originally defined in ‘Data.Semigroup’)
or ‘Data.Attoparsec.Text.option’,
imported from ‘Data.Attoparsec.Text’ at src/GraphQL/Internal/Syntax/Parser.hs:23:5-10
(and originally defined in ‘Data.Attoparsec.Combinator’)
|
333 | optempty = option mempty
| ^^^^^^
cabal: Leaving directory '/var/folders/zb/jqp3vqj170nc5vkyw8k2p_tw0000gn/T/cabal-tmp-13818/graphql-api-0.1.2'
cabal: Error: some packages failed to install:
graphql-api-0.1.2-HmKbgdf15yT6zJHN4EkHkp failed during the building phase. The
exception was:
ExitFailure 1

Cannot distinguish between missing arguments and field not wanted

As per #77

On the example schema, this query:

{
  name
  owner {
    name
  }
}

Returns:

{
  "data": {
    "dog": {
      "name": "Mortgage",
      "owner": null
    }
  },
  "errors": [
    {
      "message": "No value provided for Name {getNameText = \"dogCommand\"}, and no default specified."
    }
  ]
}

I would expect it to return:

{
  "data": {
    "dog": {
      "name": "Mortgage",
      "owner": {
        "name": "jml"
      }
    }
  }
}

Pretty printer for AST

Encoder is lovely, but it puts everything on one line. For debugging, it would be nice to have a pretty printer.

Nullable handler doesn't support objects

Probably.

Code is:

instance forall m hg. (HasResolver m hg, Functor m, ToValue (Maybe hg)) => HasResolver m (Maybe hg) where
  type Handler m (Maybe hg) = m (Maybe hg)
  resolve handler _ =  map (ok . toValue) handler

Note that it discards the selection set. That can't be good.

Clarify module structure

API and Value have submodules, but it's not clear what their relation is to the main, or how users are expected to import them.

My vote is for everything to live in Internal except for the modules that people use, which should be mostly re-exports and convenience functions. We break things up into files mostly for our own convenience, which is largely independent of what makes sense to someone trying to use this code.

Generic GraphQL deriving breaks for more than 3 entries

data Mode = Directory | NormalFile | ExecutableFile | Symlink deriving (Show, Eq, Generic, GraphQLEnum, Defaultable)

results in

    • No instance for (GraphQL.API.Enum.GenericEnumValues
                         ((C1 ('MetaCons "Directory" 'GHC.Generics.PrefixI 'False) U1
                           :+: C1 ('MetaCons "NormalFile" 'GHC.Generics.PrefixI 'False) U1)
                          :+: (C1
                                 ('MetaCons "ExecutableFile" 'GHC.Generics.PrefixI 'False) U1
                               :+: C1 ('MetaCons "Symlink" 'GHC.Generics.PrefixI 'False) U1)))
        arising from a use of ‘GraphQL.API.Enum.$dmenumToValue’

Top-level function to take queries and return responses

We have parsing, validation, resolving, and a response object. We need to string them all together.

Notes:

the 'schema' is defined by the server types, an initial value, and any supported directives (although we'll probably skip those for first release) -- so that's the server's input

the client's input is a query document, a variable map, and an optional name of an operation to run. Not sure how these are actually delivered to us.

the output is the Response object in our Output.hs module

overall process is:

  1. parse query (if it fails, respond with PreExecutionFailure)
  2. transform the AST into something valid, ignoring type checks, but making sure that arguments aren't duplicated, no circular references. (if fail, PreExecutionError)
    Note that we don't need the schema or the variables yet.
  3. substitute in the variables. now everything is literal.
  4. apply the directives. probably hard-code @Skip & @include for the first release. if fail, execution error, and continue with fragments without bad directives
    We now have something that's just objects & fragments
  5. "collect the fields" to avoid duplicate processing (c.f. https://facebook.github.io/graphql/#sec-Field-Collection). still not 100% sure about this.
  6. delegate to server-supplied code, collecting any results and errors
  7. wrap whatever we've got off in a Response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.