Skip to content

Commit

Permalink
fix: resolver generator
Browse files Browse the repository at this point in the history
  • Loading branch information
rintoj committed Jun 6, 2024
1 parent adb59d8 commit 1846e2f
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 57 deletions.
85 changes: 72 additions & 13 deletions src/gql/gql-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ export function getCommentFromDecorator(node: ts.Node, name: string) {
}
}

export function conditional<T>(
value: boolean | undefined,
ifTrue: T | ((...args: any[]) => T),
ifFalse: T | ((...args: any[]) => T),
) {
if (value === true) return typeof ifTrue === 'function' ? (ifTrue as any)() : ifTrue
return typeof ifFalse === 'function' ? (ifFalse as any)() : ifFalse
}

export function hasImplementationByName(node: ts.ClassDeclaration, name: string) {
return !!getImplementationByName(node, name)
}

export function getImplementationByName(node: ts.ClassDeclaration, name: string) {
const heritageClause = node.heritageClauses?.find(
clause =>
!!clause.types.find(
type =>
ts.isExpressionWithTypeArguments(type) &&
ts.isIdentifier(type.expression) &&
type.expression.text === name,
),
)
return heritageClause?.types.find(
type =>
ts.isExpressionWithTypeArguments(type) &&
ts.isIdentifier(type.expression) &&
type.expression.text === name,
)
}

