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