haskell-graphql / graphql-api

Write type-safe GraphQL services in Haskell
BSD 3-Clause "New" or "Revised" License
406 stars 35 forks source link

How to construct a complex Object from Record? #217

Open Benjmhart opened 5 years ago

Benjmhart commented 5 years ago

I can't seem to construct a multi-field argument without several errors: Here's what I have so far:

type QuoteGQL = GQLAPI.Object "QuoteGQL" '[]
  '[ GQLAPI.Argument "symbol" Text :> GQLAPI.Field "globalQuote" QuoteR ]

type QuoteR =  GQLAPI.Object "QuoteR" '[] 
  '[ GQLAPI.Field  "symbol"           Text
   , GQLAPI.Field  "open"             Text
   , GQLAPI.Field  "high"             Text
   , GQLAPI.Field  "low"              Text
   , GQLAPI.Field  "price"            Text
   , GQLAPI.Field  "volume"           Text
   , GQLAPI.Field  "latestTradingDay" Text
   , GQLAPI.Field  "previousClose"    Text
   , GQLAPI.Field  "change"           Text
   , GQLAPI.Field  "changePercent"    Text
  ]

data QuoteRecord = QuoteRecord  { symbol              :: Text
                                , open                :: Text
                                , high                :: Text
                                , low                 :: Text
                                , price               :: Text
                                , volume              :: Text
                                , latestTradingDay    :: Text
                                , previousClose       :: Text
                                , change              :: Text
                                , changePercent       :: Text
                                } deriving (Eq, Show, Generic)

makeQuoteR :: QuoteRecord -> Maybe QuoteR
makeQuoteR qr = GQLV.objectFromList 
  [ ( "symbol",           TV.toValue $ symbol qr)
  , ( "open",             TV.toValue $ open qr) 
  , ( "high",             TV.toValue $ high qr) 
  , ( "low",              TV.toValue $ low qr) 
  , ( "price",            TV.toValue $ price qr) 
  , ( "volume",           TV.toValue $ volume qr) 
  , ( "latestTradingDay", TV.toValue $ latestTradingDay qr) 
  , ( "previousClose",    TV.toValue $ previousClose qr) 
  , ( "change",           TV.toValue $ change qr) 
  , ( "changePercent",    TV.toValue $ changePercent qr) 
  ]

makeQuoteGQL is failing to typeCheck with Expected type: Maybe QuoteR Actual type: Maybe (GQLV.Object' GQLV.ConstScalar)

How on Earth can I construct a QuoteR - I need to use the record type as an intermediary because GQLAPI.Object does not implement FromJSON

what am I missing for these more complex types?

theobat commented 5 years ago

So this is where I think something is wrong with how we do things in graphql-api, we don't have a way to automatically derive haskell records from/to graphql ASTs (hence my issue there: https://github.com/haskell-graphql/graphql-api/issues/211)

To answer your question, you can't create a QuoteR, it's a simple "alias" to a GQLV.Object' GQLV.ConstScalar, this means in particular that this type is never embodied in your graphql server, it's just resolved in its handler but that's about it. If you want to resolve it, you can define its relevant Handler where you actually return pure makeQuoteGQL yourQuoteRecordbut that's the only place where you know the AST you've constructed in makeQuoteGQL is a QuoteR (in types it's just a Graphql value).

The reason why it works this way is, as far as I understand it, that subtyping in haskell is hard and subtyping is very much the idea of graphql, the AST (graphql Values in this project) way of doing it is a way to solve the problem, but as of today it creates A. a lot of boilerplate to create a schema in the GQL DSL and with the classical haskell way and B. a weird understanding for the layperson beginning with the lib (as opposed to something like Deriving GQL automatically like aeson does it for json)...

I wish we could define our schemas with plain haskell objects like:

data QuoteRecord = QuoteRecord  { symbol              :: Text
                                , open                :: Text
                                , high                :: Text
                                , low                 :: Text
                                , price               :: Text
                                , volume              :: Text
                                , latestTradingDay    :: Text
                                , previousClose       :: Text
                                , change              :: Text
                                , changePercent       :: Text
                                } deriving (Eq, Show, Generic, GQL)

But it's not possible in this lib (and seemingly won't be possible in the near future, but I might be missing something).

Benjmhart commented 5 years ago

@theobat - I understand that it's a simple alias, however I can't even seem to construct the alias above - shouldn't I be able to build a GQLV.Object' GQLV.ConstScalar that IS a QuoteR using the makeQuoteR function?, and likewise to wrap it with a GQLV.Object' that is a QuoteGQL? I feel like I'm missing something in the construction

theobat commented 5 years ago

Well it's not even an alias, I was wrong (but it's the same problem for me, it's just worse than I thought) it's a schema definition, it's completely unrelated to the Object value that you constructed (which I tried to explain with my own words):

-- this is for what you built
data Value' scalar
  = ValueScalar' scalar
  | ValueList' (List' scalar)
  | ValueObject' (Object' scalar)
  deriving (Eq, Ord, Show, Functor)

-- this is for QuoteR
data Object (name :: Symbol) (interfaces :: [Type]) (fields :: [Type])
theobat commented 5 years ago

And by the way it seems like there's no constructor at all for the data Object of the schema. Which would very much align with what I said above, you cannot build a value for QuoteR. I don't think this is the right approach, but it's the one in this lib (and any alternative with a more intuitive embedding int the haskell type system brings its own set of problems). @jml @teh if you care to comment, I'd be curious about what you think of all that :)

Benjmhart commented 5 years ago

@theobat - thanks for confirming my suspicions, that there is no way to actually construct my type

teh commented 5 years ago

You are right @theobat - the only way to construct a result is via a handler.

The idea at the time IIRC was that we only wanted to execute a resolver when required, e.g. imagine change in QuoteRecord being more expensive than the other fields so you only want to compute change when the client requests that field.

If you mostly care about convenience I think it'd be possible to write a generic resolver for records because we already have one for sum types to enum here: https://github.com/haskell-graphql/graphql-api/blob/master/src/GraphQL/Internal/API/Enum.hs#L113