-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from all commits
a048998
977f59b
39818e1
2733c9b
7124f63
f4822d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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' | ||
|
@@ -91,79 +95,127 @@ 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, { | ||
[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] }) || {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not that it seems |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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-expect-error See https://github.com/graphql/graphql-js/pull/3511 - remove this comments once merged | ||
if (schema.extensions?.[didMapSchemaSymbol] === true) { | ||
return | ||
} | ||
const transformedSchema = wrapAffectedResolvers(schema, options) | ||
transformedSchema.extensions = { | ||
...schema.extensions, | ||
[didMapSchemaSymbol]: true, | ||
} | ||
replaceSchema(transformedSchema) | ||
}, | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://www.graphql-tools.com/docs/schema-directives#implementing-schema-directives for more context