Skip to content

Commit

Permalink
refactor: collectFields to separate utility (#3187)
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Jun 18, 2021
1 parent 40db639 commit dab4f44
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 170 deletions.
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;
}
158 changes: 8 additions & 150 deletions src/execution/execute.ts
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
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

0 comments on commit dab4f44

Please sign in to comment.