haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.83k stars 414 forks source link

GraphQL #554

Closed jkarni closed 7 years ago

jkarni commented 8 years ago

@cpennington on IRC had the idea of integrating servant with GraphQL. That is, an interpretation that from the application developers perspective would feel like servant-server, but would handle GraphQL queries. This sounds pretty cool, as servant applications would then all be able to handle GraphQL as well.

arianvp commented 8 years ago

Idea:

Move GraphQL Schema definitions into the Servant DSL .

var userType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
  }
});

would be moved to the Servant type-level. Next we add a MimeType for GraphQL that allows us to parse GraphQL queries and use the typing information to be able to build a correct handler type that returns a graphql object.

alpmestan commented 8 years ago

@arianvp with TH?

arianvp commented 8 years ago

https://chadaustin.me/2016/02/dropbox-hack-week-graphql-server-in-haskell/ https://skillsmatter.com/skillscasts/6683-keynote-from-lennart-augustsson

arianvp commented 8 years ago

This is the result we got at the Munihac hackathon towards this goal: https://github.com/kcsongor/generic-records

kosmikus commented 8 years ago

@arianvp I also have an SOP-based version of record subtyping here: https://github.com/kosmikus/records-sop Not exactly sure what the requirements are, though, and it still needs an unreleased version of generics-sop. Should probably look at Csongor's version now and figure out the differences.

jkarni commented 8 years ago

generic-records and records-sop both look amazing!

I'm not entirely sure I get @arianvp 's original proposal.

(Without knowing very much about GraphQL) my thought and hope, which may be what Arian was suggesting, was to not change the Servant DSL at all, but instead just generate the graphQL schema from the API definition. Each endpoint being one query in the schema:

type API = "user" :> Capture "id" String :> Get '[JSON] User

data User = User { id :: String, name :: String}

Leads to the same behavior as:

var schema = new graphql.GraphQLSchema({
  query: new graphql.GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: userType,
        args: {
          id: { type: graphql.GraphQLString }
        },
        resolve: function (_, args) {
          return data[args.id];
        }
      }
    }
  })
});

var userType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
  }
});

With a combinator "WithGraphQL" over the API to also server as GraphQL. Which (again, without knowing much about GraphQL) would be dope.

arianvp commented 8 years ago

Hmm. the problem is that REST and GraphQL are really a totally different thing. in GraphQL you only have 1 HTTP endpoint you talk to, and the actual graph queries are sent over the network. In this its more similar to SQL than REST.

I think we can do similar type-safe stuff for GraphQL HTTP APIs as we do for REST HTTP APIs, but it would lead to something new in my opinion, that doesn't really fit into current servant API descriptions.

jkarni commented 8 years ago

Hmm. the problem is that REST and GraphQL are really a totally different thing. in GraphQL you only have 1 HTTP endpoint you talk to, and the actual graph queries are sent over the network. In this its more similar to SQL than REST.

Maybe I could have been clearer about what I'm proposing. It's that we can generate that one GraphQL endpoint from the existing API description. Just take a look at the servant code and javascript code (from the GraphQL tutorial) above. They contain roughly the same data. So:

type API = "user" :> Capture "id" String :> Get '[JSON] User

data User = User { id :: String, name :: String}

server :: Server API
server = ...

type WholeAPI = API :<|> GraphQLFor API

wholeServer :: Server WholeAPI
wholeServer = server :<|> graphQLFor server

Should in theory be possible, no?

jkarni commented 8 years ago

Ok, here's a sketch.

I haven't shown how to implement the instance for :<|>. There are nuances there that I'd like to sleep on, but it's pretty evident to me that it should work.

In the comment is how we'd run a query. There's the graphql package to help with parsing.

I used the 'HasName' class to pick field names so that we could operate only at the (servant-server) value-level, but maybe in practice we'd like to use paths for that? Similarly, I ignored argument names in functions, but perhaps in practice we'd like to use Capture and/or QueryParam names.

jkarni commented 7 years ago

Closing this, as it'll be a separate package if it ever happens. Feel free to open issues in servant-graphql