Skip to content

Commit

Permalink
feat(codemod): Add codemod to make relation resolvers partial (#6342)
Browse files Browse the repository at this point in the history
Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
  • Loading branch information
2 people authored and jtoar committed Sep 8, 2022
1 parent e8538be commit d521ead
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 2 deletions.
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'

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

0 comments on commit d521ead

Please sign in to comment.