Skip to content

Commit cd8cde9

Browse files
author
Sylvain Lebresne
authored
Various fixes around @interfaceObject (#2303)
There were a few things that didn't worked as expected or weren't validated correctly in the handling of `@interfaceObject` and this commit fixes those issues. The issues concretely fixed are: - Proper validation of shareability for fields provided by @interfaceObject, including cases where multiple @interfaceObject "abstract" the same field. - If `@interfaceObject` is used for some interface `I`, reject if the subgraph also defines some of the implementations of `I` (this may be something that we lift later, but since the `@interfaceObject` abstract the type in question, we should take some time to think about the consequence if we do lift that limitation). - Rejects `@override` on both `@interfaceObject` fields and object type fields which are otherwise provided by an @interfaceObject in another subgraph. Here again, we can probably lift such limitation later, but we need to think that through. - Fix handling of external fields on implementation type when those field are provided by an `@interfaceObject`. This was preventing using such fields in `@requires` for instance. - Rejects `@override` when used on interface fields. This is actually not strictly `@interfaceObject` related, but simply noticed that we were not properly rejecting it (much like we didn't reject `@shareable` on interface fields until a recent fix). The code definitively does not handle `@override` on an interface field (and like for `@shareable`, it's a tad unclear what it would mean really), and accepting it while ignoring it is just confusing.
1 parent 6427281 commit cd8cde9

File tree

9 files changed

+692
-43
lines changed

9 files changed

+692
-43
lines changed

composition-js/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ This CHANGELOG pertains only to Apollo Federation packages in the 2.x range. The
77
## 2.3.0
88

99
- Preserves source of union members and enum values in supergraph [PR #2288](https://github.com/apollographql/federation/pull/2288).
10+
- __BREAKING__: composition now rejects `@override` on interface fields. The `@override` directive was not
11+
meant to be supported on interfaces and was not having any impact whatsoever. If an existing subgraph does have a
12+
`@override` on an interface field, this will now be rejected, but the `@override` can simply and safely be removed
13+
since it previously was ignored.
1014

1115
## 2.2.0
1216

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

+208
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,160 @@ describe('composition', () => {
21132113
// (definition and extension) but also that it's properly taking into account.
21142114
assertCompositionSuccess(result);
21152115
});
2116+
2117+
describe('@interfaceObject', () => {
2118+
// An @interfaceObject type provides fields for all the implementation it abstracts, which should impact the shareability
2119+
// for those concrete impelmentations. That is, if a field is provided by both an @interfaceObject and also by one concrete
2120+
// implementation in another subgraph, then it needs to be marked @shareable. Those test check this as well as some
2121+
// variants.
2122+
2123+
it.each([
2124+
{
2125+
shareableOnConcreteType: false,
2126+
shareableOnInterfaceObject: false,
2127+
nonShareableErrorDetail: 'all of them',
2128+
},
2129+
{
2130+
shareableOnConcreteType: true,
2131+
shareableOnInterfaceObject: false,
2132+
nonShareableErrorDetail: 'subgraph "subgraphA" (through @interfaceObject field "I.x")',
2133+
},
2134+
{
2135+
shareableOnConcreteType: false,
2136+
shareableOnInterfaceObject: true,
2137+
nonShareableErrorDetail: 'subgraph "subgraphB"',
2138+
},
2139+
{
2140+
shareableOnConcreteType: true,
2141+
shareableOnInterfaceObject: true,
2142+
},
2143+
])(
2144+
'enforces shareable constraints for field "abstracted" by @interfaceObject and shared (shareable on concrete type: $shareableOnConcreteType, shareable on @interfaceObject: $shareableOnInterfaceObject)',
2145+
({ shareableOnConcreteType, shareableOnInterfaceObject, nonShareableErrorDetail}) => {
2146+
const subgraphA = {
2147+
typeDefs: gql`
2148+
type Query {
2149+
iFromA: I
2150+
}
2151+
2152+
type I @interfaceObject @key(fields: "id") {
2153+
id: ID!
2154+
x: Int${shareableOnInterfaceObject ? ' @shareable' : ''}
2155+
}
2156+
`,
2157+
name: 'subgraphA',
2158+
};
2159+
2160+
const subgraphB = {
2161+
typeDefs: gql`
2162+
type Query {
2163+
iFromB: I
2164+
}
2165+
2166+
interface I @key(fields: "id") {
2167+
id: ID!
2168+
x: Int
2169+
}
2170+
2171+
type A implements I @key(fields: "id") {
2172+
id: ID!
2173+
x: Int${shareableOnConcreteType ? ' @shareable' : ''}
2174+
}
2175+
`,
2176+
name: 'subgraphB',
2177+
};
2178+
2179+
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
2180+
if (nonShareableErrorDetail) {
2181+
expect(result.errors).toBeDefined();
2182+
expect(errors(result)).toStrictEqual([[
2183+
'INVALID_FIELD_SHARING',
2184+
`Non-shareable field "A.x" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" (through @interfaceObject field "I.x") and "subgraphB" and defined as non-shareable in ${nonShareableErrorDetail}`
2185+
]]);
2186+
} else {
2187+
expect(result.errors).toBeUndefined();
2188+
}
2189+
}
2190+
);
2191+
2192+
it.each([
2193+
{
2194+
shareableOnI1: false,
2195+
shareableOnI2: false,
2196+
nonShareableErrorDetail: 'all of them',
2197+
},
2198+
{
2199+
shareableOnI1: true,
2200+
shareableOnI2: false,
2201+
nonShareableErrorDetail: 'subgraph "subgraphA" (through @interfaceObject field "I2.x")',
2202+
},
2203+
{
2204+
shareableOnI1: false,
2205+
shareableOnI2: true,
2206+
nonShareableErrorDetail: 'subgraph "subgraphA" (through @interfaceObject field "I1.x")',
2207+
},
2208+
{
2209+
shareableOnI1: true,
2210+
shareableOnI2: true,
2211+
},
2212+
])(
2213+
'enforces shareability in a single subgraph with 2 intersecting @interfaceObject (shareable on first @interfaceObject: $shareableOnI1, shareable on second @interfaceObject: $shareableOnI2)',
2214+
({ shareableOnI1, shareableOnI2, nonShareableErrorDetail}) => {
2215+
const subgraphA = {
2216+
typeDefs: gql`
2217+
type Query {
2218+
i1FromA: I1
2219+
i2FromA: I2
2220+
}
2221+
2222+
type I1 @interfaceObject @key(fields: "id") {
2223+
id: ID!
2224+
x: Int${shareableOnI1 ? ' @shareable' : ''}
2225+
}
2226+
2227+
type I2 @interfaceObject @key(fields: "id") {
2228+
id: ID!
2229+
x: Int${shareableOnI2 ? ' @shareable' : ''}
2230+
}
2231+
`,
2232+
name: 'subgraphA',
2233+
};
2234+
2235+
const subgraphB = {
2236+
typeDefs: gql`
2237+
type Query {
2238+
i1FromB: I1
2239+
i2FromB: I2
2240+
}
2241+
2242+
interface I1 @key(fields: "id") {
2243+
id: ID!
2244+
}
2245+
2246+
interface I2 @key(fields: "id") {
2247+
id: ID!
2248+
}
2249+
2250+
type A implements I1 & I2 @key(fields: "id") {
2251+
id: ID!
2252+
}
2253+
`,
2254+
name: 'subgraphB',
2255+
};
2256+
2257+
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
2258+
if (nonShareableErrorDetail) {
2259+
expect(result.errors).toBeDefined();
2260+
expect(errors(result)).toStrictEqual([[
2261+
'INVALID_FIELD_SHARING',
2262+
`Non-shareable field "A.x" is resolved from multiple subgraphs: it is resolved from subgraphs "subgraphA" (through @interfaceObject field "I1.x") and "subgraphA" (through @interfaceObject field "I2.x") and defined as non-shareable in ${nonShareableErrorDetail}`
2263+
]]);
2264+
} else {
2265+
expect(result.errors).toBeUndefined();
2266+
}
2267+
}
2268+
);
2269+
});
21162270
});
21172271

