graphql reason-apollo - recursive parsing of options
Asked Answered
P

2

6

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?

Paxwax answered 21/2, 2018 at 11:18 Comment(0)
M
3

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.

Mistranslate answered 26/2, 2018 at 21:58 Comment(0)
C
2

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.

Cardinalate answered 26/2, 2018 at 21:50 Comment(3)
Very cool, thank you for the detailed info! I've been curious about bs-json but haven't dipped in yet. Are you using reason on neilkistner.com? It looks like you might be using the github API there. Have you seen anyone using bs-json with reason-apollo?Paxwax
I'm currently rewriting my personal site to use Reason. Well, it is completed, but I am finishing up some of the server stuff I had, which I am trying to do in Reason, but the front end is all complete. I haven't pushed those changes up to GitHub yet. I haven't personally seen people combining those two libraries, but a lot of talk in the Reason discord about reason-apollo, so it might be better to ask there.Cardinalate
@P.MyerNore I'm not sure why do you need bs-json with reason-apollo ? graphql_ppx provides a parse function to parse result to Js.t so bs-json is not needed hereDelapaz

© 2022 - 2024 — McMap. All rights reserved.