From eeb9b9c7dbe61243d49ccf0b58959d1c6c408688 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 25 Jun 2024 17:43:42 -0600 Subject: [PATCH 01/11] Remove .idea files from Git (#3048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes JetBrains config from Git. Having these checked in breaks IDEs for JetBrains users (like me 😁). This is part of #3046 broken out for easier review. --- .gitignore | 3 + .idea/.gitignore | 10 - .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/inspectionProfiles/Project_Default.xml | 1313 ----------------- .idea/jsLibraryMappings.xml | 6 - .idea/misc.xml | 6 - .idea/modules.xml | 15 - .idea/modules/composition-js.iml | 11 - .idea/modules/docs.iml | 8 - .../federation-integration-testsuite-js.iml | 10 - .idea/modules/gateway-js.iml | 16 - .idea/modules/internals-js.iml | 11 - .idea/modules/query-graphs-js.iml | 11 - .idea/modules/query-planner-js.iml | 12 - .idea/modules/subgraph-js.iml | 11 - .idea/vcs.xml | 6 - 16 files changed, 3 insertions(+), 1451 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/jsLibraryMappings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/modules/composition-js.iml delete mode 100644 .idea/modules/docs.iml delete mode 100644 .idea/modules/federation-integration-testsuite-js.iml delete mode 100644 .idea/modules/gateway-js.iml delete mode 100644 .idea/modules/internals-js.iml delete mode 100644 .idea/modules/query-graphs-js.iml delete mode 100644 .idea/modules/query-planner-js.iml delete mode 100644 .idea/modules/subgraph-js.iml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 319016da3..0f12fe5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ node_modules/ # Local Netlify folder .netlify + +# JetBrains IDE config +.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ecf172b8a..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ -dataSources.xml -/codeStyles/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2..000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index abda3f006..000000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,1313 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index d23208fbb..000000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 10af178fd..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index ce2705504..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/composition-js.iml b/.idea/modules/composition-js.iml deleted file mode 100644 index 6fc8fc209..000000000 --- a/.idea/modules/composition-js.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/docs.iml b/.idea/modules/docs.iml deleted file mode 100644 index 77cd0752d..000000000 --- a/.idea/modules/docs.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/federation-integration-testsuite-js.iml b/.idea/modules/federation-integration-testsuite-js.iml deleted file mode 100644 index a743dc74d..000000000 --- a/.idea/modules/federation-integration-testsuite-js.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/gateway-js.iml b/.idea/modules/gateway-js.iml deleted file mode 100644 index cccaf51d0..000000000 --- a/.idea/modules/gateway-js.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/internals-js.iml b/.idea/modules/internals-js.iml deleted file mode 100644 index a83f1c61f..000000000 --- a/.idea/modules/internals-js.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/query-graphs-js.iml b/.idea/modules/query-graphs-js.iml deleted file mode 100644 index 01f7dfb47..000000000 --- a/.idea/modules/query-graphs-js.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/query-planner-js.iml b/.idea/modules/query-planner-js.iml deleted file mode 100644 index 110bd5a25..000000000 --- a/.idea/modules/query-planner-js.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules/subgraph-js.iml b/.idea/modules/subgraph-js.iml deleted file mode 100644 index fb3829ebb..000000000 --- a/.idea/modules/subgraph-js.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfb..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 5a72dd80fb6746d31c0727c39f1dda27a987be76 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 26 Jun 2024 10:25:53 -0600 Subject: [PATCH 02/11] Run Prettier on `internals-js` (#3050) Part of #3046, split out for easier review. --- .prettierrc.js | 2 +- .../src/__tests__/definitions.test.ts | 223 +++-- .../directiveAndTypeSpecifications.test.ts | 80 +- .../extractSubgraphsFromSupergraph.test.ts | 51 +- internals-js/src/__tests__/federation.test.ts | 11 +- .../__tests__/graphQLJSSchemaToAST.test.ts | 18 +- internals-js/src/__tests__/operations.test.ts | 769 +++++++++++------- .../removeInaccessibleElements.test.ts | 434 +++++----- .../src/__tests__/schemaUpgrader.test.ts | 116 ++- .../src/__tests__/subgraphValidation.test.ts | 664 ++++++++++----- .../src/__tests__/toAPISchema.test.ts | 11 +- internals-js/src/__tests__/utils.test.ts | 38 +- internals-js/src/__tests__/values.test.ts | 130 +-- .../src/specs/__tests__/coreSpec.test.ts | 241 +++--- 14 files changed, 1729 insertions(+), 1059 deletions(-) diff --git a/.prettierrc.js b/.prettierrc.js index ce4bf3f7b..9fd7a62c7 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -15,7 +15,7 @@ module.exports = { requirePragma: true, overrides: [ { - files: '{docs/{,source/**},.,{gateway-js,federation-integration-testsuite-js,query-planner-js,subgraph-js}/**,test}/{*.js,*.ts}', + files: '{docs/{,source/**},.,{gateway-js,federation-integration-testsuite-js,query-planner-js,subgraph-js,internals-js}/**,test}/{*.js,*.ts}', options: { requirePragma: false, trailingComma: 'all', diff --git a/internals-js/src/__tests__/definitions.test.ts b/internals-js/src/__tests__/definitions.test.ts index 404ecd6f7..5a603c5f3 100644 --- a/internals-js/src/__tests__/definitions.test.ts +++ b/internals-js/src/__tests__/definitions.test.ts @@ -9,12 +9,14 @@ import { UnionType, InputObjectType, } from '../definitions'; -import { - printSchema as printGraphQLjsSchema, -} from 'graphql'; +import { printSchema as printGraphQLjsSchema } from 'graphql'; import { defaultPrintOptions, printSchema } from '../print'; import { buildSchema } from '../buildSchema'; -import { buildSubgraph, federationMetadata, newEmptyFederation2Schema } from '../federation'; +import { + buildSubgraph, + federationMetadata, + newEmptyFederation2Schema, +} from '../federation'; function parseSchema(schema: string): Schema { try { @@ -48,50 +50,71 @@ declare global { namespace jest { interface Matchers { toHaveField(name: string, type?: Type): R; - toHaveDirective(directive: DirectiveDefinition, args?: TArgs): R; + toHaveDirective( + directive: DirectiveDefinition, + args?: TArgs, + ): R; } } } expect.extend({ - toHaveField(parentType: ObjectType | InterfaceType, name: string, type?: Type) { + toHaveField( + parentType: ObjectType | InterfaceType, + name: string, + type?: Type, + ) { const field = parentType.field(name); if (!field) { return { - message: () => `Cannot find field '${name}' in Object Type ${parentType} with fields [${[...parentType.fields()]}]`, - pass: false + message: () => + `Cannot find field '${name}' in Object Type ${parentType} with fields [${[ + ...parentType.fields(), + ]}]`, + pass: false, }; } if (field.name != name) { return { - message: () => `Type ${parentType} has a field linked to name ${name} but that field name is actually ${field.name}`, - pass: false + message: () => + `Type ${parentType} has a field linked to name ${name} but that field name is actually ${field.name}`, + pass: false, }; } if (type && field.type != type) { return { - message: () => `Expected field ${parentType}.${name} to have type ${type} but got type ${field.type}`, - pass: false + message: () => + `Expected field ${parentType}.${name} to have type ${type} but got type ${field.type}`, + pass: false, }; } return { - message: () => `Expected ${parentType} not to have field ${name} but it does (${field})`, - pass: true - } + message: () => + `Expected ${parentType} not to have field ${name} but it does (${field})`, + pass: true, + }; }, - toHaveDirective(element: SchemaElement, definition: DirectiveDefinition, args?: Record) { + toHaveDirective( + element: SchemaElement, + definition: DirectiveDefinition, + args?: Record, + ) { const directives = element.appliedDirectivesOf(definition); if (directives.length == 0) { return { - message: () => `Cannot find directive @${definition} applied to element ${element} (whose applied directives are [${element.appliedDirectives.join(', ')}]`, - pass: false + message: () => + `Cannot find directive @${definition} applied to element ${element} (whose applied directives are [${element.appliedDirectives.join( + ', ', + )}]`, + pass: false, }; } if (!args) { return { - message: () => `Expected directive @${definition} to not be applied to ${element} but it is`, - pass: true + message: () => + `Expected directive @${definition} to not be applied to ${element} but it is`, + pass: true, }; } @@ -99,35 +122,45 @@ expect.extend({ if (directive.matchArguments(args)) { return { // Not 100% certain that message is correct but I don't think it's going to be used ... - message: () => `Expected directive ${directive.name} applied to ${element} to have arguments ${JSON.stringify(args)} but got ${JSON.stringify(directive.arguments)}`, - pass: true + message: () => + `Expected directive ${ + directive.name + } applied to ${element} to have arguments ${JSON.stringify( + args, + )} but got ${JSON.stringify(directive.arguments)}`, + pass: true, }; } } return { - message: () => `Element ${element} has application of directive @${definition} but not with the requested arguments. Got applications: [${directives.join(', ')}]`, - pass: false - } + message: () => + `Element ${element} has application of directive @${definition} but not with the requested arguments. Got applications: [${directives.join( + ', ', + )}]`, + pass: false, + }; }, }); test('building a simple schema programatically', () => { const schema = newEmptyFederation2Schema(); - const queryType = schema.schemaDefinition.setRoot('query', schema.addType(new ObjectType('Query'))).type; + const queryType = schema.schemaDefinition.setRoot( + 'query', + schema.addType(new ObjectType('Query')), + ).type; const typeA = schema.addType(new ObjectType('A')); const key = federationMetadata(schema)!.keyDirective(); queryType.addField('a', typeA); typeA.addField('q', queryType); - typeA.applyDirective(key, { fields: 'a'}); + typeA.applyDirective(key, { fields: 'a' }); expect(queryType).toBe(schema.schemaDefinition.root('query')!.type); expect(queryType).toHaveField('a', typeA); expect(typeA).toHaveField('q', queryType); - expect(typeA).toHaveDirective(key, { fields: 'a'}); + expect(typeA).toHaveDirective(key, { fields: 'a' }); }); - test('parse schema and modify', () => { const sdl = ` schema { @@ -164,7 +197,10 @@ test('parse schema and modify', () => { }); test('removal of all directives of a schema', () => { - const subgraph = buildSubgraph('foo', '', ` + const subgraph = buildSubgraph( + 'foo', + '', + ` schema @foo { query: Query } @@ -188,11 +224,12 @@ test('removal of all directives of a schema', () => { directive @foo on SCHEMA | FIELD_DEFINITION | OBJECT directive @foobar on UNION directive @bar on ARGUMENT_DEFINITION | FIELD_DEFINITION - `).validate(); + `, + ).validate(); const schema = subgraph.schema; for (const element of schema.allSchemaElement()) { - element.appliedDirectives.forEach(d => d.remove()); + element.appliedDirectives.forEach((d) => d.remove()); } expect(subgraph.toString()).toMatchString(` @@ -232,17 +269,20 @@ test('removal of an enum type should remove enum values', () => { } `); - const enumType = schema.type("Enum"); - expectEnumType(enumType) + const enumType = schema.type('Enum'); + expectEnumType(enumType); const enumValues = Array.from(enumType.values); - enumType.remove() + enumType.remove(); for (const value of enumValues) { - expect(value.isAttached()).toBe(false) + expect(value.isAttached()).toBe(false); } }); test('removal of all inaccessible elements of a schema', () => { - const subgraph = buildSubgraph('foo', '', ` + const subgraph = buildSubgraph( + 'foo', + '', + ` schema @foo { query: Query } @@ -265,7 +305,8 @@ test('removal of all inaccessible elements of a schema', () => { directive @inaccessible on FIELD_DEFINITION | OBJECT | ARGUMENT_DEFINITION | UNION directive @foo on SCHEMA | FIELD_DEFINITION directive @bar on ARGUMENT_DEFINITION | FIELD_DEFINITION - `); + `, + ); const schema = subgraph.schema; const inaccessibleDirective = schema.directive('inaccessible')!; @@ -475,7 +516,8 @@ test('handling of descriptions', () => { // Checking we get back the schema through printing it is mostly good enough, but let's just // make sure long descriptions don't get annoying formatting newlines for instance when acessed on the // schema directly. - const longComment = "Something that explains what the product is. This can just be the title of the product, but this can be more than that if we want to. But it should be useful you know, otherwise our customer won't buy it."; + const longComment = + "Something that explains what the product is. This can just be the title of the product, but this can be more than that if we want to. But it should be useful you know, otherwise our customer won't buy it."; const product = schema.type('Product'); expectInterfaceType(product); expect(product.field('description')!.description).toBe(longComment); @@ -536,9 +578,18 @@ test('handling of extensions', () => { const aunion = schema.type('AUnion'); expectUnionType(aunion); - expect([...aunion.types()].map(t => t.name)).toEqual(['AType', 'AType2', 'AType3']); - - expect(subgraph.toString({ ...defaultPrintOptions, mergeTypesAndExtensions: true })).toMatchString(` + expect([...aunion.types()].map((t) => t.name)).toEqual([ + 'AType', + 'AType2', + 'AType3', + ]); + + expect( + subgraph.toString({ + ...defaultPrintOptions, + mergeTypesAndExtensions: true, + }), + ).toMatchString(` directive @foo on SCALAR type Query { @@ -666,7 +717,7 @@ describe('type extension where definition is empty', () => { expect(schema.type('Foo')?.hasNonExtensionElements()).toBeTruthy(); expect(schema.type('Foo')?.hasExtensionElements()).toBeTruthy(); }); -}) +}); test('reject type defined multiple times', () => { const sdl = ` @@ -683,7 +734,9 @@ test('reject type defined multiple times', () => { } `; - expect(() => buildSchema(sdl).validate()).toThrow('There can be only one type named "Foo"'); + expect(() => buildSchema(sdl).validate()).toThrow( + 'There can be only one type named "Foo"', + ); }); test('default arguments for directives', () => { @@ -718,12 +771,12 @@ test('default arguments for directives', () => { const d3 = v3.appliedDirectivesOf(exampleDirective)[0]; expect(d1.arguments()).toEqual({}); - expect(d2.arguments()).toEqual({ inputObject: {}}); - expect(d3.arguments()).toEqual({ inputObject: { number: 3 }}); + expect(d2.arguments()).toEqual({ inputObject: {} }); + expect(d3.arguments()).toEqual({ inputObject: { number: 3 } }); - expect(d1.arguments(true)).toEqual({ inputObject: { number: 3 }}); - expect(d2.arguments(true)).toEqual({ inputObject: { number: 3 }}); - expect(d3.arguments(true)).toEqual({ inputObject: { number: 3 }}); + expect(d1.arguments(true)).toEqual({ inputObject: { number: 3 } }); + expect(d2.arguments(true)).toEqual({ inputObject: { number: 3 } }); + expect(d3.arguments(true)).toEqual({ inputObject: { number: 3 } }); }); describe('clone', () => { @@ -741,17 +794,20 @@ describe('clone', () => { } `).clone(); - expect(schema.elementByCoordinate("@foo")).toBeDefined(); - expect(schema.elementByCoordinate("@wizz")).toBeDefined(); - expect(schema.elementByCoordinate("@fuzz")).toBeDefined(); - expect(schema.elementByCoordinate("@buzz")).toBeDefined(); - expect(schema.elementByCoordinate("@baz")).toBeDefined(); - expect(schema.elementByCoordinate("@bar")).toBeDefined(); + expect(schema.elementByCoordinate('@foo')).toBeDefined(); + expect(schema.elementByCoordinate('@wizz')).toBeDefined(); + expect(schema.elementByCoordinate('@fuzz')).toBeDefined(); + expect(schema.elementByCoordinate('@buzz')).toBeDefined(); + expect(schema.elementByCoordinate('@baz')).toBeDefined(); + expect(schema.elementByCoordinate('@bar')).toBeDefined(); }); // https://github.com/apollographql/federation/issues/1794 it('should allow using an imported federation diretive in another directive', () => { - const schema = buildSubgraph('foo', "", ` + const schema = buildSubgraph( + 'foo', + '', + ` extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@tag"]) @@ -760,9 +816,10 @@ describe('clone', () => { type Query { hi: String! @foo } - `).schema.clone(); - expect(schema.elementByCoordinate("@foo")).toBeDefined(); - expect(schema.elementByCoordinate("@tag")).toBeDefined(); + `, + ).schema.clone(); + expect(schema.elementByCoordinate('@foo')).toBeDefined(); + expect(schema.elementByCoordinate('@tag')).toBeDefined(); }); it('should allow type use in directives', () => { @@ -775,7 +832,7 @@ describe('clone', () => { } `).clone(); - expect(schema.elementByCoordinate("@foo")).toBeDefined(); + expect(schema.elementByCoordinate('@foo')).toBeDefined(); }); it('should allow recursive directive definitions', () => { @@ -787,8 +844,8 @@ describe('clone', () => { getData(arg: String @foo): String! } `).clone(); - expect(schema.elementByCoordinate("@foo")).toBeDefined(); - expect(schema.elementByCoordinate("@bar")).toBeDefined(); + expect(schema.elementByCoordinate('@foo')).toBeDefined(); + expect(schema.elementByCoordinate('@bar')).toBeDefined(); }); }); @@ -825,7 +882,9 @@ describe('Conversion to graphQL-js schema', () => { `; const schema = parseSchema(sdl); - expect(printGraphQLjsSchema(schema.toGraphQLJSSchema({ includeDefer: true }))).toMatchString(` + expect( + printGraphQLjsSchema(schema.toGraphQLJSSchema({ includeDefer: true })), + ).toMatchString(` directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT type Query { @@ -833,7 +892,11 @@ describe('Conversion to graphQL-js schema', () => { } `); - expect(printGraphQLjsSchema(schema.toGraphQLJSSchema({ includeDefer: true, includeStream: true }))).toMatchString(` + expect( + printGraphQLjsSchema( + schema.toGraphQLJSSchema({ includeDefer: true, includeStream: true }), + ), + ).toMatchString(` directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT directive @stream(label: String, initialCount: Int = 0, if: Boolean! = true) on FIELD @@ -868,7 +931,9 @@ describe('Conversion to graphQL-js schema', () => { } `); - expect(printGraphQLjsSchema(apiSchema.toGraphQLJSSchema({ includeDefer: true }))).toMatchString(` + expect( + printGraphQLjsSchema(apiSchema.toGraphQLJSSchema({ includeDefer: true })), + ).toMatchString(` directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT type Query { @@ -876,7 +941,14 @@ describe('Conversion to graphQL-js schema', () => { } `); - expect(printGraphQLjsSchema(apiSchema.toGraphQLJSSchema({ includeDefer: true, includeStream: true }))).toMatchString(` + expect( + printGraphQLjsSchema( + apiSchema.toGraphQLJSSchema({ + includeDefer: true, + includeStream: true, + }), + ), + ).toMatchString(` directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT directive @stream(label: String, initialCount: Int = 0, if: Boolean! = true) on FIELD @@ -888,7 +960,6 @@ describe('Conversion to graphQL-js schema', () => { }); }); - test('retrieving elements by coordinate', () => { const sdl = ` directive @foo(bar: Int) on FIELD @@ -919,14 +990,20 @@ test('retrieving elements by coordinate', () => { `; const schema = parseSchema(sdl); - expect(schema.elementByCoordinate('Query')).toBe(schema.schemaDefinition.rootType('query')); - expect(schema.elementByCoordinate('Query.t')).toBe(schema.schemaDefinition.rootType('query')?.field('t')); + expect(schema.elementByCoordinate('Query')).toBe( + schema.schemaDefinition.rootType('query'), + ); + expect(schema.elementByCoordinate('Query.t')).toBe( + schema.schemaDefinition.rootType('query')?.field('t'), + ); const typeT = schema.type('T') as ObjectType; expect(schema.elementByCoordinate('T')).toBe(typeT); expect(schema.elementByCoordinate('T.f1')).toBe(typeT.field('f1')); expect(schema.elementByCoordinate('T.f2')).toBe(typeT.field('f2')); - expect(schema.elementByCoordinate('T.f1(x:)')).toBe(typeT.field('f1')?.argument('x')); + expect(schema.elementByCoordinate('T.f1(x:)')).toBe( + typeT.field('f1')?.argument('x'), + ); const typeI = schema.type('I') as InterfaceType; expect(schema.elementByCoordinate('I')).toBe(typeI); @@ -944,7 +1021,9 @@ test('retrieving elements by coordinate', () => { const directiveFoo = schema.directive('foo')!; expect(schema.elementByCoordinate('@foo')).toBe(directiveFoo); - expect(schema.elementByCoordinate('@foo(bar:)')).toBe(directiveFoo.argument('bar')); + expect(schema.elementByCoordinate('@foo(bar:)')).toBe( + directiveFoo.argument('bar'), + ); expect(schema.elementByCoordinate('SomeType')).toBeUndefined(); expect(schema.elementByCoordinate('T.f3')).toBeUndefined(); @@ -961,7 +1040,7 @@ test('retrieving elements by coordinate', () => { expect(() => schema.elementByCoordinate('O.x(foo:)')).toThrow(); // Note that because 'Date' is a scalar, it cannot have fields expect(() => schema.elementByCoordinate('Date.bar')).toThrow(); -}) +}); test('parse error', () => { const schema = ` diff --git a/internals-js/src/__tests__/directiveAndTypeSpecifications.test.ts b/internals-js/src/__tests__/directiveAndTypeSpecifications.test.ts index df0fcc366..dfaa9aaa6 100644 --- a/internals-js/src/__tests__/directiveAndTypeSpecifications.test.ts +++ b/internals-js/src/__tests__/directiveAndTypeSpecifications.test.ts @@ -1,41 +1,61 @@ -import { DirectiveLocation } from "graphql"; -import "../definitions"; -import { createDirectiveSpecification } from "../directiveAndTypeSpecification"; -import { ARGUMENT_COMPOSITION_STRATEGIES } from "../argumentCompositionStrategies"; -import { TAG_VERSIONS } from "../specs/tagSpec"; +import { DirectiveLocation } from 'graphql'; +import '../definitions'; +import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; +import { ARGUMENT_COMPOSITION_STRATEGIES } from '../argumentCompositionStrategies'; +import { TAG_VERSIONS } from '../specs/tagSpec'; const supergraphSpecification = () => TAG_VERSIONS.latest(); test('must have supergraph link if composed', () => { - expect(() => createDirectiveSpecification({ - name: 'foo', - locations: [DirectiveLocation.OBJECT], - composes: true, - })).toThrow('Should provide a @link specification to use in supergraph for directive @foo if it composes'); + expect(() => + createDirectiveSpecification({ + name: 'foo', + locations: [DirectiveLocation.OBJECT], + composes: true, + }), + ).toThrow( + 'Should provide a @link specification to use in supergraph for directive @foo if it composes', + ); }); test('must have a merge strategy on all arguments if any', () => { - expect(() => createDirectiveSpecification({ - name: 'foo', - locations: [DirectiveLocation.OBJECT], - composes: true, - supergraphSpecification, - args: [ - { name: "v1", type: (schema) => schema.intType(), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.MAX }, - { name: "v2", type: (schema) => schema.intType() } - ], - })).toThrow('Invalid directive specification for @foo: not all arguments define a composition strategy'); + expect(() => + createDirectiveSpecification({ + name: 'foo', + locations: [DirectiveLocation.OBJECT], + composes: true, + supergraphSpecification, + args: [ + { + name: 'v1', + type: (schema) => schema.intType(), + compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.MAX, + }, + { name: 'v2', type: (schema) => schema.intType() }, + ], + }), + ).toThrow( + 'Invalid directive specification for @foo: not all arguments define a composition strategy', + ); }); test('must be not be repeatable if it has a merge strategy', () => { - expect(() => createDirectiveSpecification({ - name: 'foo', - locations: [DirectiveLocation.OBJECT], - composes: true, - repeatable: true, - supergraphSpecification, - args: [ - { name: "v", type: (schema) => schema.intType(), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.MAX }, - ], - })).toThrow('Invalid directive specification for @foo: @foo is repeatable and should not define composition strategy for its arguments'); + expect(() => + createDirectiveSpecification({ + name: 'foo', + locations: [DirectiveLocation.OBJECT], + composes: true, + repeatable: true, + supergraphSpecification, + args: [ + { + name: 'v', + type: (schema) => schema.intType(), + compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.MAX, + }, + ], + }), + ).toThrow( + 'Invalid directive specification for @foo: @foo is repeatable and should not define composition strategy for its arguments', + ); }); diff --git a/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts b/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts index 6685a7974..955f1c823 100644 --- a/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts +++ b/internals-js/src/__tests__/extractSubgraphsFromSupergraph.test.ts @@ -1,5 +1,4 @@ -import { Supergraph, InputObjectType, ObjectType, printSchema } from ".."; - +import { Supergraph, InputObjectType, ObjectType, printSchema } from '..'; test('handles types having no fields referenced by other objects in a subgraph correctly', () => { /* @@ -107,7 +106,7 @@ test('handles types having no fields referenced by other objects in a subgraph c expect(c.type('A')).toBeUndefined(); expect(c.type('B')).toBeUndefined(); -}) +}); test('handles types having no fields referenced by other interfaces in a subgraph correctly', () => { /* @@ -213,7 +212,7 @@ test('handles types having no fields referenced by other interfaces in a subgrap expect(c.type('A')).toBeUndefined(); expect(c.type('B')).toBeUndefined(); -}) +}); test('handles types having no fields referenced by other unions in a subgraph correctly', () => { /* @@ -314,7 +313,7 @@ test('handles types having no fields referenced by other unions in a subgraph co expect(b.type('B')).toBeUndefined(); expect(b.type('C')).toBeUndefined(); expect(a.type('D')).toBeDefined(); -}) +}); test('handles types having only some of their fields removed in a subgraph correctly', () => { /* @@ -425,7 +424,7 @@ test('handles types having only some of their fields removed in a subgraph corre expect(c.type('A')).toBeDefined(); expect(c.type('B')).toBeDefined(); -}) +}); test('handles unions types having no members in a subgraph correctly', () => { /* @@ -527,7 +526,7 @@ test('handles unions types having no members in a subgraph correctly', () => { expect(b.type('B')).toBeUndefined(); expect(b.type('C')).toBeUndefined(); expect(a.type('D')).toBeDefined(); -}) +}); test('preserves default values of input object fields', () => { const supergraph = ` @@ -583,12 +582,14 @@ test('preserves default values of input object fields', () => { const subgraphs = Supergraph.build(supergraph).subgraphs(); - const subgraph = subgraphs.get('service') - const inputType = subgraph?.schema.type('Input') as InputObjectType | undefined - const inputFieldA = inputType?.field('a') + const subgraph = subgraphs.get('service'); + const inputType = subgraph?.schema.type('Input') as + | InputObjectType + | undefined; + const inputFieldA = inputType?.field('a'); - expect(inputFieldA?.defaultValue).toBe(1234) -}) + expect(inputFieldA?.defaultValue).toBe(1234); +}); test('throw meaningful error for invalid federation directive fieldSet', () => { const supergraph = ` @@ -645,13 +646,13 @@ test('throw meaningful error for invalid federation directive fieldSet', () => { `; expect(() => Supergraph.build(supergraph).subgraphs()).toThrow( - 'Error extracting subgraph "serviceB" from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n' - + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n' - + '\n' - + 'Details:\n' - + '[serviceB] On field "A.a", for @requires(fields: "b { x }"): Cannot query field "b" on type "A" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).' + 'Error extracting subgraph "serviceB" from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n' + + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n' + + '\n' + + 'Details:\n' + + '[serviceB] On field "A.a", for @requires(fields: "b { x }"): Cannot query field "b" on type "A" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).', ); -}) +}); test('throw meaningful error for type erased from supergraph due to extending an entity without a key', () => { // Supergraph generated by fed1 composition from: @@ -731,13 +732,13 @@ test('throw meaningful error for type erased from supergraph due to extending an `; expect(() => Supergraph.build(supergraph).subgraphs()).toThrow( - 'Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n' - + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n' - + '\n' - + 'Details:\n' - + 'Error: Cannot find type "T" in subgraph "serviceB"' + 'Error extracting subgraphs from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n' + + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph.\n' + + '\n' + + 'Details:\n' + + 'Error: Cannot find type "T" in subgraph "serviceB"', ); -}) +}); test('types that are empty because of overridden fields are erased', () => { const supergraph = ` @@ -919,7 +920,7 @@ type T prop: String! }`); -expect(printedSchema).toMatch(` + expect(printedSchema).toMatch(` type U @key(fields: "id") { diff --git a/internals-js/src/__tests__/federation.test.ts b/internals-js/src/__tests__/federation.test.ts index dd81d9293..eae48a335 100644 --- a/internals-js/src/__tests__/federation.test.ts +++ b/internals-js/src/__tests__/federation.test.ts @@ -1,7 +1,7 @@ -import gql from "graphql-tag"; -import { buildSubgraph, federationMetadata } from ".."; +import gql from 'graphql-tag'; +import { buildSubgraph, federationMetadata } from '..'; -it("detects federation 1 subgraphs correctly", () => { +it('detects federation 1 subgraphs correctly', () => { const schema = gql` type Query { s: String @@ -14,10 +14,9 @@ it("detects federation 1 subgraphs correctly", () => { expect(metadata?.isFed2Schema()).toBeFalsy(); }); -it("detects federation 2 subgraphs correctly", () => { +it('detects federation 2 subgraphs correctly', () => { const schema = gql` - extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0") + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0") type Query { s: String diff --git a/internals-js/src/__tests__/graphQLJSSchemaToAST.test.ts b/internals-js/src/__tests__/graphQLJSSchemaToAST.test.ts index 9c5fa0735..acacc7e29 100644 --- a/internals-js/src/__tests__/graphQLJSSchemaToAST.test.ts +++ b/internals-js/src/__tests__/graphQLJSSchemaToAST.test.ts @@ -3,15 +3,20 @@ import { buildSchema, GraphQLSchema, introspectionFromSchema, - print -} from "graphql"; -import { graphQLJSSchemaToAST } from "../graphQLJSSchemaToAST"; - -function validateRoundtrip(schemaStr: string, expectedWithoutASTNodes: string | undefined = schemaStr) { + print, +} from 'graphql'; +import { graphQLJSSchemaToAST } from '../graphQLJSSchemaToAST'; + +function validateRoundtrip( + schemaStr: string, + expectedWithoutASTNodes: string | undefined = schemaStr, +) { const schema = buildSchema(schemaStr); expect(print(graphQLJSSchemaToAST(schema))).toMatchString(schemaStr); if (expectedWithoutASTNodes) { - expect(print(graphQLJSSchemaToAST(withoutASTNodes(schema)))).toMatchString(expectedWithoutASTNodes); + expect(print(graphQLJSSchemaToAST(withoutASTNodes(schema)))).toMatchString( + expectedWithoutASTNodes, + ); } } @@ -152,4 +157,3 @@ it('round-trip with extensions', () => { validateRoundtrip(schema, noAST); }); - diff --git a/internals-js/src/__tests__/operations.test.ts b/internals-js/src/__tests__/operations.test.ts index 6e26b7fc2..28c048c9e 100644 --- a/internals-js/src/__tests__/operations.test.ts +++ b/internals-js/src/__tests__/operations.test.ts @@ -6,8 +6,26 @@ import { } from '../definitions'; import { buildSchema } from '../buildSchema'; import { FederationBlueprint } from '../federation'; -import { FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition, Operation, operationFromDocument, parseOperation } from '../operations'; -import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, SelectionNode, SelectionSetNode, validate } from 'graphql'; +import { + FragmentRestrictionAtType, + MutableSelectionSet, + NamedFragmentDefinition, + Operation, + operationFromDocument, + parseOperation, +} from '../operations'; +import { + DocumentNode, + FieldNode, + GraphQLError, + Kind, + OperationDefinitionNode, + OperationTypeNode, + parse, + SelectionNode, + SelectionSetNode, + validate, +} from 'graphql'; import { assert } from '../utils'; import gql from 'graphql-tag'; @@ -52,7 +70,9 @@ describe('generate query fragments', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { entities { ... on B { @@ -66,7 +86,8 @@ describe('generate query fragments', () => { } } } - `); + `, + ); const withGeneratedFragments = operation.generateQueryFragments(); console.log(withGeneratedFragments.toString()); @@ -102,9 +123,9 @@ describe('fragments optimization', () => { query, expanded, }: { - schema: Schema, - query: string, - expanded: string, + schema: Schema; + query: string; + expanded: string; }) { const operation = parseOperation(schema, query); const withoutFragments = operation.expandAllFragments(); @@ -146,7 +167,9 @@ describe('fragments optimization', () => { union U = T1 | T2 `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` fragment OnT1 on T1 { a b @@ -177,7 +200,8 @@ describe('fragments optimization', () => { } } } - `); + `, + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -270,7 +294,9 @@ describe('fragments optimization', () => { union U = T1 | T2 `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` fragment OnT1 on T1 { a b @@ -309,7 +335,8 @@ describe('fragments optimization', () => { } } } - `); + `, + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -478,7 +505,6 @@ describe('fragments optimization', () => { } `); - // The subtlety here is that `FA` contains `__typename` and so after we're reused it, the // selection will look like: // { @@ -493,7 +519,7 @@ describe('fragments optimization', () => { // directly, it is fine to reuse). testFragmentsRoundtrip({ schema, - query: ` + query: ` fragment FA on A { __typename x @@ -1027,7 +1053,9 @@ describe('fragments optimization', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ...TFrag @@ -1047,7 +1075,8 @@ describe('fragments optimization', () => { __typename id } - `); + `, + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1279,7 +1308,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { id @@ -1296,8 +1327,11 @@ describe('fragments optimization', () => { f(arg: 1) } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1339,7 +1373,9 @@ describe('fragments optimization', () => { // // And so this test does make sure we do not generate the query above (do not use `F1` in `t1`). const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment F1 on I { @@ -1388,7 +1424,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { id @@ -1410,8 +1448,11 @@ describe('fragments optimization', () => { } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1433,7 +1474,9 @@ describe('fragments optimization', () => { // first, and then we need to make sure we do not apply `F2` even though it's restriction // inside `t1` matches its selection set. const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment F1 on T1 { @@ -1493,7 +1536,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { id @@ -1524,8 +1569,11 @@ describe('fragments optimization', () => { } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1556,7 +1604,9 @@ describe('fragments optimization', () => { // within the first `T1` branch. But they can't both be used because their `... on WithF` part conflict, // and even though that part is dead in `T1`, this would still be illegal graphQL. const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment F1 on I { @@ -1613,7 +1663,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` fragment onV1V2 on SomeV { ... on V1 { x @@ -1636,8 +1688,11 @@ describe('fragments optimization', () => { } } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1662,7 +1717,9 @@ describe('fragments optimization', () => { `); const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment onV1V2 on SomeV { @@ -1716,7 +1773,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` fragment onV1V3 on SomeV { ... on V1 { x @@ -1749,8 +1808,11 @@ describe('fragments optimization', () => { } } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1781,7 +1843,9 @@ describe('fragments optimization', () => { `); const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment onV1V3 on SomeV { @@ -1856,7 +1920,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` fragment onV1V2 on SomeV { ... on V1 { x @@ -1887,8 +1953,11 @@ describe('fragments optimization', () => { } } } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -1921,7 +1990,9 @@ describe('fragments optimization', () => { `); const optimized = withoutFragments.optimize(operation.fragments!, 1); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment onV1V2 on SomeV { @@ -1981,7 +2052,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t1 { ...GetAll @@ -2004,8 +2077,11 @@ describe('fragments optimization', () => { fragment GetT2 on T2 { b } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -2031,7 +2107,9 @@ describe('fragments optimization', () => { // "getting rid" of the `...GetT2` spread, keeping in the query, which is // invalid (we cannot have `...GetT2` inside `t1`). const optimized = withoutFragments.optimize(operation.fragments!, 2); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment GetT2 on T2 { @@ -2074,7 +2152,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { u1 { ...F1 @@ -2109,8 +2189,11 @@ describe('fragments optimization', () => { } ...F2 } - `); - expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); + `, + ); + expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual( + [], + ); const withoutFragments = operation.expandAllFragments(); expect(withoutFragments.toString()).toMatchString(` @@ -2149,9 +2232,13 @@ describe('fragments optimization', () => { // We use `mapToExpandedSelectionSets` with a no-op mapper because this will still expand the selections // and re-optimize them, which 1) happens to match what happens in the query planner and 2) is necessary // for reproducing a bug that this test was initially added to cover. - const newFragments = operation.fragments!.mapToExpandedSelectionSets((s) => s); + const newFragments = operation.fragments!.mapToExpandedSelectionSets( + (s) => s, + ); const optimized = withoutFragments.optimize(newFragments, 2); - expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual([]); + expect(validate(gqlSchema, parse(optimized.toString()))).toStrictEqual( + [], + ); expect(optimized.toString()).toMatchString(` fragment F3 on U { @@ -2209,7 +2296,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { ...Outer @@ -2233,7 +2322,8 @@ describe('fragments optimization', () => { y } } - `); + `, + ); expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); const withoutFragments = operation.expandAllFragments(); @@ -2309,7 +2399,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { ...Outer @@ -2342,7 +2434,8 @@ describe('fragments optimization', () => { fragment WillBeUnused on Y { v } - `); + `, + ); expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); const withoutFragments = operation.expandAllFragments(); @@ -2387,7 +2480,9 @@ describe('fragments optimization', () => { `); const gqlSchema = schema.toGraphQLJSSchema(); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t1 { ...TFields @@ -2416,7 +2511,8 @@ describe('fragments optimization', () => { x y } - `); + `, + ); expect(validate(gqlSchema, parse(operation.toString()))).toStrictEqual([]); const withoutFragments = operation.expandAllFragments(); @@ -2505,14 +2601,23 @@ describe('validations', () => { `); expect(() => { - parseOperation(schema, ` + parseOperation( + schema, + ` ${rootKind} { ... ${directive} { x } } - `) - }).toThrowError(new GraphQLError(`The @defer and @stream directives cannot be used on ${rootKind} root type "${defaultRootName(rootKind as SchemaRootKind)}"`)); + `, + ); + }).toThrowError( + new GraphQLError( + `The @defer and @stream directives cannot be used on ${rootKind} root type "${defaultRootName( + rootKind as SchemaRootKind, + )}"`, + ), + ); }); test('allows nullable variable for non-nullable input field with default', () => { @@ -2527,11 +2632,14 @@ describe('validations', () => { `); // Just testing that this parse correctly and does not throw an exception. - parseOperation(schema, ` + parseOperation( + schema, + ` query test($x: Int) { f(i: { x: $x }) } - `); + `, + ); }); }); @@ -2567,77 +2675,69 @@ describe('empty branches removal', () => { kind: Kind.OPERATION_DEFINITION, operation: OperationTypeNode.QUERY, selectionSet: op, - } + }; const document: DocumentNode = { kind: Kind.DOCUMENT, definitions: [opDef], - } + }; operation = operationFromDocument(schema, document, { validate: false }); } - return operation.selectionSet.withoutEmptyBranches()?.toString() + return operation.selectionSet.withoutEmptyBranches()?.toString(); }; - - it.each([ - '{ t { a } }', - '{ t { a b } }', - '{ t { a c { x y } } }', - ])('is identity if there is no empty branch', (op) => { - expect(withoutEmptyBranches(op)).toBe(op); - }); + it.each(['{ t { a } }', '{ t { a b } }', '{ t { a c { x y } } }'])( + 'is identity if there is no empty branch', + (op) => { + expect(withoutEmptyBranches(op)).toBe(op); + }, + ); it('removes simple empty branches', () => { - expect(withoutEmptyBranches( - astSSet( - astField('t', astSSet( - astField('a'), - astField('c', astSSet()), - )) - ) - )).toBe('{ t { a } }'); - - expect(withoutEmptyBranches( - astSSet( - astField('t', astSSet( - astField('c', astSSet()), - astField('a'), - )) - ) - )).toBe('{ t { a } }'); - - expect(withoutEmptyBranches( - astSSet( - astField('t', astSSet()) - ) - )).toBeUndefined(); + expect( + withoutEmptyBranches( + astSSet( + astField('t', astSSet(astField('a'), astField('c', astSSet()))), + ), + ), + ).toBe('{ t { a } }'); + + expect( + withoutEmptyBranches( + astSSet( + astField('t', astSSet(astField('c', astSSet()), astField('a'))), + ), + ), + ).toBe('{ t { a } }'); + + expect( + withoutEmptyBranches(astSSet(astField('t', astSSet()))), + ).toBeUndefined(); }); it('removes cascading empty branches', () => { - expect(withoutEmptyBranches( - astSSet( - astField('t', astSSet( - astField('c', astSSet()), - )) - ) - )).toBeUndefined(); - - expect(withoutEmptyBranches( - astSSet( - astField('u'), - astField('t', astSSet( - astField('c', astSSet()), - )) - ) - )).toBe('{ u }'); - - expect(withoutEmptyBranches( - astSSet( - astField('t', astSSet( - astField('c', astSSet()), - )), - astField('u'), - ) - )).toBe('{ u }'); + expect( + withoutEmptyBranches( + astSSet(astField('t', astSSet(astField('c', astSSet())))), + ), + ).toBeUndefined(); + + expect( + withoutEmptyBranches( + astSSet( + astField('u'), + astField('t', astSSet(astField('c', astSSet()))), + ), + ), + ).toBe('{ u }'); + + expect( + withoutEmptyBranches( + astSSet( + astField('t', astSSet(astField('c', astSSet()))), + astField('u'), + ), + ), + ).toBe('{ u }'); }); }); @@ -2676,7 +2776,9 @@ describe('basic operations', () => { directive @customSkip(if: Boolean!, label: String!) on FIELD | INLINE_FRAGMENT `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { v1 @@ -2697,12 +2799,15 @@ describe('basic operations', () => { } } } - `); + `, + ); test('forEachElement', () => { // We collect a pair of (parent type, field-or-fragment). const actual: [string, string][] = []; - operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()])); + operation.selectionSet.forEachElement((elt) => + actual.push([elt.parentType.name, elt.toString()]), + ); expect(actual).toStrictEqual([ ['Query', 't'], ['T', 'v1'], @@ -2717,20 +2822,23 @@ describe('basic operations', () => { ['B', 'b2'], ['T', 'v2'], ]); - }) + }); describe('same field merging', () => { test('do merge when same field and no directive', () => { - const operation = operationFromDocument(schema, gql` - query Test { - t { - v1 - } - t { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test { + t { + v1 + } + t { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test { @@ -2743,16 +2851,19 @@ describe('basic operations', () => { }); test('do merge when both have the _same_ directive', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t @skip(if: $skipIf) { - v1 - } - t @skip(if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t @skip(if: $skipIf) { + v1 + } + t @skip(if: $skipIf) { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2765,16 +2876,19 @@ describe('basic operations', () => { }); test('do merge when both have the _same_ directive, even if argument order differs', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t @customSkip(if: $skipIf, label: "foo") { - v1 - } - t @customSkip(label: "foo", if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t @customSkip(if: $skipIf, label: "foo") { + v1 + } + t @customSkip(label: "foo", if: $skipIf) { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2787,16 +2901,19 @@ describe('basic operations', () => { }); test('do not merge when one has a directive and the other do not', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t { - v1 - } - t @skip(if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t { + v1 + } + t @skip(if: $skipIf) { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2811,16 +2928,19 @@ describe('basic operations', () => { }); test('do not merge when both have _differing_ directives', () => { - const operation = operationFromDocument(schema, gql` - query Test($skip1: Boolean!, $skip2: Boolean!) { - t @skip(if: $skip1) { - v1 - } - t @skip(if: $skip2) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t @skip(if: $skip1) { + v1 + } + t @skip(if: $skip2) { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skip1: Boolean!, $skip2: Boolean!) { @@ -2835,16 +2955,19 @@ describe('basic operations', () => { }); test('do not merge @defer directive, even if applied the same way', () => { - const operation = operationFromDocument(schema, gql` - query Test { - t @defer { - v1 - } - t @defer { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test { + t @defer { + v1 + } + t @defer { + v2 + } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test { @@ -2861,18 +2984,21 @@ describe('basic operations', () => { describe('same fragment merging', () => { test('do merge when same fragment and no directive', () => { - const operation = operationFromDocument(schema, gql` - query Test { - t { - ... on T { - v1 - } - ... on T { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test { + t { + ... on T { + v1 + } + ... on T { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test { @@ -2887,18 +3013,21 @@ describe('basic operations', () => { }); test('do merge when both have the _same_ directive', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t { - ... on T @skip(if: $skipIf) { - v1 - } - ... on T @skip(if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t { + ... on T @skip(if: $skipIf) { + v1 + } + ... on T @skip(if: $skipIf) { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2913,18 +3042,21 @@ describe('basic operations', () => { }); test('do merge when both have the _same_ directive, even if argument order differs', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t { - ... on T @customSkip(if: $skipIf, label: "foo") { - v1 - } - ... on T @customSkip(label: "foo", if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t { + ... on T @customSkip(if: $skipIf, label: "foo") { + v1 + } + ... on T @customSkip(label: "foo", if: $skipIf) { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2939,18 +3071,21 @@ describe('basic operations', () => { }); test('do not merge when one has a directive and the other do not', () => { - const operation = operationFromDocument(schema, gql` - query Test($skipIf: Boolean!) { - t { - ... on T { - v1 - } - ... on T @skip(if: $skipIf) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skipIf: Boolean!) { + t { + ... on T { + v1 + } + ... on T @skip(if: $skipIf) { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skipIf: Boolean!) { @@ -2967,18 +3102,21 @@ describe('basic operations', () => { }); test('do not merge when both have _differing_ directives', () => { - const operation = operationFromDocument(schema, gql` - query Test($skip1: Boolean!, $skip2: Boolean!) { - t { - ... on T @skip(if: $skip1) { - v1 - } - ... on T @skip(if: $skip2) { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test($skip1: Boolean!, $skip2: Boolean!) { + t { + ... on T @skip(if: $skip1) { + v1 + } + ... on T @skip(if: $skip2) { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test($skip1: Boolean!, $skip2: Boolean!) { @@ -2995,18 +3133,21 @@ describe('basic operations', () => { }); test('do not merge @defer directive, even if applied the same way', () => { - const operation = operationFromDocument(schema, gql` - query Test { - t { - ... on T @defer { - v1 - } - ... on T @defer { - v2 + const operation = operationFromDocument( + schema, + gql` + query Test { + t { + ... on T @defer { + v1 + } + ... on T @defer { + v2 + } } } - } - `); + `, + ); expect(operation.toString()).toMatchString(` query Test { @@ -3040,20 +3181,17 @@ describe('MutableSelectionSet', () => { `); type Value = { - count: number + count: number; }; let calls = 0; const sets: string[] = []; const queryType = schema.schemaDefinition.rootType('query')!; - const ss = MutableSelectionSet.emptyWithMemoized( - queryType, - (s) => { - sets.push(s.toString()); - return { count: ++calls }; - } - ); + const ss = MutableSelectionSet.emptyWithMemoized(queryType, (s) => { + sets.push(s.toString()); + return { count: ++calls }; + }); expect(ss.memoized().count).toBe(1); // Calling a 2nd time with no change to make sure we're not re-generating the value. @@ -3081,7 +3219,12 @@ describe('MutableSelectionSet', () => { expect(ss.memoized().count).toBe(3); // But that of the clone should have changed. expect(cloned.memoized().count).toBe(4); - expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']); + expect(sets).toStrictEqual([ + '{}', + '{ t { v1 } }', + '{ t { v1 v3 } }', + '{ t { v1 v3 v2 } }', + ]); // And here we make sure that if we update the fist selection, we don't have v3 in the set received ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet); @@ -3089,7 +3232,13 @@ describe('MutableSelectionSet', () => { // the total count should be 5 (even if the previous count for `ss` was only 3). expect(ss.memoized().count).toBe(5); expect(cloned.memoized().count).toBe(4); - expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']); + expect(sets).toStrictEqual([ + '{}', + '{ t { v1 } }', + '{ t { v1 v3 } }', + '{ t { v1 v3 v2 } }', + '{ t { v1 v3 v4 } }', + ]); }); }); @@ -3129,36 +3278,51 @@ describe('unsatisfiable branches removal', () => { `); const normalized = (op: string) => { - return parseOperation(schema, op).normalize().toString(false, false) + return parseOperation(schema, op).normalize().toString(false, false); }; - - it.each([ - '{ i { a } }', - '{ i { ... on T1 { a b c } } }', - ])('is identity if there is no unsatisfiable branches', (op) => { - expect(normalized(op)).toBe(op); - }); + it.each(['{ i { a } }', '{ i { ... on T1 { a b c } } }'])( + 'is identity if there is no unsatisfiable branches', + (op) => { + expect(normalized(op)).toBe(op); + }, + ); it.each([ { input: '{ i { ... on I { a } } }', output: '{ i { a } }' }, - { input: '{ i { ... on T1 { ... on I { a b } } } }', output: '{ i { ... on T1 { a b } } }' }, - { input: '{ i { ... on I { a ... on T2 { d } } } }', output: '{ i { a ... on T2 { d } } }' }, - { input: '{ i { ... on T2 { ... on I { a ... on J { b } } } } }', output: '{ i { ... on T2 { a } } }' }, - ])('removes unsatisfiable branches', ({input, output}) => { + { + input: '{ i { ... on T1 { ... on I { a b } } } }', + output: '{ i { ... on T1 { a b } } }', + }, + { + input: '{ i { ... on I { a ... on T2 { d } } } }', + output: '{ i { a ... on T2 { d } } }', + }, + { + input: '{ i { ... on T2 { ... on I { a ... on J { b } } } } }', + output: '{ i { ... on T2 { a } } }', + }, + ])('removes unsatisfiable branches', ({ input, output }) => { expect(normalized(input)).toBe(output); }); }); describe('named fragment selection set restrictions at type', () => { - const expandAtType = (frag: NamedFragmentDefinition, schema: Schema, typeName: string): FragmentRestrictionAtType => { + const expandAtType = ( + frag: NamedFragmentDefinition, + schema: Schema, + typeName: string, + ): FragmentRestrictionAtType => { const type = schema.type(typeName); - assert(type && isCompositeType(type), `Invalid type ${typeName}`) + assert(type && isCompositeType(type), `Invalid type ${typeName}`); // `expandedSelectionSetAtType` assumes it's argument passes `canApplyAtType`, so let's make sure we're // not typo-ing something in our tests. - assert(frag.canApplyDirectlyAtType(type), `${frag.name} cannot be applied at type ${typeName}`); + assert( + frag.canApplyDirectlyAtType(type), + `${frag.name} cannot be applied at type ${typeName}`, + ); return frag.expandedSelectionSetAtType(type); - } + }; test('for fragment on object types', () => { const schema = parseSchema(` @@ -3172,7 +3336,9 @@ describe('named fragment selection set restrictions at type', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t1 { ...FonT1 @@ -3183,7 +3349,8 @@ describe('named fragment selection set restrictions at type', () => { x y } - `); + `, + ); const frag = operation.fragments?.get('FonT1')!; @@ -3223,7 +3390,9 @@ describe('named fragment selection set restrictions at type', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t1 { ...FonI1 @@ -3245,12 +3414,15 @@ describe('named fragment selection set restrictions at type', () => { x } } - `); + `, + ); const frag = operation.fragments?.get('FonI1')!; let { selectionSet, validator } = expandAtType(frag, schema, 'I1'); - expect(selectionSet.toString()).toBe('{ x ... on T1 { x } ... on T2 { x } ... on I2 { x } ... on I3 { x } }'); + expect(selectionSet.toString()).toBe( + '{ x ... on T1 { x } ... on T2 { x } ... on I2 { x } ... on I3 { x } }', + ); // Note: Due to `FieldsInSetCanMerge` rule, we can't use trimmed validators for // fragments on non-object types. expect(validator?.toString()).toMatchString(` @@ -3308,7 +3480,9 @@ describe('named fragment selection set restrictions at type', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t1 { ...FonU1 @@ -3333,7 +3507,8 @@ describe('named fragment selection set restrictions at type', () => { } } } - `); + `, + ); const frag = operation.fragments?.get('FonU1')!; @@ -3341,7 +3516,9 @@ describe('named fragment selection set restrictions at type', () => { // possible runtimes. let { selectionSet, validator } = expandAtType(frag, schema, 'U1'); - expect(selectionSet.toString()).toBe('{ ... on T1 { x y } ... on T2 { z w } }'); + expect(selectionSet.toString()).toBe( + '{ ... on T1 { x y } ... on T2 { z w } }', + ); // Similar remarks than on interfaces (the validator is strictly speaking not necessary, but // this happens due to the "lifting" of selection mentioned above, is a bit hard to avoid, // and is essentially harmess (it may result in a bit more cpu cycles in some cases but @@ -3451,7 +3628,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t { ...FragOnT @@ -3472,7 +3651,8 @@ describe('named fragment rebasing on subgraphs', () => { v5 } } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); @@ -3523,7 +3703,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t { ...FragOnT @@ -3542,7 +3724,8 @@ describe('named fragment rebasing on subgraphs', () => { x y } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); @@ -3591,7 +3774,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { i { ...FragOnI @@ -3608,7 +3793,8 @@ describe('named fragment rebasing on subgraphs', () => { y } } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); @@ -3651,7 +3837,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { i { ...FragOnI @@ -3663,12 +3851,14 @@ describe('named fragment rebasing on subgraphs', () => { id x } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); - const subgraph = buildSchema(` + const subgraph = buildSchema( + ` extend schema @link( url: "https://specs.apollo.dev/federation/v2.5", @@ -3710,7 +3900,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { t { ...F1 @@ -3737,7 +3929,8 @@ describe('named fragment rebasing on subgraphs', () => { c d } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); @@ -3781,7 +3974,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { ...TheQuery } @@ -3799,7 +3994,8 @@ describe('named fragment rebasing on subgraphs', () => { z } } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); @@ -3842,7 +4038,9 @@ describe('named fragment rebasing on subgraphs', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { ...TQuery } @@ -3856,7 +4054,8 @@ describe('named fragment rebasing on subgraphs', () => { } } } - `); + `, + ); const fragments = operation.fragments; assert(fragments, 'Should have some fragments'); diff --git a/internals-js/src/__tests__/removeInaccessibleElements.test.ts b/internals-js/src/__tests__/removeInaccessibleElements.test.ts index 513bf71b0..9afa860bd 100644 --- a/internals-js/src/__tests__/removeInaccessibleElements.test.ts +++ b/internals-js/src/__tests__/removeInaccessibleElements.test.ts @@ -4,13 +4,13 @@ import { InterfaceType, ObjectType, UnionType, -} from "../definitions"; -import { buildSchema } from "../buildSchema"; -import { removeInaccessibleElements } from "../specs/inaccessibleSpec"; -import { GraphQLError } from "graphql"; -import { errorCauses } from "../error"; +} from '../definitions'; +import { buildSchema } from '../buildSchema'; +import { removeInaccessibleElements } from '../specs/inaccessibleSpec'; +import { GraphQLError } from 'graphql'; +import { errorCauses } from '../error'; -describe("removeInaccessibleElements", () => { +describe('removeInaccessibleElements', () => { const INACCESSIBLE_V02_HEADER = ` directive @core(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA @@ -52,7 +52,7 @@ describe("removeInaccessibleElements", () => { expect(causes).toHaveLength(expectedCauseCount); const messages = causes.map((cause) => cause.message); for (const message of messages) { - expect(typeof message === "string").toBeTruthy(); + expect(typeof message === 'string').toBeTruthy(); } messages.sort(); return messages; @@ -106,7 +106,7 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Query.someField')).toBeDefined(); }); it(`fails for no @inaccessible definition`, () => { @@ -206,8 +206,8 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query.someField")).toBeDefined(); - expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined(); + expect(schema.elementByCoordinate('Query.someField')).toBeDefined(); + expect(schema.elementByCoordinate('Query.privateField')).toBeUndefined(); }); it(`handles renames of @inaccessible via import "as"`, () => { @@ -242,8 +242,8 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query.someField")).toBeDefined(); - expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined(); + expect(schema.elementByCoordinate('Query.someField')).toBeDefined(); + expect(schema.elementByCoordinate('Query.privateField')).toBeUndefined(); }); it(`fails for @inaccessible built-ins`, () => { @@ -473,25 +473,25 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query")).toBeDefined(); - expect(schema.elementByCoordinate("Mutation")).toBeUndefined(); - expect(schema.elementByCoordinate("Subscription")).toBeUndefined(); - expect(schema.elementByCoordinate("Object")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Query')).toBeDefined(); + expect(schema.elementByCoordinate('Mutation')).toBeUndefined(); + expect(schema.elementByCoordinate('Subscription')).toBeUndefined(); + expect(schema.elementByCoordinate('Object')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer3.privatefield") + schema.elementByCoordinate('Referencer3.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4")).toBeUndefined(); - const unionType = schema.elementByCoordinate("Referencer5"); + expect(schema.elementByCoordinate('Referencer4')).toBeUndefined(); + const unionType = schema.elementByCoordinate('Referencer5'); expect(unionType instanceof UnionType).toBeTruthy(); - expect((unionType as UnionType).hasTypeMember("Query")).toBeTruthy(); - expect((unionType as UnionType).hasTypeMember("Object")).toBeFalsy(); - expect(schema.elementByCoordinate("Referencer6")).toBeUndefined(); + expect((unionType as UnionType).hasTypeMember('Query')).toBeTruthy(); + expect((unionType as UnionType).hasTypeMember('Object')).toBeFalsy(); + expect(schema.elementByCoordinate('Referencer6')).toBeUndefined(); }); it(`fails to remove @inaccessible object types for breaking removals`, () => { @@ -594,33 +594,33 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("VisibleInterface")).toBeDefined(); - expect(schema.elementByCoordinate("Interface")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('VisibleInterface')).toBeDefined(); + expect(schema.elementByCoordinate('Interface')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer3.privatefield") + schema.elementByCoordinate('Referencer3.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4")).toBeUndefined(); - const objectType = schema.elementByCoordinate("Referencer5"); + expect(schema.elementByCoordinate('Referencer4')).toBeUndefined(); + const objectType = schema.elementByCoordinate('Referencer5'); expect(objectType instanceof ObjectType).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("VisibleInterface") + (objectType as ObjectType).implementsInterface('VisibleInterface'), ).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Interface") + (objectType as ObjectType).implementsInterface('Interface'), ).toBeFalsy(); - const interfaceType = schema.elementByCoordinate("Referencer6"); + const interfaceType = schema.elementByCoordinate('Referencer6'); expect(interfaceType instanceof InterfaceType).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("VisibleInterface") + (interfaceType as InterfaceType).implementsInterface('VisibleInterface'), ).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Interface") + (interfaceType as InterfaceType).implementsInterface('Interface'), ).toBeFalsy(); }); @@ -703,18 +703,18 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("VisibleUnion")).toBeDefined(); - expect(schema.elementByCoordinate("Union")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('VisibleUnion')).toBeDefined(); + expect(schema.elementByCoordinate('Union')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer3.privatefield") + schema.elementByCoordinate('Referencer3.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer4')).toBeUndefined(); }); it(`fails to remove @inaccessible union types for breaking removals`, () => { @@ -829,34 +829,34 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("VisibleInputObject")).toBeDefined(); - expect(schema.elementByCoordinate("InputObject")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('VisibleInputObject')).toBeDefined(); + expect(schema.elementByCoordinate('InputObject')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.someField(privateArg:)") + schema.elementByCoordinate('Referencer1.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer2.privateField") + schema.elementByCoordinate('Referencer2.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer4.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer4.someField(privateArg:)") + schema.elementByCoordinate('Referencer4.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer5.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer5.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer5.privateField") + schema.elementByCoordinate('Referencer5.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer6")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer7.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer6')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer7.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer7.privatefield") + schema.elementByCoordinate('Referencer7.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer8")).toBeUndefined(); - expect(schema.elementByCoordinate("@referencer9")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer8')).toBeUndefined(); + expect(schema.elementByCoordinate('@referencer9')).toBeDefined(); expect( - schema.elementByCoordinate("@referencer9(privateArg:)") + schema.elementByCoordinate('@referencer9(privateArg:)'), ).toBeUndefined(); }); @@ -1007,44 +1007,44 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("VisibleEnum")).toBeDefined(); - expect(schema.elementByCoordinate("Enum")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('VisibleEnum')).toBeDefined(); + expect(schema.elementByCoordinate('Enum')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer3.privatefield") + schema.elementByCoordinate('Referencer3.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer5.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer4')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer5.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer5.someField(privateArg:)") + schema.elementByCoordinate('Referencer5.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer6.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer6.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer6.privateField") + schema.elementByCoordinate('Referencer6.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer7")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer8.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer7')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer8.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer8.someField(privateArg:)") + schema.elementByCoordinate('Referencer8.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer9.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer9.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer9.privateField") + schema.elementByCoordinate('Referencer9.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer10")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer11.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer10')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer11.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer11.privatefield") + schema.elementByCoordinate('Referencer11.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer12")).toBeUndefined(); - expect(schema.elementByCoordinate("@referencer13")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer12')).toBeUndefined(); + expect(schema.elementByCoordinate('@referencer13')).toBeDefined(); expect( - schema.elementByCoordinate("@referencer13(privateArg:)") + schema.elementByCoordinate('@referencer13(privateArg:)'), ).toBeUndefined(); }); @@ -1206,44 +1206,44 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("VisibleScalar")).toBeDefined(); - expect(schema.elementByCoordinate("Scalar")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('VisibleScalar')).toBeDefined(); + expect(schema.elementByCoordinate('Scalar')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer3.privatefield") + schema.elementByCoordinate('Referencer3.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer5.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer4')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer5.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer5.someField(privateArg:)") + schema.elementByCoordinate('Referencer5.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer6.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer6.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer6.privateField") + schema.elementByCoordinate('Referencer6.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer7")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer8.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer7')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer8.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer8.someField(privateArg:)") + schema.elementByCoordinate('Referencer8.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer9.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer9.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer9.privateField") + schema.elementByCoordinate('Referencer9.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer10")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer11.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer10')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer11.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer11.privatefield") + schema.elementByCoordinate('Referencer11.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer12")).toBeUndefined(); - expect(schema.elementByCoordinate("@referencer13")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer12')).toBeUndefined(); + expect(schema.elementByCoordinate('@referencer13')).toBeDefined(); expect( - schema.elementByCoordinate("@referencer13(privateArg:)") + schema.elementByCoordinate('@referencer13(privateArg:)'), ).toBeUndefined(); }); @@ -1364,30 +1364,30 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query.someField")).toBeDefined(); - expect(schema.elementByCoordinate("Query.privateField")).toBeUndefined(); - expect(schema.elementByCoordinate("Mutation.someField")).toBeDefined(); - expect(schema.elementByCoordinate("Mutation.privateField")).toBeUndefined(); - expect(schema.elementByCoordinate("Subscription.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Query.someField')).toBeDefined(); + expect(schema.elementByCoordinate('Query.privateField')).toBeUndefined(); + expect(schema.elementByCoordinate('Mutation.someField')).toBeDefined(); + expect(schema.elementByCoordinate('Mutation.privateField')).toBeUndefined(); + expect(schema.elementByCoordinate('Subscription.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Subscription.privateField") + schema.elementByCoordinate('Subscription.privateField'), ).toBeUndefined(); - const objectType = schema.elementByCoordinate("Object"); + const objectType = schema.elementByCoordinate('Object'); expect(objectType instanceof ObjectType).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Referencer1") + (objectType as ObjectType).implementsInterface('Referencer1'), ).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Referencer2") + (objectType as ObjectType).implementsInterface('Referencer2'), ).toBeFalsy(); - expect(schema.elementByCoordinate("Object.someField")).toBeDefined(); - expect(schema.elementByCoordinate("Object.privateField")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Object.someField')).toBeDefined(); + expect(schema.elementByCoordinate('Object.privateField')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); }); it(`fails to remove @inaccessible object fields for breaking removals`, () => { @@ -1491,24 +1491,24 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - const interfaceType = schema.elementByCoordinate("Interface"); + const interfaceType = schema.elementByCoordinate('Interface'); expect(interfaceType instanceof InterfaceType).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Referencer1") + (interfaceType as InterfaceType).implementsInterface('Referencer1'), ).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Referencer2") + (interfaceType as InterfaceType).implementsInterface('Referencer2'), ).toBeFalsy(); - expect(schema.elementByCoordinate("Interface.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Interface.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Interface.privateField") + schema.elementByCoordinate('Interface.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.privatefield") + schema.elementByCoordinate('Referencer1.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); }); it(`fails to remove @inaccessible interface fields for breaking removals`, () => { @@ -1615,45 +1615,45 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Query.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Query.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Query.someField(privateArg:)") + schema.elementByCoordinate('Query.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Mutation.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Mutation.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Mutation.someField(privateArg:)") + schema.elementByCoordinate('Mutation.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Subscription.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Subscription.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Subscription.someField(privateArg:)") + schema.elementByCoordinate('Subscription.someField(privateArg:)'), ).toBeUndefined(); - const objectType = schema.elementByCoordinate("Object"); + const objectType = schema.elementByCoordinate('Object'); expect(objectType instanceof ObjectType).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Referencer1") + (objectType as ObjectType).implementsInterface('Referencer1'), ).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Referencer2") + (objectType as ObjectType).implementsInterface('Referencer2'), ).toBeTruthy(); expect( - (objectType as ObjectType).implementsInterface("Referencer3") + (objectType as ObjectType).implementsInterface('Referencer3'), ).toBeFalsy(); expect( - schema.elementByCoordinate("Object.someField(someArg:)") + schema.elementByCoordinate('Object.someField(someArg:)'), ).toBeDefined(); expect( - schema.elementByCoordinate("Object.someField(privateArg:)") + schema.elementByCoordinate('Object.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.someField(privateArg:)") + schema.elementByCoordinate('Referencer1.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeDefined(); - expect(schema.elementByCoordinate("Referencer2.someField")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); - expect(schema.elementByCoordinate("ObjectDefault.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2.someField')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); + expect(schema.elementByCoordinate('ObjectDefault.someField')).toBeDefined(); expect( - schema.elementByCoordinate("ObjectDefault.someField(privateArg:)") + schema.elementByCoordinate('ObjectDefault.someField(privateArg:)'), ).toBeUndefined(); }); @@ -1771,54 +1771,54 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - const interfaceType = schema.elementByCoordinate("Interface"); + const interfaceType = schema.elementByCoordinate('Interface'); expect(interfaceType instanceof InterfaceType).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Referencer1") + (interfaceType as InterfaceType).implementsInterface('Referencer1'), ).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Referencer2") + (interfaceType as InterfaceType).implementsInterface('Referencer2'), ).toBeTruthy(); expect( - (interfaceType as InterfaceType).implementsInterface("Referencer3") + (interfaceType as InterfaceType).implementsInterface('Referencer3'), ).toBeFalsy(); expect( - schema.elementByCoordinate("Interface.someField(someArg:)") + schema.elementByCoordinate('Interface.someField(someArg:)'), ).toBeDefined(); expect( - schema.elementByCoordinate("Interface.someField(privateArg:)") + schema.elementByCoordinate('Interface.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.someField(privateArg:)") + schema.elementByCoordinate('Referencer1.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2")).toBeDefined(); - expect(schema.elementByCoordinate("Referencer2.someField")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); - expect(schema.elementByCoordinate("Interface.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2')).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2.someField')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); + expect(schema.elementByCoordinate('Interface.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Interface.someField(privateArg:)") + schema.elementByCoordinate('Interface.someField(privateArg:)'), ).toBeUndefined(); const objectArg = schema.elementByCoordinate( - "Referencer4.someField(privateArg:)" + 'Referencer4.someField(privateArg:)', ); expect(objectArg instanceof ArgumentDefinition).toBeTruthy(); expect( ( objectArg as ArgumentDefinition> - ).isRequired() + ).isRequired(), ).toBeFalsy(); - expect(schema.elementByCoordinate("Referencer5")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer5')).toBeUndefined(); const interfaceArg = schema.elementByCoordinate( - "Referencer6.someField(privateArg:)" + 'Referencer6.someField(privateArg:)', ); expect(interfaceArg instanceof ArgumentDefinition).toBeTruthy(); expect( ( interfaceArg as ArgumentDefinition> - ).isRequired() + ).isRequired(), ).toBeFalsy(); - expect(schema.elementByCoordinate("Referencer7")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer7')).toBeUndefined(); }); it(`fails to remove @inaccessible interface field arguments for breaking removals`, () => { @@ -1973,46 +1973,46 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("InputObject.someField")).toBeDefined(); + expect(schema.elementByCoordinate('InputObject.someField')).toBeDefined(); expect( - schema.elementByCoordinate("InputObject.privateField") + schema.elementByCoordinate('InputObject.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.someField(privateArg:)") + schema.elementByCoordinate('Referencer1.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer2.privateField") + schema.elementByCoordinate('Referencer2.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer4.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer4.someField(privateArg:)") + schema.elementByCoordinate('Referencer4.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer5.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer5.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer5.privateField") + schema.elementByCoordinate('Referencer5.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer6")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer7.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer6')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer7.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer7.privatefield") + schema.elementByCoordinate('Referencer7.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer8")).toBeUndefined(); - expect(schema.elementByCoordinate("@referencer9")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer8')).toBeUndefined(); + expect(schema.elementByCoordinate('@referencer9')).toBeDefined(); expect( - schema.elementByCoordinate("@referencer9(privateArg:)") + schema.elementByCoordinate('@referencer9(privateArg:)'), ).toBeUndefined(); expect( - schema.elementByCoordinate("Referencer10.someField(privateArg:)") + schema.elementByCoordinate('Referencer10.someField(privateArg:)'), ).toBeDefined(); - expect(schema.elementByCoordinate("Referencer11")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer11')).toBeUndefined(); expect( - schema.elementByCoordinate("InputObjectDefault.someField") + schema.elementByCoordinate('InputObjectDefault.someField'), ).toBeDefined(); expect( - schema.elementByCoordinate("InputObjectDefault.privatefield") + schema.elementByCoordinate('InputObjectDefault.privatefield'), ).toBeUndefined(); }); @@ -2179,39 +2179,39 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("Enum.SOME_VALUE")).toBeDefined(); - expect(schema.elementByCoordinate("Enum.PRIVATE_VALUE")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer1.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Enum.SOME_VALUE')).toBeDefined(); + expect(schema.elementByCoordinate('Enum.PRIVATE_VALUE')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer1.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer1.someField(privateArg:)") + schema.elementByCoordinate('Referencer1.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer2.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer2.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer2.privateField") + schema.elementByCoordinate('Referencer2.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer3")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer4.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer3')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer4.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer4.someField(privateArg:)") + schema.elementByCoordinate('Referencer4.someField(privateArg:)'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer5.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer5.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer5.privateField") + schema.elementByCoordinate('Referencer5.privateField'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer6")).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer7.someField")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer6')).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer7.someField')).toBeDefined(); expect( - schema.elementByCoordinate("Referencer7.privatefield") + schema.elementByCoordinate('Referencer7.privatefield'), ).toBeUndefined(); - expect(schema.elementByCoordinate("Referencer8")).toBeUndefined(); - expect(schema.elementByCoordinate("@referencer9")).toBeDefined(); + expect(schema.elementByCoordinate('Referencer8')).toBeUndefined(); + expect(schema.elementByCoordinate('@referencer9')).toBeDefined(); expect( - schema.elementByCoordinate("@referencer9(privateArg:)") + schema.elementByCoordinate('@referencer9(privateArg:)'), ).toBeUndefined(); expect( - schema.elementByCoordinate("Referencer10.someField(privateArg:)") + schema.elementByCoordinate('Referencer10.someField(privateArg:)'), ).toBeDefined(); - expect(schema.elementByCoordinate("Referencer11")).toBeUndefined(); + expect(schema.elementByCoordinate('Referencer11')).toBeUndefined(); }); it(`fails to remove @inaccessible enum values for breaking removals`, () => { @@ -2296,15 +2296,15 @@ describe("removeInaccessibleElements", () => { removeInaccessibleElements(schema); schema.validate(); - expect(schema.elementByCoordinate("@directive(someArg:)")).toBeDefined(); + expect(schema.elementByCoordinate('@directive(someArg:)')).toBeDefined(); expect( - schema.elementByCoordinate("@directive(privateArg:)") + schema.elementByCoordinate('@directive(privateArg:)'), ).toBeUndefined(); expect( - schema.elementByCoordinate("@directiveDefault(someArg:)") + schema.elementByCoordinate('@directiveDefault(someArg:)'), ).toBeDefined(); expect( - schema.elementByCoordinate("@directiveDefault(privateArg:)") + schema.elementByCoordinate('@directiveDefault(privateArg:)'), ).toBeUndefined(); }); diff --git a/internals-js/src/__tests__/schemaUpgrader.test.ts b/internals-js/src/__tests__/schemaUpgrader.test.ts index 4ac656950..008e09a76 100644 --- a/internals-js/src/__tests__/schemaUpgrader.test.ts +++ b/internals-js/src/__tests__/schemaUpgrader.test.ts @@ -1,11 +1,22 @@ -import { FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED, printSchema } from '..'; +import { + FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED, + printSchema, +} from '..'; import { ObjectType } from '../definitions'; import { buildSubgraph, Subgraphs } from '../federation'; -import { UpgradeChangeID, UpgradeResult, upgradeSubgraphsIfNecessary } from '../schemaUpgrader'; - -function changeMessages(res: UpgradeResult, subgraphName: string, id: UpgradeChangeID): string[] { +import { + UpgradeChangeID, + UpgradeResult, + upgradeSubgraphsIfNecessary, +} from '../schemaUpgrader'; + +function changeMessages( + res: UpgradeResult, + subgraphName: string, + id: UpgradeChangeID, +): string[] { const changes = res.changes?.get(subgraphName)?.get(id); - return changes?.map(c => c.toString()) ?? []; + return changes?.map((c) => c.toString()) ?? []; } /** @@ -57,36 +68,46 @@ test('upgrade complex schema', () => { const res = upgradeSubgraphsIfNecessary(subgraphs); expect(res.errors).toBeUndefined(); - expect(changeMessages(res, 's1', 'EXTERNAL_ON_TYPE_EXTENSION_REMOVAL')).toStrictEqual([ - 'Removed @external from field "Product.upc" as it is a key of an extension type' + expect( + changeMessages(res, 's1', 'EXTERNAL_ON_TYPE_EXTENSION_REMOVAL'), + ).toStrictEqual([ + 'Removed @external from field "Product.upc" as it is a key of an extension type', ]); expect(changeMessages(res, 's1', 'TYPE_EXTENSION_REMOVAL')).toStrictEqual([ - 'Switched type "Product" from an extension to a definition' + 'Switched type "Product" from an extension to a definition', ]); expect(changeMessages(res, 's1', 'UNUSED_EXTERNAL_REMOVAL')).toStrictEqual([ - 'Removed @external field "Product.name" as it was not used in any @key, @provides or @requires' + 'Removed @external field "Product.name" as it was not used in any @key, @provides or @requires', ]); - expect(changeMessages(res, 's1', 'EXTERNAL_ON_INTERFACE_REMOVAL')).toStrictEqual([ - 'Removed @external directive on interface type field "I.description": @external is nonsensical on interface fields' + expect( + changeMessages(res, 's1', 'EXTERNAL_ON_INTERFACE_REMOVAL'), + ).toStrictEqual([ + 'Removed @external directive on interface type field "I.description": @external is nonsensical on interface fields', ]); - expect(changeMessages(res, 's1', 'INACTIVE_PROVIDES_OR_REQUIRES_REMOVAL')).toStrictEqual([ - 'Removed directive @requires(fields: "upc") on "Product.inventory": none of the fields were truly @external' + expect( + changeMessages(res, 's1', 'INACTIVE_PROVIDES_OR_REQUIRES_REMOVAL'), + ).toStrictEqual([ + 'Removed directive @requires(fields: "upc") on "Product.inventory": none of the fields were truly @external', ]); - expect(changeMessages(res, 's1', 'INACTIVE_PROVIDES_OR_REQUIRES_FIELDS_REMOVAL')).toStrictEqual([ - 'Updated directive @provides(fields: "upc description") on "Query.products" to @provides(fields: "description"): removed fields that were not truly @external' + expect( + changeMessages(res, 's1', 'INACTIVE_PROVIDES_OR_REQUIRES_FIELDS_REMOVAL'), + ).toStrictEqual([ + 'Updated directive @provides(fields: "upc description") on "Query.products" to @provides(fields: "description"): removed fields that were not truly @external', ]); expect(changeMessages(res, 's1', 'KEY_ON_INTERFACE_REMOVAL')).toStrictEqual([ - 'Removed @key on interface "I": while allowed by federation 0.x, @key on interfaces were completely ignored/had no effect' + 'Removed @key on interface "I": while allowed by federation 0.x, @key on interfaces were completely ignored/had no effect', ]); - expect(changeMessages(res, 's1', 'PROVIDES_ON_NON_COMPOSITE_REMOVAL')).toStrictEqual([ - 'Removed @provides directive on field "Random.x" as it is of non-composite type "Int": while not rejected by federation 0.x, such @provide is nonsensical and was ignored' + expect( + changeMessages(res, 's1', 'PROVIDES_ON_NON_COMPOSITE_REMOVAL'), + ).toStrictEqual([ + 'Removed @provides directive on field "Random.x" as it is of non-composite type "Int": while not rejected by federation 0.x, such @provide is nonsensical and was ignored', ]); expect(res.subgraphs?.get('s1')?.toString()).toMatchString(` @@ -140,7 +161,9 @@ test('update federation directive non-string arguments', () => { const res = upgradeSubgraphsIfNecessary(subgraphs); expect(res.errors).toBeUndefined(); - expect(changeMessages(res, 's', 'FIELDS_ARGUMENT_COERCION_TO_STRING')).toStrictEqual([ + expect( + changeMessages(res, 's', 'FIELDS_ARGUMENT_COERCION_TO_STRING'), + ).toStrictEqual([ 'Coerced "fields" argument for directive @key for "A" into a string: coerced from @key(fields: id) to @key(fields: "id")', 'Coerced "fields" argument for directive @key for "A" into a string: coerced from @key(fields: ["id", "x"]) to @key(fields: "id x")', ]); @@ -164,7 +187,7 @@ test('update federation directive non-string arguments', () => { x: Int } `); -}) +}); test('remove tag on external field if found on definition', () => { const s1 = ` @@ -196,11 +219,21 @@ test('remove tag on external field if found on definition', () => { 'Removed @tag(name: "a tag") application on @external "A.y" as the @tag application is on another definition', ]); - const typeAInS1 = res.subgraphs?.get('s1')?.schema.type("A") as ObjectType; - const typeAInS2 = res.subgraphs?.get('s2')?.schema.type("A") as ObjectType; - expect(typeAInS1.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([]); - expect(typeAInS2.field("y")?.appliedDirectivesOf('tag').map((d) => d.toString())).toStrictEqual([ '@tag(name: "a tag")' ]); -}) + const typeAInS1 = res.subgraphs?.get('s1')?.schema.type('A') as ObjectType; + const typeAInS2 = res.subgraphs?.get('s2')?.schema.type('A') as ObjectType; + expect( + typeAInS1 + .field('y') + ?.appliedDirectivesOf('tag') + .map((d) => d.toString()), + ).toStrictEqual([]); + expect( + typeAInS2 + .field('y') + ?.appliedDirectivesOf('tag') + .map((d) => d.toString()), + ).toStrictEqual(['@tag(name: "a tag")']); +}); test('reject @interfaceObject usage if not all subgraphs are fed2', () => { // Note that this test both validates the rejection of fed1 subgraph when @interfaceObject is used somewhere, but also @@ -240,10 +273,10 @@ test('reject @interfaceObject usage if not all subgraphs are fed2', () => { subgraphs.add(buildSubgraph('s2', 'http://s2', s2)); const res = upgradeSubgraphsIfNecessary(subgraphs); expect(res.errors?.map((e) => e.message)).toStrictEqual([ - 'The @interfaceObject directive can only be used if all subgraphs have federation 2 subgraph schema (schema with a `@link` to "https://specs.apollo.dev/federation" version 2.0 or newer): ' - + '@interfaceObject is used in subgraph "s1" but subgraph "s2" is not a federation 2 subgraph schema.' + 'The @interfaceObject directive can only be used if all subgraphs have federation 2 subgraph schema (schema with a `@link` to "https://specs.apollo.dev/federation" version 2.0 or newer): ' + + '@interfaceObject is used in subgraph "s1" but subgraph "s2" is not a federation 2 subgraph schema.', ]); -}) +}); test('handles the addition of @shareable when an @external is used on a type', () => { const s1 = ` @@ -278,22 +311,27 @@ test('handles the addition of @shareable when an @external is used on a type', ( // 2. field `T.x` in s1 must be marked @shareable since it is resolved by s2 (since again, it's @external annotation is ignored). const s2Upgraded = res.subgraphs?.get('s2')!; - expect(s2Upgraded.schema.type('T')?.hasAppliedDirective('external')).toBe(false); + expect(s2Upgraded.schema.type('T')?.hasAppliedDirective('external')).toBe( + false, + ); const s1Upgraded = res.subgraphs?.get('s1')!; - expect((s1Upgraded.schema.type('T') as ObjectType).field('x')?.hasAppliedDirective('shareable')).toBe(true); - -}) + expect( + (s1Upgraded.schema.type('T') as ObjectType) + .field('x') + ?.hasAppliedDirective('shareable'), + ).toBe(true); +}); -test("fully upgrades a schema with no @link directive", () => { +test('fully upgrades a schema with no @link directive', () => { const subgraph = buildSubgraph( - "subgraph", - "", + 'subgraph', + '', `#graphql type Query { hello: String } - ` + `, ); const subgraphs = new Subgraphs(); @@ -316,12 +354,12 @@ test("fully upgrades a schema with no @link directive", () => { // router that supports the build pipeline they're upgrading to, but that // mechanism isn't in place yet. // - Trevor - expect(printSchema(result.subgraphs!.get("subgraph")!.schema!)).toContain( -`schema + expect(printSchema(result.subgraphs!.get('subgraph')!.schema!)).toContain( + `schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"]) { query: Query -}` +}`, ); }); diff --git a/internals-js/src/__tests__/subgraphValidation.test.ts b/internals-js/src/__tests__/subgraphValidation.test.ts index 9232ff84a..ab0292e12 100644 --- a/internals-js/src/__tests__/subgraphValidation.test.ts +++ b/internals-js/src/__tests__/subgraphValidation.test.ts @@ -1,13 +1,13 @@ import { DocumentNode } from 'graphql'; import gql from 'graphql-tag'; import { Subgraph } from '..'; -import { buildSubgraph } from "../federation" +import { buildSubgraph } from '../federation'; import { defaultPrintOptions, printSchema } from '../print'; import { buildForErrors } from './testUtils'; describe('fieldset-based directives', () => { it('rejects field defined with arguments in @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -15,14 +15,17 @@ describe('fieldset-based directives', () => { type T @key(fields: "f") { f(x: Int): Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_FIELDS_HAS_ARGS', '[S] On type "T", for @key(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @key)'] + [ + 'KEY_FIELDS_HAS_ARGS', + '[S] On type "T", for @key(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @key)', + ], ]); }); it('rejects field defined with arguments in @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "f") } @@ -30,14 +33,17 @@ describe('fieldset-based directives', () => { type T { f(x: Int): Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_FIELDS_HAS_ARGS', '[S] On field "Query.t", for @provides(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @provides)'] + [ + 'PROVIDES_FIELDS_HAS_ARGS', + '[S] On field "Query.t", for @provides(fields: "f"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @provides)', + ], ]); }); it('rejects @provides on non-external fields', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "f") } @@ -45,14 +51,17 @@ describe('fieldset-based directives', () => { type T { f: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_FIELDS_MISSING_EXTERNAL', '[S] On field "Query.t", for @provides(fields: "f"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)'] + [ + 'PROVIDES_FIELDS_MISSING_EXTERNAL', + '[S] On field "Query.t", for @provides(fields: "f"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)', + ], ]); }); it('rejects @requires on non-external fields', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -61,14 +70,19 @@ describe('fieldset-based directives', () => { f: Int g: Int @requires(fields: "f") } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_FIELDS_MISSING_EXTERNAL', '[S] On field "T.g", for @requires(fields: "f"): field "T.f" should not be part of a @requires since it is already provided by this subgraph (it is not marked @external)'] + [ + 'REQUIRES_FIELDS_MISSING_EXTERNAL', + '[S] On field "T.g", for @requires(fields: "f"): field "T.f" should not be part of a @requires since it is already provided by this subgraph (it is not marked @external)', + ], ]); }); - it.each(['2.0', '2.1', '2.2'])('rejects @key on interfaces _in the %p spec_', (version) => { - const subgraph = gql` + it.each(['2.0', '2.1', '2.2'])( + 'rejects @key on interfaces _in the %p spec_', + (version) => { + const subgraph = gql` extend schema @link(url: "https://specs.apollo.dev/federation/v${version}", import: ["@key"]) @@ -79,14 +93,18 @@ describe('fieldset-based directives', () => { interface T @key(fields: "f") { f: Int } - ` - expect(buildForErrors(subgraph, { asFed2: false })).toStrictEqual([ - ['KEY_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @key on interface "T": @key is not yet supported on interfaces'], - ]); - }); + `; + expect(buildForErrors(subgraph, { asFed2: false })).toStrictEqual([ + [ + 'KEY_UNSUPPORTED_ON_INTERFACE', + '[S] Cannot use @key on interface "T": @key is not yet supported on interfaces', + ], + ]); + }, + ); it('rejects @provides on interfaces', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -98,14 +116,17 @@ describe('fieldset-based directives', () => { type U { g: Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @provides on field "T.f" of parent type "T": @provides is not yet supported within interfaces'], + [ + 'PROVIDES_UNSUPPORTED_ON_INTERFACE', + '[S] Cannot use @provides on field "T.f" of parent type "T": @provides is not yet supported within interfaces', + ], ]); }); it('rejects @requires on interfaces', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -114,15 +135,21 @@ describe('fieldset-based directives', () => { f: Int @external g: Int @requires(fields: "f") } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_UNSUPPORTED_ON_INTERFACE', '[S] Cannot use @requires on field "T.g" of parent type "T": @requires is not yet supported within interfaces' ], - ['EXTERNAL_ON_INTERFACE', '[S] Interface type field "T.f" is marked @external but @external is not allowed on interface fields (it is nonsensical).' ], + [ + 'REQUIRES_UNSUPPORTED_ON_INTERFACE', + '[S] Cannot use @requires on field "T.g" of parent type "T": @requires is not yet supported within interfaces', + ], + [ + 'EXTERNAL_ON_INTERFACE', + '[S] Interface type field "T.f" is marked @external but @external is not allowed on interface fields (it is nonsensical).', + ], ]); }); it('rejects unused @external', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -130,14 +157,17 @@ describe('fieldset-based directives', () => { type T { f: Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).'], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); it('rejects @provides on non-object fields', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: Int @provides(fields: "f") } @@ -145,14 +175,17 @@ describe('fieldset-based directives', () => { type T { f: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_ON_NON_OBJECT_FIELD', '[S] Invalid @provides directive on field "Query.t": field has type "Int" which is not a Composite Type'], + [ + 'PROVIDES_ON_NON_OBJECT_FIELD', + '[S] Invalid @provides directive on field "Query.t": field has type "Int" which is not a Composite Type', + ], ]); }); it('rejects a non-string argument to @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -160,14 +193,17 @@ describe('fieldset-based directives', () => { type T @key(fields: ["f"]) { f: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_INVALID_FIELDS_TYPE', '[S] On type "T", for @key(fields: ["f"]): Invalid value for argument "fields": must be a string.'], + [ + 'KEY_INVALID_FIELDS_TYPE', + '[S] On type "T", for @key(fields: ["f"]): Invalid value for argument "fields": must be a string.', + ], ]); }); it('rejects a non-string argument to @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: ["f"]) } @@ -175,18 +211,24 @@ describe('fieldset-based directives', () => { type T { f: Int @external } - ` + `; // Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on // `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully // not a big deal (having errors dependencies is not exactly unheard of). expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_INVALID_FIELDS_TYPE', '[S] On field "Query.t", for @provides(fields: ["f"]): Invalid value for argument "fields": must be a string.'], - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ], + [ + 'PROVIDES_INVALID_FIELDS_TYPE', + '[S] On field "Query.t", for @provides(fields: ["f"]): Invalid value for argument "fields": must be a string.', + ], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); it('rejects a non-string argument to @requires', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -195,20 +237,26 @@ describe('fieldset-based directives', () => { f: Int @external g: Int @requires(fields: ["f"]) } - ` + `; // Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on // `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully // not a big deal (having errors dependencies is not exactly unheard of). expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_INVALID_FIELDS_TYPE', '[S] On field "T.g", for @requires(fields: ["f"]): Invalid value for argument "fields": must be a string.'], - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ], + [ + 'REQUIRES_INVALID_FIELDS_TYPE', + '[S] On field "T.g", for @requires(fields: ["f"]): Invalid value for argument "fields": must be a string.', + ], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); // Special case of non-string argument, specialized because it hits a different // code-path due to enum values being parsed as string and requiring special care. it('rejects an enum-like argument to @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -216,16 +264,19 @@ describe('fieldset-based directives', () => { type T @key(fields: f) { f: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_INVALID_FIELDS_TYPE', '[S] On type "T", for @key(fields: f): Invalid value for argument "fields": must be a string.'], + [ + 'KEY_INVALID_FIELDS_TYPE', + '[S] On type "T", for @key(fields: f): Invalid value for argument "fields": must be a string.', + ], ]); }); // Special case of non-string argument, specialized because it hits a different // code-path due to enum values being parsed as string and requiring special care. it('rejects an enum-lik argument to @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: f) } @@ -233,20 +284,26 @@ describe('fieldset-based directives', () => { type T { f: Int @external } - ` + `; // Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on // `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully // not a big deal (having errors dependencies is not exactly unheard of). expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_INVALID_FIELDS_TYPE', '[S] On field "Query.t", for @provides(fields: f): Invalid value for argument "fields": must be a string.'], - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ], + [ + 'PROVIDES_INVALID_FIELDS_TYPE', + '[S] On field "Query.t", for @provides(fields: f): Invalid value for argument "fields": must be a string.', + ], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); // Special case of non-string argument, specialized because it hits a different // code-path due to enum values being parsed as string and requiring special care. it('rejects an enum-like argument to @requires', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -255,18 +312,24 @@ describe('fieldset-based directives', () => { f: Int @external g: Int @requires(fields: f) } - ` + `; // Note: since the error here is that we cannot parse the key `fields`, this also mean that @external on // `f` will appear unused and we get an error for it. It's kind of hard to avoid cleanly and hopefully // not a big deal (having errors dependencies is not exactly unheard of). expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_INVALID_FIELDS_TYPE', '[S] On field "T.g", for @requires(fields: f): Invalid value for argument "fields": must be a string.'], - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ], + [ + 'REQUIRES_INVALID_FIELDS_TYPE', + '[S] On field "T.g", for @requires(fields: f): Invalid value for argument "fields": must be a string.', + ], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); it('rejects an invalid `fields` argument to @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -274,14 +337,17 @@ describe('fieldset-based directives', () => { type T @key(fields: ":f") { f: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_INVALID_FIELDS', '[S] On type "T", for @key(fields: ":f"): Syntax Error: Expected Name, found ":".'], + [ + 'KEY_INVALID_FIELDS', + '[S] On type "T", for @key(fields: ":f"): Syntax Error: Expected Name, found ":".', + ], ]); }); it('rejects an invalid `fields` argument to @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "{{f}}") } @@ -289,15 +355,21 @@ describe('fieldset-based directives', () => { type T { f: Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_INVALID_FIELDS', '[S] On field "Query.t", for @provides(fields: "{{f}}"): Syntax Error: Expected Name, found "{".'], - ['EXTERNAL_UNUSED', '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).' ], + [ + 'PROVIDES_INVALID_FIELDS', + '[S] On field "Query.t", for @provides(fields: "{{f}}"): Syntax Error: Expected Name, found "{".', + ], + [ + 'EXTERNAL_UNUSED', + '[S] Field "T.f" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface; the field declaration has no use and should be removed (or the field should not be @external).', + ], ]); }); it('rejects an invalid `fields` argument to @requires', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -306,14 +378,17 @@ describe('fieldset-based directives', () => { f: Int @external g: Int @requires(fields: "f b") } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_INVALID_FIELDS', '[S] On field "T.g", for @requires(fields: "f b"): Cannot query field "b" on type "T" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).'], + [ + 'REQUIRES_INVALID_FIELDS', + '[S] On field "T.g", for @requires(fields: "f b"): Cannot query field "b" on type "T" (if the field is defined in another subgraph, you need to add it to this subgraph with @external).', + ], ]); }); it('rejects @key on an interface field', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -325,14 +400,17 @@ describe('fieldset-based directives', () => { interface I { i: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_FIELDS_SELECT_INVALID_TYPE', '[S] On type "T", for @key(fields: "f"): field "T.f" is a Interface type which is not allowed in @key'], + [ + 'KEY_FIELDS_SELECT_INVALID_TYPE', + '[S] On type "T", for @key(fields: "f"): field "T.f" is a Interface type which is not allowed in @key', + ], ]); }); it('rejects @key on an union field', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -342,14 +420,17 @@ describe('fieldset-based directives', () => { } union U = Query | T - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_FIELDS_SELECT_INVALID_TYPE', '[S] On type "T", for @key(fields: "f"): field "T.f" is a Union type which is not allowed in @key'], + [ + 'KEY_FIELDS_SELECT_INVALID_TYPE', + '[S] On type "T", for @key(fields: "f"): field "T.f" is a Union type which is not allowed in @key', + ], ]); }); it('rejects directive applications in @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -362,14 +443,17 @@ describe('fieldset-based directives', () => { x: Int y: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['KEY_DIRECTIVE_IN_FIELDS_ARG', '[S] On type "T", for @key(fields: "v { x ... @include(if: false) { y }}"): cannot have directive applications in the @key(fields:) argument but found @include(if: false).'], + [ + 'KEY_DIRECTIVE_IN_FIELDS_ARG', + '[S] On type "T", for @key(fields: "v { x ... @include(if: false) { y }}"): cannot have directive applications in the @key(fields:) argument but found @include(if: false).', + ], ]); }); it('rejects directive applications in @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "v { ... on V @skip(if: true) { x y } }") } @@ -383,14 +467,17 @@ describe('fieldset-based directives', () => { x: Int y: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_DIRECTIVE_IN_FIELDS_ARG', '[S] On field "Query.t", for @provides(fields: "v { ... on V @skip(if: true) { x y } }"): cannot have directive applications in the @provides(fields:) argument but found @skip(if: true).'], + [ + 'PROVIDES_DIRECTIVE_IN_FIELDS_ARG', + '[S] On field "Query.t", for @provides(fields: "v { ... on V @skip(if: true) { x y } }"): cannot have directive applications in the @provides(fields:) argument but found @skip(if: true).', + ], ]); }); it('rejects directive applications in @requires', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -400,14 +487,17 @@ describe('fieldset-based directives', () => { a: Int @requires(fields: "... @skip(if: false) { b }") b: Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['REQUIRES_DIRECTIVE_IN_FIELDS_ARG', '[S] On field "T.a", for @requires(fields: "... @skip(if: false) { b }"): cannot have directive applications in the @requires(fields:) argument but found @skip(if: false).'], + [ + 'REQUIRES_DIRECTIVE_IN_FIELDS_ARG', + '[S] On field "T.a", for @requires(fields: "... @skip(if: false) { b }"): cannot have directive applications in the @requires(fields:) argument but found @skip(if: false).', + ], ]); }); it('can collect multiple errors in a single `fields` argument', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "f(x: 3)") } @@ -416,15 +506,21 @@ describe('fieldset-based directives', () => { id: ID f(x: Int): Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['PROVIDES_FIELDS_HAS_ARGS', '[S] On field "Query.t", for @provides(fields: "f(x: 3)"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @provides)'], - ['PROVIDES_FIELDS_MISSING_EXTERNAL', '[S] On field "Query.t", for @provides(fields: "f(x: 3)"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)'], + [ + 'PROVIDES_FIELDS_HAS_ARGS', + '[S] On field "Query.t", for @provides(fields: "f(x: 3)"): field T.f cannot be included because it has arguments (fields with argument are not allowed in @provides)', + ], + [ + 'PROVIDES_FIELDS_MISSING_EXTERNAL', + '[S] On field "Query.t", for @provides(fields: "f(x: 3)"): field "T.f" should not be part of a @provides since it is already provided by this subgraph (it is not marked @external)', + ], ]); }); it('rejects aliases in @key', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -432,14 +528,17 @@ describe('fieldset-based directives', () => { type T @key(fields: "foo: id") { id: ID! } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - [ 'KEY_INVALID_FIELDS', '[S] On type "T", for @key(fields: "foo: id"): Cannot use alias "foo" in "foo: id": aliases are not currently supported in @key' ], + [ + 'KEY_INVALID_FIELDS', + '[S] On type "T", for @key(fields: "foo: id"): Cannot use alias "foo" in "foo: id": aliases are not currently supported in @key', + ], ]); }); it('rejects aliases in @provides', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T @provides(fields: "bar: x") } @@ -448,14 +547,17 @@ describe('fieldset-based directives', () => { id: ID! x: Int @external } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - [ 'PROVIDES_INVALID_FIELDS', '[S] On field "Query.t", for @provides(fields: "bar: x"): Cannot use alias "bar" in "bar: x": aliases are not currently supported in @provides' ], + [ + 'PROVIDES_INVALID_FIELDS', + '[S] On field "Query.t", for @provides(fields: "bar: x"): Cannot use alias "bar" in "bar: x": aliases are not currently supported in @provides', + ], ]); }); it('rejects aliases in @requires', () => { - const subgraph = gql` + const subgraph = gql` type Query { t: T } @@ -471,17 +573,23 @@ describe('fieldset-based directives', () => { a: Int b: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - [ 'REQUIRES_INVALID_FIELDS', '[S] On field "T.g", for @requires(fields: "foo: y"): Cannot use alias "foo" in "foo: y": aliases are not currently supported in @requires' ], - [ 'REQUIRES_INVALID_FIELDS', '[S] On field "T.h", for @requires(fields: "x { m: a n: b }"): Cannot use alias "m" in "m: a": aliases are not currently supported in @requires' ], + [ + 'REQUIRES_INVALID_FIELDS', + '[S] On field "T.g", for @requires(fields: "foo: y"): Cannot use alias "foo" in "foo: y": aliases are not currently supported in @requires', + ], + [ + 'REQUIRES_INVALID_FIELDS', + '[S] On field "T.h", for @requires(fields: "x { m: a n: b }"): Cannot use alias "m" in "m: a": aliases are not currently supported in @requires', + ], ]); }); }); describe('root types', () => { it('rejects using Query as type name if not the query root', () => { - const subgraph = gql` + const subgraph = gql` schema { query: MyQuery } @@ -493,14 +601,17 @@ describe('root types', () => { type Query { g: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['ROOT_QUERY_USED', '[S] The schema has a type named "Query" but it is not set as the query root type ("MyQuery" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'], + [ + 'ROOT_QUERY_USED', + '[S] The schema has a type named "Query" but it is not set as the query root type ("MyQuery" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.', + ], ]); }); it('rejects using Mutation as type name if not the mutation root', () => { - const subgraph = gql` + const subgraph = gql` schema { mutation: MyMutation } @@ -512,14 +623,17 @@ describe('root types', () => { type Mutation { g: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['ROOT_MUTATION_USED', '[S] The schema has a type named "Mutation" but it is not set as the mutation root type ("MyMutation" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'], + [ + 'ROOT_MUTATION_USED', + '[S] The schema has a type named "Mutation" but it is not set as the mutation root type ("MyMutation" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.', + ], ]); }); it('rejects using Subscription as type name if not the subscription root', () => { - const subgraph = gql` + const subgraph = gql` schema { subscription: MySubscription } @@ -531,32 +645,49 @@ describe('root types', () => { type Subscription { g: Int } - ` + `; expect(buildForErrors(subgraph)).toStrictEqual([ - ['ROOT_SUBSCRIPTION_USED', '[S] The schema has a type named "Subscription" but it is not set as the subscription root type ("MySubscription" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.'], + [ + 'ROOT_SUBSCRIPTION_USED', + '[S] The schema has a type named "Subscription" but it is not set as the subscription root type ("MySubscription" is instead): this is not supported by federation. If a root type does not use its default name, there should be no other type with that default name.', + ], ]); }); }); describe('custom error message for misnamed directives', () => { it.each([ - { name: 'fed1', extraMsg: ' If so, note that it is a federation 2 directive but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.' }, + { + name: 'fed1', + extraMsg: + ' If so, note that it is a federation 2 directive but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.', + }, { name: 'fed2', extraMsg: '' }, - - ])('has suggestions if a federation directive name is mispelled in $name', ({name, extraMsg}) => { - const subgraph = gql` - type T @keys(fields: "id") { - id: Int @foo - foo: String @sharable - } - `; - - expect(buildForErrors(subgraph, { asFed2 : name === 'fed2' })).toStrictEqual([ - ['INVALID_GRAPHQL', `[S] Unknown directive "@foo".`], - ['INVALID_GRAPHQL', `[S] Unknown directive "@sharable". Did you mean "@shareable"?${extraMsg}`], - ['INVALID_GRAPHQL', `[S] Unknown directive "@keys". Did you mean "@key"?`], - ]); - }); + ])( + 'has suggestions if a federation directive name is mispelled in $name', + ({ name, extraMsg }) => { + const subgraph = gql` + type T @keys(fields: "id") { + id: Int @foo + foo: String @sharable + } + `; + + expect( + buildForErrors(subgraph, { asFed2: name === 'fed2' }), + ).toStrictEqual([ + ['INVALID_GRAPHQL', `[S] Unknown directive "@foo".`], + [ + 'INVALID_GRAPHQL', + `[S] Unknown directive "@sharable". Did you mean "@shareable"?${extraMsg}`, + ], + [ + 'INVALID_GRAPHQL', + `[S] Unknown directive "@keys". Did you mean "@key"?`, + ], + ]); + }, + ); it('has suggestions if a fed2 directive is used in fed1', () => { const subgraph = gql` @@ -566,16 +697,21 @@ describe('custom error message for misnamed directives', () => { } `; - expect(buildForErrors(subgraph, { asFed2 : false })).toStrictEqual([ - ['INVALID_GRAPHQL', `[S] Unknown directive "@shareable". If you meant the \"@shareable\" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`], + expect(buildForErrors(subgraph, { asFed2: false })).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + `[S] Unknown directive "@shareable". If you meant the \"@shareable\" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.`, + ], ]); }); it('has suggestions if a fed2 directive is used under the wrong name (for the schema)', () => { const subgraph = gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", - import: [ { name: "@key", as: "@myKey"} ]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [{ name: "@key", as: "@myKey" }] + ) type T @key(fields: "id") { id: Int @@ -584,9 +720,15 @@ describe('custom error message for misnamed directives', () => { `; // Note: it's a fed2 schema, but we manually add the @link, so we pass `asFed2: false` to avoid having added twice. - expect(buildForErrors(subgraph, { asFed2 : false })).toStrictEqual([ - ['INVALID_GRAPHQL', `[S] Unknown directive "@shareable". If you meant the \"@shareable\" federation directive, you should use fully-qualified name "@federation__shareable" or add "@shareable" to the \`import\` argument of the @link to the federation specification.`], - ['INVALID_GRAPHQL', `[S] Unknown directive "@key". If you meant the "@key" federation directive, you should use "@myKey" as it is imported under that name in the @link to the federation specification of this schema.`], + expect(buildForErrors(subgraph, { asFed2: false })).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + `[S] Unknown directive "@shareable". If you meant the \"@shareable\" federation directive, you should use fully-qualified name "@federation__shareable" or add "@shareable" to the \`import\` argument of the @link to the federation specification.`, + ], + [ + 'INVALID_GRAPHQL', + `[S] Unknown directive "@key". If you meant the "@key" federation directive, you should use "@myKey" as it is imported under that name in the @link to the federation specification of this schema.`, + ], ]); }); }); @@ -659,13 +801,18 @@ describe('@core/@link handling', () => { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! } - ` + `; const validateFullSchema = (subgraph: Subgraph) => { // Note: we merge types and extensions to avoid having to care whether the @link are on a schema definition or schema extension // as 1) this will vary (we add them to extensions in our test, but when auto-added, they are added to the schema definition) // and 2) it doesn't matter in practice, it's valid in all cases. - expect(printSchema(subgraph.schema, { ...defaultPrintOptions, mergeTypesAndExtensions: true})).toMatchString(expectedFullSchema); - } + expect( + printSchema(subgraph.schema, { + ...defaultPrintOptions, + mergeTypesAndExtensions: true, + }), + ).toMatchString(expectedFullSchema); + }; it('expands everything if only the federation spec is linked', () => { const doc = gql` @@ -703,13 +850,19 @@ describe('@core/@link handling', () => { gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! } - directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + directive @key( + fields: federation__FieldSet! + resolvable: Boolean = true + ) repeatable on OBJECT | INTERFACE scalar federation__FieldSet @@ -718,7 +871,10 @@ describe('@core/@link handling', () => { gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! @@ -728,7 +884,10 @@ describe('@core/@link handling', () => { `, gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! @@ -738,17 +897,21 @@ describe('@core/@link handling', () => { `, gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! } - directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION + directive @federation__external( + reason: String + ) on OBJECT | FIELD_DEFINITION `, gql` - extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0") + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0") type T { k: ID! @@ -775,13 +938,18 @@ describe('@core/@link handling', () => { gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! } - directive @key(fields: federation__FieldSet!) repeatable on OBJECT | INTERFACE + directive @key( + fields: federation__FieldSet! + ) repeatable on OBJECT | INTERFACE scalar federation__FieldSet `, @@ -789,7 +957,10 @@ describe('@core/@link handling', () => { gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@inaccessible"] + ) type T { k: ID! @inaccessible @@ -800,26 +971,38 @@ describe('@core/@link handling', () => { // @key is repeatable, but you're welcome to restrict yourself to never repeating it. gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! } - directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) on OBJECT | INTERFACE + directive @key( + fields: federation__FieldSet! + resolvable: Boolean = true + ) on OBJECT | INTERFACE scalar federation__FieldSet `, // @key `resolvable` argument is optional, but you're welcome to force users to always provide it. gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k", resolvable: true) { k: ID! } - directive @key(fields: federation__FieldSet!, resolvable: Boolean!) repeatable on OBJECT | INTERFACE + directive @key( + fields: federation__FieldSet! + resolvable: Boolean! + ) repeatable on OBJECT | INTERFACE scalar federation__FieldSet `, @@ -828,13 +1011,21 @@ describe('@core/@link handling', () => { gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key"] + ) type T @key(fields: "k") { k: ID! } - directive @link(url: String!, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + directive @link( + url: String! + as: String + for: link__Purpose + import: [link__Import] + ) repeatable on SCHEMA scalar link__Import scalar link__Purpose @@ -854,14 +1045,18 @@ describe('@core/@link handling', () => { k: ID! } - directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION | SCHEMA + directive @federation__external( + reason: String + ) on OBJECT | FIELD_DEFINITION | SCHEMA `; // @external is not allowed on 'schema' and likely never will. - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ 'DIRECTIVE_DEFINITION_INVALID', '[S] Invalid definition for directive "@federation__external": "@federation__external" should have locations OBJECT, FIELD_DEFINITION, but found (non-subset) OBJECT, FIELD_DEFINITION, SCHEMA', - ]]); + ], + ]); }); it('errors on invalid non-repeatable directive marked repeateable', () => { @@ -877,10 +1072,12 @@ describe('@core/@link handling', () => { `; // @external is not repeatable (and has no reason to be since it has no arguments). - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'DIRECTIVE_DEFINITION_INVALID', - '[S] Invalid definition for directive "@federation__external": "@federation__external" should not be repeatable', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'DIRECTIVE_DEFINITION_INVALID', + '[S] Invalid definition for directive "@federation__external": "@federation__external" should not be repeatable', + ], + ]); }); it('errors on unknown argument of known directive', () => { @@ -895,10 +1092,12 @@ describe('@core/@link handling', () => { directive @federation__external(foo: Int) on OBJECT | FIELD_DEFINITION `; - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'DIRECTIVE_DEFINITION_INVALID', - '[S] Invalid definition for directive "@federation__external": unknown/unsupported argument "foo"', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'DIRECTIVE_DEFINITION_INVALID', + '[S] Invalid definition for directive "@federation__external": unknown/unsupported argument "foo"', + ], + ]); }); it('errors on invalid type for a known argument', () => { @@ -910,13 +1109,18 @@ describe('@core/@link handling', () => { k: ID! } - directive @key(fields: String!, resolvable: String) repeatable on OBJECT | INTERFACE + directive @key( + fields: String! + resolvable: String + ) repeatable on OBJECT | INTERFACE `; - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'DIRECTIVE_DEFINITION_INVALID', - '[S] Invalid definition for directive "@key": argument "resolvable" should have type "Boolean" but found type "String"', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'DIRECTIVE_DEFINITION_INVALID', + '[S] Invalid definition for directive "@key": argument "resolvable" should have type "Boolean" but found type "String"', + ], + ]); }); it('errors on a required argument defined as optional', () => { @@ -928,21 +1132,25 @@ describe('@core/@link handling', () => { k: ID! } - directive @key(fields: federation__FieldSet, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + directive @key( + fields: federation__FieldSet + resolvable: Boolean = true + ) repeatable on OBJECT | INTERFACE scalar federation__FieldSet `; - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'DIRECTIVE_DEFINITION_INVALID', - '[S] Invalid definition for directive "@key": argument "fields" should have type "federation__FieldSet!" but found type "federation__FieldSet"', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'DIRECTIVE_DEFINITION_INVALID', + '[S] Invalid definition for directive "@key": argument "fields" should have type "federation__FieldSet!" but found type "federation__FieldSet"', + ], + ]); }); it('errors on invalid definition for @link Purpose', () => { const doc = gql` - extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0") + extend schema @link(url: "https://specs.apollo.dev/federation/v2.0") type T { k: ID! @@ -954,10 +1162,12 @@ describe('@core/@link handling', () => { } `; - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'TYPE_DEFINITION_INVALID', - '[S] Invalid definition for type "Purpose": expected values [EXECUTION, SECURITY] but found [EXECUTION, RANDOM].', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'TYPE_DEFINITION_INVALID', + '[S] Invalid definition for type "Purpose": expected values [EXECUTION, SECURITY] but found [EXECUTION, RANDOM].', + ], + ]); }); it('allows any (non-scalar) type in redefinition when expected type is a scalar', () => { @@ -970,7 +1180,10 @@ describe('@core/@link handling', () => { } # 'fields' should be of type 'federation_FieldSet!', but ensure we allow 'String!' alternatively. - directive @key(fields: String!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + directive @key( + fields: String! + resolvable: Boolean = true + ) repeatable on OBJECT | INTERFACE `; // Just making sure this don't error out. @@ -987,18 +1200,21 @@ describe('@core/@link handling', () => { directive @key(fields: String!) on OBJECT `; - // Test for fed2 (with @key being @link-ed) - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] The directive "@key" can only be used once at this location.', - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] The directive "@key" can only be used once at this location.', + ], + ]); // Test for fed1 - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] The directive "@key" can only be used once at this location.', - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] The directive "@key" can only be used once at this location.', + ], + ]); }); }); @@ -1069,12 +1285,14 @@ describe('federation 1 schema', () => { directive @key(fields: _FieldSet!, unknown: Int) on OBJECT | INTERFACE `; - expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([[ - 'DIRECTIVE_DEFINITION_INVALID', - '[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown"' - ]]); + expect(buildForErrors(doc, { asFed2: false })).toStrictEqual([ + [ + 'DIRECTIVE_DEFINITION_INVALID', + '[S] Invalid definition for directive "@key": unknown/unsupported argument "unknown"', + ], + ]); }); -}) +}); describe('@shareable', () => { it('can only be applied to fields of object types', () => { @@ -1084,10 +1302,12 @@ describe('@shareable', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_SHAREABLE_USAGE', - '[S] Invalid use of @shareable on field "I.a": only object type fields can be marked with @shareable' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_SHAREABLE_USAGE', + '[S] Invalid use of @shareable on field "I.a": only object type fields can be marked with @shareable', + ], + ]); }); it('rejects duplicate @shareable on the same definition declaration', () => { @@ -1098,10 +1318,12 @@ describe('@shareable', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_SHAREABLE_USAGE', - '[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_SHAREABLE_USAGE', + '[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration', + ], + ]); }); it('rejects duplicate @shareable on the same extension declaration', () => { @@ -1115,10 +1337,12 @@ describe('@shareable', () => { b: Int } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_SHAREABLE_USAGE', - '[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_SHAREABLE_USAGE', + '[S] Invalid duplicate application of @shareable on the same type declaration of "E": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration', + ], + ]); }); it('rejects duplicate @shareable on a field', () => { @@ -1128,10 +1352,12 @@ describe('@shareable', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_SHAREABLE_USAGE', - '[S] Invalid duplicate application of @shareable on field "E.a": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_SHAREABLE_USAGE', + '[S] Invalid duplicate application of @shareable on field "E.a": @shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration', + ], + ]); }); }); @@ -1162,10 +1388,12 @@ describe('@interfaceObject/@key on interfaces validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INTERFACE_KEY_NOT_ON_IMPLEMENTATION', - '[S] Key @key(fields: "id1") on interface type "I" is missing on implementation types "A" and "C".', - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INTERFACE_KEY_NOT_ON_IMPLEMENTATION', + '[S] Key @key(fields: "id1") on interface type "I" is missing on implementation types "A" and "C".', + ], + ]); }); it('@key on interfaces with @key on some implementation non resolvable', () => { @@ -1190,10 +1418,12 @@ describe('@interfaceObject/@key on interfaces validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INTERFACE_KEY_NOT_ON_IMPLEMENTATION', - '[S] Key @key(fields: "id1") on interface type "I" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in type "C".', - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INTERFACE_KEY_NOT_ON_IMPLEMENTATION', + '[S] Key @key(fields: "id1") on interface type "I" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in type "C".', + ], + ]); }); it('ensures order of fields in key does not matter', () => { @@ -1246,9 +1476,11 @@ describe('@interfaceObject/@key on interfaces validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INTERFACE_OBJECT_USAGE_ERROR', - '[S] The @interfaceObject directive can only be applied to entity types but type "B" has no @key in this subgraph.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INTERFACE_OBJECT_USAGE_ERROR', + '[S] The @interfaceObject directive can only be applied to entity types but type "B" has no @key in this subgraph.', + ], + ]); }); }); diff --git a/internals-js/src/__tests__/toAPISchema.test.ts b/internals-js/src/__tests__/toAPISchema.test.ts index b25173aa5..d1e4a50d0 100644 --- a/internals-js/src/__tests__/toAPISchema.test.ts +++ b/internals-js/src/__tests__/toAPISchema.test.ts @@ -15,17 +15,16 @@ describe('toAPISchema', () => { let schema: Schema; beforeAll(() => { - const schemaPath = path.join( - __dirname, - 'supergraphSdl.graphql', - ); + const schemaPath = path.join(__dirname, 'supergraphSdl.graphql'); const supergraphSdl = fs.readFileSync(schemaPath, 'utf8'); schema = buildSchema(supergraphSdl).toAPISchema(); }); it(`doesn't include core directives`, () => { - expect(directiveNames(schema)).toEqual(expect.not.arrayContaining(['core'])); + expect(directiveNames(schema)).toEqual( + expect.not.arrayContaining(['core']), + ); }); it(`doesn't include join directives`, () => { @@ -47,7 +46,7 @@ describe('toAPISchema', () => { it(`does pass through other custom directives`, () => { expect(directiveNames(schema)).toEqual( - expect.arrayContaining([ 'transform', 'stream' ]), + expect.arrayContaining(['transform', 'stream']), ); }); }); diff --git a/internals-js/src/__tests__/utils.test.ts b/internals-js/src/__tests__/utils.test.ts index 227da95b2..ba78630bf 100644 --- a/internals-js/src/__tests__/utils.test.ts +++ b/internals-js/src/__tests__/utils.test.ts @@ -27,8 +27,18 @@ describe('OrderedMap', () => { orderedMap.add('nine', 9); // keys are in alphabetical order - expect(orderedMap.keys()).toEqual(['eight', 'five', 'four', 'nine', 'one', 'seven', 'six', 'three', 'two']); - const sortedArr = [8,5,4,9,1,7,6,3,2]; + expect(orderedMap.keys()).toEqual([ + 'eight', + 'five', + 'four', + 'nine', + 'one', + 'seven', + 'six', + 'three', + 'two', + ]); + const sortedArr = [8, 5, 4, 9, 1, 7, 6, 3, 2]; expect(orderedMap.values()).toEqual(sortedArr); // test using spread operator to make sure iterator is performing correctly @@ -50,21 +60,23 @@ describe('OrderedMap', () => { }); it('sort by string length', () => { - const orderedMap = new OrderedMap((a: string, b: string) => { - if (a.length < b.length) { - return -1; - } else if (b.length < a.length) { - return 1; - } - return 0; - }); + const orderedMap = new OrderedMap( + (a: string, b: string) => { + if (a.length < b.length) { + return -1; + } else if (b.length < a.length) { + return 1; + } + return 0; + }, + ); orderedMap.add('eight', 8); orderedMap.add('seventy', 70); orderedMap.add('six', 6); orderedMap.add('four', 4); expect(orderedMap.keys()).toEqual(['six', 'four', 'eight', 'seventy']); - const sortedArr = [6,4,8,70]; + const sortedArr = [6, 4, 8, 70]; expect(orderedMap.values()).toEqual(sortedArr); // test using spread operator to make sure iterator is performing correctly @@ -83,8 +95,8 @@ describe('OrderedMap', () => { orderedMap.add(5, 50); orderedMap.add(8, 80); - const keys = [1,2,3,4,5,6,7,8,9]; - const values = [10,20,30,40,50,60,70,80,90]; + const keys = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + const values = [10, 20, 30, 40, 50, 60, 70, 80, 90]; expect(orderedMap.keys()).toEqual(keys); expect(orderedMap.values()).toEqual(values); expect([...orderedMap]).toEqual(values); diff --git a/internals-js/src/__tests__/values.test.ts b/internals-js/src/__tests__/values.test.ts index 021fcddb9..eaab39aa7 100644 --- a/internals-js/src/__tests__/values.test.ts +++ b/internals-js/src/__tests__/values.test.ts @@ -1,6 +1,4 @@ -import { - Schema, -} from '../definitions'; +import { Schema } from '../definitions'; import { buildSchema } from '../buildSchema'; import { parseOperation } from '../operations'; import gql from 'graphql-tag'; @@ -27,11 +25,14 @@ test('handles non-list value for list argument (as singleton)', () => { } `); - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query { f(v: MONDAY) } - `); + `, + ); expect(operation.toString(false, false)).toBe('{ f(v: [MONDAY]) }'); expect(operation.selectionSet.toSelectionSetNode()).toMatchInlineSnapshot(` @@ -79,10 +80,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: "foo") provided for argument Query.f(a:) of type Int.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: "foo") provided for argument Query.f(a:) of type Int.', + ], + ]); }); it('errors on invalid default value in directive argument', () => { @@ -94,10 +97,12 @@ describe('default value validation', () => { directive @myDirective(a: Int = "foo") on FIELD `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: "foo") provided for argument @myDirective(a:) of type Int.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: "foo") provided for argument @myDirective(a:) of type Int.', + ], + ]); }); it('errors on invalid default value in input field', () => { @@ -107,10 +112,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: "foo") provided for input field I.x of type Int.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: "foo") provided for input field I.x of type Int.', + ], + ]); }); it('errors on invalid default value for existing input field', () => { @@ -125,10 +132,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: {x: 2, y: "3"}) provided for argument Query.f(i:) of type I.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: {x: 2, y: "3"}) provided for argument Query.f(i:) of type I.', + ], + ]); }); it('errors on default value containing unexpected input fields', () => { @@ -143,10 +152,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: {x: 1, y: 2, z: 3}) provided for argument Query.f(i:) of type I.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: {x: 1, y: 2, z: 3}) provided for argument Query.f(i:) of type I.', + ], + ]); }); it('errors on default value being unknown enum value', () => { @@ -168,10 +179,12 @@ describe('default value validation', () => { // it in the error message). We could fix this someday if we change to using a specific class/object for // enum values internally (though this might have backward compatbility constraints), but in the meantime, // it's unlikely to trip users too much. - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: "THREE") provided for argument Query.f(e:) of type E.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: "THREE") provided for argument Query.f(e:) of type E.', + ], + ]); }); it('errors on default value being unknown enum value (as string)', () => { @@ -186,10 +199,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: "TWOO") provided for argument Query.f(e:) of type E.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: "TWOO") provided for argument Query.f(e:) of type E.', + ], + ]); }); it('accepts default value enum value as string, if a valid enum value', () => { @@ -242,7 +257,7 @@ describe('default value validation', () => { it('accepts any value for a custom scalar in an input field', () => { const doc = gql` input I { - x: Scalar = { z: { a: 4} } + x: Scalar = { z: { a: 4 } } } scalar Scalar @@ -278,16 +293,18 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: 2) provided for argument Query.f(x:) of type [[[String]!]]!.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: 2) provided for argument Query.f(x:) of type [[[String]!]]!.', + ], + ]); }); it('accepts default value coercible to its type but needing multiple/nested coercions', () => { const doc = gql` type Query { - f(x: I = { j: {x: 1, z: "Foo"} }): Int + f(x: I = { j: { x: 1, z: "Foo" } }): Int } input I { @@ -333,10 +350,12 @@ describe('default value validation', () => { } `; - expect(buildForErrors(doc)).toStrictEqual([[ - 'INVALID_GRAPHQL', - '[S] Invalid default value (got: null) provided for argument Query.f(i:) of type Int!.' - ]]); + expect(buildForErrors(doc)).toStrictEqual([ + [ + 'INVALID_GRAPHQL', + '[S] Invalid default value (got: null) provided for argument Query.f(i:) of type Int!.', + ], + ]); }); it('Accepts null default value for nullable input', () => { @@ -361,9 +380,9 @@ describe('values printing', () => { FOO BAR } - ` + `; expect(printSchema(parseSchema(sdl))).toMatchString(sdl); - }) + }); it('prints enums value when its coercible to list through multiple coercions', () => { const sdl = ` @@ -375,16 +394,25 @@ describe('values printing', () => { FOO BAR } - ` + `; expect(printSchema(parseSchema(sdl))).toMatchString(sdl); - }) + }); }); describe('objectEquals tests', () => { it('simple object equality tests', () => { - expect(valueEquals({ foo: 'foo' }, { foo: 'foo'})).toBe(true); - expect(valueEquals({ foo: 'foo', bar: undefined }, { foo: 'foo', bar: undefined})).toBe(true); - expect(valueEquals({ foo: 'foo' }, { foo: 'foo', bar: undefined})).toBe(false); - expect(valueEquals({ foo: 'foo', bar: undefined }, { foo: 'foo' })).toBe(false); + expect(valueEquals({ foo: 'foo' }, { foo: 'foo' })).toBe(true); + expect( + valueEquals( + { foo: 'foo', bar: undefined }, + { foo: 'foo', bar: undefined }, + ), + ).toBe(true); + expect(valueEquals({ foo: 'foo' }, { foo: 'foo', bar: undefined })).toBe( + false, + ); + expect(valueEquals({ foo: 'foo', bar: undefined }, { foo: 'foo' })).toBe( + false, + ); }); }); diff --git a/internals-js/src/specs/__tests__/coreSpec.test.ts b/internals-js/src/specs/__tests__/coreSpec.test.ts index 1a987d58f..e4814fa84 100644 --- a/internals-js/src/specs/__tests__/coreSpec.test.ts +++ b/internals-js/src/specs/__tests__/coreSpec.test.ts @@ -1,10 +1,16 @@ -import { DocumentNode, GraphQLError } from "graphql"; -import gql from "graphql-tag"; -import { buildSubgraph } from "../../federation"; -import { assert } from "../../utils"; -import { buildSchemaFromAST } from "../../buildSchema"; -import { removeAllCoreFeatures, FeatureDefinitions, FeatureVersion, FeatureDefinition, FeatureUrl } from "../coreSpec"; -import { errorCauses } from "../../error"; +import { DocumentNode, GraphQLError } from 'graphql'; +import gql from 'graphql-tag'; +import { buildSubgraph } from '../../federation'; +import { assert } from '../../utils'; +import { buildSchemaFromAST } from '../../buildSchema'; +import { + removeAllCoreFeatures, + FeatureDefinitions, + FeatureVersion, + FeatureDefinition, + FeatureUrl, +} from '../coreSpec'; +import { errorCauses } from '../../error'; function expectErrors( subgraphDefs: DocumentNode, @@ -16,7 +22,7 @@ function expectErrors( // Note: we use buildSubgraph because currently it's the only one that does auto-magic import of // directive definition, and we don't want to bother with adding the @link definition to every // example. - buildSubgraph('S', '', subgraphDefs) + buildSubgraph('S', '', subgraphDefs); } catch (e) { // Kind-of ugly, but if Jest has a better option, I haven't found it. thrownError = e; @@ -29,22 +35,25 @@ function expectErrors( assert(causes, 'Should have some causes'); // Note: all the received message with start with "[S] ", so the `slice` below // strips the extra prefix. This avoid leaking the subgraph name to leak to the tests themselves. - expect(causes.map((e) => e.message.slice(4))).toStrictEqual(expectedErrorMessages); + expect(causes.map((e) => e.message.slice(4))).toStrictEqual( + expectedErrorMessages, + ); } describe('@link(import:) argument', () => { test('errors on misformed values', () => { const schema = gql` - extend schema @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: [ - 2, - { foo: "bar" }, - { name: "@key", badName: "foo"}, - { name: 42 }, - { as: "bar" }, - ] - ) + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [ + 2 + { foo: "bar" } + { name: "@key", badName: "foo" } + { name: 42 } + { as: "bar" } + ] + ) type Query { q: Int @@ -62,13 +71,14 @@ describe('@link(import:) argument', () => { test('errors on mismatch between name and alias', () => { const schema = gql` - extend schema @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: [ - { name: "@key", as: "myKey" }, - { name: "FieldSet", as: "@fieldSet" }, - ] - ) + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [ + { name: "@key", as: "myKey" } + { name: "FieldSet", as: "@fieldSet" } + ] + ) type Query { q: Int @@ -83,10 +93,11 @@ describe('@link(import:) argument', () => { test('errors on importing unknown elements for known features', () => { const schema = gql` - extend schema @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: [ "@foo", "key", { name: "@sharable" } ] - ) + extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@foo", "key", { name: "@sharable" }] + ) type Query { q: Int @@ -96,7 +107,7 @@ describe('@link(import:) argument', () => { expectErrors(schema, [ 'Cannot import unknown element "@foo".', 'Cannot import unknown element "key". Did you mean directive "@key"?', - 'Cannot import unknown element "@sharable\". Did you mean "@shareable"?', + 'Cannot import unknown element "@sharable". Did you mean "@shareable"?', ]); }); }); @@ -104,7 +115,12 @@ describe('@link(import:) argument', () => { describe('removeAllCoreFeatures', () => { it('removes core (and only core) feature definitions, accounting for aliasing', () => { const schema = buildSchemaFromAST(gql` - directive @lonk(url: String, as: String, for: Porpoise, import: [lonk__Import]) repeatable on SCHEMA + directive @lonk( + url: String + as: String + for: Porpoise + import: [lonk__Import] + ) repeatable on SCHEMA scalar lonk__Import @@ -122,20 +138,18 @@ describe('removeAllCoreFeatures', () => { extend schema @lonk( - url: "https://specs.apollo.dev/link/v1.0", - as: "lonk", - import: [ - { name: "Purpose", as: "Porpoise" } - ] + url: "https://specs.apollo.dev/link/v1.0" + as: "lonk" + import: [{ name: "Purpose", as: "Porpoise" }] ) @lonk( - url: "https://localhost/foobar/v1.0", + url: "https://localhost/foobar/v1.0" as: "foo" import: [ - "bar", - "@baz", - { name: "qux", as: "qax" }, - { name: "@quz", as: "@qaz" }, + "bar" + "@baz" + { name: "qux", as: "qax" } + { name: "@quz", as: "@qaz" } ] ) @@ -185,29 +199,29 @@ describe('removeAllCoreFeatures', () => { removeAllCoreFeatures(schema); schema.validate(); - expect(schema.elementByCoordinate("@lonk")).toBeUndefined(); - expect(schema.elementByCoordinate("lonk__Import")).toBeUndefined(); - expect(schema.elementByCoordinate("Porpoise")).toBeUndefined(); - expect(schema.elementByCoordinate("foobar")).toBeDefined(); - expect(schema.elementByCoordinate("foobar__Scalar")).toBeDefined(); - expect(schema.elementByCoordinate("@foobar")).toBeDefined(); - expect(schema.elementByCoordinate("@foobar__directive")).toBeDefined(); - expect(schema.elementByCoordinate("foo")).toBeDefined(); - expect(schema.elementByCoordinate("foo__Scalar")).toBeUndefined(); - expect(schema.elementByCoordinate("@foo")).toBeUndefined(); - expect(schema.elementByCoordinate("@foo__directive")).toBeUndefined(); - expect(schema.elementByCoordinate("bar")).toBeUndefined(); - expect(schema.elementByCoordinate("foo__bar")).toBeUndefined(); - expect(schema.elementByCoordinate("@baz")).toBeUndefined(); - expect(schema.elementByCoordinate("@foo__baz")).toBeUndefined(); - expect(schema.elementByCoordinate("qux")).toBeDefined(); - expect(schema.elementByCoordinate("@quz")).toBeDefined(); - expect(schema.elementByCoordinate("qax")).toBeUndefined(); - expect(schema.elementByCoordinate("foo__qax")).toBeUndefined(); - expect(schema.elementByCoordinate("foo__qux")).toBeUndefined(); - expect(schema.elementByCoordinate("@qaz")).toBeUndefined(); - expect(schema.elementByCoordinate("@foo__qaz")).toBeUndefined(); - expect(schema.elementByCoordinate("@foo__quz")).toBeUndefined(); + expect(schema.elementByCoordinate('@lonk')).toBeUndefined(); + expect(schema.elementByCoordinate('lonk__Import')).toBeUndefined(); + expect(schema.elementByCoordinate('Porpoise')).toBeUndefined(); + expect(schema.elementByCoordinate('foobar')).toBeDefined(); + expect(schema.elementByCoordinate('foobar__Scalar')).toBeDefined(); + expect(schema.elementByCoordinate('@foobar')).toBeDefined(); + expect(schema.elementByCoordinate('@foobar__directive')).toBeDefined(); + expect(schema.elementByCoordinate('foo')).toBeDefined(); + expect(schema.elementByCoordinate('foo__Scalar')).toBeUndefined(); + expect(schema.elementByCoordinate('@foo')).toBeUndefined(); + expect(schema.elementByCoordinate('@foo__directive')).toBeUndefined(); + expect(schema.elementByCoordinate('bar')).toBeUndefined(); + expect(schema.elementByCoordinate('foo__bar')).toBeUndefined(); + expect(schema.elementByCoordinate('@baz')).toBeUndefined(); + expect(schema.elementByCoordinate('@foo__baz')).toBeUndefined(); + expect(schema.elementByCoordinate('qux')).toBeDefined(); + expect(schema.elementByCoordinate('@quz')).toBeDefined(); + expect(schema.elementByCoordinate('qax')).toBeUndefined(); + expect(schema.elementByCoordinate('foo__qax')).toBeUndefined(); + expect(schema.elementByCoordinate('foo__qux')).toBeUndefined(); + expect(schema.elementByCoordinate('@qaz')).toBeUndefined(); + expect(schema.elementByCoordinate('@foo__qaz')).toBeUndefined(); + expect(schema.elementByCoordinate('@foo__quz')).toBeUndefined(); }); }); @@ -229,12 +243,7 @@ describe('FeatureVersion', () => { // version.toString() strings, but do not perform numeric lexicographic // comparison of the major and minor numbers, so 'v10...' < 'v2...' and the // following comparisons produce unintuitive results. - expect([ - v2_3 < v10_0, - v2_3 <= v10_0, - v2_3 > v10_0, - v2_3 >= v10_0, - ]).toEqual( + expect([v2_3 < v10_0, v2_3 <= v10_0, v2_3 > v10_0, v2_3 >= v10_0]).toEqual( // This should really be [true, true, false, false], if JavaScript // supported more flexible/general operator overloading. [false, false, true, true], @@ -266,25 +275,75 @@ describe('getMinimumRequiredVersion tests', () => { it('various combinations', () => { const versions = new FeatureDefinitions('test') .add(new TestFeatureDefinition(new FeatureVersion(0, 1))) - .add(new TestFeatureDefinition(new FeatureVersion(0, 2), new FeatureVersion(1, 0))) - .add(new TestFeatureDefinition(new FeatureVersion(0, 3), new FeatureVersion(2,0))) - .add(new TestFeatureDefinition(new FeatureVersion(0, 4), new FeatureVersion(2,1))) - .add(new TestFeatureDefinition(new FeatureVersion(0, 5), new FeatureVersion(2,2))); - - expect(versions.getMinimumRequiredVersion(new FeatureVersion(0, 1)).version).toEqual(new FeatureVersion(0, 1)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(1, 0)).version).toEqual(new FeatureVersion(0, 2)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(1, 1)).version).toEqual(new FeatureVersion(0, 2)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 0)).version).toEqual(new FeatureVersion(0, 3)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 1)).version).toEqual(new FeatureVersion(0, 4)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 2)).version).toEqual(new FeatureVersion(0, 5)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 3)).version).toEqual(new FeatureVersion(0, 5)); + .add( + new TestFeatureDefinition( + new FeatureVersion(0, 2), + new FeatureVersion(1, 0), + ), + ) + .add( + new TestFeatureDefinition( + new FeatureVersion(0, 3), + new FeatureVersion(2, 0), + ), + ) + .add( + new TestFeatureDefinition( + new FeatureVersion(0, 4), + new FeatureVersion(2, 1), + ), + ) + .add( + new TestFeatureDefinition( + new FeatureVersion(0, 5), + new FeatureVersion(2, 2), + ), + ); + + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(0, 1)).version, + ).toEqual(new FeatureVersion(0, 1)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(1, 0)).version, + ).toEqual(new FeatureVersion(0, 2)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(1, 1)).version, + ).toEqual(new FeatureVersion(0, 2)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 0)).version, + ).toEqual(new FeatureVersion(0, 3)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 1)).version, + ).toEqual(new FeatureVersion(0, 4)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 2)).version, + ).toEqual(new FeatureVersion(0, 5)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 3)).version, + ).toEqual(new FeatureVersion(0, 5)); // now add a new major version and test again. All previous version should be forced to the new major - versions.add(new TestFeatureDefinition(new FeatureVersion(1, 0), new FeatureVersion(2, 4))); - versions.add(new TestFeatureDefinition(new FeatureVersion(1, 1), new FeatureVersion(2, 5))); - - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 3)).version).toEqual(new FeatureVersion(1, 0)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 4)).version).toEqual(new FeatureVersion(1, 0)); - expect(versions.getMinimumRequiredVersion(new FeatureVersion(2, 5)).version).toEqual(new FeatureVersion(1, 1)); - }) -}) + versions.add( + new TestFeatureDefinition( + new FeatureVersion(1, 0), + new FeatureVersion(2, 4), + ), + ); + versions.add( + new TestFeatureDefinition( + new FeatureVersion(1, 1), + new FeatureVersion(2, 5), + ), + ); + + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 3)).version, + ).toEqual(new FeatureVersion(1, 0)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 4)).version, + ).toEqual(new FeatureVersion(1, 0)); + expect( + versions.getMinimumRequiredVersion(new FeatureVersion(2, 5)).version, + ).toEqual(new FeatureVersion(1, 1)); + }); +}); From 6cf28f266e6ceb03f01e6b709e667e9416964bab Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 26 Jun 2024 10:28:13 -0600 Subject: [PATCH 03/11] Split out satisfiability from `composeServices` (#3049) Create a `validateSatisfiability` function separate from `composeServces`. Allow fow disabling satisfiability when running `composeServices`. This is part of #3046, split out for easier review. --------- Co-authored-by: Taylor Jones <45475656+tayrrible@users.noreply.github.com> Co-authored-by: Taylor Jones --- composition-js/src/compose.ts | 112 ++++++++++++++++++++++++---------- 1 file changed, 81 insertions(+), 31 deletions(-) diff --git a/composition-js/src/compose.ts b/composition-js/src/compose.ts index 4c75e701e..8b17c4aea 100644 --- a/composition-js/src/compose.ts +++ b/composition-js/src/compose.ts @@ -14,7 +14,7 @@ import { } from "@apollo/federation-internals"; import { GraphQLError } from "graphql"; import { buildFederatedQueryGraph, buildSupergraphAPIQueryGraph } from "@apollo/query-graphs"; -import { mergeSubgraphs } from "./merging"; +import { MergeResult, mergeSubgraphs } from "./merging"; import { validateGraphComposition } from "./validate"; import { CompositionHint } from "./hints"; @@ -37,6 +37,8 @@ export interface CompositionSuccess { export interface CompositionOptions { sdlPrintOptions?: PrintOptions; allowedFieldTypeMergingSubtypingRules?: SubtypingRule[]; + /// Flag to toggle if satisfiability should be performed during composition + runSatisfiability?: boolean; } function validateCompositionOptions(options: CompositionOptions) { @@ -45,59 +47,58 @@ function validateCompositionOptions(options: CompositionOptions) { assert(!options?.allowedFieldTypeMergingSubtypingRules?.includes("list_upgrade"), "The `list_upgrade` field subtyping rule is currently not supported"); } +/** + * Used to compose a supergraph from subgraphs + * `options.runSatisfiability` will default to `true` + * + * @param subgraphs Subgraphs + * @param options CompositionOptions + */ export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}): CompositionResult { - validateCompositionOptions(options); - - const upgradeResult = upgradeSubgraphsIfNecessary(subgraphs); - if (upgradeResult.errors) { - return { errors: upgradeResult.errors }; - } + const { runSatisfiability = true, sdlPrintOptions } = options; - const toMerge = upgradeResult.subgraphs; - const validationErrors = toMerge.validate(); - if (validationErrors) { - return { errors: validationErrors }; - } + validateCompositionOptions(options); - const mergeResult = mergeSubgraphs(toMerge); + const mergeResult = validateSubgraphsAndMerge(subgraphs); if (mergeResult.errors) { return { errors: mergeResult.errors }; } - // We pass `null` for the `supportedFeatures` to disable the feature support validation. Validating feature support - // is useful when executing/handling a supergraph, but here we're just validating the supergraph we've just created, - // and there is no reason to error due to an unsupported feature. - const supergraph = new Supergraph(mergeResult.supergraph, null); - const supergraphQueryGraph = buildSupergraphAPIQueryGraph(supergraph); - const federatedQueryGraph = buildFederatedQueryGraph(supergraph, false); - const { errors, hints } = validateGraphComposition( - supergraph.schema, - supergraph.subgraphNameToGraphEnumValue(), - supergraphQueryGraph, - federatedQueryGraph - ); - if (errors) { - return { errors }; + let satisfiabilityResult; + if (runSatisfiability) { + satisfiabilityResult = validateSatisfiability({ + supergraphSchema: mergeResult.supergraph + }); + if (satisfiabilityResult.errors) { + return { errors: satisfiabilityResult.errors }; + } } // printSchema calls validateOptions, which can throw let supergraphSdl; try { supergraphSdl = printSchema( - supergraph.schema, - options.sdlPrintOptions ?? shallowOrderPrintedDefinitions(defaultPrintOptions), + mergeResult.supergraph, + sdlPrintOptions ?? shallowOrderPrintedDefinitions(defaultPrintOptions), ); } catch (err) { return { errors: [err] }; } return { - schema: supergraph.schema, + schema: mergeResult.supergraph, supergraphSdl, - hints: mergeResult.hints.concat(hints ?? []), + hints: [...mergeResult.hints, ...(satisfiabilityResult?.hints ?? [])], }; } +/** + * Method to validate and compose services + * + * @param services List of Service definitions + * @param options CompositionOptions + * @returns CompositionResult + */ export function composeServices(services: ServiceDefinition[], options: CompositionOptions = {}): CompositionResult { const subgraphs = subgraphsFromServiceList(services); if (Array.isArray(subgraphs)) { @@ -106,5 +107,54 @@ export function composeServices(services: ServiceDefinition[], options: Composit // include the subgraph name in their message. return { errors: subgraphs }; } + return compose(subgraphs, options); } + +type SatisfiabilityArgs = { + supergraphSchema: Schema + supergraphSdl?: never +} | { supergraphSdl: string, supergraphSchema?: never }; + +/** + * Run satisfiability check for a supergraph + * + * Can pass either the supergraph's Schema or SDL to validate + * @param args: SatisfiabilityArgs + * @returns { errors? : GraphQLError[], hints? : CompositionHint[] } + */ +export function validateSatisfiability({ supergraphSchema, supergraphSdl} : SatisfiabilityArgs) : { + errors? : GraphQLError[], + hints? : CompositionHint[], +} { + // We pass `null` for the `supportedFeatures` to disable the feature support validation. Validating feature support + // is useful when executing/handling a supergraph, but here we're just validating the supergraph we've just created, + // and there is no reason to error due to an unsupported feature. + const supergraph = supergraphSchema ? new Supergraph(supergraphSchema, null) : Supergraph.build(supergraphSdl); + const supergraphQueryGraph = buildSupergraphAPIQueryGraph(supergraph); + const federatedQueryGraph = buildFederatedQueryGraph(supergraph, false); + return validateGraphComposition(supergraph.schema, supergraph.subgraphNameToGraphEnumValue(), supergraphQueryGraph, federatedQueryGraph); +} + +type ValidateSubgraphsAndMergeResult = MergeResult | { errors: GraphQLError[] }; + +/** + * Upgrade subgraphs if necessary, then validates subgraphs before attempting to merge + * + * @param subgraphs + * @returns ValidateSubgraphsAndMergeResult + */ +function validateSubgraphsAndMerge(subgraphs: Subgraphs) : ValidateSubgraphsAndMergeResult { + const upgradeResult = upgradeSubgraphsIfNecessary(subgraphs); + if (upgradeResult.errors) { + return { errors: upgradeResult.errors }; + } + + const toMerge = upgradeResult.subgraphs; + const validationErrors = toMerge.validate(); + if (validationErrors) { + return { errors: validationErrors }; + } + + return mergeSubgraphs(toMerge); +} From 0ccfd937d4b4a576f890665ceebbd7986fac5d0c Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Mon, 22 Jul 2024 12:45:05 -0500 Subject: [PATCH 04/11] Implement @cost and @listSize directives for demand control (#3074) # Overview Implements two new directives for demand control, based on the [IBM Cost Specification](https://ibm.github.io/graphql-specs/cost-spec.html). ``` directive @cost(weight: Int!) on | ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR directive @listSize( assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true ) on FIELD_DEFINITION ``` The `@cost` directive allows users to specify a custom weight for fields, enums, input objects, and arguments. The weight is used in the demand control cost calculation, both for static estimates as well as actual cost calculations. The `@listSize` directive allows users to specify expected sizes of list fields in their schema. This can be a static value set through `assumedSize` or a dynamic value using `slicingArguments` to get the value from some paging parameters. ## Differences from the spec The main difference from the IBM spec is that we use an `Int!` for weight argument of `@cost`. This allows the parser to enforce this is parameterized with proper numeric values instead of finding out at runtime that an invalid `String!` weight was passed. ## Caveats for shared fields When `@cost` or `@listSize` are used on a `@shareable` field with different values, the composed directive will use a merged value that takes the maximum weight or assumed size, when applicable. --- .changeset/happy-bats-exist.md | 6 + .../compose.composeDirective.test.ts.snap | 2 +- .../compose.composeDirective.test.ts | 6 +- .../__tests__/compose.demandControl.test.ts | 583 ++++++++++++++++++ ...e.directiveArgumentMergeStrategies.test.ts | 38 +- composition-js/src/composeDirectiveManager.ts | 1 + composition-js/src/merging/merge.ts | 4 +- .../__tests__/gateway/lifecycle-hooks.test.ts | 2 +- .../src/argumentCompositionStrategies.ts | 37 ++ .../src/extractSubgraphsFromSupergraph.ts | 63 +- internals-js/src/federation.ts | 23 +- internals-js/src/index.ts | 1 + internals-js/src/specs/coreSpec.ts | 21 + internals-js/src/specs/costSpec.ts | 60 ++ internals-js/src/specs/federationSpec.ts | 10 +- internals-js/src/supergraphs.ts | 1 + 16 files changed, 841 insertions(+), 17 deletions(-) create mode 100644 .changeset/happy-bats-exist.md create mode 100644 composition-js/src/__tests__/compose.demandControl.test.ts create mode 100644 internals-js/src/specs/costSpec.ts diff --git a/.changeset/happy-bats-exist.md b/.changeset/happy-bats-exist.md new file mode 100644 index 000000000..c81f41a6c --- /dev/null +++ b/.changeset/happy-bats-exist.md @@ -0,0 +1,6 @@ +--- +"@apollo/composition": minor +"@apollo/federation-internals": minor +--- + +Implements two new directives for defining custom costs for demand control. The `@cost` directive allows setting a custom weight to a particular field in the graph, overriding the default cost calculation. The `@listSize` directive gives the cost calculator information about how to estimate the size of lists returned by subgraphs. This can either be a static size or a value derived from input arguments, such as paging parameters. diff --git a/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap b/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap index 6d651f4ec..b99934cd0 100644 --- a/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap +++ b/composition-js/src/__tests__/__snapshots__/compose.composeDirective.test.ts.snap @@ -4,7 +4,7 @@ exports[`composing custom core directives custom tag directive works when federa "schema @link(url: \\"https://specs.apollo.dev/link/v1.0\\") @link(url: \\"https://specs.apollo.dev/join/v0.3\\", for: EXECUTION) - @link(url: \\"https://specs.apollo.dev/tag/v0.3\\", as: \\"mytag\\") + @link(url: \\"https://specs.apollo.dev/tag/v0.3\\", import: [{name: \\"@tag\\", as: \\"@mytag\\"}]) @link(url: \\"https://custom.dev/tag/v1.0\\", import: [\\"@tag\\"]) { query: Query diff --git a/composition-js/src/__tests__/compose.composeDirective.test.ts b/composition-js/src/__tests__/compose.composeDirective.test.ts index e263b57d4..fe1d845d4 100644 --- a/composition-js/src/__tests__/compose.composeDirective.test.ts +++ b/composition-js/src/__tests__/compose.composeDirective.test.ts @@ -796,7 +796,7 @@ describe('composing custom core directives', () => { expect(errors(result)).toStrictEqual([ [ 'DIRECTIVE_COMPOSITION_ERROR', - 'Could not find matching directive definition for argument to @composeDirective "@fooz" in subgraph "subgraphA". Did you mean "@foo"?', + 'Could not find matching directive definition for argument to @composeDirective "@fooz" in subgraph "subgraphA". Did you mean "@foo" or "@cost"?', ] ]); }); @@ -926,8 +926,8 @@ describe('composing custom core directives', () => { expectCoreFeature(schema, 'https://custom.dev/tag', '1.0', [{ name: '@tag' }]); const feature = schema.coreFeatures?.getByIdentity('https://specs.apollo.dev/tag'); expect(feature?.url.toString()).toBe('https://specs.apollo.dev/tag/v0.3'); - expect(feature?.imports).toEqual([]); - expect(feature?.nameInSchema).toEqual('mytag'); + expect(feature?.imports).toEqual([{ name: '@tag', as: '@mytag' }]); + expect(feature?.nameInSchema).toEqual('tag'); expect(printSchema(schema)).toMatchSnapshot(); }); diff --git a/composition-js/src/__tests__/compose.demandControl.test.ts b/composition-js/src/__tests__/compose.demandControl.test.ts new file mode 100644 index 000000000..31a11fc26 --- /dev/null +++ b/composition-js/src/__tests__/compose.demandControl.test.ts @@ -0,0 +1,583 @@ +import { + ArgumentDefinition, + asFed2SubgraphDocument, + EnumType, + FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS, + FieldDefinition, + InputObjectType, + ObjectType, + ServiceDefinition, + Supergraph +} from '@apollo/federation-internals'; +import { composeServices, CompositionResult } from '../compose'; +import gql from 'graphql-tag'; +import { assertCompositionSuccess, errors } from "./testHelper"; + +const subgraphWithCost = { + name: 'subgraphWithCost', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + enum AorB @cost(weight: 15) { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @cost(weight: 20) + } + + type Query { + fieldWithCost: Int @cost(weight: 5) + argWithCost(arg: Int @cost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `), +}; + +const subgraphWithListSize = { + name: 'subgraphWithListSize', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + fieldWithListSize: [String!] @listSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `), +}; + +const subgraphWithRenamedCost = { + name: 'subgraphWithCost', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }]) + + enum AorB @renamedCost(weight: 15) { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @renamedCost(weight: 20) + } + + type Query { + fieldWithCost: Int @renamedCost(weight: 5) + argWithCost(arg: Int @renamedCost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `), +}; + +const subgraphWithRenamedListSize = { + name: 'subgraphWithListSize', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@listSize", as: "@renamedListSize" }]) + + type Query { + fieldWithListSize: [String!] @renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `), +}; + +const subgraphWithCostFromFederationSpec = { + name: 'subgraphWithCost', + typeDefs: asFed2SubgraphDocument( + gql` + enum AorB @cost(weight: 15) { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @cost(weight: 20) + } + + type Query { + fieldWithCost: Int @cost(weight: 5) + argWithCost(arg: Int @cost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `, + { includeAllImports: true }, + ), +}; + +const subgraphWithListSizeFromFederationSpec = { + name: 'subgraphWithListSize', + typeDefs: asFed2SubgraphDocument( + gql` + type Query { + fieldWithListSize: [String!] @listSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `, + { includeAllImports: true }, + ), +}; + +const subgraphWithRenamedCostFromFederationSpec = { + name: 'subgraphWithCost', + typeDefs: + gql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: [{ name: "@cost", as: "@renamedCost" }]) + + enum AorB @renamedCost(weight: 15) { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @renamedCost(weight: 20) + } + + type Query { + fieldWithCost: Int @renamedCost(weight: 5) + argWithCost(arg: Int @renamedCost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `, +}; + +const subgraphWithRenamedListSizeFromFederationSpec = { + name: 'subgraphWithListSize', + typeDefs: + gql` + extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: [{ name: "@listSize", as: "@renamedListSize" }]) + + type Query { + fieldWithListSize: [String!] @renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `, +}; + +// Used to test @cost applications on FIELD_DEFINITION +function fieldWithCost(result: CompositionResult): FieldDefinition | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('fieldWithCost'); +} + +// Used to test @cost applications on ARGUMENT_DEFINITION +function argumentWithCost(result: CompositionResult): ArgumentDefinition> | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('argWithCost') + ?.argument('arg'); +} + +// Used to test @cost applications on ENUM +function enumWithCost(result: CompositionResult): EnumType | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('enumWithCost') + ?.type as EnumType; +} + +// Used to test @cost applications on INPUT_FIELD_DEFINITION +function inputWithCost(result: CompositionResult): InputObjectType | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('inputWithCost') + ?.argument('someInput') + ?.type as InputObjectType; +} + +// Used to test @listSize applications on FIELD_DEFINITION +function fieldWithListSize(result: CompositionResult): FieldDefinition | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('fieldWithListSize'); +} + +describe('demand control directive composition', () => { + it.each([ + [subgraphWithCost, subgraphWithListSize], + [subgraphWithCostFromFederationSpec, subgraphWithListSizeFromFederationSpec], + ])('propagates @cost and @listSize to the supergraph', (costSubgraph: ServiceDefinition, listSizeSubgraph: ServiceDefinition) => { + const result = composeServices([costSubgraph, listSizeSubgraph]); + assertCompositionSuccess(result); + expect(result.hints).toEqual([]); + + const costDirectiveApplications = fieldWithCost(result)?.appliedDirectivesOf('cost'); + expect(costDirectiveApplications?.toString()).toMatchString(`@cost(weight: 5)`); + + const argCostDirectiveApplications = argumentWithCost(result)?.appliedDirectivesOf('cost'); + expect(argCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 10)`); + + const enumCostDirectiveApplications = enumWithCost(result)?.appliedDirectivesOf('cost'); + expect(enumCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 15)`); + + const inputCostDirectiveApplications = inputWithCost(result)?.field('somethingWithCost')?.appliedDirectivesOf('cost'); + expect(inputCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 20)`); + + const listSizeDirectiveApplications = fieldWithListSize(result)?.appliedDirectivesOf('listSize'); + expect(listSizeDirectiveApplications?.toString()).toMatchString(`@listSize(assumedSize: 2000, requireOneSlicingArgument: false)`); + }); + + describe('when renamed', () => { + it.each([ + [subgraphWithRenamedCost, subgraphWithRenamedListSize], + [subgraphWithRenamedCostFromFederationSpec, subgraphWithRenamedListSizeFromFederationSpec] + ])('propagates the renamed @cost and @listSize to the supergraph', (costSubgraph: ServiceDefinition, listSizeSubgraph: ServiceDefinition) => { + const result = composeServices([costSubgraph, listSizeSubgraph]); + assertCompositionSuccess(result); + expect(result.hints).toEqual([]); + + // Ensure the new directive names are specified in the supergraph so we can use them during extraction + const links = result.schema.schemaDefinition.appliedDirectivesOf("link"); + const costLinks = links.filter((link) => link.arguments().url === "https://specs.apollo.dev/cost/v0.1"); + expect(costLinks.length).toBe(1); + expect(costLinks[0].toString()).toEqual(`@link(url: "https://specs.apollo.dev/cost/v0.1", import: [{name: "@cost", as: "@renamedCost"}, {name: "@listSize", as: "@renamedListSize"}])`); + + // Ensure the directives are applied to the expected fields with the new names + const costDirectiveApplications = fieldWithCost(result)?.appliedDirectivesOf('renamedCost'); + expect(costDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 5)`); + + const argCostDirectiveApplications = argumentWithCost(result)?.appliedDirectivesOf('renamedCost'); + expect(argCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 10)`); + + const enumCostDirectiveApplications = enumWithCost(result)?.appliedDirectivesOf('renamedCost'); + expect(enumCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 15)`); + + const listSizeDirectiveApplications = fieldWithListSize(result)?.appliedDirectivesOf('renamedListSize'); + expect(listSizeDirectiveApplications?.toString()).toMatchString(`@renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false)`); + }); + }); + + describe('when renamed in one subgraph but not the other', () => { + it('does not compose', () => { + const subgraphWithDefaultName = { + name: 'subgraphWithDefaultName', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + field1: Int @cost(weight: 5) + } + `), + }; + const subgraphWithDifferentName = { + name: 'subgraphWithDifferentName', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }]) + + type Query { + field2: Int @renamedCost(weight: 10) + } + `), + }; + + const result = composeServices([subgraphWithDefaultName, subgraphWithDifferentName]); + expect(errors(result)).toEqual([ + [ + "LINK_IMPORT_NAME_MISMATCH", + `The "@cost" directive (from https://specs.apollo.dev/cost/v0.1) is imported with mismatched name between subgraphs: it is imported as "@renamedCost" in subgraph "subgraphWithDifferentName" but "@cost" in subgraph "subgraphWithDefaultName"` + ] + ]); + }); + }); + + describe('when used on @shareable fields', () => { + it('hints about merged @cost arguments', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + sharedWithCost: Int @shareable @cost(weight: 5) + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + sharedWithCost: Int @shareable @cost(weight: 10) + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + expect(result.hints).toMatchInlineSnapshot(` + Array [ + CompositionHint { + "coordinate": undefined, + "definition": Object { + "code": "MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS", + "description": "A non-repeatable directive has been applied to a schema element in different subgraphs with different arguments and the arguments values were merged using the directive configured strategies.", + "level": Object { + "name": "INFO", + "value": 40, + }, + }, + "element": undefined, + "message": "Directive @cost is applied to \\"Query.sharedWithCost\\" in multiple subgraphs with different arguments. Merging strategies used by arguments: { \\"weight\\": MAX }", + "nodes": undefined, + }, + ] + `); + }); + + it('hints about merged @listSize arguments', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sharedWithListSize: [Int] @shareable @listSize(assumedSize: 10) + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sharedWithListSize: [Int] @shareable @listSize(assumedSize: 20) + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + expect(result.hints).toMatchInlineSnapshot(` + Array [ + CompositionHint { + "coordinate": undefined, + "definition": Object { + "code": "MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS", + "description": "A non-repeatable directive has been applied to a schema element in different subgraphs with different arguments and the arguments values were merged using the directive configured strategies.", + "level": Object { + "name": "INFO", + "value": 40, + }, + }, + "element": undefined, + "message": "Directive @listSize is applied to \\"Query.sharedWithListSize\\" in multiple subgraphs with different arguments. Merging strategies used by arguments: { \\"assumedSize\\": NULLABLE_MAX, \\"slicingArguments\\": NULLABLE_UNION, \\"sizedFields\\": NULLABLE_UNION, \\"requireOneSlicingArgument\\": NULLABLE_AND }", + "nodes": undefined, + }, + ] + `); + }); + }); +}); + +describe('demand control directive extraction', () => { + it.each([ + subgraphWithCost, + subgraphWithRenamedCost, + subgraphWithCostFromFederationSpec, + subgraphWithRenamedCostFromFederationSpec + ])('extracts @cost from the supergraph', (subgraph: ServiceDefinition) => { + const result = composeServices([subgraph]); + assertCompositionSuccess(result); + const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraphWithCost.name); + + expect(extracted?.toString()).toMatchString(` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + enum AorB + @federation__cost(weight: 15) + { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @federation__cost(weight: 20) + } + + type Query { + fieldWithCost: Int @federation__cost(weight: 5) + argWithCost(arg: Int @federation__cost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `); + }); + + it.each([ + subgraphWithListSize, + subgraphWithRenamedListSize, + subgraphWithListSizeFromFederationSpec, + subgraphWithRenamedListSizeFromFederationSpec + ])('extracts @listSize from the supergraph', (subgraph: ServiceDefinition) => { + const result = composeServices([subgraph]); + assertCompositionSuccess(result); + const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraphWithListSize.name); + + expect(extracted?.toString()).toMatchString(` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + fieldWithListSize: [String!] @federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `); + }); + + it('extracts @listSize with dynamic cost arguments', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sizedList(first: Int!): HasInts @shareable @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) + } + + type HasInts { + ints: [Int!] @shareable + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sizedList(first: Int!): HasInts @shareable @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: false) + } + + type HasInts { + ints: [Int!] @shareable + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + const supergraph = Supergraph.build(result.supergraphSdl); + + const expectedSubgraph = ` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type HasInts { + ints: [Int!] @shareable + } + + type Query { + sizedList(first: Int!): HasInts @shareable @federation__listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: false) + } + `; + expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(expectedSubgraph); + expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(expectedSubgraph); + }); + + describe('when used on @shareable fields', () => { + it('extracts @cost using the max weight across subgraphs', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + sharedWithCost: Int @shareable @cost(weight: 5) + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + + type Query { + sharedWithCost: Int @shareable @cost(weight: 10) + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + const supergraph = Supergraph.build(result.supergraphSdl); + + const expectedSchema = ` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + sharedWithCost: Int @shareable @federation__cost(weight: 10) + } + `; + // Even though different costs went in, the arguments are merged by taking the max weight. + // This means the extracted costs for the shared field have the same weight on the way out. + expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(expectedSchema); + expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(expectedSchema); + }); + + it('extracts @listSize using the max assumed size across subgraphs', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sharedWithListSize: [Int] @shareable @listSize(assumedSize: 10) + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + + type Query { + sharedWithListSize: [Int] @shareable @listSize(assumedSize: 20) + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + const supergraph = Supergraph.build(result.supergraphSdl); + + const expectedSubgraph = ` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + sharedWithListSize: [Int] @shareable @federation__listSize(assumedSize: 20, requireOneSlicingArgument: true) + } + `; + expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(expectedSubgraph); + expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(expectedSubgraph); + }); + }); +}); diff --git a/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts b/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts index 7307a96ba..b1be5c54c 100644 --- a/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts +++ b/composition-js/src/__tests__/compose.directiveArgumentMergeStrategies.test.ts @@ -121,6 +121,42 @@ describe('composition of directive with non-trivial argument strategies', () => resultValues: { t: ['foo', 'bar'], k: ['v1', 'v2'], b: ['x'], }, + }, + { + name: 'nullable_and', + type: (schema: Schema) => schema.booleanType(), + compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_AND, + argValues: { + s1: { t: true, k: true }, + s2: { t: undefined, k: false, b: false }, + }, + resultValues: { + t: true, k: false, b: false, + }, + }, + { + name: 'nullable_max', + type: (schema: Schema) => schema.intType(), + compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_MAX, + argValues: { + s1: { t: 3, k: 1 }, + s2: { t: 2, k: undefined, b: undefined }, + }, + resultValues: { + t: 3, k: 1, b: undefined, + }, + }, + { + name: 'nullable_union', + type: (schema: Schema) => new ListType(new NonNullType(schema.stringType())), + compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_UNION, + argValues: { + s1: { t: ['foo', 'bar'], k: [] }, + s2: { t: ['foo'], k: ['v1', 'v2'], b: ['x'] }, + }, + resultValues: { + t: ['foo', 'bar'], k: ['v1', 'v2'], b: ['x'], + }, }])('works for $name', ({ name, type, compositionStrategy, argValues, resultValues }) => { createTestFeature({ url: 'https://specs.apollo.dev', @@ -183,7 +219,7 @@ describe('composition of directive with non-trivial argument strategies', () => const s = result.schema; expect(directiveStrings(s.schemaDefinition, name)).toStrictEqual([ - `@link(url: "https://specs.apollo.dev/${name}/v0.1")` + `@link(url: "https://specs.apollo.dev/${name}/v0.1", import: ["@${name}"])` ]); const t = s.type('T') as ObjectType; diff --git a/composition-js/src/composeDirectiveManager.ts b/composition-js/src/composeDirectiveManager.ts index e2fe7b170..b4cfd706b 100644 --- a/composition-js/src/composeDirectiveManager.ts +++ b/composition-js/src/composeDirectiveManager.ts @@ -66,6 +66,7 @@ const DISALLOWED_IDENTITIES = [ 'https://specs.apollo.dev/requiresScopes', 'https://specs.apollo.dev/source', 'https://specs.apollo.dev/context', + 'https://specs.apollo.dev/cost', ]; export class ComposeDirectiveManager { diff --git a/composition-js/src/merging/merge.ts b/composition-js/src/merging/merge.ts index 57e9b618a..6d5e06c23 100644 --- a/composition-js/src/merging/merge.ts +++ b/composition-js/src/merging/merge.ts @@ -449,7 +449,7 @@ class Merger { // don't bother adding the spec to the supergraph. if (nameInSupergraph) { const specInSupergraph = compositionSpec.supergraphSpecification(this.latestFedVersionUsed); - const errors = this.linkSpec.applyFeatureToSchema(this.merged, specInSupergraph, nameInSupergraph === specInSupergraph.url.name ? undefined : nameInSupergraph, specInSupergraph.defaultCorePurpose); + const errors = this.linkSpec.applyFeatureAsLink(this.merged, specInSupergraph, specInSupergraph.defaultCorePurpose, [{ name, as: name === nameInSupergraph ? undefined : nameInSupergraph }], ); assert(errors.length === 0, "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"); const feature = this.merged?.coreFeatures?.getByIdentity(specInSupergraph.url.identity); assert(feature, 'Should have found the feature we just added'); @@ -459,7 +459,7 @@ class Merger { throw argumentsMerger; } this.mergedFederationDirectiveNames.add(nameInSupergraph); - this.mergedFederationDirectiveInSupergraph.set(specInSupergraph.url.name, { + this.mergedFederationDirectiveInSupergraph.set(name, { definition: this.merged.directive(nameInSupergraph)!, argumentsMerger, staticArgumentTransform: compositionSpec.staticArgumentTransform, diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 43b65e309..458175876 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -149,7 +149,7 @@ describe('lifecycle hooks', () => { // the supergraph (even just formatting differences), this ID will change // and this test will have to updated. expect(secondCall[0]!.compositionId).toMatchInlineSnapshot( - `"6dc1bde2b9818fabec62208c5d8825abaa1bae89635fa6f3a5ffea7b78fc6d82"`, + `"4aa2278e35df345ff5959a30546d2e9ef9e997204b4ffee4a42344b578b36068"`, ); // second call should have previous info in the second arg expect(secondCall[1]!.compositionId).toEqual(expectedFirstId); diff --git a/internals-js/src/argumentCompositionStrategies.ts b/internals-js/src/argumentCompositionStrategies.ts index 9ceebaa90..85a870773 100644 --- a/internals-js/src/argumentCompositionStrategies.ts +++ b/internals-js/src/argumentCompositionStrategies.ts @@ -59,4 +59,41 @@ export const ARGUMENT_COMPOSITION_STRATEGIES = { return acc.concat(newValues); }, []), }, + NULLABLE_AND: { + name: 'NULLABLE_AND', + isTypeSupported: supportFixedTypes((schema: Schema) => [schema.booleanType()]), + mergeValues: (values: (boolean | null | undefined)[]) => values.reduce((acc, next) => { + if (acc === null || acc === undefined) { + return next; + } else if (next === null || next === undefined) { + return acc; + } else { + return acc && next; + } + }, undefined), + }, + NULLABLE_MAX: { + name: 'NULLABLE_MAX', + isTypeSupported: supportFixedTypes((schema: Schema) => [schema.intType(), new NonNullType(schema.intType())]), + mergeValues: (values: any[]) => values.reduce((a: any, b: any) => a !== undefined && b !== undefined ? Math.max(a, b) : a ?? b, undefined), + }, + NULLABLE_UNION: { + name: 'NULLABLE_UNION', + isTypeSupported: (_: Schema, type: InputType) => ({ valid: isListType(type) }), + mergeValues: (values: any[]) => { + if (values.every((v) => v === undefined)) { + return undefined; + } + + const combined = new Set(); + for (const subgraphValues of values) { + if (Array.isArray(subgraphValues)) { + for (const value of subgraphValues) { + combined.add(value); + } + } + } + return Array.from(combined); + } + } } diff --git a/internals-js/src/extractSubgraphsFromSupergraph.ts b/internals-js/src/extractSubgraphsFromSupergraph.ts index 6ee15899a..1d8a2e4d3 100644 --- a/internals-js/src/extractSubgraphsFromSupergraph.ts +++ b/internals-js/src/extractSubgraphsFromSupergraph.ts @@ -40,7 +40,7 @@ import { parseSelectionSet } from "./operations"; import fs from 'fs'; import path from 'path'; import { validateStringContainsBoolean } from "./utils"; -import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; +import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; function filteredTypes( supergraph: Schema, @@ -224,11 +224,13 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtra } const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition); + const originalDirectiveNames = getOriginalDirectiveNames(supergraph); const args: ExtractArguments = { supergraph, subgraphs, joinSpec, filteredTypes: types, + originalDirectiveNames, getSubgraph, getSubgraphEnumValue, }; @@ -292,6 +294,7 @@ type ExtractArguments = { subgraphs: Subgraphs, joinSpec: JoinSpecDefinition, filteredTypes: NamedType[], + originalDirectiveNames: Record, getSubgraph: (application: Directive) => Subgraph | undefined, getSubgraphEnumValue: (subgraphName: string) => string } @@ -434,6 +437,8 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo 1; for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { - addSubgraphField({ field, type: subgraphType, subgraph, isShareable }); + addSubgraphField({ field, type: subgraphType, subgraph, isShareable, originalDirectiveNames }); } } else { const isShareable = isObjectType(type) @@ -468,15 +473,31 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo { + const originalDirectiveNames: Record = {}; + for (const linkDirective of supergraph.schemaDefinition.appliedDirectivesOf("link")) { + if (linkDirective.arguments().url && linkDirective.arguments().import) { + for (const importedDirective of linkDirective.arguments().import) { + if (importedDirective.name && importedDirective.as) { + originalDirectiveNames[importedDirective.name.replace('@', '')] = importedDirective.as.replace('@', ''); } } } } + + return originalDirectiveNames; } function extractInputObjContent(args: ExtractArguments, info: TypeInfo[]) { const fieldDirective = args.joinSpec.fieldDirective(args.supergraph); + const originalDirectiveNames = args.originalDirectiveNames; for (const { type, subgraphsInfo } of info) { for (const field of type.fields()) { @@ -484,7 +505,7 @@ function extractInputObjContent(args: ExtractArguments, info: TypeInfo[]) { // This was added in join 0.3, so it can genuinely be undefined. const enumValueDirective = args.joinSpec.enumValueDirective(args.supergraph); + const originalDirectiveNames = args.originalDirectiveNames; for (const { type, subgraphsInfo } of info) { + for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { + propagateDemandControlDirectives(type, subgraphType, subgraph, originalDirectiveNames); + } + for (const value of type.values) { const enumValueApplications = enumValueDirective ? value.appliedDirectivesOf(enumValueDirective) : []; if (enumValueApplications.length === 0) { @@ -620,6 +646,20 @@ function maybeDumpSubgraphSchema(subgraph: Subgraph): string { } } +function propagateDemandControlDirectives(source: SchemaElement, dest: SchemaElement, subgraph: Subgraph, originalDirectiveNames?: Record) { + const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST] ?? FederationDirectiveName.COST; + const costDirective = source.appliedDirectivesOf(costDirectiveName).pop(); + if (costDirective) { + dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments()); + } + + const listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE] ?? FederationDirectiveName.LIST_SIZE; + const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop(); + if (listSizeDirective) { + dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments()); + } +} + function errorToString(e: any,): string { const causes = errorCauses(e); return causes ? printErrors(causes) : String(e); @@ -631,12 +671,14 @@ function addSubgraphField({ subgraph, isShareable, joinFieldArgs, + originalDirectiveNames, }: { field: FieldDefinition, type: ObjectType | InterfaceType, subgraph: Subgraph, isShareable: boolean, joinFieldArgs?: JoinFieldDirectiveArguments, + originalDirectiveNames?: Record, }): FieldDefinition { const copiedFieldType = joinFieldArgs?.type ? decodeType(joinFieldArgs.type, subgraph.schema, subgraph.name) @@ -644,7 +686,8 @@ function addSubgraphField({ const subgraphField = type.addField(field.name, copiedFieldType); for (const arg of field.arguments()) { - subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue); + const argDef = subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue); + propagateDemandControlDirectives(arg, argDef, subgraph, originalDirectiveNames) } if (joinFieldArgs?.requires) { subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': joinFieldArgs.requires}); @@ -689,6 +732,9 @@ function addSubgraphField({ if (isShareable && !external && !usedOverridden) { subgraphField.applyDirective(subgraph.metadata().shareableDirective()); } + + propagateDemandControlDirectives(field, subgraphField, subgraph, originalDirectiveNames); + return subgraphField; } @@ -697,11 +743,13 @@ function addSubgraphInputField({ type, subgraph, joinFieldArgs, + originalDirectiveNames, }: { field: InputFieldDefinition, type: InputObjectType, subgraph: Subgraph, joinFieldArgs?: JoinFieldDirectiveArguments, + originalDirectiveNames?: Record }): InputFieldDefinition { const copiedType = joinFieldArgs?.type ? decodeType(joinFieldArgs?.type, subgraph.schema, subgraph.name) @@ -709,6 +757,9 @@ function addSubgraphInputField({ const inputField = type.addField(field.name, copiedType); inputField.defaultValue = field.defaultValue + + propagateDemandControlDirectives(field, inputField, subgraph, originalDirectiveNames); + return inputField; } diff --git a/internals-js/src/federation.ts b/internals-js/src/federation.ts index 5282accba..6980ba593 100644 --- a/internals-js/src/federation.ts +++ b/internals-js/src/federation.ts @@ -100,6 +100,7 @@ import { SourceFieldDirectiveArgs, SourceTypeDirectiveArgs, } from "./specs/sourceSpec"; +import { CostDirectiveArguments, ListSizeDirectiveArguments } from "./specs/costSpec"; const linkSpec = LINK_VERSIONS.latest(); const tagSpec = TAG_VERSIONS.latest(); @@ -1275,6 +1276,14 @@ export class FederationMetadata { return this.getPost20FederationDirective(FederationDirectiveName.CONTEXT); } + costDirective(): Post20FederationDirectiveDefinition { + return this.getPost20FederationDirective(FederationDirectiveName.COST); + } + + listSizeDirective(): Post20FederationDirectiveDefinition { + return this.getPost20FederationDirective(FederationDirectiveName.LIST_SIZE); + } + allFederationDirectives(): DirectiveDefinition[] { const baseDirectives: DirectiveDefinition[] = [ this.keyDirective(), @@ -1338,6 +1347,16 @@ export class FederationMetadata { baseDirectives.push(fromContextDirective); } + const costDirective = this.costDirective(); + if (isFederationDirectiveDefinedInSchema(costDirective)) { + baseDirectives.push(costDirective); + } + + const listSizeDirective = this.listSizeDirective(); + if (isFederationDirectiveDefinedInSchema(listSizeDirective)) { + baseDirectives.push(listSizeDirective); + } + return baseDirectives; } @@ -1831,9 +1850,9 @@ export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = fal // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ... -export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField", "@context", "@fromContext"])'; +export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField", "@context", "@fromContext", "@cost", "@listSize"])'; // This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests. -export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.8", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; +export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; // This is the federation @link for tests that go through the SchemaUpgrader. export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; diff --git a/internals-js/src/index.ts b/internals-js/src/index.ts index 3898ccfd8..9400a73ab 100644 --- a/internals-js/src/index.ts +++ b/internals-js/src/index.ts @@ -25,3 +25,4 @@ export * from './specs/authenticatedSpec'; export * from './specs/requiresScopesSpec'; export * from './specs/policySpec'; export * from './specs/sourceSpec'; +export * from './specs/costSpec'; diff --git a/internals-js/src/specs/coreSpec.ts b/internals-js/src/specs/coreSpec.ts index 8c4b95200..909769ef4 100644 --- a/internals-js/src/specs/coreSpec.ts +++ b/internals-js/src/specs/coreSpec.ts @@ -552,6 +552,27 @@ export class CoreSpecDefinition extends FeatureDefinition { return feature.addElementsToSchema(schema); } + applyFeatureAsLink(schema: Schema, feature: FeatureDefinition, purpose?: CorePurpose, imports?: CoreImport[]): GraphQLError[] { + const existing = schema.schemaDefinition.appliedDirectivesOf(linkDirectiveDefaultName).find((link) => link.arguments().url === feature.toString()); + if (existing) { + existing.remove(); + } + + const coreDirective = this.coreDirective(schema); + const args: LinkDirectiveArgs = { + url: feature.toString(), + import: (existing?.arguments().import ?? []).concat(imports?.map((i) => i.as ? { name: `@${i.name}`, as: `@${i.as}` } : `@${i.name}`)), + feature: undefined, + }; + + if (this.supportPurposes() && purpose) { + args.for = purpose; + } + + schema.schemaDefinition.applyDirective(coreDirective, args); + return feature.addElementsToSchema(schema); + } + extractFeatureUrl(args: CoreOrLinkDirectiveArgs): FeatureUrl { return FeatureUrl.parse(args[this.urlArgName()]!); } diff --git a/internals-js/src/specs/costSpec.ts b/internals-js/src/specs/costSpec.ts new file mode 100644 index 000000000..f6f1bda54 --- /dev/null +++ b/internals-js/src/specs/costSpec.ts @@ -0,0 +1,60 @@ +import { DirectiveLocation } from 'graphql'; +import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; +import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from './coreSpec'; +import { ListType, NonNullType } from '../definitions'; +import { registerKnownFeature } from '../knownCoreFeatures'; +import { ARGUMENT_COMPOSITION_STRATEGIES } from '../argumentCompositionStrategies'; + +export const costIdentity = 'https://specs.apollo.dev/cost'; + +export class CostSpecDefinition extends FeatureDefinition { + constructor(version: FeatureVersion, readonly minimumFederationVersion: FeatureVersion) { + super(new FeatureUrl(costIdentity, 'cost', version), minimumFederationVersion); + + this.registerDirective(createDirectiveSpecification({ + name: 'cost', + locations: [ + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.ENUM, + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.SCALAR + ], + args: [{ name: 'weight', type: (schema) => new NonNullType(schema.intType()), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.MAX }], + composes: true, + repeatable: false, + supergraphSpecification: (fedVersion) => COST_VERSIONS.getMinimumRequiredVersion(fedVersion), + })); + + this.registerDirective(createDirectiveSpecification({ + name: 'listSize', + locations: [DirectiveLocation.FIELD_DEFINITION], + args: [ + { name: 'assumedSize', type: (schema) => schema.intType(), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_MAX }, + { name: 'slicingArguments', type: (schema) => new ListType(new NonNullType(schema.stringType())), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_UNION }, + { name: 'sizedFields', type: (schema) => new ListType(new NonNullType(schema.stringType())), compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_UNION }, + { name: 'requireOneSlicingArgument', type: (schema) => schema.booleanType(), defaultValue: true, compositionStrategy: ARGUMENT_COMPOSITION_STRATEGIES.NULLABLE_AND }, + ], + composes: true, + repeatable: false, + supergraphSpecification: (fedVersion) => COST_VERSIONS.getMinimumRequiredVersion(fedVersion) + })); + } +} + +export const COST_VERSIONS = new FeatureDefinitions(costIdentity) + .add(new CostSpecDefinition(new FeatureVersion(0, 1), new FeatureVersion(2, 9))); + +registerKnownFeature(COST_VERSIONS); + +export interface CostDirectiveArguments { + weight: number; +} + +export interface ListSizeDirectiveArguments { + assumedSize?: number; + slicingArguments?: string[]; + sizedFields?: string[]; + requireOneSlicingArgument?: boolean; +} diff --git a/internals-js/src/specs/federationSpec.ts b/internals-js/src/specs/federationSpec.ts index 16adeb26b..0b8c52542 100644 --- a/internals-js/src/specs/federationSpec.ts +++ b/internals-js/src/specs/federationSpec.ts @@ -20,6 +20,7 @@ import { REQUIRES_SCOPES_VERSIONS } from "./requiresScopesSpec"; import { POLICY_VERSIONS } from './policySpec'; import { SOURCE_VERSIONS } from './sourceSpec'; import { CONTEXT_VERSIONS } from './contextSpec'; +import { COST_VERSIONS } from "./costSpec"; export const federationIdentity = 'https://specs.apollo.dev/federation'; @@ -48,6 +49,8 @@ export enum FederationDirectiveName { SOURCE_FIELD = 'sourceField', CONTEXT = 'context', FROM_CONTEXT = 'fromContext', + COST = 'cost', + LIST_SIZE = 'listSize', } const fieldSetTypeSpec = createScalarTypeSpecification({ name: FederationTypeName.FIELD_SET }); @@ -182,6 +185,10 @@ export class FederationSpecDefinition extends FeatureDefinition { if (version.gte(new FeatureVersion(2, 8))) { this.registerSubFeature(CONTEXT_VERSIONS.find(new FeatureVersion(0, 1))!); } + + if (version.gte(new FeatureVersion(2, 9))) { + this.registerSubFeature(COST_VERSIONS.find(new FeatureVersion(0, 1))!); + } } } @@ -194,6 +201,7 @@ export const FEDERATION_VERSIONS = new FeatureDefinitions Date: Wed, 14 Aug 2024 08:15:48 -0700 Subject: [PATCH 05/11] Support OBJECT and SCALAR locations for @cost directive (#3106) As a follow-up to https://github.com/apollographql/federation/pull/3074, properly support specifying `@cost` on `SCALAR` and `OBJECT` locations. --- .../__tests__/compose.demandControl.test.ts | 172 ++++++++++++------ .../src/__tests__/schemaUpgrader.test.ts | 38 ++-- .../src/extractSubgraphsFromSupergraph.ts | 7 +- 3 files changed, 151 insertions(+), 66 deletions(-) diff --git a/composition-js/src/__tests__/compose.demandControl.test.ts b/composition-js/src/__tests__/compose.demandControl.test.ts index 31a11fc26..62a5a4ebe 100644 --- a/composition-js/src/__tests__/compose.demandControl.test.ts +++ b/composition-js/src/__tests__/compose.demandControl.test.ts @@ -6,6 +6,7 @@ import { FieldDefinition, InputObjectType, ObjectType, + ScalarType, ServiceDefinition, Supergraph } from '@apollo/federation-internals'; @@ -27,11 +28,19 @@ const subgraphWithCost = { somethingWithCost: Int @cost(weight: 20) } + scalar ExpensiveInt @cost(weight: 30) + + type ExpensiveObject @cost(weight: 40) { + id: ID + } + type Query { fieldWithCost: Int @cost(weight: 5) argWithCost(arg: Int @cost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `), }; @@ -41,8 +50,13 @@ const subgraphWithListSize = { typeDefs: asFed2SubgraphDocument(gql` extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) + type HasInts { + ints: [Int!] + } + type Query { fieldWithListSize: [String!] @listSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } `), }; @@ -61,11 +75,19 @@ const subgraphWithRenamedCost = { somethingWithCost: Int @renamedCost(weight: 20) } + scalar ExpensiveInt @renamedCost(weight: 30) + + type ExpensiveObject @renamedCost(weight: 40) { + id: ID + } + type Query { fieldWithCost: Int @renamedCost(weight: 5) argWithCost(arg: Int @renamedCost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `), }; @@ -75,8 +97,13 @@ const subgraphWithRenamedListSize = { typeDefs: asFed2SubgraphDocument(gql` extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@listSize", as: "@renamedListSize" }]) + type HasInts { + ints: [Int!] @shareable + } + type Query { fieldWithListSize: [String!] @renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @renamedListSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } `), }; @@ -94,11 +121,19 @@ const subgraphWithCostFromFederationSpec = { somethingWithCost: Int @cost(weight: 20) } + scalar ExpensiveInt @cost(weight: 30) + + type ExpensiveObject @cost(weight: 40) { + id: ID + } + type Query { fieldWithCost: Int @cost(weight: 5) argWithCost(arg: Int @cost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `, { includeAllImports: true }, @@ -109,8 +144,13 @@ const subgraphWithListSizeFromFederationSpec = { name: 'subgraphWithListSize', typeDefs: asFed2SubgraphDocument( gql` + type HasInts { + ints: [Int!] + } + type Query { fieldWithListSize: [String!] @listSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } `, { includeAllImports: true }, @@ -132,11 +172,19 @@ const subgraphWithRenamedCostFromFederationSpec = { somethingWithCost: Int @renamedCost(weight: 20) } + scalar ExpensiveInt @renamedCost(weight: 30) + + type ExpensiveObject @renamedCost(weight: 40) { + id: ID + } + type Query { fieldWithCost: Int @renamedCost(weight: 5) argWithCost(arg: Int @renamedCost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `, }; @@ -147,8 +195,13 @@ const subgraphWithRenamedListSizeFromFederationSpec = { gql` extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: [{ name: "@listSize", as: "@renamedListSize" }]) + type HasInts { + ints: [Int!] + } + type Query { fieldWithListSize: [String!] @renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @renamedListSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } `, }; @@ -193,7 +246,27 @@ function inputWithCost(result: CompositionResult): InputObjectType | undefined { ?.type as InputObjectType; } -// Used to test @listSize applications on FIELD_DEFINITION +// Used to test @cost applications on SCALAR +function scalarWithCost(result: CompositionResult): ScalarType | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('scalarWithCost') + ?.type as ScalarType +} + +// Used to test @cost applications on OBJECT +function objectWithCost(result: CompositionResult): ObjectType | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('objectWithCost') + ?.type as ObjectType +} + +// Used to test @listSize applications on FIELD_DEFINITION with a statically assumed size function fieldWithListSize(result: CompositionResult): FieldDefinition | undefined { return result .schema @@ -202,6 +275,15 @@ function fieldWithListSize(result: CompositionResult): FieldDefinition | undefined { + return result + .schema + ?.schemaDefinition + .rootType('query') + ?.field('fieldWithDynamicListSize'); +} + describe('demand control directive composition', () => { it.each([ [subgraphWithCost, subgraphWithListSize], @@ -223,8 +305,17 @@ describe('demand control directive composition', () => { const inputCostDirectiveApplications = inputWithCost(result)?.field('somethingWithCost')?.appliedDirectivesOf('cost'); expect(inputCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 20)`); + const scalarCostDirectiveApplications = scalarWithCost(result)?.appliedDirectivesOf('cost'); + expect(scalarCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 30)`); + + const objectCostDirectiveApplications = objectWithCost(result)?.appliedDirectivesOf('cost'); + expect(objectCostDirectiveApplications?.toString()).toMatchString(`@cost(weight: 40)`); + const listSizeDirectiveApplications = fieldWithListSize(result)?.appliedDirectivesOf('listSize'); expect(listSizeDirectiveApplications?.toString()).toMatchString(`@listSize(assumedSize: 2000, requireOneSlicingArgument: false)`); + + const dynamicListSizeDirectiveApplications = fieldWithDynamicListSize(result)?.appliedDirectivesOf('listSize'); + expect(dynamicListSizeDirectiveApplications?.toString()).toMatchString(`@listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true)`); }); describe('when renamed', () => { @@ -252,8 +343,20 @@ describe('demand control directive composition', () => { const enumCostDirectiveApplications = enumWithCost(result)?.appliedDirectivesOf('renamedCost'); expect(enumCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 15)`); + const inputCostDirectiveApplications = inputWithCost(result)?.field('somethingWithCost')?.appliedDirectivesOf('renamedCost'); + expect(inputCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 20)`); + + const scalarCostDirectiveApplications = scalarWithCost(result)?.appliedDirectivesOf('renamedCost'); + expect(scalarCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 30)`); + + const objectCostDirectiveApplications = objectWithCost(result)?.appliedDirectivesOf('renamedCost'); + expect(objectCostDirectiveApplications?.toString()).toMatchString(`@renamedCost(weight: 40)`); + const listSizeDirectiveApplications = fieldWithListSize(result)?.appliedDirectivesOf('renamedListSize'); expect(listSizeDirectiveApplications?.toString()).toMatchString(`@renamedListSize(assumedSize: 2000, requireOneSlicingArgument: false)`); + + const dynamicListSizeDirectiveApplications = fieldWithDynamicListSize(result)?.appliedDirectivesOf('renamedListSize'); + expect(dynamicListSizeDirectiveApplications?.toString()).toMatchString(`@renamedListSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true)`); }); }); @@ -406,6 +509,15 @@ describe('demand control directive extraction', () => { B } + scalar ExpensiveInt + @federation__cost(weight: 30) + + type ExpensiveObject + @federation__cost(weight: 40) + { + id: ID + } + input InputTypeWithCost { somethingWithCost: Int @federation__cost(weight: 20) } @@ -415,6 +527,8 @@ describe('demand control directive extraction', () => { argWithCost(arg: Int @federation__cost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `); }); @@ -436,63 +550,15 @@ describe('demand control directive extraction', () => { query: Query } - type Query { - fieldWithListSize: [String!] @federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false) - } - `); - }); - - it('extracts @listSize with dynamic cost arguments', () => { - const subgraphA = { - name: 'subgraph-a', - typeDefs: asFed2SubgraphDocument(gql` - extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) - - type Query { - sizedList(first: Int!): HasInts @shareable @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) - } - - type HasInts { - ints: [Int!] @shareable - } - `) - }; - const subgraphB = { - name: 'subgraph-b', - typeDefs: asFed2SubgraphDocument(gql` - extend schema @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@listSize"]) - - type Query { - sizedList(first: Int!): HasInts @shareable @listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: false) - } - - type HasInts { - ints: [Int!] @shareable - } - `) - }; - - const result = composeServices([subgraphA, subgraphB]); - assertCompositionSuccess(result); - const supergraph = Supergraph.build(result.supergraphSdl); - - const expectedSubgraph = ` - schema - ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} - { - query: Query - } - type HasInts { - ints: [Int!] @shareable + ints: [Int!] } type Query { - sizedList(first: Int!): HasInts @shareable @federation__listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: false) + fieldWithListSize: [String!] @federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @federation__listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } - `; - expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(expectedSubgraph); - expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(expectedSubgraph); + `); }); describe('when used on @shareable fields', () => { diff --git a/internals-js/src/__tests__/schemaUpgrader.test.ts b/internals-js/src/__tests__/schemaUpgrader.test.ts index f93b03b37..47bed9958 100644 --- a/internals-js/src/__tests__/schemaUpgrader.test.ts +++ b/internals-js/src/__tests__/schemaUpgrader.test.ts @@ -366,8 +366,8 @@ test('fully upgrades a schema with no @link directive', () => { test("don't add @shareable to subscriptions", () => { const subgraph1 = buildSubgraph( - "subgraph1", - "", + 'subgraph1', + '', `#graphql type Query { hello: String @@ -376,12 +376,12 @@ test("don't add @shareable to subscriptions", () => { type Subscription { update: String! } - ` + `, ); - + const subgraph2 = buildSubgraph( - "subgraph2", - "", + 'subgraph2', + '', `#graphql type Query { hello: String @@ -390,16 +390,30 @@ test("don't add @shareable to subscriptions", () => { type Subscription { update: String! } - ` + `, ); const subgraphs = new Subgraphs(); subgraphs.add(subgraph1); subgraphs.add(subgraph2); const result = upgradeSubgraphsIfNecessary(subgraphs); - expect(printSchema(result.subgraphs!.get("subgraph1")!.schema!)).not.toContain('update: String! @shareable'); - expect(printSchema(result.subgraphs!.get("subgraph2")!.schema!)).not.toContain('update: String! @shareable'); - - expect(result.subgraphs!.get("subgraph1")!.schema.type('Subscription')?.appliedDirectivesOf('@shareable').length).toBe(0); - expect(result.subgraphs!.get("subgraph2")!.schema.type('Subscription')?.appliedDirectivesOf('@shareable').length).toBe(0); + expect( + printSchema(result.subgraphs!.get('subgraph1')!.schema!), + ).not.toContain('update: String! @shareable'); + expect( + printSchema(result.subgraphs!.get('subgraph2')!.schema!), + ).not.toContain('update: String! @shareable'); + + expect( + result + .subgraphs!.get('subgraph1')! + .schema.type('Subscription') + ?.appliedDirectivesOf('@shareable').length, + ).toBe(0); + expect( + result + .subgraphs!.get('subgraph2')! + .schema.type('Subscription') + ?.appliedDirectivesOf('@shareable').length, + ).toBe(0); }); diff --git a/internals-js/src/extractSubgraphsFromSupergraph.ts b/internals-js/src/extractSubgraphsFromSupergraph.ts index 1d8a2e4d3..99663291d 100644 --- a/internals-js/src/extractSubgraphsFromSupergraph.ts +++ b/internals-js/src/extractSubgraphsFromSupergraph.ts @@ -351,7 +351,8 @@ function addAllEmptySubgraphTypes(args: ExtractArguments): TypesInfo { for (const application of typeApplications) { const subgraph = getSubgraph(application); assert(subgraph, () => `Should have found the subgraph for ${application}`); - subgraph.schema.addType(newNamedType(type.kind, type.name)); + const subgraphType = subgraph.schema.addType(newNamedType(type.kind, type.name)); + propagateDemandControlDirectives(type, subgraphType, subgraph, args.originalDirectiveNames); } break; } @@ -449,6 +450,10 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo Date: Wed, 14 Aug 2024 08:16:36 -0700 Subject: [PATCH 06/11] Skip directive extraction for custom directives which happen to conflict with default federation directive names (#3108) When we extract demand control directives from the supergraph to the query planner's subgraphs, we look for well-known directive names and copy instances of them over. When looking up the directive name from the imports, we would use the default expected name. If a custom directive with the default name was imported, we would wrongly try to copy it over as the federation directive. This change makes it so we only extract directives which are imported from `specs.apollo.dev`, such that we don't confuse a custom `@cost` directive with the one defined in federation. --- .../__tests__/compose.demandControl.test.ts | 123 +++++++++++++++++- .../src/extractSubgraphsFromSupergraph.ts | 53 ++++++-- 2 files changed, 161 insertions(+), 15 deletions(-) diff --git a/composition-js/src/__tests__/compose.demandControl.test.ts b/composition-js/src/__tests__/compose.demandControl.test.ts index 62a5a4ebe..cf1bab81f 100644 --- a/composition-js/src/__tests__/compose.demandControl.test.ts +++ b/composition-js/src/__tests__/compose.demandControl.test.ts @@ -206,6 +206,36 @@ const subgraphWithRenamedListSizeFromFederationSpec = { `, }; +const subgraphWithUnimportedCost = { + name: 'subgraphWithCost', + typeDefs: asFed2SubgraphDocument(gql` + enum AorB @federation__cost(weight: 15) { + A + B + } + + input InputTypeWithCost { + somethingWithCost: Int @federation__cost(weight: 20) + } + + type Query { + fieldWithCost: Int @federation__cost(weight: 5) + argWithCost(arg: Int @federation__cost(weight: 10)): Int + enumWithCost: AorB + inputWithCost(someInput: InputTypeWithCost): Int + } + `), +}; + +const subgraphWithUnimportedListSize = { + name: 'subgraphWithListSize', + typeDefs: asFed2SubgraphDocument(gql` + type Query { + fieldWithListSize: [String!] @federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false) + } + `), +}; + // Used to test @cost applications on FIELD_DEFINITION function fieldWithCost(result: CompositionResult): FieldDefinition | undefined { return result @@ -482,6 +512,29 @@ describe('demand control directive composition', () => { `); }); }); + + describe('when using fully qualified names instead of importing', () => { + it('propagates @federation__cost and @federation__listSize to the supergraph', () => { + const result = composeServices([subgraphWithUnimportedCost, subgraphWithUnimportedListSize]); + assertCompositionSuccess(result); + expect(result.hints).toEqual([]); + + const costDirectiveApplications = fieldWithCost(result)?.appliedDirectivesOf('federation__cost'); + expect(costDirectiveApplications?.toString()).toMatchString(`@federation__cost(weight: 5)`); + + const argCostDirectiveApplications = argumentWithCost(result)?.appliedDirectivesOf('federation__cost'); + expect(argCostDirectiveApplications?.toString()).toMatchString(`@federation__cost(weight: 10)`); + + const enumCostDirectiveApplications = enumWithCost(result)?.appliedDirectivesOf('federation__cost'); + expect(enumCostDirectiveApplications?.toString()).toMatchString(`@federation__cost(weight: 15)`); + + const inputCostDirectiveApplications = inputWithCost(result)?.field('somethingWithCost')?.appliedDirectivesOf('federation__cost'); + expect(inputCostDirectiveApplications?.toString()).toMatchString(`@federation__cost(weight: 20)`); + + const listSizeDirectiveApplications = fieldWithListSize(result)?.appliedDirectivesOf('federation__listSize'); + expect(listSizeDirectiveApplications?.toString()).toMatchString(`@federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false)`); + }); + }) }); describe('demand control directive extraction', () => { @@ -489,11 +542,12 @@ describe('demand control directive extraction', () => { subgraphWithCost, subgraphWithRenamedCost, subgraphWithCostFromFederationSpec, - subgraphWithRenamedCostFromFederationSpec + subgraphWithRenamedCostFromFederationSpec, + subgraphWithUnimportedCost, ])('extracts @cost from the supergraph', (subgraph: ServiceDefinition) => { const result = composeServices([subgraph]); assertCompositionSuccess(result); - const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraphWithCost.name); + const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraph.name); expect(extracted?.toString()).toMatchString(` schema @@ -537,11 +591,12 @@ describe('demand control directive extraction', () => { subgraphWithListSize, subgraphWithRenamedListSize, subgraphWithListSizeFromFederationSpec, - subgraphWithRenamedListSizeFromFederationSpec + subgraphWithRenamedListSizeFromFederationSpec, + subgraphWithUnimportedListSize, ])('extracts @listSize from the supergraph', (subgraph: ServiceDefinition) => { const result = composeServices([subgraph]); assertCompositionSuccess(result); - const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraphWithListSize.name); + const extracted = Supergraph.build(result.supergraphSdl).subgraphs().get(subgraph.name); expect(extracted?.toString()).toMatchString(` schema @@ -646,4 +701,64 @@ describe('demand control directive extraction', () => { expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(expectedSubgraph); }); }); + + describe('when the supergraph uses custom directives with the same name', () => { + it('does not attempt to extract them to the subgraphs', () => { + const subgraphA = { + name: 'subgraph-a', + typeDefs: asFed2SubgraphDocument(gql` + extend schema + @link(url: "https://example.com/myCustomDirective/v1.0", import: ["@cost"]) + @composeDirective(name: "@cost") + + directive @cost(name: String!) on FIELD_DEFINITION + + type Query { + a: Int @cost(name: "cost") + } + `) + }; + const subgraphB = { + name: 'subgraph-b', + typeDefs: asFed2SubgraphDocument(gql` + extend schema + @link(url: "https://example.com/myOtherCustomDirective/v1.0", import: ["@listSize"]) + @composeDirective(name: "@listSize") + + directive @listSize(name: String!) on FIELD_DEFINITION + + type Query { + b: [Int] @listSize(name: "listSize") + } + `) + }; + + const result = composeServices([subgraphA, subgraphB]); + assertCompositionSuccess(result); + const supergraph = Supergraph.build(result.supergraphSdl); + + expect(supergraph.subgraphs().get(subgraphA.name)?.toString()).toMatchString(` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + a: Int + } + `); + expect(supergraph.subgraphs().get(subgraphB.name)?.toString()).toMatchString(` + schema + ${FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS} + { + query: Query + } + + type Query { + b: [Int] + } + `); + }); + }); }); diff --git a/internals-js/src/extractSubgraphsFromSupergraph.ts b/internals-js/src/extractSubgraphsFromSupergraph.ts index 99663291d..3d412d039 100644 --- a/internals-js/src/extractSubgraphsFromSupergraph.ts +++ b/internals-js/src/extractSubgraphsFromSupergraph.ts @@ -40,7 +40,7 @@ import { parseSelectionSet } from "./operations"; import fs from 'fs'; import path from 'path'; import { validateStringContainsBoolean } from "./utils"; -import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; +import { CONTEXT_VERSIONS, ContextSpecDefinition, DirectiveDefinition, FeatureUrl, FederationDirectiveName, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; function filteredTypes( supergraph: Schema, @@ -224,7 +224,7 @@ export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtra } const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition); - const originalDirectiveNames = getOriginalDirectiveNames(supergraph); + const originalDirectiveNames = getApolloDirectiveNames(supergraph); const args: ExtractArguments = { supergraph, subgraphs, @@ -485,13 +485,40 @@ function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo { +/** + * Builds a map of original name to new name for Apollo feature directives. This is + * used to handle cases where a directive is renamed via an import statement. For + * example, importing a directive with a custom name like + * ```graphql + * @link(url: "https://specs.apollo.dev/cost/v0.1", import: [{ name: "@cost", as: "@renamedCost" }]) + * ``` + * results in a map entry of `cost -> renamedCost` with the `@` prefix removed. + * + * If the directive is imported under its default name, that also results in an entry. So, + * ```graphql + * @link(url: "https://specs.apollo.dev/cost/v0.1", import: ["@cost"]) + * ``` + * results in a map entry of `cost -> cost`. This duals as a way to check if a directive + * is included in the supergraph schema. + * + * **Important:** This map does _not_ include directives imported from identities other + * than `specs.apollo.dev`. This helps us avoid extracting directives to subgraphs + * when a custom directive's name conflicts with that of a default one. + */ +function getApolloDirectiveNames(supergraph: Schema): Record { const originalDirectiveNames: Record = {}; for (const linkDirective of supergraph.schemaDefinition.appliedDirectivesOf("link")) { if (linkDirective.arguments().url && linkDirective.arguments().import) { + const url = FeatureUrl.maybeParse(linkDirective.arguments().url); + if (!url?.identity.includes("specs.apollo.dev")) { + continue; + } + for (const importedDirective of linkDirective.arguments().import) { if (importedDirective.name && importedDirective.as) { originalDirectiveNames[importedDirective.name.replace('@', '')] = importedDirective.as.replace('@', ''); + } else if (typeof importedDirective === 'string') { + originalDirectiveNames[importedDirective.replace('@', '')] = importedDirective.replace('@', ''); } } } @@ -652,16 +679,20 @@ function maybeDumpSubgraphSchema(subgraph: Subgraph): string { } function propagateDemandControlDirectives(source: SchemaElement, dest: SchemaElement, subgraph: Subgraph, originalDirectiveNames?: Record) { - const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST] ?? FederationDirectiveName.COST; - const costDirective = source.appliedDirectivesOf(costDirectiveName).pop(); - if (costDirective) { - dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments()); + const costDirectiveName = originalDirectiveNames?.[FederationDirectiveName.COST]; + if (costDirectiveName) { + const costDirective = source.appliedDirectivesOf(costDirectiveName).pop(); + if (costDirective) { + dest.applyDirective(subgraph.metadata().costDirective().name, costDirective.arguments()); + } } - const listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE] ?? FederationDirectiveName.LIST_SIZE; - const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop(); - if (listSizeDirective) { - dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments()); + const listSizeDirectiveName = originalDirectiveNames?.[FederationDirectiveName.LIST_SIZE]; + if (listSizeDirectiveName) { + const listSizeDirective = source.appliedDirectivesOf(listSizeDirectiveName).pop(); + if (listSizeDirective) { + dest.applyDirective(subgraph.metadata().listSizeDirective().name, listSizeDirective.arguments()); + } } } From 1d14e32cbd97aa5b90e6af4a319d634043200bee Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Wed, 14 Aug 2024 08:32:54 -0700 Subject: [PATCH 07/11] Document new demand control directives (#3111) This pull request documents the directives added in https://github.com/apollographql/federation/pull/3074. These will be released in the next federation version (v2.9), which is targeted for the end of the month. Both directives are inspired by the [IBM cost specification](https://ibm.github.io/graphql-specs/cost-spec.html#sec-The-Cost-Directive). So, most of the documentation parrots their specification. --------- Co-authored-by: Edward Huang --- .../__tests__/compose.demandControl.test.ts | 13 ++ .../federated-directives.mdx | 211 ++++++++++++++++++ docs/source/federation-versions.mdx | 74 ++++++ 3 files changed, 298 insertions(+) diff --git a/composition-js/src/__tests__/compose.demandControl.test.ts b/composition-js/src/__tests__/compose.demandControl.test.ts index cf1bab81f..e2c2279b3 100644 --- a/composition-js/src/__tests__/compose.demandControl.test.ts +++ b/composition-js/src/__tests__/compose.demandControl.test.ts @@ -218,11 +218,19 @@ const subgraphWithUnimportedCost = { somethingWithCost: Int @federation__cost(weight: 20) } + scalar ExpensiveInt @federation__cost(weight: 30) + + type ExpensiveObject @federation__cost(weight: 40) { + id: ID + } + type Query { fieldWithCost: Int @federation__cost(weight: 5) argWithCost(arg: Int @federation__cost(weight: 10)): Int enumWithCost: AorB inputWithCost(someInput: InputTypeWithCost): Int + scalarWithCost: ExpensiveInt + objectWithCost: ExpensiveObject } `), }; @@ -230,8 +238,13 @@ const subgraphWithUnimportedCost = { const subgraphWithUnimportedListSize = { name: 'subgraphWithListSize', typeDefs: asFed2SubgraphDocument(gql` + type HasInts { + ints: [Int!] + } + type Query { fieldWithListSize: [String!] @federation__listSize(assumedSize: 2000, requireOneSlicingArgument: false) + fieldWithDynamicListSize(first: Int!): HasInts @federation__listSize(slicingArguments: ["first"], sizedFields: ["ints"], requireOneSlicingArgument: true) } `), }; diff --git a/docs/source/federated-schemas/federated-directives.mdx b/docs/source/federated-schemas/federated-directives.mdx index a8b009c2e..5a01af6f1 100644 --- a/docs/source/federated-schemas/federated-directives.mdx +++ b/docs/source/federated-schemas/federated-directives.mdx @@ -982,3 +982,214 @@ The selection syntax for `@fromContext` used in its `ContextFieldValue` is simil When the same contextual value is set in multiple places, the `ContextFieldValue` must resolve all types from each place into a single value that matches the parameter type. For examples using `@context` and `@fromContext`, see [Using contexts to share data along type hierarchies](../entities/use-contexts). + +## Customizing demand controls + + + +### `@cost` + + + + + +```graphql +directive @cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR +``` + +The `@cost` directive defines a custom weight for a schema location. For GraphOS Router, it customizes the operation cost calculation of the [demand control feature](/router/executing-operations/demand-control/). + +If `@cost` is not specified for a field, a default value is used: +- Scalars and enums have default cost of 0 +- Composite input and output types have default cost of 1 + +Regardless of whether `@cost` is specified on a field, the field cost for that field also accounts for its arguments and selections. + +#### Arguments + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ +##### `weight` + +`Int!` + + +**Required.** Assigns a custom weight for scoring the current field. + +
+ + + +### `@listSize` + + + + + +```graphql +directive @listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +``` + +The `@listSize` directive is used to customize the cost calculation of the [demand control feature](/router/executing-operations/demand-control/) of GraphOS Router. + +In the static analysis phase, the cost calculator does not know how many entities will be returned by each list field in a given query. By providing an estimated list size for a field with `@listSize`, the cost calculator can produce a more accurate estimate the cost during static analysis. + +#### Configuring static list sizes + +The simplest way to define a list size for a field is to use the `assumedSize` argument. This defines a static assumed maximum length for a given list field in the schema. + +```graphql +type Query { + items: [Item!] @listSize(assumedSize: 10) +} + +type Item @key(fields: "id") { + id: ID +} +``` + +In this case, all queries for `items` are expected to receive at most ten items in the list. + +#### Configuring dynamic list sizes + +When using paging parameters, the length of a list field can be determined by an input value. You can use the `slicingArguments` argument to tell the router to expect as many elements as the query requests. + +```graphql +type Query { + items(first: Int, last: Int): [Item!] @listSize(slicingArguments: ["first", "last"], requireOneSlicingArgument: false) +} +``` + +In this example, the `items` field can be requested with paging parameters. If the client sends a query with multiple slicing arguments, the scoring algorithm will use the maximum value of all specified slicing arguments. The following query is assumed to return ten items in the scoring algorithm. + +```graphql +query MultipleSlicingArgumentsQuery { + items(first: 5, last: 10) +} +``` + +In some cases, you may want to enforce that only one slicing argument is used. For example, you may want to ensure that clients request either the first _n_ items or the last _n_ items, but not both. You can do this by setting `requireOneSlicingArgument` to `true`. + +```graphql +type Query { + items(first: Int, last: Int): [Item!] @listSize(slicingArguments: ["first", "last"], requireOneSlicingArgument: true) +} +``` + +With this updated schema, sending the the above `MultipleSlicingArgumentsQuery` with its two slicing arguments to a graph would result in an error, as would sending a query with no slicing arguments. + +#### Cursor support + +Some pagination patterns include extra information along with the requested entities. For example, we may have some schema with a cursor type. + +```graphql +type Query { + items(first: Int): Cursor! @listSize(slicingArguments: ["first"], sizedFields: ["page"]) +} + +type Cursor { + page: [Item!] + nextPageToken: String +} + +type Item @key(fields: "id") { + id: ID +} +``` + +This application of `@listSize` indicates that the length of the `page` field inside `Cursor` is determined by the `first` argument. + + +#### Arguments + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name /
Type
Description
+ +##### `assumedSize` + +`Int` + + +Indicates that the annotated list field will return at most this many items. + +
+ +##### `slicingArguments` + +`[String!]` + + + +Indicates that the annotated list field returns as many items as are requested by a paging argument. If multiple arguments are passed, the maximum value of the arguments is used. + +If both this and `assumedSize` are specified, the value from `slicingArguments` will take precedence. + +
+ +##### `sizedFields` + +`[String!]` + + +Supports cursor objects by indicating that the expected list size should be applied to fields within the returned object. + +
+ +##### `requireOneSlicingArgument` + +`Boolean` + + +If `true`, indicates that queries must supply exactly one argument from `slicingArguments`. + +If `slicingArguments` are not specified, this value is ignored. + +The default value is `true`. + +
diff --git a/docs/source/federation-versions.mdx b/docs/source/federation-versions.mdx index 13cbb03bc..b1cebbb1f 100644 --- a/docs/source/federation-versions.mdx +++ b/docs/source/federation-versions.mdx @@ -26,6 +26,80 @@ For a comprehensive changelog for Apollo Federation and its associated libraries - If you maintain a [subgraph-compatible library](./building-supergraphs/compatible-subgraphs/), consult this article to stay current with recently added directives. All of these directive definitions are also listed in the [subgraph specification](./subgraph-spec/#subgraph-schema-additions). +## v2.9 + +
+ + + +
+ +First release + +**August 2024** + +
+ +
+ +Minimum router version + +**TBD** + +
+ +
+ +
+ +#### Directive changes + + + + + + + + + + + + + + + + + + + + + +
TopicDescription
+ +#### `@cost` + + + +Introduced. [Learn more](./federated-types/federated-directives/#cost). + +```graphql +directive @cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR +``` + +
+ +#### `@listSize` + + + +Introduced. [Learn more](./federated-types/federated-directives/#listsize). + +```graphql +directive @listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION +``` + +
+ ## v2.8
From b91e81a62484434ada040af6bf8db0649234569a Mon Sep 17 00:00:00 2001 From: Taylor Ninesling Date: Wed, 21 Aug 2024 09:07:46 -0700 Subject: [PATCH 08/11] Fix typo in demand control directive docs (#3124) Description for `@listSize` is missing a word. --- docs/source/federated-schemas/federated-directives.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/federated-schemas/federated-directives.mdx b/docs/source/federated-schemas/federated-directives.mdx index 5a01af6f1..6c750b598 100644 --- a/docs/source/federated-schemas/federated-directives.mdx +++ b/docs/source/federated-schemas/federated-directives.mdx @@ -1048,7 +1048,7 @@ directive @listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: The `@listSize` directive is used to customize the cost calculation of the [demand control feature](/router/executing-operations/demand-control/) of GraphOS Router. -In the static analysis phase, the cost calculator does not know how many entities will be returned by each list field in a given query. By providing an estimated list size for a field with `@listSize`, the cost calculator can produce a more accurate estimate the cost during static analysis. +In the static analysis phase, the cost calculator does not know how many entities will be returned by each list field in a given query. By providing an estimated list size for a field with `@listSize`, the cost calculator can produce a more accurate estimate of the cost during static analysis. #### Configuring static list sizes From acfe3193429c7f99b4fc564b20828aaa8659a75c Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 21 Aug 2024 10:37:19 -0700 Subject: [PATCH 09/11] Add missing query graph edge for interfaces implementing interfaces (#3122) During `addAdditionalAbstractTypeEdges()`, we add edges between abstract types, but we specifically were skipping edges between interfaces implementing interfaces because that function expected it to be added already by `addAbstractTypeEdges()`. However, the latter function only handles edges between concrete (object) and abstract types. This PR accordingly updates `addAdditionalAbstractTypeEdges()` to stop skipping interfaces-implementing-interfaces edges, and updates tests accordingly. --- .changeset/cuddly-badgers-sit.md | 6 ++++++ gateway-js/src/__tests__/executeQueryPlan.test.ts | 12 ++---------- query-graphs-js/src/querygraph.ts | 6 ------ 3 files changed, 8 insertions(+), 16 deletions(-) create mode 100644 .changeset/cuddly-badgers-sit.md diff --git a/.changeset/cuddly-badgers-sit.md b/.changeset/cuddly-badgers-sit.md new file mode 100644 index 000000000..c9880a281 --- /dev/null +++ b/.changeset/cuddly-badgers-sit.md @@ -0,0 +1,6 @@ +--- +"@apollo/gateway": patch +"@apollo/query-graphs": patch +--- + +Avoid type explosion for inline fragments where the type condition is an interface that implements the parent type. diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 160f4cf0e..c9e585f2f 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -2300,22 +2300,14 @@ describe('executeQueryPlan', () => { queryPlan = buildPlan(operation, queryPlanner); - // TODO: we're actually type-exploding in this case because currently, as soon as we need to type-explode, we do - // so into all the runtime types, while here it could make sense to only type-explode into the direct sub-types= - // (the sub-interfaces). We should fix this (but it's only sub-optimal, not incorrect). expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Fetch(service: "S1") { { allValues { __typename - ... on T1 { - a - } - ... on T2 { - a - } - ... on T4 { + ... on SubInterface1 { + __typename a } } diff --git a/query-graphs-js/src/querygraph.ts b/query-graphs-js/src/querygraph.ts index 1d98286c1..d2f738d40 100644 --- a/query-graphs-js/src/querygraph.ts +++ b/query-graphs-js/src/querygraph.ts @@ -1611,12 +1611,6 @@ class GraphBuilderFromSchema extends GraphBuilder { for (let j = i; j < abstractTypesWithTheirRuntimeTypes.length; j++) { const t2 = abstractTypesWithTheirRuntimeTypes[j]; - // We ignore the pair if both are interfaces and one implements the other. We'll already have appropriate - // edges if that's the case. - if (isInterfaceType(t1.type) && isInterfaceType(t2.type) && (t1.type.implementsInterface(t2.type) || t2.type.implementsInterface(t1.type))) { - continue; - } - let addT1ToT2 = false; let addT2ToT1 = false; if (t1.type === t2.type) { From 02c2a34a62c3717a4885449172e404f19ebf66c9 Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 21 Aug 2024 10:37:41 -0700 Subject: [PATCH 10/11] Avoid unnecessary array copying in `ConditionValidationState.advance()` (#3109) We were using `concat()` in `ConditionValidationState.advance()`, which was frequently copying the array. Shifting this to use `push()` instead increases performance significantly for some graphs. --- .changeset/fast-points-wonder.md | 7 +++++++ query-graphs-js/src/conditionsValidation.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-points-wonder.md diff --git a/.changeset/fast-points-wonder.md b/.changeset/fast-points-wonder.md new file mode 100644 index 000000000..710f230b1 --- /dev/null +++ b/.changeset/fast-points-wonder.md @@ -0,0 +1,7 @@ +--- +"@apollo/federation-internals": patch +"@apollo/gateway": patch +"@apollo/composition": patch +--- + +Reduce memory overhead during satisfiability checking when there are many options. diff --git a/query-graphs-js/src/conditionsValidation.ts b/query-graphs-js/src/conditionsValidation.ts index 7bae900ad..7763c170c 100644 --- a/query-graphs-js/src/conditionsValidation.ts +++ b/query-graphs-js/src/conditionsValidation.ts @@ -25,7 +25,7 @@ class ConditionValidationState { ) {} advance(supergraph: Schema): ConditionValidationState[] | null { - let newOptions: SimultaneousPathsWithLazyIndirectPaths[] = []; + const newOptions: SimultaneousPathsWithLazyIndirectPaths[] = []; for (const paths of this.subgraphOptions) { const pathsOptions = advanceSimultaneousPathsWithOperation( supergraph, @@ -40,7 +40,7 @@ class ConditionValidationState { if (!pathsOptions) { continue; } - newOptions = newOptions.concat(pathsOptions); + newOptions.push(...pathsOptions); } // If we got no options, it means that particular selection of the conditions cannot be satisfied, so the From e0a5075c0d12a0e2f7ef303b246e3216a139d3e0 Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Wed, 21 Aug 2024 21:57:10 -0500 Subject: [PATCH 11/11] Fragment variable definitions erased in subgraph queries (#3119) Fixes #3112 Before creating an Operation, we call `collectUsedVariables`, which will only pull values from the selection set and not from fragments. This isn't great, because a variable used in a fragment won't be collected, and it doesn't make sense to collect variables from fragments because it's before they are optimized and many will be unused. The inelegant solution I came up with is to pass in available variables in calls to `optimize` or `generateQueryFragments` for an operation where we can add back in the unused variables. This should be ok, because we are guaranteed that exactly one of them will get called by `toPlanNode`. Pretty sure there won't be too much overhead added because we'll only call this once per subgraph fetch. --------- Co-authored-by: Sachin D. Shinde --- .changeset/poor-seahorses-whisper.md | 6 ++++ internals-js/src/operations.ts | 45 +++++++++++++++++++++++++--- query-planner-js/src/buildPlan.ts | 20 +++++++++++-- 3 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 .changeset/poor-seahorses-whisper.md diff --git a/.changeset/poor-seahorses-whisper.md b/.changeset/poor-seahorses-whisper.md new file mode 100644 index 000000000..0dd498b33 --- /dev/null +++ b/.changeset/poor-seahorses-whisper.md @@ -0,0 +1,6 @@ +--- +"@apollo/query-planner": patch +"@apollo/federation-internals": patch +--- + +Fix issue where variable was not passed into subgraph when embedded in a fragment diff --git a/internals-js/src/operations.ts b/internals-js/src/operations.ts index fb2adb6a9..5db67d995 100644 --- a/internals-js/src/operations.ts +++ b/internals-js/src/operations.ts @@ -56,6 +56,8 @@ import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values"; import { v1 as uuidv1 } from 'uuid'; +export const DEFAULT_MIN_USAGES_TO_OPTIMIZE = 2; + function validate(condition: any, message: () => string, sourceAST?: ASTNode): asserts condition { if (!condition) { throw ERRORS.INVALID_GRAPHQL.err(message(), { nodes: sourceAST }); @@ -934,25 +936,55 @@ export class Operation extends DirectiveTargetElement { this.appliedDirectives, ); } + + private collectUndefinedVariablesFromFragments(fragments: NamedFragments): Variable[] { + const collector = new VariableCollector(); + for (const namedFragment of fragments.definitions()) { + namedFragment.selectionSet.usedVariables().forEach(v => { + if (!this.variableDefinitions.definition(v)) { + collector.add(v); + } + }); + } + return collector.variables(); + } // Returns a copy of this operation with the provided updated selection set and fragments. - private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation { + private withUpdatedSelectionSetAndFragments( + newSelectionSet: SelectionSet, + newFragments: NamedFragments | undefined, + allAvailableVariables?: VariableDefinitions, + ): Operation { if (this.selectionSet === newSelectionSet && newFragments === this.fragments) { return this; } + + let newVariableDefinitions = this.variableDefinitions; + if (allAvailableVariables && newFragments) { + const undefinedVariables = this.collectUndefinedVariablesFromFragments(newFragments); + if (undefinedVariables.length > 0) { + newVariableDefinitions = new VariableDefinitions(); + newVariableDefinitions.addAll(this.variableDefinitions); + newVariableDefinitions.addAll(allAvailableVariables.filter(undefinedVariables)); + } + } return new Operation( this.schema(), this.rootKind, newSelectionSet, - this.variableDefinitions, + newVariableDefinitions, newFragments, this.name, this.appliedDirectives, ); } - optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation { + optimize( + fragments?: NamedFragments, + minUsagesToOptimize: number = DEFAULT_MIN_USAGES_TO_OPTIMIZE, + allAvailableVariables?: VariableDefinitions, + ): Operation { assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`) if (!fragments || fragments.isEmpty()) { return this; @@ -1001,11 +1033,16 @@ export class Operation extends DirectiveTargetElement { } } - return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined); + return this.withUpdatedSelectionSetAndFragments( + optimizedSelection, + finalFragments ?? undefined, + allAvailableVariables, + ); } generateQueryFragments(): Operation { const [minimizedSelectionSet, fragments] = this.selectionSet.minimizeSelectionSet(); + return new Operation( this.schema(), this.rootKind, diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index 6a27faaa1..d12701a91 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -25,7 +25,6 @@ import { Variable, VariableDefinition, VariableDefinitions, - VariableCollector, newDebugLogger, selectionOfElement, selectionSetOfElement, @@ -64,6 +63,8 @@ import { isInputType, possibleRuntimeTypes, NamedType, + VariableCollector, + DEFAULT_MIN_USAGES_TO_OPTIMIZE, } from "@apollo/federation-internals"; import { advanceSimultaneousPathsWithOperation, @@ -1591,16 +1592,29 @@ class FetchGroup { if (this.generateQueryFragments) { operation = operation.generateQueryFragments(); } else { - operation = operation.optimize(fragments?.forSubgraph(this.subgraphName, subgraphSchema)); + operation = operation.optimize( + fragments?.forSubgraph(this.subgraphName, subgraphSchema), + DEFAULT_MIN_USAGES_TO_OPTIMIZE, + variableDefinitions, + ); } + // collect all used variables in the selection and in used Fragments + const usedVariables = new Set(selection.usedVariables().map(v => v.name)); + if (operation.fragments) { + for (const namedFragment of operation.fragments.definitions()) { + namedFragment.selectionSet.usedVariables().forEach(v => { + usedVariables.add(v.name); + }); + } + } const operationDocument = operationToDocument(operation); const fetchNode: FetchNode = { kind: 'Fetch', id: this.id, serviceName: this.subgraphName, requires: inputNodes ? trimSelectionNodes(inputNodes.selections) : undefined, - variableUsages: selection.usedVariables().map(v => v.name), + variableUsages: Array.from(usedVariables), operation: stripIgnoredCharacters(print(operationDocument)), operationKind: schemaRootKindToOperationKind(operation.rootKind), operationName: operation.name,