Skip to content

Commit

Permalink
Support INTERFACE_FIELD_NO_IMPLEM
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Jul 8, 2024
1 parent 2252426 commit 2934bad
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-ligers-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@theguild/federation-composition": minor
---

Support INTERFACE_FIELD_NO_IMPLEM
56 changes: 55 additions & 1 deletion __tests__/supergraph/errors/INTERFACE_FIELD_NO_IMPLEM.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect, test } from 'vitest';
import { graphql, testVersions } from '../../shared/testkit.js';

testVersions((api, version) => {
test('INTERFACE_FIELD_NO_IMPLEM', () => {
test('INTERFACE_FIELD_NO_IMPLEM (entity)', () => {
expect(
api.composeServices([
{
Expand Down Expand Up @@ -59,4 +59,58 @@ testVersions((api, version) => {
}),
);
});

test('INTERFACE_FIELD_NO_IMPLEM (data)', () => {
expect(
api.composeServices([
{
name: 'foo',
typeDefs: graphql`
type Query {
foo: Foo
}
type Foo implements Person {
name: String
age: Int
}
interface Person {
name: String
age: Int
}
`,
},
{
name: 'bar',
typeDefs: graphql`
type Query {
bar: Bar
}
type Bar implements Person {
name: String
}
interface Person {
name: String
}
`,
},
]),
).toEqual(
expect.objectContaining({
errors: expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining(
`Interface field "Person.age" is declared in subgraph "foo" but type "Bar", which implements "Person" ${api.library === 'apollo' ? 'only ' : ''}in subgraph "bar" does not have field "age".`,
),
extensions: expect.objectContaining({
code: 'INTERFACE_FIELD_NO_IMPLEM',
}),
}),
]),
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,38 @@ export function InterfaceFieldNoImplementationRule(
throw new Error('Expected interface, got ' + interfaceTypeState.kind);
}

const nonRequiredFields: string[] = [];

for (const [graph, interfaceStateInGraph] of interfaceTypeState.byGraph) {
if (!interfaceStateInGraph.isInterfaceObject) {
continue;
}

for (const [fieldName, interfaceFieldState] of interfaceTypeState.fields) {
const interfaceFieldStateInGraph = interfaceFieldState.byGraph.get(graph);
if (!interfaceFieldStateInGraph) {
continue;
}

if (interfaceFieldStateInGraph.external) {
continue;
}

nonRequiredFields.push(fieldName);
}
}

for (const [fieldName, interfaceFieldState] of interfaceTypeState.fields) {
// skip fields that are defined in interface objects or in interface entities
if (nonRequiredFields.includes(fieldName)) {
continue;
}

// TODO: detect if a field is missing in a non-entity object type definition
if (objectTypeState.fields.has(fieldName) && objectTypeState.isEntity) {
continue;
}

for (const [graph, objectTypeInGraph] of objectTypeState.byGraph) {
// check if object in the graph, implements an interface of the same name
if (!objectTypeInGraph.interfaces.has(interfaceName)) {
Expand All @@ -35,14 +66,14 @@ export function InterfaceFieldNoImplementationRule(
const objectFieldState = objectTypeState.fields.get(fieldName);

// if not, make sure it implements the field
if (!objectFieldState || !objectFieldState.byGraph.has(graph)) {
if (!objectFieldState?.byGraph.has(graph)) {
const interfaceFieldDefinedInGraphs = Array.from(
interfaceFieldState.byGraph.keys(),
).map(context.graphIdToName);
const declaredIn =
interfaceFieldDefinedInGraphs.length === 1
? `subgraph "${interfaceFieldDefinedInGraphs[0]}"`
: `subgraphs "${interfaceFieldDefinedInGraphs.map(g => `"${g}"`).join(', ')}"`;
: `subgraphs ${interfaceFieldDefinedInGraphs.map(g => `"${g}"`).join(', ')}`;

context.reportError(
new GraphQLError(
Expand Down

0 comments on commit 2934bad

Please sign in to comment.