21182272
it('handles renamed federation directives', () => {
@@ -3544,5 +3698,59 @@ describe('composition', () => {
35443698
'[subgraphA] Interface type "I" has a resolvable key (@key(fields: "id")) in subgraph "subgraphA" but that subgraph is missing some of the supergraph implementation types of "I". Subgraph "subgraphA" should define type "C" (and have it implement "I").',
35453699
]]);
35463700
});
3701+
3702+
it('errors if a subgraph defines both an @interfaceObject and some implemenations', () => {
3703+
const subgraphA = {
3704+
typeDefs: gql`
3705+
type Query {
3706+
iFromA: I
3707+
}
3708+
3709+
interface I @key(fields: "id") {
3710+
id: ID!
3711+
x: Int
3712+
}
3713+
3714+
type A implements I @key(fields: "id") {
3715+
id: ID!
3716+
x: Int
3717+
w: Int
3718+
}
3719+
3720+
type B implements I @key(fields: "id") {
3721+
id: ID!
3722+
x: Int
3723+
z: Int
3724+
}
3725+
`,
3726+
name: 'subgraphA',
3727+
};
3728+
3729+
const subgraphB = {
3730+
typeDefs: gql`
3731+
type Query {
3732+
iFromB: I
3733+
}
3734+
3735+
type I @interfaceObject @key(fields: "id") {
3736+
id: ID!
3737+
y: Int
3738+
}
3739+
3740+
type A @key(fields: "id") {
3741+
id: ID!
3742+
y: Int
3743+
}
3744+
`,
3745+
name: 'subgraphB',
3746+
};
3747+
3748+
const result = composeAsFed2Subgraphs([subgraphA, subgraphB]);
3749+
expect(result.errors).toBeDefined();
3750+
expect(errors(result)).toStrictEqual([[
3751+
'INTERFACE_OBJECT_USAGE_ERROR',
3752+
'[subgraphB] Interface type "I" is defined as an @interfaceObject in subgraph "subgraphB" so that subgraph should not define any of the implementation types of "I", but it defines type "A"',
3753+
]]);
3754+
});
35473755
});
35483756
});

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

