6
votes

I'm using Reason-Apollo to parse a pretty nested GraphQL response from my server. I'm having trouble parsing the hairy tree of options returned from my GraphQL server (I'm using django-graphene).

Here is the GraphQL query and the Reason React module using Reason Apollo:

module GroupQuery = [%graphql {|
query GetChatGroup($chatGroupId: ID!){
  chatGroup(id: $chatGroupId) {
    id
    users {
      edges {
        node {
          id
          name
          isCurrentUser
        }
      }
    }
    messages {
      edges {
        node {
          id
          text
          author {
            name
            abbreviation
            photoUrl
            isCurrentUser
          }
        }
      }
    }
  }
}
|}];

/*eventually will be a reducerComponent*/
let component = ReasonReact.statelessComponent("RechatWindow");

module Query = RechatApollo.Instance.Query;

let parseMessages = chatGroup =>
  switch chatGroup {
  | Some(chatGroup) =>
    switch chatGroup##messages {
    | Some(messages) =>
      let edges = messages##edges;
      switch edges {
      | Some(edges) =>
        let parsedNodes =
          Js.Array.map(
            node =>
              switch node {
              | Some(node) =>
                let id = node##id;
                let text = node##text;
                let author = node##author;
                switch (id, text, author) {
                | (Some(id), Some(text), Some(author)) =>
                  let name = author##name;
                  let abbrev = author##abbreviation;
                  let isCurrentUser = author##isCurrentUser;
                  switch (name, abbrev, isCurrentUser) {
                  | (Some(name), Some(abbrev), Some(isCurrentUser)) =>
                    id ++ " - " ++ text ++ " - " ++ name ++ " - " ++ abbrev ++ " - "
                  | _ => "Error retrieving message 3"
                  };
                | _ => "Error retrieving message 2"
                };
              | _ => "Error retrieving message 1"
              },
            edges
          );
        parsedNodes;
      | None => [||]
      };
    | None => [||]
    };
  | None => [||]
  };

let make = (_children) => {
  ...component,
  render: (_) => {
    let unexpectedError = <div> (ReasonReact.stringToElement("There was an internal error")) </div>;
      let groupQuery = GroupQuery.make(~chatGroupId="Q2hhdEdyb3VwVHlwZTox", ());
      <Query query=groupQuery>
      ...((response, parse) => {
        switch response {
           | Loading => <div> (ReasonReact.stringToElement("Loading")) </div>
           | Failed(error) => <div> (ReasonReact.stringToElement(error)) </div>
           | Loaded(result) => {
              let chatGroup = parse(result)##chatGroup;
              let parsedMessages = parseMessages(chatGroup);
               <ul>
                 (
                   ReasonReact.arrayToElement(
                     Array.map(message => <li> (ste(message)) </li>, parsedMessages)
                   )
                 )
               </ul>;
           }
        }
       })
    </Query>
  }
};

Here is the return data from the GraphQL query from GraphiQL:

