diff --git a/composition/src/errors/errors.ts b/composition/src/errors/errors.ts index 9a6fce564e..b74536da5e 100644 --- a/composition/src/errors/errors.ts +++ b/composition/src/errors/errors.ts @@ -181,7 +181,7 @@ export function shareableFieldDefinitionsError(parent: ObjectContainer, children } if (shareableSubgraphs.length < 1) { errorMessages.push( - `\n The field "${fieldName}" is defined in the following subgraphs: "${shareableSubgraphs.join('", "')}".` + + `\n The field "${fieldName}" is defined in the following subgraphs: "${[...field.subgraphs].join('", "')}".` + `\n However, it it is not declared "@shareable" in any of them.`, ); } else { @@ -571,4 +571,12 @@ export function invalidArgumentsError(fieldPath: string, invalidArguments: Inval `", which is not a valid input type.\n`; } return new Error(message); -} \ No newline at end of file +} + +export const noQueryRootTypeError = new Error( + `A valid federated graph must have at least one populated query root type.\n` + + ` For example:\n` + + ` type Query {\n` + + ` dummy: String\n` + + ` }` +); \ No newline at end of file diff --git a/composition/src/federation/federation-factory.ts b/composition/src/federation/federation-factory.ts index 0d010274c5..d5e5b631f3 100644 --- a/composition/src/federation/federation-factory.ts +++ b/composition/src/federation/federation-factory.ts @@ -72,6 +72,7 @@ import { invalidUnionError, minimumSubgraphRequirementError, noBaseTypeExtensionError, + noQueryRootTypeError, shareableFieldDefinitionsError, subgraphValidationError, subgraphValidationFailureErrorMessage, @@ -101,6 +102,7 @@ import { DEFAULT_SUBSCRIPTION, FIELD_NAME, INLINE_FRAGMENT, + QUERY, } from '../utils/string-constants'; import { doSetsHaveAnyOverlap, @@ -1134,6 +1136,9 @@ export class FederationFactory { container.node.interfaces = this.getAndValidateImplementedInterfaces(container); definitions.push(container.node); } + if (!this.parentMap.has(QUERY)) { + this.errors.push(noQueryRootTypeError); + } if (this.errors.length > 0) { return { errors: this.errors }; } diff --git a/composition/tests/arguments.test.ts b/composition/tests/arguments.test.ts index f59053ca43..e06fc13c83 100644 --- a/composition/tests/arguments.test.ts +++ b/composition/tests/arguments.test.ts @@ -25,12 +25,16 @@ describe('Argument federation tests', () => { subgraphWithArgument('subgraph-b', 'String'), ]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federationResult.federatedGraphAST)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: String): String - } + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: String): String + } `, ), ); @@ -44,10 +48,14 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Float!): String - } + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Float!): String + } `, ), ); @@ -59,12 +67,16 @@ describe('Argument federation tests', () => { subgraphWithArgumentAndDefaultValue('subgraph-b', 'Int', '1337'), ]); expect(errors).toBeUndefined(); - expect(documentNodeToNormalizedString(federationResult.federatedGraphAST)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Int): String - } + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Int): String + } `, ), ); @@ -78,10 +90,14 @@ describe('Argument federation tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - `type Object { - field(input: Boolean = false): String - } + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + + type Object { + field(input: Boolean = false): String + } `, ), ); @@ -144,6 +160,10 @@ describe('Argument federation tests', () => { interface Interface { field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean): String } + + type Query { + dummy: String! + } type Object implements Interface { field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean): String @@ -225,6 +245,10 @@ const subgraphWithArgument = (name: string, typeName: string): Subgraph => ({ name, url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + type Object @shareable { field(input: ${typeName}): String } @@ -235,6 +259,10 @@ const subgraphWithArgumentAndDefaultValue = (name: string, typeName: string, def name, url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + type Object @shareable { field(input: ${typeName} = ${defaultValue}): String } @@ -245,6 +273,10 @@ const subgraphA = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + interface Interface { field(requiredInAll: Int!, requiredOrOptionalInAll: String!, optionalInAll: Boolean, optionalInSome: Float): String } diff --git a/composition/tests/entities.test.ts b/composition/tests/entities.test.ts index 2b79f4340c..d340494f16 100644 --- a/composition/tests/entities.test.ts +++ b/composition/tests/entities.test.ts @@ -10,8 +10,11 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOneBaseSchema + ` + type Query { + dummy: String! + } + type Trainer { id: Int! details: Details! @@ -38,8 +41,12 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOneBaseSchema + ` + type Query { + dummy: String! + trainer: Trainer! + } + type Trainer { id: Int! details: Details! @@ -51,10 +58,6 @@ describe('Entities federation tests', () => { age: Int! } - type Query { - trainer: Trainer! - } - type Pokemon { name: String! level: Int! @@ -167,8 +170,7 @@ describe('Entities federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOneBaseSchema + ` type Trainer { id: Int! pokemon: [Pokemon!]! @@ -179,6 +181,10 @@ describe('Entities federation tests', () => { name: String! level: Int! } + + type Query { + dummy: String! + } type Details { name: String! @@ -194,6 +200,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! + } + type Trainer @key(fields: "id") { id: Int! details: Details! @@ -296,6 +306,10 @@ const subgraphG: Subgraph = { name: 'subgraph-g', url: '', definitions: parse(` + type Query { + dummy: String! + } + extend type Trainer @key(fields: "id") { id: Int! details: Details! diff --git a/composition/tests/enums.test.ts b/composition/tests/enums.test.ts index 3660c8c6a2..a54c833ff4 100644 --- a/composition/tests/enums.test.ts +++ b/composition/tests/enums.test.ts @@ -1,7 +1,7 @@ import { federateSubgraphs, incompatibleSharedEnumError, Subgraph } from '../src'; import { parse } from 'graphql'; import { describe, expect, test } from 'vitest'; -import { documentNodeToNormalizedString, normalizeString, versionOneBaseSchema } from './utils/utils'; +import { documentNodeToNormalizedString, normalizeString, versionTwoBaseSchema } from './utils/utils'; describe('Enum federation tests', () => { const parentName = 'Instruction'; @@ -12,8 +12,11 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -31,8 +34,11 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -52,8 +58,11 @@ describe('Enum federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT } @@ -69,11 +78,13 @@ describe('Enum federation tests', () => { test('that enums must be consistent if used as both an input and output', () => { const { errors, federationResult } = federateSubgraphs([subgraphC, subgraphD]); expect(errors).toBeUndefined(); - const federatedGraph = federationResult!.federatedGraphAST; - expect(documentNodeToNormalizedString(federatedGraph)).toBe( + expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionTwoBaseSchema + ` + type Query { + dummy: String! + } + enum Instruction { FIGHT POKEMON @@ -103,6 +114,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + enum Instruction { FIGHT POKEMON @@ -125,6 +140,10 @@ const subgraphC = { name: 'subgraph-c', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + enum Instruction { FIGHT POKEMON diff --git a/composition/tests/inputs.test.ts b/composition/tests/inputs.test.ts index 8d445e6b2b..ddca28cefc 100644 --- a/composition/tests/inputs.test.ts +++ b/composition/tests/inputs.test.ts @@ -10,8 +10,11 @@ describe('Input federation tests', () => { const federatedGraph = federationResult!.federatedGraphAST; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionOneBaseSchema + - ` + versionOneBaseSchema + ` + type Query { + dummy: String! + } + input TechnicalMachine { move: String! number: Int! @@ -35,6 +38,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! + } + input TechnicalMachine { move: String! number: Int! diff --git a/composition/tests/interfaces.test.ts b/composition/tests/interfaces.test.ts index 846dd16fea..a73d692d2a 100644 --- a/composition/tests/interfaces.test.ts +++ b/composition/tests/interfaces.test.ts @@ -166,14 +166,17 @@ describe('Interface tests', () => { const federatedGraph = federationResult!.federatedGraphAST!; expect(documentNodeToNormalizedString(federatedGraph)).toBe( normalizeString( - versionTwoBaseSchema + - ` + versionTwoBaseSchema + ` interface Character { name: String! age: Int! isFriend: Boolean! } + type Query { + dummy: String! + } + type Trainer implements Character { name: String! age: Int! @@ -196,8 +199,7 @@ describe('Interface tests', () => { expect(errors).toBeUndefined(); expect(documentNodeToNormalizedString(federationResult!.federatedGraphAST)).toBe( normalizeString( - versionTwoBaseSchema + - ` + versionTwoBaseSchema + ` interface Character { name: String! age: Int! @@ -208,6 +210,10 @@ describe('Interface tests', () => { name: String! } + type Query { + dummy: String! + } + type Trainer implements Character & Human { name: String! age: Int! @@ -229,6 +235,10 @@ describe('Interface tests', () => { isFriend: Boolean! } + type Query { + dummy: String! + } + interface Human implements Character { name: String! isFriend: Boolean! @@ -303,6 +313,10 @@ const subgraphA: Subgraph = { name: 'subgraph-a', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + interface Character { name: String! } @@ -343,6 +357,10 @@ const subgraphC: Subgraph = { name: 'subgraph-c', url: '', definitions: parse(` + type Query { + dummy: String! @shareable + } + interface Character { isFriend: Boolean! } @@ -382,6 +400,10 @@ const subgraphE: Subgraph = { name: 'subgraph-e', url: '', definitions: parse(` + type Query { + dummy: String! + } + interface Animal { sounds(a: String!, b: Int!): [String] } diff --git a/composition/tests/unions.test.ts b/composition/tests/unions.test.ts index fb9cfe7d5d..6786655635 100644 --- a/composition/tests/unions.test.ts +++ b/composition/tests/unions.test.ts @@ -25,6 +25,10 @@ describe('Union federation tests', () => { type Charmander { name: String! } + + type Query { + starter: Starters + } type Chikorita { name: String! @@ -66,6 +70,10 @@ const subgraphA = { type Charmander { name: String! } + + type Query { + starter: Starters + } `), }; @@ -73,6 +81,10 @@ const subgraphB = { name: 'subgraph-b', url: '', definitions: parse(` + type Query { + starter: Starters + } + union Starters = Chikorita | Totodile | Cyndaquil type Chikorita { diff --git a/controlplane/test/composition-errors.test.ts b/controlplane/test/composition-errors.test.ts index 2f6484953e..23702ff953 100644 --- a/controlplane/test/composition-errors.test.ts +++ b/controlplane/test/composition-errors.test.ts @@ -14,6 +14,7 @@ import { ImplementationErrors, incompatibleParentKindFatalError, InvalidFieldImplementation, + noQueryRootTypeError, unimplementedInterfaceFieldsError, } from '@wundergraph/composition'; import database from '../src/core/plugins/database'; @@ -302,7 +303,7 @@ describe('CompositionErrors', (ctx) => { ); }); - test.skip('Should cause composition errors if the subgraphs have no query', () => { + test('that an error is returned if the federated graph has no query root type', () => { const subgraph1 = { definitions: parse(` type TypeA { @@ -323,12 +324,11 @@ describe('CompositionErrors', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2]); + const { errors } = composeSubgraphs([subgraph1, subgraph2]); - expect(result.errors).toBeDefined(); - expect(result.errors?.[0].message).toBe( - 'No queries found in any subgraph: a supergraph must have a query root type.', - ); + expect(errors).toBeDefined(); + expect(errors).toHaveLength(1); + expect(errors?.[0]).toStrictEqual(noQueryRootTypeError); }); test('Should cause an composition error when a type and a interface are defined with the same name in different subgraphs', () => {