captbaritone / grats Goto Github PK
View Code? Open in Web Editor NEWImplementation-First GraphQL for TypeScript
Home Page: https://grats.capt.dev
License: MIT License
Implementation-First GraphQL for TypeScript
Home Page: https://grats.capt.dev
License: MIT License
These are not a public api, but could be helpful for folks to understand
When we mix:
type
The result is unsafe. This is because TS does not have any way to validate that a type
implements and interface
. Therefore, the properties/methods we expect to find on the interface may not exist on the type, resulting in a runtime error.
/** @gqlInterface SomeInterface */
interface SomeInterface {
/** @gqlField */
some_field: string;
}
/**
* @gqlType
* @gqlImplements SomeInterface
*/
type SomeType = {
__typename: "SomeType";
/** @gqlField */
name: string;
};
/**
* @gqlField
*/
export function some_field(someType: SomeType): string {
return someType.name;
}
/**
* @gqlField
*/
export function some_other_field(parent: SomeInterface): string {
// This could error at runtime since it might be called with an instance of `SomeType`
return parent.some_field.toUpperCase();
}
express-graphql is deprecated and graphql-http does not include graphiql
Right now Extractor leverages the type context in ways that mutate it to track observed values. This makes the data tracking less explicit and also prevents an incremental future where individual file parses can be reused.
Instead, we should have Extractor return an artifact that is based purely on syntactic analysis and perform the mutation and definition lookups for a second phase.
So, ideal outcomes:
This is theoretically handled by semver, but we should provide a better error
/**
* @gqlType
*
* This is a note for myself
*/
export type Query = unknown;
/** @gqlField */
export function queryField(_: Query): string {
return ''
}
# ERROR MESSAGE
# =============
# Grats playground bug encountered. Please report this error:
#
# l@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2504646
# o@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2550813
# q@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2554958
# z@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2597543
# y@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2592981
# S@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2599039
# 8284/ue/r<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:41216
# ue@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:41897
# 8284/qe/</o<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:43569
# 8284/qe/<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:43763
# 7781/run/<@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2227204
# run@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2227180
#
The error dump doesn't give us much to work with, however, if we use this code:
/**
* @gqlType
*
* ThisIsANoteForMyself
*/
export type Query = unknown;
/** @gqlField */
export function queryField(_: Query): string {
return ''
}
We get this as the resulting schema:
type ThisIsANoteForMyself {
queryField: String
}
Maybe @gqlType
needs to take only until the next newline to extract the type's new name?
We should look at graphql-scalars
for compatibility. And Pothos' concept of Input and Output for scalars.
We should also see how Strawberry and Juniper address this.
Specifically, we need a way to let users define these bits of a custom scalar: https://github.com/graphql/graphql-js/blob/688ee2f2a2f938e80bcf3808bc435d3d87a3ff3e/src/type/definition.ts#L548-L551
I had originally opted not to support defining interfaces via classes because of the mismatch between TypeScript having single inheritance and GraphQL having multiple inheritance. You can't express:
type User implements Person, Node {
# ...
}
using only class inheritance with TypeScript. That said, people do have inheritance setup in their TypeScript code bases which make sense as interfaces as they expose that same code in GraphQL. Following this design principle, we ought to support this for the cases where users have such code.
There will be some challenges:
This is a nice place to introduce your overall schema, but we can't currently add it via Grats.
In order to "move fast" during development we added a dependency on graphql-tools/utils
. Our use of them is pretty minimal, so in keeping with our Design Principles we should try to extract just the bits we need with our own minimal implementations.
These would tag a single object type as the type used in the corresponding schema
declaration in the resulting SDL. So, something like this:
/**
* @gqlType
* @gqlSchema.query
*/
export class MyOddlyNamedQueryType {
/** @gqlField */
me(): string {
return new 'yo';
}
}
Would generate:
type MyOddlyNamedQueryType {
me: String
}
schema {
query: MyOddlyNamedQueryType
}
Same for @gqlSchema.mutation
and @gqlSchema.subscription
. Obviously, at build/introspection time, a check should run that asserts that each of these @gqlSchema.*
appear only once. Defaults would still be used; so, there is a @gqlSchema.query
on a type, but there's no @gqlSchema.mutation
anywhere, it is assumed that the schema's mutation
key would be whatever object type is named Mutation
.
# ERROR MESSAGE
# =============
# Grats playground bug encountered. Please report this error:
#
# Is@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4259459
# Rs@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4260178
# Js@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4260359
# vm@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4343317
# ue@https://grats.capt.dev/assets/js/492.e7db54da.js:28:42020
# 1273/src/compiler/parser.ts/e.parseSourceFile/f<@https://grats.capt.dev/assets/js/492.e7db54da.js:28:41534
# 1273/src/compiler/parser.ts/e.parseSourceFile@https://grats.capt.dev/assets/js/492.e7db54da.js:28:41790
# wF@https://grats.capt.dev/assets/js/492.e7db54da.js:28:23938
# getSourceFile@https://grats.capt.dev/assets/js/492.e7db54da.js:2:5629
# 4182/e.createProgram/Nt/o<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2716133
# Nt@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2717664
# 4182/e.createProgram/wt/<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714689
# Et@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714080
# wt@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714666
# St@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2711583
# 4182/e.createProgram/<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2681888
# 4182/e.forEach@https://grats.capt.dev/assets/js/492.e7db54da.js:23:381412
# 4182/e.createProgram@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2681857
# 8284/ue/i<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:38188
# ue@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:39552
# 8284/Oe/</o<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:41185
# 8284/Oe/<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:41379
# 7781/run/<@https://grats.capt.dev/assets/js/492.e7db54da.js:28:2227204
# run@https://grats.capt.dev/assets/js/492.e7db54da.js:28:2227180
I attempted to fix it, but cloning the repo, installing and running with yarn
yields several import errors (which I tried to hack around) and then some more hook-related errors...
Not sure exactly what triggers this, or what a good repro is.
I haven't looked into this at all yet, but we should support this.
This should help us prevent links from breaking as the website evolves.
Could include @generated
to ensure Prettier ignores them
@ObjectType({
description: "A person who has composed opera.",
})
/** @gqlType */
export default class Composer {
@Field((type) => String)
/** @gqlField */
url(): string {
return `/composer/${this._composer.id}`;
}
}
Silently extracts zero things. Moving the docblocks above the decorators works, but we should error in this case if possible to show you where to find them.
We currently support extracting GraphQL type definitions from TypeScript class definitions and GraphQL interface definitions from TypeScript interface definitions. But it is reasonable that someone might want to expose an interface in their TypeScript code as a GraphQL type.
/** GQLType */
interface MyType {
/** GQLField */
myField: string
}
Should generate:
type MyType {
myField: String
}
Extractor.extractType
to handle interface definitionsExtractor.typeClassDeclaration
into a new method named typeInterfaceDeclaration
, and make the necessary changes.fixtures/type_definition
and its contents and call it fixtures/type_definition_from_interface
or similar. Update all the tests to use interfaces.See CONTRIBUTING.md for project level guidance.
I'm seeing issues where regenerating the schema can lead to imports being written in different order despite no changes.
Notably, this could ensure links are clickable.
If a field arg is typed in the DSL as optional, a request that passes an explicit null, is considered valid. This means graphql-js
will pass through a user's explicit null. This, in turn, means that if Grats is going to generate an optional argument, the user's field resolver must be capable of accepting null
.
Today it's possible to define a function which is not expecting in its typescript types, but could get passed a null at runtime:
/** @gqlType */
export default class Query {
/** @gqlField */
someField(args: {greeting: string | undefined}): string {
if(args.greeting === undefined) {
return "Hello!"
}
return args.greeting; // OOPS! Might return `null` here, which is not a `string`.
}
}
type Query {
someField(greeting: String): String
}
I think the best solution here is for Grats to error in this case and force you to add | null
to the argument's type.
Overall we should move config validation to use a more methodical approach. Perhaps have some object representing the expected schema.
1ccab75 adds a change how we handle multiple files each with an error, but we have no test coverage for this, nor any ability to validate the behavior.
I read this: https://grats.capt.dev/docs/usage/cli
grats -V
doesn't show the version.grats --output=./schema.graphql
does not do anything.My TsConfig:
{
"compilerOptions": {
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
Currently Grats uses schema directives to encode metadata that it extracted from the code which it will is necessary for the correct execution of user's resolvers. For example, if you define a field on some type using a free function, Grats needs to tell the executor where that function lives.
/** @gqlField */
export function allUsers(_: Query): UserResolver[] {
return [new UserResolver(), new UserResolver()];
}
Extracts:
type Query {
allUsers: [User!]! @exported(filename: "../examples/express-graphql/dist/models/User.js", functionName: "allUsers")
# ...
}
Since by default graphql-js
looks for all fields as methods/properties on the parent object, the same issue occurs if you want to assign a field a name that differs from the property/method name and so Grats also has a @methodName
directive.
Today this is achieved by the extraction portion of Relay adding server directives to each field that has additional data like a method name that differs, or a specific free function that should be called. Grats then "applies" these directives by wrapping the default resolver function for each field before returning the GraphQLSchema
object.
This has a certain elegance because the extraction portion just derives a single, spec compliant SDL text, and starting up can be done by simply ingesting that single file.
However, the downside is that the schema becomes pretty noisy for human inspection.
I can see this tradoeff getting worse as/if we explore other features, like being able to accept positional arguments rather than requiring all args to be bundled into a single object.
Here are the options I see right now, but hopefully others exist as well:
To start with, ideally we don't need to re-extract from files that have not changed
See the diff in 7f2bada
src/tests/fixtures/field_values/StringFieldKillsParentOnExceptionWithoutNullableByDefaultEnables.invalid.ts.expected
changes the highlight, but it should be the same range converted from ts to GraphQL and then back to ts for reporting. One (or more?) of those transforms must be wrong.
As of 2023-10-30, when entering this code in the playground:
/** @gqlType */
class Query {
/**
* @gqlField
*/
async test(): Promise<string> {
return '';
}
}
We get an error about Promise
not being defined:
# ERROR MESSAGE
# =============
# index.ts:6:17 - error TS2705: An async function or method in ES5/ES3 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option.
#
# 6 async test(): Promise<string> {
# ~~~~~~~~~~~~~~~
#
I thought this was going to be an easy thing to fix, except, it looks like the playground's TS config already targets a recent environment (ES2021)?
createDefaultMapFromCDN(
{ target: ts.ScriptTarget.ES2021, lib: ["es2021"] },
ts.version,
shouldCache,
ts,
lzstring,
)
However, I also found this commented line:
const compilerOpts = {
allowJs: true,
baseUrl: "./",
paths: { grats: [GRATS_PATH] },
// lib: ["es2021"],
};
const host = createVirtualCompilerHost(system, compilerOpts, ts);
I'm curious what is the Chesterton's Fence reason for having lib: ["es2021"]
in one component but not in the other?
Here's a repo that runs repo and documents some warts and workarounds.
What I have found so far:
import { Float, Int, ID } from "grats"
) are importable just fine, but seems like the docblocks aren't reachable.
schema.ts
in this repo).graphql
version is pulled, otherwise graphql
itself will complain.GraphQL JS expects GraphQL field arguments to be passed to the resolver as a single args
object where each argument is a named property. This makes sense in a world where your executor does not know anything about the implementation of your resolvers, but in Grats I think we can offer a more ergonomic alternative: positional arguments.
So, instead of:
/** @gqlField */
export function greeting(_: Query, args: {name: string, greeting: string}): string {
return `${args.greeting}, ${args.name}`;
}
We could support the simpler:
/** @gqlField */
export function greeting(_: Query, name: string, greeting: string): string {
return `${greeting}, ${name}`;
}
This becomes especially nice when you want to have descriptions or @deprecated
on your args:
/** @gqlField */
export function greeting(
_: Query,
/** Name by which to greet the person */
name: string,
/** Salutation to use when greeting */
greeting: string,
/** @deprecated Prefer the `name` arg */
userName: string
): string {
return `${greeting}, ${name}`;
}
At extraction time Grats could annotate the fields with directives defining which field name maps to which position. That mapping could then be implemented using the same type of resolver wrapper we use for @methodName
.
One reason not to use positional arguments is that it interferes with your non-GraphQL arguments like context and the extra metadata argument. However, if Grats knew which type was your context type (for example via a @gqlContext
tag), it could just know which position you were expecting your context in, and pass it in this position using a similar mapping strategy:
/** @gqlContext */
type RequestContext = { username: string };
/** @gqlField */
export function greeting(
_: Query,
greeting: string,
ctx: GqlContext
): string {
return `${greeting}, ${ctx.username}`;
}
v.s the more verbose
/** @gqlField */
export function greeting(
_: Query,
args: {
/** Name by which to greet the person */
name: string,
/** Salutation to use when greeting */
greeting: string,
/** @deprecated Prefer the `name` arg */
userName: string
}
): string {
return `${args.greeting}, ${args.name}`;
}
Now even the context object does not need to be defined at a fixed postion. Maybe you prefer getting your context first?
/** @gqlContext */
type RequestContext = { username: string };
/** @gqlField */
export function greeting(
_: Query,
ctx: GqlContext,
greeting: string
): string {
return `${greeting}, ${ctx.username}`;
}
(On the other hand, maybe having a strong convention around context going after all GraphQL arguments is better for clarity in the code base. In which case that last option might be a bridge too far.)
Should be used in:
Will graphql-js
pass a number through to your resolver if you define the arg type as ID? I would expect it to coerce to string before passing to the user resolver.
import { Float as GqlFloat, Int as GqlInt } from "grats";
/** @gqlScalar */
type GqlDate = Date;
/**
* Object representing cooking recipe
* @gqlType
*/
export class Recipe {
/** @gqlField */
title!: string;
/**
* @gqlField
* @deprecated Use 'description' field instead
*/
get specification(): string | undefined {
return this.description;
}
/**
* The recipe description with preparation info
* @gqlField
*/
description?: string;
/** @gqlField */
ratings!: GqlInt[];
/** @gqlField */
creationDate!: GqlDate;
/** @gqlField */
ratingsCount!: GqlInt;
/** @gqlField */
get averageRating(): GqlFloat | null {
const ratingsCount = this.ratings.length;
if (ratingsCount === 0) {
return null;
}
const ratingsSum = this.ratings.reduce((a, b) => a + b, 0);
return ratingsSum / ratingsCount;
}
}
Presumably broken with the upgrade of Docusaurus. See the "Functional style fields" section of https://grats.capt.dev/docs/dockblock-tags/fields/
The third argument to a resolver is always the request context. This can be anything the user configures, so it's generally hard to typecheck effectively. However, Grats knows each function/method that is a GraphQL field (thanks to the @gqlField
annotations) so could be charged with ensuring all fields are typed in a way that matches the developer's expectations.
This would require some way for Grats users to inform users what type they are passing to their executor. I'll need to brainstorm what options make sense here:
GratsContextValue
?) that can be defined by the user in their code and Grats will look for/** @gqlContext */
on the context type declaration.We should also examine how existing TS GraphQL servers address this. I'm thinking specifically of TypeGraphQL and Pothos.
Not in the semver range, but we should provide a better experience here.
Given the following typescript definitions:
/**
* @gqlInterface Node
*/
export interface Node {
/** @gqlField */
id: string;
}
/** @gqlInterface Something */
interface SomethingElse {
/** @gqlField */
hello: string;
}
/** @gqlType */
export class User extends Node implements SomethingElse {
__typename: "User";
/** @gqlField */
id: string;
/** @gqlField */
hello: string;
/** @gqlField */
username: string;
/** @gqlField */
email: string;
}
We get the generated type
type User implements SomethingElse {
email: String
hello: String
id: String
username: String
}
But if we switch User
to implement both Node
and SomethingElse
rather than extending one, we get both correctly added to the generated schema
type User implements Node & SomethingElse {
email: String
hello: String
id: String
username: String
}
Given that both extends
and implements
are supported for interfaces, I would expect both to work when applied to the same class.
From Discord: if helpful you can actually use the entire thing in the tamagui repo if you're willing to wait for yarn install and ignore all the tamagui stuff, just:
git clone [email protected]:tamagui/tamagui.git
yarn
yarn build
yarn vertql
and you can see it run / genreate schema etc.
Is there a recommended way? I can't find anything in the docs.
I tried making a Root class and used that as the root resolver.
The Root class contains a Mutation and a Query class.
Except this doesn't work.
The example express app you have only uses Query.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.