From da811fd1f7e6437bc078526a084bbd9e548c8f1a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 21 Apr 2021 11:43:10 -0700 Subject: [PATCH 01/92] 1. move all error tests into describe('error'..., 2. make sure all test's use parse() instead of gql() so that the loc fields are populated on each type definition --- .../graphqlErrorSerializer.ts | 1 + .../__tests__/composeAndValidate.test.ts | 220 +++++++++--------- 2 files changed, 112 insertions(+), 109 deletions(-) diff --git a/federation-integration-testsuite-js/src/snapshotSerializers/graphqlErrorSerializer.ts b/federation-integration-testsuite-js/src/snapshotSerializers/graphqlErrorSerializer.ts index 3363d747d..b833d2775 100644 --- a/federation-integration-testsuite-js/src/snapshotSerializers/graphqlErrorSerializer.ts +++ b/federation-integration-testsuite-js/src/snapshotSerializers/graphqlErrorSerializer.ts @@ -10,6 +10,7 @@ export default { return print({ message: value.message, code: value.extensions ? value.extensions.code : 'MISSING_ERROR', + locations: value.locations, }); }, } as Plugin; diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index a2e7f6120..e3549eecc 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -6,6 +6,7 @@ import { GraphQLScalarType, specifiedDirectives, printSchema, + parse, } from 'graphql'; import { astSerializer, @@ -117,49 +118,6 @@ it('composes and validates all (24) permutations without error', () => { }); }); -it('errors when a type extension has no base', () => { - const serviceA = { - typeDefs: gql` - schema { - query: MyRoot - } - - type MyRoot { - products: [Product]! - } - - type Product @key(fields: "sku") { - sku: String! - upc: String! - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - extend type Location { - id: ID - } - `, - name: 'serviceB', - }; - - const compositionResult = composeAndValidate([serviceA, serviceB]); - - assertCompositionFailure(compositionResult); - - expect(compositionResult.errors).toHaveLength(1); - expect(compositionResult.errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTENSION_WITH_NO_BASE", - "message": "[serviceB] Location -> \`Location\` is an extension type, but \`Location\` is not defined in any service", - }, - ] - `); -}); - it("doesn't throw errors when a type is unknown, but captures them instead", () => { const serviceA = { typeDefs: gql` @@ -280,56 +238,6 @@ it('treats interfaces with @extends as interface extensions', () => { `); }); -it('errors on invalid usages of default operation names', () => { - const serviceA = { - typeDefs: gql` - schema { - query: RootQuery - } - - type RootQuery { - product: Product - } - - type Product @key(fields: "id") { - id: ID! - query: Query - } - - type Query { - invalidUseOfQuery: Boolean - } - `, - name: 'serviceA', - }; - - const serviceB = { - typeDefs: gql` - type Query { - validUseOfQuery: Boolean - } - - extend type Product @key(fields: "id") { - id: ID! @external - sku: String - } - `, - name: 'serviceB', - }; - - const compositionResult = composeAndValidate([serviceA, serviceB]); - - assertCompositionFailure(compositionResult); - expect(compositionResult.errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "ROOT_QUERY_USED", - "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", - }, - ] - `); -}); - describe('composition of value types', () => { function getSchemaWithValueType(valueType: DocumentNode) { const serviceA = { @@ -447,9 +355,103 @@ describe('composition of value types', () => { }); describe('errors', () => { + + it('on invalid usages of default operation names', () => { + const serviceA = { + typeDefs: parse(` + schema { + query: RootQuery + } + + type RootQuery { + product: Product + } + + type Product @key(fields: "id") { + id: ID! + query: Query + } + + type Query { + invalidUseOfQuery: Boolean + } + `), + name: 'serviceA', + }; + + const serviceB = { + typeDefs: parse(` + type Query { + validUseOfQuery: Boolean + } + + extend type Product @key(fields: "id") { + id: ID! @external + sku: String + } + `), + name: 'serviceB', + }; + + const compositionResult = composeAndValidate([serviceA, serviceB]); + + assertCompositionFailure(compositionResult); + expect(compositionResult.errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "ROOT_QUERY_USED", + "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", + }, + ] + `); + }); + + it('when a type extension has no base', () => { + const serviceA = { + typeDefs: parse(` + schema { + query: MyRoot + } + + type MyRoot { + products: [Product]! + } + + type Product @key(fields: "sku") { + sku: String! + upc: String! + } + `), + name: 'serviceA', + }; + + const serviceB = { + typeDefs: parse(` + extend type Location { + id: ID + } + `), + name: 'serviceB', + }; + + const compositionResult = composeAndValidate([serviceA, serviceB]); + + assertCompositionFailure(compositionResult); + + expect(compositionResult.errors).toHaveLength(1); + expect(compositionResult.errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTENSION_WITH_NO_BASE", + "message": "[serviceB] Location -> \`Location\` is an extension type, but \`Location\` is not defined in any service", + }, + ] + `); + }); + it('when used as an entity', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product } @@ -458,12 +460,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Query { topProducts: [Product] } @@ -472,7 +474,7 @@ describe('composition of value types', () => { sku: ID! color: String! } - `, + `), name: 'serviceB', }; @@ -490,7 +492,7 @@ describe('composition of value types', () => { it('on field type mismatch', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product } @@ -499,12 +501,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Query { topProducts: [Product] } @@ -513,7 +515,7 @@ describe('composition of value types', () => { sku: ID! color: String } - `, + `), name: 'serviceB', }; @@ -531,7 +533,7 @@ describe('composition of value types', () => { it('on kind mismatch', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product } @@ -540,12 +542,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Query { topProducts: [Product] } @@ -554,7 +556,7 @@ describe('composition of value types', () => { sku: ID! color: String! } - `, + `), name: 'serviceB', }; @@ -571,7 +573,7 @@ describe('composition of value types', () => { it('on union types mismatch', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product } @@ -585,12 +587,12 @@ describe('composition of value types', () => { } union Product = Couch | Mattress - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Query { topProducts: [Product] } @@ -604,7 +606,7 @@ describe('composition of value types', () => { } union Product = Couch | Cabinet - `, + `), name: 'serviceB', }; From 0e9cb22063f5fa0af4685f237dbf7555f9d85407 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 10:46:51 -0700 Subject: [PATCH 02/92] EXTENSION_OF_WRONG_KIND pass extension node to errorWithCode to get location & update tests --- .../sdl/__tests__/possibleTypeExtensions.test.ts | 15 +++++++++++---- .../validate/sdl/possibleTypeExtensions.ts | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts index 4c925f8b7..1b074e30c 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts @@ -4,6 +4,7 @@ import { GraphQLSchema, specifiedDirectives, extendSchema, + parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; import gql from 'graphql-tag'; @@ -115,20 +116,20 @@ describe('PossibleTypeExtensionsType', () => { it('errors when trying to extend a type with a different `Kind`', () => { const serviceList = [ { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: ID } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` input Product { id: ID! } - `, + `), name: 'serviceB', }, ]; @@ -143,6 +144,12 @@ describe('PossibleTypeExtensionsType', () => { Array [ Object { "code": "EXTENSION_OF_WRONG_KIND", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + ], "message": "[serviceA] Product -> \`Product\` was originally defined as a InputObjectTypeDefinition and can only be extended by a InputObjectTypeExtension. serviceA defines Product as a ObjectTypeExtension", }, ] diff --git a/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts index f7e37850a..6992a8304 100644 --- a/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts +++ b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts @@ -58,6 +58,7 @@ export function PossibleTypeExtensions( 'EXTENSION_OF_WRONG_KIND', logServiceAndType(serviceName, typeName) + `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, + node, ), ); } @@ -70,6 +71,7 @@ export function PossibleTypeExtensions( 'EXTENSION_OF_WRONG_KIND', logServiceAndType(serviceName, typeName) + `\`${typeName}\` was originally defined as a ${baseKind} and can only be extended by a ${expectedKind}. ${serviceName} defines ${typeName} as a ${node.kind}`, + node, ), ); } From 5b68ef1d5bf902767a5e01686e7a7d7d896dfb3d Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 10:49:40 -0700 Subject: [PATCH 03/92] EXTENSION_WITH_NO_BASE pass extension node to errorWithCode to get location & update tests --- .../__tests__/composeAndValidate.test.ts | 16 ++++++++++++++-- .../sdl/__tests__/possibleTypeExtensions.test.ts | 10 ++++++++-- .../validate/sdl/possibleTypeExtensions.ts | 1 + 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index e3549eecc..abb4807b4 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -120,7 +120,7 @@ it('composes and validates all (24) permutations without error', () => { it("doesn't throw errors when a type is unknown, but captures them instead", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { foo: Bar! } @@ -129,7 +129,7 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () id: ID! @external thing: String } - `, + `), name: 'serviceA', }; @@ -148,6 +148,12 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () }, Object { "code": "EXTENSION_WITH_NO_BASE", + "locations": Array [ + Object { + "column": 7, + "line": 6, + }, + ], "message": "[serviceA] Bar -> \`Bar\` is an extension type, but \`Bar\` is not defined in any service", }, Object { @@ -443,6 +449,12 @@ describe('composition of value types', () => { Array [ Object { "code": "EXTENSION_WITH_NO_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + ], "message": "[serviceB] Location -> \`Location\` is an extension type, but \`Location\` is not defined in any service", }, ] diff --git a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts index 1b074e30c..909593133 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts @@ -86,11 +86,11 @@ describe('PossibleTypeExtensionsType', () => { it('errors when there is an extension with no base', () => { const serviceList = [ { - typeDefs: gql` + typeDefs: parse(` extend type Product { id: ID! } - `, + `), name: 'serviceA', }, ]; @@ -107,6 +107,12 @@ describe('PossibleTypeExtensionsType', () => { Array [ Object { "code": "EXTENSION_WITH_NO_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + ], "message": "[serviceA] Product -> \`Product\` is an extension type, but \`Product\` is not defined in any service", }, ] diff --git a/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts index 6992a8304..0d6d04cf5 100644 --- a/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts +++ b/federation-js/src/composition/validate/sdl/possibleTypeExtensions.ts @@ -81,6 +81,7 @@ export function PossibleTypeExtensions( 'EXTENSION_WITH_NO_BASE', logServiceAndType(serviceName, typeName) + `\`${typeName}\` is an extension type, but \`${typeName}\` is not defined in any service`, + node, ), ); } From cbb82969a83a0f17ab8893fd63c22626502a3364 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 10:56:02 -0700 Subject: [PATCH 04/92] KEY_FIELDS_SELECT_INVALID_TYPE add field node to errorWithCode to get location & update tests --- .../keyFieldsSelectInvalidType.test.ts | 21 +++++++++++++++---- .../keyFieldsSelectInvalidType.ts | 3 +++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index f37d66aec..756127c3a 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -3,6 +3,7 @@ import { composeServices } from '../../../compose'; import { keyFieldsSelectInvalidType as validateKeyFieldsSelectInvalidType } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -64,12 +65,12 @@ describe('keyFieldsSelectInvalidType', () => { }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: String! @external price: Int! @requires(fields: "sku") } - `, + `), name: 'serviceB', }; @@ -86,6 +87,12 @@ describe('keyFieldsSelectInvalidType', () => { Array [ Object { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", + "locations": Array [ + Object { + "column": 9, + "line": 1, + }, + ], "message": "[serviceA] Product -> A @key selects Product.featuredItem, which is an interface type. Keys cannot select interfaces.", }, ] @@ -106,12 +113,12 @@ describe('keyFieldsSelectInvalidType', () => { }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: String! @external name: String! } - `, + `), name: 'serviceB', }; @@ -128,6 +135,12 @@ describe('keyFieldsSelectInvalidType', () => { Array [ Object { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", + "locations": Array [ + Object { + "column": 9, + "line": 1, + }, + ], "message": "[serviceA] Product -> A @key selects Product.price, which is a union type. Keys cannot select union types.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index c011fb00e..acaec97da 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -42,6 +42,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} could not be found`, + field, ), ); } @@ -57,6 +58,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, + field, ), ); } @@ -71,6 +73,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, + field, ), ); } From cd4971d0e49967858537d9d37943a68afe781616 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 10:59:59 -0700 Subject: [PATCH 05/92] KEY_FIELDS_MISSING_ON_BASE add field node on errorWithCode to get locations & update test --- .../__tests__/keyFieldsMissingOnBase.test.ts | 15 +++++++++++---- .../postComposition/keyFieldsMissingOnBase.ts | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index 2901c3bc4..073cb083e 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -3,6 +3,7 @@ import { composeServices } from '../../../compose'; import { keyFieldsMissingOnBase as validateKeyFieldsMissingOnBase } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -46,23 +47,23 @@ describe('keyFieldsMissingOnBase', () => { it('warns if @key references a field added by another service', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku uid") { sku: String! upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { uid: String! sku: String! @external price: Int! @requires(fields: "sku") } - `, + `), name: 'serviceB', }; @@ -76,6 +77,12 @@ describe('keyFieldsMissingOnBase', () => { Array [ Object { "code": "KEY_FIELDS_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 13, + "line": 1, + }, + ], "message": "[serviceA] Product -> A @key selects uid, but Product.uid was either created or overwritten by serviceB, not serviceA", }, ] diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index fa34bd3a8..52fe873da 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -38,6 +38,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ 'KEY_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, + field, ), ); } From 965b73a84e995849684e467759a5f0f8fb4167a2 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:15:21 -0700 Subject: [PATCH 06/92] KEY_FIELDS_MISSING_EXTERNAL add field node or parent node on errorWithCode to get location & update test --- .../keyFieldsMissingExternal.test.ts | 43 ++++++++++++++++--- .../keyFieldsMissingExternal.ts | 4 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts index d024db3a9..59dd47197 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { keyFieldsMissingExternal as validateKeyFieldsMissingExternal } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -84,7 +85,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument doesn't reference an @external field", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! upc: String! @@ -95,7 +96,7 @@ describe('keyFieldsMissingExternal', () => { id: ID! value: String! } - `, + `), name: 'serviceA', }; @@ -105,6 +106,12 @@ describe('keyFieldsMissingExternal', () => { Array [ Object { "code": "KEY_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceA] Product -> A @key directive specifies the \`sku\` field which has no matching @external field.", }, ] @@ -113,7 +120,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument references a field that isn't known", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { upc: String! @external color: Color! @@ -123,7 +130,7 @@ describe('keyFieldsMissingExternal', () => { id: ID! value: String! } - `, + `), name: 'serviceA', }; @@ -133,6 +140,12 @@ describe('keyFieldsMissingExternal', () => { Array [ Object { "code": "KEY_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 35, + "line": 1, + }, + ], "message": "[serviceA] Product -> A @key directive specifies a field which is not found in this service. Add a field to this type with @external.", }, ] @@ -141,7 +154,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument doesn't reference an @external field", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` extend type Car @key(fields: "model { name kit { upc } } year") { model: Model! @external year: String! @external @@ -155,7 +168,7 @@ describe('keyFieldsMissingExternal', () => { type Kit { upc: String! } - `, + `), name: 'serviceA', }; @@ -165,14 +178,32 @@ describe('keyFieldsMissingExternal', () => { Array [ Object { "code": "KEY_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 8, + }, + ], "message": "[serviceA] Model -> A @key directive specifies the \`name\` field which has no matching @external field.", }, Object { "code": "KEY_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 9, + }, + ], "message": "[serviceA] Model -> A @key directive specifies the \`kit\` field which has no matching @external field.", }, Object { "code": "KEY_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 13, + }, + ], "message": "[serviceA] Kit -> A @key directive specifies the \`upc\` field which has no matching @external field.", }, ] diff --git a/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts index be0bffd76..ddfb0e178 100644 --- a/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts @@ -76,7 +76,7 @@ export const keyFieldsMissingExternal = ({ visit( keyDirectiveSelectionSet, visitWithTypeInfo(typeInfo, { - Field() { + Field(node) { const fieldDef = typeInfo.getFieldDef(); const parentType = typeInfo.getParentType(); if (parentType) { @@ -87,6 +87,7 @@ export const keyFieldsMissingExternal = ({ 'KEY_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, parentType.name) + `A @key directive specifies a field which is not found in this service. Add a field to this type with @external.`, + node, ), ); return; @@ -102,6 +103,7 @@ export const keyFieldsMissingExternal = ({ 'KEY_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, parentType.name) + `A @key directive specifies the \`${fieldDef.name}\` field which has no matching @external field.`, + fieldDef.astNode || undefined, ), ); } From 321e8caea42e3e8d68f0d4c34c6c8f6e4263893e Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:19:00 -0700 Subject: [PATCH 07/92] KEY_MISSING_ON_BASE add parent node to errorWithCode to get location & update test --- .../__tests__/keysMatchBaseService.test.ts | 15 +++++++++++---- .../postComposition/keysMatchBaseService.ts | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index 0d76733aa..b8a76fe13 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -3,6 +3,7 @@ import { composeServices } from '../../../compose'; import { keysMatchBaseService as validateKeysMatchBaseService } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -42,22 +43,22 @@ describe('keysMatchBaseService', () => { it('requires a @key to be specified on the originating type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product { sku: String! upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external price: Int! } - `, + `), name: 'serviceB', }; @@ -74,6 +75,12 @@ describe('keysMatchBaseService', () => { expect(validationErrors[0]).toMatchInlineSnapshot(` Object { "code": "KEY_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], "message": "[serviceA] Product -> appears to be an entity but no @key directives are specified on the originating type.", } `); diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index ae0cf8adc..c9aedf3ad 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -33,6 +33,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'KEY_MISSING_ON_BASE', logServiceAndType(serviceName, parentTypeName) + `appears to be an entity but no @key directives are specified on the originating type.`, + parentType.astNode || undefined, ), ); continue; From 594baa6c249bf0ae867c0eb395f8e95ee487e91c Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:28:14 -0700 Subject: [PATCH 08/92] MULTIPLE_KEYS_ON_EXTENSION add parentType node to errorWithCode to get locations & add test --- .../__tests__/keysMatchBaseService.test.ts | 46 +++++++++++++++++++ .../postComposition/keysMatchBaseService.ts | 1 + 2 files changed, 47 insertions(+) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index b8a76fe13..a18e9bc52 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -86,6 +86,52 @@ describe('keysMatchBaseService', () => { `); }); + it('requires an extending service use only one @key specified on the originating type', () => { + const serviceA = { + typeDefs: parse(` + type Product @key(fields: "sku") @key(fields: "upc") { + sku: String! + upc: String! + } + `), + name: 'serviceA', + }; + + const serviceB = { + typeDefs: parse(` + extend type Product @key(fields: "sku") @key(fields: "upc") { + sku: String! @external + upc: String! @external + price: Int! + } + `), + name: 'serviceB', + }; + + const serviceList = [serviceA, serviceB]; + const compositionResult = composeServices(serviceList); + assertCompositionSuccess(compositionResult); + const { schema } = compositionResult; + + const validationErrors = validateKeysMatchBaseService({ + schema, + serviceList, + }); + expect(validationErrors).toHaveLength(1); + expect(validationErrors[0]).toMatchInlineSnapshot(` + Object { + "code": "MULTIPLE_KEYS_ON_EXTENSION", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], + "message": "[serviceB] Product -> is extended from service serviceA but specifies multiple @key directives. Extensions may only specify one @key.", + } + `); + }); + it('requires extending services to use a @key specified by the originating type', () => { const serviceA = { typeDefs: gql` diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index c9aedf3ad..f651c6b23 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -51,6 +51,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'MULTIPLE_KEYS_ON_EXTENSION', logServiceAndType(extendingService, parentTypeName) + `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, + parentType.astNode || undefined, ), ); return; From 554282011f974808f90f8bdd6a93b171d87b533b Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:48:09 -0700 Subject: [PATCH 09/92] KEY_NOT_SPECIFIED add parent type node to errorWithCode to get locations & update test --- .../__tests__/keysMatchBaseService.test.ts | 14 ++++++++++---- .../postComposition/keysMatchBaseService.ts | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index a18e9bc52..537b6dc03 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -134,22 +134,22 @@ describe('keysMatchBaseService', () => { it('requires extending services to use a @key specified by the originating type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku upc") { sku: String! upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external price: Int! } - `, + `), name: 'serviceB', }; @@ -166,6 +166,12 @@ describe('keysMatchBaseService', () => { expect(validationErrors[0]).toMatchInlineSnapshot(` Object { "code": "KEY_NOT_SPECIFIED", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], "message": "[serviceB] Product -> extends from serviceA but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are: @key(fields: \\"sku upc\\")", } diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index f651c6b23..3d8e4543e 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -71,6 +71,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ `\t${availableKeys .map((fieldSet) => `@key(fields: "${fieldSet}")`) .join('\n\t')}`, + parentType.astNode || undefined ), ); return; From c820bfe173377ecb0d8b73e26e7e325017f9bc49 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:52:20 -0700 Subject: [PATCH 10/92] EXTERNAL_UNUSED add field node to errorWithCode to get locations & update tests --- .../__tests__/externalUnused.test.ts | 29 ++++++++++++++----- .../postComposition/externalUnused.ts | 1 + 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts index 16b9a399e..a74b95ebb 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -2,30 +2,31 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { externalUnused as validateExternalUnused } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); describe('externalUnused', () => { it('warns when there is an unused @external field', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "id") { sku: String! upc: String! id: ID! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: String! @external id: ID! @external price: Int! @requires(fields: "id") } - `, + `), name: 'serviceB', }; @@ -36,6 +37,12 @@ describe('externalUnused', () => { Array [ Object { "code": "EXTERNAL_UNUSED", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceB] Product.sku -> is marked as @external but is not used by a @requires, @key, or @provides directive.", }, ] @@ -338,7 +345,7 @@ describe('externalUnused', () => { it('does error when @external is used on a field of a concrete type is not shared by its implemented interface', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Car implements Vehicle @key(fields: "id") { id: ID! speed: Int @@ -348,11 +355,11 @@ describe('externalUnused', () => { id: ID! speed: Int } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Car implements Vehicle @key(fields: "id") { id: ID! @external speed: Int @external @@ -362,7 +369,7 @@ describe('externalUnused', () => { id: ID! speed: Int } - `, + `), name: 'serviceB', }; const serviceList = [serviceA, serviceB]; @@ -372,6 +379,12 @@ describe('externalUnused', () => { Array [ Object { "code": "EXTERNAL_UNUSED", + "locations": Array [ + Object { + "column": 11, + "line": 5, + }, + ], "message": "[serviceB] Car.wheelSize -> is marked as @external but is not used by a @requires, @key, or @provides directive.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/externalUnused.ts b/federation-js/src/composition/validate/postComposition/externalUnused.ts index 1b10c1c8d..6334381db 100644 --- a/federation-js/src/composition/validate/postComposition/externalUnused.ts +++ b/federation-js/src/composition/validate/postComposition/externalUnused.ts @@ -226,6 +226,7 @@ export const externalUnused: PostCompositionValidator = ({ schema }) => { externalFieldName, ) + `is marked as @external but is not used by a @requires, @key, or @provides directive.`, + externalField, ), ); } From d702a35aa969f4f2988efd2284b63e409a8dde45 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 11:59:24 -0700 Subject: [PATCH 11/92] EXTERNAL_TYPE_MISMATCH add external field node to get locations & update tests --- .../__tests__/externalTypeMismatch.test.ts | 36 ++++++++++++++----- .../postComposition/externalTypeMismatch.ts | 2 ++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts index 8d3d4d29d..46d7b1d97 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts @@ -1,31 +1,31 @@ -import gql from 'graphql-tag'; import { externalTypeMismatch as validateExternalTypeMismatch } from '../'; import { composeServices } from '../../../compose'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); describe('validateExternalDirectivesOnSchema', () => { it('warns when the type of an @external field doesnt match the base', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku skew") { sku: String! skew: String upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: String @external skew: String! @external price: Int! @requires(fields: "sku skew") } - `, + `), name: 'serviceB', }; @@ -36,10 +36,22 @@ describe('validateExternalDirectivesOnSchema', () => { Array [ Object { "code": "EXTERNAL_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceB] Product.sku -> Type \`String\` does not match the type of the original field in serviceA (\`String!\`)", }, Object { "code": "EXTERNAL_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.skew -> Type \`String!\` does not match the type of the original field in serviceA (\`String\`)", }, ] @@ -48,22 +60,22 @@ describe('validateExternalDirectivesOnSchema', () => { it("warns when an @external field's type does not exist in the composed schema", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: NonExistentType! @external id: String! @requires(fields: "sku") } - `, + `), name: 'serviceB', }; @@ -74,6 +86,12 @@ describe('validateExternalDirectivesOnSchema', () => { Array [ Object { "code": "EXTERNAL_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceB] Product.sku -> the type of the @external field does not exist in the resulting composed schema", }, ] diff --git a/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts index 234760790..dc859748b 100644 --- a/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts +++ b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts @@ -42,6 +42,7 @@ export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_TYPE_MISMATCH', logServiceAndType(serviceName, typeName, externalFieldName) + `the type of the @external field does not exist in the resulting composed schema`, + externalField, ), ); } else if ( @@ -53,6 +54,7 @@ export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_TYPE_MISMATCH', logServiceAndType(serviceName, typeName, externalFieldName) + `Type \`${externalFieldType}\` does not match the type of the original field in ${typeFederationMetadata.serviceName} (\`${matchingBaseField.type}\`)`, + externalField, ), ); } From e7d11d96c5fb016d8b1f8829573152c2382e2110 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 12:02:56 -0700 Subject: [PATCH 12/92] EXTERNAL_MISSING_ON_BASE add external field node on errorWithCode to get locations & update tests --- .../__tests__/externalMissingOnBase.test.ts | 35 ++++++++++++++----- .../postComposition/externalMissingOnBase.ts | 2 ++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts index 1cd0c1386..b505b2abc 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { externalMissingOnBase as validateExternalMissingOnBase } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -18,24 +19,24 @@ describe('externalMissingOnBase', () => { }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external id: String! @external price: Int! @requires(fields: "sku id") } - `, + `), name: 'serviceB', }; const serviceC = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external id: String! test: Int @external } - `, + `), name: 'serviceC', }; @@ -46,10 +47,22 @@ describe('externalMissingOnBase', () => { Array [ Object { "code": "EXTERNAL_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.id -> marked @external but id was defined in serviceC, not in the service that owns Product (serviceA)", }, Object { "code": "EXTERNAL_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 5, + }, + ], "message": "[serviceC] Product.test -> marked @external but test is not defined on the base service of Product (serviceA)", }, ] @@ -58,22 +71,22 @@ describe('externalMissingOnBase', () => { it("warns when an @external field isn't defined anywhere else", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { specialId: String! @external id: String! @requires(fields: "specialId") } - `, + `), name: 'serviceB', }; @@ -84,6 +97,12 @@ describe('externalMissingOnBase', () => { Array [ Object { "code": "EXTERNAL_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceB] Product.specialId -> marked @external but specialId is not defined on the base service of Product (serviceA)", }, ] diff --git a/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts index cb5218ffb..5b5c4a3b6 100644 --- a/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts @@ -35,6 +35,7 @@ export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, externalFieldName) + `marked @external but ${externalFieldName} is not defined on the base service of ${typeName} (${typeFederationMetadata.serviceName})`, + externalField, ), ); continue; @@ -50,6 +51,7 @@ export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, externalFieldName) + `marked @external but ${externalFieldName} was defined in ${fieldFederationMetadata.serviceName}, not in the service that owns ${typeName} (${typeFederationMetadata.serviceName})`, + externalField, ), ); } From ed1d4133ed1bb5daec3cb141a6f24bc91b861d9e Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 12:07:26 -0700 Subject: [PATCH 13/92] EXTERNAL_USED_ON_BASE add field node to errorWithCode to get locations & update test --- .../__tests__/externalUsedOnBase.test.ts | 11 +++++++++-- .../validate/preComposition/externalUsedOnBase.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts index c8506f303..b3431ec3e 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { externalUsedOnBase as validateExternalUsedOnBase } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -28,13 +29,13 @@ describe('externalUsedOnBase', () => { it('warns when there is a @external field on a base type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @external id: ID! } - `, + `), name: 'serviceA', }; @@ -43,6 +44,12 @@ describe('externalUsedOnBase', () => { Array [ Object { "code": "EXTERNAL_USED_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Product.upc -> Found extraneous @external directive. @external cannot be used on base types.", }, ] diff --git a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts index ddb2b2bd0..cd7a6bda6 100644 --- a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts @@ -29,6 +29,7 @@ export const externalUsedOnBase = ({ field.name.value, ) + `Found extraneous @external directive. @external cannot be used on base types.`, + field, ), ); } From 6cdff46f683f8363ef435b6c692d880f06e2f502 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:01:15 -0700 Subject: [PATCH 14/92] PROVIDES_FIELDS_MISSING_EXTERNAL add field astnode to errorWithCode to get locations & update test --- .../providesFieldsMissingExternals.test.ts | 15 +++++++++++---- .../providesFieldsMissingExternal.ts | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts index 7e544e4bf..0ea1ee65c 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts @@ -3,6 +3,7 @@ import { composeServices } from '../../../compose'; import { providesFieldsMissingExternal as validateProdivesFieldsMissingExternal } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -64,18 +65,18 @@ describe('providesFieldsMissingExternal', () => { it('warns when there is a @provides with no matching @external field', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! id: ID! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Review @key(fields: "id") { id: ID! product: Product @provides(fields: "id") @@ -85,7 +86,7 @@ describe('providesFieldsMissingExternal', () => { sku: String! @external price: Int! } - `, + `), name: 'serviceB', }; @@ -101,6 +102,12 @@ describe('providesFieldsMissingExternal', () => { Array [ Object { "code": "PROVIDES_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Review.product -> provides the field \`id\` and requires Product.id to be marked as @external.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts index 6a4cb2eef..00b4a5336 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts @@ -48,6 +48,7 @@ export const providesFieldsMissingExternal: PostCompositionValidator = ({ 'PROVIDES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `provides the field \`${selection.name.value}\` and requires ${fieldType}.${selection.name.value} to be marked as @external.`, + field.astNode || undefined, ), ); } From 20038021e385dbe1293e02d7ba242f5f058e4bd6 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:04:35 -0700 Subject: [PATCH 15/92] PROVIDES_NOT_ON_ENTITY add field ast node on errorWithCode to get locations & update tests --- .../__tests__/providesNotOnEntity.test.ts | 57 +++++++++++++------ .../postComposition/providesNotOnEntity.ts | 2 + 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts index 34a41c5ad..e9a87f834 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { providesNotOnEntity as validateProvidesNotOnEntity } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -95,7 +96,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a type that is not an entity', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @@ -106,17 +107,17 @@ describe('providesNotOnEntity', () => { sku: String! quantity: Int! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external lineItem: LineItem @provides(fields: "quantity") } - `, + `), name: 'serviceB', }; @@ -127,6 +128,12 @@ describe('providesNotOnEntity', () => { Array [ Object { "code": "PROVIDES_NOT_ON_ENTITY", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.lineItem -> uses the @provides directive but \`Product.lineItem\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", }, ] @@ -135,7 +142,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a type that is not a list of entity', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @@ -146,17 +153,17 @@ describe('providesNotOnEntity', () => { sku: String! quantity: Int! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external lineItems: [LineItem] @provides(fields: "quantity") } - `, + `), name: 'serviceB', }; @@ -167,6 +174,12 @@ describe('providesNotOnEntity', () => { Array [ Object { "code": "PROVIDES_NOT_ON_ENTITY", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.lineItems -> uses the @provides directive but \`Product.lineItems\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.", }, ] @@ -175,7 +188,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a non-object type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @@ -188,17 +201,17 @@ describe('providesNotOnEntity', () => { SONG ALBUM } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external category: Category @provides(fields: "id") } - `, + `), name: 'serviceB', }; @@ -209,6 +222,12 @@ describe('providesNotOnEntity', () => { Array [ Object { "code": "PROVIDES_NOT_ON_ENTITY", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.category -> uses the @provides directive but \`Product.category\` returns \`Category\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", }, ] @@ -217,7 +236,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a list of non-object type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @@ -230,17 +249,17 @@ describe('providesNotOnEntity', () => { SONG ALBUM } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { sku: String! @external categories: [Category] @provides(fields: "id") } - `, + `), name: 'serviceB', }; @@ -251,6 +270,12 @@ describe('providesNotOnEntity', () => { Array [ Object { "code": "PROVIDES_NOT_ON_ENTITY", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceB] Product.categories -> uses the @provides directive but \`Product.categories\` returns \`[Category]\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts index 6fd8f362a..a00d1e376 100644 --- a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts +++ b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts @@ -52,6 +52,7 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`, + field.astNode || undefined, ), ); continue; @@ -66,6 +67,7 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`, + field.astNode || undefined, ), ); } From a88b5e8a1a053c6c01057565074b643090df7932 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:08:34 -0700 Subject: [PATCH 16/92] PROVIDES_FIELDS_SELECT_INVALID_TYPE add field ast node on errorWithCode to get locations & update tests --- .../providesFieldsSelectInvalidType.test.ts | 43 +++++++++++++------ .../providesFieldsSelectInvalidType.ts | 4 ++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts index 70275b957..7592bd9c8 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts @@ -3,6 +3,7 @@ import { composeServices } from '../../../compose'; import { providesFieldsSelectInvalidType as validateprovidesFieldsSelectInvalidType } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -48,7 +49,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of a list type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "wishLists") @@ -62,12 +63,12 @@ describe('providesFieldsSelectInvalidType', () => { extend type WishList @key(fields: "id") { id: ID! @external } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type User @key(fields: "id") { id: ID! wishLists: [WishList] @@ -76,7 +77,7 @@ describe('providesFieldsSelectInvalidType', () => { type WishList @key(fields: "id") { id: ID! } - `, + `), name: 'serviceB', }; @@ -93,6 +94,12 @@ describe('providesFieldsSelectInvalidType', () => { Array [ Object { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Review.author -> A @provides selects User.wishLists, which is a list type. A field cannot @provide lists.", }, ] @@ -101,7 +108,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of an interface type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "account") @@ -115,12 +122,12 @@ describe('providesFieldsSelectInvalidType', () => { extend interface Account { username: String @external } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type User @key(fields: "id") { id: ID! account: Account @@ -129,7 +136,7 @@ describe('providesFieldsSelectInvalidType', () => { interface Account { username: String } - `, + `), name: 'serviceB', }; @@ -146,6 +153,12 @@ describe('providesFieldsSelectInvalidType', () => { Array [ Object { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Review.author -> A @provides selects User.account, which is an interface type. A field cannot @provide interfaces.", }, ] @@ -154,7 +167,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of a union type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "account") @@ -174,12 +187,12 @@ describe('providesFieldsSelectInvalidType', () => { extend type SMSAccount @key(fields: "phone") { phone: String! @external } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type User @key(fields: "id") { id: ID! account: Account @@ -194,7 +207,7 @@ describe('providesFieldsSelectInvalidType', () => { type SMSAccount @key(fields: "phone") { phone: String! } - `, + `), name: 'serviceB', }; @@ -211,6 +224,12 @@ describe('providesFieldsSelectInvalidType', () => { Array [ Object { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Review.author -> A @provides selects User.account, which is a union type. A field cannot @provide union types.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts index 8b308475e..e257c4933 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -52,6 +52,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, + field.astNode || undefined, ), ); continue; @@ -67,6 +68,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, + field.astNode || undefined, ), ); } @@ -80,6 +82,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, + field.astNode || undefined, ), ); } @@ -94,6 +97,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, + field.astNode || undefined, ), ); } From 24cf58735593e546cfe5bb44a1b521fbb9e8f520 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:12:17 -0700 Subject: [PATCH 17/92] REQUIRES_FIELDS_MISSING_EXTERNAL add field ast node to errorWithCode call to get locations & update tests --- .../requiresFieldsMissingExternals.test.ts | 15 +++++++++++---- .../requiresFieldsMissingExternal.ts | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts index 64587378b..56cc4a589 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { requiresFieldsMissingExternal as validateRequiresFieldsMissingExternal } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -39,22 +40,22 @@ describe('requiresFieldsMissingExternal', () => { it('warns when there is a @requires with no matching @external field', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! id: ID! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { price: Int! @requires(fields: "id") } - `, + `), name: 'serviceB', }; @@ -68,6 +69,12 @@ describe('requiresFieldsMissingExternal', () => { Array [ Object { "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", + "locations": Array [ + Object { + "column": 11, + "line": 3, + }, + ], "message": "[serviceB] Product.price -> requires the field \`id\` to be marked as @external.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index 02c049f69..85cc34b30 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -45,6 +45,7 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be marked as @external.`, + field.astNode || undefined, ), ); } From f9ab2cae2a181e3fa5f98bb32dfb5c77ea3cf8c6 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:15:14 -0700 Subject: [PATCH 18/92] REQUIRES_FIELDS_MISSING_ON_BASE add field astnode to errorWithCode to get locations & update test --- .../requiresFieldsMissingOnBase.test.ts | 19 +++++++++++++------ .../requiresFieldsMissingOnBase.ts | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts index 5a9aa4e58..5d0707e52 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { requiresFieldsMissingOnBase as validateRequiresFieldsMissingOnBase } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -37,28 +38,28 @@ describe('requiresFieldsMissingOnBase', () => { it('warns when requires selects a field not found on the base type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { id: ID! } - `, + `), name: 'serviceB', }; const serviceC = { - typeDefs: gql` + typeDefs: parse(` extend type Product @key(fields: "sku") { id: ID! @external weight: Float! @requires(fields: "id") } - `, + `), name: 'serviceC', }; const serviceList = [serviceA, serviceB, serviceC]; @@ -71,6 +72,12 @@ describe('requiresFieldsMissingOnBase', () => { Array [ Object { "code": "REQUIRES_FIELDS_MISSING_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceC] Product.weight -> requires the field \`id\` to be @external. @external fields must exist on the base type, not an extension.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 401dcd667..7d7bd010b 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -42,6 +42,7 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, + field.astNode || undefined, ), ); } From 951dc976f4ea93c44e7fe1cafa82f15c01dc14ae Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:17:12 -0700 Subject: [PATCH 19/92] REQUIRES_USED_ON_BASE add field node to errorWithCode to get locations & update test --- .../__tests__/requiresUsedOnBase.test.ts | 11 +++++++++-- .../validate/preComposition/requiresUsedOnBase.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts index 8af1564b0..aa5aca82f 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { requiresUsedOnBase as validateRequiresUsedOnBase } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -28,13 +29,13 @@ describe('requiresUsedOnBase', () => { it('warns when there is a @requires field on a base type', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: String! upc: String! @requires(fields: "sku") id: ID! } - `, + `), name: 'serviceA', }; @@ -43,6 +44,12 @@ describe('requiresUsedOnBase', () => { Array [ Object { "code": "REQUIRES_USED_ON_BASE", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Product.upc -> Found extraneous @requires directive. @requires cannot be used on base types.", }, ] diff --git a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts index a70a32b35..db39fbbff 100644 --- a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts @@ -29,6 +29,7 @@ export const requiresUsedOnBase = ({ field.name.value, ) + `Found extraneous @requires directive. @requires cannot be used on base types.`, + field, ), ); } From ae74390881514f23eb0cc80b95e7bcb136864a74 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:20:26 -0700 Subject: [PATCH 20/92] EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES add directive node to errorWithCode to get locations & update test --- .../executableDirectivesInAllServices.test.ts | 19 +++++++++++++------ .../executableDirectivesInAllServices.ts | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts index 7cffdc588..4cd4263ed 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { executableDirectivesInAllServices } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -52,27 +53,27 @@ describe('executableDirectivesInAllServices', () => { it("throws errors when custom, executable directives aren't defined in every service", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` directive @stream on FIELD - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Query { thing: String } - `, + `), name: 'serviceB', }; const serviceC = { - typeDefs: gql` + typeDefs: parse(` extend type Query { otherThing: String } - `, + `), name: 'serviceC', }; @@ -83,6 +84,12 @@ describe('executableDirectivesInAllServices', () => { Array [ Object { "code": "EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], "message": "[@stream] -> Custom directives must be implemented in every service. The following services do not implement the @stream directive: serviceB, serviceC.", }, ] diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts index 7f548ee05..46e8b812a 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts @@ -50,6 +50,7 @@ export const executableDirectivesInAllServices: PostCompositionValidator = ({ `Custom directives must be implemented in every service. The following services do not implement the @${ directive.name } directive: ${serviceNamesWithoutDirective.join(', ')}.`, + directive.astNode || undefined, ), ); } From 0858b2f642334396ffb1c9ca3837356eb1dffc67 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:24:13 -0700 Subject: [PATCH 21/92] EXECUTABLE_DIRECTIVES_IDENTICAL add directive node to errorWithCode to get locations & update test --- .../executableDirectivesIdentical.test.ts | 33 +++++++++++++------ .../executableDirectivesIdentical.ts | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts index 1c1ebf7f2..bfe50d330 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { executableDirectivesIdentical } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -54,23 +55,23 @@ describe('executableDirectivesIdentical', () => { it("throws errors when custom, executable directives aren't defined with the same locations in every service", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` directive @stream on FIELD - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` directive @stream on FIELD | QUERY - `, + `), name: 'serviceB', }; const serviceC = { - typeDefs: gql` + typeDefs: parse(` directive @stream on INLINE_FRAGMENT - `, + `), name: 'serviceC', }; @@ -81,6 +82,12 @@ describe('executableDirectivesIdentical', () => { Array [ Object { "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], "message": "[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: serviceA: directive @stream on FIELD serviceB: directive @stream on FIELD | QUERY @@ -92,16 +99,16 @@ describe('executableDirectivesIdentical', () => { it("throws errors when custom, executable directives aren't defined with the same arguments in every service", () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` directive @instrument(tag: String!) on FIELD - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` directive @instrument(tag: Boolean) on FIELD - `, + `), name: 'serviceB', }; @@ -112,6 +119,12 @@ describe('executableDirectivesIdentical', () => { Array [ Object { "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", + "locations": Array [ + Object { + "column": 9, + "line": 2, + }, + ], "message": "[@instrument] -> custom directives must be defined identically across all services. See below for a list of current implementations: serviceA: directive @instrument(tag: String!) on FIELD serviceB: directive @instrument(tag: Boolean) on FIELD", diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts index 8dd0b4387..fe695ce8e 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts @@ -51,6 +51,7 @@ export const executableDirectivesIdentical: PostCompositionValidator = ({ return `\t${serviceName}: ${print(definition)}`; }) .join('\n')}`, + directive.astNode || undefined, ), ); } From c77733fe81e5565c0c3031d76bf1e882f96bb7e2 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:30:09 -0700 Subject: [PATCH 22/92] DUPLICATE_SCALAR_DEFINITION add duplicate scalar node to errorWithCode to get locations & update test --- .../__tests__/duplicateEnumOrScalar.test.ts | 11 +++++++++-- .../validate/preComposition/duplicateEnumOrScalar.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts index ebadfe6a6..90bad25c2 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { duplicateEnumOrScalar as validateDuplicateEnumOrScalar } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -71,7 +72,7 @@ describe('duplicateEnumOrScalar', () => { it('errors when there are multiple definitions of the same scalar', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` scalar Date type Product @key(fields: "color { id value }") { sku: String! @@ -80,7 +81,7 @@ describe('duplicateEnumOrScalar', () => { } scalar Date - `, + `), name: 'serviceA', }; @@ -89,6 +90,12 @@ describe('duplicateEnumOrScalar', () => { Array [ Object { "code": "DUPLICATE_SCALAR_DEFINITION", + "locations": Array [ + Object { + "column": 9, + "line": 9, + }, + ], "message": "[serviceA] Date -> The scalar, \`Date\` was defined multiple times in this service. Remove one of the definitions for \`Date\`", }, ] diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts index b37520564..19f686493 100644 --- a/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts @@ -37,6 +37,7 @@ export const duplicateEnumOrScalar = ({ 'DUPLICATE_SCALAR_DEFINITION', logServiceAndType(serviceName, name) + `The scalar, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, + definition, ), ); return definition; From 47a4495ae11c87882eb034351b79176a52000b98 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:31:24 -0700 Subject: [PATCH 23/92] DUPLICATE_ENUM_DEFINITION add duplicate enum definition to errorWithCode to get locations & update test --- .../__tests__/duplicateEnumOrScalar.test.ts | 10 ++++++++-- .../validate/preComposition/duplicateEnumOrScalar.ts | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts index 90bad25c2..75d5aa726 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts @@ -35,7 +35,7 @@ describe('duplicateEnumOrScalar', () => { }); it('errors when there are multiple definitions of the same enum', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "color { id value }") { sku: String! upc: String! @@ -55,7 +55,7 @@ describe('duplicateEnumOrScalar', () => { enum ProductType { DIGITAL } - `, + `), name: 'serviceA', }; @@ -64,6 +64,12 @@ describe('duplicateEnumOrScalar', () => { Array [ Object { "code": "DUPLICATE_ENUM_DEFINITION", + "locations": Array [ + Object { + "column": 9, + "line": 18, + }, + ], "message": "[serviceA] ProductType -> The enum, \`ProductType\` was defined multiple times in this service. Remove one of the definitions for \`ProductType\`", }, ] diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts index 19f686493..386afc31b 100644 --- a/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumOrScalar.ts @@ -22,6 +22,7 @@ export const duplicateEnumOrScalar = ({ 'DUPLICATE_ENUM_DEFINITION', logServiceAndType(serviceName, name) + `The enum, \`${name}\` was defined multiple times in this service. Remove one of the definitions for \`${name}\``, + definition, ), ); return definition; From 4d4c8628331364e2ad566292fd195beb264fdda5 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:32:51 -0700 Subject: [PATCH 24/92] DUPLICATE_ENUM_VALUE add enum type def / extension node to errorWithCode to get locations & update tests --- .../__tests__/duplicateEnumValue.test.ts | 11 +++++++++-- .../validate/preComposition/duplicateEnumValue.ts | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts index 8700bcb66..6f8944b96 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { duplicateEnumValue as validateDuplicateEnumValue } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -36,7 +37,7 @@ describe('duplicateEnumValue', () => { }); it('errors when there are duplicate enum values in a single service', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "color { id value }") { sku: String! upc: String! @@ -57,7 +58,7 @@ describe('duplicateEnumValue', () => { DIGITAL BOOK } - `, + `), name: 'serviceA', }; @@ -66,6 +67,12 @@ describe('duplicateEnumValue', () => { Array [ Object { "code": "DUPLICATE_ENUM_VALUE", + "locations": Array [ + Object { + "column": 9, + "line": 18, + }, + ], "message": "[serviceA] ProductType.BOOK -> The enum, \`ProductType\` has multiple definitions of the \`BOOK\` value.", }, ] diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts index 5e83eb417..679377d5c 100644 --- a/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts @@ -27,6 +27,7 @@ export const duplicateEnumValue = ({ 'DUPLICATE_ENUM_VALUE', logServiceAndType(serviceName, name, valueName) + `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, + definition, ), ); return; @@ -54,6 +55,7 @@ export const duplicateEnumValue = ({ 'DUPLICATE_ENUM_VALUE', logServiceAndType(serviceName, name, valueName) + `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, + definition, ), ); return; From 45740f87292b136a2ff2f2acce4846734b811a16 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 13:47:55 -0700 Subject: [PATCH 25/92] ENUM_MISMATCH & ENUM_MISMATCH_TYPE - add enum type node to errorsWithCode to get the location & update tests need to figure out why the tests don't print the errors as I expect --- .../sdl/__tests__/matchingEnums.test.ts | 71 +++++++++++++------ .../composition/validate/sdl/matchingEnums.ts | 4 ++ 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts index 18b53803b..9b395635b 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts @@ -3,6 +3,7 @@ import { DocumentNode, GraphQLSchema, specifiedDirectives, + parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; import gql from 'graphql-tag'; @@ -11,6 +12,7 @@ import { astSerializer, typeSerializer, selectionSetSerializer, + graphqlErrorSerializer, } from 'apollo-federation-integration-testsuite'; import federationDirectives from '../../../../directives'; import { ServiceDefinition } from '../../../types'; @@ -19,6 +21,7 @@ import { MatchingEnums } from '../matchingEnums'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(typeSerializer); expect.addSnapshotSerializer(selectionSetSerializer); +expect.addSnapshotSerializer(graphqlErrorSerializer); // simulate the first half of the composition process const createDefinitionsDocumentForServices = ( @@ -75,20 +78,20 @@ describe('matchingEnums', () => { it('errors when enums in separate services dont match', () => { const serviceList = [ { - typeDefs: gql` + typeDefs: parse(` enum ProductCategory { BED BATH } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` enum ProductCategory { BEYOND } - `, + `), name: 'serviceB', }, ]; @@ -99,7 +102,16 @@ describe('matchingEnums', () => { const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: The \`ProductCategory\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB]], + Object { + "code": "ENUM_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + ], + "message": "The \`ProductCategory\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB]", + }, ] `); }); @@ -107,7 +119,7 @@ describe('matchingEnums', () => { it('errors when enums in separate services dont match', () => { const serviceList = [ { - typeDefs: gql` + typeDefs: parse(` type Query { products: [Product]! } @@ -122,27 +134,27 @@ describe('matchingEnums', () => { BOOK FURNITURE } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` enum ProductType { FURNITURE BOOK DIGITAL } - `, + `), name: 'serviceB', }, { - typeDefs: gql` + typeDefs: parse(` enum ProductType { FURNITURE BOOK DIGITAL } - `, + `), name: 'serviceC', }, ]; @@ -153,38 +165,48 @@ describe('matchingEnums', () => { const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: The \`ProductType\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB, serviceC]], + Object { + "code": "ENUM_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 12, + }, + ], + "message": "The \`ProductType\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB, serviceC]", + }, ] - `); + `, + ); }); it('errors when an enum name is defined as another type in a service', () => { const serviceList = [ { - typeDefs: gql` + typeDefs: parse(` enum ProductType { BOOK FURNITURE } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type ProductType { id: String } - `, + `), name: 'serviceB', }, { - typeDefs: gql` + typeDefs: parse(` enum ProductType { FURNITURE BOOK DIGITAL } - `, + `), name: 'serviceC', }, ]; @@ -195,7 +217,16 @@ describe('matchingEnums', () => { const errors = validateSDL(definitionsDocument, schema, [MatchingEnums]); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: [serviceA] ProductType -> ProductType is an enum in [serviceA, serviceC], but not in [serviceB]], + Object { + "code": "ENUM_MISMATCH_TYPE", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + ], + "message": "[serviceA] ProductType -> ProductType is an enum in [serviceA, serviceC], but not in [serviceB]", + }, ] `); }); diff --git a/federation-js/src/composition/validate/sdl/matchingEnums.ts b/federation-js/src/composition/validate/sdl/matchingEnums.ts index bb162963a..38eec13e2 100644 --- a/federation-js/src/composition/validate/sdl/matchingEnums.ts +++ b/federation-js/src/composition/validate/sdl/matchingEnums.ts @@ -88,6 +88,8 @@ export function MatchingEnums(context: SDLValidationContext): ASTVisitor { ) .map(serviceNames => `[${serviceNames.join(', ')}]`) .join(', ')}`, + // the location on this error will be the first definition of this enum + definitions[0], ), ); } @@ -113,6 +115,8 @@ export function MatchingEnums(context: SDLValidationContext): ASTVisitor { `${name} is an enum in [${servicesWithEnum.join( ', ', )}], but not in [${servicesWithoutEnum.join(', ')}]`, + // the location on this error will be the first definition of this enum + definitions[0], ), ); } From 0b79f145a2857591a0b72df6913447ba44f9191d Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:03:27 -0700 Subject: [PATCH 26/92] RESERVED_FIELD_USED add field node to errorWithCode to get locations & update tests --- .../__tests__/reservedFieldUsed.test.ts | 53 ++++++++++++++++--- .../preComposition/reservedFieldUsed.ts | 1 + 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts index 4b380b54c..8505f3ca2 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { reservedFieldUsed as validateReservedFieldUsed } from '..'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -25,7 +26,7 @@ describe('reservedFieldUsed', () => { it('warns when _service or _entities is used at the query root', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product _service: String! @@ -35,7 +36,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `, + `), name: 'serviceA', }; @@ -44,10 +45,22 @@ describe('reservedFieldUsed', () => { Array [ Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 4, + }, + ], "message": "[serviceA] Query._service -> _service is a field reserved for federation and can't be used at the Query root.", }, Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 5, + }, + ], "message": "[serviceA] Query._entities -> _entities is a field reserved for federation and can't be used at the Query root.", }, ] @@ -56,7 +69,7 @@ describe('reservedFieldUsed', () => { it('warns when _service or _entities is used in a schema extension', () => { const schemaDefinition = { - typeDefs: gql` + typeDefs: parse(` schema { query: RootQuery } @@ -69,12 +82,12 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `, + `), name: 'schemaDefinition', }; const schemaExtension = { - typeDefs: gql` + typeDefs: parse(` extend schema { query: RootQuery } @@ -87,7 +100,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `, + `), name: 'schemaExtension', }; @@ -100,6 +113,12 @@ describe('reservedFieldUsed', () => { Array [ Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 8, + }, + ], "message": "[schemaDefinition] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", }, ] @@ -108,6 +127,12 @@ describe('reservedFieldUsed', () => { Array [ Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 7, + }, + ], "message": "[schemaExtension] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", }, ] @@ -116,7 +141,7 @@ describe('reservedFieldUsed', () => { it('warns when reserved fields are used on custom Query types', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` schema { query: RootQuery } @@ -130,7 +155,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `, + `), name: 'serviceA', }; @@ -141,10 +166,22 @@ describe('reservedFieldUsed', () => { Array [ Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 8, + }, + ], "message": "[serviceA] RootQuery._service -> _service is a field reserved for federation and can't be used at the Query root.", }, Object { "code": "RESERVED_FIELD_USED", + "locations": Array [ + Object { + "column": 11, + "line": 9, + }, + ], "message": "[serviceA] RootQuery._entities -> _entities is a field reserved for federation and can't be used at the Query root.", }, ] diff --git a/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts b/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts index 0434f7bd2..6a86a271b 100644 --- a/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts +++ b/federation-js/src/composition/validate/preComposition/reservedFieldUsed.ts @@ -36,6 +36,7 @@ export const reservedFieldUsed = ({ 'RESERVED_FIELD_USED', logServiceAndType(serviceName, rootQueryName, fieldName) + `${fieldName} is a field reserved for federation and can\'t be used at the Query root.`, + field, ), ); } From 1a6b1d7925303a153aaa7fb6bf1ca2213c22c746 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:07:50 -0700 Subject: [PATCH 27/92] ROOT_QUERY_USED ROOT_MUTATION_USED ROOT_SUBSCRIPTION_USED add node to errorWithCode to get locations & update tests --- .../__tests__/composeAndValidate.test.ts | 6 ++++++ .../__tests__/rootFieldUsed.test.ts | 21 +++++++++++++++---- .../preNormalization/rootFieldUsed.ts | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index abb4807b4..3668de8cf 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -406,6 +406,12 @@ describe('composition of value types', () => { Array [ Object { "code": "ROOT_QUERY_USED", + "locations": Array [ + Object { + "column": 11, + "line": 15, + }, + ], "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", }, ] diff --git a/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts index 4de0b8610..afae07fe6 100644 --- a/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts +++ b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { rootFieldUsed as validateRootFieldUsed } from '../'; import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -67,7 +68,7 @@ describe('rootFieldUsed', () => { it('warns when a schema definition / extension is provided, as well as a default root type or extension', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` schema { query: RootQuery } @@ -83,7 +84,7 @@ describe('rootFieldUsed', () => { type Query { invalidUseOfQuery: Boolean } - `, + `), name: 'serviceA', }; @@ -95,6 +96,12 @@ describe('rootFieldUsed', () => { Array [ Object { "code": "ROOT_QUERY_USED", + "locations": Array [ + Object { + "column": 9, + "line": 14, + }, + ], "message": "[serviceA] Query -> Found invalid use of default root operation name \`Query\`. \`Query\` is disallowed when \`Schema.query\` is set to a type other than \`Query\`.", }, ] @@ -103,7 +110,7 @@ describe('rootFieldUsed', () => { it('warns against using default operation type names (Query, Mutation, Subscription) when a non-default operation type name is provided in the schema definition', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` schema { mutation: RootMutation } @@ -115,7 +122,7 @@ describe('rootFieldUsed', () => { type Mutation { invalidUseOfMutation: Boolean } - `, + `), name: 'serviceA', }; @@ -126,6 +133,12 @@ describe('rootFieldUsed', () => { Array [ Object { "code": "ROOT_MUTATION_USED", + "locations": Array [ + Object { + "column": 9, + "line": 10, + }, + ], "message": "[serviceA] Mutation -> Found invalid use of default root operation name \`Mutation\`. \`Mutation\` is disallowed when \`Schema.mutation\` is set to a type other than \`Mutation\`.", }, ] diff --git a/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts b/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts index 1e6bdc139..ff2d49101 100644 --- a/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts +++ b/federation-js/src/composition/validate/preNormalization/rootFieldUsed.ts @@ -71,6 +71,7 @@ export const rootFieldUsed = ({ `ROOT_${rootOperationName.toUpperCase()}_USED`, logServiceAndType(serviceName, rootOperationName) + `Found invalid use of default root operation name \`${rootOperationName}\`. \`${rootOperationName}\` is disallowed when \`Schema.${rootOperationName.toLowerCase()}\` is set to a type other than \`${rootOperationName}\`.`, + node, ), ); } From b23a9f7f9d7a10090e08e48c1e607aea07a25630 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:16:13 -0700 Subject: [PATCH 28/92] VALUE_TYPE_FIELD_TYPE_MISMATCH nodes were already passed, both subgraph nodes = locations for both subgraphs in location. updated tests --- .../__tests__/composeAndValidate.test.ts | 10 +++++++ .../uniqueTypeNamesWithFields.test.ts | 29 ++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index 3668de8cf..cae8a16ee 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -544,6 +544,16 @@ describe('composition of value types', () => { expect(compositionResult.errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 6, + }, + Object { + "column": 11, + "line": 6, + }, + ], "message": "[serviceA] Product.color -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.color\` as a String! and String respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", } `); diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index 7d7ce6d82..defebb6fa 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -3,6 +3,7 @@ import { specifiedDirectives, Kind, DocumentNode, + parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; import gql from 'graphql-tag'; @@ -80,23 +81,23 @@ describe('UniqueTypeNamesWithFields', () => { it('object type definitions (non-identical, value types with type mismatch)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` type Product { sku: ID! color: String quantity: Int } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type Product { sku: String! color: String quantity: Int! } - `, + `), name: 'serviceB', }, ]); @@ -109,10 +110,30 @@ describe('UniqueTypeNamesWithFields', () => { Array [ Object { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] Product.sku -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.sku\` as a ID! and String! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", }, Object { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] Product.quantity -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.quantity\` as a Int and Int! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", }, ] From 1cbfa5f5f5c72003241673af6b180935687a489f Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:19:23 -0700 Subject: [PATCH 29/92] VALUE_TYPE_INPUT_VALUE_MISMATCH nodes were already passed to errorWithCodes = locations correspond to both services. updated tests --- .../uniqueTypeNamesWithFields.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index defebb6fa..be1fc5cbd 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -143,19 +143,19 @@ describe('UniqueTypeNamesWithFields', () => { it('object type definitions (non-identical, field input value types with type mismatch)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` type Person { age(relative: Boolean!): Int } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type Person { age(relative: Boolean): Int } - `, + `), name: 'serviceB', }, ]); @@ -168,6 +168,16 @@ describe('UniqueTypeNamesWithFields', () => { Array [ Object { "code": "VALUE_TYPE_INPUT_VALUE_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] Person -> A field's input type (\`relative\`) was defined differently in different services. \`serviceA\` and \`serviceB\` define \`relative\` as a Boolean! and Boolean respectively. In order to define \`Person\` in multiple places, the input values and their types must be identical.", }, ] From 586a3fecf912201bdf28c8d08a1e4541196e125c Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:23:27 -0700 Subject: [PATCH 30/92] VALUE_TYPE_NO_ENTITY already passed 2 nodes to errorWithCode = locations for each service. updated tests --- .../__tests__/composeAndValidate.test.ts | 10 ++++++ .../uniqueTypeNamesWithFields.test.ts | 36 ++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index cae8a16ee..e94cf7994 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -503,6 +503,16 @@ describe('composition of value types', () => { expect(compositionResult.errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_NO_ENTITY", + "locations": Array [ + Object { + "column": 11, + "line": 6, + }, + Object { + "column": 11, + "line": 6, + }, + ], "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", } `); diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index be1fc5cbd..231357464 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -516,19 +516,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types cannot be entities (part 1)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: ID } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type Product { sku: ID } - `, + `), name: 'serviceB', }, ]); @@ -540,6 +540,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_NO_ENTITY", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", } `); @@ -548,19 +558,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types cannot be entities (part 2)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` type Product { sku: ID } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type Product @key(fields: "sku") { sku: ID } - `, + `), name: 'serviceB', }, ]); @@ -572,6 +582,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_NO_ENTITY", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceB] Product -> Value types cannot be entities (using the \`@key\` directive). Please ensure that the \`Product\` type is extended properly or remove the \`@key\` directive if this is not an entity.", } `); From 4b839f9e9eff8745d9ad1a9cb438d597b7f01b21 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:26:43 -0700 Subject: [PATCH 31/92] VALUE_TYPE_UNION_TYPES_MISMATCH nodes already passed = 2 locations, one for each subgraph. Updated tests --- .../__tests__/composeAndValidate.test.ts | 10 ++++++++++ .../sdl/__tests__/matchingUnions.test.ts | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index e94cf7994..bdd0b5a6f 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -654,6 +654,16 @@ describe('composition of value types', () => { expect(compositionResult.errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 14, + }, + Object { + "column": 11, + "line": 14, + }, + ], "message": "[serviceA] Product -> The union \`Product\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The types Cabinet, Mattress are mismatched.", } `); diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts index a0d41bbe0..8fd0de4f1 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts @@ -3,6 +3,7 @@ import { specifiedDirectives, Kind, DocumentNode, + parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; import gql from 'graphql-tag'; @@ -50,7 +51,7 @@ describe('MatchingUnions', () => { it('enforces unique union names on non-identical union types', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` union ProductOrError = Product | Error type Error { @@ -61,11 +62,11 @@ describe('MatchingUnions', () => { type Product @key(fields: "sku") { sku: ID! } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` union ProductOrError = Product type Error { @@ -77,7 +78,7 @@ describe('MatchingUnions', () => { sku: ID! @external colors: [String] } - `, + `), name: 'serviceB', }, ]); @@ -87,6 +88,16 @@ describe('MatchingUnions', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 2, + }, + Object { + "column": 11, + "line": 2, + }, + ], "message": "[serviceA] ProductOrError -> The union \`ProductOrError\` is defined in services \`serviceA\` and \`serviceB\`, however their types do not match. Union types with the same name must also consist of identical types. The type Error is mismatched.", } `); From b720ded895a697986455fc4a7469f3be0ab973f0 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:38:02 -0700 Subject: [PATCH 32/92] VALUE_TYPE_KIND_MISMATCH passed both subgraph nodes, so location shows both - updated tests --- .../__tests__/composeAndValidate.test.ts | 10 +++ .../uniqueTypeNamesWithFields.test.ts | 72 ++++++++++++++----- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index bdd0b5a6f..9cf4bd195 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -604,6 +604,16 @@ describe('composition of value types', () => { expect(compositionResult.errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_KIND_MISMATCH", + "locations": Array [ + Object { + "column": 11, + "line": 6, + }, + Object { + "column": 11, + "line": 6, + }, + ], "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InterfaceTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", } `); diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index 231357464..842d4b0bf 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -381,19 +381,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` input Product { sku: ID } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type Product { sku: ID } - `, + `), name: 'serviceB', }, ]); @@ -406,6 +406,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_KIND_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] Product -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`Product\` is defined as both a \`ObjectTypeDefinition\` and a \`InputObjectTypeDefinition\`. In order to define \`Product\` in multiple places, the kinds must be identical.", } `); @@ -414,19 +424,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (scalar)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` scalar DateTime - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type DateTime { day: Int formatted: String # ... } - `, + `), name: 'serviceB', }, ]); @@ -439,6 +449,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_KIND_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`ScalarTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", } `); @@ -447,19 +467,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (union)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` union DateTime = Date | Time - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type DateTime { day: Int formatted: String # ... } - `, + `), name: 'serviceB', }, ]); @@ -472,6 +492,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_KIND_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`UnionTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", } `); @@ -480,22 +510,22 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (enum)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: gql` + typeDefs: parse(` enum DateTime { DATE TIME } - `, + `), name: 'serviceA', }, { - typeDefs: gql` + typeDefs: parse(` type DateTime { day: Int formatted: String # ... } - `, + `), name: 'serviceB', }, ]); @@ -508,6 +538,16 @@ describe('UniqueTypeNamesWithFields', () => { expect(errors[0]).toMatchInlineSnapshot(` Object { "code": "VALUE_TYPE_KIND_MISMATCH", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 13, + "line": 2, + }, + ], "message": "[serviceA] DateTime -> Found kind mismatch on expected value type belonging to services \`serviceA\` and \`serviceB\`. \`DateTime\` is defined as both a \`ObjectTypeDefinition\` and a \`EnumTypeDefinition\`. In order to define \`DateTime\` in multiple places, the kinds must be identical.", } `); From a212a3e89564f547f80d8666a677110a3833f10e Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 14:42:35 -0700 Subject: [PATCH 33/92] MISSING_ERROR edit remaining composeAndValidate errors using parse --- .../src/composition/__tests__/composeAndValidate.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index 9cf4bd195..50e64ebf1 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -144,6 +144,12 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () Array [ Object { "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 14, + "line": 3, + }, + ], "message": "Unknown type \\"Bar\\".", }, Object { @@ -158,6 +164,7 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () }, Object { "code": "MISSING_ERROR", + "locations": Array [], "message": "Type Query must define one or more fields.", }, ] From d4eb5e8d8a4f881d4455ebb854d54abefa6ca42f Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 22 Apr 2021 16:13:18 -0700 Subject: [PATCH 34/92] update compose.test.ts with locations & graphqlErrorSerializer --- .../src/composition/__tests__/compose.test.ts | 224 ++++++++++++------ 1 file changed, 153 insertions(+), 71 deletions(-) diff --git a/federation-js/src/composition/__tests__/compose.test.ts b/federation-js/src/composition/__tests__/compose.test.ts index 1c2ecf0a3..79cafebe7 100644 --- a/federation-js/src/composition/__tests__/compose.test.ts +++ b/federation-js/src/composition/__tests__/compose.test.ts @@ -2,6 +2,7 @@ import { GraphQLObjectType, isSpecifiedDirective, GraphQLDirective, + parse, } from 'graphql'; import gql from 'graphql-tag'; import { composeServices } from '../compose'; @@ -9,6 +10,7 @@ import { astSerializer, typeSerializer, selectionSetSerializer, + graphqlErrorSerializer, } from 'apollo-federation-integration-testsuite'; import { normalizeTypeDefs } from '../normalize'; import { @@ -20,6 +22,7 @@ import { expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(typeSerializer); expect.addSnapshotSerializer(selectionSetSerializer); +expect.addSnapshotSerializer(graphqlErrorSerializer); describe('composeServices', () => { it('should include types from different services', () => { @@ -124,9 +127,9 @@ describe('composeServices', () => { const product = schema.getType('Product') as GraphQLObjectType; expect(getFederationMetadata(product)?.serviceName).toEqual('serviceA'); - expect(getFederationMetadata(product.getFields()['price'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['price'])?.serviceName, + ).toEqual('serviceB'); }); it('works when extension service is first', () => { @@ -164,9 +167,9 @@ describe('composeServices', () => { const product = schema.getType('Product') as GraphQLObjectType; expect(getFederationMetadata(product)?.serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price'])?.serviceName).toEqual( - 'serviceA', - ); + expect( + getFederationMetadata(product.getFields()['price'])?.serviceName, + ).toEqual('serviceA'); }); it('works with multiple extensions on the same type', () => { @@ -215,41 +218,41 @@ describe('composeServices', () => { const product = schema.getType('Product') as GraphQLObjectType; expect(getFederationMetadata(product)?.serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price'])?.serviceName).toEqual( - 'serviceA', - ); - expect(getFederationMetadata(product.getFields()['color'])?.serviceName).toEqual( - 'serviceC', - ); + expect( + getFederationMetadata(product.getFields()['price'])?.serviceName, + ).toEqual('serviceA'); + expect( + getFederationMetadata(product.getFields()['color'])?.serviceName, + ).toEqual('serviceC'); }); it('allows extensions to overwrite other extension fields', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` extend type Product { price: Int! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Product { sku: String! name: String! } - `, + `), name: 'serviceB', }; const serviceC = { - typeDefs: gql` + typeDefs: parse(` extend type Product { price: Float! color: String! } - `, + `), name: 'serviceC', }; @@ -257,10 +260,23 @@ describe('composeServices', () => { assertCompositionFailure(compositionResult); const { errors, schema } = compositionResult; expect(errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Field "Product.price" can only be defined once.], - ] - `); + Array [ + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 13, + "line": 3, + }, + Object { + "column": 13, + "line": 3, + }, + ], + "message": "Field \\"Product.price\\" can only be defined once.", + }, + ] + `); expect(schema).toBeDefined(); const product = schema.getType('Product') as GraphQLObjectType; @@ -274,9 +290,9 @@ describe('composeServices', () => { `); expect(getFederationMetadata(product)?.serviceName).toEqual('serviceB'); - expect(getFederationMetadata(product.getFields()['price'])?.serviceName).toEqual( - 'serviceC', - ); + expect( + getFederationMetadata(product.getFields()['price'])?.serviceName, + ).toEqual('serviceC'); }); it('preserves arguments for fields', () => { @@ -360,32 +376,32 @@ describe('composeServices', () => { name: String! } `); - expect(getFederationMetadata(product.getFields()['sku'])?.serviceName).toEqual( - 'serviceB', - ); - expect(getFederationMetadata(product.getFields()['name'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['sku'])?.serviceName, + ).toEqual('serviceB'); + expect( + getFederationMetadata(product.getFields()['name'])?.serviceName, + ).toEqual('serviceB'); }); describe('collisions & error handling', () => { it('handles collisions on type extensions as expected', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product { sku: String! name: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { name: String! } - `, + `), name: 'serviceB', }; @@ -395,7 +411,16 @@ describe('composeServices', () => { expect(schema).toBeDefined(); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 15, + "line": 3, + }, + ], + "message": "[serviceA] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + }, ] `); @@ -407,14 +432,14 @@ describe('composeServices', () => { name: String! } `); - expect(getFederationMetadata(product.getFields()['name'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['name'])?.serviceName, + ).toEqual('serviceB'); }); it('reports multiple errors correctly', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Query { product: Product } @@ -423,17 +448,17 @@ describe('composeServices', () => { sku: String! name: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { sku: String! name: String! } - `, + `), name: 'serviceB', }; @@ -443,8 +468,26 @@ describe('composeServices', () => { expect(schema).toBeDefined(); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: [serviceA] Product.sku -> Field "Product.sku" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], - [GraphQLError: [serviceA] Product.name -> Field "Product.name" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 15, + "line": 3, + }, + ], + "message": "[serviceA] Product.sku -> Field \\"Product.sku\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + }, + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 15, + "line": 4, + }, + ], + "message": "[serviceA] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + }, ] `); @@ -456,30 +499,30 @@ describe('composeServices', () => { name: String! } `); - expect(getFederationMetadata(product.getFields()['name'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['name'])?.serviceName, + ).toEqual('serviceB'); }); it('handles collisions of base types as expected (newest takes precedence)', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` type Product { sku: String! name: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` type Product { id: ID! name: String! price: Int! } - `, + `), name: 'serviceB', }; @@ -489,8 +532,34 @@ describe('composeServices', () => { expect(schema).toBeDefined(); expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: Field "Product.name" can only be defined once.], - [GraphQLError: There can be only one type named "Product".], + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 15, + "line": 4, + }, + Object { + "column": 15, + "line": 4, + }, + ], + "message": "Field \\"Product.name\\" can only be defined once.", + }, + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 13, + "line": 2, + }, + Object { + "column": 18, + "line": 2, + }, + ], + "message": "There can be only one type named \\"Product\\".", + }, ] `); @@ -567,7 +636,7 @@ describe('composeServices', () => { // TODO: should there be a validation warning of some sort for this? it('allows overwriting a type that implements an interface improperly', () => { const serviceA = { - typeDefs: gql` + typeDefs: parse(` interface Item { id: ID! } @@ -577,16 +646,16 @@ describe('composeServices', () => { sku: String! name: String! } - `, + `), name: 'serviceA', }; const serviceB = { - typeDefs: gql` + typeDefs: parse(` extend type Product { id: String! } - `, + `), name: 'serviceB', }; @@ -595,7 +664,16 @@ describe('composeServices', () => { const { errors, schema } = compositionResult; expect(errors).toMatchInlineSnapshot(` Array [ - [GraphQLError: [serviceA] Product.id -> Field "Product.id" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.], + Object { + "code": "MISSING_ERROR", + "locations": Array [ + Object { + "column": 13, + "line": 3, + }, + ], + "message": "[serviceA] Product.id -> Field \\"Product.id\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + }, ] `); expect(schema).toBeDefined(); @@ -611,9 +689,9 @@ describe('composeServices', () => { const product = schema.getType('Product') as GraphQLObjectType; expect(getFederationMetadata(product)?.serviceName).toEqual('serviceA'); - expect(getFederationMetadata(product.getFields()['id'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['id'])?.serviceName, + ).toEqual('serviceB'); }); }); @@ -826,7 +904,8 @@ describe('composeServices', () => { const product = schema.getType('Product')!; - expect(getFederationMetadata(product)?.externals).toMatchInlineSnapshot(` + expect(getFederationMetadata(product)?.externals) + .toMatchInlineSnapshot(` Object { "serviceB--MISSING": Array [ Object { @@ -885,9 +964,9 @@ describe('composeServices', () => { price: Int! } `); - expect(getFederationMetadata(product.getFields()['price'])?.serviceName).toEqual( - 'serviceB', - ); + expect( + getFederationMetadata(product.getFields()['price'])?.serviceName, + ).toEqual('serviceB'); expect(getFederationMetadata(product)?.serviceName).toEqual('serviceA'); }); }); @@ -994,7 +1073,8 @@ describe('composeServices', () => { const { schema } = compositionResult; const review = schema.getType('Review') as GraphQLObjectType; - expect(getFederationMetadata(review.getFields()['product'])).toMatchInlineSnapshot(` + expect(getFederationMetadata(review.getFields()['product'])) + .toMatchInlineSnapshot(` Object { "belongsToValueType": false, "provides": sku, @@ -1126,7 +1206,9 @@ describe('composeServices', () => { const { schema } = compositionResult; const valueType = schema.getType('ValueType') as GraphQLObjectType; - const userFieldFederationMetadata = getFederationMetadata(valueType.getFields()['user']); + const userFieldFederationMetadata = getFederationMetadata( + valueType.getFields()['user'], + ); expect(userFieldFederationMetadata?.belongsToValueType).toBe(true); expect(userFieldFederationMetadata?.serviceName).toBe(null); }); @@ -1351,7 +1433,7 @@ describe('composeServices', () => { it('keeps executable directives in the schema', () => { const serviceA = { typeDefs: gql` - directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT `, name: 'serviceA', }; @@ -1367,13 +1449,13 @@ describe('composeServices', () => { it('keeps executable directives in the schema', () => { const serviceA = { typeDefs: gql` - directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @defer on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT `, name: 'serviceA', }; const serviceB = { typeDefs: gql` - directive @stream on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @stream on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT `, name: 'serviceB', }; From b806b4c74303f5445d5615a57fd2df2b4df3c9d8 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:14:48 -0700 Subject: [PATCH 35/92] expose all locations for ENUM_MISMATCH errors, rather than only the first --- .../src/composition/validate/sdl/matchingEnums.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/matchingEnums.ts b/federation-js/src/composition/validate/sdl/matchingEnums.ts index 38eec13e2..a4f81e385 100644 --- a/federation-js/src/composition/validate/sdl/matchingEnums.ts +++ b/federation-js/src/composition/validate/sdl/matchingEnums.ts @@ -88,8 +88,8 @@ export function MatchingEnums(context: SDLValidationContext): ASTVisitor { ) .map(serviceNames => `[${serviceNames.join(', ')}]`) .join(', ')}`, - // the location on this error will be the first definition of this enum - definitions[0], + // the locations on this error will correspond to the index in the service list + definitions, ), ); } @@ -115,8 +115,8 @@ export function MatchingEnums(context: SDLValidationContext): ASTVisitor { `${name} is an enum in [${servicesWithEnum.join( ', ', )}], but not in [${servicesWithoutEnum.join(', ')}]`, - // the location on this error will be the first definition of this enum - definitions[0], + // the locations on this error will correspond to the index of the service list + definitions, ), ); } From 5792613f1341d3d602cadd98f18fdf08f01e6729 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:24:17 -0700 Subject: [PATCH 36/92] install strip-indent --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4b9473404..a00077653 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "prettier": "2.2.1", "rollup": "2.45.2", "rollup-plugin-node-polyfills": "0.2.1", + "strip-indent": "^3.0.0", "ts-jest": "26.5.5", "typescript": "4.2.4", "winston": "3.3.3", From d2189809001982802afaf42bb15fc84e097c5ff9 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:26:44 -0700 Subject: [PATCH 37/92] write test util function gql with parse and strip indent --- federation-integration-testsuite-js/src/index.ts | 1 + .../src/utils/gql.ts | 15 +++++++++++++++ .../src/utils/index.ts | 1 + 3 files changed, 17 insertions(+) create mode 100644 federation-integration-testsuite-js/src/utils/gql.ts create mode 100644 federation-integration-testsuite-js/src/utils/index.ts diff --git a/federation-integration-testsuite-js/src/index.ts b/federation-integration-testsuite-js/src/index.ts index efcca38f1..b70b35a92 100644 --- a/federation-integration-testsuite-js/src/index.ts +++ b/federation-integration-testsuite-js/src/index.ts @@ -1,2 +1,3 @@ export * from './snapshotSerializers'; export * from './fixtures'; +export * from './utils'; diff --git a/federation-integration-testsuite-js/src/utils/gql.ts b/federation-integration-testsuite-js/src/utils/gql.ts new file mode 100644 index 000000000..bc7dce500 --- /dev/null +++ b/federation-integration-testsuite-js/src/utils/gql.ts @@ -0,0 +1,15 @@ +import { parse } from "graphql"; +import stripIndent from 'strip-indent'; + +export function gql( + literals: string | readonly string[], +) { + + if (typeof literals === 'string') { + literals = [literals]; + } + + let result = literals[0]; + + return parse(stripIndent(result)); +} diff --git a/federation-integration-testsuite-js/src/utils/index.ts b/federation-integration-testsuite-js/src/utils/index.ts new file mode 100644 index 000000000..bc8c0798e --- /dev/null +++ b/federation-integration-testsuite-js/src/utils/index.ts @@ -0,0 +1 @@ +export { gql } from './gql'; From 0037abcbb27618d65629da3b75c0060843e41b70 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:34:52 -0700 Subject: [PATCH 38/92] gql args - not sure if we need this so put it in a new commit --- federation-integration-testsuite-js/src/utils/gql.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/federation-integration-testsuite-js/src/utils/gql.ts b/federation-integration-testsuite-js/src/utils/gql.ts index bc7dce500..1e8bd1408 100644 --- a/federation-integration-testsuite-js/src/utils/gql.ts +++ b/federation-integration-testsuite-js/src/utils/gql.ts @@ -3,6 +3,7 @@ import stripIndent from 'strip-indent'; export function gql( literals: string | readonly string[], + ...args: any[] ) { if (typeof literals === 'string') { @@ -11,5 +12,14 @@ export function gql( let result = literals[0]; + args.forEach((arg, i) => { + if (arg && arg.kind === 'Document') { + result += arg.loc.source.body; + } else { + result += arg; + } + result += literals[i + 1]; + }); + return parse(stripIndent(result)); } From b9d3adc6e58e38dabdb4065ba103bd63a82b4b50 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:40:04 -0700 Subject: [PATCH 39/92] compose & composeAndValidate - parse & gql uses -> new gql test util alias & update tests --- .../src/composition/__tests__/compose.test.ts | 67 ++++++++-------- .../__tests__/composeAndValidate.test.ts | 80 +++++++++---------- 2 files changed, 72 insertions(+), 75 deletions(-) diff --git a/federation-js/src/composition/__tests__/compose.test.ts b/federation-js/src/composition/__tests__/compose.test.ts index 79cafebe7..82c2a129c 100644 --- a/federation-js/src/composition/__tests__/compose.test.ts +++ b/federation-js/src/composition/__tests__/compose.test.ts @@ -2,15 +2,14 @@ import { GraphQLObjectType, isSpecifiedDirective, GraphQLDirective, - parse, } from 'graphql'; -import gql from 'graphql-tag'; import { composeServices } from '../compose'; import { astSerializer, typeSerializer, selectionSetSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import { normalizeTypeDefs } from '../normalize'; import { @@ -228,31 +227,31 @@ describe('composeServices', () => { it('allows extensions to overwrite other extension fields', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` extend type Product { price: Int! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Product { sku: String! name: String! } - `), + `, name: 'serviceB', }; const serviceC = { - typeDefs: parse(` + typeDefs: gql` extend type Product { price: Float! color: String! } - `), + `, name: 'serviceC', }; @@ -265,11 +264,11 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 13, + "column": 3, "line": 3, }, Object { - "column": 13, + "column": 3, "line": 3, }, ], @@ -387,21 +386,21 @@ describe('composeServices', () => { describe('collisions & error handling', () => { it('handles collisions on type extensions as expected', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product { sku: String! name: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { name: String! } - `), + `, name: 'serviceB', }; @@ -415,7 +414,7 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 15, + "column": 3, "line": 3, }, ], @@ -439,7 +438,7 @@ describe('composeServices', () => { it('reports multiple errors correctly', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product } @@ -448,17 +447,17 @@ describe('composeServices', () => { sku: String! name: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: String! name: String! } - `), + `, name: 'serviceB', }; @@ -472,7 +471,7 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 15, + "column": 3, "line": 3, }, ], @@ -482,7 +481,7 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 15, + "column": 3, "line": 4, }, ], @@ -506,23 +505,23 @@ describe('composeServices', () => { it('handles collisions of base types as expected (newest takes precedence)', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product { sku: String! name: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Product { id: ID! name: String! price: Int! } - `), + `, name: 'serviceB', }; @@ -536,11 +535,11 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 15, + "column": 3, "line": 4, }, Object { - "column": 15, + "column": 3, "line": 4, }, ], @@ -550,11 +549,11 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 18, + "column": 6, "line": 2, }, ], @@ -636,7 +635,7 @@ describe('composeServices', () => { // TODO: should there be a validation warning of some sort for this? it('allows overwriting a type that implements an interface improperly', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` interface Item { id: ID! } @@ -646,16 +645,16 @@ describe('composeServices', () => { sku: String! name: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { id: String! } - `), + `, name: 'serviceB', }; @@ -668,7 +667,7 @@ describe('composeServices', () => { "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 13, + "column": 3, "line": 3, }, ], diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index 50e64ebf1..a3c71430a 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -1,17 +1,16 @@ import { composeAndValidate } from '../composeAndValidate'; -import gql from 'graphql-tag'; import { GraphQLObjectType, DocumentNode, GraphQLScalarType, specifiedDirectives, printSchema, - parse, } from 'graphql'; import { astSerializer, typeSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import { assertCompositionFailure, @@ -120,7 +119,7 @@ it('composes and validates all (24) permutations without error', () => { it("doesn't throw errors when a type is unknown, but captures them instead", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { foo: Bar! } @@ -129,7 +128,7 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () id: ID! @external thing: String } - `), + `, name: 'serviceA', }; @@ -146,7 +145,7 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () "code": "MISSING_ERROR", "locations": Array [ Object { - "column": 14, + "column": 8, "line": 3, }, ], @@ -156,7 +155,7 @@ it("doesn't throw errors when a type is unknown, but captures them instead", () "code": "EXTENSION_WITH_NO_BASE", "locations": Array [ Object { - "column": 7, + "column": 1, "line": 6, }, ], @@ -368,10 +367,9 @@ describe('composition of value types', () => { }); describe('errors', () => { - it('on invalid usages of default operation names', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` schema { query: RootQuery } @@ -388,12 +386,12 @@ describe('composition of value types', () => { type Query { invalidUseOfQuery: Boolean } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Query { validUseOfQuery: Boolean } @@ -402,7 +400,7 @@ describe('composition of value types', () => { id: ID! @external sku: String } - `), + `, name: 'serviceB', }; @@ -415,7 +413,7 @@ describe('composition of value types', () => { "code": "ROOT_QUERY_USED", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 15, }, ], @@ -427,7 +425,7 @@ describe('composition of value types', () => { it('when a type extension has no base', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` schema { query: MyRoot } @@ -440,16 +438,16 @@ describe('composition of value types', () => { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Location { id: ID } - `), + `, name: 'serviceB', }; @@ -464,7 +462,7 @@ describe('composition of value types', () => { "code": "EXTENSION_WITH_NO_BASE", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 2, }, ], @@ -476,7 +474,7 @@ describe('composition of value types', () => { it('when used as an entity', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product } @@ -485,12 +483,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Query { topProducts: [Product] } @@ -499,7 +497,7 @@ describe('composition of value types', () => { sku: ID! color: String! } - `), + `, name: 'serviceB', }; @@ -512,11 +510,11 @@ describe('composition of value types', () => { "code": "VALUE_TYPE_NO_ENTITY", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 6, }, Object { - "column": 11, + "column": 1, "line": 6, }, ], @@ -527,7 +525,7 @@ describe('composition of value types', () => { it('on field type mismatch', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product } @@ -536,12 +534,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Query { topProducts: [Product] } @@ -550,7 +548,7 @@ describe('composition of value types', () => { sku: ID! color: String } - `), + `, name: 'serviceB', }; @@ -563,11 +561,11 @@ describe('composition of value types', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 6, }, Object { - "column": 11, + "column": 1, "line": 6, }, ], @@ -578,7 +576,7 @@ describe('composition of value types', () => { it('on kind mismatch', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product } @@ -587,12 +585,12 @@ describe('composition of value types', () => { sku: ID! color: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Query { topProducts: [Product] } @@ -601,7 +599,7 @@ describe('composition of value types', () => { sku: ID! color: String! } - `), + `, name: 'serviceB', }; @@ -613,11 +611,11 @@ describe('composition of value types', () => { "code": "VALUE_TYPE_KIND_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 6, }, Object { - "column": 11, + "column": 1, "line": 6, }, ], @@ -628,7 +626,7 @@ describe('composition of value types', () => { it('on union types mismatch', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product } @@ -642,12 +640,12 @@ describe('composition of value types', () => { } union Product = Couch | Mattress - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Query { topProducts: [Product] } @@ -661,7 +659,7 @@ describe('composition of value types', () => { } union Product = Couch | Cabinet - `), + `, name: 'serviceB', }; @@ -673,11 +671,11 @@ describe('composition of value types', () => { "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 14, }, Object { - "column": 11, + "column": 1, "line": 14, }, ], From c787920da89b9e596a8cbd8da15dcba1d39fa865 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 14:42:11 -0700 Subject: [PATCH 40/92] executableDirectivesIdentical - parse & gql uses -> new gql test util alias & update tests --- .../executableDirectivesIdentical.test.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts index bfe50d330..e38d7830d 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { executableDirectivesIdentical } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -55,23 +56,23 @@ describe('executableDirectivesIdentical', () => { it("throws errors when custom, executable directives aren't defined with the same locations in every service", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` directive @stream on FIELD - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` directive @stream on FIELD | QUERY - `), + `, name: 'serviceB', }; const serviceC = { - typeDefs: parse(` + typeDefs: gql` directive @stream on INLINE_FRAGMENT - `), + `, name: 'serviceC', }; @@ -84,7 +85,7 @@ describe('executableDirectivesIdentical', () => { "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], @@ -99,16 +100,16 @@ describe('executableDirectivesIdentical', () => { it("throws errors when custom, executable directives aren't defined with the same arguments in every service", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` directive @instrument(tag: String!) on FIELD - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` directive @instrument(tag: Boolean) on FIELD - `), + `, name: 'serviceB', }; @@ -121,7 +122,7 @@ describe('executableDirectivesIdentical', () => { "code": "EXECUTABLE_DIRECTIVES_IDENTICAL", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], From a0bc39db46cfdb871f1650c763b57b8e38edd581 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:04:13 -0700 Subject: [PATCH 41/92] executableDirectivesInAllServices.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../executableDirectivesInAllServices.test.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts index 4cd4263ed..9966c0abb 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesInAllServices.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { executableDirectivesInAllServices } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -53,27 +54,27 @@ describe('executableDirectivesInAllServices', () => { it("throws errors when custom, executable directives aren't defined in every service", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` directive @stream on FIELD - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Query { thing: String } - `), + `, name: 'serviceB', }; const serviceC = { - typeDefs: parse(` + typeDefs: gql` extend type Query { otherThing: String } - `), + `, name: 'serviceC', }; @@ -86,7 +87,7 @@ describe('executableDirectivesInAllServices', () => { "code": "EXECUTABLE_DIRECTIVES_IN_ALL_SERVICES", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], From 00e8eab91832bd2449b613ea0d9e280f25ee7bc9 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:05:53 -0700 Subject: [PATCH 42/92] externalMissingOnBase.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/externalMissingOnBase.test.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts index b505b2abc..6f0f2be21 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { externalMissingOnBase as validateExternalMissingOnBase } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -19,24 +20,24 @@ describe('externalMissingOnBase', () => { }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external id: String! @external price: Int! @requires(fields: "sku id") } - `), + `, name: 'serviceB', }; const serviceC = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external id: String! test: Int @external } - `), + `, name: 'serviceC', }; @@ -49,7 +50,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -59,7 +60,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 5, }, ], @@ -71,22 +72,22 @@ describe('externalMissingOnBase', () => { it("warns when an @external field isn't defined anywhere else", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { specialId: String! @external id: String! @requires(fields: "specialId") } - `), + `, name: 'serviceB', }; @@ -99,7 +100,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], From 1a7499e0650a0d34311615f0f2c06791873b1981 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:07:05 -0700 Subject: [PATCH 43/92] externalTypeMismatch.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/externalTypeMismatch.test.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts index 46d7b1d97..e3229eede 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts @@ -1,31 +1,33 @@ import { externalTypeMismatch as validateExternalTypeMismatch } from '../'; import { composeServices } from '../../../compose'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); describe('validateExternalDirectivesOnSchema', () => { it('warns when the type of an @external field doesnt match the base', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku skew") { sku: String! skew: String upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: String @external skew: String! @external price: Int! @requires(fields: "sku skew") } - `), + `, name: 'serviceB', }; @@ -38,7 +40,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], @@ -48,7 +50,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -60,22 +62,22 @@ describe('validateExternalDirectivesOnSchema', () => { it("warns when an @external field's type does not exist in the composed schema", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: NonExistentType! @external id: String! @requires(fields: "sku") } - `), + `, name: 'serviceB', }; @@ -88,7 +90,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], From 4114f3505c717d0323f3094a025f3f8fd10ce7b4 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:08:12 -0700 Subject: [PATCH 44/92] externalUnused.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/externalUnused.test.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts index a74b95ebb..26cf2fa7d 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -1,32 +1,33 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { externalUnused as validateExternalUnused } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); describe('externalUnused', () => { it('warns when there is an unused @external field', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "id") { sku: String! upc: String! id: ID! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: String! @external id: ID! @external price: Int! @requires(fields: "id") } - `), + `, name: 'serviceB', }; @@ -39,7 +40,7 @@ describe('externalUnused', () => { "code": "EXTERNAL_UNUSED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], @@ -345,7 +346,7 @@ describe('externalUnused', () => { it('does error when @external is used on a field of a concrete type is not shared by its implemented interface', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Car implements Vehicle @key(fields: "id") { id: ID! speed: Int @@ -355,11 +356,11 @@ describe('externalUnused', () => { id: ID! speed: Int } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Car implements Vehicle @key(fields: "id") { id: ID! @external speed: Int @external @@ -369,7 +370,7 @@ describe('externalUnused', () => { id: ID! speed: Int } - `), + `, name: 'serviceB', }; const serviceList = [serviceA, serviceB]; @@ -381,7 +382,7 @@ describe('externalUnused', () => { "code": "EXTERNAL_UNUSED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 5, }, ], From 11f64ec5ae19bfb2e76027cc0cd21fba018497ff Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:09:43 -0700 Subject: [PATCH 45/92] keyFieldsMissingOnBase.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/keyFieldsMissingOnBase.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index 073cb083e..111f83b67 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -1,9 +1,7 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { keyFieldsMissingOnBase as validateKeyFieldsMissingOnBase } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { gql, graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; -import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -47,23 +45,23 @@ describe('keyFieldsMissingOnBase', () => { it('warns if @key references a field added by another service', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku uid") { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { uid: String! sku: String! @external price: Int! @requires(fields: "sku") } - `), + `, name: 'serviceB', }; From f61b626b11d36dd7ec0f9af8f16a21ff412e342a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:11:15 -0700 Subject: [PATCH 46/92] keyFieldsSelectInvalidType.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/keyFieldsSelectInvalidType.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index 756127c3a..9e6a4a153 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -1,9 +1,7 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { keyFieldsSelectInvalidType as validateKeyFieldsSelectInvalidType } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { gql, graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; -import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -65,12 +63,12 @@ describe('keyFieldsSelectInvalidType', () => { }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: String! @external price: Int! @requires(fields: "sku") } - `), + `, name: 'serviceB', }; @@ -113,12 +111,12 @@ describe('keyFieldsSelectInvalidType', () => { }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: String! @external name: String! } - `), + `, name: 'serviceB', }; From 162a90aabbea86df7e4d3de2ede0137749b355a7 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:12:44 -0700 Subject: [PATCH 47/92] keysMatchBaseService.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/keysMatchBaseService.test.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index 537b6dc03..20952fd44 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -1,9 +1,10 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { keysMatchBaseService as validateKeysMatchBaseService } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; -import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -43,22 +44,22 @@ describe('keysMatchBaseService', () => { it('requires a @key to be specified on the originating type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external price: Int! } - `), + `, name: 'serviceB', }; @@ -77,7 +78,7 @@ describe('keysMatchBaseService', () => { "code": "KEY_MISSING_ON_BASE", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], @@ -88,23 +89,23 @@ describe('keysMatchBaseService', () => { it('requires an extending service use only one @key specified on the originating type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") @key(fields: "upc") { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") @key(fields: "upc") { sku: String! @external upc: String! @external price: Int! } - `), + `, name: 'serviceB', }; @@ -123,7 +124,7 @@ describe('keysMatchBaseService', () => { "code": "MULTIPLE_KEYS_ON_EXTENSION", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], @@ -134,22 +135,22 @@ describe('keysMatchBaseService', () => { it('requires extending services to use a @key specified by the originating type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku upc") { sku: String! upc: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external price: Int! } - `), + `, name: 'serviceB', }; @@ -168,7 +169,7 @@ describe('keysMatchBaseService', () => { "code": "KEY_NOT_SPECIFIED", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 2, }, ], From d2731ff6a037eeef27ce0d84341bc32332939dc8 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:14:01 -0700 Subject: [PATCH 48/92] providesFieldsMissingExternals.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../providesFieldsMissingExternals.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts index 0ea1ee65c..f1af70207 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts @@ -1,9 +1,10 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { providesFieldsMissingExternal as validateProdivesFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; -import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -65,18 +66,18 @@ describe('providesFieldsMissingExternal', () => { it('warns when there is a @provides with no matching @external field', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! id: ID! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type Review @key(fields: "id") { id: ID! product: Product @provides(fields: "id") @@ -86,7 +87,7 @@ describe('providesFieldsMissingExternal', () => { sku: String! @external price: Int! } - `), + `, name: 'serviceB', }; @@ -104,7 +105,7 @@ describe('providesFieldsMissingExternal', () => { "code": "PROVIDES_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From 300b426ed61df6af5bbad660f8300f93b21fb5bd Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:15:33 -0700 Subject: [PATCH 49/92] providesFieldsSelectInvalidType.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../providesFieldsSelectInvalidType.test.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts index 7592bd9c8..8c02f522b 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts @@ -1,9 +1,10 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { providesFieldsSelectInvalidType as validateprovidesFieldsSelectInvalidType } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; -import { parse } from 'graphql'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -49,7 +50,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of a list type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "wishLists") @@ -63,12 +64,12 @@ describe('providesFieldsSelectInvalidType', () => { extend type WishList @key(fields: "id") { id: ID! @external } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type User @key(fields: "id") { id: ID! wishLists: [WishList] @@ -77,7 +78,7 @@ describe('providesFieldsSelectInvalidType', () => { type WishList @key(fields: "id") { id: ID! } - `), + `, name: 'serviceB', }; @@ -96,7 +97,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -108,7 +109,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of an interface type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "account") @@ -122,12 +123,12 @@ describe('providesFieldsSelectInvalidType', () => { extend interface Account { username: String @external } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type User @key(fields: "id") { id: ID! account: Account @@ -136,7 +137,7 @@ describe('providesFieldsSelectInvalidType', () => { interface Account { username: String } - `), + `, name: 'serviceB', }; @@ -155,7 +156,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -167,7 +168,7 @@ describe('providesFieldsSelectInvalidType', () => { it('warns if @provides references fields of a union type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Review @key(fields: "id") { id: ID! author: User @provides(fields: "account") @@ -187,12 +188,12 @@ describe('providesFieldsSelectInvalidType', () => { extend type SMSAccount @key(fields: "phone") { phone: String! @external } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` type User @key(fields: "id") { id: ID! account: Account @@ -207,7 +208,7 @@ describe('providesFieldsSelectInvalidType', () => { type SMSAccount @key(fields: "phone") { phone: String! } - `), + `, name: 'serviceB', }; @@ -226,7 +227,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From 64246b52a9997046188dd8bc9c751744f2d2cf94 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:16:45 -0700 Subject: [PATCH 50/92] providesNotOnEntity.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/providesNotOnEntity.test.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts index e9a87f834..1d365a7e6 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { providesNotOnEntity as validateProvidesNotOnEntity } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -96,7 +97,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a type that is not an entity', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @@ -107,17 +108,17 @@ describe('providesNotOnEntity', () => { sku: String! quantity: Int! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external lineItem: LineItem @provides(fields: "quantity") } - `), + `, name: 'serviceB', }; @@ -130,7 +131,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -142,7 +143,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a type that is not a list of entity', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @@ -153,17 +154,17 @@ describe('providesNotOnEntity', () => { sku: String! quantity: Int! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external lineItems: [LineItem] @provides(fields: "quantity") } - `), + `, name: 'serviceB', }; @@ -176,7 +177,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -188,7 +189,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a non-object type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @@ -201,17 +202,17 @@ describe('providesNotOnEntity', () => { SONG ALBUM } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external category: Category @provides(fields: "id") } - `), + `, name: 'serviceB', }; @@ -224,7 +225,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -236,7 +237,7 @@ describe('providesNotOnEntity', () => { it('warns when there is a @provides on a list of non-object type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @@ -249,17 +250,17 @@ describe('providesNotOnEntity', () => { SONG ALBUM } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! @external categories: [Category] @provides(fields: "id") } - `), + `, name: 'serviceB', }; @@ -272,7 +273,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From b6e7cf61b8ecf1889fe66dca102c681888dfd8ee Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:17:41 -0700 Subject: [PATCH 51/92] requiresFieldsMissingExternals.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../requiresFieldsMissingExternals.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts index 56cc4a589..067954a4b 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { requiresFieldsMissingExternal as validateRequiresFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -40,22 +41,22 @@ describe('requiresFieldsMissingExternal', () => { it('warns when there is a @requires with no matching @external field', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! id: ID! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product { price: Int! @requires(fields: "id") } - `), + `, name: 'serviceB', }; @@ -71,7 +72,7 @@ describe('requiresFieldsMissingExternal', () => { "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], From b2f6a19779fca51d8c58d41f72672b07956441fd Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:18:41 -0700 Subject: [PATCH 52/92] requiresFieldsMissingOnBase.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../requiresFieldsMissingOnBase.test.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts index 5d0707e52..53b1a809d 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts @@ -1,8 +1,9 @@ -import gql from 'graphql-tag'; import { composeServices } from '../../../compose'; import { requiresFieldsMissingOnBase as validateRequiresFieldsMissingOnBase } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -38,28 +39,28 @@ describe('requiresFieldsMissingOnBase', () => { it('warns when requires selects a field not found on the base type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! } - `), + `, name: 'serviceA', }; const serviceB = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { id: ID! } - `), + `, name: 'serviceB', }; const serviceC = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { id: ID! @external weight: Float! @requires(fields: "id") } - `), + `, name: 'serviceC', }; const serviceList = [serviceA, serviceB, serviceC]; @@ -74,7 +75,7 @@ describe('requiresFieldsMissingOnBase', () => { "code": "REQUIRES_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From b715495710164edb899be3fcfa3f705170d45f68 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:20:26 -0700 Subject: [PATCH 53/92] duplicateEnumOrScalar.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/duplicateEnumOrScalar.test.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts index 75d5aa726..f559bc20f 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumOrScalar.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { duplicateEnumOrScalar as validateDuplicateEnumOrScalar } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -35,7 +36,7 @@ describe('duplicateEnumOrScalar', () => { }); it('errors when there are multiple definitions of the same enum', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "color { id value }") { sku: String! upc: String! @@ -55,30 +56,30 @@ describe('duplicateEnumOrScalar', () => { enum ProductType { DIGITAL } - `), + `, name: 'serviceA', }; const warnings = validateDuplicateEnumOrScalar(serviceA); expect(warnings).toMatchInlineSnapshot(` - Array [ - Object { - "code": "DUPLICATE_ENUM_DEFINITION", - "locations": Array [ - Object { - "column": 9, - "line": 18, - }, - ], - "message": "[serviceA] ProductType -> The enum, \`ProductType\` was defined multiple times in this service. Remove one of the definitions for \`ProductType\`", - }, - ] - `); + Array [ + Object { + "code": "DUPLICATE_ENUM_DEFINITION", + "locations": Array [ + Object { + "column": 1, + "line": 18, + }, + ], + "message": "[serviceA] ProductType -> The enum, \`ProductType\` was defined multiple times in this service. Remove one of the definitions for \`ProductType\`", + }, + ] + `); }); it('errors when there are multiple definitions of the same scalar', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` scalar Date type Product @key(fields: "color { id value }") { sku: String! @@ -87,7 +88,7 @@ describe('duplicateEnumOrScalar', () => { } scalar Date - `), + `, name: 'serviceA', }; @@ -98,7 +99,7 @@ describe('duplicateEnumOrScalar', () => { "code": "DUPLICATE_SCALAR_DEFINITION", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 9, }, ], From 513ca047a498d46236a715ff3b1ebc62316cb5ed Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:21:12 -0700 Subject: [PATCH 54/92] duplicateEnumValue.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/duplicateEnumValue.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts index 6f8944b96..6cf31ec13 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { duplicateEnumValue as validateDuplicateEnumValue } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -37,7 +38,7 @@ describe('duplicateEnumValue', () => { }); it('errors when there are duplicate enum values in a single service', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "color { id value }") { sku: String! upc: String! @@ -58,7 +59,7 @@ describe('duplicateEnumValue', () => { DIGITAL BOOK } - `), + `, name: 'serviceA', }; @@ -69,7 +70,7 @@ describe('duplicateEnumValue', () => { "code": "DUPLICATE_ENUM_VALUE", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 18, }, ], From 665ff7e8b092979eb85677e2c888988e0cbd6dd6 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:22:04 -0700 Subject: [PATCH 55/92] externalUsedOnBase.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/externalUsedOnBase.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts index b3431ec3e..d6ab15f4e 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { externalUsedOnBase as validateExternalUsedOnBase } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -29,13 +30,13 @@ describe('externalUsedOnBase', () => { it('warns when there is a @external field on a base type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @external id: ID! } - `), + `, name: 'serviceA', }; @@ -46,7 +47,7 @@ describe('externalUsedOnBase', () => { "code": "EXTERNAL_USED_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From d3a7b9d037b237039593fb668c5bc65dbafe69ee Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:22:59 -0700 Subject: [PATCH 56/92] keyFieldsMissingExternal.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../keyFieldsMissingExternal.test.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts index 59dd47197..6178b84f7 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/keyFieldsMissingExternal.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { keyFieldsMissingExternal as validateKeyFieldsMissingExternal } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -85,7 +86,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument doesn't reference an @external field", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { sku: String! upc: String! @@ -96,7 +97,7 @@ describe('keyFieldsMissingExternal', () => { id: ID! value: String! } - `), + `, name: 'serviceA', }; @@ -108,7 +109,7 @@ describe('keyFieldsMissingExternal', () => { "code": "KEY_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 3, }, ], @@ -120,7 +121,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument references a field that isn't known", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` extend type Product @key(fields: "sku") { upc: String! @external color: Color! @@ -130,7 +131,7 @@ describe('keyFieldsMissingExternal', () => { id: ID! value: String! } - `), + `, name: 'serviceA', }; @@ -154,7 +155,7 @@ describe('keyFieldsMissingExternal', () => { it("warns when a @key argument doesn't reference an @external field", () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` extend type Car @key(fields: "model { name kit { upc } } year") { model: Model! @external year: String! @external @@ -168,7 +169,7 @@ describe('keyFieldsMissingExternal', () => { type Kit { upc: String! } - `), + `, name: 'serviceA', }; @@ -180,7 +181,7 @@ describe('keyFieldsMissingExternal', () => { "code": "KEY_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 8, }, ], @@ -190,7 +191,7 @@ describe('keyFieldsMissingExternal', () => { "code": "KEY_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 9, }, ], @@ -200,7 +201,7 @@ describe('keyFieldsMissingExternal', () => { "code": "KEY_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 13, }, ], From d7ec439db678948972e80edc416e4492bdc5b726 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:23:59 -0700 Subject: [PATCH 57/92] requiresUsedOnBase.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/requiresUsedOnBase.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts index aa5aca82f..253497961 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { requiresUsedOnBase as validateRequiresUsedOnBase } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -29,13 +30,13 @@ describe('requiresUsedOnBase', () => { it('warns when there is a @requires field on a base type', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: String! upc: String! @requires(fields: "sku") id: ID! } - `), + `, name: 'serviceA', }; @@ -46,7 +47,7 @@ describe('requiresUsedOnBase', () => { "code": "REQUIRES_USED_ON_BASE", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], From 615b2675015ceda60a6ece0ba391e03a8e566771 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:24:58 -0700 Subject: [PATCH 58/92] reservedFieldUsed.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/reservedFieldUsed.test.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts index 8505f3ca2..775524c35 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/reservedFieldUsed.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { reservedFieldUsed as validateReservedFieldUsed } from '..'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -26,7 +27,7 @@ describe('reservedFieldUsed', () => { it('warns when _service or _entities is used at the query root', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` type Query { product: Product _service: String! @@ -36,7 +37,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `), + `, name: 'serviceA', }; @@ -47,7 +48,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 4, }, ], @@ -57,7 +58,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 5, }, ], @@ -69,7 +70,7 @@ describe('reservedFieldUsed', () => { it('warns when _service or _entities is used in a schema extension', () => { const schemaDefinition = { - typeDefs: parse(` + typeDefs: gql` schema { query: RootQuery } @@ -82,12 +83,12 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `), + `, name: 'schemaDefinition', }; const schemaExtension = { - typeDefs: parse(` + typeDefs: gql` extend schema { query: RootQuery } @@ -100,7 +101,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `), + `, name: 'schemaExtension', }; @@ -115,7 +116,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 8, }, ], @@ -129,7 +130,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 7, }, ], @@ -141,7 +142,7 @@ describe('reservedFieldUsed', () => { it('warns when reserved fields are used on custom Query types', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` schema { query: RootQuery } @@ -155,7 +156,7 @@ describe('reservedFieldUsed', () => { type Product { sku: String } - `), + `, name: 'serviceA', }; @@ -168,7 +169,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 8, }, ], @@ -178,7 +179,7 @@ describe('reservedFieldUsed', () => { "code": "RESERVED_FIELD_USED", "locations": Array [ Object { - "column": 11, + "column": 3, "line": 9, }, ], From 037237a184e1f3ce8867b8374d2d168679cbbe5c Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:25:48 -0700 Subject: [PATCH 59/92] rootFieldUsed.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/rootFieldUsed.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts index afae07fe6..27336d78b 100644 --- a/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts +++ b/federation-js/src/composition/validate/preNormalization/__tests__/rootFieldUsed.test.ts @@ -1,7 +1,8 @@ -import gql from 'graphql-tag'; import { rootFieldUsed as validateRootFieldUsed } from '../'; -import { graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; -import { parse } from 'graphql'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -68,7 +69,7 @@ describe('rootFieldUsed', () => { it('warns when a schema definition / extension is provided, as well as a default root type or extension', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` schema { query: RootQuery } @@ -84,7 +85,7 @@ describe('rootFieldUsed', () => { type Query { invalidUseOfQuery: Boolean } - `), + `, name: 'serviceA', }; @@ -98,7 +99,7 @@ describe('rootFieldUsed', () => { "code": "ROOT_QUERY_USED", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 14, }, ], @@ -110,7 +111,7 @@ describe('rootFieldUsed', () => { it('warns against using default operation type names (Query, Mutation, Subscription) when a non-default operation type name is provided in the schema definition', () => { const serviceA = { - typeDefs: parse(` + typeDefs: gql` schema { mutation: RootMutation } @@ -122,7 +123,7 @@ describe('rootFieldUsed', () => { type Mutation { invalidUseOfMutation: Boolean } - `), + `, name: 'serviceA', }; @@ -135,7 +136,7 @@ describe('rootFieldUsed', () => { "code": "ROOT_MUTATION_USED", "locations": Array [ Object { - "column": 9, + "column": 1, "line": 10, }, ], From 8181e387a5743828049061a68c9be41893d39188 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:28:01 -0700 Subject: [PATCH 60/92] matchingEnums.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../sdl/__tests__/matchingEnums.test.ts | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts index 9b395635b..39a4da400 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingEnums.test.ts @@ -3,16 +3,15 @@ import { DocumentNode, GraphQLSchema, specifiedDirectives, - parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; import { buildMapsFromServiceList } from '../../../compose'; import { astSerializer, typeSerializer, selectionSetSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import federationDirectives from '../../../../directives'; import { ServiceDefinition } from '../../../types'; @@ -78,20 +77,20 @@ describe('matchingEnums', () => { it('errors when enums in separate services dont match', () => { const serviceList = [ { - typeDefs: parse(` + typeDefs: gql` enum ProductCategory { BED BATH } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` enum ProductCategory { BEYOND } - `), + `, name: 'serviceB', }, ]; @@ -106,7 +105,11 @@ describe('matchingEnums', () => { "code": "ENUM_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, + "line": 2, + }, + Object { + "column": 1, "line": 2, }, ], @@ -119,7 +122,7 @@ describe('matchingEnums', () => { it('errors when enums in separate services dont match', () => { const serviceList = [ { - typeDefs: parse(` + typeDefs: gql` type Query { products: [Product]! } @@ -134,27 +137,27 @@ describe('matchingEnums', () => { BOOK FURNITURE } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` enum ProductType { FURNITURE BOOK DIGITAL } - `), + `, name: 'serviceB', }, { - typeDefs: parse(` + typeDefs: gql` enum ProductType { FURNITURE BOOK DIGITAL } - `), + `, name: 'serviceC', }, ]; @@ -169,44 +172,51 @@ describe('matchingEnums', () => { "code": "ENUM_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 12, }, + Object { + "column": 1, + "line": 2, + }, + Object { + "column": 1, + "line": 2, + }, ], "message": "The \`ProductType\` enum does not have identical values in all services. Groups of services with identical values are: [serviceA], [serviceB, serviceC]", }, ] - `, - ); + `); }); it('errors when an enum name is defined as another type in a service', () => { const serviceList = [ { - typeDefs: parse(` + typeDefs: gql` enum ProductType { BOOK FURNITURE } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type ProductType { id: String } - `), + `, name: 'serviceB', }, { - typeDefs: parse(` + typeDefs: gql` enum ProductType { FURNITURE BOOK DIGITAL } - `), + `, name: 'serviceC', }, ]; @@ -221,7 +231,15 @@ describe('matchingEnums', () => { "code": "ENUM_MISMATCH_TYPE", "locations": Array [ Object { - "column": 11, + "column": 1, + "line": 2, + }, + Object { + "column": 1, + "line": 2, + }, + Object { + "column": 1, "line": 2, }, ], From 0f699eef10284aa51049ae1c51472ae7186face6 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:29:23 -0700 Subject: [PATCH 61/92] matchingUnions.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../validate/sdl/__tests__/matchingUnions.test.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts index 8fd0de4f1..7653f99b0 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/matchingUnions.test.ts @@ -3,13 +3,12 @@ import { specifiedDirectives, Kind, DocumentNode, - parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; import { typeSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import { UniqueUnionTypes } from '..'; import { ServiceDefinition } from '../../../types'; @@ -51,7 +50,7 @@ describe('MatchingUnions', () => { it('enforces unique union names on non-identical union types', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` union ProductOrError = Product | Error type Error { @@ -62,11 +61,11 @@ describe('MatchingUnions', () => { type Product @key(fields: "sku") { sku: ID! } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` union ProductOrError = Product type Error { @@ -78,7 +77,7 @@ describe('MatchingUnions', () => { sku: ID! @external colors: [String] } - `), + `, name: 'serviceB', }, ]); @@ -90,11 +89,11 @@ describe('MatchingUnions', () => { "code": "VALUE_TYPE_UNION_TYPES_MISMATCH", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 2, }, Object { - "column": 11, + "column": 1, "line": 2, }, ], From 2e895f289c714b1273472f84f1274806db8e8c7b Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:30:21 -0700 Subject: [PATCH 62/92] possibleTypeExtensions.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../__tests__/possibleTypeExtensions.test.ts | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts index 909593133..5aeed9aa3 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/possibleTypeExtensions.test.ts @@ -4,14 +4,13 @@ import { GraphQLSchema, specifiedDirectives, extendSchema, - parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; import { buildMapsFromServiceList } from '../../../compose'; import { typeSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import federationDirectives from '../../../../directives'; import { ServiceDefinition } from '../../../types'; @@ -86,11 +85,11 @@ describe('PossibleTypeExtensionsType', () => { it('errors when there is an extension with no base', () => { const serviceList = [ { - typeDefs: parse(` + typeDefs: gql` extend type Product { id: ID! } - `), + `, name: 'serviceA', }, ]; @@ -109,7 +108,7 @@ describe('PossibleTypeExtensionsType', () => { "code": "EXTENSION_WITH_NO_BASE", "locations": Array [ Object { - "column": 11, + "column": 1, "line": 2, }, ], @@ -122,20 +121,20 @@ describe('PossibleTypeExtensionsType', () => { it('errors when trying to extend a type with a different `Kind`', () => { const serviceList = [ { - typeDefs: parse(` + typeDefs: gql` extend type Product { sku: ID } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` input Product { id: ID! } - `), + `, name: 'serviceB', }, ]; @@ -147,19 +146,19 @@ describe('PossibleTypeExtensionsType', () => { schema = extendSchema(schema, definitions, { assumeValidSDL: true }); errors.push(...validateSDL(extensions, schema, [PossibleTypeExtensions])); expect(errors).toMatchInlineSnapshot(` - Array [ - Object { - "code": "EXTENSION_OF_WRONG_KIND", - "locations": Array [ - Object { - "column": 11, - "line": 2, - }, - ], - "message": "[serviceA] Product -> \`Product\` was originally defined as a InputObjectTypeDefinition and can only be extended by a InputObjectTypeExtension. serviceA defines Product as a ObjectTypeExtension", - }, - ] - `); + Array [ + Object { + "code": "EXTENSION_OF_WRONG_KIND", + "locations": Array [ + Object { + "column": 1, + "line": 2, + }, + ], + "message": "[serviceA] Product -> \`Product\` was originally defined as a InputObjectTypeDefinition and can only be extended by a InputObjectTypeExtension. serviceA defines Product as a ObjectTypeExtension", + }, + ] + `); }); it('does not error', () => { From da7c6600cc1d13bd7fb30077be396a310b8c409d Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:32:35 -0700 Subject: [PATCH 63/92] uniqueTypeNamesWithFields.test.ts - parse & gql uses -> new gql test util alias & update tests --- .../uniqueTypeNamesWithFields.test.ts | 103 +++++++++--------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index 842d4b0bf..7e3992941 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -3,13 +3,12 @@ import { specifiedDirectives, Kind, DocumentNode, - parse, } from 'graphql'; import { validateSDL } from 'graphql/validation/validate'; -import gql from 'graphql-tag'; import { typeSerializer, graphqlErrorSerializer, + gql, } from 'apollo-federation-integration-testsuite'; import federationDirectives from '../../../../directives'; import { UniqueTypeNamesWithFields } from '..'; @@ -81,23 +80,23 @@ describe('UniqueTypeNamesWithFields', () => { it('object type definitions (non-identical, value types with type mismatch)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` type Product { sku: ID! color: String quantity: Int } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type Product { sku: String! color: String quantity: Int! } - `), + `, name: 'serviceB', }, ]); @@ -112,11 +111,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -126,11 +125,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -143,19 +142,19 @@ describe('UniqueTypeNamesWithFields', () => { it('object type definitions (non-identical, field input value types with type mismatch)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` type Person { age(relative: Boolean!): Int } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type Person { age(relative: Boolean): Int } - `), + `, name: 'serviceB', }, ]); @@ -170,11 +169,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_INPUT_VALUE_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -381,19 +380,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` input Product { sku: ID } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type Product { sku: ID } - `), + `, name: 'serviceB', }, ]); @@ -408,11 +407,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_KIND_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -424,19 +423,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (scalar)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` scalar DateTime - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type DateTime { day: Int formatted: String # ... } - `), + `, name: 'serviceB', }, ]); @@ -451,11 +450,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_KIND_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -467,19 +466,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (union)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` union DateTime = Date | Time - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type DateTime { day: Int formatted: String # ... } - `), + `, name: 'serviceB', }, ]); @@ -494,11 +493,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_KIND_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -510,22 +509,22 @@ describe('UniqueTypeNamesWithFields', () => { it('value types must be of the same kind (enum)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` enum DateTime { DATE TIME } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type DateTime { day: Int formatted: String # ... } - `), + `, name: 'serviceB', }, ]); @@ -540,11 +539,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_KIND_MISMATCH", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -556,19 +555,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types cannot be entities (part 1)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: ID } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type Product { sku: ID } - `), + `, name: 'serviceB', }, ]); @@ -582,11 +581,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_NO_ENTITY", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], @@ -598,19 +597,19 @@ describe('UniqueTypeNamesWithFields', () => { it('value types cannot be entities (part 2)', () => { const [definitions] = createDocumentsForServices([ { - typeDefs: parse(` + typeDefs: gql` type Product { sku: ID } - `), + `, name: 'serviceA', }, { - typeDefs: parse(` + typeDefs: gql` type Product @key(fields: "sku") { sku: ID } - `), + `, name: 'serviceB', }, ]); @@ -624,11 +623,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_NO_ENTITY", "locations": Array [ Object { - "column": 13, + "column": 1, "line": 2, }, Object { - "column": 13, + "column": 1, "line": 2, }, ], From 40ad8a620b0285d231d85451778217e38f418b54 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:35:33 -0700 Subject: [PATCH 64/92] || undefined -> ?? undefined --- .../postComposition/executableDirectivesIdentical.ts | 2 +- .../postComposition/executableDirectivesInAllServices.ts | 2 +- .../validate/postComposition/keysMatchBaseService.ts | 6 +++--- .../postComposition/providesFieldsMissingExternal.ts | 2 +- .../postComposition/providesFieldsSelectInvalidType.ts | 8 ++++---- .../validate/postComposition/providesNotOnEntity.ts | 4 ++-- .../postComposition/requiresFieldsMissingExternal.ts | 2 +- .../postComposition/requiresFieldsMissingOnBase.ts | 2 +- .../validate/preComposition/keyFieldsMissingExternal.ts | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts index fe695ce8e..9663c12e1 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts @@ -51,7 +51,7 @@ export const executableDirectivesIdentical: PostCompositionValidator = ({ return `\t${serviceName}: ${print(definition)}`; }) .join('\n')}`, - directive.astNode || undefined, + directive.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts index 46e8b812a..a19ea4159 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts @@ -50,7 +50,7 @@ export const executableDirectivesInAllServices: PostCompositionValidator = ({ `Custom directives must be implemented in every service. The following services do not implement the @${ directive.name } directive: ${serviceNamesWithoutDirective.join(', ')}.`, - directive.astNode || undefined, + directive.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index 3d8e4543e..09c014ae3 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -33,7 +33,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'KEY_MISSING_ON_BASE', logServiceAndType(serviceName, parentTypeName) + `appears to be an entity but no @key directives are specified on the originating type.`, - parentType.astNode || undefined, + parentType.astNode ?? undefined, ), ); continue; @@ -51,7 +51,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'MULTIPLE_KEYS_ON_EXTENSION', logServiceAndType(extendingService, parentTypeName) + `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, - parentType.astNode || undefined, + parentType.astNode ?? undefined, ), ); return; @@ -71,7 +71,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ `\t${availableKeys .map((fieldSet) => `@key(fields: "${fieldSet}")`) .join('\n\t')}`, - parentType.astNode || undefined + parentType.astNode ?? undefined ), ); return; diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts index 00b4a5336..528f6a4b0 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts @@ -48,7 +48,7 @@ export const providesFieldsMissingExternal: PostCompositionValidator = ({ 'PROVIDES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `provides the field \`${selection.name.value}\` and requires ${fieldType}.${selection.name.value} to be marked as @external.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts index e257c4933..8d2586297 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -52,7 +52,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); continue; @@ -68,7 +68,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } @@ -82,7 +82,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } @@ -97,7 +97,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts index a00d1e376..0a6e0a4a4 100644 --- a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts +++ b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts @@ -52,7 +52,7 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); continue; @@ -67,7 +67,7 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index 85cc34b30..fc8bf6b06 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -45,7 +45,7 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be marked as @external.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 7d7bd010b..1aad40e2e 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -42,7 +42,7 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, - field.astNode || undefined, + field.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts index ddfb0e178..7b4409978 100644 --- a/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/preComposition/keyFieldsMissingExternal.ts @@ -103,7 +103,7 @@ export const keyFieldsMissingExternal = ({ 'KEY_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, parentType.name) + `A @key directive specifies the \`${fieldDef.name}\` field which has no matching @external field.`, - fieldDef.astNode || undefined, + fieldDef.astNode ?? undefined, ), ); } From e0fd417f682a9064b6e07ce04942efd269d50219 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 27 Apr 2021 15:38:26 -0700 Subject: [PATCH 65/92] update CHANGELOG.md --- federation-js/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/federation-js/CHANGELOG.md b/federation-js/CHANGELOG.md index 4302516d4..fc18f6d82 100644 --- a/federation-js/CHANGELOG.md +++ b/federation-js/CHANGELOG.md @@ -6,6 +6,10 @@ - _Nothing yet! Stay tuned!_ +## v0.24.0 + +- Composition errors now include `location` corresponding to the line number & column in the subgraph sdl. [PR #686](https://github.com/apollographql/federation/pull/686/files) + ## v0.23.0 - __BREAKING__ - Update CSDL to the new core schema format, implementing the currently-being-introduced core and join specs. `composeAndValidate` now returns `supergraphSdl` in the new format instead of `composedSdl` in the previous CSDL format. [PR #622](https://github.com/apollographql/federation/pull/622) From 73fe00da790583676d961bca2d732493fc49ea83 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 28 Apr 2021 10:02:02 -0700 Subject: [PATCH 66/92] Update federation-js/CHANGELOG.md Co-authored-by: Jesse Rosenberger --- federation-js/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/federation-js/CHANGELOG.md b/federation-js/CHANGELOG.md index fc18f6d82..fef2b154f 100644 --- a/federation-js/CHANGELOG.md +++ b/federation-js/CHANGELOG.md @@ -8,7 +8,7 @@ ## v0.24.0 -- Composition errors now include `location` corresponding to the line number & column in the subgraph sdl. [PR #686](https://github.com/apollographql/federation/pull/686/files) +- Composition errors now include `locations` corresponding to the line number & column in the subgraph SDL. [PR #686](https://github.com/apollographql/federation/pull/686/files) ## v0.23.0 From d8353b5df3a6e521150c60f3a6e89a89a316fa16 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Thu, 29 Apr 2021 14:25:27 +0300 Subject: [PATCH 67/92] Update federation-integration-testsuite-js/src/utils/gql.ts --- federation-integration-testsuite-js/src/utils/gql.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/federation-integration-testsuite-js/src/utils/gql.ts b/federation-integration-testsuite-js/src/utils/gql.ts index 1e8bd1408..7c2695537 100644 --- a/federation-integration-testsuite-js/src/utils/gql.ts +++ b/federation-integration-testsuite-js/src/utils/gql.ts @@ -1,6 +1,12 @@ import { parse } from "graphql"; import stripIndent from 'strip-indent'; - +// This function signature is mostly lifted from the upstream `graphql-tag`. +// The motivation for this `gql` implementation is the desire to preserve +// the `locations` attributes from parsing, which `graphql-tag` purges. +// We don't typically use the function-invocation (e.g., `gql("string")`), +// but the same mode is preserved from `graphql-tag`. The similarities +// might be appreciated. (Or not, but having symmetry in a same-named +// function is somewhat appealing, indeed.) export function gql( literals: string | readonly string[], ...args: any[] From b1bceb163da4f47f6a3797c4bff01fc354526f5c Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 29 Apr 2021 10:59:33 -0700 Subject: [PATCH 68/92] location on selection set errors should match the field node, not the selection set's fields - update tests --- .../__tests__/keyFieldsMissingOnBase.test.ts | 9 ++++++--- .../__tests__/keyFieldsSelectInvalidType.test.ts | 13 ++++++++----- .../postComposition/keyFieldsMissingOnBase.ts | 2 +- .../postComposition/keyFieldsSelectInvalidType.ts | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index 111f83b67..abada5f73 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -1,6 +1,9 @@ import { composeServices } from '../../../compose'; import { keyFieldsMissingOnBase as validateKeyFieldsMissingOnBase } from '../'; -import { gql, graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -77,8 +80,8 @@ describe('keyFieldsMissingOnBase', () => { "code": "KEY_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 13, - "line": 1, + "column": 3, + "line": 3, }, ], "message": "[serviceA] Product -> A @key selects uid, but Product.uid was either created or overwritten by serviceB, not serviceA", diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index 9e6a4a153..707e4b3a9 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -1,6 +1,9 @@ import { composeServices } from '../../../compose'; import { keyFieldsSelectInvalidType as validateKeyFieldsSelectInvalidType } from '../'; -import { gql, graphqlErrorSerializer } from 'apollo-federation-integration-testsuite'; +import { + gql, + graphqlErrorSerializer, +} from 'apollo-federation-integration-testsuite'; import { assertCompositionSuccess } from '../../../utils'; expect.addSnapshotSerializer(graphqlErrorSerializer); @@ -87,8 +90,8 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 9, - "line": 1, + "column": 3, + "line": 3, }, ], "message": "[serviceA] Product -> A @key selects Product.featuredItem, which is an interface type. Keys cannot select interfaces.", @@ -135,8 +138,8 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 9, - "line": 1, + "column": 3, + "line": 4, }, ], "message": "[serviceA] Product -> A @key selects Product.price, which is a union type. Keys cannot select union types.", diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index 52fe873da..ecb875982 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -38,7 +38,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ 'KEY_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, - field, + matchingField.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index acaec97da..84c963c45 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -42,7 +42,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} could not be found`, - field, + namedType.astNode ?? undefined, ), ); } @@ -58,7 +58,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, - field, + matchingField.astNode ?? undefined, ), ); } @@ -73,7 +73,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, - field, + matchingField.astNode ?? undefined, ), ); } From 42dfbbd2bfb5f5550b5be47d99c4847bd97efa5a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Fri, 30 Apr 2021 12:48:54 -0700 Subject: [PATCH 69/92] use location of type reflected in error message rather than field in other service? --- .../__tests__/keyFieldsMissingOnBase.test.ts | 4 ++-- .../__tests__/keyFieldsSelectInvalidType.test.ts | 8 ++++---- .../validate/postComposition/keyFieldsMissingOnBase.ts | 2 +- .../postComposition/keyFieldsSelectInvalidType.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index abada5f73..4b4de61b3 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -80,8 +80,8 @@ describe('keyFieldsMissingOnBase', () => { "code": "KEY_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 3, - "line": 3, + "column": 1, + "line": 2, }, ], "message": "[serviceA] Product -> A @key selects uid, but Product.uid was either created or overwritten by serviceB, not serviceA", diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index 707e4b3a9..0445c4f40 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -90,8 +90,8 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 3, - "line": 3, + "column": 1, + "line": 2, }, ], "message": "[serviceA] Product -> A @key selects Product.featuredItem, which is an interface type. Keys cannot select interfaces.", @@ -138,8 +138,8 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 3, - "line": 4, + "column": 1, + "line": 2, }, ], "message": "[serviceA] Product -> A @key selects Product.price, which is a union type. Keys cannot select union types.", diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index ecb875982..c867d0bbf 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -38,7 +38,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ 'KEY_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, - matchingField.astNode ?? undefined, + namedType.astNode ?? undefined, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index 84c963c45..fb957575f 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -58,7 +58,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, - matchingField.astNode ?? undefined, + namedType.astNode ?? undefined, ), ); } @@ -73,7 +73,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, - matchingField.astNode ?? undefined, + namedType.astNode ?? undefined, ), ); } From 9b8bb3479ac2563e81f0a32904f625b66f81f013 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 17:30:25 -0700 Subject: [PATCH 70/92] VALUE_TYPE_FIELD_TYPE_MISMATCH -> select the field instead of the whole type --- .../__tests__/composeAndValidate.test.ts | 8 ++++---- .../__tests__/uniqueTypeNamesWithFields.test.ts | 16 ++++++++-------- .../validate/sdl/uniqueTypeNamesWithFields.ts | 7 +++++-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/federation-js/src/composition/__tests__/composeAndValidate.test.ts b/federation-js/src/composition/__tests__/composeAndValidate.test.ts index a3c71430a..fee3126ee 100644 --- a/federation-js/src/composition/__tests__/composeAndValidate.test.ts +++ b/federation-js/src/composition/__tests__/composeAndValidate.test.ts @@ -561,12 +561,12 @@ describe('composition of value types', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 1, - "line": 6, + "column": 10, + "line": 8, }, Object { - "column": 1, - "line": 6, + "column": 10, + "line": 8, }, ], "message": "[serviceA] Product.color -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.color\` as a String! and String respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index 7e3992941..69a99f91d 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -111,12 +111,12 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 1, - "line": 2, + "column": 3, + "line": 3, }, Object { - "column": 1, - "line": 2, + "column": 3, + "line": 3, }, ], "message": "[serviceA] Product.sku -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.sku\` as a ID! and String! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", @@ -125,12 +125,12 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 1, - "line": 2, + "column": 3, + "line": 5, }, Object { - "column": 1, - "line": 2, + "column": 3, + "line": 5, }, ], "message": "[serviceA] Product.quantity -> A field was defined differently in different services. \`serviceA\` and \`serviceB\` define \`Product.quantity\` as a Int and Int! respectively. In order to define \`Product\` in multiple places, the fields and their types must be identical.", diff --git a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts index 7245911b6..9e44172c8 100644 --- a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts +++ b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts @@ -2,6 +2,8 @@ import { GraphQLError, ASTVisitor, TypeDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, } from 'graphql'; import { SDLValidationContext } from 'graphql/validation/ValidationContext'; @@ -91,7 +93,6 @@ export function UniqueTypeNamesWithFields( return; } - const typesHaveSameFieldShape = fieldsDiff.length === 0 || fieldsDiff.every(([fieldName, types]) => { @@ -99,6 +100,8 @@ export function UniqueTypeNamesWithFields( // In this case, we can push a useful error to hint to the user that we // think they tried to define a value type, but one of the fields has a type mismatch. if (types.length === 2) { + const fieldNode = 'fields' in node && (node.fields as Readonly<(FieldDefinitionNode | InputValueDefinitionNode)[]>)?.find((field) => field.name.value === fieldName); + const duplicateFieldNode = 'fields' in duplicateTypeNode && (duplicateTypeNode.fields as Readonly<(FieldDefinitionNode | InputValueDefinitionNode)[]>)?.find(field => field.name.value === fieldName); possibleErrors.push( errorWithCode( 'VALUE_TYPE_FIELD_TYPE_MISMATCH', @@ -113,7 +116,7 @@ export function UniqueTypeNamesWithFields( }\` define \`${typeName}.${fieldName}\` as a ${types[1]} and ${ types[0] } respectively. In order to define \`${typeName}\` in multiple places, the fields and their types must be identical.`, - [node, duplicateTypeNode], + fieldNode && duplicateFieldNode ? [fieldNode, duplicateFieldNode] : undefined, ), ); return true; From d180c034a12dbd2056370f071339a7b21e7fb1d3 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 17:59:57 -0700 Subject: [PATCH 71/92] add node for each service that defines a given directive in EXECUTABLE_DIRECTIVES_IDENTICAL --- .../__tests__/executableDirectivesIdentical.test.ts | 12 ++++++++++++ .../postComposition/executableDirectivesIdentical.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts index e38d7830d..4b18359c7 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/executableDirectivesIdentical.test.ts @@ -88,6 +88,14 @@ describe('executableDirectivesIdentical', () => { "column": 1, "line": 2, }, + Object { + "column": 1, + "line": 2, + }, + Object { + "column": 1, + "line": 2, + }, ], "message": "[@stream] -> custom directives must be defined identically across all services. See below for a list of current implementations: serviceA: directive @stream on FIELD @@ -125,6 +133,10 @@ describe('executableDirectivesIdentical', () => { "column": 1, "line": 2, }, + Object { + "column": 1, + "line": 2, + }, ], "message": "[@instrument] -> custom directives must be defined identically across all services. See below for a list of current implementations: serviceA: directive @instrument(tag: String!) on FIELD diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts index 9663c12e1..dbaafe714 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesIdentical.ts @@ -42,6 +42,7 @@ export const executableDirectivesIdentical: PostCompositionValidator = ({ }); if (shouldError) { + const directiveDefinitionNodes = definitions.map(([_, directiveDefinitionNode]) => directiveDefinitionNode); errors.push( errorWithCode( 'EXECUTABLE_DIRECTIVES_IDENTICAL', @@ -51,7 +52,7 @@ export const executableDirectivesIdentical: PostCompositionValidator = ({ return `\t${serviceName}: ${print(definition)}`; }) .join('\n')}`, - directive.astNode ?? undefined, + directiveDefinitionNodes, ), ); } From 7f3a66863cb627d66e4d3b593a3ef6f1d28db982 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 18:08:50 -0700 Subject: [PATCH 72/92] more specific locations in externalTypeMismatch & externalUnused --- .../postComposition/__tests__/externalTypeMismatch.test.ts | 6 +++--- .../postComposition/__tests__/externalUnused.test.ts | 4 ++-- .../validate/postComposition/externalTypeMismatch.ts | 4 ++-- .../composition/validate/postComposition/externalUnused.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts index e3229eede..a8786f128 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalTypeMismatch.test.ts @@ -40,7 +40,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 3, + "column": 8, "line": 3, }, ], @@ -50,7 +50,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 3, + "column": 9, "line": 4, }, ], @@ -90,7 +90,7 @@ describe('validateExternalDirectivesOnSchema', () => { "code": "EXTERNAL_TYPE_MISMATCH", "locations": Array [ Object { - "column": 3, + "column": 8, "line": 3, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts index 26cf2fa7d..07372ba39 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -40,7 +40,7 @@ describe('externalUnused', () => { "code": "EXTERNAL_UNUSED", "locations": Array [ Object { - "column": 3, + "column": 16, "line": 3, }, ], @@ -382,7 +382,7 @@ describe('externalUnused', () => { "code": "EXTERNAL_UNUSED", "locations": Array [ Object { - "column": 3, + "column": 18, "line": 5, }, ], diff --git a/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts index dc859748b..313f0afbe 100644 --- a/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts +++ b/federation-js/src/composition/validate/postComposition/externalTypeMismatch.ts @@ -42,7 +42,7 @@ export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_TYPE_MISMATCH', logServiceAndType(serviceName, typeName, externalFieldName) + `the type of the @external field does not exist in the resulting composed schema`, - externalField, + externalField.type, ), ); } else if ( @@ -54,7 +54,7 @@ export const externalTypeMismatch: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_TYPE_MISMATCH', logServiceAndType(serviceName, typeName, externalFieldName) + `Type \`${externalFieldType}\` does not match the type of the original field in ${typeFederationMetadata.serviceName} (\`${matchingBaseField.type}\`)`, - externalField, + externalField.type, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/externalUnused.ts b/federation-js/src/composition/validate/postComposition/externalUnused.ts index 6334381db..d1d7bc0fa 100644 --- a/federation-js/src/composition/validate/postComposition/externalUnused.ts +++ b/federation-js/src/composition/validate/postComposition/externalUnused.ts @@ -226,7 +226,7 @@ export const externalUnused: PostCompositionValidator = ({ schema }) => { externalFieldName, ) + `is marked as @external but is not used by a @requires, @key, or @provides directive.`, - externalField, + externalField.directives, ), ); } From af5776d01256337518c78ac2e3fec8e873da66be Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 18:14:32 -0700 Subject: [PATCH 73/92] select the key directive on keyFieldsMissingOnBase select key on keyFieldsSelectInvalidType locations keysMatchBaseService reflects the parent type node if no key is found, otherwise the field argument on key --- federation-js/src/composition/utils.ts | 12 ++++++++++++ .../__tests__/keyFieldsMissingOnBase.test.ts | 2 +- .../__tests__/keyFieldsSelectInvalidType.test.ts | 4 ++-- .../__tests__/keysMatchBaseService.test.ts | 4 ++-- .../postComposition/keyFieldsMissingOnBase.ts | 11 +++++++++-- .../keyFieldsSelectInvalidType.ts | 15 +++++++++++---- .../postComposition/keysMatchBaseService.ts | 16 +++++++++++++--- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts index 9e6fd2239..adb4f913f 100644 --- a/federation-js/src/composition/utils.ts +++ b/federation-js/src/composition/utils.ts @@ -40,6 +40,7 @@ import { FederationType, FederationDirective, FederationField, + ServiceDefinition, } from './types'; import federationDirectives from '../directives'; import { assert, isNotNullOrUndefined } from '../utilities'; @@ -555,6 +556,17 @@ export function typeNodesAreEquivalent( ); } + +export function findTypeNodeInServiceList(typeName: string, serviceName: string, serviceList: ServiceDefinition[]) { + return serviceList.find( + service => service.name === serviceName + )?.typeDefs.definitions.find( + definition => + 'name' in definition + && definition.name?.value === typeName + ); +} + /** * A map of `Kind`s from their definition to their respective extensions */ diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index 4b4de61b3..b907c86fa 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -80,7 +80,7 @@ describe('keyFieldsMissingOnBase', () => { "code": "KEY_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 1, + "column": 19, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index 0445c4f40..fabd181ef 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -90,7 +90,7 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 1, + "column": 19, "line": 2, }, ], @@ -138,7 +138,7 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 1, + "column": 19, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index 20952fd44..7dff133c0 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -124,7 +124,7 @@ describe('keysMatchBaseService', () => { "code": "MULTIPLE_KEYS_ON_EXTENSION", "locations": Array [ Object { - "column": 1, + "column": 26, "line": 2, }, ], @@ -169,7 +169,7 @@ describe('keysMatchBaseService', () => { "code": "KEY_NOT_SPECIFIED", "locations": Array [ Object { - "column": 1, + "column": 26, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index c867d0bbf..37932ccd9 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -7,6 +7,7 @@ import { PostCompositionValidator } from '.'; */ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -30,6 +31,12 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ // NOTE: We don't need to warn if there is no matching field. // keyFieldsSelectInvalidType already does that :) if (matchingField) { + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const fieldNode = typeNode && 'directives' in typeNode ? + typeNode.directives?.find(directive => + directive.name.value === 'key')?.arguments?.find + (argument => argument.name.value === 'fields') : undefined; + const fieldFederationMetadata = getFederationMetadata(matchingField); // warn if not from base type OR IF IT WAS OVERWITTEN if (fieldFederationMetadata?.serviceName) { @@ -38,7 +45,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ 'KEY_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, - namedType.astNode ?? undefined, + fieldNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index fb957575f..ebfd81daf 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -7,7 +7,7 @@ import { isUnionType, GraphQLError, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -17,6 +17,7 @@ import { PostCompositionValidator } from '.'; */ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -36,13 +37,19 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ // find corresponding field for each selected field const matchingField = allFieldsInType[name]; + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const fieldNode = typeNode && 'directives' in typeNode ? + typeNode.directives?.find( + directive => directive.name.value === 'key')?.arguments?.find( + argument => argument.name.value === 'fields') : undefined; + if (!matchingField) { errors.push( errorWithCode( 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} could not be found`, - namedType.astNode ?? undefined, + fieldNode, ), ); } @@ -58,7 +65,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, - namedType.astNode ?? undefined, + fieldNode, ), ); } @@ -73,7 +80,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, - namedType.astNode ?? undefined, + fieldNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index 3134dee25..b0a5b7ac2 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -9,6 +9,7 @@ import { logServiceAndType, errorWithCode, getFederationMetadata, + findTypeNodeInServiceList, } from '../../utils'; import { PostCompositionValidator } from '.'; @@ -19,6 +20,7 @@ import { PostCompositionValidator } from '.'; */ export const keysMatchBaseService: PostCompositionValidator = function ({ schema, + serviceList, }) { const errors: GraphQLError[] = []; const types = schema.getTypeMap(); @@ -38,7 +40,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'KEY_MISSING_ON_BASE', logServiceAndType(serviceName, parentTypeName) + `appears to be an entity but no @key directives are specified on the originating type.`, - parentType.astNode ?? undefined, + findTypeNodeInServiceList(parentTypeName, serviceName, serviceList), ), ); continue; @@ -50,13 +52,17 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ .filter(([service]) => service !== serviceName) .forEach(([extendingService, keyFields = []]) => { // Extensions can't specify more than one key + const extendingServiceTypeNode = findTypeNodeInServiceList(parentTypeName, extendingService, serviceList); if (keyFields.length > 1) { errors.push( errorWithCode( 'MULTIPLE_KEYS_ON_EXTENSION', logServiceAndType(extendingService, parentTypeName) + `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, - parentType.astNode ?? undefined, + extendingServiceTypeNode && 'directives' in extendingServiceTypeNode ? + extendingServiceTypeNode.directives?.find(directive => + directive.name.value === 'key')?.arguments?.find + (argument => argument.name.value === 'fields') : undefined, ), ); return; @@ -68,6 +74,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ // services which can link one key to another via "joins". const extensionKey = printFieldSet(keyFields[0]); if (!availableKeys.includes(extensionKey)) { + const extendingServiceTypeNode = findTypeNodeInServiceList(parentTypeName, extendingService, serviceList); errors.push( errorWithCode( 'KEY_NOT_SPECIFIED', @@ -76,7 +83,10 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ `\t${availableKeys .map((fieldSet) => `@key(fields: "${fieldSet}")`) .join('\n\t')}`, - parentType.astNode ?? undefined + extendingServiceTypeNode && 'directives' in extendingServiceTypeNode ? + extendingServiceTypeNode.directives?.find(directive => + directive.name.value === 'key')?.arguments?.find + (argument => argument.name.value === 'fields') : undefined, ), ); return; From 3b75ff358eafcc049bc3ea93bfe6bd759d700b5b Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 18:42:34 -0700 Subject: [PATCH 74/92] providesFieldsMissingExternals points to the actionable field that is missing the external directive: --- .../__tests__/providesFieldsMissingExternals.test.ts | 2 +- .../postComposition/providesFieldsMissingExternal.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts index f1af70207..26bd95459 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsMissingExternals.test.ts @@ -106,7 +106,7 @@ describe('providesFieldsMissingExternal', () => { "locations": Array [ Object { "column": 3, - "line": 4, + "line": 3, }, ], "message": "[serviceB] Review.product -> provides the field \`id\` and requires Product.id to be marked as @external.", diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts index 528f6a4b0..9ecff3cc5 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsMissingExternal.ts @@ -1,5 +1,5 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { isObjectType, FieldNode, GraphQLError, InputValueDefinitionNode, FieldDefinitionNode } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -7,6 +7,7 @@ import { PostCompositionValidator } from '.'; */ export const providesFieldsMissingExternal: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -43,12 +44,14 @@ export const providesFieldsMissingExternal: PostCompositionValidator = ({ ) : undefined; if (!foundMatchingExternal) { + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); errors.push( errorWithCode( 'PROVIDES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `provides the field \`${selection.name.value}\` and requires ${fieldType}.${selection.name.value} to be marked as @external.`, - field.astNode ?? undefined, + typeNode && 'fields' in typeNode ? + (typeNode?.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?.find(field => field.name.value === selection.name.value): undefined, ), ); } From 4de9a8de27f3a782515082324f27e97d3b7f6677 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 18:50:35 -0700 Subject: [PATCH 75/92] select fields on provides directive in providesFieldsSelectInvalidType --- .../providesFieldsSelectInvalidType.test.ts | 6 +++--- .../providesFieldsSelectInvalidType.ts | 21 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts index 8c02f522b..8339f8e5a 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts @@ -97,7 +97,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 3, + "column": 26, "line": 4, }, ], @@ -156,7 +156,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 3, + "column": 26, "line": 4, }, ], @@ -227,7 +227,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 3, + "column": 26, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts index 8d2586297..c6d50e331 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -7,8 +7,10 @@ import { isNonNullType, getNullableType, isUnionType, + InputValueDefinitionNode, + FieldDefinitionNode, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -18,6 +20,7 @@ import { PostCompositionValidator } from '.'; */ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -46,13 +49,21 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ for (const selection of selections) { const name = selection.name.value; const matchingField = allFields[name]; + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const providesDirectiveNode = typeNode && 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?.find(field => field.name.value === fieldName)?. + directives?.find( + directive => directive.name.value === "provides" + )?.arguments?.find( + argument => argument.name.value === 'fields' + ) : undefined; if (!matchingField) { errors.push( errorWithCode( 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); continue; @@ -68,7 +79,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); } @@ -82,7 +93,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); } @@ -97,7 +108,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); } From f92859679366874b94fceec5564f2518a9662b4c Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 18:53:39 -0700 Subject: [PATCH 76/92] providesNotOnEntity location is on provides directive --- .../__tests__/providesNotOnEntity.test.ts | 8 ++++---- .../postComposition/providesNotOnEntity.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts index 1d365a7e6..4e9cc0fad 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts @@ -131,7 +131,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 3, + "column": 32, "line": 4, }, ], @@ -177,7 +177,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 3, + "column": 35, "line": 4, }, ], @@ -225,7 +225,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 3, + "column": 32, "line": 4, }, ], @@ -273,7 +273,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 3, + "column": 36, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts index 0a6e0a4a4..9789e8774 100644 --- a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts +++ b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts @@ -3,14 +3,16 @@ import { GraphQLError, isListType, isNonNullType, + FieldDefinitionNode, + InputValueDefinitionNode, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** * Provides directive can only be added to return types that are entities */ -export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { +export const providesNotOnEntity: PostCompositionValidator = ({ schema, serviceList }) => { const errors: GraphQLError[] = []; const types = schema.getTypeMap(); @@ -46,13 +48,21 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { // field has a @provides directive on it if (fieldFederationMetadata?.provides) { + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const providesDirectiveNode = typeNode && 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?.find(field => field.name.value === fieldName)?. + directives?.find( + directive => directive.name.value === "provides" + )?.arguments?.find( + argument => argument.name.value === 'fields' + ) : undefined; if (!isObjectType(baseType)) { errors.push( errorWithCode( 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` returns \`${field.type}\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); continue; @@ -67,7 +77,7 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema }) => { 'PROVIDES_NOT_ON_ENTITY', logServiceAndType(serviceName, typeName, fieldName) + `uses the @provides directive but \`${typeName}.${fieldName}\` does not return a type that has a @key. Try adding a @key to the \`${baseType}\` type.`, - field.astNode ?? undefined, + providesDirectiveNode, ), ); } From bdd5592d6474268c6cc1047b9023f61e3e09bb00 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Tue, 4 May 2021 20:31:53 -0700 Subject: [PATCH 77/92] requiresfieldsmissingexternals more specific location. TODO: update selection set to add location offset --- .../requiresFieldsMissingExternals.test.ts | 2 +- .../requiresFieldsMissingExternal.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts index 067954a4b..11f483a93 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts @@ -72,7 +72,7 @@ describe('requiresFieldsMissingExternal', () => { "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 3, + "column": 25, "line": 3, }, ], diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index fc8bf6b06..623eb46ff 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -1,5 +1,5 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -7,6 +7,7 @@ import { PostCompositionValidator } from '.'; */ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -40,12 +41,22 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ ) : undefined; if (!foundMatchingExternal) { + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const argumentNode = + typeNode && + 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. + find(field => field.name.value === fieldName)?.directives?.find( + directive => directive.name.value === "requires" + )?.arguments?.find( + argument => argument.name.value === "fields" + ) : undefined; errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be marked as @external.`, - field.astNode ?? undefined, + argumentNode ), ); } From e364771d7ecfc1eb781c62ebf9ff2a6ee3691a0a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 12:32:42 -0700 Subject: [PATCH 78/92] requiresFieldsMissingOnBase point to the fields arg on the requires directive --- .../requiresFieldsMissingOnBase.test.ts | 2 +- .../requiresFieldsMissingOnBase.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts index 53b1a809d..ad53338b5 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts @@ -75,7 +75,7 @@ describe('requiresFieldsMissingOnBase', () => { "code": "REQUIRES_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 28, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 1aad40e2e..39c69ab6d 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ -import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -7,6 +7,7 @@ import { PostCompositionValidator } from '.'; */ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ schema, + serviceList, }) => { const errors: GraphQLError[] = []; @@ -37,12 +38,22 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ const typeFederationMetadata = getFederationMetadata(matchingFieldOnType); if (typeFederationMetadata?.serviceName) { + const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); + const argumentNode = + typeNode && + 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. + find(field => field.name.value === fieldName)?.directives?.find( + directive => directive.name.value === "requires" + )?.arguments?.find( + argument => argument.name.value === "fields" + ) : undefined; errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, - field.astNode ?? undefined, + argumentNode ), ); } From 0a1e2033f28cfc38405bdae5ddd4f5d9c4ef868d Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 13:43:58 -0700 Subject: [PATCH 79/92] add comments to come back to when issue #705 is addressed --- .../postComposition/executableDirectivesInAllServices.ts | 2 ++ .../validate/postComposition/keysMatchBaseService.ts | 2 ++ .../validate/postComposition/requiresFieldsMissingExternal.ts | 2 ++ .../validate/postComposition/requiresFieldsMissingOnBase.ts | 2 ++ .../src/composition/validate/sdl/uniqueTypeNamesWithFields.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts index a19ea4159..a9d0c540e 100644 --- a/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts +++ b/federation-js/src/composition/validate/postComposition/executableDirectivesInAllServices.ts @@ -50,6 +50,8 @@ export const executableDirectivesInAllServices: PostCompositionValidator = ({ `Custom directives must be implemented in every service. The following services do not implement the @${ directive.name } directive: ${serviceNamesWithoutDirective.join(', ')}.`, + // TODO (Issue #705): when we can associate locations to service names, we should expose + // locations of the services where this directive is not used directive.astNode ?? undefined, ), ); diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index b0a5b7ac2..c6979af02 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -83,6 +83,8 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ `\t${availableKeys .map((fieldSet) => `@key(fields: "${fieldSet}")`) .join('\n\t')}`, + // TODO (Issue #705): when we can associate locations to service names, we should expose the location + // of each of the key directives extendingServiceTypeNode && 'directives' in extendingServiceTypeNode ? extendingServiceTypeNode.directives?.find(directive => directive.name.value === 'key')?.arguments?.find diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index 623eb46ff..4ce61164b 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -56,6 +56,8 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_EXTERNAL', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be marked as @external.`, + // TODO (Issue #705): when we can associate locations to service name's this should be the node of the + // field on the other service that needs to be marked external argumentNode ), ); diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 39c69ab6d..4fa296656 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -53,6 +53,8 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ 'REQUIRES_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, fieldName) + `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, + // TODO (Issue #705): when we can associate locations to service name's this should be the node of the + // field on the other service that needs to be marked external argumentNode ), ); diff --git a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts index 9e44172c8..106d29315 100644 --- a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts +++ b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts @@ -116,6 +116,8 @@ export function UniqueTypeNamesWithFields( }\` define \`${typeName}.${fieldName}\` as a ${types[1]} and ${ types[0] } respectively. In order to define \`${typeName}\` in multiple places, the fields and their types must be identical.`, + // TODO (Issue #705): when we can associate locations to service names + // add locations for each service where this field was defined fieldNode && duplicateFieldNode ? [fieldNode, duplicateFieldNode] : undefined, ), ); From 65a6cc6156a77e1b02315d1903e8d666bab5cae9 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 13:47:58 -0700 Subject: [PATCH 80/92] externalUsedOnBase more specific location -> to the @external directive --- .../preComposition/__tests__/externalUsedOnBase.test.ts | 2 +- .../composition/validate/preComposition/externalUsedOnBase.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts index d6ab15f4e..51d72ee63 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/externalUsedOnBase.test.ts @@ -47,7 +47,7 @@ describe('externalUsedOnBase', () => { "code": "EXTERNAL_USED_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 16, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts index cd7a6bda6..053699eb0 100644 --- a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts @@ -29,7 +29,7 @@ export const externalUsedOnBase = ({ field.name.value, ) + `Found extraneous @external directive. @external cannot be used on base types.`, - field, + field.directives.find(directive => directive.name.value === "external"), ), ); } From c6409141b68c7d1ef0e5816fef899b3534d3a490 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 14:04:51 -0700 Subject: [PATCH 81/92] requiresUsedOnBase - more specific -> requires directive --- .../preComposition/__tests__/requiresUsedOnBase.test.ts | 2 +- .../composition/validate/preComposition/requiresUsedOnBase.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts index 253497961..4c7d9d50b 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/requiresUsedOnBase.test.ts @@ -47,7 +47,7 @@ describe('requiresUsedOnBase', () => { "code": "REQUIRES_USED_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 16, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts index db39fbbff..9e1764106 100644 --- a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts @@ -29,7 +29,7 @@ export const requiresUsedOnBase = ({ field.name.value, ) + `Found extraneous @requires directive. @requires cannot be used on base types.`, - field, + field.directives.find(directive => directive.name.value === name), ), ); } From 8b638e8ed87c544913e0b4255a78dae684daeefb Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 14:12:07 -0700 Subject: [PATCH 82/92] duplicateEnumValue error location is on the duplicate value rather than the enum --- .../preComposition/__tests__/duplicateEnumValue.test.ts | 4 ++-- .../composition/validate/preComposition/duplicateEnumValue.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts index 6cf31ec13..73d17815c 100644 --- a/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts +++ b/federation-js/src/composition/validate/preComposition/__tests__/duplicateEnumValue.test.ts @@ -70,8 +70,8 @@ describe('duplicateEnumValue', () => { "code": "DUPLICATE_ENUM_VALUE", "locations": Array [ Object { - "column": 1, - "line": 18, + "column": 3, + "line": 20, }, ], "message": "[serviceA] ProductType.BOOK -> The enum, \`ProductType\` has multiple definitions of the \`BOOK\` value.", diff --git a/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts index 679377d5c..29a60e77b 100644 --- a/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts +++ b/federation-js/src/composition/validate/preComposition/duplicateEnumValue.ts @@ -55,7 +55,7 @@ export const duplicateEnumValue = ({ 'DUPLICATE_ENUM_VALUE', logServiceAndType(serviceName, name, valueName) + `The enum, \`${name}\` has multiple definitions of the \`${valueName}\` value.`, - definition, + definition.values?.find(enumValue => enumValue.name.value === valueName), ), ); return; From ae3e2ca28f42b6192a3d87242b26892cee2fa87f Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 14:19:08 -0700 Subject: [PATCH 83/92] uniqueTypeNamesWithFields points to return types --- .../sdl/__tests__/uniqueTypeNamesWithFields.test.ts | 8 ++++---- .../composition/validate/sdl/uniqueTypeNamesWithFields.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts index 69a99f91d..2cfd2f7ec 100644 --- a/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts +++ b/federation-js/src/composition/validate/sdl/__tests__/uniqueTypeNamesWithFields.test.ts @@ -111,11 +111,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 3, + "column": 8, "line": 3, }, Object { - "column": 3, + "column": 8, "line": 3, }, ], @@ -125,11 +125,11 @@ describe('UniqueTypeNamesWithFields', () => { "code": "VALUE_TYPE_FIELD_TYPE_MISMATCH", "locations": Array [ Object { - "column": 3, + "column": 13, "line": 5, }, Object { - "column": 3, + "column": 13, "line": 5, }, ], diff --git a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts index 106d29315..30a6c84b9 100644 --- a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts +++ b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts @@ -118,7 +118,7 @@ export function UniqueTypeNamesWithFields( } respectively. In order to define \`${typeName}\` in multiple places, the fields and their types must be identical.`, // TODO (Issue #705): when we can associate locations to service names // add locations for each service where this field was defined - fieldNode && duplicateFieldNode ? [fieldNode, duplicateFieldNode] : undefined, + fieldNode && duplicateFieldNode ? [fieldNode.type, duplicateFieldNode.type] : undefined, ), ); return true; From fcb15c414c2e997f7780101467314c1512932363 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 14:27:58 -0700 Subject: [PATCH 84/92] another comment --- .../src/composition/validate/sdl/uniqueTypeNamesWithFields.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts index 30a6c84b9..182761248 100644 --- a/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts +++ b/federation-js/src/composition/validate/sdl/uniqueTypeNamesWithFields.ts @@ -145,6 +145,8 @@ export function UniqueTypeNamesWithFields( }\` define \`${name}\` as a ${types[1]} and ${ types[0] } respectively. In order to define \`${typeName}\` in multiple places, the input values and their types must be identical.`, + // TODO (Issue #705): when we can associate locations to service names + // add locations for each service where this field was defined [node, duplicateTypeNode], ), ); From 4a94b788ecdb5957d73a0bfd1c98a0e0682a682a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Wed, 5 May 2021 14:48:44 -0700 Subject: [PATCH 85/92] pass accurate nodes & update the error message to match for uniqueFieldDefinitionNodes --- .../src/composition/__tests__/compose.test.ts | 16 ++++++++-------- .../validate/sdl/uniqueFieldDefinitionNames.ts | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/federation-js/src/composition/__tests__/compose.test.ts b/federation-js/src/composition/__tests__/compose.test.ts index 82c2a129c..5ff699863 100644 --- a/federation-js/src/composition/__tests__/compose.test.ts +++ b/federation-js/src/composition/__tests__/compose.test.ts @@ -415,10 +415,10 @@ describe('composeServices', () => { "locations": Array [ Object { "column": 3, - "line": 3, + "line": 4, }, ], - "message": "[serviceA] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + "message": "[serviceB] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", }, ] `); @@ -472,20 +472,20 @@ describe('composeServices', () => { "locations": Array [ Object { "column": 3, - "line": 3, + "line": 7, }, ], - "message": "[serviceA] Product.sku -> Field \\"Product.sku\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + "message": "[serviceB] Product.sku -> Field \\"Product.sku\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", }, Object { "code": "MISSING_ERROR", "locations": Array [ Object { "column": 3, - "line": 4, + "line": 8, }, ], - "message": "[serviceA] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + "message": "[serviceB] Product.name -> Field \\"Product.name\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", }, ] `); @@ -668,10 +668,10 @@ describe('composeServices', () => { "locations": Array [ Object { "column": 3, - "line": 3, + "line": 7, }, ], - "message": "[serviceA] Product.id -> Field \\"Product.id\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", + "message": "[serviceB] Product.id -> Field \\"Product.id\\" already exists in the schema. It cannot also be defined in this type extension. If this is meant to be an external field, add the \`@external\` directive.", }, ] `); diff --git a/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts b/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts index 63f7364fa..b5e761b0d 100644 --- a/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts +++ b/federation-js/src/composition/validate/sdl/uniqueFieldDefinitionNames.ts @@ -101,14 +101,16 @@ export function UniqueFieldDefinitionNames( const fieldName = fieldDef.name.value; if (hasField(existingTypeMap[typeName], fieldName)) { + const type = existingTypeMap[typeName]; context.reportError( new GraphQLError( existedFieldDefinitionNameMessage( typeName, fieldName, - existingTypeMap[typeName].astNode!.serviceName!, + node.serviceName ?? '', ), - fieldDef.name, + isObjectType(type) || isInterfaceType(type) || isInputObjectType(type) ? + type.getFields()[fieldName].astNode : undefined, ), ); } else if (fieldNames[fieldName]) { From 8eda3db02aad444083f5044dba93bc97e19c6b5b Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 6 May 2021 17:23:56 -0700 Subject: [PATCH 86/92] point to only the external directive on externalUnused, externalMismatch, externalUsedOnBase --- .../__tests__/externalMissingOnBase.test.ts | 6 +-- .../__tests__/externalUnused.test.ts | 48 +++++++++++++++++++ .../postComposition/externalMissingOnBase.ts | 6 +-- .../postComposition/externalUnused.ts | 2 +- .../preComposition/externalUsedOnBase.ts | 4 +- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts index 6f0f2be21..c03393f68 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalMissingOnBase.test.ts @@ -50,7 +50,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 15, "line": 4, }, ], @@ -60,7 +60,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 13, "line": 5, }, ], @@ -100,7 +100,7 @@ describe('externalMissingOnBase', () => { "code": "EXTERNAL_MISSING_ON_BASE", "locations": Array [ Object { - "column": 3, + "column": 22, "line": 3, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts index 07372ba39..59c659395 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -391,4 +391,52 @@ describe('externalUnused', () => { ] `); }); + + it('point to the right location on error when multiple directives are on the field in question', () => { + const serviceA = { + typeDefs: gql` + type Car implements Vehicle @key(fields: "id") { + id: ID! + speed: Int + wheelSize: Int + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceA', + }; + const serviceB = { + typeDefs: gql` + extend type Car implements Vehicle @key(fields: "id") { + id: ID! @external + speed: Int @external + wheelSize: Int @requires(fields: "id") @external + } + interface Vehicle { + id: ID! + speed: Int + } + `, + name: 'serviceB', + }; + const serviceList = [serviceA, serviceB]; + const { schema } = composeServices(serviceList); + const errors = validateExternalUnused({ schema, serviceList }); + expect(errors).toMatchInlineSnapshot(` + Array [ + Object { + "code": "EXTERNAL_UNUSED", + "locations": Array [ + Object { + "column": 42, + "line": 5, + }, + ], + "message": "[serviceB] Car.wheelSize -> is marked as @external but is not used by a @requires, @key, or @provides directive.", + }, + ] + `); + }); }); diff --git a/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts index 5b5c4a3b6..347e2a563 100644 --- a/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/externalMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findDirectivesOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -35,7 +35,7 @@ export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, externalFieldName) + `marked @external but ${externalFieldName} is not defined on the base service of ${typeName} (${typeFederationMetadata.serviceName})`, - externalField, + findDirectivesOnNode(externalField, 'external'), ), ); continue; @@ -51,7 +51,7 @@ export const externalMissingOnBase: PostCompositionValidator = ({ schema }) => { 'EXTERNAL_MISSING_ON_BASE', logServiceAndType(serviceName, typeName, externalFieldName) + `marked @external but ${externalFieldName} was defined in ${fieldFederationMetadata.serviceName}, not in the service that owns ${typeName} (${typeFederationMetadata.serviceName})`, - externalField, + findDirectivesOnNode(externalField, 'external'), ), ); } diff --git a/federation-js/src/composition/validate/postComposition/externalUnused.ts b/federation-js/src/composition/validate/postComposition/externalUnused.ts index d1d7bc0fa..b88da6df9 100644 --- a/federation-js/src/composition/validate/postComposition/externalUnused.ts +++ b/federation-js/src/composition/validate/postComposition/externalUnused.ts @@ -226,7 +226,7 @@ export const externalUnused: PostCompositionValidator = ({ schema }) => { externalFieldName, ) + `is marked as @external but is not used by a @requires, @key, or @provides directive.`, - externalField.directives, + findDirectivesOnNode(externalField, 'external'), ), ); } diff --git a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts index 053699eb0..24bf3ad3a 100644 --- a/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/externalUsedOnBase.ts @@ -1,7 +1,7 @@ import { visit, GraphQLError } from 'graphql'; import { ServiceDefinition } from '../../types'; -import { logServiceAndType, errorWithCode } from '../../utils'; +import { logServiceAndType, errorWithCode, findDirectivesOnNode } from '../../utils'; /** * - There are no fields with @external on base type definitions @@ -29,7 +29,7 @@ export const externalUsedOnBase = ({ field.name.value, ) + `Found extraneous @external directive. @external cannot be used on base types.`, - field.directives.find(directive => directive.name.value === "external"), + findDirectivesOnNode(field, 'external') ), ); } From a79ae166c74a1eff06889cc5b810fea86761773d Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 6 May 2021 18:38:04 -0700 Subject: [PATCH 87/92] point to the selection set on the @key errors --- federation-js/src/composition/utils.ts | 29 +++++++++++++++---- .../__tests__/keyFieldsMissingOnBase.test.ts | 2 +- .../keyFieldsSelectInvalidType.test.ts | 4 +-- .../__tests__/keysMatchBaseService.test.ts | 4 +-- .../postComposition/keyFieldsMissingOnBase.ts | 9 ++---- .../keyFieldsSelectInvalidType.ts | 13 ++++----- .../postComposition/keysMatchBaseService.ts | 15 +++------- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts index adb4f913f..4db2c9a68 100644 --- a/federation-js/src/composition/utils.ts +++ b/federation-js/src/composition/utils.ts @@ -31,7 +31,7 @@ import { OperationTypeNode, isDirective, isNamedType, - SchemaDefinitionNode, + DefinitionNode, } from 'graphql'; import { ExternalFieldDefinition, @@ -62,20 +62,39 @@ export function mapFieldNamesToServiceName( export function findDirectivesOnNode( node: Maybe< - | TypeDefinitionNode - | TypeExtensionNode | FieldDefinitionNode - | SchemaDefinitionNode + | FieldNode + | DefinitionNode >, directiveName: string, ) { - return node && node.directives + return node && 'directives' in node && node.directives ? node.directives.filter( directive => directive.name.value === directiveName, ) : []; } +export function findSelectionSetOnNode( + node: Maybe< + | FieldDefinitionNode + | DefinitionNode + >, + directiveName: string, + elementInSelectionSet: string, +) { + // assume there is only one directive with this name that selects this element + return node && 'directives' in node && node.directives + ? node.directives.find( + directive => + directive.name.value === directiveName && directive.arguments?.some( + argument => argument.value.kind === "StringValue" && + argument.value.value.includes(elementInSelectionSet) + ))?.arguments?.find( + argument => argument.name.value === 'fields')?.value + : undefined; +} + export function stripExternalFieldsFromTypeDefs( typeDefs: DocumentNode, serviceName: string, diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts index b907c86fa..a21875ede 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsMissingOnBase.test.ts @@ -80,7 +80,7 @@ describe('keyFieldsMissingOnBase', () => { "code": "KEY_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 19, + "column": 27, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts index fabd181ef..a56a4a721 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keyFieldsSelectInvalidType.test.ts @@ -90,7 +90,7 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 19, + "column": 27, "line": 2, }, ], @@ -138,7 +138,7 @@ describe('keyFieldsSelectInvalidType', () => { "code": "KEY_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 19, + "column": 27, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts index 7dff133c0..3266cba50 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/keysMatchBaseService.test.ts @@ -124,7 +124,7 @@ describe('keysMatchBaseService', () => { "code": "MULTIPLE_KEYS_ON_EXTENSION", "locations": Array [ Object { - "column": 26, + "column": 1, "line": 2, }, ], @@ -169,7 +169,7 @@ describe('keysMatchBaseService', () => { "code": "KEY_NOT_SPECIFIED", "locations": Array [ Object { - "column": 26, + "column": 34, "line": 2, }, ], diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index 37932ccd9..211c8c61b 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -32,10 +32,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ // keyFieldsSelectInvalidType already does that :) if (matchingField) { const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const fieldNode = typeNode && 'directives' in typeNode ? - typeNode.directives?.find(directive => - directive.name.value === 'key')?.arguments?.find - (argument => argument.name.value === 'fields') : undefined; + const selectionSetNode = findSelectionSetOnNode(typeNode, 'key', name); const fieldFederationMetadata = getFederationMetadata(matchingField); // warn if not from base type OR IF IT WAS OVERWITTEN @@ -45,7 +42,7 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ 'KEY_FIELDS_MISSING_ON_BASE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} was either created or overwritten by ${fieldFederationMetadata.serviceName}, not ${serviceName}`, - fieldNode, + selectionSetNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index ebfd81daf..478f9ed47 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -7,7 +7,7 @@ import { isUnionType, GraphQLError, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -38,10 +38,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ // find corresponding field for each selected field const matchingField = allFieldsInType[name]; const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const fieldNode = typeNode && 'directives' in typeNode ? - typeNode.directives?.find( - directive => directive.name.value === 'key')?.arguments?.find( - argument => argument.name.value === 'fields') : undefined; + const selectionSetNode = findSelectionSetOnNode(typeNode, 'key', name); if (!matchingField) { errors.push( @@ -49,7 +46,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${name}, but ${typeName}.${name} could not be found`, - fieldNode, + selectionSetNode, ), ); } @@ -65,7 +62,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is an interface type. Keys cannot select interfaces.`, - fieldNode, + selectionSetNode, ), ); } @@ -80,7 +77,7 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ 'KEY_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName) + `A @key selects ${typeName}.${name}, which is a union type. Keys cannot select union types.`, - fieldNode, + selectionSetNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index c6979af02..f71055088 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -10,6 +10,7 @@ import { errorWithCode, getFederationMetadata, findTypeNodeInServiceList, + findSelectionSetOnNode, } from '../../utils'; import { PostCompositionValidator } from '.'; @@ -59,10 +60,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ 'MULTIPLE_KEYS_ON_EXTENSION', logServiceAndType(extendingService, parentTypeName) + `is extended from service ${serviceName} but specifies multiple @key directives. Extensions may only specify one @key.`, - extendingServiceTypeNode && 'directives' in extendingServiceTypeNode ? - extendingServiceTypeNode.directives?.find(directive => - directive.name.value === 'key')?.arguments?.find - (argument => argument.name.value === 'fields') : undefined, + extendingServiceTypeNode, ), ); return; @@ -73,8 +71,8 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ // In the future, `@key`s just need to be "reachable" through a number of // services which can link one key to another via "joins". const extensionKey = printFieldSet(keyFields[0]); + const selectionSetNode = findSelectionSetOnNode(extendingServiceTypeNode, 'key', extensionKey); if (!availableKeys.includes(extensionKey)) { - const extendingServiceTypeNode = findTypeNodeInServiceList(parentTypeName, extendingService, serviceList); errors.push( errorWithCode( 'KEY_NOT_SPECIFIED', @@ -83,12 +81,7 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ `\t${availableKeys .map((fieldSet) => `@key(fields: "${fieldSet}")`) .join('\n\t')}`, - // TODO (Issue #705): when we can associate locations to service names, we should expose the location - // of each of the key directives - extendingServiceTypeNode && 'directives' in extendingServiceTypeNode ? - extendingServiceTypeNode.directives?.find(directive => - directive.name.value === 'key')?.arguments?.find - (argument => argument.name.value === 'fields') : undefined, + selectionSetNode, ), ); return; From 0ac045df645d24570e71a8cf1a4136205ceb9efb Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 6 May 2021 18:56:47 -0700 Subject: [PATCH 88/92] make the provides errors more specfic to the selection set, or less specific in the case where the error is about the number of key directives --- federation-js/src/composition/utils.ts | 2 ++ .../providesFieldsSelectInvalidType.test.ts | 6 ++--- .../__tests__/providesNotOnEntity.test.ts | 8 +++---- .../providesFieldsSelectInvalidType.ts | 22 +++++++++---------- .../postComposition/providesNotOnEntity.ts | 20 ++++++++++------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts index 4db2c9a68..6a589702e 100644 --- a/federation-js/src/composition/utils.ts +++ b/federation-js/src/composition/utils.ts @@ -63,6 +63,7 @@ export function mapFieldNamesToServiceName( export function findDirectivesOnNode( node: Maybe< | FieldDefinitionNode + | InputValueDefinitionNode | FieldNode | DefinitionNode >, @@ -78,6 +79,7 @@ export function findDirectivesOnNode( export function findSelectionSetOnNode( node: Maybe< | FieldDefinitionNode + | InputValueDefinitionNode | DefinitionNode >, directiveName: string, diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts index 8339f8e5a..966340d36 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesFieldsSelectInvalidType.test.ts @@ -97,7 +97,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 26, + "column": 34, "line": 4, }, ], @@ -156,7 +156,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 26, + "column": 34, "line": 4, }, ], @@ -227,7 +227,7 @@ describe('providesFieldsSelectInvalidType', () => { "code": "PROVIDES_FIELDS_SELECT_INVALID_TYPE", "locations": Array [ Object { - "column": 26, + "column": 34, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts index 4e9cc0fad..68b3f27ef 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/providesNotOnEntity.test.ts @@ -131,7 +131,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 32, + "column": 22, "line": 4, }, ], @@ -177,7 +177,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 35, + "column": 25, "line": 4, }, ], @@ -225,7 +225,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 32, + "column": 22, "line": 4, }, ], @@ -273,7 +273,7 @@ describe('providesNotOnEntity', () => { "code": "PROVIDES_NOT_ON_ENTITY", "locations": Array [ Object { - "column": 36, + "column": 26, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts index c6d50e331..318fd9add 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -10,7 +10,7 @@ import { InputValueDefinitionNode, FieldDefinitionNode, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -50,20 +50,18 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ const name = selection.name.value; const matchingField = allFields[name]; const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const providesDirectiveNode = typeNode && 'fields' in typeNode ? - (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?.find(field => field.name.value === fieldName)?. - directives?.find( - directive => directive.name.value === "provides" - )?.arguments?.find( - argument => argument.name.value === 'fields' - ) : undefined; + const fieldNode = typeNode && 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[]) + ?.find(field => field.name.value === fieldName) : undefined; + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'provides', name); + if (!matchingField) { errors.push( errorWithCode( 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${name}, but ${fieldType.name}.${name} could not be found`, - providesDirectiveNode, + selectionSetNode, ), ); continue; @@ -79,7 +77,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a list type. A field cannot @provide lists.`, - providesDirectiveNode, + selectionSetNode, ), ); } @@ -93,7 +91,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is an interface type. A field cannot @provide interfaces.`, - providesDirectiveNode, + selectionSetNode, ), ); } @@ -108,7 +106,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ 'PROVIDES_FIELDS_SELECT_INVALID_TYPE', logServiceAndType(serviceName, typeName, fieldName) + `A @provides selects ${fieldType.name}.${name}, which is a union type. A field cannot @provide union types.`, - providesDirectiveNode, + selectionSetNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts index 9789e8774..e28653653 100644 --- a/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts +++ b/federation-js/src/composition/validate/postComposition/providesNotOnEntity.ts @@ -6,7 +6,13 @@ import { FieldDefinitionNode, InputValueDefinitionNode, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { + logServiceAndType, + errorWithCode, + getFederationMetadata, + findTypeNodeInServiceList, + findDirectivesOnNode +} from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -49,13 +55,11 @@ export const providesNotOnEntity: PostCompositionValidator = ({ schema, serviceL // field has a @provides directive on it if (fieldFederationMetadata?.provides) { const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const providesDirectiveNode = typeNode && 'fields' in typeNode ? - (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?.find(field => field.name.value === fieldName)?. - directives?.find( - directive => directive.name.value === "provides" - )?.arguments?.find( - argument => argument.name.value === 'fields' - ) : undefined; + const fieldNode = typeNode && 'fields' in typeNode ? + (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[]) + ?.find(field => field.name.value === fieldName) : undefined; + const providesDirectiveNode = findDirectivesOnNode(fieldNode, 'provides'); + if (!isObjectType(baseType)) { errors.push( errorWithCode( From 0e4302e8240ad7166e4bec926f6ec4ff42f9bb74 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Thu, 6 May 2021 19:03:19 -0700 Subject: [PATCH 89/92] make requires errors more specific to the selection set --- .../requiresFieldsMissingExternals.test.ts | 2 +- .../__tests__/requiresFieldsMissingOnBase.test.ts | 2 +- .../requiresFieldsMissingExternal.ts | 13 +++++-------- .../postComposition/requiresFieldsMissingOnBase.ts | 13 +++++-------- .../validate/preComposition/requiresUsedOnBase.ts | 4 ++-- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts index 11f483a93..e02c46708 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingExternals.test.ts @@ -72,7 +72,7 @@ describe('requiresFieldsMissingExternal', () => { "code": "REQUIRES_FIELDS_MISSING_EXTERNAL", "locations": Array [ Object { - "column": 25, + "column": 33, "line": 3, }, ], diff --git a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts index ad53338b5..51683e6df 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/requiresFieldsMissingOnBase.test.ts @@ -75,7 +75,7 @@ describe('requiresFieldsMissingOnBase', () => { "code": "REQUIRES_FIELDS_MISSING_ON_BASE", "locations": Array [ Object { - "column": 28, + "column": 36, "line": 4, }, ], diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index 4ce61164b..808ded185 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -42,15 +42,12 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ : undefined; if (!foundMatchingExternal) { const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const argumentNode = + const fieldNode = typeNode && 'fields' in typeNode ? (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. - find(field => field.name.value === fieldName)?.directives?.find( - directive => directive.name.value === "requires" - )?.arguments?.find( - argument => argument.name.value === "fields" - ) : undefined; + find(field => field.name.value === fieldName) : undefined; + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', selection.name.value); errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_EXTERNAL', @@ -58,7 +55,7 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ `requires the field \`${selection.name.value}\` to be marked as @external.`, // TODO (Issue #705): when we can associate locations to service name's this should be the node of the // field on the other service that needs to be marked external - argumentNode + selectionSetNode, ), ); } diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 4fa296656..0d4b0762c 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -39,15 +39,12 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ if (typeFederationMetadata?.serviceName) { const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const argumentNode = + const fieldNode = typeNode && 'fields' in typeNode ? (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. - find(field => field.name.value === fieldName)?.directives?.find( - directive => directive.name.value === "requires" - )?.arguments?.find( - argument => argument.name.value === "fields" - ) : undefined; + find(field => field.name.value === fieldName) : undefined; + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', selection.name.value); errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_ON_BASE', @@ -55,7 +52,7 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ `requires the field \`${selection.name.value}\` to be @external. @external fields must exist on the base type, not an extension.`, // TODO (Issue #705): when we can associate locations to service name's this should be the node of the // field on the other service that needs to be marked external - argumentNode + selectionSetNode, ), ); } diff --git a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts index 9e1764106..bed62b67f 100644 --- a/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts +++ b/federation-js/src/composition/validate/preComposition/requiresUsedOnBase.ts @@ -1,7 +1,7 @@ import { GraphQLError, visit } from 'graphql'; import { ServiceDefinition } from '../../types'; -import { logServiceAndType, errorWithCode } from '../../utils'; +import { logServiceAndType, errorWithCode, findDirectivesOnNode } from '../../utils'; /** * - There are no fields with @requires on base type definitions @@ -29,7 +29,7 @@ export const requiresUsedOnBase = ({ field.name.value, ) + `Found extraneous @requires directive. @requires cannot be used on base types.`, - field.directives.find(directive => directive.name.value === name), + findDirectivesOnNode(field, 'requires'), ), ); } From 0b5a18840017226b04a9ed6b1bd885aafd729a7a Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Fri, 7 May 2021 12:04:12 -0700 Subject: [PATCH 90/92] search for selection set by whole printed selection set, rather than single field. Update test name to be plural & write definition for selection set search --- .../src/composition/__tests__/utils.test.ts | 2 +- federation-js/src/composition/utils.ts | 59 ++++++++++++++----- .../__tests__/externalUnused.test.ts | 2 +- .../postComposition/keyFieldsMissingOnBase.ts | 5 +- .../keyFieldsSelectInvalidType.ts | 5 +- .../postComposition/keysMatchBaseService.ts | 14 ++--- .../providesFieldsSelectInvalidType.ts | 4 +- .../requiresFieldsMissingExternal.ts | 4 +- .../requiresFieldsMissingOnBase.ts | 4 +- 9 files changed, 61 insertions(+), 38 deletions(-) diff --git a/federation-js/src/composition/__tests__/utils.test.ts b/federation-js/src/composition/__tests__/utils.test.ts index 5301bef01..1eee09b2f 100644 --- a/federation-js/src/composition/__tests__/utils.test.ts +++ b/federation-js/src/composition/__tests__/utils.test.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; import deepFreeze from 'deep-freeze'; -import { stripExternalFieldsFromTypeDefs } from '../utils'; +import { findSelectionSetOnNode, isDirectiveDefinitionNode, stripExternalFieldsFromTypeDefs } from '../utils'; import { astSerializer } from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts index 6a589702e..4425f5947 100644 --- a/federation-js/src/composition/utils.ts +++ b/federation-js/src/composition/utils.ts @@ -31,7 +31,11 @@ import { OperationTypeNode, isDirective, isNamedType, - DefinitionNode, + EnumValueDefinitionNode, + SchemaDefinitionNode, + ExecutableDefinitionNode, + TypeSystemExtensionNode, + stripIgnoredCharacters, } from 'graphql'; import { ExternalFieldDefinition, @@ -49,6 +53,10 @@ export function isStringValueNode(node: any): node is StringValueNode { return node.kind === Kind.STRING; } +export function isDirectiveDefinitionNode(node: any): node is DirectiveDefinitionNode { + return node.kind === Kind.DIRECTIVE_DEFINITION; +} + // Create a map of { fieldName: serviceName } for each field. export function mapFieldNamesToServiceName( fields: ReadonlyArray, @@ -64,37 +72,56 @@ export function findDirectivesOnNode( node: Maybe< | FieldDefinitionNode | InputValueDefinitionNode - | FieldNode - | DefinitionNode + | EnumValueDefinitionNode + | SchemaDefinitionNode + | ExecutableDefinitionNode + | SelectionNode + | TypeDefinitionNode + | TypeSystemExtensionNode >, directiveName: string, ) { - return node && 'directives' in node && node.directives - ? node.directives.filter( + return node?.directives?.filter( directive => directive.name.value === directiveName, - ) - : []; + ) ?? []; +} + +export function printFieldSet(selections: readonly SelectionNode[]): string { + return selections + .map((selection) => stripIgnoredCharacters(print(selection))) + .join(' '); } +/** + * Find a matching selection set on a node given it's string form, + * directive name and the node to search on + * + * @param node + * @param directiveName + * @param printedSelectionSet + * @returns + */ export function findSelectionSetOnNode( node: Maybe< | FieldDefinitionNode | InputValueDefinitionNode - | DefinitionNode + | EnumValueDefinitionNode + | SchemaDefinitionNode + | ExecutableDefinitionNode + | SelectionNode + | TypeDefinitionNode + | TypeSystemExtensionNode >, directiveName: string, - elementInSelectionSet: string, + printedSelectionSet: string, ) { - // assume there is only one directive with this name that selects this element - return node && 'directives' in node && node.directives - ? node.directives.find( + return node?.directives?.find( directive => directive.name.value === directiveName && directive.arguments?.some( - argument => argument.value.kind === "StringValue" && - argument.value.value.includes(elementInSelectionSet) + argument => isStringValueNode(argument.value) && + argument.value.value.includes(printedSelectionSet) ))?.arguments?.find( - argument => argument.name.value === 'fields')?.value - : undefined; + argument => argument.name.value === 'fields')?.value; } export function stripExternalFieldsFromTypeDefs( diff --git a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts index 59c659395..e8e2226d7 100644 --- a/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts +++ b/federation-js/src/composition/validate/postComposition/__tests__/externalUnused.test.ts @@ -392,7 +392,7 @@ describe('externalUnused', () => { `); }); - it('point to the right location on error when multiple directives are on the field in question', () => { + it('points to the right location on error when multiple directives are on the field in question', () => { const serviceA = { typeDefs: gql` type Car implements Vehicle @key(fields: "id") { diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts index 211c8c61b..537a0ba83 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, isDirectiveDefinitionNode, printFieldSet } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -32,7 +32,8 @@ export const keyFieldsMissingOnBase: PostCompositionValidator = ({ // keyFieldsSelectInvalidType already does that :) if (matchingField) { const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const selectionSetNode = findSelectionSetOnNode(typeNode, 'key', name); + const selectionSetNode = !isDirectiveDefinitionNode(typeNode) ? + findSelectionSetOnNode(typeNode, 'key', printFieldSet(selectionSet)) : undefined; const fieldFederationMetadata = getFederationMetadata(matchingField); // warn if not from base type OR IF IT WAS OVERWITTEN diff --git a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts index 478f9ed47..0336c6d7e 100644 --- a/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/keyFieldsSelectInvalidType.ts @@ -7,7 +7,7 @@ import { isUnionType, GraphQLError, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, isDirectiveDefinitionNode, printFieldSet } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -38,7 +38,8 @@ export const keyFieldsSelectInvalidType: PostCompositionValidator = ({ // find corresponding field for each selected field const matchingField = allFieldsInType[name]; const typeNode = findTypeNodeInServiceList(typeName, serviceName, serviceList); - const selectionSetNode = findSelectionSetOnNode(typeNode, 'key', name); + const selectionSetNode = !isDirectiveDefinitionNode(typeNode) ? + findSelectionSetOnNode(typeNode, 'key', printFieldSet(selectionSet)) : undefined; if (!matchingField) { errors.push( diff --git a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts index f71055088..dff864800 100644 --- a/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts +++ b/federation-js/src/composition/validate/postComposition/keysMatchBaseService.ts @@ -1,9 +1,6 @@ import { isObjectType, GraphQLError, - SelectionNode, - stripIgnoredCharacters, - print, } from 'graphql'; import { logServiceAndType, @@ -11,6 +8,8 @@ import { getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, + isDirectiveDefinitionNode, + printFieldSet, } from '../../utils'; import { PostCompositionValidator } from '.'; @@ -71,7 +70,8 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ // In the future, `@key`s just need to be "reachable" through a number of // services which can link one key to another via "joins". const extensionKey = printFieldSet(keyFields[0]); - const selectionSetNode = findSelectionSetOnNode(extendingServiceTypeNode, 'key', extensionKey); + const selectionSetNode = !isDirectiveDefinitionNode(extendingServiceTypeNode) ? + findSelectionSetOnNode(extendingServiceTypeNode, 'key', extensionKey) : undefined; if (!availableKeys.includes(extensionKey)) { errors.push( errorWithCode( @@ -93,9 +93,3 @@ export const keysMatchBaseService: PostCompositionValidator = function ({ return errors; }; - -function printFieldSet(selections: readonly SelectionNode[]): string { - return selections - .map((selection) => stripIgnoredCharacters(print(selection))) - .join(' '); -} diff --git a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts index 318fd9add..8ca68bce5 100644 --- a/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts +++ b/federation-js/src/composition/validate/postComposition/providesFieldsSelectInvalidType.ts @@ -10,7 +10,7 @@ import { InputValueDefinitionNode, FieldDefinitionNode, } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, printFieldSet } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -53,7 +53,7 @@ export const providesFieldsSelectInvalidType: PostCompositionValidator = ({ const fieldNode = typeNode && 'fields' in typeNode ? (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[]) ?.find(field => field.name.value === fieldName) : undefined; - const selectionSetNode = findSelectionSetOnNode(fieldNode, 'provides', name); + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'provides', printFieldSet(selections)); if (!matchingField) { errors.push( diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts index 808ded185..106d20a31 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingExternal.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, printFieldSet } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -47,7 +47,7 @@ export const requiresFieldsMissingExternal: PostCompositionValidator = ({ 'fields' in typeNode ? (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. find(field => field.name.value === fieldName) : undefined; - const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', selection.name.value); + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', printFieldSet(selections)); errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_EXTERNAL', diff --git a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts index 0d4b0762c..74fae733c 100644 --- a/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts +++ b/federation-js/src/composition/validate/postComposition/requiresFieldsMissingOnBase.ts @@ -1,5 +1,5 @@ import { isObjectType, FieldNode, GraphQLError, FieldDefinitionNode, InputValueDefinitionNode } from 'graphql'; -import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode } from '../../utils'; +import { logServiceAndType, errorWithCode, getFederationMetadata, findTypeNodeInServiceList, findSelectionSetOnNode, printFieldSet } from '../../utils'; import { PostCompositionValidator } from '.'; /** @@ -44,7 +44,7 @@ export const requiresFieldsMissingOnBase: PostCompositionValidator = ({ 'fields' in typeNode ? (typeNode.fields as (FieldDefinitionNode | InputValueDefinitionNode)[])?. find(field => field.name.value === fieldName) : undefined; - const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', selection.name.value); + const selectionSetNode = findSelectionSetOnNode(fieldNode, 'requires', printFieldSet(selections)); errors.push( errorWithCode( 'REQUIRES_FIELDS_MISSING_ON_BASE', From 606a341ec66fcee3e6bb85f5f9e527fa75916661 Mon Sep 17 00:00:00 2001 From: mayakoneval Date: Mon, 10 May 2021 10:45:09 -0700 Subject: [PATCH 91/92] use printFieldSet from utils all over, change includes to === in findSelectionSetOnNode --- federation-js/src/composition/utils.ts | 7 ++++++- federation-js/src/service/printSupergraphSdl.ts | 13 +------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/federation-js/src/composition/utils.ts b/federation-js/src/composition/utils.ts index 4425f5947..58779a21b 100644 --- a/federation-js/src/composition/utils.ts +++ b/federation-js/src/composition/utils.ts @@ -86,6 +86,11 @@ export function findDirectivesOnNode( ) ?? []; } +/** + * Core change: print fieldsets for @join__field's @key, @requires, and @provides args + * + * @param selections + */ export function printFieldSet(selections: readonly SelectionNode[]): string { return selections .map((selection) => stripIgnoredCharacters(print(selection))) @@ -119,7 +124,7 @@ export function findSelectionSetOnNode( directive => directive.name.value === directiveName && directive.arguments?.some( argument => isStringValueNode(argument.value) && - argument.value.value.includes(printedSelectionSet) + argument.value.value === printedSelectionSet ))?.arguments?.find( argument => argument.name.value === 'fields')?.value; } diff --git a/federation-js/src/service/printSupergraphSdl.ts b/federation-js/src/service/printSupergraphSdl.ts index 7381772f2..9dc344498 100644 --- a/federation-js/src/service/printSupergraphSdl.ts +++ b/federation-js/src/service/printSupergraphSdl.ts @@ -26,12 +26,12 @@ import { GraphQLString, DEFAULT_DEPRECATION_REASON, SelectionNode, - stripIgnoredCharacters, } from 'graphql'; import { Maybe, FederationType, FederationField, ServiceDefinition } from '../composition'; import { assert } from '../utilities'; import { CoreDirective } from '../coreSpec'; import { getJoinDefinitions } from '../joinSpec'; +import { printFieldSet } from '../composition/utils'; type Options = { /** @@ -363,17 +363,6 @@ function printFields( return printBlock(fields, isEntity); } -/** - * Core change: print fieldsets for @join__field's @key, @requires, and @provides args - * - * @param selections - */ -function printFieldSet(selections: readonly SelectionNode[]): string { - return selections - .map((selection) => stripIgnoredCharacters(print(selection))) - .join(' '); -} - /** * Core change: print @join__field directives * From d34729edc5db227af9a223e5d0e2ff26317692b7 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Mon, 10 May 2021 12:41:40 -0700 Subject: [PATCH 92/92] Remove unused imports --- federation-js/src/composition/__tests__/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/federation-js/src/composition/__tests__/utils.test.ts b/federation-js/src/composition/__tests__/utils.test.ts index 1eee09b2f..5301bef01 100644 --- a/federation-js/src/composition/__tests__/utils.test.ts +++ b/federation-js/src/composition/__tests__/utils.test.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; import deepFreeze from 'deep-freeze'; -import { findSelectionSetOnNode, isDirectiveDefinitionNode, stripExternalFieldsFromTypeDefs } from '../utils'; +import { stripExternalFieldsFromTypeDefs } from '../utils'; import { astSerializer } from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer);