-
Notifications
You must be signed in to change notification settings - Fork 2k
Async scalars with access to context
and info
?
#2663
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
Comments
Thanks for contributing this feature request. This idea has been discussed before here. The resolver map you're using is something specific to The context and info objects provided to field resolvers are specific to generating the output values returned by the server. But scalars can represent input values like arguments, input object fields or arguments and that same information would not be available in those contexts. |
@danielrearden Thanks a lot, I didn't realize scalars can also be inputs which makes it more tricky. (I also didn't realize that resolver map is specific to graphql-tools, thanks for pointing that out.) Still, when inputs are parsed via The Lastly, what about asynchronicity? Could scalar parsing / serialization function be async, or is there some hard limitation why they cannot? Alternatively, are there other ideas on how to run a resolver-like function (with an async backend API call) for all fields that are of a certain scalar type? |
Specifically, could something like this work? const myScalar = new GraphQLScalarType({
// has access to most things like a resolver, returns a Promise
serialize: async (value, args, context, info) =>
round(value, context.backendApi),
// demonstrates async input validation
parseValue: async (value, context) => {
const rounded = await round(value, context.backendApi);
if (value !== rounded) {
throw new GraphQLError("Please provide a rounded number");
}
return value;
},
parseLiteral: async (ast, context) => /* similar to above */,
}); |
@borekb Scalars can be used as default values inside SDL and during schema building we don't have any context at all: input SomeType {
someField: SomeScalar = 5
} We need to parse it and validate it during schema building without context and synchronously. If none of them works you can you please describe your use case in more detail? |
@IvanGoncharov Our GraphQL server deals with financial amounts as JS numbers which is something we cannot change right now and means that we sometimes get numbers like 0.30000000000000004. The GraphQL server should ensure that when sending such numbers to clients, they are rounded by a backend's rounding policy. We have a schema like this: # Like Float but rounded by backend's rounding policy
scalar MonetaryAmount
type Money {
amount: MonetaryAmount!
currencyCode: String!
}
Query {
aFieldUsingScalar: MonetaryAmount!
aFieldUsingObjectType: Money!
# ... and many more fields that have Money or MonetaryAmount somewhere in them
} Fields that use the const resolvers = {
Query: {
Money: {
amount: async (money, _, { dataSources: { backendApi } }) => {
const roundingInfo = await backendApi.getRoundingInfo(...);
round(money.amount, roundingInfo.decimalPlaces);
}
},
}
}; When new fields are added to our schema and they use the But how to do it for scalars? (There are reasons out of our control why certain fields use the scalar directly, skipping the Money type.) I think something like this would be an ideal solution: const MonetaryAmount = new GraphQLScalarType({
serialize: async (value, context) => {
const roundingInfo = await context.backendApi.getRoundingInfo(...);
round(money.amount, roundingInfo.decimalPlaces);
}
}); IF we ignore inputs for now, do you think the About inputs and especially the default values in SDL, that's a great example that I didn't realize before. So do I get this right?:
Is that correct? Incidentally, just yesterday we changed the types of our inputs from MonetaryAmount back to Floats. To prevent schema mistakes, we even throw from the 'parse' functions: MonetaryAmount: new GraphQLScalarType({
name: 'MonetaryAmount',
description: 'MonetaryAmount is a Float that is rounded by the backend\'s rounding policy',
serialize: value => value,
parseValue: () => throwOnCustomScalarInput('MonetaryAmount'),
parseLiteral: () => throwOnCustomScalarInput('MonetaryAmount'),
}), The reasoning was that we cannot control user input (nothing stops them from sending 0.30000000000000004) and we do not have a way to round the numbers in the parse functions, or even check whether the number is already rounded or not (who knows, maybe the backend rounding policy is 17 decimal places). Overall, I see now how inputs are tricky, or probably just downright impossible to make async. But what about outputs? It would be very useful if |
@borekb If you have some resolution logic that's duplicated across multiple fields, then you should extract this logic into a separate function that can then be called from each resolver. If the resolvers are identical, then you can even do something like:
If you're using Apollo Server or
In other words, if you're just trying to avoid repetition in your code when building your schema, there are existing avenues for doing so. |
We use
Yes, it true for both parsing/printing SDL and sending/receiving introspection results.
Theoretically yes, practically it would result in very strange API where a function doesn't receive most of the parameters and can respond with a promise only in certain situations. Moreover awaiting inside every What you can do is to use wrappers for numbers that you want to wrap and address rounding it before or during serilization: class NumberWithRounding {
constructor(private value: Number) {}
valueOf() { return this.value; }
round(decimalPlaces) { return round(value, decimalPlaces); }
}
const MonetaryAmount = new GraphQLScalarType({
serialize: (value) => new NumberWithRounding(value),
});
// ...
const roundingInfo = await context.backendApi.getRoundingInfo(...);
JSON.stringify(result, (_key, value) {
if (value instanceof NumberWithRounding) {
return value.round(roundingInfo.decimalPlaces);
}
return value;
}) |
@IvanGoncharov Thanks a lot, this nicely explains why scalars are sync and have to be this way. Really appreciated! Also thanks for the example, that's a clever solution that I think should achieve what we're after. I'm just not sure where to put the code after |
@danielrearden We're already de-duplicating rounding logic in a way you suggested – we have a single function that many resolvers call. The issue is with the "many resolvers" part – we have to remember to correctly implement rounding resolvers for fields that happen to use scalars (vs. the |
@borekb After execution is finished but before you send respond. |
Another option is to make use of the mapSchema function from graphql-tools https://www.graphql-tools.com/docs/schema-directives#full-mapschema-api You can easily iterate through your executable schema and find every object field of that scalar type and modify the resolver to do whatever you need. This is a parallel approach to existing middleware options. |
Even though the link references directives you can use map schema to do whatever you need even without directives just based on the field type... |
@yaacovCR That sounds very useful, thanks! |
I should say that mapSchema as currently implemented creates a new schema with all new types that are rewired together. This does mean that not every schema is compatible with mapSchema. Specifically, resolveType resolvers for interfaces should return type names instead of actual types, maps of types should be keyed by typeName rather than the actual type, etc, etc. All GraphQL type system extensions should be in the extensions field, as any annotations of GraphQL type system objects that are not in the extensions field will be lost. |
Closing since it looks like a bunch of alternative solutions suggested. |
Note that the use case in the related ticket #3539 relates to customizing error message languages, with the parse/serialize functions the normal place where these errors and messages are generated. The implementation within #3600 that allows access to |
In our resolver map, we have two quite different things:
info
orcontext
, and they are usually async.info
orcontext
(or anything request-related) and their functions likeserialize
aren't async.I wonder if scalars could be more resolver-like?!
Concrete example
Our use case is price rounding. In our case, GraphQL server code doesn't know how to round – it needs to ask a backend API for rounding rules – but it is GraphQL code's responsibility to ensure that values are rounded.
Most prices in our schema are represented by a
Money
type that looks like this:We can then do something like this, which works well:
Some parts of our schema, however, use the scalar directly, skipping the
Money
type. For example:I'd love to be able to write resolver-like code for it, like this:
Currently, we need to solve this on the specific field level, for example, if there's a field like:
we can hook into
actionField
and process the rounding there. But it would be safer and easier if we could solve it at the type level.What do you think? Has this been considered before? I didn't find much but maybe my search skills have failed me.
The text was updated successfully, but these errors were encountered: