Unable to iterate graphql response


#1

Tinkering with the Spacex graphql API and I’m getting this back:

> {"data":{"launches":[{"flight_number":1,"mission_name":"FalconSat","launch_year":2006,"__typename":"Launch"},{"flight_number":2,"mission_name":"DemoSat","launch_year":2007,"__typename":"Launch"},{"flight_number":3,"mission_name":"Trailblazer","launch_year":2008,"__typename":"Launch"},{"flight_number":4,"mission_name":"RatSat","launch_year":2008,"__typename":"Launch"},{"flight_number":5,"mission_name":"RazakSat","launch_year":2009,"__typename":"Launch"},{"flight_number":6,"mission_name":"Falcon 9 Test Flight","launch_year":2010,"__typename":"Launch"},{"flight_number":7,"mission_name":"COTS 1","launch_year":2010,"__typename":"Launch"},{"flight_number":8,"mission_name":"COTS 2","launch_year":2012,"__typename":"Launch"},{"flight_number":9,"mission_name":"CRS-1","launch_year":2012,"__typename":"Launch"},{"flight_number":10,"mission_name":"CRS-2","launch_year":2013,"__typename":"Launch"},{"flight_number":11,"mission_name":"CASSIOPE","launch_year":2013,"__typename":"Launch"},{"flight_number":12,"mission_name":"SES-8","launch_year":2013,"__typename":"Launch"},{"flight_number":13,"mission_name":"Thaicom 6","launch_year":2014,"__typename":"Launch"},{"flight_number":14,"mission_name":"CRS-3","launch_year":2014,"__typename":"Launch"},{"flight_number":15,"mission_name":"OG-2 Mission 1","launch_year":2014,"__typename":"Launch"},{"flight_number":16,"mission_name":"AsiaSat 8","launch_year":2014,"__typename":"Launch"},{"flight_number":17,"mission_name":"AsiaSat 6","launch_year":2014,"__typename":"Launch"},{"flight_number":18,"mission_name":"CRS-4","launch_year":2014,"__typename":"Launch"},{"flight_number":19,"mission_name":"CRS-5","launch_year":2015,"__typename":"Launch"},{"flight_number":20,"mission_name":"DSCOVR","launch_year":2015,"__typename":"Launch"},{"flight_number":21,"mission_name":"ABS-3A / Eutelsat 115W B","launch_year":2015,"__typename":"Launch"},{"flight_number":22,"mission_name":"CRS-6","launch_year":2015,"__typename":"Launch"},{"flight_number":23,"mission_name":"TürkmenÄlem 52°E / MonacoSAT","launch_year":2015,"__typename":"Launch"},{"flight_number":24,"mission_name":"CRS-7","launch_year":2015,"__typename":"Launch"},{"flight_number":25,"mission_name":"OG-2 Mission 2","launch_year":2015,"__typename":"Launch"},{"flight_number":26,"mission_name":"Jason 3","launch_year":2016,"__typename":"Launch"},{"flight_number":27,"mission_name":"SES-9","launch_year":2016,"__typename":"Launch"},{"flight_number":28,"mission_name":"CRS-8","launch_year":2016,"__typename":"Launch"},{"flight_number":29,"mission_name":"JCSAT-2B","launch_year":2016,"__typename":"Launch"},{"flight_number":30,"mission_name":"Thaicom 8","launch_year":2016,"__typename":"Launch"},{"flight_number":31,"mission_name":"ABS-2A / Eutelsat 117W B","launch_year":2016,"__typename":"Launch"},{"flight_number":32,"mission_name":"CRS-9","launch_year":2016,"__typename":"Launch"},{"flight_number":33,"mission_name":"JCSAT-16","launch_year":2016,"__typename":"Launch"},{"flight_number":34,"mission_name":"Amos-6","launch_year":2016,"__typename":"Launch"},{"flight_number":35,"mission_name":"Iridium NEXT Mission 1","launch_year":2017,"__typename":"Launch"},{"flight_number":36,"mission_name":"CRS-10","launch_year":2017,"__typename":"Launch"},{"flight_number":37,"mission_name":"EchoStar 23","launch_year":2017,"__typename":"Launch"},{"flight_number":38,"mission_name":"SES-10","launch_year":2017,"__typename":"Launch"},{"flight_number":39,"mission_name":"NROL-76","launch_year":2017,"__typename":"Launch"},{"flight_number":40,"mission_name":"Inmarsat-5 F4","launch_year":2017,"__typename":"Launch"},{"flight_number":41,"mission_name":"CRS-11","launch_year":2017,"__typename":"Launch"},{"flight_number":42,"mission_name":"BulgariaSat-1","launch_year":2017,"__typename":"Launch"},{"flight_number":43,"mission_name":"Iridium NEXT Mission 2","launch_year":2017,"__typename":"Launch"},{"flight_number":44,"mission_name":"Intelsat 35e","launch_year":2017,"__typename":"Launch"},{"flight_number":45,"mission_name":"CRS-12","launch_year":2017,"__typename":"Launch"},{"flight_number":46,"mission_name":"FormoSat-5","launch_year":2017,"__typename":"Launch"},{"flight_number":47,"mission_name":"Boeing X-37B OTV-5","launch_year":2017,"__typename":"Launch"},{"flight_number":48,"mission_name":"Iridium NEXT Mission 3","launch_year":2017,"__typename":"Launch"},{"flight_number":49,"mission_name":"SES-11 / Echostar 105","launch_year":2017,"__typename":"Launch"},{"flight_number":50,"mission_name":"KoreaSat 5A","launch_year":2017,"__typename":"Launch"},{"flight_number":51,"mission_name":"CRS-13","launch_year":2017,"__typename":"Launch"},{"flight_number":52,"mission_name":"Iridium NEXT Mission 4","launch_year":2017,"__typename":"Launch"},{"flight_number":53,"mission_name":"ZUMA","launch_year":2018,"__typename":"Launch"},{"flight_number":54,"mission_name":"SES-16 / GovSat-1","launch_year":2018,"__typename":"Launch"},{"flight_number":55,"mission_name":"Falcon Heavy Test Flight","launch_year":2018,"__typename":"Launch"},{"flight_number":56,"mission_name":"Paz / Starlink Demo","launch_year":2018,"__typename":"Launch"},{"flight_number":57,"mission_name":"Hispasat 30W-6","launch_year":2018,"__typename":"Launch"},{"flight_number":58,"mission_name":"Iridium NEXT Mission 5","launch_year":2018,"__typename":"Launch"},{"flight_number":59,"mission_name":"CRS-14","launch_year":2018,"__typename":"Launch"},{"flight_number":60,"mission_name":"TESS","launch_year":2018,"__typename":"Launch"},{"flight_number":61,"mission_name":"Bangabandhu-1","launch_year":2018,"__typename":"Launch"},{"flight_number":62,"mission_name":"Iridium NEXT Mission 6","launch_year":2018,"__typename":"Launch"},{"flight_number":63,"mission_name":"SES-12","launch_year":2018,"__typename":"Launch"},{"flight_number":64,"mission_name":"CRS-15","launch_year":2018,"__typename":"Launch"},{"flight_number":65,"mission_name":"Telstar 19V","launch_year":2018,"__typename":"Launch"},{"flight_number":66,"mission_name":"Iridium NEXT Mission 7","launch_year":2018,"__typename":"Launch"},{"flight_number":67,"mission_name":"Merah Putih","launch_year":2018,"__typename":"Launch"},{"flight_number":68,"mission_name":"Telstar 18V","launch_year":2018,"__typename":"Launch"},{"flight_number":69,"mission_name":"SAOCOM 1A","launch_year":2018,"__typename":"Launch"},{"flight_number":70,"mission_name":"Es’hail 2","launch_year":2018,"__typename":"Launch"},{"flight_number":71,"mission_name":"SSO-A","launch_year":2018,"__typename":"Launch"},{"flight_number":72,"mission_name":"CRS-16","launch_year":2018,"__typename":"Launch"},{"flight_number":73,"mission_name":"GPS III SV01","launch_year":2018,"__typename":"Launch"},{"flight_number":74,"mission_name":"Iridium NEXT Mission 8","launch_year":2019,"__typename":"Launch"},{"flight_number":75,"mission_name":"Nusantara Satu (PSN-6) / S5 / Beresheet","launch_year":2019,"__typename":"Launch"},{"flight_number":76,"mission_name":"CCtCap Demo Mission 1","launch_year":2019,"__typename":"Launch"},{"flight_number":77,"mission_name":"ArabSat 6A","launch_year":2019,"__typename":"Launch"},{"flight_number":78,"mission_name":"CRS-17","launch_year":2019,"__typename":"Launch"},{"flight_number":79,"mission_name":"Starlink 1 (v0.9)","launch_year":2019,"__typename":"Launch"},{"flight_number":80,"mission_name":"RADARSAT Constellation","launch_year":2019,"__typename":"Launch"},{"flight_number":81,"mission_name":"STP-2","launch_year":2019,"__typename":"Launch"},{"flight_number":82,"mission_name":"CRS-18","launch_year":2019,"__typename":"Launch"},{"flight_number":83,"mission_name":"Amos-17","launch_year":2019,"__typename":"Launch"},{"flight_number":84,"mission_name":"JCSat 18 / Kacific 1","launch_year":2019,"__typename":"Launch"},{"flight_number":85,"mission_name":"CRS-19","launch_year":2019,"__typename":"Launch"},{"flight_number":86,"mission_name":"GPS III SV03 (Columbus)","launch_year":2019,"__typename":"Launch"},{"flight_number":87,"mission_name":"ALINA","launch_year":2019,"__typename":"Launch"},{"flight_number":88,"mission_name":"GTO-2","launch_year":2019,"__typename":"Launch"},{"flight_number":89,"mission_name":"SXM-7","launch_year":2019,"__typename":"Launch"},{"flight_number":90,"mission_name":"Starlink 2","launch_year":2019,"__typename":"Launch"},{"flight_number":91,"mission_name":"Starlink 3","launch_year":2019,"__typename":"Launch"},{"flight_number":92,"mission_name":"Crew Dragon In Flight Abort Test","launch_year":2019,"__typename":"Launch"},{"flight_number":94,"mission_name":"CRS-20","launch_year":2020,"__typename":"Launch"},{"flight_number":95,"mission_name":"CCtCap Demo Mission 2","launch_year":2020,"__typename":"Launch"},{"flight_number":96,"mission_name":"SAOCOM 1B","launch_year":2020,"__typename":"Launch"},{"flight_number":97,"mission_name":"GPS III SV04","launch_year":2020,"__typename":"Launch"},{"flight_number":98,"mission_name":"Turksat 5A","launch_year":2020,"__typename":"Launch"},{"flight_number":99,"mission_name":"GTO-C","launch_year":2020,"__typename":"Launch"},{"flight_number":100,"mission_name":"SXM-8","launch_year":2020,"__typename":"Launch"},{"flight_number":101,"mission_name":"USCV-1 (NASA Crew Flight 1)","launch_year":2020,"__typename":"Launch"},{"flight_number":102,"mission_name":"CRS-21","launch_year":2020,"__typename":"Launch"},{"flight_number":103,"mission_name":"GPS SV05","launch_year":2020,"__typename":"Launch"}]}}

