• Stars
    star
    623
  • Rank 69,451 (Top 2 %)
  • Language
    OCaml
  • License
    MIT License
  • Created over 7 years ago
  • Updated 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

GraphQL servers in OCaml

GraphQL Servers in OCaml

Build Status

This repo contains a library for creating GraphQL servers in OCaml. Note that the API is still under active development.

Current feature set:

  • Type-safe schema design
  • GraphQL parser in pure OCaml using Menhir
  • Query execution
  • Introspection of schemas
  • Arguments for fields
  • Allows variables in queries
  • Lwt support
  • Async support
  • Example with HTTP server and GraphiQL
  • GraphQL Subscriptions

Documentation

Four OPAM packages are provided:

  • graphql provides the core functionality and is IO-agnostic. It provides a functor Graphql.Schema.Make(IO) to instantiate with your own IO monad.
  • graphql-lwt provides the module Graphql_lwt.Schema with Lwt support in field resolvers.
  • graphql-async provides the module Graphql_async.Schema with Async support in field resolvers.
  • graphql_parser provides query parsing functionality.
  • graphql-cohttp allows exposing a schema over HTTP using Cohttp.

API documentation:

Examples

GraphiQL

To run a sample GraphQL server also serving GraphiQL, do the following:

opam install dune graphql-lwt graphql-cohttp cohttp-lwt-unix
git clone [email protected]:andreas/ocaml-graphql-server.git
dune exec examples/server.exe

Now open http://localhost:8080/graphql.

Defining a Schema

open Graphql

type role = User | Admin
type user = {
  id   : int;
  name : string;
  role : role;
}

let users = [
  { id = 1; name = "Alice"; role = Admin };
  { id = 2; name = "Bob"; role = User }
]

let role = Schema.(enum "role"
  ~doc:"The role of a user"
  ~values:[
    enum_value "USER" ~value:User;
    enum_value "ADMIN" ~value:Admin;
  ]
)

let user = Schema.(obj "user"
  ~doc:"A user in the system"
  ~fields:[
    field "id"
      ~doc:"Unique user identifier"
      ~typ:(non_null int)
      ~args:Arg.[]
      ~resolve:(fun info p -> p.id)
    ;
    field "name"
      ~typ:(non_null string)
      ~args:Arg.[]
      ~resolve:(fun info p -> p.name)
    ;
    field "role"
      ~typ:(non_null role)
      ~args:Arg.[]
      ~resolve:(fun info p -> p.role)
  ]
)

let schema = Schema.(schema [
  field "users"
    ~typ:(non_null (list (non_null user)))
    ~args:Arg.[]
    ~resolve:(fun info () -> users)
])

Running a Query

Without variables:

match Graphql_parser.parse "{ users { name } }" with
| Ok query -> Graphql.Schema.execute schema ctx query
| Error err -> failwith err

With variables parsed from JSON:

match Graphql_parser.parse "{ users(limit: $x) { name } }" with
| Ok query ->
    let json_variables = Yojson.Basic.(from_string "{\"x\": 42}" |> Util.to_assoc) in
    let variables = (json_variables :> (string * Graphql_parser.const_value) list)
    Graphql.Schema.execute schema ctx ~variables query
| Error err ->
    failwith err

Recursive Objects

The function Schema.fix can be used to define both self-recursive and mutually recursive objects:

(* self-recursive *)
type tweet = {
  id : int;
  replies : tweet list;
}

let tweet = Schema.(fix (fun recursive ->
  recursive.obj "tweet"
    ~fields:(fun tweet -> [
      field "id"
        ~typ:(non_null int)
        ~args:Arg.[]
        ~resolve:(fun info t -> t.id)
        ;
      field "replies"
        ~typ:(non_null (list (non_null tweet)))
        ~args:Arg.[]
        ~resolve:(fun info t -> t.replies)
    ])))
(* mutually recursive *)
let foo, bar = Schema.(fix (fun recursive ->
  let foo = recursive.obj "foo" ~fields:(fun (_, bar) -> [
      field "bar"
        ~typ:bar
        ~args:Arg.[]
        ~resolve:(fun info foo -> foo.bar)
    ])
  in
  let bar = recursive.obj "bar" ~fields:(fun (foo, _) -> [
    field "foo"
      ~typ:foo
      ~args:Arg.[]
      ~resolve:(fun info bar -> bar.foo)
      ])
  in
  foo, bar))

Lwt Support

open Lwt.Infix
open Graphql_lwt

let schema = Schema.(schema [
  io_field "wait"
    ~typ:(non_null float)
    ~args:Arg.[
      arg "duration" ~typ:float;
    ]
    ~resolve:(fun info () ->
      Lwt_result.ok (Lwt_unix.sleep duration >|= fun () -> duration)
    )
])

Async Support

open Core.Std
open Async.Std
open Graphql_async

let schema = Schema.(schema [
  io_field "wait"
    ~typ:(non_null float)
    ~args:Arg.[
      arg "duration" ~typ:float;
    ]
    ~resolve:(fun info () ->
      after (Time.Span.of_float duration) >>| fun () -> duration
    )
])

Arguments

Arguments for a field can either be required, optional or optional with a default value:

Schema.(obj "math"
  ~fields:(fun _ -> [
    field "sum"
      ~typ:int
      ~args:Arg.[
        arg  "x" ~typ:(non_null int); (* <-- required *)
        arg  "y" ~typ:int;            (* <-- optional *)
        arg' "z" ~typ:int ~default:7  (* <-- optional w/ default *)
      ]
      ~resolve:(fun info () x y z ->
        let y' = match y with Some n -> n | None -> 42 in
        x + y' + z
      )
  ])
)

Note that you must use arg' to provide a default value.

Subscriptions

Schema.(schema [
     ...
  ]
  ~subscriptions:[
    subscription_field "user_created"
      ~typ:(non_null user)
      ~resolve:(fun info ->
        let user_stream, push_to_user_stream = Lwt_stream.create () in
        let destroy_stream = (fun () -> push_to_user_stream None) in
        Lwt_result.return (user_stream, destroy_stream))
    ])

HTTP Server

Using Lwt:

open Graphql_lwt

let schema = Schema.(schema [
  ...
])

module Graphql_cohttp_lwt = Graphql_cohttp.Make (Schema) (Cohttp_lwt.Body)

let () =
  let callback = Graphql_cohttp_lwt.make_callback (fun _req -> ()) schema in
  let server = Cohttp_lwt_unix.Server.make ~callback () in
  let mode = `TCP (`Port 8080) in
  Cohttp_lwt_unix.Server.create ~mode server
  |> Lwt_main.run

Design

Only valid schemas should pass the type checker. If a schema compiles, the following holds:

  1. The type of a field agrees with the return type of the resolve function.
  2. The arguments of a field agrees with the accepted arguments of the resolve function.
  3. The source of a field agrees with the type of the object to which it belongs.
  4. The context argument for all resolver functions in a schema agree.

The following blog posts introduces the core design concepts: