Skip to content

Commit

Permalink
Re-apply changes, address olds comments, but do not start on function…
Browse files Browse the repository at this point in the history
…al work
  • Loading branch information
mjmahone committed Aug 1, 2022
1 parent f3b0fa6 commit ce1ff75
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 144 deletions.
150 changes: 150 additions & 0 deletions src/execution/__tests__/variables-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,156 @@ describe('Execute: Handles inputs', () => {
});
});

describe('using fragment arguments', () => {
it('when there are no fragment arguments', () => {
const result = executeQuery(`
query {
...a
}
fragment a on TestType {
fieldWithNonNullableStringInput(input: "A")
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNonNullableStringInput: '"A"',
},
});
});

it('when a value is required and provided', () => {
const result = executeQuery(`
query {
...a(value: "A")
}
fragment a($value: String!) on TestType {
fieldWithNonNullableStringInput(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNonNullableStringInput: '"A"',
},
});
});

it('when a value is required and not provided', () => {
const result = executeQuery(`
query {
...a
}
fragment a($value: String!) on TestType {
fieldWithNullableStringInput(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNullableStringInput: null,
},
});
});

it('when the definition has a default and is provided', () => {
const result = executeQuery(`
query {
...a(value: "A")
}
fragment a($value: String! = "B") on TestType {
fieldWithNonNullableStringInput(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNonNullableStringInput: '"A"',
},
});
});

it('when the definition has a default and is not provided', () => {
const result = executeQuery(`
query {
...a
}
fragment a($value: String! = "B") on TestType {
fieldWithNonNullableStringInput(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNonNullableStringInput: '"B"',
},
});
});

it('when the definition has a non-nullable default and is provided null', () => {
const result = executeQuery(`
query {
...a(value: null)
}
fragment a($value: String! = "B") on TestType {
fieldWithNullableStringInput(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNullableStringInput: 'null',
},
});
});

it('when the definition has no default and is not provided', () => {
const result = executeQuery(`
query {
...a
}
fragment a($value: String) on TestType {
fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $value)
}
`);
expect(result).to.deep.equal({
data: {
fieldWithNonNullableStringInputAndDefaultArgumentValue:
'"Hello World"',
},
});
});

it('when the argument variable is nested in a complex type', () => {
const result = executeQuery(`
query {
...a(value: "C")
}
fragment a($value: String) on TestType {
list(input: ["A", "B", $value, "D"])
}
`);
expect(result).to.deep.equal({
data: {
list: '["A", "B", "C", "D"]',
},
});
});

it('when argument variables are used recursively', () => {
const result = executeQuery(`
query {
...a(aValue: "C")
}
fragment a($aValue: String) on TestType {
...b(bValue: $aValue)
}
fragment b($bValue: String) on TestType {
list(input: ["A", "B", $bValue, "D"])
}
`);
expect(result).to.deep.equal({
data: {
list: '["A", "B", "C", "D"]',
},
});
});
});

describe('getVariableValues: limit maximum number of coercion errors', () => {
const doc = parse(`
query ($input: [String!]) {
Expand Down
47 changes: 46 additions & 1 deletion src/execution/collectFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap';
import type { ObjMap } from '../jsutils/ObjMap';

import type {
ArgumentNode,
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
SelectionSetNode,
ValueNode,
VariableDefinitionNode,
} from '../language/ast';
import { Kind } from '../language/kinds';
import { visit } from '../language/visitor';

import type { GraphQLObjectType } from '../type/definition';
import { isAbstractType } from '../type/definition';
Expand Down Expand Up @@ -139,12 +143,18 @@ function collectFieldsImpl(
) {
continue;
}
const selectionSetWithAppliedArguments =
selectionSetWithFragmentArgumentsApplied(
fragment.argumentDefinitions,
selection.arguments,
fragment.selectionSet,
);
collectFieldsImpl(
schema,
fragments,
variableValues,
runtimeType,
fragment.selectionSet,
selectionSetWithAppliedArguments,
fields,
visitedFragmentNames,
);
Expand All @@ -154,6 +164,41 @@ function collectFieldsImpl(
}
}

function selectionSetWithFragmentArgumentsApplied(
argumentDefinitions: Readonly<Array<VariableDefinitionNode>> | undefined,
fragmentArguments: Readonly<Array<ArgumentNode>> | undefined,
selectionSet: SelectionSetNode,
): SelectionSetNode {
const providedArguments = new Map<string, ArgumentNode>();
if (fragmentArguments) {
for (const argument of fragmentArguments) {
providedArguments.set(argument.name.value, argument);
}
}

const fragmentArgumentValues = new Map<string, ValueNode>();
if (argumentDefinitions) {
for (const argumentDef of argumentDefinitions) {
const variableName = argumentDef.variable.name.value;
const providedArg = providedArguments.get(variableName);
if (providedArg) {
// Not valid if the providedArg is null and argumentDef is non-null
fragmentArgumentValues.set(variableName, providedArg.value);
} else if (argumentDef.defaultValue) {
fragmentArgumentValues.set(variableName, argumentDef.defaultValue);
}
// If argumentDef is non-null, expect a provided arg or non-null default value.
// Otherwise just preserve the variable as-is: it will be treated as unset by the executor.
}
}

return visit(selectionSet, {
Variable(node) {
return fragmentArgumentValues.get(node.name.value);
},
});
}

/**
* Determines if a field should be included based on the `@include` and `@skip`
* directives, where `@skip` has higher precedence than `@include`.
Expand Down
13 changes: 8 additions & 5 deletions src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,16 @@ describe('Parser', () => {
expect('loc' in result).to.equal(false);
});

it('Legacy: allows parsing fragment defined variables', () => {
it('allows parsing fragment defined arguments', () => {
const document = 'fragment a($v: Boolean = false) on t { f(v: $v) }';

expect(() =>
parse(document, { allowLegacyFragmentVariables: true }),
).to.not.throw();
expect(() => parse(document)).to.throw('Syntax Error');
expect(() => parse(document)).to.not.throw();
});

it('allows parsing fragment spread arguments', () => {
const document = 'fragment a on t { ...b(v: $v) }';

expect(() => parse(document)).to.not.throw();
});

it('contains location that can be Object.toStringified, JSON.stringified, or jsutils.inspected', () => {
Expand Down
40 changes: 32 additions & 8 deletions src/language/__tests__/printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,34 +110,58 @@ describe('Printer: Query document', () => {
`);
});

