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

Avoid using internal api of graphql-js #1331

Merged
merged 2 commits into from Apr 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}