Skip to content

Commit c99de57

Browse files
author
Sylvain Lebresne
authored
Allow fields with arguments in @requires (#2120)
If such field with arguments is used in a @requires, then that @requires must provide a value for any required argument and may provide a value for any non-required one (like in any normal graphQL selection set). Additionally, this commit rejects aliases in @requires, @provides and @key. Not because aliases cannot ever make sense in those directives, but rather because it currently does not work (it breaks at runtime) but wasn't rejected properly. Fixes #1987
1 parent 6d247d6 commit c99de57

File tree

11 files changed

+592
-85
lines changed

11 files changed

+592
-85
lines changed

composition-js/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
This CHANGELOG pertains only to Apollo Federation packages in the 2.x range. The Federation v0.x equivalent for this package can be found [here](https://github.com/apollographql/federation/blob/version-0.x/federation-js/CHANGELOG.md) on the `version-0.x` branch of this repo.
44

5+
- Allow fields with arguments in `@requires` [PR #2120](https://github.com/apollographql/federation/pull/2120).
6+
57
## 2.1.0
68

79
- Don't apply @shareable when upgrading fed1 supergraphs if it's already @shareable [PR #2043](https://github.com/apollographql/federation/pull/2043)

composition-js/src/__tests__/compose.test.ts

+72
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,78 @@ describe('composition', () => {
15291529
'Argument "Query.q(a:)" is required in some subgraphs but does not appear in all subgraphs: it is required in subgraph "subgraphA" but does not appear in subgraph "subgraphB"']
15301530
]);
15311531
});
1532+
1533+
it('errors if a subgraph argument is "@required" without arguments but that argument is mandatory in the supergraph', () => {
1534+
const subgraphA = {
1535+
typeDefs: gql`
1536+
type Query {
1537+
t: T
1538+
}
1539+
1540+
type T @key(fields: "id") {
1541+
id: ID!
1542+
x(arg: Int): Int @external
1543+
y: Int @requires(fields: "x")
1544+
}
1545+
`,
1546+
name: 'subgraphA',
1547+
};
1548+
1549+
const subgraphB = {
1550+
typeDefs: gql`
1551+
type T @key(fields: "id") {
1552+
id: ID!
1553+
x(arg: Int!): Int
1554+
}
1555+
`,
1556+
name: 'subgraphB',
1557+
};
1558+
1559+
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
1560+
1561+
expect(result.errors).toBeDefined();
1562+
expect(errors(result)).toStrictEqual([
1563+
[
1564+
'REQUIRES_INVALID_FIELDS',
1565+
'[subgraphA] On field "T.y", for @requires(fields: "x"): no value provided for argument "arg" of field "T.x" but a value is mandatory as "arg" is required in subgraph "subgraphB"',
1566+
]
1567+
]);
1568+
});
1569+
1570+
it('errors if a subgraph argument is "@required" with an argument, but that argument is not in the supergraph', () => {
1571+
const subgraphA = {
1572+
typeDefs: gql`
1573+
type Query {
1574+
t: T
1575+
}
1576+
1577+
type T @key(fields: "id") {
1578+
id: ID!
1579+
x(arg: Int): Int @external
1580+
y: Int @requires(fields: "x(arg: 42)")
1581+
}
1582+
`,
1583+
name: 'subgraphA',
1584+
};
1585+
1586+
const subgraphB = {
1587+
typeDefs: gql`
1588+
type T @key(fields: "id") {
1589+
id: ID!
1590+
x: Int
1591+
}
1592+
`,
1593+
name: 'subgraphB',
1594+
};
1595+
1596+
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
1597+
expect(errors(result)).toStrictEqual([
1598+
[
1599+
'REQUIRES_INVALID_FIELDS',
1600+
'[subgraphA] On field "T.y", for @requires(fields: "x(arg: 42)"): cannot provide a value for argument "arg" of field "T.x" as argument "arg" is not defined in subgraph "subgraphB"',
1601+
]
1602+
]);
1603+
});
15321604
});
15331605

15341606
describe('post-merge validation', () => {

composition-js/src/merging/merge.ts

+127
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ import {
6565
filterTypesOfKind,
6666
isNonNullType,
6767
isExecutableDirectiveLocation,
68+
parseFieldSetArgument,
69+
isCompositeType,
70+
isDefined,
71+
addSubgraphToError,
6872
} from "@apollo/federation-internals";
6973
import { ASTNode, GraphQLError, DirectiveLocation } from "graphql";
7074
import {
@@ -74,6 +78,7 @@ import {
7478
} from "../hints";
7579
import { ComposeDirectiveManager } from '../composeDirectiveManager';
7680
import { MismatchReporter } from './reporter';
81+
import { inspect } from "util";
7782

7883

7984
const linkSpec = LINK_VERSIONS.latest();
@@ -2092,6 +2097,128 @@ class Merger {
20922097
}
20932098
}
20942099
}
2100+
2101+
// We need to redo some validation for @requires after merge. The reason is that each subgraph validates that its own
2102+
// @requires are valid, but "requirements" are requested from _other_ subgraphs (by definition of @requires really),
2103+
// and there is a few situations (see details below) where a validity within the originated subgraph does not entail
2104+
// validity for all subgraph that would have to provide those "requirements".
2105+
// Long story short, we need to re-validate every @requires against the supergraph to guarantee it will always work
2106+
// at runtime.
2107+
for (const subgraph of this.subgraphs) {
2108+
for (const requiresApplication of subgraph.metadata().requiresDirective().applications()) {
2109+
const originalField = requiresApplication.parent as FieldDefinition<CompositeType>;
2110+
assert(originalField.kind === 'FieldDefinition', () => `Expected ${inspect(originalField)} to be a field`);
2111+
const mergedType = this.merged.type(originalField.parent.name);
2112+
// The type should exists: there is a few types we don't merge, but those are from specific core features and they shouldn't have @provides.
2113+
// In fact, if we were to not merge a type with a @provides, this would essentially mean that @provides cannot work, so worth catching
2114+
// the issue early if this ever happen for some reason. And of course, the type should be composite since it is in at least the one
2115+
// subgraph we're checking.
2116+
assert(mergedType && isCompositeType(mergedType), () => `Merged type ${originalField.parent.name} should exist should have field ${originalField.name}`)
2117+
assert(isCompositeType(mergedType), `${mergedType} should be a composite type but got ${mergedType.kind}`);
2118+
try {
2119+
parseFieldSetArgument({
2120+
parentType: mergedType,
2121+
directive: requiresApplication,
2122+
decorateValidationErrors: false,
2123+
});
2124+
} catch (e) {
2125+
if (!(e instanceof GraphQLError)) {
2126+
throw e;
2127+
}
2128+
2129+
// Providing a useful error message to the user here is tricky in the general case because what we checked is that
2130+
// a given subgraph @provides definition is invalid "on the supergraph", but the user seeing the error will not have
2131+
// the supergraph, so we need to express the error in terms of the subgraphs.
2132+
// But in practice, there is only a handful of cases that can trigger an error here. Indeed, at this point we know that
2133+
// - the @require application is valid in its original subgraph.
2134+
// - there was not merging errors (we don't call this whole method otherwise).
2135+
// This eliminate the risk of the error being due to some invalid syntax, of some subsection on a non-composite or missing
2136+
// on on a composite one (merging would have error), or of some unknown field in the selection (output types are merged
2137+
// by union, so any field that was in the subgraph will be in the supergraph), or even any error due to the types of fields
2138+
// involved (because the merged type is always a (non-strict) supertype of its counterpart in any subgraph, and anything
2139+
// that could be queried in a subtype can be queried on a supertype).
2140+
// As such, the only errors that we can have here are due to field arguments: because they are merged by intersection,
2141+
// it _is_ possible that something that is valid in a subgraph is not valid in the supergraph. And the only 2 things that
2142+
// can make such invalidity are:
2143+
// 1. an argument may not be in the supergraph: it is in the subgraph, but not in all the subgraphs having the field,
2144+
// and the `@provides` passes a concrete value to that argument.
2145+
// 2. the type of an argument in the supergraph is a strict subtype the type that argument has in `subgraph` (the one
2146+
// with the `@provides`) _and_ the `@provides` selection relies on the type difference. Now, argument types are input
2147+
// types and the only subtyping difference input types is related to nullability (neither interfaces nor union are
2148+
// input types in particular), so the only case this can happen is if a field `x` has some argument `a` type `A` in
2149+
// `subgraph` but type `!A` with no default in the supergraph, _and_ the `@provides` queries that field `x` _without_
2150+
// value for `a` (valid when `a` has type `A` but not with `!A` and no default).
2151+
// So to ensure we provide good error messages, we brute-force detecting those 2 possible cases and have a special
2152+
// treatment for each.
2153+
// Note that this detection is based on pattern-matching the error message, which is somewhat fragile, but because we
2154+
// only have 2 cases, we can easily cover them with unit tests, which means there is no practical risk of a message
2155+
// change breaking this code and being released undetected. A cleaner implementation would probably require having
2156+
// error codes and classes for all the graphqQL validations, but doing so cleanly is a fair amount of effort and probably
2157+
// no justified "just for this particular case".
2158+
const requireAST = requiresApplication.sourceAST ? [ addSubgraphToASTNode(requiresApplication.sourceAST, subgraph.name)] : [];
2159+
2160+
const that = this;
2161+
const registerError = (
2162+
arg: string,
2163+
field: string,
2164+
isIncompatible: (f: FieldDefinition<any>) => boolean,
2165+
makeMsg: (incompatibleSubgraphs: string) => string,
2166+
) => {
2167+
const incompatibleSubgraphs = that.subgraphs.values().map((otherSubgraph) => {
2168+
if (otherSubgraph.name === subgraph.name) {
2169+
return undefined;
2170+
}
2171+
const fieldInOther = otherSubgraph.schema.elementByCoordinate(field);
2172+
const fieldIsIncompatible = fieldInOther
2173+
&& fieldInOther instanceof FieldDefinition
2174+
&& isIncompatible(fieldInOther);
2175+
return fieldIsIncompatible
2176+
? {
2177+
name: otherSubgraph.name,
2178+
node: fieldInOther.sourceAST ? addSubgraphToASTNode(fieldInOther.sourceAST, otherSubgraph.name) : undefined,
2179+
}
2180+
: undefined;
2181+
}).filter(isDefined);
2182+
assert(incompatibleSubgraphs.length > 0, () => `Got error on ${arg} of ${field} but no "incompatible" subgraphs (error: ${e})`);
2183+
const nodes = requireAST.concat(incompatibleSubgraphs.map((s) => s.node).filter(isDefined));
2184+
const error = ERRORS.REQUIRES_INVALID_FIELDS.err(
2185+
`On field "${originalField.coordinate}", for ${requiresApplication}: ${makeMsg(printSubgraphNames(incompatibleSubgraphs.map((s) => s.name)))}`,
2186+
{ nodes }
2187+
);
2188+
that.errors.push(addSubgraphToError(error, subgraph.name));
2189+
}
2190+
2191+
const unknownArgument = e.message.match(/Unknown argument \"(?<arg>[^"]*)\" found in value: \"(?<field>[^"]*)\" has no argument.*/);
2192+
if (unknownArgument) {
2193+
const arg = unknownArgument.groups?.arg!;
2194+
const field = unknownArgument.groups?.field!;
2195+
registerError(
2196+
arg,
2197+
field,
2198+
(f) => !f.argument(arg),
2199+
(incompatibleSubgraphs) => `cannot provide a value for argument "${arg}" of field "${field}" as argument "${arg}" is not defined in ${incompatibleSubgraphs}`,
2200+
);
2201+
continue;
2202+
}
2203+
2204+
const missingMandatory = e.message.match(/Missing mandatory value for argument \"(?<arg>[^"]*)\" of field \"(?<field>[^"]*)\".*/);
2205+
if (missingMandatory) {
2206+
const arg = missingMandatory.groups?.arg!;
2207+
const field = missingMandatory.groups?.field!;
2208+
registerError(
2209+
arg,
2210+
field,
2211+
(f) => !!f.argument(arg)?.isRequired(),
2212+
(incompatibleSubgraphs) => `no value provided for argument "${arg}" of field "${field}" but a value is mandatory as "${arg}" is required in ${incompatibleSubgraphs}`,
2213+
);
2214+
continue;
2215+
}
2216+
2217+
assert(false, () => `Unexpected error throw by ${requiresApplication} when evaluated on supergraph: ${e.message}`);
2218+
}
2219+
}
2220+
}
2221+
20952222
}
20962223

