-
Notifications
You must be signed in to change notification settings - Fork 7
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 #368
Comments
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. 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 |
The thumbs up total of this and OPs of issues linking here are 22+15+37=74. |
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? |
@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. |
@brunoreis you may be able to use However, it may not be ideal for use on the client because you have to construct a Another option is to fetch the type metadata from the server and build a custom index to it; I did this in 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. |
@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. |
@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. |
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. |
@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. |
@bebbi I not sure I fully understand you. 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. |
@jedwards1211 As far as I can understand we're on the same page. |
@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 |
@jedwards1211 the https://github.com/taion/graphql-type-json works only to send json to the server, not the opposite. |
@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 |
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:
|
even if it's not exactly standard, the GraphQL docs do say that
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 |
@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? |
i see and try to use in outputs and worked. don't work with input's for now, but it's no big deal. |
Cool. I don't know elixir but it looks like inputs may work if you make the |
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) |
Any movement on this? |
I've noticed support for custom scalars is referenced in the GraphQL Tools docs, but as far as I can see it isn't in the pipeline for apollo-client (including 3.0, or at all). Could someone in the know confirm this? |
This is definitely a must have ! Any news about appolo-client plans for support ? |
While a complete fix needs thoughtful architecture and discussion (from the original ticket and cloned to this), a fix for Discussion of
Re-produce the issue at
I'd love to have |
I don't get it. I went through 2 years old history discussing this and nothing viable seems to come out of it? I wonder, when I look at the example from graphql-tools page, they are using import { makeExecutableSchema } from 'graphql-tools';
import GraphQLJSON from 'graphql-type-json';
const schemaString = `
scalar JSON
type Foo {
aField: JSON
}
type Query {
foo: Foo
}
`;
const resolveFunctions = {
JSON: GraphQLJSON
};
const jsSchema = makeExecutableSchema({ typeDefs: schemaString, resolvers: resolveFunctions }); Edit: Nevermind, turns out that |
Ok, I think I've found fairly good approach, at least to handle ISO dates. With that link, I can easily transform iso date strings to https://github.com/with-heart/apollo-link-response-resolver BusinessHourInterval: {
openAt: parseISO,
closeAt: parseISO,
}, For the reverse direction when I am using As for the TypeScript, we are using |
I have been working on a custom ApolloLink to deal with this situation. https://github.com/eturino/apollo-link-scalars If you pass it a It can optionally also validate enums, throwing an error if an invalid enum value is received. By default, for each scalar, it will apply the parsing/serializing functions defined for that type in the As a small disclaimer: this is a POC. I just finished testing it on my team's app but I haven't tested performance. import { withScalars } from "apollo-link-scalars"
import { ApolloLink } from "apollo-link";
import { HttpLink } from "apollo-link-http";
import { schema } from './my-schema'
const link = ApolloLink.from([
withScalars({ schema }),
new HttpLink({ uri: "http://example.org/graphql" })
]);
// we can also pass a custom map of functions. These will have priority over the GraphQLTypes parsing and serializing functions from the Schema.
const typesMap = {
CustomScalar: {
serialize: (parsed: CustomScalar) => parsed.toString(),
parseValue: (raw: string | number | null): CustomScalar | null => {
return raw ? new CustomScalar(raw) : null
}
}
};
const link2 = ApolloLink.from([
withScalars({ schema, typesMap }),
new HttpLink({ uri: "http://example.org/graphql" })
]); //cc @FredyC |
note of archived response-resolvers repo with-heart/apollo-link-response-resolver#18 Link to https://github.com/apollographql/apollo-feature-requests/issues/2
…izing interaction add links to the original apollo-client issue apollographql/apollo-client#585 as well as the new issue https://github.com/apollographql/apollo-feature-requests/issues/2
There is FieldPolicy in Apollo Client 3. It allows us to serialize/unserialize values when read/write from/to the cache. It's on field rather than on type level. Thus one needs to configure the transformation for every field again. However it mitigates the problem of serializing the cache. |
@rjdestigter the example above doesn't need custom scalar support from Apollo Client. You don't need any runtime code at all - this just exists on a type level. Have your codegen output your type as scalars: {
UserId: 'src/user/types#UserId_UsingSymbolAsBrand',
}, (see docs for typescript-operations) declare const tagSymbol: unique symbol
type UserId_UsingSymbolAsBrand = string & { [tagSymbol]: "UserId"} From that moment on, all Tagged types are a type-level construct. That Edit: edited in the correct type referencing method shown further down by @floriancargoet |
Ah, ok. My FUD was how to refer in the to an imported type in config:
...
scalars:
UserId: "import(\"src/user/types.ts\").UserId" |
@rjdestigter I can't find it in the docs but there's a syntax to magically import custom scalars in
This will be turned into For your example, it would be:
|
@jpvajda Hi, regarding the message you did post on the 15th of December. Our use case sounds straightforward but let's describe it as follows: Our graphql schema has 2 scalars On the client side, I'm generating typescript types using codegen from the graphql schema so that everything is typed properly. By default, any scalar unknown by codegen will be typed "any", here is an extract of the generated /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** A date containing the hour. Ex. 1977-04-22T06:00:00Z */
DateTime: any;
/** A date without an hour. Ex 2022-01-01 */
PlainDate: any;
}; As is, in this particular case, I could without taking any risk configure codegen to type the string format of DateTime and PlainDate as a template literal. For instance (dummy code generated for the example) scalars.ts type PlainDate = `${number}-${number}-${number}`;
type DateTime = `${number}-${number}-${number}T${number}:${number}:${number}.${number}`; And let codegen know about those types using: config:
scalars:
PlainDate: path/to/scalars#PlainDate
DateTime: path/to/scalars#DateTime So that the new generated file will be typed as follows export type Scalars = {
...
/** A date containing the hour. Ex. 1977-04-22T06:00:00Z */
DateTime: DateTime;
/** A date without an hour. Ex 2022-01-01 */
PlainDate: PlainDate;
}; At this point, there is no problem because a template literal is a string, and the gql transport layer will send this string without any problem. Now, instead of receiving a string object, whatever it's format, I would prefer if I could receive an object I can directly use without having to revive it manually. While I could type the DateTime and PlainDate as follows: export type Scalars = {
...
/** A date containing the hour. Ex. 1977-04-22T06:00:00Z */
DateTime: Date;
/** A date without an hour. Ex 2022-01-01 */
PlainDate: Temporal.PlainDate;
}; Without a reviver on the client side, I will obvioulsy still receive a string, and therefore wrongly type this string. Now, as I read in a comment, Apollo Client is not server and does not have the schema by itself, it therefore has no information on the expected scalar type. It surely has the object's This is pretty much what this plugin is trying to achieve as seen in this article (which itself mentioned this git issue, «salut Patrick si tu me lis») To summarize:
It's a little bit like dynamic vs static or runtime vs compile My understanding is that apollo client should provide an official way to manually revive those scalars so that a codegen plugin could then automate this process. I see 2 things to be done
Of course, this is not very sexy having to manually register each scalar within a type to let apollo know which property should be revived, but if it can be manually done, then it will be feasible for codegen to build something on top of it. Another problem: while reviving parameters from the data using the typename is what we need, we have to think about serializing the inputs back to json so that they can be sent over the line. By default, the method Also, how would chrome's apollo works with those transformations ? my understanding is that it would stay with the underlying JSON data. |
Is there at least a way to manually configure these scalars to something other than can I configure codegen so that instead of /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
/** A date containing the hour. Ex. 1977-04-22T06:00:00Z */
DateTime: any;
}; it generates /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
/** A date containing the hour. Ex. 1977-04-22T06:00:00Z */
DateTime: string; // ----> generate this instead of `any`
}; any is very harmful, if we can at least configure it this way this is better than the current state |
@fanckush assuming you're using GraphQL Codegen, you can configure the If you'd prefer a different default instead of Hope this helps! |
@jerelmiller thanks that's exactly what I meant. Just to clarify, this would only change the type and doesn't perform any transformation or modification to the actual fields, right? |
correct |
@jerelmiller how do we actually make the transformation appends in apollo client ? Do we have to use Apollo Client Link ? The main used example atm is |
@Yimiprod Could you clarify what you mean by "appends" here? I want to make sure to best answer your question.
@fanckush that is correct, the change I mentioned only changes types, not the actual parsed value. We do plan to support custom scalars sometime within the next couple minors (likely 3.10 or 3.11), but we haven't quite yet begun work on this yet. We know this is a highly requested feature (as evidenced by all the activity and reactions here) and want to start thinking about this after our work with Suspense is complete. As an aside, one of the reasons custom scalars are difficult to handle today, either in Apollo Link or via a type policy, without full support from the client, is because of the interaction with the cache. The cache expects all values to be JSON-serialized values, so parsing scalars in other areas has the potential to do weird things with the cache. I personally have a vested interest in this feature as I've wanted this so many times in the past. Keep an eye on our roadmap for updates as we will post an update there when we plan to start development. I am definitely looking forward to closing this one out, hopefully sooner than later! |
Hi all; our codegen plugin We've fixed both issues, but also looking forward to a native apollo solution as well. Thanks! |
Does anyone know when native support for the Apollo Client will be released? I see a lot of activity on this feature request and wondering if a launch is imminent :) |
@serengetisunset I don't know if there are any recent plans to natively support, especially since it's 6 years the issue is open. I guess it's not so easy to do anyways because you would need to go through each field in the server response and check if it should be parsed. After that there's still the need to override the type in the codegen, so your types are correct. If you don't mind trying something new, you might take https://github.com/liontariai/samarium for a test drive, I just implemented the final version of the custom scalar support. And it lazily deserializes your custom scalars with your given function and allows for setting the types for the whole client at once. See explanations here: liontariai/samarium#9 (comment) It's still beta and might not work for all schemas that apollo works with, so any feedback is greatly appreciated :) |
@serengetisunset this is on our short list of features to pull in next. We've been focused on other features up to this point (React Suspense, data masking, new testing utilities, etc.). We're hoping to pull this in soon as we are very aware of the popularity of this request. To give some background, the reason we haven't quite pulled this in yet is the complexity required to integrate this. One of the biggest challenges is that Apollo Client isn't aware of your schema (nor would it be feasible to require this because it could add tons of bundle size to your app for large schemas). We need to come up with a way that allows you to define which scalar fields map to a particular custom scalar type in a scalable way (i.e. imagine you have 100 date fields, how do you configure these in a reasonable way without knowledge of the whole schema and that doesn't require you to define 100 type policies). Some other things that have been on my mind that I think would be awesome:
We're also noodling on whether we can/should enable this for any third party or custom cache implementation. Adding this feature exclusively to Again, we are hoping to pull this in soon! Just wanted to throw some details in here on why its a bit more complex than meets the eye :) @liontariai cool to hear about other projects in the ecosystem, but if you wouldn't mind keeping this discussion to this particular feature, that would be appreciated. |
I didn't intend to steer the discussion into a different direction - even though my little reference to the 6 years was on purpose, I myself have been missing this feature for a long time :) ... so I'm happy to hear smth from you. Also, I agree that it is quite challenging to do, as I pointed out myself... I'm not sure if this is even possible without the help of additional codegen tools. From my understanding external codegen via other libraries is normal practice with apollo client and may also help in this case. Since you need knowledge of the schema to pull this off, it would make sense to collect the paths to custom scalar types in your queries at code/type-gen time and have the apollo client utilize this? One could probably make a @graphql-codegen plugin for this. If the output map is something like this:
It could be used by the Cache, or am I missing something? The thing could be implemented with the library I mentioned, since while building the query, the information about the type and the path in the query is there. Just something I was thinking about... to combine both things. |
Haha totally hear you on that and can understand the frustration. Selfishly I've also wanted this feature for some time, so I definitely want to see it sooner than later. Hang with us a little longer!
You bring up a good point here which is that anything we add will likely need to be workable with GraphQL codegen. I'd like to avoid requiring codegen to use this feature, though we'll encourage its use to make configuration significantly easier. My thinking here is that codegen would do something similar to the
I'm sure we'll be doing a lot of experimentation on the format here to see what feels right. This brings me back to my comment about the For this reason, we may very well may need to only allow this feature with We should only need a mapping between the type and the field that contains the scalar since we can derive the rest from the response itself (hence why The only other part I'd say is missing is where you'd configure how to actually parse and serialize that scalar. I imagine this will look similar to a GraphQL server where you configure a scalars: {
Date: {
parse: (rawValue) => { /* ... */ },
serialize: (parsedValue) => { /* ... */ },
fields: {
Project: ['createdAt', 'updatedAt'],
Post: ['lastSeenAt']
}
}
} Again, not something that is final, but just trying to illustrate some of the parts needed to make this feature work. |
Migrated from: apollographql/apollo-client#585
The text was updated successfully, but these errors were encountered: