mhallin/graphql_ppx

Generate a serialize function that convert Config.t to Js.Json.t (opposite of parse function basically)

dawee opened this issue · 5 comments

dawee commented

When we want to write in the cache using writeQuery, writeFragment, fetchMore.updateQuery ... we need to send it in a JSON format.

It can work as is, if the query has no optional, no fragments in it. But if it does, then the parse function returns a type that includes one or more variants. Which is great, but it can't be reused as is as a JSON format.

Even if we try an unsafe cast it wouldn't work because Bucklescript will translate the variant as an array.

Dog(4) => [2534, 4].

What would solve this would be a generated serialize function that do the opposite of what parse is doing:

module MyQuery = [%graphql
  {|
     ....
  |}
];

readQuery(...) |> MyQuery.parse |> myTransform |> MyQuery.serialize |> writeQuery(...)

It’s a great idea. I found it problematic to writeQuery to apollo cache with enum field which is transpiled to poly variant this number in JavaScript.

I’ll try to find some time to work on it ;)

Giving such schema into consideration:

interface User {
  id: ID!
}

type AdminUser implements User {
  id: ID!
  name: String!
}

type AnonymousUser implements User {
  id: ID!
  anonymousId: Int!
}

type OtherUser implements User {
  id: ID!
}

and such query:

query {
  users {
    id
    ... on AdminUser {
      name
    }
    ... on AnonymousUser {
      anonymousId
    }
  }
}

We can receive example JSON from the server:

{
  "users": [
      { "__typename": "AdminUser", "id": "1", "name": "bob" },
      { "__typename": "AnonymousUser", "id": "2", "anonymousId": 1},
      { "__typename": "OtherUser", "id": "3"}
  ]
}

Our Reason/OCaml output will be presented as polymorphic variant handling most of the cases:

type t = [
  | `AdminUser({. "id": string, "name": string})
  | `Anonymous({. "id": string, "anonymousId": string})
  | `User({. "id": string })
]

The default case is a User which is an interface name and an "other" case handling all future additions and not handled by fragments interface implementation. This implementation works ok as @dawee uses in his project. But it has one downside. We lost information which makes it able to serialize Reason/OCaml parsed representation back into JSON. My proposed change would be to add string field into "other" cases as follows: User(string, {. "id": string }) where a string is real __typename present on the JSON response. In pattern matching, it is just a small change but gives us all required info to implement serialize function.

I have basic implementation working in my Reason fork: https://github.com/baransu/graphql_ppx_re/tree/serialize_fn

Having this implemented would allow us to have proper cache support in reason-apollo or other GraphQL clients.

/cc @dawee @Gregoirevda

dawee commented

I think it makes sense that the default variant keeps the typename. It might be useful if you want to log it:

fun
  | `User(typename, _) => Js.log("The user type " ++ typename ++ " is not managed yet")

Because in most cases for us the variant with the interface name is the case we don't manage.

@baransu I like this idea. What do you think @mhallin ?

Another challenge is @bsDecoder(fn: my_decoder). While it's nice to have such functionality, it's only one-way decoding done at query parse time. It works great but with serialize function, we have data already decoded and now way to encode it back into its original shape.

Possible solutions I can see are:

  • deprecate @bsDecoder
  • introduce @bsSerialize(decode: decode_fn, encode: encode_fn)
  • throw or warn users when using serialize on the query that has @bsDecoder defined