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

refactor: collectFields to separate utility #3187

Merged
merged 1 commit into from
Jun 18, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
@@ -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;
}
158 changes: 8 additions & 150 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ import { locatedError } from '../error/locatedError';
import type {
DocumentNode,
OperationDefinitionNode,
SelectionSetNode,
FieldNode,
FragmentSpreadNode,
InlineFragmentNode,
FragmentDefinitionNode,
} from '../language/ast';
import { Kind } from '../language/kinds';
Expand All @@ -46,10 +43,6 @@ import {
TypeMetaFieldDef,
TypeNameMetaFieldDef,
} from '../type/introspection';
import {
GraphQLIncludeDirective,
GraphQLSkipDirective,
} from '../type/directives';
import {
isObjectType,
isAbstractType,
Expand All @@ -58,14 +51,10 @@ import {
isNonNullType,
} from '../type/definition';

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

import {
getVariableValues,
getArgumentValues,
getDirectiveValues,
} from './values';
import { getVariableValues, getArgumentValues } from './values';
import { collectFields } from './collectFields';

/**
* Terminology
Expand Down Expand Up @@ -336,7 +325,9 @@ function executeOperation(
): PromiseOrValue<ObjMap<unknown> | null> {
const type = getOperationRootType(exeContext.schema, operation);
const fields = collectFields(
exeContext,
exeContext.schema,
exeContext.fragments,
exeContext.variableValues,
type,
operation.selectionSet,
new Map(),
Expand Down Expand Up @@ -447,141 +438,6 @@ function executeFields(
return promiseForObject(results);
}

/**
* 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: 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(exeContext, 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(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.has(fragName) ||
!shouldIncludeNode(exeContext, selection)
) {
continue;
}
visitedFragmentNames.add(fragName);
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.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;
}

/**
* Implements the "Executing field" section of the spec
* In particular, this function figures out the value that the field returns by
Expand Down Expand Up @@ -1081,7 +937,9 @@ function _collectSubfields(
for (const node of fieldNodes) {
if (node.selectionSet) {
subFieldNodes = collectFields(
exeContext,
exeContext.schema,
exeContext.fragments,
exeContext.variableValues,
returnType,
node.selectionSet,
subFieldNodes,
Expand Down
9 changes: 6 additions & 3 deletions src/subscription/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { locatedError } from '../error/locatedError';
import type { DocumentNode } from '../language/ast';

import type { ExecutionResult, ExecutionContext } from '../execution/execute';
import { collectFields } from '../execution/collectFields';
import { getArgumentValues } from '../execution/values';
import {
assertValidExecutionArguments,
buildExecutionContext,
buildResolveInfo,
collectFields,
execute,
getFieldDef,
} from '../execution/execute';
Expand Down Expand Up @@ -189,10 +189,13 @@ export async function createSourceEventStream(
async function executeSubscription(
exeContext: ExecutionContext,
): Promise<unknown> {
const { schema, operation, variableValues, rootValue } = exeContext;
const { schema, fragments, operation, variableValues, rootValue } =
exeContext;
const type = getOperationRootType(schema, operation);
const fields = collectFields(
exeContext,
schema,
fragments,
variableValues,
type,
operation.selectionSet,
new Map(),
Expand Down
Loading