20972224
private updateInaccessibleErrorsWithLinkToSubgraphs(

docs/source/errors.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ The following errors might be raised during composition:
7676
| `REQUIRED_INACCESSIBLE` | An element is marked as @inaccessible but is required by an element visible in the API schema. | 2.0.0 | |
7777
| `REQUIRED_INPUT_FIELD_MISSING_IN_SOME_SUBGRAPH` | A field of an input object type is mandatory in some subgraphs, but the field is not defined in all the subgraphs that define the input object type. | 2.0.0 | |
7878
| `REQUIRES_DIRECTIVE_IN_FIELDS_ARG` | The `fields` argument of a `@requires` directive includes some directive applications. This is not supported | 2.1.0 | |
79-
| `REQUIRES_FIELDS_HAS_ARGS` | The `fields` argument of a `@requires` directive includes a field defined with arguments (which is not currently supported). | 2.0.0 | |
8079
| `REQUIRES_FIELDS_MISSING_EXTERNAL` | The `fields` argument of a `@requires` directive includes a field that is not marked as `@external`. | 0.x | |
8180
| `REQUIRES_INVALID_FIELDS_TYPE` | The value passed to the `fields` argument of a `@requires` directive is not a string. | 2.0.0 | |
8281
| `REQUIRES_INVALID_FIELDS` | The `fields` argument of a `@requires` directive is invalid (it has invalid syntax, includes unknown fields, ...). | 2.0.0 | |
@@ -116,6 +115,7 @@ The following error codes have been removed and are no longer generated by the m
116115
| `NON_REPEATABLE_DIRECTIVE_ARGUMENTS_MISMATCH` | Since federation 2.1.0, the case this error used to cover is now a warning (with code `INCONSISTENT_NON_REPEATABLE_DIRECTIVE_ARGUMENTS`) instead of an error |
117116
| `PROVIDES_FIELDS_SELECT_INVALID_TYPE` | @provides can now be used on field of interface, union and list types |
118117
| `PROVIDES_NOT_ON_ENTITY` | @provides can now be used on any type. |
118+
| `REQUIRES_FIELDS_HAS_ARGS` | Since federation 2.1.1, using fields with arguments in a @requires is fully supported |
119119
| `REQUIRES_FIELDS_MISSING_ON_BASE` | Fields in @requires can now be from any subgraph. |
120120
| `REQUIRES_USED_ON_BASE` | As there is not type ownership anymore, there is also no particular limitation as to which subgraph can use a @requires. |
121121
| `RESERVED_FIELD_USED` | This error was previously not correctly enforced: the _service and _entities, if present, were overridden; this is still the case |

gateway-js/CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ This CHANGELOG pertains only to Apollo Federation packages in the 2.x range. The
44

55
## vNext
66

7+
- Allow fields with arguments in `@requires` [PR #2120](https://github.com/apollographql/federation/pull/2120).
8+
79
## 2.1.2-alpha.1
8-
- Fix potential inefficient planning due to __typename [PR #2137](https://github.com/apollographql/federation/pull/2137).
10+
11+
- Fix potential inefficient planning due to `__typename` [PR #2137](https://github.com/apollographql/federation/pull/2137).
912
- Fix potential assertion during query planning [PR #2133](https://github.com/apollographql/federation/pull/2133).
1013

1114
## 2.1.2-alpha.0

0 commit comments

Comments
 (0)