import { AccumulatorMap } from '../jsutils/AccumulatorMap.js';
import { invariant } from '../jsutils/invariant.js';
import type { ObjMap } from '../jsutils/ObjMap.js';
import type {
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
OperationDefinitionNode,
SelectionSetNode,
} from '../language/ast.js';
import { OperationTypeNode } from '../language/ast.js';
import { Kind } from '../language/kinds.js';
import type { GraphQLObjectType } from '../type/definition.js';
import { isAbstractType } from '../type/definition.js';
import {
GraphQLDeferDirective,
GraphQLIncludeDirective,
GraphQLSkipDirective,
} from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';
import { typeFromAST } from '../utilities/typeFromAST.js';
import { getDirectiveValues } from './values.js';
export interface DeferUsage {
label: string | undefined;
parentDeferUsage: DeferUsage | undefined;
}
export interface FieldDetails {
node: FieldNode;
deferUsage: DeferUsage | undefined;
}
export type FieldGroup = ReadonlyArray<FieldDetails>;
export type GroupedFieldSet = ReadonlyMap<string, FieldGroup>;
interface CollectFieldsContext {
schema: GraphQLSchema;
fragments: ObjMap<FragmentDefinitionNode>;
variableValues: { [variable: string]: unknown };
operation: OperationDefinitionNode;
runtimeType: GraphQLObjectType;
visitedFragmentNames: Set<string>;
}
export function collectFields(
schema: GraphQLSchema,
fragments: ObjMap<FragmentDefinitionNode>,
variableValues: { [variable: string]: unknown },
runtimeType: GraphQLObjectType,
operation: OperationDefinitionNode,
): {
groupedFieldSet: GroupedFieldSet;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const groupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];
const context: CollectFieldsContext = {
schema,
fragments,
variableValues,
runtimeType,
operation,
visitedFragmentNames: new Set(),
};
collectFieldsImpl(
context,
operation.selectionSet,
groupedFieldSet,
newDeferUsages,
);
return { groupedFieldSet, newDeferUsages };
}
export function collectSubfields(
schema: GraphQLSchema,
fragments: ObjMap<FragmentDefinitionNode>,
variableValues: { [variable: string]: unknown },
operation: OperationDefinitionNode,
returnType: GraphQLObjectType,
fieldGroup: FieldGroup,
): {
groupedFieldSet: GroupedFieldSet;
newDeferUsages: ReadonlyArray<DeferUsage>;
} {
const context: CollectFieldsContext = {
schema,
fragments,
variableValues,
runtimeType: returnType,
operation,
visitedFragmentNames: new Set(),
};
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
const newDeferUsages: Array<DeferUsage> = [];
for (const fieldDetail of fieldGroup) {
const node = fieldDetail.node;
if (node.selectionSet) {
collectFieldsImpl(
context,
node.selectionSet,
subGroupedFieldSet,
newDeferUsages,
fieldDetail.deferUsage,
);
}
}
return {
groupedFieldSet: subGroupedFieldSet,
newDeferUsages,
};
}
function collectFieldsImpl(
context: CollectFieldsContext,
selectionSet: SelectionSetNode,
groupedFieldSet: AccumulatorMap<string, FieldDetails>,
newDeferUsages: Array<DeferUsage>,
deferUsage?: DeferUsage,
): void {
const {
schema,
fragments,
variableValues,
runtimeType,
operation,
visitedFragmentNames,
} = context;
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case Kind.FIELD: {
if (!shouldIncludeNode(variableValues, selection)) {
continue;
}
groupedFieldSet.add(getFieldEntryKey(selection), {
node: selection,
deferUsage,
});
break;
}
case Kind.INLINE_FRAGMENT: {
if (
!shouldIncludeNode(variableValues, selection) ||
!doesFragmentConditionMatch(schema, selection, runtimeType)
) {
continue;
}
const newDeferUsage = getDeferUsage(
operation,
variableValues,
selection,
deferUsage,
);
if (!newDeferUsage) {
collectFieldsImpl(
context,
selection.selectionSet,
groupedFieldSet,
newDeferUsages,
deferUsage,
);
} else {
newDeferUsages.push(newDeferUsage);
collectFieldsImpl(
context,
selection.selectionSet,
groupedFieldSet,
newDeferUsages,
newDeferUsage,
);
}
break;
}
case Kind.FRAGMENT_SPREAD: {
const fragName = selection.name.value;
const newDeferUsage = getDeferUsage(
operation,
variableValues,
selection,
deferUsage,
);
if (
!newDeferUsage &&
(visitedFragmentNames.has(fragName) ||
!shouldIncludeNode(variableValues, selection))
) {
continue;
}
const fragment = fragments[fragName];
if (
fragment == null ||
!doesFragmentConditionMatch(schema, fragment, runtimeType)
) {
continue;
}
if (!newDeferUsage) {
visitedFragmentNames.add(fragName);
collectFieldsImpl(
context,
fragment.selectionSet,
groupedFieldSet,
newDeferUsages,
deferUsage,
);
} else {
newDeferUsages.push(newDeferUsage);
collectFieldsImpl(
context,
fragment.selectionSet,
groupedFieldSet,
newDeferUsages,
newDeferUsage,
);
}
break;
}
}
}
}
function getDeferUsage(
operation: OperationDefinitionNode,
variableValues: { [variable: string]: unknown },
node: FragmentSpreadNode | InlineFragmentNode,
parentDeferUsage: DeferUsage | undefined,
): DeferUsage | undefined {
const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues);
if (!defer) {
return;
}
if (defer.if === false) {
return;
}
invariant(
operation.operation !== OperationTypeNode.SUBSCRIPTION,
'`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.',
);
return {
label: typeof defer.label === 'string' ? defer.label : undefined,
parentDeferUsage,
};
}
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;
}
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;
}
function getFieldEntryKey(node: FieldNode): string {
return node.alias ? node.alias.value : node.name.value;
}