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: allow experimental customization of execution algorithm #3163

Closed
wants to merge 6 commits into from
Closed
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
27 changes: 27 additions & 0 deletions src/execution/__tests__/executor-test.ts
Expand Up @@ -18,6 +18,7 @@ import {
GraphQLUnionType,
} from '../../type/definition';

import { Executor } from '../executor';
import { execute, executeSync } from '../execute';

describe('Execute: Handles basic execution tasks', () => {
Expand Down Expand Up @@ -1151,6 +1152,32 @@ describe('Execute: Handles basic execution tasks', () => {
expect(result).to.deep.equal({ data: { foo: null } });
});

it('uses a custom Executor', () => {
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
foo: { type: GraphQLString },
},
}),
});
const document = parse('{ foo }');

class CustomExecutor extends Executor {
executeField() {
return 'foo';
}
}

const result = executeSync({
schema,
document,
CustomExecutor,
});

expect(result).to.deep.equal({ data: { foo: 'foo' } });
});

it('uses a custom field resolver', () => {
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
Expand Down
159 changes: 159 additions & 0 deletions src/execution/collectFields.ts
@@ -0,0 +1,159 @@
import type { ObjMap } from '../jsutils/ObjMap';

import type {
SelectionSetNode,
FieldNode,
FragmentSpreadNode,
InlineFragmentNode,
FragmentDefinitionNode,
} from '../language/ast';
import { Kind } from '../language/kinds';

import type { GraphQLSchema } from '../type/schema';
import type { GraphQLObjectType } from '../type/definition';
import {
GraphQLIncludeDirective,
GraphQLSkipDirective,
} from '../type/directives';
import { isAbstractType } from '../type/definition';

import { typeFromAST } from '../utilities/typeFromAST';

import { getDirectiveValues } from './values';

/**
* 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(
schema: GraphQLSchema,
fragments: ObjMap<FragmentDefinitionNode>,
variableValues: { [variable: string]: unknown },
runtimeType: GraphQLObjectType,
selectionSet: SelectionSetNode,
fields: Map<string, Array<FieldNode>>,
visitedFragmentNames: Set<string>,
): Map<string, ReadonlyArray<FieldNode>> {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case Kind.FIELD: {
if (!shouldIncludeNode(variableValues, selection)) {
continue;
}
const name = getFieldEntryKey(selection);
const fieldList = fields.get(name);
if (fieldList !== undefined) {
fieldList.push(selection);
} else {
fields.set(name, [selection]);
}
break;
}
case Kind.INLINE_FRAGMENT: {
if (
!shouldIncludeNode(variableValues, selection) ||
!doesFragmentConditionMatch(schema, selection, runtimeType)
) {
continue;
}
collectFields(
schema,
fragments,
variableValues,
runtimeType,
selection.selectionSet,
fields,
visitedFragmentNames,
);
break;
}
case Kind.FRAGMENT_SPREAD: {
const fragName = selection.name.value;
if (
visitedFragmentNames.has(fragName) ||
!shouldIncludeNode(variableValues, selection)
) {
continue;
}
visitedFragmentNames.add(fragName);
const fragment = fragments[fragName];
if (
!fragment ||
!doesFragmentConditionMatch(schema, fragment, runtimeType)
) {
continue;
}
collectFields(
schema,
fragments,
variableValues,
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(
variableValues: { [variable: string]: unknown },
node: FragmentSpreadNode | FieldNode | InlineFragmentNode,
): boolean {
const skip = getDirectiveValues(GraphQLSkipDirective, node, variableValues);
if (skip?.if === true) {
return false;
}

const include = getDirectiveValues(
GraphQLIncludeDirective,
node,
variableValues,
);
if (include?.if === false) {
return false;
}
return true;
}

/**
* Determines if a fragment is applicable to the given type.
*/
function doesFragmentConditionMatch(
schema: GraphQLSchema,
fragment: FragmentDefinitionNode | InlineFragmentNode,
type: GraphQLObjectType,
): boolean {
const typeConditionNode = fragment.typeCondition;
if (!typeConditionNode) {
return true;
}
const conditionalType = typeFromAST(schema, typeConditionNode);
if (conditionalType === type) {
return true;
}
if (isAbstractType(conditionalType)) {
return schema.isSubType(conditionalType, type);
}
return false;
}

/**
* Implements the logic to compute the key of a given field's entry
*/
function getFieldEntryKey(node: FieldNode): string {
return node.alias ? node.alias.value : node.name.value;
}