{
  "data": {
    "chatGroup": {
      "id": "Q2hhdEdyb3VwVHlwZTox",
      "users": {
        "edges": [
          {
            "node": {
              "id": "VXNlclR5cGU6MzQ=",
              "name": "User 1",
              "isCurrentUser": false
            }
          },
          {
            "node": {
              "id": "VXNlclR5cGU6MQ==",
              "name": "User 2",
              "isCurrentUser": true
            }
          }
        ]
      },
      "messages": {
        "edges": [
          {
            "node": {
              "id": "Q2hhdE1lc3NhZ2VUeXBlOjE=",
              "text": "my first message",
              "author": {
                "name": "User 1",
                "abbreviation": "U1",
                "photoUrl": "",
                "isCurrentUser": true
              }
            }
          }, ...

I have a syntax error somewhere ...

  137 ┆ | Loaded(result) => {
  138 ┆    let chatGroup = parse(result)##chatGroup;
  139 ┆    let parsedMessages = parseMessages(chatGroup);
  140 ┆     <ul>
  141 ┆       (

  This has type:
    option(Js.t({. id : string,
                  messages : option(Js.t({. edges : array(option(Js.t(
                                                                 {. node : 
                                                                   option(
                                                                   Js.t(
                                                                   {. author : 
                                                                    Js.t(
                                                                    {. abbreviation : 
                                                                    option(
                                                                    string),
                                                                    isCurrentUser : 
                                                                    option(
                                                                    Js.boolean),
                                                                    name : 
                                                                    option(
                                                                    string),
                                                                    photoUrl : 
                                                                    option(
                                                                    string) }),
                                                                    id : 
                                                                    string,
                                                                    text : 
                                                                    string })) }))) })),
                  users : option(Js.t({. edges : array(option(Js.t({. node : 
                                                                    option(
                                                                    Js.t(
                                                                    {. id : 
                                                                    string,
                                                                    isCurrentUser : 
                                                                    option(
                                                                    Js.boolean),
                                                                    name : 
                                                                    option(
                                                                    string) })) }))) })) }))
  But somewhere wanted:
    option(Js.t({.. messages : option(Js.t({.. edges : option(Js.Array.t(
                                                              option(
                                                              Js.t({.. author : 
                                                                    option(
                                                                    Js.t(
                                                                    {.. abbreviation : 
                                                                    option(
                                                                    string),
                                                                    isCurrentUser : 
                                                                    option('a),
                                                                    name : 
                                                                    option(
                                                                    string) })),
                                                                    id : 
                                                                    option(
                                                                    string),
                                                                    text : 
                                                                    option(
                                                                    string) })))) })) }))
  Types for method edges are incompatible

My immediate question: what is the error here?

On a deeper level, parsing all of these options to render the desired response seems like it would generally produce pretty unclear code. So what is the common paradigm around parsing options in JS when using ReasonML / OCaml? Is there an idiomatic way to get all of the options that will be there most of the time? Should I be creating an object type or a record type and parsing into those, and then rendering from the "known" object or record structures?

Or perhaps my graphql_schema.json and endpoint needs to have more required options?

Also, I'm using Relay's GraphQL convention of having edges { node { ... node fields ... } }, and it seems like if there are any edges then there should be at least one node. Is there any way to cut down on the option verbosity when using relay-style GraphQL?

2

2 Answers

3
votes

The large types in the error message can make it hard to see what's going on, so it's helpful to boil it down to just the type differences. It's complaining about the messages field that it says has the type:

option(Js.t({. edges : array(option(Js.t(...

while it's actually used as:

option(Js.t({.. edges : option(Js.Array.t(Js.t(...

So edges is actually a non-optional array whereas you are using it as an option(Js.Array.t). You do not need to check if it is Some, perhaps just if it is an empty array []. Then you'll want to use Array.map to handle the non-empty case.

Try going through and fixing your usage so that the inferred type matches the the type you're getting from your query until it compiles successfully.

2
votes

Best I can tell is you're parsing into option(Js.Array.t), but when you go to render, you're referencing it as an array(option(Js.t)). One option to get you closer to resolving would be changing the Array.map to Js.Array.map in the render function.

Since you mentioned alternatives, I'll share what I am doing below:


I am using bs-json to parse my GraphQL responses from the GitHub API.

Here is the query:

let query = {|
  query {
    viewer {
      projects: repositories ( orderBy: { direction: DESC, field: STARGAZERS }, affiliations: [ OWNER ], first: 100, isFork: false ) {
        nodes {
          ...RepoFields
        }
      }
      contributions1: pullRequests( first: 100, states: [ MERGED ] ) {
        nodes {
          repository {
            ...RepoFields
          }
        }
      },
      contributions2: pullRequests( last: 100, states: [ MERGED ] ) {
        nodes {
          repository {
            ...RepoFields
          }
        }
      }
    }
  }

  fragment RepoFields on Repository {
    name
    nameWithOwner
    shortDescriptionHTML( limit: 100 )
    stargazers {
      totalCount
    }
    url
  }
|};

And then I build a little decoder module:

module Decode = {
  open Json.Decode;

  let repo = ( ~nameField="name", json ) => {
    name: json |> field(nameField, string),
    stars: json |> at([ "stargazers", "totalCount" ], int),
    description: json |> field("shortDescriptionHTML", string),
    url: json |> field("url", string),
  };

  let repo2 = json =>
    json |> field("repository", repo(~nameField="nameWithOwner"));

  let rec uniq = ( free, lst ) =>
    switch lst {
    | [] => free
    | [ hd, ...tl ] =>
      switch ( List.mem(hd, tl) ) {
      | true => uniq(free, tl)
      | false => uniq([ hd, ...free ], tl)
      }
    };

  let all = json => {
    contributions: (
        (json |> at([ "data", "viewer", "contributions1", "nodes" ], list(repo2))) @
        (json |> at([ "data", "viewer", "contributions2", "nodes" ], list(repo2)))
      )
        |> uniq([])
        |> List.sort(( left, right ) => right.stars - left.stars),
    projects: json |> at([ "data", "viewer", "projects", "nodes" ], list(repo)),
  };
};

Which parses into a record type of:

type github = {
  description: string,
  name: string,
  stars: int,
  url: string,
};

type gh = {
  contributions: list(github),
  projects: list(github),
};

Here is my fetcher:

let get =
  Resync.(Refetch.(
    request(`POST, "https://api.github.com/graphql",
      ~headers=[
        `Authorization(`Bearer("******")),
        `ContentType("application/graphql")
      ],
      ~body=`Json(body))
    |> fetch
      |> Future.flatMap(
          fun | Response.Ok(_, response) => Response.json(response)
              | Response.Error({ reason }, _) => raise(FetchError(reason)))
      |> Future.map(Decode.all)
  ));

^ The decoding is done there on the Future.map. This is another library by Glenn, refetch.

And I am passing the contributions and projects above into my app as props.