+108
Original file line numberDiff line numberDiff line change
@@ -757,4 +757,112 @@ describe("composition involving @override directive", () => {
757757
`@override cannot be used on field "T.k" on subgraph "Subgraph1" since "T.k" on "Subgraph1" is marked with directive "@external"`,
758758
]);
759759
});
760+
761+
// At the moment, we've punted on @override support when interacting with @interfaceObject, so the
762+
// following tests mainly cover the various possible use and show that it currently correcly raise
763+
// some validation errors. We may lift some of those limitation in the future.
764+
describe('@interfaceObject', () => {
765+
it("does not allow @override on @interfaceObject fields", () => {
766+
// We currently rejects @override on fields of an @interfaceObject type. We could lift
767+
// that limitation in the future, and that would mean such override overrides the field
768+
// in _all_ the implementations of the target subtype, but that would imply generalizing
769+
// the handling overriden fields and the override error messages, so we keep that for
770+
// later.
771+
// Note that it would be a tad simpler to support @override on an @interfaceObject if
772+
// the `from` subgraph is also an @interfaceObject, as we can essentially ignore that
773+
// we have @interfaceObject in such case, but it's a corner case and it's clearer for
774+
// not to just always reject @override on @interfaceObject.
775+
const subgraph1 = {
776+
name: "Subgraph1",
777+
url: "https://Subgraph1",
778+
typeDefs: gql`
779+
type Query {
780+
i1: I
781+
}
782+
783+
type I @interfaceObject @key(fields: "k") {
784+
k: ID
785+
a: Int @override(from: "Subgraph2")
786+
}
787+
`,
788+
};
789+
790+
const subgraph2 = {
791+
name: "Subgraph2",
792+
url: "https://Subgraph2",
793+
typeDefs: gql`
794+
type Query {
795+
i2: I
796+
}
797+
798+
interface I @key(fields: "k") {
799+
k: ID
800+
a: Int
801+
}
802+
803+
type A implements I @key(fields: "k") {
804+
k: ID
805+
a: Int
806+
}
807+
`,
808+
};
809+
810+
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
811+
expect(result.errors).toBeDefined();
812+
expect(errors(result)).toContainEqual([
813+
"OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE",
814+
'@override is not yet supported on fields of @interfaceObject types: cannot be used on field "I.a" on subgraph "Subgraph1".',
815+
]);
816+
});
817+
818+
it("does not allow @override when overriden field is an @interfaceObject field", () => {
819+
// We don't allow @override on a concrete type field when the `from` subgraph has
820+
// an @interfaceObject field "covering" that field. In theory, this could have some
821+
// use if one wanted to move a field from an @interfaceObject into all its implementations
822+
// (in another subgraph) but it's also a bit hard to validate/use because we would have
823+
// to check that all the implementations have an @override for it to be correct and
824+
// it's unclear how useful that gets.
825+
const subgraph1 = {
826+
name: "Subgraph1",
827+
url: "https://Subgraph1",
828+
typeDefs: gql`
829+
type Query {
830+
i1: I
831+
}
832+
833+
type I @interfaceObject @key(fields: "k") {
834+
k: ID
835+
a: Int
836+
}
837+
`,
838+
};
839+
840+
const subgraph2 = {
841+
name: "Subgraph2",
842+
url: "https://Subgraph2",
843+
typeDefs: gql`
844+
type Query {
845+
i2: I
846+
}
847+
848+
interface I @key(fields: "k") {
849+
k: ID
850+
a: Int
851+
}
852+
853+
type A implements I @key(fields: "k") {
854+
k: ID
855+
a: Int @override(from: "Subgraph1")
856+
}
857+
`,
858+
};
859+
860+
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
861+
expect(result.errors).toBeDefined();
862+
expect(errors(result)).toContainEqual([
863+
"OVERRIDE_COLLISION_WITH_ANOTHER_DIRECTIVE",
864+
'Invalid @override on field "A.a" of subgraph "Subgraph2": source subgraph "Subgraph1" does not have field "A.a" but abstract it in type "I" and overriding abstracted fields is not supported.',
865+
]);
866+
});
867+
});
760868
});

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

+40
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,43 @@ it('preserves the source of enum values', () => {
9090

9191
composeAndTestReversibility([s1, s2]);
9292
});
93+
94+
describe('@interfaceObject', () => {
95+
it('correctly extract external fields of concrete type only provided by an @interfaceObject', () => {
96+
const s1 = {
97+
typeDefs: gql`
98+
type Query {
99+
iFromS1: I
100+
}
101+
102+
interface I @key(fields: "id") {
103+
id: ID!
104+
x: Int
105+
}
106+
107+
type T implements I @key(fields: "id") {
108+
id: ID!
109+
x: Int @external
110+
y: Int @requires(fields: "x")
111+
}
112+
`,
113+
name: 'S1',
114+
};
115+
116+
const s2 = {
117+
typeDefs: gql`
118+
type Query {
119+
iFromS2: I
120+
}
121+
122+
type I @interfaceObject @key(fields: "id") {
123+
id: ID!
124+
x: Int
125+
}
126+
`,
127+
name: 'S2',
128+
};
129+
130+
composeAndTestReversibility([s1, s2]);
131+
});
132+
});

0 commit comments

Comments
 (0)