GraphQL bindings

irmin-graphql provides a nice interface for accessing remote Irmin stores over a network. This section will show you how to run an irmin-graphql server and query it using your favorite GraphQL client!

Installation

irmin-graphql is part of the latest irmin release, so all that's needed is:

$ opam install irmin-cli

Running the server

To start the GraphQL server:

$ irmin graphql --port 8080

This will start the server on localhost:8080 - this can be customized using the --address and --port flags. By default irmin-graphql provides an GraphiQL editor for writing queries from within the browser which can be accessed at http://localhost:8080/graphql.

Schema

Using the GraphiQL web interface you can explore the schema using the Docs button in the upper-right corner. Additionally, there are tools like [get-graphql-schema] which will dump the entire schema for you.

Queries

Using irmin-graphql it is possible to collect information about Irmin databases (and Git repositories) using GraphQL.

Get

To start off we will create a query to retrieve the value stored at the path abc:

query {
  main {
    tree {
      get(key: "abc")
    }
  }
}

The following would accomplish the same thing in my-branch:

query {
  branch(name: "my-branch") {
    tree {
      get(key: "a/b/c")
    }
  }
}

It's also possible to set or update multiple keys using the set_tree and update_tree mutations. The following will set /a to "foo" and /b to "bar":

mutation {
  set_tree(
    key: "/"
    tree: [{ key: "a", value: "foo" }, { key: "b", value: "bar" }]
  ) {
    hash
  }
}

And updating multiple keys is similar:

mutation {
  update_tree(
    key: "/"
    tree: [{ key: "a", value: "testing" }, { key: "b", value: null }]
  ) {
    hash
  }
}

this will set a to "testing" and remove the value associated with b.

Branch info

Using main/branch queries we are able to find lots of information about the attached Irmin store:

query {
  main {
    head {
      hash
      info
      parents
    }
  }
}

Bulk queries

Due to difficulties representing infinitely recursive datatypes in GraphQL, an Irmin tree is represented using the [TreeItem!] type. TreeItem has the following keys:

  • key
  • value
  • metadata

Using this new information, it is possible to list every key/value pair using:

query {
  main {
    head {
      tree {
        list_contents_recursively {
          key
          value
        }
      }
    }
  }
}

Which can also be augmented using get_tree to return a specific subtree:

query {
  main {
    head {
      tree {
        get_tree(key: "a") {
          list_contents_recursively {
            key
            value
          }
        }
      }
    }
  }
}

Mutations

irmin-graphql also supports mutations, which are basically queries with side-effects.

Set

For example, setting a key is easy:

mutation {
  set(key: "a/b/c", value: "123") {
    hash
  }
}

The example above sets the key "a/b/c" (["a"; "b"; "c"] in OCaml) to "123" and returns the new commit's hash.

Sync

clone, push and pull are also supported as long as they're supported by the underlying store! This allows data to be synchronized across servers using a simple mutation:

mutation {
  pull(remote: "git://github.com/mirage/irmin") {
    hash
  }
}

Bulk updates

To update multiple values you can use set_tree and update_tree. The difference between the two is set_tree will modify the tree to match to provided tree, while update_tree will only modify the provided keys - updating the value if one is provided, otherwise removing the current value if null is provided. For example:

mutation {
  set_tree(
    key: "/"
    tree: [{ key: "a/b/c", value: "123" }, { key: "d/e/f", value: "456" }]
  ) {
    hash
  }
}

will set a/b/c to "123" and d/e/f to "456", and if there are any other keys they will be removed. To keep the existing values, update_tree should be used:

mutation {
  update_tree(
    key: "/"
    tree: [
      { key: "a/b/c", value: "123" }
      { key: "d/e/f", value: "456" }
      { key: "testing", value: null }
    ]
  ) {
    hash
  }
}

The above query will set the values of a/b/c and d/e/f, while removing the value associated with testing - all other values will be left as-is.

GraphQL servers in OCaml

It is also possible to use the irmin-graphql OCaml interface to embed a GraphQL server in any application!

Using Irmin_graphql_unix.Server.Make you can convert an existing Irmin.S typed module into a GraphQL server:

module Graphql_store = Irmin_git_unix.Mem.KV(Irmin.Contents.String)
module Graphql = Irmin_graphql_unix.Server.Make(Graphql_store)(struct let remote = Some Graphql_store.remote end)

The following code will initialize and run the server:

let run_server () =
  (* Set up the Irmin store *)
  let* repo = Graphql_store.Repo.v (Irmin_git.config "/tmp/irmin") in

  (* Initialize the GraphQL server *)
  let server = Graphql.v repo in

  (* Run the server *)
  let on_exn exn =
    Logs.debug (fun l -> l "on_exn: %s" (Printexc.to_string exn))
  in
  Cohttp_lwt_unix.Server.create ~on_exn ~mode:(`TCP (`Port 1234)) server

Customization

It is possible to use a custom JSON representation for a type by implementing your own Schema.typ value:

module Example_type = struct
  type t = {x: string; y: string; z: string}

  let t =
    let open Irmin.Type in
    record "Example_type" (fun x y z -> {x; y; z})
    |+ field "x" string (fun t -> t.x)
    |+ field "y" string (fun t -> t.y)
    |+ field "z" string (fun t -> t.z)
    |> sealr

  let merge = Irmin.Merge.(option (default t))
end

let schema_typ : (unit, Example_type.t option) Irmin_graphql.Server.Schema.typ =
  let open Example_type in
  Irmin_graphql.Server.Schema.(obj "Example"
    ~fields:[
      field "x"
        ~typ:(non_null string)
        ~args:[]
        ~resolve:(fun _ t -> t.x)
      ;
      field "y"
        ~typ:(non_null string)
        ~args:[]
        ~resolve:(fun _ t -> t.y)
      ;
      field "z"
        ~typ:(non_null string)
        ~args:[]
        ~resolve:(fun _ t -> t.z)
      ;
    ]
  )

(You may also opt to use Irmin_graphql.Server.Default_types, which can be used on any Irmin.S)

Once you've done this for one or both of schema_typ and arg_type, you must wrap them in Irmin_graphql.Server.CUSTOM_TYPES before passing them to Irmin_graphql.Server.Make_ext:

module Store = Irmin_mem.KV.Make (Example_type)

module Custom_types = struct
  module Defaults = Irmin_graphql.Server.Default_types (Store)

  (* Use the default types for most things *)
  module Path = Defaults.Path
  module Metadata = Defaults.Metadata
  module Hash = Defaults.Hash
  module Branch = Defaults.Branch
  module Contents_key = Defaults.Contents_key
  module Node_key = Defaults.Node_key
  module Commit_key = Defaults.Commit_key

  module Contents = struct
    include Defaults.Contents
    let schema_type = schema_typ
  end
end

module Config = struct
  module Info = Irmin_unix.Info(Store.Info)

  let remote = None

  type info = Info.t
  let info = Info.v
end

module Graphql_ext =
  Irmin_graphql.Server.Make_ext
    (Cohttp_lwt_unix.Server)
    (Config)
    (Store)
    (Custom_types)