it('Legacy: prints fragment with variable directives', () => {
const queryASTWithVariableDirective = parse(
it('prints fragment with argument definition directives', () => {
const fragmentWithArgumentDefinitionDirective = parse(
'fragment Foo($foo: TestType @test) on TestType @testDirective { id }',
{ allowLegacyFragmentVariables: true },
);
expect(print(queryASTWithVariableDirective)).to.equal(dedent`
expect(print(fragmentWithArgumentDefinitionDirective)).to.equal(dedent`
fragment Foo($foo: TestType @test) on TestType @testDirective {
id
}
`);
});

it('Legacy: correctly prints fragment defined variables', () => {
const fragmentWithVariable = parse(
it('correctly prints fragment defined arguments', () => {
const fragmentWithArgumentDefinition = parse(
`
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
`,
{ allowLegacyFragmentVariables: true },
);
expect(print(fragmentWithVariable)).to.equal(dedent`
expect(print(fragmentWithArgumentDefinition)).to.equal(dedent`
fragment Foo($a: ComplexType, $b: Boolean = false) on TestType {
id
}
`);
});

it('prints fragment spread with arguments', () => {
const fragmentSpreadWithArguments = parse(
'fragment Foo on TestType { ...Bar(a: {x: $x}, b: true) }',
);
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
fragment Foo on TestType {
...Bar(a: { x: $x }, b: true)
}
`);
});

it('prints fragment spread with multi-line arguments', () => {
const fragmentSpreadWithArguments = parse(
'fragment Foo on TestType { ...Bar(a: {x: $x, y: $y, z: $z, xy: $xy}, b: true, c: "a long string extending arguments over max length") }',
);
expect(print(fragmentSpreadWithArguments)).to.equal(dedent`
fragment Foo on TestType {
...Bar(
a: { x: $x, y: $y, z: $z, xy: $xy }
b: true
c: "a long string extending arguments over max length"
)
}
`);
});

it('prints kitchen sink without altering ast', () => {
const ast = parse(kitchenSinkQuery, {
noLocation: true,
Expand Down
46 changes: 44 additions & 2 deletions src/language/__tests__/visitor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,9 @@ describe('Visitor', () => {
]);
});

it('Legacy: visits variables defined in fragments', () => {
it('visits arguments defined on fragments', () => {
const ast = parse('fragment a($v: Boolean = false) on t { f }', {
noLocation: true,
allowLegacyFragmentVariables: true,
});
const visited: Array<any> = [];

Expand Down Expand Up @@ -505,6 +504,49 @@ describe('Visitor', () => {
]);
});

it('visits arguments on fragment spreads', () => {
const ast = parse('fragment a on t { ...s(v: false) }', {
noLocation: true,
});
const visited: Array<any> = [];

visit(ast, {
enter(node) {
checkVisitorFnArgs(ast, arguments);
visited.push(['enter', node.kind, getValue(node)]);
},
leave(node) {
checkVisitorFnArgs(ast, arguments);
visited.push(['leave', node.kind, getValue(node)]);
},
});

expect(visited).to.deep.equal([
['enter', 'Document', undefined],
['enter', 'FragmentDefinition', undefined],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['enter', 'NamedType', undefined],
['enter', 'Name', 't'],
['leave', 'Name', 't'],
['leave', 'NamedType', undefined],
['enter', 'SelectionSet', undefined],
['enter', 'FragmentSpread', undefined],
['enter', 'Name', 's'],
['leave', 'Name', 's'],
['enter', 'Argument', { kind: 'BooleanValue', value: false }],
['enter', 'Name', 'v'],
['leave', 'Name', 'v'],
['enter', 'BooleanValue', false],
['leave', 'BooleanValue', false],
['leave', 'Argument', { kind: 'BooleanValue', value: false }],
['leave', 'FragmentSpread', undefined],
['leave', 'SelectionSet', undefined],
['leave', 'FragmentDefinition', undefined],
['leave', 'Document', undefined],
]);
});

it('n', () => {
const ast = parse(kitchenSinkQuery, {
experimentalClientControlledNullability: true,
Expand Down
Loading

0 comments on commit ce1ff75

Please sign in to comment.