Skip to content
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

feat(ts): Improves return types of QueryResolvers, MutationResolvers and <Model>Resolvers #6228

Merged
merged 34 commits into from
Aug 30, 2022

Conversation

dac09
Copy link
Collaborator

@dac09 dac09 commented Aug 18, 2022

This PR is a combination of fixes (for bugs I introduced during the strict-mode PR) and new features

What does this do?

In v2, we started mapping Prisma types (#5216) to QueryResolvers and MutationResolvers. This was a great improvement, but it required that all services return the full Prisma model and had a few other downsides. This PR addresses these:

A. Introduces a new utility type MergePrismaWithSdlTypes<PrismaUser, User> which is used in codegen to do the mapping. Which achieves the following cases:

Key Insight:
SDL is source of truth for KEYS
Prisma types is source of truth for VALUES (unless SDL-only field)

1. Return a subset of the fields 🆕
Let's say I modify my SDL to only return a subset of the fields in Post. In this case, I never want to return the createdAt property from GraphQl

export const schema = gql`
  type Post {
    id: Int!
    title: String!
    body: String!
-   createdAt: DateTime
  }

The Resolver types used to error out if I do not return createdAt in the resolver.

What's happening here is that SDL type gets updated, but the Prisma type doesn't (obviously, because we are mapping it, and the field still exists in the DB). The new helper will "pick" all the fields that are common in both the Prisma and the SDL Post type.

2. Return extra fields 🆕
In the opposite case, I'd like to return extra properties from my GraphQL API, that does not exist in the database/Prisma model. e.g.

export const schema = gql`
  type Post {
    id: Int!
    title: String!
    body: String!
+  sdlOnlyProperty: String!
  }

The generated types now take this into account.

3. Making certain fields optional in SDL, but still keeping them requried in Prisma schema 🆕

 type Post {
    id: Int!
    title: String!
-  body: String!
+   body: String # 👈 make body optional, even though prisma schema has body required

This will be handled as well, and your resolvers will not be required to return a body.

B Relation types 🆕
Before this PR, we didn't provide types for relations.

type Post {
	author: User # 👈 author could be anything, we didn't type it
    authorId: String # this was OK, because it came from Prisma

What happens now, in this example, we provide the types for Post.author, but we make this property optional, because its a relation. Redwood generates relation resolvers at the bottom of a service file

Screenshot 2022-08-22 at 15 37 52

Making it optional solves the "infinite" nesting problem with graphql types i.e. Posts.author.posts.author.posts.... - but you still get type safety if you want to return the relation directly from your service, and not from the relation resolvers

B. Introduces removeNulls utility function 🆕
See relevant sections in the doc:

This is covered in detail in Prisma's docs, which we strongly recommend reading.
But the gist of it is that, for Prisma's create and update operations, you have to make sure nulls are converted to undefined from your GraphQL mutation inputs - unless the field is nullable in your Prisma schema.

This is because a null in graphql is not equivalent to null in Prisma.

@dac09 dac09 added the release:feature This PR introduces a new feature label Aug 18, 2022
@nx-cloud
Copy link

nx-cloud bot commented Aug 18, 2022

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 5d59f5f. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this branch


✅ Successfully ran 14 targets

Sent with 💌 from NxCloud.

@netlify
Copy link

netlify bot commented Aug 18, 2022

Deploy Preview for redwoodjs-docs ready!

Name Link
🔨 Latest commit 5d59f5f
🔍 Latest deploy log https://app.netlify.com/sites/redwoodjs-docs/deploys/63076283964e330009e1586d
😎 Deploy Preview https://deploy-preview-6228--redwoodjs-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@dac09 dac09 force-pushed the fix/merge-mapped-types-codegen branch from 1c74d4b to 9c9593f Compare August 18, 2022 11:59
@dac09 dac09 changed the title fix(ts): Improves return types of QueryResolvers, MutationResolvers and <Model>Resolvers feat(ts): Improves return types of QueryResolvers, MutationResolvers and <Model>Resolvers Aug 22, 2022
@dac09 dac09 force-pushed the fix/merge-mapped-types-codegen branch from 3a6b563 to 878f942 Compare August 22, 2022 15:10
@dac09 dac09 marked this pull request as ready for review August 22, 2022 15:18
) => TResult extends PromiseLike<infer TResultAwaited>
? Promise<Partial<TResultAwaited>>
: Promise<Partial<TResult>> | Partial<TResult>;`
) => TResult | Promise<TResult>`
Copy link
Collaborator Author

@dac09 dac09 Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return Resolver's ReturnType to the default. I introduced this in strict-mode pr and have subsquently realised this is a terrible idea.

@@ -159,11 +185,11 @@ function getPluginConfig() {
scalars: {
// We need these, otherwise these scalars are mapped to any
BigInt: 'number',
DateTime: 'string',
Date: 'string',
DateTime: 'Date | string', // @Note: because DateTime fields can be valid Date-strings too
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important change.

Redwood has built-in handling for Dates (because we https://www.graphql-scalars.dev/docs/scalars/date-time configured) - but our types were only allowing string. This was a misconfiguration, but not super visible, because we were mapping Prisma types anyway.

For DateTime fields you can send back Date but also a valid date-string. Note that when you set DateTime in your SDL, even if you format it yourself, it will convert it back to the RFC3339.

Note also that the code here only affects types we generate, I'm just highlighting what the behaviour actually is and updating the types to match it!

@@ -22,15 +22,30 @@ import { getTsConfigs } from '../project'

export const generateTypeDefGraphQLApi = async () => {
const filename = path.join(getPaths().api.types, 'graphql.d.ts')
const prismaModels = getPrismaModels()
const prismaImports = Object.keys(prismaModels).map((key) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved importing Prisma types here, because we now use a "custom mapper" generic MergePrismaWithSdlTypes<> in the mapper object.

@dac09 dac09 force-pushed the fix/merge-mapped-types-codegen branch from 72d4aa3 to f787261 Compare August 22, 2022 17:42
@@ -239,7 +239,7 @@ type Params = {
* // key being used in dbAccessor in src/functions/auth.ts 👇
* const getCurrentUser = async (session: DbAuthSession<User['id']>)
*/
export interface DbAuthSession<TIdType = unknown> {
export interface DbAuthSession<TIdType = any> {
Copy link
Collaborator Author

@dac09 dac09 Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tobbe had to set this to any again, because it complains about currentUser function being passed into the graphql handler in strict mode otherwise.

Copy link
Member

@Tobbe Tobbe Aug 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next time I see this I'll get an itch to try to switch it to unknown again... How did you notice it was complaining? If I can check on my own next time it doesn't have to bounce in a PR :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a tsc error in the projects graphql function.

It happens because findUnique (when trying to find the user) can return null I think

where: { id },
})
}
```

### Relation resolvers in services
Copy link
Collaborator Author

@dac09 dac09 Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting a little messy here with this.

This part of the doc is half ts strict mode - and half "dealing with quirks of gql+prisma"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not just this part, also the part above about removeNulls

@@ -172,7 +172,7 @@ storybookTest(
await page.goto(STORYBOOK_URL)

// Click Redwood link in left nav
await page.locator('css=[data-item-id=redwood--page]').click()
await page.locator('id=redwood--page').click()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why I had to change this now 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm getting the same error in #6226, but it doesn't happen to any renovate PRs. Weird

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I take that back, was failing because I forgot to update a string here: 82ec512

Copy link
Collaborator Author

@dac09 dac09 Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it works in CI, but fails locally without this change.

@dac09 dac09 mentioned this pull request Aug 23, 2022
18 tasks
where: { id },
})
}
```

### Relation resolvers in services
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not just this part, also the part above about removeNulls

docs/docs/typescript/strict-mode.md Outdated Show resolved Hide resolved
docs/docs/typescript/strict-mode.md Outdated Show resolved Hide resolved
docs/docs/typescript/strict-mode.md Outdated Show resolved Hide resolved
* @param obj - Object to remove nulls from
* See {@link https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined Prisma docs: null vs undefined}
*/
export const removeNulls = (obj: Record<number | symbol | string, any>) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dnull is recursive. Does this one have to be too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh darn, maybe it should.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated it and added a test, but I need to think more about the use cases for this a little. Nothing is ever easy.....

@@ -22,11 +24,11 @@ export type Scalars = {
Int: number;
Float: number;
BigInt: number;
Date: string;
DateTime: string;
Date: Date | string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious to see what you decided to do here 🙂 Why did you have to keep the possibility of this being a string?

Copy link
Collaborator Author

@dac09 dac09 Aug 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained it in a comment, but it got "outdated"

Important change.
Redwood has built-in handling for Dates (because we use graphql-scalars.dev/docs/scalars/date-time configured) - but our types were only allowing string. This was a misconfiguration, but not super visible, because we were mapping Prisma types anyway.
For DateTime fields you can send back Date but also a valid date-string. Note that when you set DateTime in your SDL, even if you format it yourself, it will convert it back to the RFC3339.
Note also that the code here only affects types we generate, I'm just highlighting what the behaviour actually is and updating the types to match it!

packages/internal/src/generate/graphqlCodeGen.ts Outdated Show resolved Hide resolved
packages/internal/src/generate/graphqlCodeGen.ts Outdated Show resolved Hide resolved
packages/internal/src/generate/graphqlCodeGen.ts Outdated Show resolved Hide resolved
packages/internal/src/generate/graphqlCodeGen.ts Outdated Show resolved Hide resolved
Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
@jtoar
Copy link
Contributor

jtoar commented Aug 26, 2022

@dac09 tried to help you out by handling the merge conflict in a snapshot file due to the jest 29 upgrade; let me know if t looks like I screwed something up and can revert.

@@ -35,3 +35,26 @@ export function normalizeRequest(event: APIGatewayProxyEvent): Request {
body,
}
}

// Internal note: Equivalent to dnull package on npm, which seems to have import issues in latest versions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a build shipped for this: brielov/dnull#65

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Orta! We might switch to dnull internally and keep the removeNulls export

@dac09 dac09 enabled auto-merge (squash) August 30, 2022 11:03
@dac09 dac09 merged commit 259fd77 into redwoodjs:main Aug 30, 2022
@redwoodjs-bot redwoodjs-bot bot added this to the next-release milestone Aug 30, 2022
@dac09 dac09 deleted the fix/merge-mapped-types-codegen branch August 30, 2022 11:34
@jtoar jtoar modified the milestones: next-release, v3.0.0 Sep 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:feature This PR introduces a new feature
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

4 participants