New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom scalars #2

Open
Akryum opened this Issue Jul 27, 2018 · 20 comments

Comments

Projects
None yet
9 participants
@Akryum
Copy link

Akryum commented Jul 27, 2018

Migrated from: apollographql/apollo-client#585

@fbartho

This comment has been minimized.

Copy link

fbartho commented Jul 31, 2018

It'd be great if we had support for custom scalars in Apollo Client. A pretty crucial feature would be for custom scalars to be provided by native code. apollo-link-state users would particularly benefit from this.

The canonical example is Date objects (from ISOString or Unix Timestamp "raw" formats), but it would be valuable to vend other complex objects from a local object pool in certain circumstances beyond just dates.

In the original thread there was a reference that maybe a library-provided semi-static list of common scalars might be an acceptable compromise, but immediately, people would want momentjs dates instead of JS Dates.

@bebbi

This comment has been minimized.

Copy link

bebbi commented Sep 14, 2018

The thumbs up total of this and OPs of issues linking here are 22+15+37=74.
Most need json parse and date formats.

@brunoreis

This comment has been minimized.

Copy link

brunoreis commented Sep 27, 2018

I'm thinking about creating a hoc where I can pass a query, the custom scalar type and a conversion function. I would compose that hoc adding it just after the query hoc so that it would convert data injected from that query.

Can someone point me how to inspect the schema of a specific query to detect what are the paths to the fields that are of that custom scalar type?

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 6, 2018

@bebbi why do people need to parse json types on the client side? Unlike dates, json can be serialized just fine by server-side custom scalars and will be received as json by the client.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 6, 2018

@brunoreis you may be able to use graphql's undocumented visitWithTypeInfo, which allows you to traverse the queries while keeping track of the type of each field.

However, it may not be ideal for use on the client because you have to construct a TypeInfo object from an entire GraphQLSchema. I don't know if it would support a partial schema containing only the bare minimum fields necessary to determine what to parse on the client.

Another option is to fetch the type metadata from the server and build a custom index to it; I did this in apollo-magic-refetch. But I had to write my own code to keep track of the types while traversing the query with visit, and there are edge cases I haven't taken care of yet.

I bet there are some hooks in the Apollo caching layer that would allow you to handle custom scalars anywhere in the schema this way, without having to wrap a bunch of your components in an HoC.

@couturecraigj

This comment has been minimized.

Copy link

couturecraigj commented Nov 10, 2018

@jedwards1211 wouldn't that defeat the purpose of a type system? Maybe somebody wants to store mixed data to graphql in certain instances. That is when they would use a JSON type. I know it is not ideal but geo-data for instance can be incrementally adopted through GeoJSON in JSON (mixed) data type that is just stored as a string. Not having to parse it each time the client receives it would be huge.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 10, 2018

@couturecraigj I'm not sure what you're thinking would defeat the purpose if a type system, I was suggesting that there's probably a way to make the apollo cache parse all raw values of a date type (a custom scalar in the schema that serialize to an ISO string on the wire) back into a Date object and store them as such in the cache.

@slorber

This comment has been minimized.

Copy link

slorber commented Nov 14, 2018

Hi,

In addition to custom scalar types, I want to add that it is very important for me that the choosen solution works with codegen tools. If my Date scalar is serialized as String and deserialized or injected as props as JS Date on the client, it's important the codegen tools give me a JS Date type in the end and not a string. This seems complicated to solve, but important to mention.

@bebbi

This comment has been minimized.

Copy link

bebbi commented Nov 14, 2018

@jedwards1211 In my case for example, I'd like not having to worry about de-serialization, because this seems to create multiple redundant places where I worry about the data model.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 14, 2018

@bebbi I not sure I fully understand you.
Without worrying about de-serialization, any date field in your schema will be received by the client as a string or number (whichever you choose to serialize it as in the custom scalar type on the server).

The only way to convert back to a date on the client is to have de-serialization code.

Of course it would be terrible to have to write code for every single date field or every query that requests a date field -- which is why I'm suggesting that if the client has enough information about the schema to know which fields are dates, it would be possible to write a single piece of code that deserializes any date field encountered in the schema.

@bebbi

This comment has been minimized.

