higherkindness / mu-haskell

Mu (μ) is a purely functional framework for building micro services.
http://higherkindness.io/mu-haskell/
Apache License 2.0
333 stars 19 forks source link

Support for arbitrary scalars? (GraphQL) #293

Closed lfborjas closed 3 years ago

lfborjas commented 3 years ago

Hi there. Thank you very much for this excellent collection of libraries! Watching Alejandro's videos about the graphql capabilities inspired me to write my latest service in GraphQL, using mu-graphql!

However, I'm stuck on something:

In my schema, I'm planning to include a DateTime scalar that I would like to map to a LocalTime in Haskell. As per the graphql spec, here's an example schema:

scalar DateTime

type User{
  dob: DateTime!
}

type Query{
  firstByDoB(dob: DateTime!): User
}

I'm not quite sure what to tell the Haskell side, however; I tried adding a naïve mapping:

type ServiceMapping = '[
  , "DateTime" ':-> LocalDateTime
  ]

Where LocalDateTime is a newtype over LocalTime (from Data.Time). If I omit the mapping, the generated type seems to infer that the type of the DateTime argument in the query is ().

The crux of the matter is: I don't quite know how to wire it up: what to tell my resolver so it can actually act on that mapping: should I use method at the top level? That doesn't seem right, since it's a type, not a query/mutation -- and If I do that, I get could not find method "DateTime" errors, as well as: No instance for (mu-graphql-0.5.0.2:Mu.GraphQL.Query.Parse.ParseArg, anyway 😅

If I try a wild guess and derive schema instances manually, while keeping the mapping:

newtype LocalDateTime 
  = LocalDateTime LocalTime
  deriving newtype (Eq, Ord, Show)
  deriving (Generic)

instance FromSchema MyApiSchema "DateTime" LocalDateTime
instance ToSchema MyApiSchema "DateTime" LocalDateTime

I get "cannot find type 'DateTime' in the schema" errors in addition to the previous ones, which seems to point to it not being recognized, maybe? But at this point, I'm just flailing: it's just not clear to me how an arbitrary scalar is supposed to be "wired into" the mechanism, short of writing the schema by hand? I'm happy to write the instances of FromSchema and ToSchema by hand, since it would make sense to be responsible for how the Strings that are sent over the wire egress/ingress Haskell-land, but I think I'm stuck at an earlier stage than that.

I initially resisted using morpheus because of the proliferation of types it promotes, but I do like how they handle scalars: https://github.com/morpheusgraphql/morpheus-graphql#scalar-types (in Servant-land, I would also have just done a bit of instance carpentry for a custom type, though in the specific case of LocalTime, the necessary instances already exist for JSON/HttpApiData.)

I see there's a relatively recent PR in which you added JSON scalars explicitly to the source, does that indicate that arbitrary scalars are not supported without a similar, explicit, update/extension to internals?

serras commented 3 years ago

Unfortunately, this is the case right now, scalars are hard-coded. The main reason is not serving them, which can be extended with IntrospectTypeRef, but parsing them from the GraphQL file: how would the graphql template know that DateTime corresponds to Haskell's LocalDateTime? 🤔

Maybe we should invent some way to inject that knowledge at GraphQL schema parsing time. I'm sure we can find a solution, we know the information, we just need to send it to the right places! 😀

serras commented 3 years ago

@lfborjas I've started doing some preliminary investigation on this on #295. Hopefully this will turn into an easy way to extend the set of scalars in GraphQL.

In the meanwhile, what is the interface you expect for DateTime, is there a spec for it? Apart from adding a way to extend the scalars, I think that adding DateTime as part of the basic set of scalars would be good!

lfborjas commented 3 years ago

That's amazing Alejandro, thank you!

You know, I haven't really found anything authoritative for date/time in graphql (though I did run into this fancy thing they do in grandstack.io, which indicates that in their design DateTime if a fully fledged type, not a simple scalar!). It would seem that the general sentiment is "roll your own"? (EDIT: I'm not sure of how authoritative this resource is, but there's at least one library out there that defines Date and DateTime scalars for graphql using RFC3339 strings: https://www.graphql-scalars.dev/docs/scalars/date-time)

As expanded upon below, by way of a spec, I personally am just imagining the simple ISO8601/RFC3339-looking formatted strings that Haskell's aeson/swagger2 work with; though as I researched a bit to answer you, it does seem that DateTime opens the pandora's box of "there's a ton of types for all kinds of time representations" 😅

In the service I'm working on, I already have a servant/swagger API, so I'm using as reference the FromJSON/ToJSON/ToSchema instances the underlying libraries provide for LocalTime; in Haskell's Swagger2, they expect LocalTime (which in my mind is the target type for the DateTime scalar, though maybe I should be calling it LocalDateTime since no timezone information is provided?) to be serialized/deserialized from/to strings as yyyy-mm-ddThh:MM:ss -- instances for ZonedTime, LocalTime, TimeOfDay, NominalDiffTime, UTCTime, etc. also exist, as linked before and also for the ToParamSchema class, which I think hews close to the notion of a gql scalar? I think if some date/time scalars are to be provided, it'd be cool if a reasonable subset of the usual cases of "time with timezone", "local time [i.e. time without timezone]", "date", "time", "utc time", and even "instant in seconds"/"instant in milliseconds or picoseconds" can be supported -- all of which correspond to types in Haskell's Data.Time and which aeson/swagger2 recognize.

In my particular service I only use UTCTime for server responses (as linked above, this is serialized as "yyyy-mm-ddThh:MM:ssZ") and LocalTime (for user input, parsed as "yyyy-mm-ddThh:MM:ss" -- the users also provide their location/IP so I derive the timezone.) I reckon in the more general/sane case, to save oneself from timezone hell, one would probably want to transact in UTCTime only, or in ZonedTime if the client can reliably provide the tz offset, or maybe even just an integer instant as a unix epoch or milliseconds, for which I think Haskell's DiffTime/NominalDiffTime are applicable 🤔