diff --git a/.changeset/weak-bottles-crash.md b/.changeset/weak-bottles-crash.md new file mode 100644 index 000000000..a5a428475 --- /dev/null +++ b/.changeset/weak-bottles-crash.md @@ -0,0 +1,5 @@ +--- +"@apollo/composition": patch +--- + +Stop emitting "inconsistent value type" hints against definitions where the type is marked `@external` or all fields are marked `@external`. diff --git a/composition-js/src/__tests__/hints.test.ts b/composition-js/src/__tests__/hints.test.ts index 8d3c28c1f..eb64fae62 100644 --- a/composition-js/src/__tests__/hints.test.ts +++ b/composition-js/src/__tests__/hints.test.ts @@ -1353,4 +1353,86 @@ describe('when a directive causes an implicit federation version upgrade', () => assertCompositionSuccess(result); expect(result).toNotRaiseHints(); }); -}) +}); + +describe('when a partially-defined type is marked @external or all fields are marked @external', () => { + describe('value types', () => { + it('with type marked @external', () => { + const meSubgraph = gql` + type Query { + me: Account + } + + type Account @key(fields: "id") { + id: ID! + name: String + permissions: Permissions + } + + type Permissions { + canView: Boolean + canEdit: Boolean + } + `; + + const accountSubgraph = gql` + type Query { + account: Account + } + + type Account @key(fields: "id") { + id: ID! + permissions: Permissions @external + isViewer: Boolean @requires(fields: "permissions { canView }") + } + + type Permissions @external { + canView: Boolean + } + `; + + const result = mergeDocuments(meSubgraph, accountSubgraph); + expect(result).toNotRaiseHints(); + }); + + it('with all fields marked @external', () => { + const meSubgraph = gql` + type Query { + me: Account + } + + type Account @key(fields: "id") { + id: ID! + name: String + permissions: Permissions + } + + type Permissions { + canView: Boolean + canEdit: Boolean + canDelete: Boolean + } + `; + + const accountSubgraph = gql` + type Query { + account: Account + } + + type Account @key(fields: "id") { + id: ID! + permissions: Permissions @external + isViewer: Boolean @requires(fields: "permissions { canView canEdit }") + } + + type Permissions { + canView: Boolean @external + canEdit: Boolean @external + } + `; + + const result = mergeDocuments(meSubgraph, accountSubgraph); + expect(result).toNotRaiseHints(); + }); + }); +}); diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index 2397e1187..36724a0f3 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -965,9 +965,9 @@ class Merger { typeDescription = 'interface' break; } - for (const source of sources) { + for (const [index, source] of sources.entries()) { // As soon as we find a subgraph that has the type but not the field, we hint. - if (source && !source.field(field.name)) { + if (source && !source.field(field.name) && !this.areAllFieldsExternal(index, source)) { this.mismatchReporter.reportMismatchHint({ code: hintId, message: `Field "${field.coordinate}" of ${typeDescription} type "${dest}" is defined in some but not all subgraphs that define "${dest}": `, @@ -1081,6 +1081,10 @@ class Merger { return this.metadata(sourceIdx).isFieldFullyExternal(field); } + private areAllFieldsExternal(sourceIdx: number, type: ObjectType | InterfaceType): boolean { + return type.fields().every(f => this.isExternal(sourceIdx, f)); + } + private validateAndFilterExternal(sources: (FieldDefinition | undefined)[]): (FieldDefinition | undefined)[] { const filtered: (FieldDefinition | undefined)[] = []; for (const [i, source] of sources.entries()) {