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(codemod): Add codemod to make relation resolvers partial #6342

Merged
merged 7 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Update Resolver Types

- This codemod only affects TS projects.

It will find all service files, and if they have a relation resolver - it will convert the type to a partial.

Taking a specific case, in the test project we have Post.author, which is a relation (author is User on the DB).

```diff
// At the bottom of the file
- export const Post: PostResolvers = {
+ export const Post: Partial<PostResolvers> = {
author: (_obj, gqlArgs) =>
db.post.findUnique({ where: { id: gqlArgs?.root?.id } }).author(),
}
```

This is because of the `avoidOptionals` flag in graphql codegen. Look for this option in `packages/internal/src/generate/graphqlCodeGen.ts`


> Note:
> Very old RW projects don't even have these types in the services. This was introduced in v2.x, when we enabled Prisma model mapping in codegen.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {
QueryResolvers,
MutationResolvers,
PostResolvers,
} from 'types/graphql'

import { db } from 'src/lib/db'

export const posts: QueryResolvers['posts'] = () => {
return db.post.findMany()
}

export const post: QueryResolvers['post'] = ({ id }) => {
return db.post.findUnique({
where: { id },
})
}

export const createPost: MutationResolvers['createPost'] = ({ input }) => {
return db.post.create({
data: input,
})
}

export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
return db.post.update({
data: input,
where: { id },
})
}

export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
return db.post.delete({
where: { id },
})
}

export const Post: PostResolvers = {
author: (_obj, gqlArgs) =>
db.post.findUnique({ where: { id: gqlArgs?.root?.id } }).author() as Author,
}

// Leave these alone
interface Bazinga {
bazinga: string
}

export const CustomExport: Bazinga = {
bazinga: 'yes'
}

export const CustomExport2: Partial<Bazinga> = {}

const HelloWorld: BazingaResolvers['HelloWorld'] = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type {
QueryResolvers,
MutationResolvers,
PostResolvers,
} from 'types/graphql'

import { db } from 'src/lib/db'

export const posts: QueryResolvers['posts'] = () => {
return db.post.findMany()
}

export const post: QueryResolvers['post'] = ({ id }) => {
return db.post.findUnique({
where: { id },
})
}

export const createPost: MutationResolvers['createPost'] = ({ input }) => {
return db.post.create({
data: input,
})
}

export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => {
return db.post.update({
data: input,
where: { id },
})
}

export const deletePost: MutationResolvers['deletePost'] = ({ id }) => {
return db.post.delete({
where: { id },
})
}

export const Post: Partial<PostResolvers> = {
author: (_obj, gqlArgs) =>
db.post.findUnique({ where: { id: gqlArgs?.root?.id } }).author() as Author,
}


// Leave these alone
interface Bazinga {
bazinga: string
}

export const CustomExport: Bazinga = {
bazinga: 'yes'
}

export const CustomExport2: Partial<Bazinga> = {}

const HelloWorld: BazingaResolvers['HelloWorld'] = {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('updateResolverTypes', () => {
it('Converts PostResolvers to Partial<PostResolvers>', async () => {
await matchTransformSnapshot('updateResolverTypes', 'default')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { API, FileInfo, TSTypeAnnotation } from 'jscodeshift'
import { Identifier, TSTypeReference } from 'jscodeshift'

const isTypeReference = (
typeAnnotation: TSTypeAnnotation['typeAnnotation']
): typeAnnotation is TSTypeReference => TSTypeReference.check(typeAnnotation)

const getTypeName = (node: TSTypeReference) => {
return Identifier.check(node.typeName) ? node.typeName.name : null
}

const isWrappedInPartial = (node: TSTypeAnnotation) => {
const typeAnnotation = node.typeAnnotation

return (
isTypeReference(typeAnnotation) && getTypeName(typeAnnotation) === 'Partial'
)
}

export default function transform(file: FileInfo, api: API) {
const j = api.jscodeshift
const ast = j(file.source)

ast.find(j.TSTypeAnnotation).forEach((path) => {
const typeAnnotationNode = path.node

if (
// If it's a MutationResolvers['x'] or QueryResolvers['x']
j.TSIndexedAccessType.check(typeAnnotationNode.typeAnnotation)
) {
return
}

if (
!isWrappedInPartial(typeAnnotationNode) &&
isTypeReference(typeAnnotationNode.typeAnnotation)
) {
const originalTypeName = getTypeName(typeAnnotationNode.typeAnnotation)

if (!originalTypeName || !originalTypeName.includes('Resolvers')) {
// Skip other type annotations!
return
}

console.log(`Wrapping ${originalTypeName} in Partial....`)

path.replace(
j.tsTypeAnnotation(
j.tsTypeReference(
j.identifier('Partial'),
j.tsTypeParameterInstantiation([
j.tsTypeReference(j.identifier(originalTypeName)),
])
)
)
)
}
})

return ast.toSource()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import path from 'path'

import fg from 'fast-glob'
import task, { TaskInnerAPI } from 'tasuku'

import getRWPaths from '../../../lib/getRWPaths'
import runTransform from '../../../lib/runTransform'

export const command = 'update-resolver-types'
export const description =
'(v2.x.x->v3.x.x) Wraps types for "relation" resolvers in the bottom of service files'

export const handler = () => {
task('Update Resolver Types', async ({ setOutput }: TaskInnerAPI) => {
await runTransform({
transformPath: path.join(__dirname, 'updateResolverTypes.js'),
// Target services written in TS only
targetPaths: fg.sync('**/*.ts', {
cwd: getRWPaths().api.services,
ignore: ['**/node_modules/**', '**/*.test.ts', '**/*.scenarios.ts'],
absolute: true,
}),
})

setOutput('All done! Run `yarn rw lint --fix` to prettify your code')
})
}
3 changes: 3 additions & 0 deletions packages/codemods/src/testUtils/matchTransformSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const matchTransformSnapshot = async (
transformPath,
targetPaths: [tempFilePath],
parser,
options: {
verbose: true,
},
})

// Step 3: Read modified file and snapshot
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path'

import task from 'tasuku'
import task, { TaskInnerAPI } from 'tasuku'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think tasuku changed their types


import getRWPaths from '../../../lib/getRWPaths'
import runTransform from '../../../lib/runTransform'
Expand All @@ -11,7 +11,7 @@ export const description = '(${version}->${version}) Converts world to bazinga'
export const handler = () => {
task(
'${titleName}',
async ({ setOutput }: task.TaskInnerApi) => {
async ({ setOutput }: TaskInnerApi) => {
await runTransform({
transformPath: path.join(__dirname, '${name}.js'),
// Here we know exactly which file we need to transform, but often times you won't.
Expand Down