Skip to content
Closed
41 changes: 41 additions & 0 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,47 @@ describe('Execute: Handles basic execution tasks', () => {
);
});

it('works with deeply nested fragments', async () => {
const DataType: GraphQLObjectType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
a: { type: GraphQLString, resolve: () => 'Apple' },
}),
});

const n = 10000;
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const document = parse(`
query {
...X${n}
}
${fragments}
fragment X0 on Query {
a
}
`);

const result = await execute({
schema: new GraphQLSchema({ query: DataType }),
document,
});

expect(result).to.deep.equal({
data: {
a: 'Apple',
},
});
});

it('executes arbitrary code', async () => {
const data = {
a: () => 'Apple',
Expand Down
55 changes: 45 additions & 10 deletions src/execution/collectFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import { typeFromAST } from '../utilities/typeFromAST';

import { getDirectiveValues } from './values';

interface FragmentEntry {
fragment: FragmentDefinitionNode;
runtimeType: GraphQLObjectType;
}

/**
* Given a selectionSet, collects all of the fields and returns them.
*
Expand All @@ -37,16 +42,34 @@ export function collectFields(
runtimeType: GraphQLObjectType,
selectionSet: SelectionSetNode,
): Map<string, ReadonlyArray<FieldNode>> {
const foundFragments: Array<FragmentEntry> = [];
const fields = new Map();
const visited = new Set<string>();
collectFieldsImpl(
schema,
fragments,
variableValues,
runtimeType,
selectionSet,
fields,
new Set(),
visited,
foundFragments,
);

let entry;
while ((entry = foundFragments.pop()) !== undefined) {
collectFieldsImpl(
schema,
fragments,
variableValues,
entry.runtimeType,
entry.fragment.selectionSet,
fields,
visited,
foundFragments,
);
}

return fields;
}

Expand All @@ -68,6 +91,7 @@ export function collectSubfields(
fieldNodes: ReadonlyArray<FieldNode>,
): Map<string, ReadonlyArray<FieldNode>> {
const subFieldNodes = new Map();
const foundFragments: Array<FragmentEntry> = [];
const visitedFragmentNames = new Set<string>();
for (const node of fieldNodes) {
if (node.selectionSet) {
Expand All @@ -79,9 +103,25 @@ export function collectSubfields(
node.selectionSet,
subFieldNodes,
visitedFragmentNames,
foundFragments,
);
}
}

let entry;
while ((entry = foundFragments.pop()) !== undefined) {
collectFieldsImpl(
schema,
fragments,
variableValues,
entry.runtimeType,
entry.fragment.selectionSet,
subFieldNodes,
visitedFragmentNames,
foundFragments,
);
}

return subFieldNodes;
}

Expand All @@ -93,6 +133,7 @@ function collectFieldsImpl(
selectionSet: SelectionSetNode,
fields: Map<string, Array<FieldNode>>,
visitedFragmentNames: Set<string>,
foundFragments: Array<FragmentEntry>,
): void {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
Expand Down Expand Up @@ -124,6 +165,7 @@ function collectFieldsImpl(
selection.selectionSet,
fields,
visitedFragmentNames,
foundFragments,
);
break;
}
Expand All @@ -143,15 +185,8 @@ function collectFieldsImpl(
) {
continue;
}
collectFieldsImpl(
schema,
fragments,
variableValues,
runtimeType,
fragment.selectionSet,
fields,
visitedFragmentNames,
);

foundFragments.push({ runtimeType, fragment });
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1138,4 +1138,70 @@ describe('Validate: Overlapping fields can be merged', () => {
},
]);
});

it('does not hit stack size limits', () => {
const n = 10000;
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const query = `
query Test {
...X${n}
}
${fragments}
fragment X0 on Query {
__typename
}
`;

expectErrors(query).toDeepEqual([]);
});

it('finds conflicts in nested fragments', () => {
const n = 10000;
const fragments = Array.from(Array(n).keys()).reduce(
(acc, next) =>
acc.concat(`\n
fragment X${next + 1} on Query {
...X${next}
}
`),
'',
);

const query = `
query Test {
type: conflict
...X${n}
}
${fragments}
fragment X0 on Query {
type: conflict2
__typename
}
`;
expectErrors(query).toDeepEqual([
{
locations: [
{
column: 9,
line: 3,
},
{
column: 9,
line: 50008,
},
],
message:
'Fields "type" conflict because "conflict" and "conflict2" are different fields. Use different aliases on the fields to fetch both if this was intentional.',
},
]);
});
});
Loading