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: only wrap affected resolver functions by using the 'onSchemaChange' hook with 'mapSchema' instead of the 'onResolverCalled' hook. #4760

Merged
merged 6 commits into from
Mar 16, 2022
162 changes: 108 additions & 54 deletions packages/graphql-server/src/plugins/useRedwoodDirective.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Plugin } from '@envelop/types'
import { mapSchema, MapperKind } from '@graphql-tools/utils'
import {
defaultFieldResolver,
DirectiveNode,
DocumentNode,
getDirectiveValues,
GraphQLFieldConfig,
GraphQLObjectType,
GraphQLResolveInfo,
GraphQLSchema,
} from 'graphql'

import { GlobalContext } from '../index'
Expand Down Expand Up @@ -91,79 +95,129 @@ export function hasDirective(info: GraphQLResolveInfo): boolean {
}

export function getDirectiveByName(
info: GraphQLResolveInfo,
name: string
fieldConfig: GraphQLFieldConfig<any, any, any>,
directiveName: string
): null | DirectiveNode {
try {
const { parentType, fieldName, schema } = info
const schemaType = schema.getType(parentType.name) as GraphQLObjectType
const field = schemaType.getFields()[fieldName]
const astNode = field.astNode
const associatedDirective = astNode?.directives?.find(
(directive) => directive.name.value === name
)
const associatedDirective = fieldConfig.astNode?.directives?.find(
(directive) => directive.name.value === directiveName
)
return associatedDirective ?? null
}

return associatedDirective || null
} catch (error) {
console.error(error)
return null
}
export function isPromise(value: any): value is Promise<unknown> {
return typeof value?.then === 'function'
}

export const useRedwoodDirective = (
function wrapAffectedResolvers(
schema: GraphQLSchema,
options: DirectivePluginOptions
): Plugin<{
onResolverCalled: ValidatorDirectiveFunc | TransformerDirectiveFunc
}> => {
return {
async onResolverCalled({ args, root, context, info }) {
const directiveNode = getDirectiveByName(info, options.name)
): GraphQLSchema {
return mapSchema(schema, {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[MapperKind.OBJECT_FIELD](fieldConfig, _, __, schema) {
const directiveNode = getDirectiveByName(fieldConfig, options.name)
const directive = directiveNode
? info.schema.getDirective(directiveNode.name.value)
? schema.getDirective(directiveNode.name.value)
: null

if (directiveNode && directive) {
const directiveArgs =
getDirectiveValues(
directive,
{ directives: [directiveNode] },
info.variableValues
) || {}

getDirectiveValues(directive, { directives: [directiveNode] }) || {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not that it seems info.variableValues was not relevant at all, since we are looking at schema directives, not operation directives.

const originalResolve = fieldConfig.resolve ?? defaultFieldResolver
if (_isValidator(options)) {
await options.onResolverCalled({
root,
args,
context,
info,
directiveNode,
directiveArgs,
})
return {
...fieldConfig,
resolve: function useRedwoodDirectiveValidatorResolver(
root,
args,
context,
info
) {
const result = options.onResolverCalled({
root,
args,
context,
info,
directiveNode,
directiveArgs,
})

if (isPromise(result)) {
return result.then(() =>
originalResolve(root, args, context, info)
)
Comment on lines +143 to +146
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a small performance optimization for avoiding making stuff async where not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@yaacovCR also created this cool micro library, it could be used instead.

}
return originalResolve(root, args, context, info)
},
}
}

// In order to change the value of the field, we have to return a function in this form
// ({result, setResult}) => { setResult(newValue)}
// Not super clear but mentioned here: https://www.envelop.dev/docs/plugins/lifecycle#onexecuteapi

if (_isTransformer(options)) {
return ({ result, setResult }) => {
// @NOTE! A transformer cannot be async
const transformedValue = options.onResolverCalled({
return {
...fieldConfig,
resolve: function useRedwoodDirectiveTransformerResolver(
root,
args,
context,
info,
directiveNode,
directiveArgs,
resolvedValue: result,
})

setResult(transformedValue)
info
) {
const resolvedValue = originalResolve(root, args, context, info)
if (isPromise(resolvedValue)) {
return resolvedValue.then((resolvedValue) =>
options.onResolverCalled({
root,
args,
context,
info,
directiveNode,
directiveArgs,
resolvedValue,
})
)
}
return options.onResolverCalled({
root,
args,
context,
info,
directiveNode,
directiveArgs,
resolvedValue,
})
},
}
}
}
return fieldConfig
},
})
}

return
export const useRedwoodDirective = (
options: DirectivePluginOptions
): Plugin<{
onResolverCalled: ValidatorDirectiveFunc | TransformerDirectiveFunc
}> => {
/**
* This symbol is added to the schema extensions for checking whether the transform got already applied.
*/
const didMapSchemaSymbol = Symbol('useRedwoodDirective.didMapSchemaSymbol')
return {
onSchemaChange({ schema, replaceSchema }) {
/**
* Currently graphql-js extensions typings are limited to string keys.
* We are using symbols as each useRedwoodDirective plugin instance should use its own unique symbol.
Comment on lines +205 to +206
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created graphql/graphql-js#3511 to address this.

*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dac09 marked this conversation as resolved.
Show resolved Hide resolved
if (schema.extensions?.[didMapSchemaSymbol] === true) {
return
}
const transformedSchema = wrapAffectedResolvers(schema, options)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
dac09 marked this conversation as resolved.
Show resolved Hide resolved
transformedSchema.extensions = {
...schema.extensions,
[didMapSchemaSymbol]: true,
}
replaceSchema(transformedSchema)
Copy link
Contributor Author

@n1ru4l n1ru4l Mar 15, 2022

Choose a reason for hiding this comment

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

Since for every replaceSchema call the onSchemaChange hook of all other plugins is called we need a way to track whether a transform has already been applied for preventing an infinite loop. The schema extensions are perfect for this!

},
}
}
Expand Down