Source file graphql_cohttp.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
module type HttpBody = sig
  type t
  type +'a io

  val to_string : t -> string io
  val of_string : string -> t
end

module type S = sig
  module IO : Cohttp.S.IO
  type body
  type 'ctx schema

  type response_action =
    [ `Expert of Cohttp.Response.t
                 * (IO.ic
                    -> IO.oc
                    -> unit IO.t)
    | `Response of Cohttp.Response.t * body ]

  type 'conn callback =
    'conn ->
    Cohttp.Request.t ->
    body ->
    response_action IO.t

  val execute_request :
    'ctx schema ->
    'ctx ->
    Cohttp.Request.t ->
    body ->
    response_action IO.t

  val make_callback :
    (Cohttp.Request.t -> 'ctx) ->
    'ctx schema ->
    'conn callback
end

module Make
  (Schema : Graphql_intf.Schema)
  (Io : Cohttp.S.IO with type 'a t = 'a Schema.Io.t)
  (Body : HttpBody with type +'a io := 'a Schema.Io.t) = struct

  module Ws = Websocket.Connection.Make (Io)
  module Websocket_transport = Websocket_handler.Make (Schema.Io) (Ws)

  let (>>=) = Io.(>>=)

  type response_action =
    [ `Expert of Cohttp.Response.t
                 * (Io.ic
                    -> Io.oc
                    -> unit Io.t)
    | `Response of Cohttp.Response.t * Body.t ]

  type 'conn callback =
    'conn ->
    Cohttp.Request.t ->
    Body.t ->
    response_action Io.t

  let respond_string ~status ~body () =
    Io.return (`Response (Cohttp.Response.make ~status (), Body.of_string body))

  let static_file_response path =
    match Assets.read path with
    | Some body -> respond_string ~status:`OK ~body ()
    | None -> respond_string ~status:`Not_found ~body:"" ()

  let json_err = function
    | Ok _ as ok -> ok
    | Error err -> Error (`String err)

  let execute_query ctx schema variables operation_name query =
    let parser_result = json_err (Graphql_parser.parse query) in
    Io.return parser_result >>= function
    | Ok doc -> Schema.execute schema ctx ?variables ?operation_name doc
    | Error _ as e -> Io.return e

  let execute_request schema ctx _req body =
    Body.to_string body >>= fun body' ->
    let json = Yojson.Basic.from_string body' in
    let query = Yojson.Basic.(json |> Util.member "query" |> Util.to_string) in
    let variables = Yojson.Basic.Util.(json |> member "variables" |> to_option to_assoc) in
    let operation_name = Yojson.Basic.Util.(json |> member "operationName" |> to_option to_string) in
    let result = execute_query ctx schema (variables :> (string * Graphql_parser.const_value) list option) operation_name query in
    result >>= function
    | Ok (`Response data) ->
      let body = Yojson.Basic.to_string data in
      respond_string ~status:`OK ~body ()
    | Ok (`Stream stream) ->
      Schema.Io.Stream.close stream;
      let body = "Subscriptions are only supported via websocket transport" in
      respond_string ~status:`Internal_server_error ~body ()
    | Error err ->
      let body = Yojson.Basic.to_string err in
      respond_string ~status:`Internal_server_error ~body ()

  let make_callback : (Cohttp.Request.t -> 'ctx) -> 'ctx Schema.schema -> 'conn callback = fun make_context schema _conn (req : Cohttp.Request.t) body ->
    let req_path = Cohttp.Request.uri req |> Uri.path in
    let path_parts = Astring.String.cuts ~empty:false ~sep:"/" req_path in
    match req.meth, path_parts with
    | `GET,  ["graphql"] ->
      if Cohttp.Header.get req.Cohttp.Request.headers "Connection" = Some "Upgrade" && Cohttp.Header.get req.headers "Upgrade" = Some "websocket" then
        let handle_conn =  Websocket_transport.handle (execute_query (make_context req) schema) in
        Io.return (Ws.upgrade_connection req handle_conn)
      else
        static_file_response "index.html"
    | `GET,  ["graphql"; path] -> static_file_response path
    | `POST, ["graphql"]       -> execute_request schema (make_context req) req body
    | _ -> respond_string ~status:`Not_found ~body:"" ()
end