-
Notifications
You must be signed in to change notification settings - Fork 324
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
More typesafety for GraphQL code #6167
Conversation
codegen.json
Outdated
"config": { | ||
"mapperTypeSuffix": "Model", | ||
"mappers": { | ||
"PingPayload": "./ping#PingPayload" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
each model must be defined here, which is a bit annoying, but makes sense since it needs to be included in the built types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is a bit annoying, but currently we mostly use any
for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would LOVE to remove this sharp edge.
the problem I face is that sometimes a Source
comes from a database entity. Sometimes, it's just the return value from a mutation or query. I still haven't come up with a good enough rule to automate it. Once we do, we can write a script that automates this.
|
||
const schema = mergeSchemas({schemas: [legacySchema], typeDefs, resolvers: resolverMap}) | ||
|
||
const updateSchema = async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's probably a better way to create the schema SDL...
const legacySchema = new GraphQLSchema({query, mutation, types: rootTypes}) | ||
|
||
const typeDefs = loadFilesSync( | ||
path.join(__PROJECT_ROOT__, 'packages/server/graphql/intranetSchema/typeDefs/*.graphql') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i really like this part. no more including files 1 by 1. just include the whole directory
return res ? duration : -1 | ||
} | ||
|
||
const resolverMap: PingSuccessResolvers = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these aren't the best examples because they're so simple, but if you try to return something that doesn't resolve to a number it'll throw an error, which is great!
@@ -0,0 +1,7 @@ | |||
import {QueryResolvers} from '../resolvers/types' | |||
|
|||
export interface PingPayload {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not the greatest example, but say we wanted to just return {taskId}
, we could define that here
@@ -0,0 +1,10 @@ | |||
union PingPayload = ErrorPayload | PingSuccess |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is writing a file like this easier than the old way?
return collector | ||
} | ||
|
||
const resolverMap: Resolvers = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blimmer this is my attempt to emulate what you were doing with that codegen script
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my current thinking is to have 3 folders inside /graphql
:
- public
- public/queries
- public/mutations
- public/subscriptions
- public/types
- private
- private/queries
- private/mutations
- private/types
- github
- queries
- mutations
- gitlab
- queries
- mutations
the above won't touch the current hierarchy so we can gradually move over to this new format.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm submitting these initial ideas for now, going to shift back over to #6183 to complete that work, then come back over here to make additional progress.
@@ -12,6 +12,17 @@ | |||
"schema": "packages/server/graphql/nestedSchema/GitLab/gitlabSchema.graphql", | |||
"documents": "packages/server/graphql/nestedSchema/GitLab/queries/*.graphql", | |||
"plugins": ["typescript", "typescript-operations", "add"] | |||
}, | |||
"packages/server/graphql/intranetSchema/sdl/resolverTypes.ts": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: how do we get rid of this SDL folder?
@@ -0,0 +1,5186 @@ | |||
type Query { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 are we planning to split the schema up? Having everything in one giant file makes editing and reviewing harder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! Every new piece of business logic will have its own .graphql
typeDef. We'll use a tool that merges all those typeDefs together into a generated schema that we won't have to look at or touch.
I finished the refactor & left all the legacy stuff in a big _legacy.graphql
for the sake of time.
https://github.com/ParabolInc/parabol/pull/6228/files#diff-77adac95bcf72265d1a92f5b3932ccf2b8a6055e41e1baf2337fd7c5ee8fc830
@@ -0,0 +1,48 @@ | |||
import getRethink from '../../../../database/rethinkDriver' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 I find the folder structure a bit confusing. Why do we put 'sdl' in there? Schema definition language is what will end up in 'typeDefs' folder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed!
What do you think of this hierarchy?
inside /graphql
:
- public
- public/queries
- public/mutations
- public/subscriptions
- public/types
- private
- private/queries
- private/mutations
- private/types
- github
- queries
- mutations
- gitlab
- queries
- mutations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -0,0 +1,386 @@ | |||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | |||
// @ts-nocheck | |||
// Type definitions for webpack (module API) 1.16 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 Wouldn't you put type definitions in d.ts
files? How does this differ from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/webpack-env/index.d.ts ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i couldn't figure out how to import this without using the /// references
pattern, which is deprecated. It also had some incorrect types, so i figure I'd just copy it over & remove the dep
scripts/dev.js
Outdated
@@ -45,7 +45,10 @@ const dev = async (maybeInit) => { | |||
await Promise.all([removeArtifacts()]) | |||
} | |||
// Enable this if you're creating new github schemas |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 Comment is not up to date anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
scripts/dev.js
Outdated
// await generate(codegenSchema) | ||
try { | ||
await generate(codegenSchema) | ||
} catch {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 Do we really just want to swallow all errors? If so, there should be a comment why
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
codegen.json
Outdated
"config": { | ||
"mapperTypeSuffix": "Model", | ||
"mappers": { | ||
"PingPayload": "./ping#PingPayload" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is a bit annoying, but currently we mostly use any
for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall. The source mapping in codegen.json is a bit annoying and having definitions spread across more files, but overall it's probably the way to go.
I wish there was something like typebox for GraphQL instead, but the GraphQL types don't contain enough information and with codegen we're on a more mainstream path.
|
||
const logins: QueryResolvers['logins'] = async (_source, args) => args | ||
|
||
export default logins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 You should delete graphql/intranetSchema/queries/logins.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i saved most deletions for the subsequent PR. https://github.com/ParabolInc/parabol/pull/6228/files#diff-0281cf5ab4a0b4d7573df4f26731ffcd6bf36bdbc723eb82d1f0d83dd667e4d5
"plugins": ["typescript", "typescript-resolvers", "add"], | ||
"config": { | ||
"contextType": "../../graphql#InternalContext", | ||
"mappers": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 We might want to set "defaultMapper": void
or similar to avoid the case where people don't define it.
We could even define export type No_model_added_in_codegen_json = {}
and set it as default mapper, just so developers are pointed to it more directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i like the idea of forcing an explicit mapper!
i'll play around with the subsequent PR to test void
and never
and see which one works best.
return res ? duration : -1 | ||
} | ||
|
||
export type PingableServicesSource = Record<string, never> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 I think if there is no input, it should be undefined
or void
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we return undefined then the graphql traversal won't continue, it only continues on objects.
To test: remove return {}
from ping.ts.
looks like master fails the build now, but not because of this. all issues that weren't fixed here (folder hierarchy, explicit Source types) will be fixed or trialed in the following PR: #6228 |
Fix #4754
Today, we don't get a lot of type safety when we create objects with graphql
source
{taskId}
) isn't typed at allSo, i took a shot at what all the cool kids do these days.
{taskId}
) is called a "Model", which can be thought of like a database object vs. an object returned from graphql. that separation is nice. I'm calling them Sources in this PR so the name matches what GraphQL calls them.Reading material:
Would love to get some feedback!