Comments (11)
Here’s another thought. Perhaps we keep the mental model that GQL tags map 1 to 1 with SDL constructs.
Defining a JavaScript executor, of course, requires more than this, as we are seeing here. Perhaps these additional capabilities should be defined via a more traditional api.
For example our generated artifact could expose a getSchema
function instead of exporting the schema directly. This function would required you to pass the serialize/parse functions for all custom scalars. So using the Grats schema might look like:
import {getSchema} from "./schema";
const schema = getSchema({
scalars: {
Date: {
serialize(date: DateTime): string {
return date.toString();
},
parse(input: unknown): DateTime {
return parseDateTime(input);
}
}
}
});
Grats could take care of generating types such that you would be required to provide these functions.
Concrete example of the code we could generate:
import { MyScalar as MyScalarRuntimeType } from "./path/to/scalar";
import { GraphQLSchema, GraphQLObjectType, GraphQLScalarType } from "graphql";
export type SchemaConfig = {
scalars: {
MyScalar: {
serialize(input: MyScalarRuntimeType): unknown;
parseValue(input: unknown): MyScalarRuntimeType;
};
};
};
export function getSchema(config: SchemaConfig) {
const MyScalarType: GraphQLScalarType = new GraphQLScalarType({
description: "A description",
name: "MyScalar",
serialize: config.scalars.MyScalar.serialize,
parseValue: config.scalars.MyScalar.parseValue
});
const QueryType: GraphQLObjectType = new GraphQLObjectType({
name: "Query",
fields() {
return {
mammal: {
name: "mammal",
type: MyScalarType
}
};
}
});
return new GraphQLSchema({
query: QueryType,
types: [MyScalarType, QueryType]
});
}
from grats.
I'm proceeding with the getSchema
approach. One interesting issue is that the types exposed by graphql-js for serialize/parseValue are needlessly vague. What's worse, it seems to be intentional:
graphql/graphql-js#3451 (comment)
Probably here we'll want to break with our design principle of preferring to use our dependencies types where possible and present more sensible types. This seems especially reasonable given the comment:
There are type-safe wrappers for GraphQL-JS like nexus, TypeGraphQL, giraphql, probably others, that would allow you, I believe, to have more TS convenience, [...]
Which indicates they expect libraries at our abstraction to be more opinionated than they are.
from grats.
Is there an escape hatch for this right now if you want to have custom serializer/deserializer function?
from grats.
The only escape hatch I can think of would be monkey patching the GraphQLSchema instance created by Grats.
from grats.
An observation: Probably however we solve this, it should work the same as resolveType
on abstract types, which is another similar missing feature in Grats today.
from grats.
My intuition for this after using Grats for a couple days is that it should just be similar to how resolvers are defined for a plain object type.
/** @gqlScalar */
type DateTime = Date
/** @gqlScalar */
function serialize(date: DateTime){
return date.toISO8601()
}
/** @gqlScalar */
function parse(value: unknown): DateTime {
try {
return new Date(value);
} catch (e) {
throw new Error("Not a valid date string")
}
}
from grats.
I really appreciate hearing what felt intuitive given your existing experience with Grats! That's very helpful. I love that this approach does not require any additional syntax, however there are a few things I don't love about this approach:
- The
@gqlScalar
tag now means different things in different contexts. That would break from our existing rules where each tag maps directly to the GraphQL construct it will create. - It does not allow us to ensure you always provide a parse/serialize function when you need one (I guess we could error unless you always provide them?)
- Would we force the name of the parse/serialize functions to be "parse" and "serialize", or would we determine their meaning from the type signature? If it's the former, it would be hard to define multiple scalars in the same file without getting a name collision.
Update: Another thing that just occurred to me. In theory one could define a custom scalar who's JavaScript representation was itself a function. So, determining which of the /** @gqlScalar */
s refer to the scalar and which refer to the conversion functions might be tricky to do cleanly.
I'll try to enumerate other options as I think of them as well as their pros and cons. I don't (yet?) see a solution that feels really good.
from grats.
Object Export Options
What if defining a scalar was done by annotating an exported object literal that defined the parse/serialize methods?
/** @gqlScalar **/
export const Date = {
serialize(date: DateTime): string {
return date.toISO8601()
},
parse(value: string): DateTime {
return new Date(value);
}
}
Pros
- We can enforce that every scalar defines parse/serialize (it's easy enough to define them as the identity function if you don't need them)
- One docblock = one scalar
- No new docblock tags
Cons
The docblock tag does not actually go on the definition of the type that will be returned by fields or accepted as args.
- This breaks convention with the rest of Grats, reducing the degree to which we can expect people to intuitively "do the right thing". (That said, we can easily guide them to the right thing with an error message).
- This is especially confusing since we would (presumably??) derive the GraphQL from the exported name, but here the exported value is the serialize/parse object not the scalar value itself.
- This may also introduce complexity within Grats, since we need to know the definition of the type in order to know which field return values or arg types are referencing this type.
from grats.
I like that a lot - Downside seems to be that we lose co-location?
It also seems like we're going to expose something like getSchema for other things in the future anyway right?
from grats.
I've documented the current work-around here: 561a914
from grats.
Looking at the types in graphql-js
a bit more fully to see how our types should differ from theirs:
Here's basically what graphql-js has, and why:
type Scalar<TInternal, TExternal> = {
// Unknown becuase... resolvers might not return the right thing
serialize(outputValue: unknown): TExternal,
// Unknown because... called on user-passed variables values
parseValue(inputValue: unknown): TInternal,
parseLiteral(valueNode: ValueNode, variables?: Maybe<ObjMap<unknown>>): TInternal
}
I think Grats can do two things differently.
- Use
TInternal
for the input of.serialize
, since Grats can ensure the resolvers will return the expected type. (Assuming TypeScript type safety) - Omit the
TExternal
type param, since it only appears in one place in the whole program.- (I suppose it might also appear somewhere inside the implementation of
parseValue
after the input has been validated?).
- (I suppose it might also appear somewhere inside the implementation of
My proposal for Grat's type:
type GratsScalar<TInternal> = {
// Unknown becuase... resolvers might not return the right thing
serialize(outputValue: TInternal): any, // You can serialize as anything you want
// Unknown because... called on user-passed variables values.
parseValue(inputValue: unknown): TInternal,
parseLiteral(valueNode: ValueNode, variables?: Maybe<ObjMap<unknown>>): TInternal
}
from grats.
Related Issues (20)
- Format error messages using markdown
- Support defining input types with TypeScript interfaces
- TS Plugin Feature Ideas HOT 2
- Report an error if unknown config options are passed HOT 1
- Fails with (0 , graphql_1.assertName) is not a function if using GraphQL 15.3.0
- When used with earlier versions of TypeScript (^3.8.3) all docblocks are reported as detached
- Create advanced example app
- __typename must be const HOT 5
- Check if we correctly report @gqlField on method/prop of non @gql* construct
- Clarify license is MIT
- Add support for @specifiedBy HOT 1
- Bug: Import paths in the generated typescript schema file contains backslashes when codegen is run on Windows 11 HOT 8
- Union used in a union should not report a __typename missing error
- Have generated schema file export a NodeInterfaceTypenames type
- Add the ability to define fields using static methods just like exported functions
- Allow function fields to be generic
- Always emit stable line endings
- Allow user to specify `importModuleSpecifierEnding` in their config
- Example: likes' count HOT 1
- VSCode plugin not working with TS 5.5.2 HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from grats.