Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Avoid using internal api of graphql-js (#1331)
- Loading branch information
1 parent
f94457f
commit 65f2cb9
Showing
6 changed files
with
409 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { ExecutionContext } from 'graphql/execution/execute'; | ||
import { | ||
GraphQLObjectType, | ||
SelectionSetNode, | ||
FieldNode, | ||
Kind, | ||
FragmentSpreadNode, | ||
InlineFragmentNode, | ||
getDirectiveValues, | ||
GraphQLSkipDirective, | ||
GraphQLIncludeDirective, | ||
FragmentDefinitionNode, | ||
typeFromAST, | ||
isAbstractType, | ||
} from 'graphql'; | ||
|
||
/** | ||
* Given a selectionSet, adds all of the fields in that selection to | ||
* the passed in map of fields, and returns it at the end. | ||
* | ||
* CollectFields requires the "runtime type" of an object. For a field which | ||
* returns an Interface or Union type, the "runtime type" will be the actual | ||
* Object type returned by that field. | ||
* | ||
* @internal | ||
*/ | ||
export function collectFields( | ||
exeContext: ExecutionContext, | ||
runtimeType: GraphQLObjectType, | ||
selectionSet: SelectionSetNode, | ||
fields: Record<string, Array<FieldNode>>, | ||
visitedFragmentNames: Record<string, boolean>, | ||
): Record<string, Array<FieldNode>> { | ||
for (const selection of selectionSet.selections) { | ||
switch (selection.kind) { | ||
case Kind.FIELD: { | ||
if (!shouldIncludeNode(exeContext, selection)) { | ||
continue; | ||
} | ||
const name = getFieldEntryKey(selection); | ||
if (!fields[name]) { | ||
fields[name] = []; | ||
} | ||
fields[name].push(selection); | ||
break; | ||
} | ||
case Kind.INLINE_FRAGMENT: { | ||
if ( | ||
!shouldIncludeNode(exeContext, selection) || | ||
!doesFragmentConditionMatch(exeContext, selection, runtimeType) | ||
) { | ||
continue; | ||
} | ||
collectFields( | ||
exeContext, | ||
runtimeType, | ||
selection.selectionSet, | ||
fields, | ||
visitedFragmentNames, | ||
); | ||
break; | ||
} | ||
case Kind.FRAGMENT_SPREAD: { | ||
const fragName = selection.name.value; | ||
if ( | ||
visitedFragmentNames[fragName] || | ||
!shouldIncludeNode(exeContext, selection) | ||
) { | ||
continue; | ||
} | ||
visitedFragmentNames[fragName] = true; | ||
const fragment = exeContext.fragments[fragName]; | ||
if ( | ||
!fragment || | ||
!doesFragmentConditionMatch(exeContext, fragment, runtimeType) | ||
) { | ||
continue; | ||
} | ||
collectFields( | ||
exeContext, | ||
runtimeType, | ||
fragment.selectionSet, | ||
fields, | ||
visitedFragmentNames, | ||
); | ||
break; | ||
} | ||
} | ||
} | ||
return fields; | ||
} | ||
|
||
/** | ||
* Determines if a field should be included based on the @include and @skip | ||
* directives, where @skip has higher precedence than @include. | ||
*/ | ||
function shouldIncludeNode( | ||
exeContext: ExecutionContext, | ||
node: FragmentSpreadNode | FieldNode | InlineFragmentNode, | ||
): boolean { | ||
const skip = getDirectiveValues( | ||
GraphQLSkipDirective, | ||
node, | ||
exeContext.variableValues, | ||
); | ||
|
||
if (skip?.if === true) { | ||
return false; | ||
} | ||
|
||
const include = getDirectiveValues( | ||
GraphQLIncludeDirective, | ||
node, | ||
exeContext.variableValues, | ||
); | ||
|
||
if (include?.if === false) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Determines if a fragment is applicable to the given type. | ||
*/ | ||
function doesFragmentConditionMatch( | ||
exeContext: ExecutionContext, | ||
fragment: FragmentDefinitionNode | InlineFragmentNode, | ||
type: GraphQLObjectType, | ||
): boolean { | ||
const typeConditionNode = fragment.typeCondition; | ||
if (!typeConditionNode) { | ||
return true; | ||
} | ||
const conditionalType = typeFromAST(exeContext.schema, typeConditionNode); | ||
if (conditionalType === type) { | ||
return true; | ||
} | ||
if (isAbstractType(conditionalType)) { | ||
return exeContext.schema.isPossibleType(conditionalType, type); | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Implements the logic to compute the key of a given field's entry | ||
*/ | ||
function getFieldEntryKey(node: FieldNode): string { | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
return node.alias ? node.alias.value : node.name.value; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { | ||
valueFromAST, | ||
GraphQLField, | ||
GraphQLDirective, | ||
DirectiveNode, | ||
FieldNode, | ||
isNonNullType, | ||
GraphQLError, | ||
Kind, | ||
print, | ||
} from 'graphql'; | ||
|
||
import { keyMap } from './keyMap'; | ||
import { inspect } from './inspect'; | ||
|
||
/** | ||
* Prepares an object map of argument values given a list of argument | ||
* definitions and list of argument AST nodes. | ||
* | ||
* Note: The returned value is a plain Object with a prototype, since it is | ||
* exposed to user code. Care should be taken to not pull values from the | ||
* Object prototype. | ||
*/ | ||
export function getArgumentValues( | ||
def: GraphQLField<any, any> | GraphQLDirective, | ||
node: FieldNode | DirectiveNode, | ||
variableValues?: { [variableName: string]: any }, | ||
): { [argument: string]: any } { | ||
const coercedValues = {}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
const argumentNodes = node.arguments ?? []; | ||
const argNodeMap = keyMap(argumentNodes, (arg) => arg.name.value); | ||
|
||
for (const argDef of def.args) { | ||
const name = argDef.name; | ||
const argType = argDef.type; | ||
const argumentNode = argNodeMap[name]; | ||
|
||
if (!argumentNode) { | ||
if (argDef.defaultValue !== undefined) { | ||
coercedValues[name] = argDef.defaultValue; | ||
} else if (isNonNullType(argType)) { | ||
throw new GraphQLError( | ||
`Argument "${name}" of required type "${inspect(argType)}" ` + | ||
'was not provided.', | ||
node, | ||
); | ||
} | ||
continue; | ||
} | ||
|
||
const valueNode = argumentNode.value; | ||
let isNull = valueNode.kind === Kind.NULL; | ||
|
||
if (valueNode.kind === Kind.VARIABLE) { | ||
const variableName = valueNode.name.value; | ||
if ( | ||
variableValues == null || | ||
!hasOwnProperty(variableValues, variableName) | ||
) { | ||
if (argDef.defaultValue !== undefined) { | ||
coercedValues[name] = argDef.defaultValue; | ||
} else if (isNonNullType(argType)) { | ||
throw new GraphQLError( | ||
`Argument "${name}" of required type "${inspect(argType)}" ` + | ||
`was provided the variable "$${variableName}" which was not provided a runtime value.`, | ||
valueNode, | ||
); | ||
} | ||
continue; | ||
} | ||
isNull = variableValues[variableName] == null; | ||
} | ||
|
||
if (isNull && isNonNullType(argType)) { | ||
throw new GraphQLError( | ||
`Argument "${name}" of non-null type "${inspect(argType)}" ` + | ||
'must not be null.', | ||
valueNode, | ||
); | ||
} | ||
|
||
const coercedValue = valueFromAST(valueNode, argType, variableValues); | ||
if (coercedValue === undefined) { | ||
// Note: ValuesOfCorrectTypeRule validation should catch this before | ||
// execution. This is a runtime check to ensure execution does not | ||
// continue with an invalid argument value. | ||
throw new GraphQLError( | ||
`Argument "${name}" has invalid value ${print(valueNode)}.`, | ||
valueNode, | ||
); | ||
} | ||
coercedValues[name] = coercedValue; | ||
} | ||
return coercedValues; | ||
} | ||
|
||
function hasOwnProperty(obj: any, prop: string): boolean { | ||
return Object.prototype.hasOwnProperty.call(obj, prop); | ||
} |
Oops, something went wrong.