Trying to process that with this:

module GetLaunches = [%graphql
  {|
  query getLaunches{
    launches {
      flight_number
      mission_name
      launch_year
    }
  }
|}
];

let str = ReasonReact.string;

module LaunchItem = {
  [@react.component]
  let make = (~launch) =>
  <div>



  </div>;
};

module GetLaunchesQuery = ReasonApollo.CreateQuery(GetLaunches);

[@react.component]
let make = () =>
  <GetLaunchesQuery>
    ...{
         ({result}) =>
           switch (result) {
           | Loading => <div> {ReasonReact.string("Loading")} </div>
           | Error(error) =>
             <div> {ReasonReact.string(error##message)} </div>
           | Data(response) =>
           (
            React.array(Array.of_list(
               List.map((launch) => <LaunchItem launch/>, response##launches)
        ))
        )

           }
       }
  </GetLaunchesQuery>;

Keep getting this error:

This has type:
    option(Js.Array.t(option({. "flight_number": option(int),
                               "launch_year": option(int),
                               "mission_name": option(string)})))
  But somewhere wanted:
    list('a)

#2

You asked in Stack Overflow as well, right? As Glenn mentioned over there, you need to handle the response type including options etc. In the case of Data(response), response##launches has type option(array(option(...))) (as the error message says). You will need to handle that exact data type. I would do something like:

| Data(response) =>
  response##launches
  |> Js.Option.getWithDefault([||])
  |> Array.map(fun
    | Some(launch) => <LaunchItem launch />
    | None => React.null)

#3

I tried the above change and I now see this error:

Error: This expression has type
         array(option(list('a))) => array(React.element)
       but an expression was expected of type
         Js.Array.t(option({. "flight_number": option(int),
                             "launch_year": option(int),
                             "mission_name": option(string)})) =>
         'b
       Type array(option(list('a))) is not compatible with type
         Js.Array.t(option({. "flight_number": option(int),
                             "launch_year": option(int),
                             "mission_name": option(string)}))
           =
           array(option({. "flight_number": option(int),
                          "launch_year": option(int),
                          "mission_name": option(string)})) 
       Type list('a) is not compatible with type
         {. "flight_number": option(int), "launch_year": option(int),
           "mission_name": option(string)} 

My Launches component has:

module Launches = {
  [@react.component]
  let make = (~launches) =>
    <div>
      {
        React.array(
          Array.of_list(List.map(launch => <Launch launch />, launches)),
        )
      }
    </div>;
};

#4

I don’t see the changes I recommended in your new code snippet, so not sure what’s going on :slight_smile: if you could post a more complete snippet, along with the exact line and column number of the error message, that would be helpful.


#5

Full code snippet:

open Belt.Option;

type launch = {
  flight_number: option(int),
  mission_name: option(string),
  launch_year: option(int),
};

type launches = {launches: launch};

module GetLaunches = [%graphql
  {|
  query getLaunches @bsRecord{
      launches @bsRecord{
        flight_number
        mission_name
        launch_year
      }
  }
|}
];

let str = ReasonReact.string;

module Launch = {
  [@react.component]
  let make = (~launch) => <div> {str("launch")} </div>;
};

module Launches = {
  [@react.component]
  let make = (~launches) =>
    <div>
      {
        React.array(
          Array.of_list(List.map(launch => <Launch launch />, launches)),
        )
      }
    </div>;
};

module GetLaunchesQuery = ReasonApollo.CreateQuery(GetLaunches);

[@react.component]
let make = () =>
  <div>
    <GetLaunchesQuery>
      ...{
           ({result}) =>
             switch (result) {
             | Loading => "Loading" |> ReasonReact.string
             | Error (error) =>
              Js.log(error);
              "Error" |> ReasonReact.string
             | Data(response) =>
                switch (response##launches) {
                | launches => launches
                  |> Js.Option.getWithDefault([||])
                  |> Array.map(
                        fun
                        | Some(launches) => <Launches launches />
                        | None => React.null,
                      );
                };
             }
         }
    </GetLaunchesQuery>
  </div>;

Not sure what I’m doing wrong?


#6

Your code doesn’t follow my above recommendation :slight_smile: can you try that and let me know what (if any) errors you get?


#7

Full code, updated :slight_smile:

open Belt.Option;

type launch = {
  flight_number: option(int),
  mission_name: option(string),
  launch_year: option(int),
};

type launches = {launches: launch};

module GetLaunches = [%graphql
  {|
  query getLaunches{
  launches {
    flight_number
    mission_name
    launch_year
  }
  }
|}
];

let str = ReasonReact.string;

module Launch = {
  [@react.component]
  let make = (~launch) => <div> {str("launch")} </div>;
};

module Launches = {
  [@react.component]
  let make = (~launches) =>
<div>
  {
    React.array(
      Array.of_list(List.map(launch => <Launch launch />, launches)),
    )
  }
</div>;
};

module GetLaunchesQuery = ReasonApollo.CreateQuery(GetLaunches);

[@react.component]
let make = (~launches) =>
  <GetLaunchesQuery>
...{
     ({result}) =>
       switch (result) {
       | Loading => <div> {ReasonReact.string("Loading")} </div>
       | Error(error) =>
         <div> {ReasonReact.string(error##message)} </div>
       | Data(response) =>
         response##launches
         |> Js.Option.getWithDefault([||])
         |> Array.map(
              fun
              | Some(launches) => <Launches launches />
              | None => React.null,
            )
       }
   }
  </GetLaunchesQuery>; 

Error:

54 ┆       response##launches
  55 ┆       |> Js.Option.getWithDefault([||])
  56 ┆       |> Array.map(
  57 ┆            fun
  58 ┆            | Some(launches) => <Launches launches />
  59 ┆            | None => React.null,
  60 ┆          )
  61 ┆     }
  62 ┆ }
  
  This has type:
    array(option(list('a))) => array(React.element)
  But somewhere wanted:
    Js.Array.t(option({. "flight_number": option(int),
                        "launch_year": option(int),
                        "mission_name": option(string)})) =>
    'b
  
  The incompatible parts:
    array(option(list('a)))
    vs
    Js.Array.t(option({. "flight_number": option(int),
                        "launch_year": option(int),
                        "mission_name": option(string)}))
      (defined as
      array(option({. "flight_number": option(int),
                     "launch_year": option(int),
                     "mission_name": option(string)})))
    
    Further expanded:
      list('a)
      vs
      {. "flight_number": option(int), "launch_year": option(int),
        "mission_name": option(string)}

#8

It would be good to see the file name and line number/column number in the error message to confirm, but here’s the critical part of my above recommendation:

| Data(response) =>
  response##launches
  |> Js.Option.getWithDefault([||])
  |> Array.map(fun
    | Some(launch) => <LaunchItem launch />
    | None => React.null)

I.e., launches is a nullable array, which we handle by converting to an empty array if null. Then, we need to map over the items in the array. Each item in the array is a nullable launch item. So, in the Array.map call I am actually handling both cases, that it is null (None), or not null (Some(launch)). In the latter case, I am rendering the launch item using the LaunchItem component, which is supposed to handle a single launch item from the array.

In your code, you are saying that the launches array contains a list of launch items. As far as I can see from the GraphQL query, that is not correct. I would recommend that you try my way, that is, as I describe above.

EDIT: I should emphasize that there is a pretty straight one-to-one correspondence between the GraphQL query and the data shape that you will get (i.e. what the PPX will give you). Something like launches { a b c } will give you something like option(array(option({. "a": typeOfA, "b": typeOfB, "c": typeOfC}))), and so on.


#9

Cleaned up the code, I think I’m a step closer to getting this done :slight_smile:

open Belt.Option;

type launch = {
  mission_name: option(string),
  launch_year: option(int),
  launch_date_utc: option(string)
};

type launches = {launches: launch};

module GetLaunches = [%graphql
  {|
  query getLaunches @bsRecord {
      launches @bsRecord {
        mission_name
        launch_year
        launch_date_utc
      }
  }
|}
];

let str = ReasonReact.string;


module LaunchItem = {
  [@react.component]
  let make = (~launch) =>
    <div>
    {launch.mission_name |> ReasonReact.string}
    </div>;
};

module GetLaunchesQuery = ReasonApollo.CreateQuery(GetLaunches);

[@react.component]
let make = () =>
  <GetLaunchesQuery>
    ...{
         ({result}) =>
           switch (result) {
           | Loading => <div> {ReasonReact.string("Loading")} </div>
           | Error(error) =>
             <div> {ReasonReact.string(error##message)} </div>
           | Data(response) =>
             response##launches
             |> Js.Option.getWithDefault([||])
             |> Array.map(launch =>
                  <div>
                    <LaunchItem launch/>
                  </div>

                )
           }
       }
  </GetLaunchesQuery>;

Now the error is in the LaunchItem component:

28 │   let make = (~launch) =>
  29 │     <div>
  **30 │     {launch.mission_name |> ReasonReact.string}**
  31 │     </div>;
  32 │ };
  
  This has type:
    string => ReasonReact.reactElement
  But somewhere wanted:
    option(string) => 'a
  
  The incompatible parts:
    string
    vs
    option(string)

How do I handle option(string)? I tried a switch statement but it wouldn’t compile


#10

More details about the compiler error are needed here :slight_smile: But I would guess something like

let make = (~launch) =>
  switch (launch##mission_name) {
  | Some(name) => <div>{React.string(name)}</div>
  | None => React.null
  };

… should do the trick.


#11

I may have spoken too soon :frowning: this is the error

  47 ┆       response##launches
  48 ┆       |> Js.Option.getWithDefault([||])
  49 ┆       |> Array.map(launch =>
  50 ┆            <div>
   . ┆ ...
  53 ┆ 
  54 ┆          )
  55 ┆     }
  56 ┆ }
  
  This has type:
    array(Js.t(({.. mission_name: option(string)} as 'a))) =>
    array(ReasonReact.reactElement)
  But somewhere wanted:
    Js.Array.t(option(launch)) => 'b
  
  The incompatible parts:
    array(Js.t('a))
    vs
    Js.Array.t(option(launch)) (defined as array(option(launch)))
    
    Further expanded:
      Js.t('a)
      vs
      option(launch) 

FYI this is the graphql API https://api.spacex.land/graphql/ as you can see launches does return an array of launch.


#12

Yup, this code doesn’t seem to be handling each launch as an optional value, I recommend using the code that I suggested, it explicitly deals with optionality.

Edit: I may be wrong about the exact cause, would need to see a complete code sample and the exact line and column number of the error to be sure.

Edit 2: you’re having a lot of trouble with this partly because this is a badly-designed GraphQL schema (to be honest). Lots of fields are nullable, even array items are nullable (it’s trivially easy for a GraphQL schema provider to filter out nulls and return an array of non-nullable items). Unfortunately, even GraphQL doesn’t people from making bad APIs!


#13

I was doing a side by side comparison of implementing the same GraphQL react component in ClojureScript (Re-frame and Reagent) which is dynamically typed, it’s way easier and had no issues, and wanted to do the same with ReasonReact. I’ll try the same with Elm to see how that goes.


#14

Hey hackersapien, would you be willing to provide some thought about your experience with Reason vs. ClojureScript in general, aside from the GraphQL problem?
I’m still torn between the two and while I can see some benefits of static types, they are driving me nuts at times.


#15

I’ll be the first to admit, not everyone likes the process of slowly trudging through type errors one by one, bending one’s mental model of the code to fit within the logic of the types, and learning to interpret the type errors and figure out what they concretely mean.

However, I think that with practice one can get pretty comfortable with it. It’s not just about preventing bugs from entering the codebase in new code, but also about preventing regressions when refactoring existing code. Every time I’ve seen people talk about this, it’s been two main points: (1) people refactoring dynamically-typed codebases really missing static types, and (2) people fearlessly refactoring large statically-typed codebases because the compiler has their back. I’ve personally experienced (2) but not (1) so much because fortunately I haven’t worked on any significant dynamically-typed codebases.

Long story short, types will work out for you if you bend your brain a bit to meet them halfway :slight_smile: if interested, I can recommend a few good resources:


#16

Admittedly I don’t have much experience with both so take my eval with a lot of skepticism, but the little I’ve seen is ClojureScript is much easier to work especially when it comes to using Reactjs, and specifically with Reagent+Re-frame. I can

  • Quickly build pure UI components.
  • I don’t have to deal with props and state management.
  • The app state is a hash map which means I have access to all the clojure functions for manipulating maps and I can retrieve values easily.
  • Subscriptions are straightforward ways to access app state.
  • components just dispatch events to manipulate app state.
  • Because everything is essentially a data structure I know what I’m dealing and have the tools/functions to work with them.
    As I said this is just my take, and sure someone could make a similar case for Reason.

#17

I’ve heard that the Closure crowd likes REPL-based development, and while you can use rtop for Reason, I guess the experience might be not as smooth (not that I compared).

But anyway, with static types the dev process is more compiler-based anyway. And, yeah, REPL doesn’t save you from regressions. Tests probably do, but well-designed types do as well. And types probably make you require less tests, even if Uncle Bob says otherwise.


#18

The full code sample is posted in my previous posting :slight_smile: BTW this is the 2nd time I’ve tried GraphQL with ReasonReact, I just switched the endpoint to this one because the other one isn’t working.