Skip to content

Commit

Permalink
Avoid using internal api of graphql-js (#1331)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Apr 2, 2020
1 parent 80cbc5f commit 7472269
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/delegate/results/handleObject.ts
Expand Up @@ -6,7 +6,9 @@ import {
FieldNode,
GraphQLObjectType,
} from 'graphql';
import { collectFields, ExecutionContext } from 'graphql/execution/execute';
import { ExecutionContext } from 'graphql/execution/execute';

import { collectFields } from '../../utils/collectFields';

import {
SubschemaConfig,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/SchemaDirectiveVisitor.ts
Expand Up @@ -4,14 +4,14 @@ import {
DirectiveLocationEnum,
TypeSystemExtensionNode,
} from 'graphql';
import { getArgumentValues } from 'graphql/execution/values';

import { VisitableSchemaType } from '../Interfaces';

import each from './each';
import valueFromASTUntyped from './valueFromASTUntyped';
import { SchemaVisitor } from './SchemaVisitor';
import { visitSchema } from './visitSchema';
import { getArgumentValues } from './getArgumentValues';

const hasOwn = Object.prototype.hasOwnProperty;

Expand Down
152 changes: 152 additions & 0 deletions src/utils/collectFields.ts
@@ -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;
}
101 changes: 101 additions & 0 deletions src/utils/getArgumentValues.ts
@@ -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);
}

0 comments on commit 7472269

Please sign in to comment.