Closed konhi closed 6 months ago
if you are using resolver chains then you will need nested partials using utility-types
https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-resolvers#allow-deep-partial-with-utility-types
const config: CodegenConfig = {
schema: "**/schema.graphql",
generates: {
"src/schema": defineConfig({
add: {
"./types.generated.ts": {
content: "import type { DeepPartial } from 'utility-types';",
},
},
typesPluginsConfig: {
defaultMapper: "DeepPartial<{T}>",
},
}),
},
};
Hello 👋 Thanks for creating this issue. I think there's definitely something we can work out to make the usage clearer. 🙂
For context, this plugin is created to have first-class support the mappers way enabling resolver chaining for the following benefits:
1. Predictability and runtime safety when using a mapper - or not
By default, codegen matches resolver's TS types with GraphQL field output type. This eliminates the chance where someone forgets to return null into a non-nullable GraphQL field - which is possible with Partial
or DeepPartial
- and cause a runtime error. Furthermore:
mappers
, we explicitly tell codegen which value we don't want to return in a resolver, shifting the responsibilities to the following resolvers.2. Consistent baseline object when multiple resolvers return the same type
Consider the following schema:
type Cat {
owner: Person!
}
type Dog {
owner: Person!
}
type Person {
id: ID!
name: String!
}
With Partial
or DeepPartial
, it's easy to accidentally return the inconsistent Person
object in Cat.owner
and Dog.owner
resolvers, making it much easier to hit runtime errors, especially in bigger apps e.g.
const resolvers: Resolvers = {
Cat: {
owner: () => ({ id: "100" }) // 👈 object with only `id` can be return here, because `Person` is partial. This will result in runtime error.
}
Dog: {
owner: () => ({ name: "Bart" }) // 👈 object with only `name` can be return here, because `Person` is partial. This will result in runtime error.
}
}
On the other hand, Person
resolvers wouldn't know what's being passed in, making it hard to code and may cause runtime errors:
const resolvers: Resolvers = {
// Same Cat/Dog resolvers as before
Person: {
id: ({ id }) => id // 👈 This will trigger type error because `id` is nullable from using `Partial`
// 👈 No `name` resolver... which means it'd error if clients query for `Cat.owner.name` because `Person.name` is non-nullable in the Graph, but we don't handle the nullable case
}
}
For these reasons, Partial
and DeepPartial
mappers is supported via your mentioned config options, but not by default for this plugin. I'll add some notes to the guides to clarify the reasoning 🙂
For context, here's the file convention of using mappers
when using this plugin: https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset#adding-mappers
@eddeee888 sorry how do you do resolver chaining without DeepPartial? Do you have a code example somewhere? Without DeepPartial you get a typescript error that a field is missing even if you resolve it somewhere else
Hi @arjunyel , sure!
I've set up a small GraphQL server with some mocked data here: https://github.com/eddeee888/graphql-server-template
Let's look at the related part of the schema extracted from that repo:
# book/schema.graphql
type Book {
id: ID!
isbn: String!
}
# user/schema.graphql
extend type Query {
user(id: ID!): User
}
type User {
id: ID!
fullName: String!
booksRead: [Book!]!
}
In Query.user
resolver, we don't want to implement with booksRead
as we want to let User.booksRead
handle that logic.
Here's how we can do with this preset:
// user/schema.mappers.ts
// Preset detects a mapper by the type name `User` (GraphQL type) + `Mapper` (The customisable suffix)
export type UserMapper = {
id: string;
firstName: string;
lastName: string;
};
What this means is we can now pass this mapper object into any resolver that is supposed to return a GraphQL User
node. Our Query.user may look something like this:
// user/resolvers/Query/user.ts
export const user: NonNullable<QueryResolvers["user"]> = () => {
// We must now return a consistent `UserMapper` into GraphQL `User` node
// Without the UserMapper, we must return `{ id: string, fullName: string, booksRead: Book[] }`
return {
id: "1",
firstName: "Bart",
lastName: "Simpson"
}
};
And we can resolve User.booksRead like this:
// user/resolvers/User.ts
export const User: UserResolvers = {
booksRead: async (parent, _ , { booksAPI }) => { // `parent` is `UserMapper`
// 👇 Server preset would generate `User.booksRead` and tell us _why_ this is generated
/* User.booksRead resolver is requied because User.booksRead exists but UserMapper.booksRead does not */
/* Do booksRead resolver logic here e.g. */
const booksRead = await booksAPI.getBooksReadByUserId({ userId: parent.id });
return booksRead;
},
fullName: (parent) => {
//👇 Server preset would generate `User.fullName` resolver and tell us _why_ we this is generated
/* User.fullName resolver is required because User.fullName exists but UserMapper.fullName does not */
return `${parent.firstName} ${parent.lastName}`;
},
};
Mapper is a concept available in the codegen typescript-resolvers
plugin.
The server preset adds auto resolver-level generation to eliminate runtime error, which is one of the benefits over just using the base typescript-resolvers
plugin
You can find the full example in the mentioned repo with these main files:
Let me know if this doesn't answer your question @arjunyel ! Happy to chat more. If we are happy with this discussion, I'll close this issue in 3 days and update the docs
@eddeee888 makes total sense, you are the biggest legend in history!
Is your feature request related to a problem? Please describe. The way this plugin generates codes naturally makes users do resolvers chaining. This is a standard GraphQL practice documented by Apollo Docs > Server > Resolvers > Resolver Chains.
When using this plugin for the first time, it was unexpected to me that this wasn't supported by default. I spent few hours trying to look for a solution.
Describe the solution you'd like Enable support for resolver chaining by making this config default. We can also just update readme of this plugin and this guide: https://the-guild.dev/graphql/codegen/docs/guides/graphql-server-apollo-yoga-with-server-preset
Additional context