export function getTypeFromDecorator(node: ts.Node, name: string) {
const decorator = getDecorator(node, name)
if (!decorator || !ts.isDecorator(decorator)) return
Expand Down Expand Up @@ -87,8 +118,10 @@ export function addDecorator<
| ts.ClassDeclaration
| ts.PropertyDeclaration
| ts.MethodDeclaration
| ts.ParameterDeclaration,
| ts.ParameterDeclaration
| undefined,
>(node: T, ...decorators: ts.Decorator[]): T {
if (!node) return node
const names = decorators.map((d: any) => d.expression.expression.text)
return {
...node,
Expand Down Expand Up @@ -117,7 +150,10 @@ export function convertToMethod(node: ts.PropertyDeclaration) {
)
}

export function addExport(node: ts.ClassDeclaration | ts.PropertyDeclaration) {
export function addExport<T extends ts.ClassDeclaration | ts.PropertyDeclaration | undefined>(
node: T,
): T {
if (!node) return node
return {
...node,
modifiers: [...(node.modifiers ?? []), factory.createToken(ts.SyntaxKind.ExportKeyword)],
Expand Down Expand Up @@ -226,19 +262,25 @@ export function createContextDecorator() {
)
}

export function createResolverDecorator(type: string, context: Context) {
export function createResolverDecorator(type: string, addType: boolean, context: Context) {
context.imports.push(createImport('@nestjs/graphql', 'Resolver'))
return factory.createDecorator(
factory.createCallExpression(factory.createIdentifier('Resolver'), undefined, [
factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
factory.createIdentifier(type),
),
]),
factory.createCallExpression(
factory.createIdentifier('Resolver'),
undefined,
addType
? [
factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
factory.createIdentifier(type),
),
]
: undefined,
),
)
}

Expand Down Expand Up @@ -268,6 +310,20 @@ export function isNullable(
return !!node.questionToken
}
if (ts.isMethodDeclaration(node)) {
if (
node.type &&
ts.isTypeReferenceNode(node.type) &&
ts.isIdentifier(node.type?.typeName) &&
node.type?.typeName?.text === 'Promise' &&
node.type.typeArguments?.[0]
) {
return (
ts.isUnionTypeNode(node.type.typeArguments[0]) &&
!!node.type.typeArguments[0].types.find(
item => ts.isLiteralTypeNode(item) && item.literal.kind === ts.SyntaxKind.NullKeyword,
)
)
}
if (node.type) {
return (
ts.isUnionTypeNode(node.type) &&
Expand All @@ -283,6 +339,9 @@ export function isNullable(

export function toType(node: ts.TypeNode | undefined): string | undefined {
if (node && ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
if (node?.typeName?.text === 'Promise' && node?.typeArguments?.[0]) {
return toType(node?.typeArguments?.[0])
}
return node?.typeName?.text
} else if (
node &&
Expand Down
78 changes: 63 additions & 15 deletions src/gql/resolver/resolver-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,48 @@ describe('generateResolver', () => {
const output = await generate(
'user.resolver.ts',
`
class UserResolver { }
`,
)
expect(toParsedOutput(output)).toBe(
toParsedOutput(`
import { Resolver } from '@nestjs/graphql'
@Resolver()
export class UserResolver {}
`),
)
})

test('should use the correct model name', async () => {
const output = await generate(
'user.resolver.ts',
`
@Resolver(() => UserModel)
class UserResolver {
createdAt?: (parent: UserAPIType) => string
createdAt(context): Date | null {
}
}
`,
)
expect(toParsedOutput(output)).toBe(
toParsedOutput(`
import { Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { Context, Query, Resolver } from '@nestjs/graphql'
@Resolver(() => User)
export class UserResolver implements FieldResolver<User, UserAPIType> {
@ResolveField({ nullable: true })
@Resolver(() => UserModel)
export class UserResolver {
@Query(() => Date, { nullable: true })
createdAt(
@Parent()
parent: UserAPIType,
): string {}
@Context()
context: GQLContext,
): Date | null {}
}
`),
)
})

test('should use the correct model name', async () => {
test('should use the correct typename name', async () => {
const output = await generate(
'user.resolver.ts',
`
Expand Down Expand Up @@ -172,12 +192,12 @@ describe('generateResolver', () => {
export class UserResolver implements FieldResolver<UserModel, UserAPIType> {
@Query(() => User)
user(
@Parent()
parent: UserAPIType,
@Args('id')
id: string,
@Parent()
parent: UserAPIType,
@Context()
context: GQLContext,
): User {}
Expand Down Expand Up @@ -237,14 +257,14 @@ describe('generateResolver', () => {
export class UserResolver implements FieldResolver<UserModel, UserAPIType> {
@Mutation(() => User)
user(
@Parent()
parent: UserAPIType,
@Context()
context: GQLContext,
@Args('id')
id: string,
@Parent()
parent: UserAPIType,
): User {}
}
`),
Expand Down Expand Up @@ -278,4 +298,32 @@ describe('generateResolver', () => {
`),
)
})

test('should create query with Promise', async () => {
const output = await generate(
'user.resolver.ts',
`
class UserResolver implements FieldResolver<UserModel, UserAPIType> {
@Query()
user(id?: string): Promise<User | null> {
}
}
`,
)
expect(toParsedOutput(output)).toBe(
toParsedOutput(`
import { Args, Query, Resolver } from '@nestjs/graphql'
@Resolver(() => UserModel)
export class UserResolver implements FieldResolver<UserModel, UserAPIType> {
@Query(() => User, { nullable: true })
user(
@Args('id', { nullable: true })
id?: string,
): Promise<User | null> {}
}
`),
)
})
})
66 changes: 37 additions & 29 deletions src/gql/resolver/resolver-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
getParameterType,
getTypeFromDecorator,
hasDecorator,
hasImplementationByName,
hasParameter,
conditional,
organizeImports,
removeNullability,
transformName,
Expand All @@ -32,31 +34,30 @@ function processParameters(
parentType: string,
context: Context,
): ts.MethodDeclaration {
let parentParam
const otherParams = toNonNullArray(
node.parameters.map(parameter => {
const name = getName(parameter)
if (name === 'parent') {
parentParam = addDecorator(
withDefaultType(parameter, createReferenceType(parentType)),
createParentDecorator(context),
)
return undefined
} else if (name === 'context') {
context.imports.push(createImport('@nestjs/graphql', 'Context'))
return {
...node,
parameters: toNonNullArray(
node.parameters.map(parameter => {
const name = getName(parameter)
if (name === 'parent') {
return addDecorator(
withDefaultType(parameter, createReferenceType(parentType)),
createParentDecorator(context),
)
} else if (name === 'context') {
context.imports.push(createImport('@nestjs/graphql', 'Context'))
return addDecorator(
withDefaultType(parameter, createReferenceType('GQLContext')),
createContextDecorator(),
)
}
return addDecorator(
withDefaultType(parameter, createReferenceType('GQLContext')),
createContextDecorator(),
withDefaultType(parameter, createStringType()),
createArgsDecorator(parameter, context),
)
}
return addDecorator(
withDefaultType(parameter, createStringType()),
createArgsDecorator(parameter, context),
)
}),
)
const parameters = toNonNullArray([parentParam, ...otherParams]) as any
return { ...node, parameters }
}),
) as any,
}
}

function addImplementsFieldResolver<T extends ts.ClassDeclaration>(
Expand Down Expand Up @@ -108,11 +109,12 @@ function getTypesFromFieldResolverImplementation(node: ts.ClassDeclaration) {

function getTypes(node: ts.ClassDeclaration) {
const { modelType, parentType } = getTypesFromFieldResolverImplementation(node)
const name = getName(node)
?.trim()
?.replace(/Resolver$/, '')
return [
modelType ?? getTypeFromDecorator(node, 'Resolver') ?? getName(node)?.replace(/Resolver$/, ''),
parentType ??
getTypeNameFromParameters(node) ??
`${getName(node)?.replace(/Resolver$/, '')}Type`,
modelType ?? getTypeFromDecorator(node, 'Resolver') ?? name,
parentType ?? getTypeNameFromParameters(node),
]
}

Expand Down Expand Up @@ -141,11 +143,17 @@ function getFieldDecoratorType(node: ts.Node) {

function processClassDeclaration(classDeclaration: ts.ClassDeclaration, context: Context) {
const [modelType, parentType] = getTypes(classDeclaration)
const typeFromDecorator = getTypeFromDecorator(classDeclaration, 'Resolver')
const hasFieldResolver = hasImplementationByName(classDeclaration, 'FieldResolver')
return ts.visitEachChild(
addExport(
addDecorator(
addImplementsFieldResolver(classDeclaration, modelType, parentType),
createResolverDecorator(modelType, context),
conditional(
(hasFieldResolver || !!typeFromDecorator) && !!parentType,
() => addImplementsFieldResolver(classDeclaration, modelType, parentType),
classDeclaration,
),
createResolverDecorator(modelType, hasFieldResolver || !!typeFromDecorator, context),
),
),
node => {
Expand Down

0 comments on commit 1846e2f

Please sign in to comment.