Copy link

bebbi commented Nov 14, 2018

@jedwards1211 As far as I can understand we're on the same page.
It's json in my case. I've got various queries using fragments using that json type and would like de-serialization of that type to be controlled in one single place.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 14, 2018

@bebbi so when your server loads the JSON it needs to send from the database or a file or whatever, is it a raw string instead of an object?

If you use graphql-type-json for the field and your resolver returns an object instead of a raw string, it will be sent to the client as an object and the client won't need to deserialize at all.

@gullitmiranda

This comment has been minimized.

Copy link

gullitmiranda commented Nov 14, 2018

@jedwards1211 the https://github.com/taion/graphql-type-json works only to send json to the server, not the opposite.
what we need is to the client parse a json send by the server.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 14, 2018

@gullitmiranda I don't understand why that's been your experience, seems to me something must be misconfigured. Here is proof that it's able to send json to the client in my project (the jsonFieldMappings field below uses graphql-type-json). You can see that the client (GraphiQL) receives it as an object instead of a raw string:

image

@gullitmiranda

This comment has been minimized.

Copy link

gullitmiranda commented Nov 17, 2018

interesting @jedwards1211 . was not aware of this since I don't use the graphql-js on the server side. but from what I've been researching, this was not a standard that is accepted by the default implementation of the graphql, so many libs do not support or do not intend to support this.

some conversations:

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 17, 2018

even if it's not exactly standard, the GraphQL docs do say that

A standard GraphQL POST request should use the application/json content type, and include a JSON-encoded body

Regardless of the method by which the query and variables were sent, the response should be returned in the body of the request in JSON format

In most GraphQL service implementations, there is also a way to specify custom scalar types

So for built-in types, a standard GraphQL server is already serializing to and from JSON, and the most basic possible custom scalar system -- one that just hands you a JSON node representing the scalar in the request or accepts a JSON node from you to represent the variable in the response -- would only necessitate using the identity function for JSON custom scalar serialization/deserialization**.

Which is exactly what a commenter recently mentioned in the issue you linked:

scalar :raw_json do
  parse fn _, _ -> raise "not relevant" end
  serialize fn value -> value end
end

So in other words, any GraphQL server that supports custom scalars is capable of sending a field value that the client will receive as a JSON object instead of a string.

** The only caveat is that it also has to be possible to represent the value as a literal within the GraphQL query itself, not just within the JSON request/response body. But there is a clean mapping between JSON and GraphQL AST nodes, as demonstrated in graphql-type-json's parseLiteral function.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 17, 2018

@slorber do you have enough control over the codegen tools you use to tell them what JS type is associated with a given GraphQL custom scalar type?

@gullitmiranda

This comment has been minimized.

Copy link

gullitmiranda commented Nov 18, 2018

ye @jedwards1211

Which is exactly what a commenter recently mentioned in the issue you linked:

i see and try to use in outputs and worked. don't work with input's for now, but it's no big deal.

@jedwards1211

This comment has been minimized.

Copy link

jedwards1211 commented Nov 18, 2018

Cool. I don't know elixir but it looks like inputs may work if you make the parse fn return the argument instead of raising an error.

@gullitmiranda

This comment has been minimized.

Copy link

gullitmiranda commented Nov 19, 2018

the parse really work, but the absinthe add errors because the typing.

  1) test create template mutation succeeds with valid params (MyAppWeb.Schema.MyContext.TemplateMutationTest)
     test/my_app_web/schema/my_context/mutations/template_mutation_test.exs:12
     match (=) failed
     code:  assert {:ok, data} = run(create_template_mutation(), variables: to_input(params), context: context)
     right: {:error,
             [
               %{
                 locations: [],
                 message: "Argument \"input\" has invalid value $input.\nIn field \"fields\": Expected type \"JSON\", found {key1: \"value\", key2: \"value2\", list1: [1, {keyInList: \"value_in_list\"}]}.\nIn field \"key1\": Unknown field.\nIn field \"key2\": Unknown field.\nIn field \"list1\": Unknown field."
               }
             ]}
     stacktrace:
       test/my_app_web/schema/my_context/mutations/template_mutation_test.exs:54: (test)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment