andreas / ocaml-graphql-server

GraphQL servers in OCaml
MIT License
624 stars 60 forks source link

GraphQL client #36

Closed sgrove closed 6 years ago

sgrove commented 7 years ago

I'm able to use ocaml-graphql-server to build a demo app with the Apollo-js client, which is great!

I have a lot of the pieces in place to use a full OCaml/GraphQL stack, but the client (actually in reason) is the last challenge here. Seems like code-generation is the way to go, for lots of reasons - it can be type-checked (both client and server-side), optimized by the server to that only the hash of the query is sent over rather than the full text, etc.

I'd ultimately like to follow the design of the iOS apollo client (https://github.com/apollographql/apollo-ios), but for the time being I'm just looking to get basic query/response working - no pagination, subscriptions, etc.

Seems like the best way to do this is to have GraphQL files and a build step that takes them + a running server url and generates an OCaml module for each one, with its own executeRequest, executeRequestByHash, etc. methods.

How would you recommend going about this? Is there some way to easily parse a GraphQL query-text and then introspect a schema and its types so that I can pattern match down it and emit a Reason file?

andreas commented 7 years ago

Parsing a GraphQL query should be straight forward with the library:

(* doc : (Graphql_parser.document, string) result *)
let doc = Graphql_parser.parse "{ users { id } }" in 
...

If you were to base the code generation on an introspection query, you would have to parse the JSON result, e.g. using yojson (or ppx_deriving_yojson). You would then have to combine these two into a Reason file, as you propose.

An alternative approach would be to build this functionality into the library. I think this would allow you to have simpler and safer pattern matching than basing it on the introspection query. The downside is that it limits you to use Reason for both frontend and backend -- but maybe that's the entire point?

A third option would be a ppx-based approach. Something along the lines of:

type user = {
  id : int
  name : string
} [@@deriving graphql]

type query = {
  users : user list
  find_user : int -> user option
} [@@deriving graphql]

This could potentially generate both a server- and a client-component. I haven't thought this very much through, but it seems like a exciting approach if it could work.

andreas commented 7 years ago

I've started dabbling with an idea using PPX. I expect the end result would look something like this:

(* Example with the Github GraphQL API *)
let query = [%graphql "{
  user(login: $login) {
    email
    bio
  }
}"] in
Graphql_client.execute client query ~login:"andreas" >>= rsp ->
(* *)

where query has the type

(login:string -> string,
 <user : <email : string, bio : string option>>
) Graphql_client.query

and rsp has the type

(<user : <email : string, bio : string option>>, Graphql_client.error) result

The type of query would be generated based on the provided query string and a GraphQL schema available at compile-time (local on disk). The shape of the response is encoded as an object type and determines what is returned from Graphql_client.execute.

I have a very simple prototype working, but I think it seems promising. Any feedback appreciated :)

xvw commented 7 years ago

Yo, excellent perspective ! Could you not use this kind of syntax (instead of your propositon) :

(* Example with the Github GraphQL API *)
let%graphql query =  {|
  user(login: $login) {
    email
    bio
  }
|} in
Graphql_client.execute client query ~login:"andreas" >>= rsp ->

Using : https://caml.inria.fr/pub/docs/manual-ocaml/extn.html#sec250

andreas commented 7 years ago

@xvw As far as I can tell, your proposed example is not valid syntax (%graphql being tied to the variable name), but there are probably other options, e.g.

let%graphql query =  {|
  user(login: $login) {
    email
    bio
  }
|} in
Graphql_client.execute client query ~login:"andreas" >>= rsp ->
(* ... *)

(note that the query in your example is actually missing surrounding braces -- not sure if that was intentional on your part?)

xvw commented 7 years ago

@xvw As far as I can tell, your proposed example is not valid syntax (%graphql being tied to the variable name), but there are probably other options, e.g.

Fail, sorry !

(note that the query in your example is actually missing surrounding braces -- not sure if that was intentional on your part?)

I imagine that {| and |} could be the surrounding braces ?

andreas commented 7 years ago

I imagine that {| and |} could be the surrounding braces ?

If the string is restricted to only contain one anonymous query (Lone Anonymous Operation), then that could work. If fragments or multiple named queries are allowed, then I think it won't work.

xvw commented 7 years ago

Understand ! It was just a suggestion but I understand (and agreed) with your remarks.

andreas commented 6 years ago

I'll close this one and defer further discussion to ppx_graphql.