diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f787a3d9..16f8769d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,7 +31,6 @@ jobs: path: junit.xml workflows: - version: 2 Build: jobs: - Test: @@ -56,3 +55,6 @@ workflows: - node/run: name: Check Spelling npm-run: spell:check + - node/run: + name: Check Prettier (tests) + npm-run: prettier:check diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f02884cee..4d841d837 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,4 @@ # Run prettier f2f1d6041cd67c67fdacabe0570fe2ce642c052c +# Enforce prettier on all test files https://github.com/apollographql/federation/pull/2785 +dca3d464c8a699deaa1109ae69f376750b48c249 \ No newline at end of file diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit new file mode 100755 index 000000000..8912dd546 --- /dev/null +++ b/.git-hooks/pre-commit @@ -0,0 +1,9 @@ +#!/bin/bash + +# Grab prettier output, filter out garbage output lines (lines which aren't a +# file path), and remove the " 0ms" from the end of each line (everything after +# whitespace) +FILES=$(npm run prettier:fix | grep 'src' | sed 's/ .*//') + +# Add the files to the commit +echo "$FILES" | xargs git add \ No newline at end of file diff --git a/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts b/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts index 4881952a9..68ff85446 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.conditions.test.ts @@ -1,13 +1,23 @@ import gql from 'graphql-tag'; -import { getFederatedTestingSchema, ServiceDefinitionModule } from './execution-utils'; +import { + getFederatedTestingSchema, + ServiceDefinitionModule, +} from './execution-utils'; import { astSerializer, queryPlanSerializer, } from 'apollo-federation-integration-testsuite'; -import { Operation, parseOperation, Schema } from '@apollo/federation-internals'; +import { + Operation, + parseOperation, + Schema, +} from '@apollo/federation-internals'; import { QueryPlan } from '@apollo/query-planner'; import { LocalGraphQLDataSource } from '../datasources'; -import { GatewayExecutionResult, GatewayGraphQLRequestContext } from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { buildOperationContext } from '../operationContext'; import { executeQueryPlan } from '../executeQueryPlan'; @@ -15,7 +25,9 @@ expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); describe('Execution tests for @include/@skip', () => { - function buildRequestContext(variables: Record): GatewayGraphQLRequestContext { + function buildRequestContext( + variables: Record, + ): GatewayGraphQLRequestContext { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { @@ -28,7 +40,7 @@ describe('Execution tests for @include/@skip', () => { }; } - let s2Queries: {id : number}[] = []; + let s2Queries: { id: number }[] = []; /** * Simple subgraph schemas reused by a number of tests. This declares a simple interface `T` with 2 implems `T1` and `T2`. * There is a simple operation that returns a list of 3 simple objects: @@ -70,9 +82,9 @@ describe('Execution tests for @include/@skip', () => { { __typename: 'T2', id: 2, a2: 20 }, { __typename: 'T1', id: 3, a1: 30 }, ]; - } + }, }, - } + }, }, { name: 'S2', @@ -100,8 +112,8 @@ describe('Execution tests for @include/@skip', () => { return { ...ref, b2: 100 * ref.id }; }, }, - } - } + }, + }, ]; async function executePlan( @@ -114,7 +126,9 @@ describe('Execution tests for @include/@skip', () => { const apiSchema = schema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -127,50 +141,71 @@ describe('Execution tests for @include/@skip', () => { } describe('Constant conditions optimisation', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('top-level @include never included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @include(if: false) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(`QueryPlan {}`); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(`Object {}`); }); it('top-level @skip always skipped', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @skip(if: true) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(`QueryPlan {}`); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(`Object {}`); }); it('top-level @include always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @include(if: true) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Note that due to how we handle constant conditions, those don't get removed in the fetch nodes (which only matter when things are @@ -190,7 +225,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -210,13 +250,16 @@ describe('Execution tests for @include/@skip', () => { }); it('top-level @skip always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t @skip(if: false) { id } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Same as for @include: the @skip within the fetch is not cleared, but that's harmless. @@ -233,7 +276,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -253,7 +301,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @include never included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -262,7 +312,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Importantly, we only bother querying S1 @@ -283,7 +334,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -301,7 +357,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @skip always skipped', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -310,7 +368,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Importantly, we only bother querying S1 @@ -331,7 +390,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -349,7 +413,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @include always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -358,7 +424,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Again, constantly included is the one case that still show up in the fetch, but that's harmless. The point here is @@ -397,7 +464,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -417,7 +489,9 @@ describe('Execution tests for @include/@skip', () => { }); it('Non top-level @skip always included', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { ... on T1 { @@ -426,7 +500,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Again, constantly included is the one case that still show up in the fetch, but that's harmless. The point here is @@ -465,7 +540,12 @@ describe('Execution tests for @include/@skip', () => { } `); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -486,10 +566,14 @@ describe('Execution tests for @include/@skip', () => { }); describe('Simple variable conditions handling', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('handles default values for condition variables', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean! = false){ t { id @@ -498,7 +582,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -540,7 +625,12 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // No variables: the default (not included) should be used. - let response = await executePlan(queryPlan, operation, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -561,7 +651,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -584,7 +676,9 @@ describe('Execution tests for @include/@skip', () => { }); it('handles condition on named fragments spread', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -595,7 +689,8 @@ describe('Execution tests for @include/@skip', () => { fragment GetB1 on T1 { b1 } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -635,7 +730,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -658,7 +759,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -679,7 +782,9 @@ describe('Execution tests for @include/@skip', () => { }); it('handles condition inside named fragments', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -690,7 +795,8 @@ describe('Execution tests for @include/@skip', () => { fragment OtherGetB1 on T1 { b1 @include(if: $if) } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -730,7 +836,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -753,7 +865,9 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // Checks that the overriding of the default does work. - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -772,13 +886,17 @@ describe('Execution tests for @include/@skip', () => { `); expect(s2Queries).toHaveLength(0); }); - }) + }); describe('Fetches with multiple top-level conditioned types', () => { - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(simpleInterfaceSchemas); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema( + simpleInterfaceSchemas, + ); it('creates a condition node when a condition covers a whole fetch', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if: Boolean!){ t { id @@ -790,7 +908,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -842,7 +961,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -866,7 +991,9 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -888,7 +1015,9 @@ describe('Execution tests for @include/@skip', () => { }); it('does _not_ creates a condition node when no single condition covers the whole fetch', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!){ t { id @@ -900,7 +1029,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -950,7 +1080,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -974,7 +1110,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -999,7 +1138,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1024,7 +1166,9 @@ describe('Execution tests for @include/@skip', () => { it('handles "nested" conditions', async () => { // Shows that as long as the condition matches, even "nested" @include gets handled as a condition node. - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!){ t { id @@ -1036,7 +1180,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // We validate that the condition has been extracted. @@ -1090,7 +1235,13 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: true }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1114,7 +1265,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(3); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: false, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: false, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1135,7 +1289,10 @@ describe('Execution tests for @include/@skip', () => { expect(s2Queries).toHaveLength(0); s2Queries = []; - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: false }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: true, + if2: false, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1157,7 +1314,9 @@ describe('Execution tests for @include/@skip', () => { describe('same element having both @skip and @include', () => { it('with constant conditions, neither excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1166,7 +1325,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1208,7 +1368,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1231,7 +1396,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, both excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1240,7 +1407,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1261,7 +1429,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1282,7 +1455,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, first excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1291,7 +1466,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1312,7 +1488,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1333,7 +1514,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with constant conditions, second excluding', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { id @@ -1342,7 +1525,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -1363,7 +1547,12 @@ describe('Execution tests for @include/@skip', () => { `); s2Queries = []; - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1384,7 +1573,9 @@ describe('Execution tests for @include/@skip', () => { }); it('with variable conditions', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($if1: Boolean!, $if2: Boolean!) { t { id @@ -1393,7 +1584,8 @@ describe('Execution tests for @include/@skip', () => { } } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); // Ensures both @skip and @include have condition nodes. @@ -1441,7 +1633,13 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // With data included by both conditions - let response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: false }); + let response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { if1: true, if2: false }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1464,7 +1662,10 @@ describe('Execution tests for @include/@skip', () => { s2Queries = []; // With data excluded by one condition - response = await executePlan(queryPlan, operation, schema, serviceMap, { if1: true, if2: true }); + response = await executePlan(queryPlan, operation, schema, serviceMap, { + if1: true, + if2: true, + }); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1483,6 +1684,6 @@ describe('Execution tests for @include/@skip', () => { `); expect(s2Queries).toHaveLength(0); }); - }) - }) + }); + }); }); diff --git a/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts b/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts index 86e2e2baa..8202dcc32 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts @@ -1,13 +1,25 @@ import gql from 'graphql-tag'; -import { getFederatedTestingSchema, ServiceDefinitionModule } from './execution-utils'; -import { Operation, parseOperation, Schema } from "@apollo/federation-internals"; +import { + getFederatedTestingSchema, + ServiceDefinitionModule, +} from './execution-utils'; +import { + Operation, + parseOperation, + Schema, +} from '@apollo/federation-internals'; import { QueryPlan } from '@apollo/query-planner'; import { LocalGraphQLDataSource } from '../datasources'; -import { GatewayExecutionResult, GatewayGraphQLRequestContext } from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { buildOperationContext } from '../operationContext'; import { executeQueryPlan } from '../executeQueryPlan'; -function buildRequestContext(variables: Record): GatewayGraphQLRequestContext { +function buildRequestContext( + variables: Record, +): GatewayGraphQLRequestContext { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { @@ -30,7 +42,9 @@ async function executePlan( const apiSchema = schema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -67,20 +81,29 @@ describe('handling of introspection queries', () => { `, }, ]; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(typeDefs); + const { serviceMap, schema, queryPlanner } = + getFederatedTestingSchema(typeDefs); it('it handles aliases on introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { myAlias: __type(name: "T1") { kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -93,17 +116,25 @@ describe('handling of introspection queries', () => { }); it('it handles aliases inside introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { __type(name: "T1") { myKind: kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -116,17 +147,26 @@ describe('handling of introspection queries', () => { }); it('it handles variables passed to introspection fields', async () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` query ($name: String!) { __type(name: $name) { kind name } } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, schema, serviceMap, { name: "T1" }); + const response = await executePlan( + queryPlan, + operation, + schema, + serviceMap, + { name: 'T1' }, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { diff --git a/gateway-js/src/__tests__/executeQueryPlan.test.ts b/gateway-js/src/__tests__/executeQueryPlan.test.ts index 659ee5b7e..160f4cf0e 100644 --- a/gateway-js/src/__tests__/executeQueryPlan.test.ts +++ b/gateway-js/src/__tests__/executeQueryPlan.test.ts @@ -18,12 +18,21 @@ import { QueryPlan, QueryPlanner } from '@apollo/query-planner'; import { ApolloGateway } from '..'; import { ApolloServer } from '@apollo/server'; import { getFederatedTestingSchema } from './execution-utils'; -import { Schema, Operation, parseOperation, arrayEquals, Supergraph } from '@apollo/federation-internals'; +import { + Schema, + Operation, + parseOperation, + arrayEquals, + Supergraph, +} from '@apollo/federation-internals'; import { addResolversToSchema, GraphQLResolverMap, } from '@apollo/subgraph/src/schema-helper'; -import {GatewayExecutionResult, GatewayGraphQLRequestContext} from '@apollo/server-gateway-interface'; +import { + GatewayExecutionResult, + GatewayGraphQLRequestContext, +} from '@apollo/server-gateway-interface'; import { unwrapSingleResultKind } from './gateway/testUtils'; expect.addSnapshotSerializer(astSerializer); @@ -35,26 +44,35 @@ describe('executeQueryPlan', () => { }; const parseOp = (operation: string, operationSchema?: Schema): Operation => { - return parseOperation((operationSchema ?? schema), operation); - } + return parseOperation(operationSchema ?? schema, operation); + }; - const buildPlan = (operation: string | Operation, operationQueryPlanner?: QueryPlanner, operationSchema?: Schema): QueryPlan => { - const op = typeof operation === 'string' ? parseOp(operation, operationSchema): operation; + const buildPlan = ( + operation: string | Operation, + operationQueryPlanner?: QueryPlanner, + operationSchema?: Schema, + ): QueryPlan => { + const op = + typeof operation === 'string' + ? parseOp(operation, operationSchema) + : operation; return (operationQueryPlanner ?? queryPlanner).buildQueryPlan(op); - } + }; async function executePlan( queryPlan: QueryPlan, operation: Operation, executeRequestContext?: GatewayGraphQLRequestContext, executeSchema?: Schema, - executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource } + executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource }, ): Promise { const supergraphSchema = executeSchema ?? schema; const apiSchema = supergraphSchema.toAPISchema(); const operationContext = buildOperationContext({ schema: apiSchema.toGraphQLJSSchema(), - operationDocument: gql`${operation.toString()}`, + operationDocument: gql` + ${operation.toString()} + `, }); return executeQueryPlan( queryPlan, @@ -66,10 +84,13 @@ describe('executeQueryPlan', () => { ); } - async function executeOperation(operationString: string, requestContext?: GatewayGraphQLRequestContext): Promise { - const operation = parseOp(operationString); - const queryPlan = buildPlan(operation); - return executePlan(queryPlan, operation, requestContext); + async function executeOperation( + operationString: string, + requestContext?: GatewayGraphQLRequestContext, + ): Promise { + const operation = parseOp(operationString); + const queryPlan = buildPlan(operation); + return executePlan(queryPlan, operation, requestContext); } function overrideResolversInService( @@ -155,7 +176,7 @@ describe('executeQueryPlan', () => { 'errors.0.message', 'Something went wrong', ); - expect(response).toHaveProperty('errors.0.path', ["me"]); + expect(response).toHaveProperty('errors.0.path', ['me']); expect(response).toHaveProperty( 'errors.0.extensions.code', 'UNAUTHENTICATED', @@ -1276,7 +1297,12 @@ describe('executeQueryPlan', () => { const operation = parseOp(`${getIntrospectionQuery()}`, schema); queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data).toHaveProperty('__schema'); expect(response.errors).toBeUndefined(); @@ -1310,7 +1336,12 @@ describe('executeQueryPlan', () => { queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data).toBeUndefined(); expect(response.errors).toMatchInlineSnapshot(` Array [ @@ -1390,7 +1421,12 @@ describe('executeQueryPlan', () => { queryPlanner = new QueryPlanner(supergraph); const queryPlan = queryPlanner.buildQueryPlan(operation); - const response = await executePlan(queryPlan, operation, undefined, schema); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + ); expect(response.data?.vehicle).toEqual(null); // This kind of error is only found by the post-processing of the gateway, and post-processing errors are currently not returned @@ -1425,24 +1461,24 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); addResolversToSchema(serviceMap['S1'].schema, { Query: { getA() { return { - getA: {} + getA: {}, }; }, }, A: { q() { return Object.create(null); - } - } + }, + }, }); addResolversToSchema(serviceMap['S2'].schema, { @@ -1453,7 +1489,8 @@ describe('executeQueryPlan', () => { }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { getA { q { @@ -1461,7 +1498,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1490,7 +1529,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { @@ -1501,66 +1546,69 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can query other subgraphs when the Query type is the type of a field after a mutation', async () => { - const s1 = gql` - type Query { - one: Int - } + const s1 = gql` + type Query { + one: Int + } - type Mutation { - mutateSomething: Query - } - `; + type Mutation { + mutateSomething: Query + } + `; - const s2 = gql` - type Query { - two: Int - } - `; + const s2 = gql` + type Query { + two: Int + } + `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ - { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } - ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + { name: 'S1', typeDefs: s1 }, + { name: 'S2', typeDefs: s2 }, + ]); - let hasMutated = false; + let hasMutated = false; - addResolversToSchema(serviceMap['S1'].schema, { - Query: { - one() { - return 1; - }, + addResolversToSchema(serviceMap['S1'].schema, { + Query: { + one() { + return 1; }, - Mutation: { - mutateSomething() { - hasMutated = true; - return {}; - }, - } - }); + }, + Mutation: { + mutateSomething() { + hasMutated = true; + return {}; + }, + }, + }); - addResolversToSchema(serviceMap['S2'].schema, { - Query: { - two() { - return 2; - }, + addResolversToSchema(serviceMap['S2'].schema, { + Query: { + two() { + return 2; }, - }); + }, + }); - const operation = parseOp(` + const operation = parseOp( + ` mutation { mutateSomething { one two } } - `, schema); + `, + schema, + ); - const queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S1") { @@ -1584,10 +1632,16 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); - expect(hasMutated).toBeTruthy(); - expect(response.data).toMatchInlineSnapshot(` + expect(hasMutated).toBeTruthy(); + expect(response.data).toMatchInlineSnapshot(` Object { "mutateSomething": Object { "one": 1, @@ -1595,7 +1649,7 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can mutate other subgraphs when the Mutation type is the type of a field', async () => { const s1 = gql` @@ -1618,9 +1672,9 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); let mutateOneCalled = false; @@ -1630,21 +1684,21 @@ describe('executeQueryPlan', () => { Query: { getA() { return { - getA: {} + getA: {}, }; }, }, A: { m() { return Object.create(null); - } + }, }, Mutation: { mutateOne() { mutateOneCalled = true; return 1; - } - } + }, + }, }); addResolversToSchema(serviceMap['S2'].schema, { @@ -1656,7 +1710,8 @@ describe('executeQueryPlan', () => { }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { getA { m { @@ -1665,7 +1720,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1695,7 +1752,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(mutateOneCalled).toBeTruthy(); expect(mutateTwoCalled).toBeTruthy(); expect(response.data).toMatchInlineSnapshot(` @@ -1708,57 +1771,58 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); it('can mutate other subgraphs when the Mutation type is the type of a field after a mutation', async () => { - const s1 = gql` - type Query { - one: Int - } + const s1 = gql` + type Query { + one: Int + } - type Mutation { - mutateSomething: Mutation - } - `; + type Mutation { + mutateSomething: Mutation + } + `; - const s2 = gql` - type Mutation { - mutateTwo: Int - } - `; + const s2 = gql` + type Mutation { + mutateTwo: Int + } + `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ - { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } - ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + { name: 'S1', typeDefs: s1 }, + { name: 'S2', typeDefs: s2 }, + ]); - let somethingMutationCount = 0; - let hasMutatedTwo = false; + let somethingMutationCount = 0; + let hasMutatedTwo = false; - addResolversToSchema(serviceMap['S1'].schema, { - Query: { - one() { - return 1; - }, + addResolversToSchema(serviceMap['S1'].schema, { + Query: { + one() { + return 1; }, - Mutation: { - mutateSomething() { - ++somethingMutationCount; - return {}; - }, - } - }); + }, + Mutation: { + mutateSomething() { + ++somethingMutationCount; + return {}; + }, + }, + }); - addResolversToSchema(serviceMap['S2'].schema, { - Mutation: { - mutateTwo() { - hasMutatedTwo = true; - return 2; - }, + addResolversToSchema(serviceMap['S2'].schema, { + Mutation: { + mutateTwo() { + hasMutatedTwo = true; + return 2; }, - }); + }, + }); - const operation = parseOp(` + const operation = parseOp( + ` mutation { mutateSomething { mutateSomething { @@ -1766,11 +1830,13 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); - const queryPlan = buildPlan(operation, queryPlanner); + const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S1") { @@ -1795,11 +1861,17 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); - expect(somethingMutationCount).toBe(2); - expect(hasMutatedTwo).toBeTruthy(); - expect(response.data).toMatchInlineSnapshot(` + expect(somethingMutationCount).toBe(2); + expect(hasMutatedTwo).toBeTruthy(); + expect(response.data).toMatchInlineSnapshot(` Object { "mutateSomething": Object { "mutateSomething": Object { @@ -1808,7 +1880,7 @@ describe('executeQueryPlan', () => { }, } `); - }) + }); }); describe('interfaces on interfaces', () => { @@ -1847,7 +1919,8 @@ describe('executeQueryPlan', () => { c: String } - type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") { + type T4 implements SubInterface1 & SubInterface2 & TopInterface + @key(fields: "a") { a: Int b: String @external c: String @external @@ -1867,24 +1940,34 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); - const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}]; - const t2s_s1: any[] = [{__typename: 'T2', b: 'k1'}, {__typename: 'T2', b: 'k2'}]; - const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}]; - const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}]; + const t1s_s1: any[] = [ + { __typename: 'T1', a: 1, b: 'T1_v1' }, + { __typename: 'T1', a: 2, b: 'T1_v2' }, + ]; + const t2s_s1: any[] = [ + { __typename: 'T2', b: 'k1' }, + { __typename: 'T2', b: 'k2' }, + ]; + const t3s_s1: any[] = [{ __typename: 'T3', a: 42, c: 'T3_v1' }]; + const t4s_s1: any[] = [ + { __typename: 'T4', a: 0 }, + { __typename: 'T4', a: 10 }, + { __typename: 'T4', a: 20 }, + ]; - const t2s_s2 = new Map(); - t2s_s2.set('k1', {a: 12 , b: 'k1'}); - t2s_s2.set('k2', {a: 24 , b: 'k2'}); + const t2s_s2 = new Map(); + t2s_s2.set('k1', { a: 12, b: 'k1' }); + t2s_s2.set('k2', { a: 24, b: 'k2' }); - const t4s_s2 = new Map(); - t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'}); - t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'}); - t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'}); + const t4s_s2 = new Map(); + t4s_s2.set(0, { a: 0, b: 'b_0', c: 'c_0' }); + t4s_s2.set(10, { a: 10, b: 'b_10', c: 'c_10' }); + t4s_s2.set(20, { a: 20, b: 'b_20', c: 'c_20' }); addResolversToSchema(serviceMap['S1'].schema, { Query: { @@ -1898,16 +1981,17 @@ describe('executeQueryPlan', () => { T2: { __resolveReference(ref) { return t2s_s2.get(ref.b); - } + }, }, T4: { __resolveReference(ref) { return t4s_s2.get(ref.a); - } + }, }, }); - const operation = parseOp(` + const operation = parseOp( + ` query { allValues { a @@ -1919,7 +2003,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); @@ -1976,7 +2062,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { @@ -2058,7 +2150,8 @@ describe('executeQueryPlan', () => { c: String } - type T4 implements SubInterface1 & SubInterface2 & TopInterface @key(fields: "a") { + type T4 implements SubInterface1 & SubInterface2 & TopInterface + @key(fields: "a") { a: Int b: String @external c: String @external @@ -2078,24 +2171,34 @@ describe('executeQueryPlan', () => { } `; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ { name: 'S1', typeDefs: s1 }, - { name: 'S2', typeDefs: s2 } + { name: 'S2', typeDefs: s2 }, ]); - const t1s_s1: any[] = [{ __typename: 'T1', a: 1, b: 'T1_v1'}, {__typename: 'T1', a: 2, b: 'T1_v2'}]; - const t2s_s1: any[] = [{__typename: 'T2', a: 12}, {__typename: 'T2', a: 24}]; - const t3s_s1: any[] = [{__typename: 'T3', a: 42, c: 'T3_v1'}]; - const t4s_s1: any[] = [{__typename: 'T4', a: 0}, {__typename: 'T4', a: 10}, {__typename: 'T4', a: 20}]; + const t1s_s1: any[] = [ + { __typename: 'T1', a: 1, b: 'T1_v1' }, + { __typename: 'T1', a: 2, b: 'T1_v2' }, + ]; + const t2s_s1: any[] = [ + { __typename: 'T2', a: 12 }, + { __typename: 'T2', a: 24 }, + ]; + const t3s_s1: any[] = [{ __typename: 'T3', a: 42, c: 'T3_v1' }]; + const t4s_s1: any[] = [ + { __typename: 'T4', a: 0 }, + { __typename: 'T4', a: 10 }, + { __typename: 'T4', a: 20 }, + ]; - const t2s_s2 = new Map(); - t2s_s2.set(12, {a: 12 , b: 'k1'}); - t2s_s2.set(24, {a: 24 , b: 'k2'}); + const t2s_s2 = new Map(); + t2s_s2.set(12, { a: 12, b: 'k1' }); + t2s_s2.set(24, { a: 24, b: 'k2' }); - const t4s_s2 = new Map(); - t4s_s2.set(0, {a: 0, b: 'b_0', c: 'c_0'}); - t4s_s2.set(10, {a: 10, b: 'b_10', c: 'c_10'}); - t4s_s2.set(20, {a: 20, b: 'b_20', c: 'c_20'}); + const t4s_s2 = new Map(); + t4s_s2.set(0, { a: 0, b: 'b_0', c: 'c_0' }); + t4s_s2.set(10, { a: 10, b: 'b_10', c: 'c_10' }); + t4s_s2.set(20, { a: 20, b: 'b_20', c: 'c_20' }); addResolversToSchema(serviceMap['S1'].schema, { Query: { @@ -2109,22 +2212,25 @@ describe('executeQueryPlan', () => { T2: { __resolveReference(ref) { return t2s_s2.get(ref.b); - } + }, }, T4: { __resolveReference(ref) { return t4s_s2.get(ref.a); - } + }, }, }); - let operation = parseOp(` + let operation = parseOp( + ` query { allValues { a } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); @@ -2141,7 +2247,13 @@ describe('executeQueryPlan', () => { } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allValues": Array [ @@ -2173,7 +2285,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` query { allValues { ... on SubInterface1 { @@ -2181,7 +2294,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); @@ -2209,7 +2324,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allValues": Array [ @@ -2261,8 +2382,8 @@ describe('executeQueryPlan', () => { type MyTypeB implements MyInterface { name: String } - ` - } + `, + }; const s2 = { name: 'S2', @@ -2274,15 +2395,18 @@ describe('executeQueryPlan', () => { type MyTypeC implements MyInterface { name: String } - ` - } + `, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); addResolversToSchema(serviceMap['S1'].schema, { Query: { myField() { - return { __typename: 'MyTypeA', name: "foo" }; + return { __typename: 'MyTypeA', name: 'foo' }; }, }, }); @@ -2290,13 +2414,16 @@ describe('executeQueryPlan', () => { // First, we just query the field without conditions. // Note that there is no reason to type-explode this: clearly, `myField` will never return a `MyTypeC` since // it's resolved by S1 which doesn't know that type, but that doesn't impact the plan. - let operation = parseOp(` + let operation = parseOp( + ` query { myField { name } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2310,7 +2437,13 @@ describe('executeQueryPlan', () => { }, } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object { @@ -2321,7 +2454,8 @@ describe('executeQueryPlan', () => { // Now forcing the query planning to notice that `MyTypeC` can never happen and making // sure it doesn't ask it from S1, which doesn't know it. - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeA { @@ -2332,7 +2466,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2349,7 +2485,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object { @@ -2358,10 +2500,10 @@ describe('executeQueryPlan', () => { } `); - // Testing only getting name for `MyTypeB`, which is known by S1, but not returned // by `myField` in practice (so the result is "empty"). - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeB { @@ -2369,7 +2511,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -2386,14 +2530,21 @@ describe('executeQueryPlan', () => { }, } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object {}, } `); - operation = parseOp(` + operation = parseOp( + ` query { myField { ... on MyTypeC { @@ -2401,7 +2552,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); // Lastly, same with only getting name for `MyTypeC`. It isn't known by S1 so the condition should not // be included in the query, but we should still query `myField` to know if it resolve to "something" @@ -2421,7 +2574,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "myField": Object {}, @@ -2432,9 +2591,9 @@ describe('executeQueryPlan', () => { describe('@requires', () => { test('handles null in required field correctly (with nullable fields)', async () => { const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; const s1 = { @@ -2447,12 +2606,12 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: {id: number}) { + __resolveReference(ref: { id: number }) { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2474,7 +2633,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2484,14 +2643,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2501,7 +2664,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2550,7 +2715,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -2581,9 +2752,9 @@ describe('executeQueryPlan', () => { test('handles null in required field correctly (with @require field non-nullable)', async () => { const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; const s1 = { @@ -2600,8 +2771,8 @@ describe('executeQueryPlan', () => { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2623,7 +2794,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2633,14 +2804,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2650,7 +2825,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2700,7 +2877,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -2725,7 +2908,9 @@ describe('executeQueryPlan', () => { `); // We returning `null` for f2 which isn't nullable, so it bubbled up and we should have an error - expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f2.']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'Cannot return null for non-nullable field T1.f2.', + ]); }); test('handles null in required field correctly (with non-nullable required field)', async () => { @@ -2739,12 +2924,12 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: { id: number}) { + __resolveReference(ref: { id: number }) { return s1_data[ref.id]; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2766,7 +2951,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2776,20 +2961,24 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); const s1_data = [ - { id: 0, f1: "foo" }, + { id: 0, f1: 'foo' }, { id: 1, f1: null }, - { id: 2, f1: "bar" }, + { id: 2, f1: 'bar' }, ]; - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2799,7 +2988,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2849,7 +3040,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -2872,7 +3069,9 @@ describe('executeQueryPlan', () => { ], } `); - expect(response.errors?.map((e) => e.message)).toStrictEqual(['Cannot return null for non-nullable field T1.f1.']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'Cannot return null for non-nullable field T1.f1.', + ]); }); test('handles errors in required field correctly (with nullable fields)', async () => { @@ -2891,15 +3090,19 @@ describe('executeQueryPlan', () => { }, f1(o: { id: number }) { switch (o.id) { - case 0: return "foo"; - case 1: return [ "invalid" ]; // This will effectively throw - case 2: return "bar"; - default: throw new Error('Not handled'); + case 0: + return 'foo'; + case 1: + return ['invalid']; // This will effectively throw + case 2: + return 'bar'; + default: + throw new Error('Not handled'); } - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -2921,7 +3124,7 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: 0}, {id: 1}, {id: 2}]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; }, }, T1: { @@ -2931,14 +3134,18 @@ describe('executeQueryPlan', () => { }, f2(o: { f1: string }) { return o.f1 === null ? null : { a: `t1:${o.f1}` }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id @@ -2948,7 +3155,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -2997,7 +3206,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -3023,7 +3238,9 @@ describe('executeQueryPlan', () => { ], } `); - expect(response.errors?.map((e) => e.message)).toStrictEqual(['String cannot represent value: ["invalid"]']); + expect(response.errors?.map((e) => e.message)).toStrictEqual([ + 'String cannot represent value: ["invalid"]', + ]); }); test('ensures type condition on inaccessible type in @require works correctly', async () => { @@ -3031,7 +3248,10 @@ describe('executeQueryPlan', () => { name: 'data', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: ["@key", "@shareable"] + ) type Entity @key(fields: "id") { id: ID! @@ -3053,29 +3273,28 @@ describe('executeQueryPlan', () => { } `, resolvers: { - Query: { - dummy() { - return {}; - }, + Query: { + dummy() { + return {}; }, - Entity: { - __resolveReference() { - return {}; - }, - id() { - return "id"; - }, - data() { - return { - __typename: "Data", - foo: "foo", - bar: "bar", - }; - }, + }, + Entity: { + __resolveReference() { + return {}; }, - - } - } + id() { + return 'id'; + }, + data() { + return { + __typename: 'Data', + foo: 'foo', + bar: 'bar', + }; + }, + }, + }, + }; let requirerRepresentation: any = undefined; @@ -3083,10 +3302,16 @@ describe('executeQueryPlan', () => { name: 'requirer', typeDefs: gql` extend schema - @link( - url: "https://specs.apollo.dev/federation/v2.0", - import: ["@key", "@shareable", "@external", "@requires", "@inaccessible"] - ) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [ + "@key" + "@shareable" + "@external" + "@requires" + "@inaccessible" + ] + ) type Query { dummy: Entity @@ -3095,7 +3320,8 @@ describe('executeQueryPlan', () => { type Entity @key(fields: "id") { id: ID! data: Foo @external - requirer: String! @requires(fields: "data { foo ... on Bar { bar } }") + requirer: String! + @requires(fields: "data { foo ... on Bar { bar } }") } interface Foo { @@ -3124,24 +3350,30 @@ describe('executeQueryPlan', () => { return {}; }, id() { - return "id"; + return 'id'; }, requirer() { - return "requirer"; + return 'requirer'; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { dummy { requirer } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3202,7 +3434,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "dummy": Object { @@ -3259,14 +3497,16 @@ describe('executeQueryPlan', () => { }, One: { __resolveReference(ref: { id: string }) { - return ref.id === entityOne.id ? { ...entityOne, ...ref } : undefined; + return ref.id === entityOne.id + ? { ...entityOne, ...ref } + : undefined; }, computed(parent: any) { return `computed value: ${parent.two.external}`; }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -3282,18 +3522,24 @@ describe('executeQueryPlan', () => { return ref.id === entityTwo.id ? entityTwo : undefined; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { one { computed } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3346,7 +3592,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3406,40 +3658,43 @@ describe('executeQueryPlan', () => { defaultValue: 24, valuePassed: 42, }, - ])('requires on field with argument: $name', async ({ - argNullable, - defaultValue, - valuePassed, - }: { - argNullable: boolean, - defaultValue?: number, - valuePassed?: number, - }) => { - - const argType = `Int${argNullable ? '' : '!'}${defaultValue ? ` = ${defaultValue}` : ''}`; - const s1 = { - name: 'S1', - typeDefs: gql` + ])( + 'requires on field with argument: $name', + async ({ + argNullable, + defaultValue, + valuePassed, + }: { + argNullable: boolean; + defaultValue?: number; + valuePassed?: number; + }) => { + const argType = `Int${argNullable ? '' : '!'}${ + defaultValue ? ` = ${defaultValue}` : '' + }`; + const s1 = { + name: 'S1', + typeDefs: gql` type T @key(fields: "id") { id: Int! x(opt: ${argType}): String! } `, - resolvers: { - T: { - __resolveReference(ref: { id: number}) { - return ref; + resolvers: { + T: { + __resolveReference(ref: { id: number }) { + return ref; + }, + x(_: any, args: any) { + return `args: ${JSON.stringify(args)}`; + }, }, - x(_: any, args: any) { - return `args: ${JSON.stringify(args)}`; - } }, - } - } + }; - const s2 = { - name: 'S2', - typeDefs: gql` + const s2 = { + name: 'S2', + typeDefs: gql` type Query { t: T } @@ -3447,49 +3702,57 @@ describe('executeQueryPlan', () => { type T @key(fields: "id") { id: Int! x(opt: ${argType}): String! @external - y: String @requires(fields: "x${valuePassed ? `(opt: ${valuePassed})` : ''}") + y: String @requires(fields: "x${ + valuePassed ? `(opt: ${valuePassed})` : '' + }") } `, - resolvers: { - Query: { - t() { - return {id: 0}; + resolvers: { + Query: { + t() { + return { id: 0 }; + }, }, - }, - T: { - __resolveReference(ref: { id: number }) { - // the ref has already the id and f1 is a require is triggered, and we resolve f2 below - return ref; + T: { + __resolveReference(ref: { id: number }) { + // the ref has already the id and f1 is a require is triggered, and we resolve f2 below + return ref; + }, + y(parent: any) { + return `x: ${parent.x}`; + }, }, - y(parent: any) { - return `x: ${parent.x}`; - } - } - } - } + }, + }; - if (!argNullable && !defaultValue && !valuePassed) { - // We test all combination of `argNullable`, `defaultValue` set/unset and `valuePassed` set/unset, and all should be allowed - // except if the value is non-nullable and has neither a default nor a value passed. In that case, just ensure the error message - // is meaningful. - expect(() => getFederatedTestingSchema([ s1, s2 ])).toThrowError( - '[S2] On field "T.y", for @requires(fields: "x"): Missing mandatory value for argument "opt" of field "T.x" in selection "x"' - ); - return; - } + if (!argNullable && !defaultValue && !valuePassed) { + // We test all combination of `argNullable`, `defaultValue` set/unset and `valuePassed` set/unset, and all should be allowed + // except if the value is non-nullable and has neither a default nor a value passed. In that case, just ensure the error message + // is meaningful. + expect(() => getFederatedTestingSchema([s1, s2])).toThrowError( + '[S2] On field "T.y", for @requires(fields: "x"): Missing mandatory value for argument "opt" of field "T.x" in selection "x"', + ); + return; + } - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { t { y } } - `, schema); - const queryPlan = buildPlan(operation, queryPlanner); - expect(queryPlan).toMatchInlineSnapshot(` + `, + schema, + ); + const queryPlan = buildPlan(operation, queryPlanner); + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "S2") { @@ -3535,28 +3798,41 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); - expect(response.errors).toBeUndefined(); - expect(response.data).toMatchInlineSnapshot(` + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); + expect(response.errors).toBeUndefined(); + expect(response.data).toMatchInlineSnapshot(` Object { "t": Object { - "y": "x: args: {${valuePassed ? `\\"opt\\":${valuePassed}` : (defaultValue ? `\\"opt\\":${defaultValue}` : '')}}", + "y": "x: args: {${ + valuePassed + ? `\\"opt\\":${valuePassed}` + : defaultValue + ? `\\"opt\\":${defaultValue}` + : '' + }}", }, } `); - }); + }, + ); test('requires with nested field example from #2683 is fixed', async () => { const parentItems = [ - { "__typename": "ParentItem", "id": '1', "name": "Parent Item #1" }, - { "__typename": "ParentItem", "id": '2', "name": "Parent Item #2" }, - { "__typename": "ParentItem", "id": '3', "name": "Parent Item #3" }, + { __typename: 'ParentItem', id: '1', name: 'Parent Item #1' }, + { __typename: 'ParentItem', id: '2', name: 'Parent Item #2' }, + { __typename: 'ParentItem', id: '3', name: 'Parent Item #3' }, ]; const childItems = [ - { "__typename": "ChildItem", "id": '1', "name": "Child Item #1" }, - { "__typename": "ChildItem", "id": '2', "name": "Child Item #2" }, - { "__typename": "ChildItem", "id": '3', "name": "Child Item #3" }, + { __typename: 'ChildItem', id: '1', name: 'Child Item #1' }, + { __typename: 'ChildItem', id: '2', name: 'Child Item #2' }, + { __typename: 'ChildItem', id: '3', name: 'Child Item #3' }, ]; const s1 = { @@ -3579,11 +3855,11 @@ describe('executeQueryPlan', () => { }, ParentItem: { __resolveReference(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); + return parentItems.find(({ id }) => ref.id === id); }, - } - } - } + }, + }, + }; const s2 = { name: 'S2', @@ -3602,22 +3878,22 @@ describe('executeQueryPlan', () => { resolvers: { ChildItem: { __resolveReference(ref: { id: string }) { - return childItems.find(({id}) => ref.id === id); + return childItems.find(({ id }) => ref.id === id); }, parentItem(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); - } + return parentItems.find(({ id }) => ref.id === id); + }, }, ParentItem: { __resolveReference(ref: { id: string }) { - return parentItems.find(({id}) => ref.id === id); + return parentItems.find(({ id }) => ref.id === id); }, childItems(ref: { id: string }) { - return [childItems.find(({id}) => ref.id === id)]; - } + return [childItems.find(({ id }) => ref.id === id)]; + }, }, - } - } + }, + }; const s3 = { name: 'S3', @@ -3635,18 +3911,27 @@ describe('executeQueryPlan', () => { `, resolvers: { ChildItem: { - __resolveReference(ref: { id: string, name: string, parentItem: { name: string }}) { + __resolveReference(ref: { + id: string; + name: string; + parentItem: { name: string }; + }) { return { ...ref, message: `${ref.parentItem.name} | ${ref.name}`, }; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); - let operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + s3, + ]); + let operation = parseOp( + ` query { parentItems { childItems { @@ -3654,7 +3939,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3728,7 +4015,13 @@ describe('executeQueryPlan', () => { }, } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3758,7 +4051,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` query { parentItems { childItems { @@ -3770,7 +4064,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -3867,7 +4163,13 @@ describe('executeQueryPlan', () => { }, } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -3914,9 +4216,9 @@ describe('executeQueryPlan', () => { describe('@key', () => { test('Works on a list of scalar', async () => { const s1_data = [ - { id: [0, 1], f1: "foo" }, - { id: [2, 3], f1: "bar" }, - { id: [4, 5], f1: "baz" }, + { id: [0, 1], f1: 'foo' }, + { id: [2, 3], f1: 'bar' }, + { id: [4, 5], f1: 'baz' }, ]; const s1 = { @@ -3933,8 +4235,8 @@ describe('executeQueryPlan', () => { return s1_data.find((e) => arrayEquals(e.id, ref.id)); }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -3950,22 +4252,28 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{id: [2, 3]}, {id: [4, 5]}]; + return [{ id: [2, 3] }, { id: [4, 5] }]; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { id f1 } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3997,7 +4305,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "getT1s": Array [ @@ -4022,9 +4336,21 @@ describe('executeQueryPlan', () => { test('Works on a list of objects', async () => { const s1_data = [ - { o: [{a: 0, b: "b0", c: "zero"}, {a: 1, b: "b1", c: "one"}], f1: "foo" }, - { o: [{a: 2, b: "b2", c: "two"}], f1: "bar" }, - { o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}], f1: "baz" }, + { + o: [ + { a: 0, b: 'b0', c: 'zero' }, + { a: 1, b: 'b1', c: 'one' }, + ], + f1: 'foo', + }, + { o: [{ a: 2, b: 'b2', c: 'two' }], f1: 'bar' }, + { + o: [ + { a: 3, b: 'b3', c: 'three' }, + { a: 4, b: 'b4', c: 'four' }, + ], + f1: 'baz', + }, ]; const s1 = { @@ -4043,12 +4369,14 @@ describe('executeQueryPlan', () => { `, resolvers: { T1: { - __resolveReference(ref: { o: {a : number, c: string}[] }) { - return s1_data.find((e) => arrayEquals(e.o, ref.o, (x, y) => x.a === y.a && x.c === y.c)); + __resolveReference(ref: { o: { a: number; c: string }[] }) { + return s1_data.find((e) => + arrayEquals(e.o, ref.o, (x, y) => x.a === y.a && x.c === y.c), + ); }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -4070,15 +4398,27 @@ describe('executeQueryPlan', () => { resolvers: { Query: { getT1s() { - return [{o: [{a: 2, b: "b2", c: "two"}]}, {o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}]}]; + return [ + { o: [{ a: 2, b: 'b2', c: 'two' }] }, + { + o: [ + { a: 3, b: 'b3', c: 'three' }, + { a: 4, b: 'b4', c: 'four' }, + ], + }, + ]; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { getT1s { o { @@ -4089,7 +4429,9 @@ describe('executeQueryPlan', () => { f1 } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -4128,7 +4470,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response. expect(response.data).toMatchInlineSnapshot(` Object { @@ -4170,39 +4518,42 @@ describe('executeQueryPlan', () => { }: { s1?: { // additional resolvers for interface I - iResolversExtra?: any, + iResolversExtra?: any; // provide a default __resolveReference for the interface - hasIResolveReference?: boolean, + hasIResolveReference?: boolean; // turn an id into extra data returned by __resolveReference (if hasIResolveReference is true) - iResolveReferenceExtra?: (id: string) => { [k: string]: any }, + iResolveReferenceExtra?: (id: string) => { [k: string]: any }; // additional resolvers for type A - aResolversExtra?: any, + aResolversExtra?: any; // additional resolvers for type B - bResolversExtra?: any, - } + bResolversExtra?: any; + }; }) => { - // The example uses 2 entities: // - one of type A with id='idA' (x=1, y=2, z=3) // - one of type B with id='idB' (x=10, y=20, w=30) - const s1IBaseResolvers = (s1?.hasIResolveReference ?? true) - ? { - __resolveReference(ref: { id: string }) { - const extraFct = s1?.iResolveReferenceExtra; - const extraData = extraFct ? extraFct(ref.id) : {}; - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3, ...extraData } - : { id: ref.id, x: 10, w: 30, ...extraData }; - } - } - : {}; + const s1IBaseResolvers = + s1?.hasIResolveReference ?? true + ? { + __resolveReference(ref: { id: string }) { + const extraFct = s1?.iResolveReferenceExtra; + const extraData = extraFct ? extraFct(ref.id) : {}; + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3, ...extraData } + : { id: ref.id, x: 10, w: 30, ...extraData }; + }, + } + : {}; const subgraph1 = { name: 'S1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key"] + ) type Query { iFromS1: I @@ -4229,7 +4580,7 @@ describe('executeQueryPlan', () => { Query: { iFromS1() { return { __typename: 'A', id: 'idA' }; - } + }, }, I: { ...s1IBaseResolvers, @@ -4241,14 +4592,17 @@ describe('executeQueryPlan', () => { B: { ...(s1?.bResolversExtra ?? {}), }, - } - } + }, + }; const subgraph2 = { name: 'S2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) type Query { iFromS2: I @@ -4267,28 +4621,38 @@ describe('executeQueryPlan', () => { id: 'idB', y: 20, }; - } + }, }, I: { __resolveReference(ref: { id: string }) { return { id: ref.id, y: ref.id === 'idA' ? 2 : 20, - } + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ subgraph1, subgraph2 ]); - return async (op: string): Promise<{ plan: QueryPlan, response: GatewayExecutionResult }> => { + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + subgraph1, + subgraph2, + ]); + return async ( + op: string, + ): Promise<{ plan: QueryPlan; response: GatewayExecutionResult }> => { const operation = parseOp(op, schema); const plan = buildPlan(operation, queryPlanner); - const response = await executePlan(plan, operation, undefined, schema, serviceMap); + const response = await executePlan( + plan, + operation, + undefined, + schema, + serviceMap, + ); return { plan, response }; }; - } - + }; test('handles __typename rewriting when using @key to @interfaceObject', async () => { // We don't need extra resolving from S1 in this case. @@ -4448,135 +4812,144 @@ describe('executeQueryPlan', () => { `); }); - test.each([{ - name: 'with manual __typename', - s1: { - iResolveReferenceExtra: (id: string) => ({ __typename: id === 'idA' ? 'A' : 'B' }), - }, - }, { - name: 'with __resolveType', - s1: { - iResolversExtra: { - __resolveType(ref: { id: string }) { - return ref.id === 'idA' ? 'A' : 'B'; - } + test.each([ + { + name: 'with manual __typename', + s1: { + iResolveReferenceExtra: (id: string) => ({ + __typename: id === 'idA' ? 'A' : 'B', + }), }, }, - }, { - name: 'with isTypeOf', - s1: { - aResolversExtra: { - __isTypeOf(ref: { id: string }) { - return ref.id === 'idA'; - } - }, - bResolversExtra: { - __isTypeOf(ref: { id: string }) { - // Same remark as above. - return ref.id === 'idB'; - } + { + name: 'with __resolveType', + s1: { + iResolversExtra: { + __resolveType(ref: { id: string }) { + return ref.id === 'idA' ? 'A' : 'B'; + }, + }, }, }, - }, { - name: 'with only a __resolveType on the interface but per-runtime-types __resolveReference', - s1: { - hasIResolveReference: false, - iResolversExtra: { - __resolveType(ref: { id: string }) { - return ref.id === 'idA' ? 'A' : 'B'; - } - }, - aResolversExtra: { - __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : undefined; - } - }, - bResolversExtra: { - __resolveReference(ref: { id: string }) { - return ref.id === 'idB' - ? { id: ref.id, x: 10, w: 30 } - : undefined; - } + { + name: 'with isTypeOf', + s1: { + aResolversExtra: { + __isTypeOf(ref: { id: string }) { + return ref.id === 'idA'; + }, + }, + bResolversExtra: { + __isTypeOf(ref: { id: string }) { + // Same remark as above. + return ref.id === 'idB'; + }, + }, }, }, - }, { - name: 'errors when nothing provides the runtime type', - expectedErrors: [ - 'Abstract type "I" `__resolveReference` method must resolve to an Object type at runtime. ' - + 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, ' - + 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.' - ], - }, { - name: 'with an async __resolveReference and a non-default __resolveType', - s1: { - iResolversExtra: { - async __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; - }, - __resolveType(ref: { id: string }) { - switch (ref.id) { - case 'idA': - return 'A'; - case 'idB': - return 'B'; - default: - throw new Error('Unknown type: ' + ref.id); - } + { + name: 'with only a __resolveType on the interface but per-runtime-types __resolveReference', + s1: { + hasIResolveReference: false, + iResolversExtra: { + __resolveType(ref: { id: string }) { + return ref.id === 'idA' ? 'A' : 'B'; + }, + }, + aResolversExtra: { + __resolveReference(ref: { id: string }) { + return ref.id === 'idA' ? { id: ref.id, x: 1, z: 3 } : undefined; + }, + }, + bResolversExtra: { + __resolveReference(ref: { id: string }) { + return ref.id === 'idB' + ? { id: ref.id, x: 10, w: 30 } + : undefined; + }, }, }, }, - }, { - name: 'with an async __resolveReference and a non-default async __resolveType', - s1: { - iResolversExtra: { - async __resolveReference(ref: { id: string }) { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; - }, - async __resolveType(ref: { id: string }) { - switch (ref.id) { - case 'idA': - return 'A'; - case 'idB': - return 'B'; - default: - throw new Error('Unknown type: ' + ref.id); - } + { + name: 'errors when nothing provides the runtime type', + expectedErrors: [ + 'Abstract type "I" `__resolveReference` method must resolve to an Object type at runtime. ' + + 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, ' + + 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + ], + }, + { + name: 'with an async __resolveReference and a non-default __resolveType', + s1: { + iResolversExtra: { + async __resolveReference(ref: { id: string }) { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, + __resolveType(ref: { id: string }) { + switch (ref.id) { + case 'idA': + return 'A'; + case 'idB': + return 'B'; + default: + throw new Error('Unknown type: ' + ref.id); + } + }, }, }, }, - }, { - name: 'with an async __resolveReference and async __isTypeOf on implementations', - s1: { - iResolversExtra: { - async __resolveReference(ref: { - id: string; - // I don't understand the TypeScript error that occurs when the - // return type is removed here (like all the others); it surfaces - // because `aResolversExtra` is defined, which I can't explain. - }): Promise> { - return ref.id === 'idA' - ? { id: ref.id, x: 1, z: 3 } - : { id: ref.id, x: 10, w: 30 }; + { + name: 'with an async __resolveReference and a non-default async __resolveType', + s1: { + iResolversExtra: { + async __resolveReference(ref: { id: string }) { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, + async __resolveType(ref: { id: string }) { + switch (ref.id) { + case 'idA': + return 'A'; + case 'idB': + return 'B'; + default: + throw new Error('Unknown type: ' + ref.id); + } + }, }, }, - aResolversExtra: { - async __isTypeOf(ref: { id: string }) { - return ref.id === 'idA'; + }, + { + name: 'with an async __resolveReference and async __isTypeOf on implementations', + s1: { + iResolversExtra: { + async __resolveReference(ref: { + id: string; + // I don't understand the TypeScript error that occurs when the + // return type is removed here (like all the others); it surfaces + // because `aResolversExtra` is defined, which I can't explain. + }): Promise> { + return ref.id === 'idA' + ? { id: ref.id, x: 1, z: 3 } + : { id: ref.id, x: 10, w: 30 }; + }, }, - }, - bResolversExtra: { - async __isTypeOf(ref: { id: string }) { - return ref.id === 'idB'; + aResolversExtra: { + async __isTypeOf(ref: { id: string }) { + return ref.id === 'idA'; + }, + }, + bResolversExtra: { + async __isTypeOf(ref: { id: string }) { + return ref.id === 'idB'; + }, }, }, }, - }])('resolving an interface @key $name', async ({ s1, expectedErrors }) => { + ])('resolving an interface @key $name', async ({ s1, expectedErrors }) => { const tester = defineSchema({ s1 }); const { plan, response } = await tester(` @@ -4692,7 +5065,11 @@ describe('executeQueryPlan', () => { test('handles __typename rewriting after forced resolution of implementation type', async () => { const tester = defineSchema({ - s1: { iResolveReferenceExtra: (id: string) => ({ __typename: id === 'idA' ? 'A' : 'B' }), }, + s1: { + iResolveReferenceExtra: (id: string) => ({ + __typename: id === 'idA' ? 'A' : 'B', + }), + }, }); const { plan, response } = await tester(` @@ -4763,17 +5140,17 @@ describe('executeQueryPlan', () => { test('handles querying fields of an implementation type coming from an @interfaceObject subgraph', async () => { const products = [ { - id: "1", - title: "Jane Eyre", + id: '1', + title: 'Jane Eyre', price: 12.99, - author: "Charlotte Bronte", - ISBN: "9780743273565", + author: 'Charlotte Bronte', + ISBN: '9780743273565', }, { - id: "2", - title: "Good Will Hunting", + id: '2', + title: 'Good Will Hunting', price: 14.99, - director: "Gus Van Sant", + director: 'Gus Van Sant', duration: 126, }, ]; @@ -4782,7 +5159,10 @@ describe('executeQueryPlan', () => { name: 'products', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key"] + ) type Query { products: [Product!]! @@ -4813,9 +5193,9 @@ describe('executeQueryPlan', () => { Product: { __resolveType(product: any) { if (product.author) { - return "Book"; + return 'Book'; } else if (product.director) { - return "Movie"; + return 'Movie'; } else { return null; } @@ -4824,14 +5204,17 @@ describe('executeQueryPlan', () => { return products.find((obj) => obj.id === reference.id); }, }, - } - } + }, + }; const s2 = { name: 'reviews', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: ["@key", "@interfaceObject"] + ) type Query { allReviewedProducts: [Product!]! @@ -4851,13 +5234,17 @@ describe('executeQueryPlan', () => { resolvers: { Query: { allReviewedProducts: () => products, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - let operation = parseOp(` + let operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4865,7 +5252,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); // We're going check again with almost the query but requesting the `id` field. And the @@ -4904,7 +5293,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // Note that the 2nd product is a Movie, so we should get an empty object expect(response.data).toMatchInlineSnapshot(` Object { @@ -4917,7 +5312,8 @@ describe('executeQueryPlan', () => { } `); - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4926,13 +5322,21 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // As said above, we should get the same plan as the previous time. expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); // But now we should have the "id" of the book (and still nothing for the movie). expect(response.data).toMatchInlineSnapshot(` Object { @@ -4947,7 +5351,8 @@ describe('executeQueryPlan', () => { `); // Now with __typename just for the book - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { ... on Book { @@ -4956,7 +5361,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // The plan is almost the exact same as the previous one, but in this case we do end up asking for __typename @@ -4996,7 +5403,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allReviewedProducts": Array [ @@ -5010,7 +5423,8 @@ describe('executeQueryPlan', () => { `); // And lastly with __typename but for all products - operation = parseOp(` + operation = parseOp( + ` { allReviewedProducts { __typename @@ -5019,13 +5433,21 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); // As said above, we should get the same plan as the previous time. expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "allReviewedProducts": Array [ @@ -5046,7 +5468,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { ts: [T!]! @@ -5062,16 +5487,19 @@ describe('executeQueryPlan', () => { `, resolvers: { Query: { - ts: () => [ { id: '2' }, { id: '4' }, { id: '1' } ] + ts: () => [{ id: '2' }, { id: '4' }, { id: '1' }], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5083,22 +5511,28 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `id=${ref.id}` + v: `id=${ref.id}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { ts { v } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` @@ -5132,7 +5566,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5155,7 +5595,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { ts: [T!]! @@ -5167,16 +5610,19 @@ describe('executeQueryPlan', () => { `, resolvers: { Query: { - ts: () => [ { id: '2' }, { id: '4' }, { id: '1' } ] + ts: () => [{ id: '2' }, { id: '4' }, { id: '1' }], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject"] + ) interface I @key(fields: "id") { id: ID! @@ -5187,27 +5633,29 @@ describe('executeQueryPlan', () => { id: ID! required: String } - `, resolvers: { I: { __resolveReference(ref: any) { return [ - { id: '1', __typename: "T", required: "r1" }, - { id: '2', __typename: "T", required: "r2" }, - { id: '3', __typename: "T", required: "r3" }, - { id: '4', __typename: "T", required: "r4" }, - ].find(({id}) => id === ref.id); + { id: '1', __typename: 'T', required: 'r1' }, + { id: '2', __typename: 'T', required: 'r2' }, + { id: '3', __typename: 'T', required: 'r3' }, + { id: '4', __typename: 'T', required: 'r4' }, + ].find(({ id }) => id === ref.id); }, }, - } - } + }, + }; const s3 = { name: 's3', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5220,22 +5668,29 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `req=${ref.required}` + v: `req=${ref.required}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2, s3 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + s3, + ]); - const operation = parseOp(` + const operation = parseOp( + ` { ts { v } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); const expectedPlan = ` @@ -5286,7 +5741,13 @@ describe('executeQueryPlan', () => { `; expect(queryPlan).toMatchInlineSnapshot(expectedPlan); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "ts": Array [ @@ -5312,7 +5773,10 @@ describe('executeQueryPlan', () => { name: 's1', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key"] + ) type Query { is: [I!]! @@ -5343,19 +5807,22 @@ describe('executeQueryPlan', () => { resolvers: { Query: { is: () => [ - { __typename: 'T1', id: '2', name: 'e2', req: { id: 'r1'} }, - { __typename: 'T2', id: '4', name: 'e4', req: { id: 'r2'} }, - { __typename: 'T1', id: '1', name: 'e1', req: { id: 'r3'} } - ] + { __typename: 'T1', id: '2', name: 'e2', req: { id: 'r1' } }, + { __typename: 'T2', id: '4', name: 'e4', req: { id: 'r2' } }, + { __typename: 'T1', id: '1', name: 'e1', req: { id: 'r3' } }, + ], }, - } - } + }, + }; const s2 = { name: 's2', typeDefs: gql` extend schema - @link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@interfaceObject", "@external", "@requires"]) + @link( + url: "https://specs.apollo.dev/federation/v2.4" + import: ["@key", "@interfaceObject", "@external", "@requires"] + ) type I @key(fields: "id") @interfaceObject { id: ID! @@ -5372,16 +5839,20 @@ describe('executeQueryPlan', () => { __resolveReference(ref: any) { return { ...ref, - v: `req=${ref.req.id}` + v: `req=${ref.req.id}`, }; }, - } - } - } + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - let operation = parseOp(` + let operation = parseOp( + ` { is { ... on T1 { @@ -5392,7 +5863,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); let queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -5440,7 +5913,13 @@ describe('executeQueryPlan', () => { } `); - let response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + let response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -5459,7 +5938,8 @@ describe('executeQueryPlan', () => { `); // Sanity checking that if we ask for `v` (the field with @requires), then everything still works. - operation = parseOp(` + operation = parseOp( + ` { is { ... on T1 { @@ -5471,7 +5951,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -5528,7 +6010,13 @@ describe('executeQueryPlan', () => { } `); - response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -5552,7 +6040,7 @@ describe('executeQueryPlan', () => { const iList = [ { id: '1', - tField: 'field on a T' + tField: 'field on a T', }, ]; const S1 = { @@ -5640,7 +6128,9 @@ describe('executeQueryPlan', () => { }; const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ - S1, S2, S3, + S1, + S2, + S3, ]); const operation = parseOp( `#graphql @@ -5928,10 +6418,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1 }, { __typename: 'B', id: 'keyB', g: 'foo' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -5950,21 +6440,33 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { ... on A { @@ -5975,7 +6477,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6027,7 +6531,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6075,10 +6585,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1 }, { __typename: 'B', id: 'keyB', g: 'foo' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6097,27 +6607,41 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6169,7 +6693,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6214,10 +6744,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', g: 'foo' }, { __typename: 'B', g: 1 }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6235,26 +6765,40 @@ describe('executeQueryPlan', () => { resolvers: { A: { __resolveReference(ref: { g: string }) { - return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '' }; + return { + __typename: 'A', + g: ref.g, + f: ref.g == 'foo' ? 'fA' : '', + }; }, }, B: { __resolveReference(ref: { g: number }) { - return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '' }; + return { + __typename: 'B', + g: ref.g, + f: ref.g === 1 ? 'fB' : '', + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6302,7 +6846,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6343,13 +6893,13 @@ describe('executeQueryPlan', () => { Query: { us() { return [ - { __typename: 'A', id: 'keyA', f: 'fA'}, + { __typename: 'A', id: 'keyA', f: 'fA' }, { __typename: 'B', f: 'fB' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6365,18 +6915,24 @@ describe('executeQueryPlan', () => { return { __typename: 'A', id: ref.id, f: 'fA' }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { us { f } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // Here, the presence of the @provides "forces" the query planner to check type-explosion, and as type-exploding // is the most efficient solution, it is chosen. But as this result in `f` being queried twice at the same level @@ -6400,7 +6956,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6449,10 +7011,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', g: 'foo' }, { __typename: 'B', g: 1 }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6478,25 +7040,37 @@ describe('executeQueryPlan', () => { resolvers: { Query: { t() { - return ({ id: 0 }); - } + return { id: 0 }; + }, }, A: { __resolveReference(ref: { g: string }) { - return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '' }; + return { + __typename: 'A', + g: ref.g, + f: ref.g == 'foo' ? 'fA' : '', + }; }, }, B: { __resolveReference(ref: { g: number }) { - return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '' }; + return { + __typename: 'B', + g: ref.g, + f: ref.g === 1 ? 'fB' : '', + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); - const operation = parseOp(` + const operation = parseOp( + ` query { t { us { @@ -6504,7 +7078,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); // In the 2nd fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level // but with different types. @@ -6570,7 +7146,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` Object { @@ -6626,10 +7208,10 @@ describe('executeQueryPlan', () => { { __typename: 'A', id: 'keyA', g: 1, x: 'xA', y: 'yA' }, { __typename: 'B', id: 'keyB', g: 'foo', x: 'xB', y: 'yB' }, ]; - } + }, }, - } - } + }, + }; const s2 = { name: 'S2', @@ -6648,24 +7230,36 @@ describe('executeQueryPlan', () => { `, resolvers: { A: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'A', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, B: { - __resolveReference(ref: { id: string, g: any }) { - return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` }; + __resolveReference(ref: { id: string; g: any }) { + return { + __typename: 'B', + id: ref.id, + f: `g is type ${typeof ref.g}`, + }; }, }, - } - } + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); // We known that `g` will need to be aliased in the 2nd occurrence on B, and by default it would be aliased // as `g__alias_0`. So we query something with that exact alias to check that we avoid the conflict. We // also use alias `g__alias_1` to further ensure multiple possible conflict are handled. - const operation = parseOp(` + const operation = parseOp( + ` query { us { g__alias_0: x @@ -6673,7 +7267,9 @@ describe('executeQueryPlan', () => { g__alias_1: y } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -6725,7 +7321,13 @@ describe('executeQueryPlan', () => { } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); // We double-check that the final aliases are the one from the query expect(response.data).toMatchInlineSnapshot(` @@ -6773,11 +7375,11 @@ describe('executeQueryPlan', () => { resolvers: { Query: { t() { - return { "__typename": "T", "id": 0 }; - } - } - } - } + return { __typename: 'T', id: 0 }; + }, + }, + }, + }; const s2 = { name: 'S2', @@ -6791,20 +7393,26 @@ describe('executeQueryPlan', () => { T: { __resolveReference() { return null; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]); - const operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + s2, + ]); + const operation = parseOp( + ` { t { id x } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -6836,7 +7444,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.data).toMatchInlineSnapshot(` Object { "t": null, @@ -6891,19 +7505,22 @@ describe('executeQueryPlan', () => { Node: { __resolveType(obj: any) { if (obj['foo']) { - return 'Foo' + return 'Foo'; } if (obj['bar']) { - return 'Bar' + return 'Bar'; } return undefined; - } - } - } - } + }, + }, + }, + }; - const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1 ]); - const operation = parseOp(` + const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ + s1, + ]); + const operation = parseOp( + ` query { tests { node { @@ -6912,7 +7529,9 @@ describe('executeQueryPlan', () => { } } } - `, schema); + `, + schema, + ); const queryPlan = buildPlan(operation, queryPlanner); expect(queryPlan).toMatchInlineSnapshot(` @@ -6934,7 +7553,13 @@ describe('executeQueryPlan', () => { }, } `); - const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap); + const response = await executePlan( + queryPlan, + operation, + undefined, + schema, + serviceMap, + ); expect(response.errors).toBeUndefined(); expect(response.extensions).toBeUndefined(); expect(response.data).toMatchInlineSnapshot(` @@ -6946,32 +7571,32 @@ describe('executeQueryPlan', () => { ], } `); - }) + }); it(`handles duplicate aliased fields on entities correctly`, async () => { const s1Data = [ { id: 't1', type: 'T1' }, { id: 't2', type: 'T2' }, - ] + ]; const s1 = { name: 'S1', typeDefs: gql` - type Query { - testQuery(id: String!): I - } - - interface I { - id: String! - } - - type T1 implements I @key(fields: "id", resolvable: false) { - id: String! - } - - type T2 implements I @key(fields: "id", resolvable: false) { - id: String! - } - `, + type Query { + testQuery(id: String!): I + } + + interface I { + id: String! + } + + type T1 implements I @key(fields: "id", resolvable: false) { + id: String! + } + + type T2 implements I @key(fields: "id", resolvable: false) { + id: String! + } + `, resolvers: { Query: { testQuery(_: any, args: any) { @@ -6980,8 +7605,8 @@ describe('executeQueryPlan', () => { }, I: { __resolveType(obj: any) { - return obj.type - } + return obj.type; + }, }, }, }; @@ -6989,42 +7614,40 @@ describe('executeQueryPlan', () => { const t1 = { id: 't1', foo: { - field: "field1", + field: 'field1', }, }; const t2 = { id: 't2', bar: { - field: "field2", + field: 'field2', }, }; const s2 = { name: 'S2', typeDefs: gql` - interface I { - id: String! - } - - type Test { - field: String! - } - - type T1 implements I @key(fields: "id") { - id: String! - foo: Test - } - - type T2 implements I @key(fields: "id") { - id: String! - bar: Test - } - `, + interface I { + id: String! + } + + type Test { + field: String! + } + + type T1 implements I @key(fields: "id") { + id: String! + foo: Test + } + + type T2 implements I @key(fields: "id") { + id: String! + bar: Test + } + `, resolvers: { - Query: { - }, - Test: { - }, + Query: {}, + Test: {}, T1: { __resolveReference() { return t1; @@ -7044,7 +7667,7 @@ describe('executeQueryPlan', () => { ]); const operation = parseOp( - ` + ` query AliasedQuery($tId: String!) { testQuery(id: $tId) { ... on T1 { @@ -7060,46 +7683,42 @@ describe('executeQueryPlan', () => { } } `, - schema, + schema, ); const queryPlan = buildPlan(operation, queryPlanner); const requestContext = buildRequestContext(); - requestContext.request.variables = { tId: "t1" }; + requestContext.request.variables = { tId: 't1' }; let response = await executePlan( - queryPlan, - operation, - requestContext, - schema, - serviceMap, + queryPlan, + operation, + requestContext, + schema, + serviceMap, ); - expect(response.data).toMatchObject( - { - "testQuery": { - "foo": { - "field": "field1" - } - } - } - ) + expect(response.data).toMatchObject({ + testQuery: { + foo: { + field: 'field1', + }, + }, + }); - requestContext.request.variables = { tId: "t2" }; + requestContext.request.variables = { tId: 't2' }; response = await executePlan( - queryPlan, - operation, - requestContext, - schema, - serviceMap, + queryPlan, + operation, + requestContext, + schema, + serviceMap, ); - expect(response.data).toMatchObject( - { - "testQuery": { - "foo": { - "field": "field2" - } - } - } - ) + expect(response.data).toMatchObject({ + testQuery: { + foo: { + field: 'field2', + }, + }, + }); }); }); diff --git a/gateway-js/src/__tests__/gateway/endToEnd.test.ts b/gateway-js/src/__tests__/gateway/endToEnd.test.ts index dbd359898..254471a94 100644 --- a/gateway-js/src/__tests__/gateway/endToEnd.test.ts +++ b/gateway-js/src/__tests__/gateway/endToEnd.test.ts @@ -2,7 +2,7 @@ import { fixtures } from 'apollo-federation-integration-testsuite'; import { buildSchema, ObjectType } from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { printSchema } from 'graphql'; -import { startSubgraphsAndGateway, Services } from './testUtils' +import { startSubgraphsAndGateway, Services } from './testUtils'; import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache'; import { QueryPlan } from '@apollo/query-planner'; import { createHash } from '@apollo/utils.createhash'; @@ -20,11 +20,15 @@ afterEach(async () => { } }); - describe('caching', () => { - const cache = new InMemoryLRUCache({maxSize: Math.pow(2, 20) * (30), sizeCalculation: approximateObjectSize}); + const cache = new InMemoryLRUCache({ + maxSize: Math.pow(2, 20) * 30, + sizeCalculation: approximateObjectSize, + }); beforeEach(async () => { - services = await startSubgraphsAndGateway(fixtures, { gatewayConfig: { queryPlannerConfig: { cache } } }); + services = await startSubgraphsAndGateway(fixtures, { + gatewayConfig: { queryPlannerConfig: { cache } }, + }); }); it(`cached query plan`, async () => { @@ -43,7 +47,7 @@ describe('caching', () => { `; await services.queryGateway(query); - const queryHash:string = createHash('sha256').update(query).digest('hex'); + const queryHash: string = createHash('sha256').update(query).digest('hex'); expect(await cache.get(queryHash)).toBeTruthy(); }); diff --git a/gateway-js/src/__tests__/gateway/extensions.test.ts b/gateway-js/src/__tests__/gateway/extensions.test.ts index c1a630a0f..e862c3b70 100644 --- a/gateway-js/src/__tests__/gateway/extensions.test.ts +++ b/gateway-js/src/__tests__/gateway/extensions.test.ts @@ -19,7 +19,9 @@ describe('addExtensions', () => { it('has extensions on loaded schemas', async () => { const { schema } = await gateway.load(); - expect(schema.extensions).toEqual({ apollo: { gateway: { version: version } } }); + expect(schema.extensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); it('has extensions on schema updates', async () => { @@ -32,6 +34,8 @@ describe('addExtensions', () => { gateway.load(); const { apiSchema } = await schemaChangeBlocker; - expect(apiSchema.extensions).toEqual({ apollo: { gateway: { version: version } } }); + expect(apiSchema.extensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); }); diff --git a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts index 9b5bf0cb2..9ffa0790f 100644 --- a/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts +++ b/gateway-js/src/__tests__/gateway/lifecycle-hooks.test.ts @@ -138,7 +138,9 @@ describe('lifecycle hooks', () => { const [firstCall, secondCall] = mockDidUpdate.mock.calls; // Note that we've composing our usual test fixtures here - const expectedFirstId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex'); + const expectedFirstId = createHash('sha256') + .update(getTestingSupergraphSdl()) + .digest('hex'); expect(firstCall[0]!.compositionId).toEqual(expectedFirstId); // first call should have no second "previous" argument expect(firstCall[1]).toBeUndefined(); diff --git a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts index 133788d36..1079f70e4 100644 --- a/gateway-js/src/__tests__/gateway/opentelemetry.test.ts +++ b/gateway-js/src/__tests__/gateway/opentelemetry.test.ts @@ -1,8 +1,14 @@ import gql from 'graphql-tag'; -import {ApolloGateway, LocalGraphQLDataSource} from '../../'; -import {fixtures, spanSerializer} from 'apollo-federation-integration-testsuite'; -import {InMemorySpanExporter, SimpleSpanProcessor} from '@opentelemetry/tracing' -import {NodeTracerProvider} from '@opentelemetry/node'; +import { ApolloGateway, LocalGraphQLDataSource } from '../../'; +import { + fixtures, + spanSerializer, +} from 'apollo-federation-integration-testsuite'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { NodeTracerProvider } from '@opentelemetry/node'; import { buildSubgraphSchema } from '@apollo/subgraph'; expect.addSnapshotSerializer(spanSerializer); @@ -17,7 +23,12 @@ beforeEach(() => { }); describe('opentelemetry', () => { - async function execute(executor: any, source: string, variables: any, operationName: string) { + async function execute( + executor: any, + source: string, + variables: any, + operationName: string, + ) { await executor({ source, document: gql(source), @@ -31,8 +42,7 @@ describe('opentelemetry', () => { }); } - describe('with local data', () => - { + describe('with local data', () => { async function gateway() { const localDataSources = Object.fromEntries( fixtures.map((f) => [ @@ -47,7 +57,7 @@ describe('opentelemetry', () => { }, }); - const {executor} = await gateway.load(); + const { executor } = await gateway.load(); return executor; } @@ -62,7 +72,7 @@ describe('opentelemetry', () => { } `; - await execute(executor, source, {upc: '1'}, 'GetProduct'); + await execute(executor, source, { upc: '1' }, 'GetProduct'); expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); @@ -91,14 +101,12 @@ describe('opentelemetry', () => { `; try { - await execute(executor, source, {upc: '1'}, 'GetProduct'); - } - catch(err) {} + await execute(executor, source, { upc: '1' }, 'GetProduct'); + } catch (err) {} expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); }); - it('receives spans on fetch failure', async () => { const gateway = new ApolloGateway({ localServiceList: fixtures, @@ -117,7 +125,7 @@ describe('opentelemetry', () => { } `; - await execute(executor, source, {upc: '1'}, 'GetProduct'); + await execute(executor, source, { upc: '1' }, 'GetProduct'); expect(inMemorySpans.getFinishedSpans()).toMatchSnapshot(); }); }); diff --git a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts index 851ae5f05..914b5cdeb 100644 --- a/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts +++ b/gateway-js/src/__tests__/gateway/queryPlanCache.test.ts @@ -9,7 +9,10 @@ import { QueryPlanner } from '@apollo/query-planner'; import { unwrapSingleResultKind } from '../gateway/testUtils'; it('caches the query plan for a request', async () => { - const buildQueryPlanSpy = jest.spyOn(QueryPlanner.prototype, 'buildQueryPlan'); + const buildQueryPlanSpy = jest.spyOn( + QueryPlanner.prototype, + 'buildQueryPlan', + ); const localDataSources = Object.fromEntries( fixtures.map((f) => [ diff --git a/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts b/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts index 8df2f5425..a7ad72ded 100644 --- a/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts +++ b/gateway-js/src/__tests__/gateway/queryPlannerConfig.test.ts @@ -1,5 +1,5 @@ import gql from 'graphql-tag'; -import { startSubgraphsAndGateway, Services } from './testUtils' +import { startSubgraphsAndGateway, Services } from './testUtils'; let services: Services; @@ -33,10 +33,10 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { b: { x: 1, y: 'foo', - } + }, }), - } - } + }, + }, }; const query = ` @@ -72,22 +72,21 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { const queryPlanner = services.gateway.__testing().queryPlanner!; // If the query planner is genuinely used, we shoud have evaluated 1 plan. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); it('works when enabled', async () => { - services = await startSubgraphsAndGateway( - [subgraph], - { - gatewayConfig: { - queryPlannerConfig: { - debug: { - bypassPlannerForSingleSubgraph: true, - } - } - } - } - ); + services = await startSubgraphsAndGateway([subgraph], { + gatewayConfig: { + queryPlannerConfig: { + debug: { + bypassPlannerForSingleSubgraph: true, + }, + }, + }, + }); const response = await services.queryGateway(query); const result = await response.json(); @@ -95,7 +94,8 @@ describe('`debug.bypassPlannerForSingleSubgraph` config', () => { const queryPlanner = services.gateway.__testing().queryPlanner!; // The `bypassPlannerForSingleSubgraph` doesn't evaluate anything. It's use is the only case where `evaluatedPlanCount` can be 0. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(0); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 0, + ); }); }); - diff --git a/gateway-js/src/__tests__/gateway/reporting.test.ts b/gateway-js/src/__tests__/gateway/reporting.test.ts index 3be495036..c2f278469 100644 --- a/gateway-js/src/__tests__/gateway/reporting.test.ts +++ b/gateway-js/src/__tests__/gateway/reporting.test.ts @@ -92,22 +92,19 @@ describe('reporting', () => { return 'ok'; }); - services = await startSubgraphsAndGateway( - fixtures, - { - gatewayServerConfig: { - apollo: { - key: 'service:foo:bar', - graphRef: 'foo@current', - }, - plugins: [ - ApolloServerPluginUsageReporting({ - sendReportsImmediately: true, - }), - ], + services = await startSubgraphsAndGateway(fixtures, { + gatewayServerConfig: { + apollo: { + key: 'service:foo:bar', + graphRef: 'foo@current', }, - } - ); + plugins: [ + ApolloServerPluginUsageReporting({ + sendReportsImmediately: true, + }), + ], + }, + }); }); afterEach(async () => { @@ -134,9 +131,12 @@ describe('reporting', () => { `; const result = await toPromise( - execute(createHttpLink({ uri: services.gatewayUrl, fetch: fetch as any }), { - query, - }), + execute( + createHttpLink({ uri: services.gatewayUrl, fetch: fetch as any }), + { + query, + }, + ), ); expect(result).toMatchInlineSnapshot(` Object { diff --git a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts index 1854d0bdf..4e441fdab 100644 --- a/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts +++ b/gateway-js/src/__tests__/gateway/supergraphSdl.test.ts @@ -4,7 +4,10 @@ import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction, } from '@apollo/gateway'; -import { accounts, fixturesWithUpdate } from 'apollo-federation-integration-testsuite'; +import { + accounts, + fixturesWithUpdate, +} from 'apollo-federation-integration-testsuite'; import { createHash } from '@apollo/utils.createhash'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; @@ -54,7 +57,9 @@ afterEach(async () => { } }); -const testingFixturesDefaultCompositionId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex'); +const testingFixturesDefaultCompositionId = createHash('sha256') + .update(getTestingSupergraphSdl()) + .digest('hex'); describe('Using supergraphSdl static configuration', () => { it('successfully starts and serves requests to the proper services', async () => { @@ -64,7 +69,6 @@ describe('Using supergraphSdl static configuration', () => { .post('/', { query: '{me{username}}', variables: {} }) .reply(200, { data: { me: { username: '@apollo-user' } } }); - const result = await server.executeOperation({ query: '{ me { username } }', }); @@ -185,9 +189,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId, - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await gateway.stop(); expect(cleanup).toHaveBeenCalledTimes(1); @@ -210,9 +212,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId, - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined(); }); @@ -292,9 +292,7 @@ describe('Using supergraphSdl dynamic configuration', () => { await gateway.load(); const { state, compositionId } = gateway.__testing(); expect(state.phase).toEqual('loaded'); - expect(compositionId).toEqual( - testingFixturesDefaultCompositionId - ); + expect(compositionId).toEqual(testingFixturesDefaultCompositionId); await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError( /The gateway subgraphs health check failed\. Updating to the provided `supergraphSdl` will likely result in future request failures to subgraphs\. The following error occurred during the health check/, diff --git a/gateway-js/src/__tests__/httpSpec.test.ts b/gateway-js/src/__tests__/httpSpec.test.ts index 5dfa09fc3..0955b2c1d 100644 --- a/gateway-js/src/__tests__/httpSpec.test.ts +++ b/gateway-js/src/__tests__/httpSpec.test.ts @@ -12,7 +12,7 @@ describe('httpSpecTests.ts', () => { beforeAll(async () => { gatewayServer = new ApolloServer({ gateway: new ApolloGateway({ - supergraphSdl: getTestingSupergraphSdl() + supergraphSdl: getTestingSupergraphSdl(), }), // The test doesn't know we should send apollo-require-preflight along // with GETs. We could override `fetchFn` to add it but this seems simple enough. diff --git a/gateway-js/src/__tests__/integration/abstract-types.test.ts b/gateway-js/src/__tests__/integration/abstract-types.test.ts index 9decdf99d..8cc4ed0e3 100644 --- a/gateway-js/src/__tests__/integration/abstract-types.test.ts +++ b/gateway-js/src/__tests__/integration/abstract-types.test.ts @@ -1,6 +1,10 @@ import { execute } from '../execution-utils'; -import { astSerializer, fed2gql as gql, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + fed2gql as gql, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -823,7 +827,7 @@ describe('unions', () => { }); describe("doesn't result in duplicate fetches", () => { - it("when exploding types", async () => { + it('when exploding types', async () => { const query = `#graphql query { topProducts { @@ -1045,7 +1049,7 @@ describe("doesn't result in duplicate fetches", () => { `); }); -it("when including the same nested fields under different type conditions", async () => { + it('when including the same nested fields under different type conditions', async () => { const query = `#graphql query { topProducts { @@ -1245,8 +1249,8 @@ it("when including the same nested fields under different type conditions", asyn `); }); -it('when including multiple nested fields to the same service under different type conditions', async () => { - const query = `#graphql + it('when including multiple nested fields to the same service under different type conditions', async () => { + const query = `#graphql query { topProducts { ... on Book { @@ -1479,10 +1483,10 @@ it('when including multiple nested fields to the same service under different ty }, } `); -}); + }); -it('when exploding types through multiple levels', async () => { - const query = `#graphql + it('when exploding types through multiple levels', async () => { + const query = `#graphql query { productsByCategory { name @@ -1506,90 +1510,90 @@ it('when exploding types through multiple levels', async () => { } `; - const { queryPlan, errors } = await execute({ query }, [ - { - name: 'accounts', - typeDefs: gql` - type User @key(fields: "id") { - id: ID! - name: String - username: String @shareable - } - `, - }, - { - name: 'products', - typeDefs: gql` - type Book implements Product @key(fields: "isbn") { - isbn: String! - title: String - year: Int - name: String - price: Int - } + const { queryPlan, errors } = await execute({ query }, [ + { + name: 'accounts', + typeDefs: gql` + type User @key(fields: "id") { + id: ID! + name: String + username: String @shareable + } + `, + }, + { + name: 'products', + typeDefs: gql` + type Book implements Product @key(fields: "isbn") { + isbn: String! + title: String + year: Int + name: String + price: Int + } - type Furniture implements Product @key(fields: "sku") { - sku: String! - name: String - price: Int - weight: Int - } + type Furniture implements Product @key(fields: "sku") { + sku: String! + name: String + price: Int + weight: Int + } - interface Product { - name: String - price: Int - } + interface Product { + name: String + price: Int + } - extend type Query { - productsByCategory: [ProductCategory] - } + extend type Query { + productsByCategory: [ProductCategory] + } - interface ProductCategory { - name: String! - } + interface ProductCategory { + name: String! + } - type BookCategory implements ProductCategory { - name: String! - items: [Book] - } + type BookCategory implements ProductCategory { + name: String! + items: [Book] + } - type FurnitureCategory implements ProductCategory { - name: String! - items: [Furniture] - } - `, - }, - { - name: 'reviews', - typeDefs: gql` - extend type Book implements Product @key(fields: "isbn") { - isbn: String! @external - reviews: [Review] - } - extend type Furniture implements Product @key(fields: "sku") { - sku: String! @external - reviews: [Review] - } - extend interface Product { - reviews: [Review] - } - type Review @key(fields: "id") { - id: ID! - body: String - author: User @provides(fields: "username") - product: Product - } - extend type User @key(fields: "id") { - id: ID! @external - username: String @external - reviews: [Review] - } - `, - }, - ]); + type FurnitureCategory implements ProductCategory { + name: String! + items: [Furniture] + } + `, + }, + { + name: 'reviews', + typeDefs: gql` + extend type Book implements Product @key(fields: "isbn") { + isbn: String! @external + reviews: [Review] + } + extend type Furniture implements Product @key(fields: "sku") { + sku: String! @external + reviews: [Review] + } + extend interface Product { + reviews: [Review] + } + type Review @key(fields: "id") { + id: ID! + body: String + author: User @provides(fields: "username") + product: Product + } + extend type User @key(fields: "id") { + id: ID! @external + username: String @external + reviews: [Review] + } + `, + }, + ]); - expect(errors).toBeUndefined(); - expect(queryPlan).toMatchInlineSnapshot(` + expect(errors).toBeUndefined(); + expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "products") { @@ -1643,9 +1647,9 @@ it('when exploding types through multiple levels', async () => { }, } `); -}); + }); -it("when including the same nested fields under different type conditions that are split between services", async () => { + it('when including the same nested fields under different type conditions that are split between services', async () => { const query = `#graphql query { topProducts { diff --git a/gateway-js/src/__tests__/integration/boolean.test.ts b/gateway-js/src/__tests__/integration/boolean.test.ts index 2f82477d3..1ede900e2 100644 --- a/gateway-js/src/__tests__/integration/boolean.test.ts +++ b/gateway-js/src/__tests__/integration/boolean.test.ts @@ -1,5 +1,8 @@ import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -91,11 +94,26 @@ describe('@skip', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); @@ -127,15 +145,33 @@ describe('@skip', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); - const variables = { definitions: operation.variableDefinitions, values: {skip} }; + const variables = { + definitions: operation.variableDefinitions, + values: { skip }, + }; expect(queryPlan).toCallService('reviews', variables); expect(queryPlan).toCallService('accounts', variables); }); @@ -227,11 +263,26 @@ describe('@include', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); @@ -264,15 +315,33 @@ describe('@include', () => { expect(data).toEqual({ topReviews: [ - { body: 'Love it!', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Too expensive.', author: { name: { first: 'Ada', last: 'Lovelace' } } }, - { body: 'Could be better.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Prefer something else.', author: { name: { first: 'Alan', last: 'Turing' } } }, - { body: 'Wish I had read this before.', author: { name: { first: 'Alan', last: 'Turing' } } }, + { + body: 'Love it!', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Too expensive.', + author: { name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + body: 'Could be better.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Prefer something else.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, + { + body: 'Wish I had read this before.', + author: { name: { first: 'Alan', last: 'Turing' } }, + }, ], }); - const variables = { definitions: operation.variableDefinitions, values: {include} }; + const variables = { + definitions: operation.variableDefinitions, + values: { include }, + }; expect(queryPlan).toCallService('accounts', variables); expect(queryPlan).toCallService('reviews', variables); }); diff --git a/gateway-js/src/__tests__/integration/complex-key.test.ts b/gateway-js/src/__tests__/integration/complex-key.test.ts index 4ded8ba97..999b0b98f 100644 --- a/gateway-js/src/__tests__/integration/complex-key.test.ts +++ b/gateway-js/src/__tests__/integration/complex-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -85,13 +88,13 @@ const userService: ServiceDefinitionModule = { User: { __resolveReference(reference) { return users.find( - user => + (user) => user.id === reference.id && user.organizationId === reference.organization.id, ); }, organization(user) { - return organizations.find(org => org.id === user.organizationId); + return organizations.find((org) => org.id === user.organizationId); }, }, }, diff --git a/gateway-js/src/__tests__/integration/configuration.test.ts b/gateway-js/src/__tests__/integration/configuration.test.ts index 5af71bec1..cbe5e7114 100644 --- a/gateway-js/src/__tests__/integration/configuration.test.ts +++ b/gateway-js/src/__tests__/integration/configuration.test.ts @@ -201,9 +201,9 @@ describe('gateway startup errors', () => { } const expected = - "A valid schema couldn't be composed. The following composition errors were found:\n" - + ' [accounts] On type "User", for @key(fields: "id"): Cannot query field "id" on type "User" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).\n' - + ' [accounts] On type "Account", for @key(fields: "id"): Cannot query field "id" on type "Account" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).' + "A valid schema couldn't be composed. The following composition errors were found:\n" + + ' [accounts] On type "User", for @key(fields: "id"): Cannot query field "id" on type "User" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).\n' + + ' [accounts] On type "Account", for @key(fields: "id"): Cannot query field "id" on type "Account" (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).'; expect(err.message).toBe(expected); }); }); @@ -352,7 +352,7 @@ describe('deprecation warnings', () => { try { await gateway.load(); - // gateway will throw since we're not providing an actual service list, disregard + // gateway will throw since we're not providing an actual service list, disregard } catch {} expect(logger.warn).toHaveBeenCalledWith( diff --git a/gateway-js/src/__tests__/integration/custom-directives.test.ts b/gateway-js/src/__tests__/integration/custom-directives.test.ts index f6915fd9a..22caea0bb 100644 --- a/gateway-js/src/__tests__/integration/custom-directives.test.ts +++ b/gateway-js/src/__tests__/integration/custom-directives.test.ts @@ -98,7 +98,7 @@ describe('custom executable directives', () => { }); // With relaxed composition, instead of erroring out if a directive is not declared everywhere, we compose but don't - // include the directive in the supergraph and generate a hint. So the following test will complain that @stream + // include the directive in the supergraph and generate a hint. So the following test will complain that @stream // is unknown in the query. Not that the hints tests do test we properly raise an hint in that case. it.skip("returns validation errors when directives aren't present across all services", async () => { const invalidService = { diff --git a/gateway-js/src/__tests__/integration/fragments.test.ts b/gateway-js/src/__tests__/integration/fragments.test.ts index 939cac59c..9c08c1b1e 100644 --- a/gateway-js/src/__tests__/integration/fragments.test.ts +++ b/gateway-js/src/__tests__/integration/fragments.test.ts @@ -131,7 +131,7 @@ it('supports multiple named fragments (one level, mixed ordering)', async () => name: { first: 'Ada', last: 'Lovelace', - } + }, }, }); diff --git a/gateway-js/src/__tests__/integration/list-key.test.ts b/gateway-js/src/__tests__/integration/list-key.test.ts index cb7e84b90..5f51baa4e 100644 --- a/gateway-js/src/__tests__/integration/list-key.test.ts +++ b/gateway-js/src/__tests__/integration/list-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -62,7 +65,7 @@ const userService: ServiceDefinitionModule = { resolvers: { User: { __resolveReference(reference) { - return users.find(user => listsAreEqual(user.id, reference.id)); + return users.find((user) => listsAreEqual(user.id, reference.id)); }, }, }, diff --git a/gateway-js/src/__tests__/integration/logger.test.ts b/gateway-js/src/__tests__/integration/logger.test.ts index c323b9c7a..d990673a9 100644 --- a/gateway-js/src/__tests__/integration/logger.test.ts +++ b/gateway-js/src/__tests__/integration/logger.test.ts @@ -1,16 +1,17 @@ import { ApolloGateway } from '../..'; import type { Logger } from '@apollo/utils.logger'; -import { PassThrough } from "stream"; +import { PassThrough } from 'stream'; -import * as winston from "winston"; +import * as winston from 'winston'; import WinstonTransport from 'winston-transport'; -import * as bunyan from "bunyan"; -import * as loglevel from "loglevel"; -import * as log4js from "log4js"; +import * as bunyan from 'bunyan'; +import * as loglevel from 'loglevel'; +import * as log4js from 'log4js'; -const LOWEST_LOG_LEVEL = "debug"; +const LOWEST_LOG_LEVEL = 'debug'; -const KNOWN_DEBUG_MESSAGE = "Gateway successfully initialized (but not yet loaded)"; +const KNOWN_DEBUG_MESSAGE = + 'Gateway successfully initialized (but not yet loaded)'; async function triggerKnownDebugMessage(logger: Logger) { // Trigger a known error. @@ -18,13 +19,13 @@ async function triggerKnownDebugMessage(logger: Logger) { // message outside of the constructor, but it seemed worth testing // the compatibility with `ApolloGateway` itself rather than generically. // The error does not matter, so it is caught and ignored. - await new ApolloGateway({ logger }).load().catch(_e => undefined); + await new ApolloGateway({ logger }).load().catch((_e) => undefined); } -describe("logger", () => { +describe('logger', () => { it("works with 'winston'", async () => { const sink = jest.fn(); - const transport = new class extends WinstonTransport { + const transport = new (class extends WinstonTransport { constructor() { super({ format: winston.format.json(), @@ -34,16 +35,18 @@ describe("logger", () => { log(info: any) { sink(info); } - }; + })(); const logger = winston.createLogger({ level: 'debug' }).add(transport); await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: LOWEST_LOG_LEVEL, - message: KNOWN_DEBUG_MESSAGE, - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: LOWEST_LOG_LEVEL, + message: KNOWN_DEBUG_MESSAGE, + }), + ); }); it("works with 'bunyan'", async () => { @@ -51,30 +54,36 @@ describe("logger", () => { // Bunyan uses streams for its logging implementations. const writable = new PassThrough(); - writable.on("data", data => sink(JSON.parse(data.toString()))); + writable.on('data', (data) => sink(JSON.parse(data.toString()))); const logger = bunyan.createLogger({ - name: "test-logger-bunyan", - streams: [{ - level: LOWEST_LOG_LEVEL, - stream: writable, - }] + name: 'test-logger-bunyan', + streams: [ + { + level: LOWEST_LOG_LEVEL, + stream: writable, + }, + ], }); await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: bunyan.DEBUG, - msg: KNOWN_DEBUG_MESSAGE, - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: bunyan.DEBUG, + msg: KNOWN_DEBUG_MESSAGE, + }), + ); }); it("works with 'loglevel'", async () => { const sink = jest.fn(); - const logger = loglevel.getLogger("test-logger-loglevel") - logger.methodFactory = (_methodName, level): loglevel.LoggingMethod => - (message) => sink({ level, message }); + const logger = loglevel.getLogger('test-logger-loglevel'); + logger.methodFactory = + (_methodName, level): loglevel.LoggingMethod => + (message) => + sink({ level, message }); // The `setLevel` method must be called after overwriting `methodFactory`. // This is an intentional API design pattern of the loglevel package: @@ -96,17 +105,17 @@ describe("logger", () => { appenders: { custom: { type: { - configure: () => - (loggingEvent: log4js.LoggingEvent) => sink(loggingEvent) - } - } + configure: () => (loggingEvent: log4js.LoggingEvent) => + sink(loggingEvent), + }, + }, }, categories: { default: { appenders: ['custom'], level: LOWEST_LOG_LEVEL, - } - } + }, + }, }); const logger = log4js.getLogger(); @@ -114,9 +123,11 @@ describe("logger", () => { await triggerKnownDebugMessage(logger); - expect(sink).toHaveBeenCalledWith(expect.objectContaining({ - level: log4js.levels.DEBUG, - data: [KNOWN_DEBUG_MESSAGE], - })); + expect(sink).toHaveBeenCalledWith( + expect.objectContaining({ + level: log4js.levels.DEBUG, + data: [KNOWN_DEBUG_MESSAGE], + }), + ); }); }); diff --git a/gateway-js/src/__tests__/integration/managed.test.ts b/gateway-js/src/__tests__/integration/managed.test.ts index 07d5b2b3b..af48a3dc0 100644 --- a/gateway-js/src/__tests__/integration/managed.test.ts +++ b/gateway-js/src/__tests__/integration/managed.test.ts @@ -262,7 +262,7 @@ describe('Managed gateway with explicit UplinkSupergraphManager', () => { gateway = new ApolloGateway({ logger, - supergraphSdl: uplinkManager + supergraphSdl: uplinkManager, }); await expect(gateway.load()).resolves.not.toThrow(); diff --git a/gateway-js/src/__tests__/integration/multiple-key.test.ts b/gateway-js/src/__tests__/integration/multiple-key.test.ts index 772dbced1..e4100bddc 100644 --- a/gateway-js/src/__tests__/integration/multiple-key.test.ts +++ b/gateway-js/src/__tests__/integration/multiple-key.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute, ServiceDefinitionModule } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); @@ -45,7 +48,7 @@ const reviewService: ServiceDefinitionModule = { }, User: { reviews(user) { - return reviews.filter(review => review.authorId === user.id); + return reviews.filter((review) => review.authorId === user.id); }, }, Review: { @@ -107,8 +110,8 @@ const userService: ServiceDefinitionModule = { group: () => ({ id: 1, name: 'Apollo GraphQL' }), __resolveReference(reference) { if (reference.ssn) - return users.find(user => user.ssn === reference.ssn); - else return users.find(user => user.id === reference.id); + return users.find((user) => user.ssn === reference.ssn); + else return users.find((user) => user.id === reference.id); }, }, }, @@ -234,7 +237,7 @@ it('fetches keys as needed to reduce round trip queries', async () => { { query, }, - [userService, reviewService, actuaryService] + [userService, reviewService, actuaryService], ); expect(errors).toBeFalsy(); diff --git a/gateway-js/src/__tests__/integration/networkRequests.test.ts b/gateway-js/src/__tests__/integration/networkRequests.test.ts index bd3826e6f..0dd055cbf 100644 --- a/gateway-js/src/__tests__/integration/networkRequests.test.ts +++ b/gateway-js/src/__tests__/integration/networkRequests.test.ts @@ -455,9 +455,9 @@ describe('Downstream service health checks', () => { // Update mockSupergraphSdlRequestSuccessIfAfter( - 'originalId-1234', - 'updatedId-5678', - getTestingSupergraphSdl(fixturesWithUpdate), + 'originalId-1234', + 'updatedId-5678', + getTestingSupergraphSdl(fixturesWithUpdate), ); mockAllServicesHealthCheckSuccess(); @@ -499,8 +499,8 @@ describe('Downstream service health checks', () => { // Update (with one health check failure) mockSupergraphSdlRequestSuccessIfAfter( - 'originalId-1234', - 'updatedId-5678', + 'originalId-1234', + 'updatedId-5678', getTestingSupergraphSdl(fixturesWithUpdate), ); mockServiceHealthCheck(accounts).reply(500); diff --git a/gateway-js/src/__tests__/integration/provides.test.ts b/gateway-js/src/__tests__/integration/provides.test.ts index 14c925139..f1473d1d9 100644 --- a/gateway-js/src/__tests__/integration/provides.test.ts +++ b/gateway-js/src/__tests__/integration/provides.test.ts @@ -12,7 +12,7 @@ it('does not have to go to another service when field is given', async () => { } `; - const { data, queryPlan } = await execute( { + const { data, queryPlan } = await execute({ query, }); @@ -63,11 +63,30 @@ it('does not load fields provided even when going to other service', async () => expect(data).toEqual({ topReviews: [ - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, - { author: { username: '@complete', name: { first: 'Alan', last: 'Turing' } } }, + { + author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + author: { username: '@ada', name: { first: 'Ada', last: 'Lovelace' } }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, + { + author: { + username: '@complete', + name: { first: 'Alan', last: 'Turing' }, + }, + }, ], }); diff --git a/gateway-js/src/__tests__/integration/requires.test.ts b/gateway-js/src/__tests__/integration/requires.test.ts index 3e24d1b6f..e9bc39c5e 100644 --- a/gateway-js/src/__tests__/integration/requires.test.ts +++ b/gateway-js/src/__tests__/integration/requires.test.ts @@ -1,6 +1,9 @@ import gql from 'graphql-tag'; import { execute } from '../execution-utils'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; expect.addSnapshotSerializer(astSerializer); expect.addSnapshotSerializer(queryPlanSerializer); diff --git a/gateway-js/src/__tests__/integration/scope.test.ts b/gateway-js/src/__tests__/integration/scope.test.ts index 9bcad9569..e730c9856 100644 --- a/gateway-js/src/__tests__/integration/scope.test.ts +++ b/gateway-js/src/__tests__/integration/scope.test.ts @@ -343,7 +343,7 @@ describe('scope', () => { `); }); - it("merges nested conditions when possible", async () => { + it('merges nested conditions when possible', async () => { const query = `#graphql query GetProducts { topProducts { diff --git a/gateway-js/src/__tests__/integration/unions.test.ts b/gateway-js/src/__tests__/integration/unions.test.ts index bc8bd7fda..145325af4 100644 --- a/gateway-js/src/__tests__/integration/unions.test.ts +++ b/gateway-js/src/__tests__/integration/unions.test.ts @@ -1,5 +1,8 @@ import gql from 'graphql-tag'; -import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite'; +import { + astSerializer, + queryPlanSerializer, +} from 'apollo-federation-integration-testsuite'; import { execute } from '../execution-utils'; expect.addSnapshotSerializer(astSerializer); @@ -24,35 +27,32 @@ it('handles multiple union type conditions that share a response name (media)', } `; - const { queryPlan, errors } = await execute( - { query }, - [ - { - name: 'contentService', - typeDefs: gql` - extend type Query { - content: Content - } - union Content = Audio | Video - type Audio { - media: AudioURL - } - type AudioURL { - url: String - } - type Video { - media: VideoAspectRatio - } - type VideoAspectRatio { - aspectRatio: String - } - `, - resolvers: { - Query: {}, - }, + const { queryPlan, errors } = await execute({ query }, [ + { + name: 'contentService', + typeDefs: gql` + extend type Query { + content: Content + } + union Content = Audio | Video + type Audio { + media: AudioURL + } + type AudioURL { + url: String + } + type Video { + media: VideoAspectRatio + } + type VideoAspectRatio { + aspectRatio: String + } + `, + resolvers: { + Query: {}, }, - ], - ); + }, + ]); expect(errors).toBeUndefined(); expect(queryPlan).toMatchInlineSnapshot(` diff --git a/gateway-js/src/__tests__/integration/variables.test.ts b/gateway-js/src/__tests__/integration/variables.test.ts index 15209f44f..ed70a7006 100644 --- a/gateway-js/src/__tests__/integration/variables.test.ts +++ b/gateway-js/src/__tests__/integration/variables.test.ts @@ -109,7 +109,7 @@ it('works with default variables in the schema', async () => { name: { first: 'Ada', last: 'Lovelace', - } + }, }, }, }); diff --git a/gateway-js/src/__tests__/queryPlanCucumber.test.ts b/gateway-js/src/__tests__/queryPlanCucumber.test.ts index 9b4648e43..8715db16e 100644 --- a/gateway-js/src/__tests__/queryPlanCucumber.test.ts +++ b/gateway-js/src/__tests__/queryPlanCucumber.test.ts @@ -7,13 +7,10 @@ import { getFederatedTestingSchema } from './execution-utils'; import { operationFromDocument } from '@apollo/federation-internals'; const buildQueryPlanFeature = loadFeature( - './gateway-js/src/__tests__/build-query-plan.feature' + './gateway-js/src/__tests__/build-query-plan.feature', ); - -const features = [ - buildQueryPlanFeature -]; +const features = [buildQueryPlanFeature]; features.forEach((feature) => { defineFeature(feature, (test) => { @@ -28,26 +25,31 @@ features.forEach((feature) => { const givenQuery = () => { given(/^query$/im, (operation: string) => { operationDocument = gql(operation); - }) - } + }); + }; const thenQueryPlanShouldBe = () => { then(/^query plan$/i, (expectedQueryPlan: string) => { - queryPlan = queryPlanner.buildQueryPlan(operationFromDocument(schema, operationDocument)); + queryPlan = queryPlanner.buildQueryPlan( + operationFromDocument(schema, operationDocument), + ); const parsedExpectedPlan = JSON.parse(expectedQueryPlan); expect(queryPlan).toEqual(parsedExpectedPlan); - }) - } + }); + }; // step over each defined step in the .feature and execute the correct // matching step fn defined above scenario.steps.forEach(({ stepText }) => { const title = stepText.toLocaleLowerCase(); - if (title === "query") givenQuery(); - else if (title === "query plan") thenQueryPlanShouldBe(); - else throw new Error(`Unrecognized steps used in "build-query-plan.feature"`); + if (title === 'query') givenQuery(); + else if (title === 'query plan') thenQueryPlanShouldBe(); + else + throw new Error( + `Unrecognized steps used in "build-query-plan.feature"`, + ); }); }); }); diff --git a/gateway-js/src/__tests__/resultShaping.test.ts b/gateway-js/src/__tests__/resultShaping.test.ts index 47a88d66c..a33602964 100644 --- a/gateway-js/src/__tests__/resultShaping.test.ts +++ b/gateway-js/src/__tests__/resultShaping.test.ts @@ -1,6 +1,9 @@ -import { buildSchemaFromAST, parseOperation } from "@apollo/federation-internals" -import { gql } from "apollo-federation-integration-testsuite"; -import { computeResponse } from "../resultShaping"; +import { + buildSchemaFromAST, + parseOperation, +} from '@apollo/federation-internals'; +import { gql } from 'apollo-federation-integration-testsuite'; +import { computeResponse } from '../resultShaping'; const introspectionHandling = () => null; @@ -39,27 +42,32 @@ describe('gateway post-processing', () => { `); const input = { - "t": { - "a": 0, - "b": 'testData', - "c": [{ - __typename: 'P1', - id: 'foo', - x: 1, - y: 2, - }, { - __typename: 'P2', - x: 10, - y: 20, - w: 30, - z: 40, - }], - "d": 1, + t: { + a: 0, + b: 'testData', + c: [ + { + __typename: 'P1', + id: 'foo', + x: 1, + y: 2, + }, + { + __typename: 'P2', + x: 10, + y: 20, + w: 30, + z: 40, + }, + ], + d: 1, }, - "v": 42 - } + v: 42, + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { t { a @@ -75,13 +83,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "t": Object { @@ -121,33 +132,38 @@ describe('gateway post-processing', () => { `); const tObj = { - "a": null, - "b": null, - "c": [24, null, 42, null], - "d": [24, null, 42, null], - "e": [24, null, 42, null], - "f": [24, null, 42, null], - } + a: null, + b: null, + c: [24, null, 42, null], + d: [24, null, 42, null], + e: [24, null, 42, null], + f: [24, null, 42, null], + }; const input = { - "tNullable": tObj, - "tNonNullable": tObj, - } + tNullable: tObj, + tNonNullable: tObj, + }; test('no propagation on nullable (non-list) type', () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { tNonNullable { a } } - `); - - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + `, + ); + + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "tNonNullable": Object { @@ -160,13 +176,16 @@ describe('gateway post-processing', () => { }); test('propagation on non-nullable (non-list) type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { b } } - `); + `, + ); let res = computeResponse({ operation: operationNonNullable, @@ -183,14 +202,16 @@ describe('gateway post-processing', () => { `); expect(res.errors[0].path).toStrictEqual(['tNonNullable', 'b']); - - const operationNullable = parseOperation(schema, ` + const operationNullable = parseOperation( + schema, + ` { tNullable { b } } - `); + `, + ); res = computeResponse({ operation: operationNullable, @@ -211,19 +232,24 @@ describe('gateway post-processing', () => { }); test('no propagation on nullable list type', () => { - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { tNonNullable { c } } - `); - - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + `, + ); + + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "tNonNullable": Object { @@ -241,13 +267,16 @@ describe('gateway post-processing', () => { }); test('no propagation on null elements of non-nullable list type with nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { d } } - `); + `, + ); const res = computeResponse({ operation: operationNonNullable, @@ -272,13 +301,16 @@ describe('gateway post-processing', () => { }); test('propagation on null elements of list type with non-nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { e } } - `); + `, + ); const res = computeResponse({ operation: operationNonNullable, @@ -303,13 +335,16 @@ describe('gateway post-processing', () => { }); test('propagation on null elements of non-nullable list type with non-nullable inner element type', () => { - const operationNonNullable = parseOperation(schema, ` + const operationNonNullable = parseOperation( + schema, + ` { tNonNullable { f } } - `); + `, + ); let res = computeResponse({ operation: operationNonNullable, @@ -328,13 +363,16 @@ describe('gateway post-processing', () => { expect(res.errors[0].path).toStrictEqual(['tNonNullable', 'f', 1]); expect(res.errors[1].path).toStrictEqual(['tNonNullable', 'f', 3]); - const operationNullable = parseOperation(schema, ` + const operationNullable = parseOperation( + schema, + ` { tNullable { f } } - `); + `, + ); res = computeResponse({ operation: operationNullable, @@ -365,14 +403,17 @@ describe('gateway post-processing', () => { `); const input = { - "x": 'foo', - } + x: 'foo', + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { x } - `); + `, + ); const res = computeResponse({ operation, @@ -403,21 +444,23 @@ describe('gateway post-processing', () => { `); const input = { - "t": { - "a": 42, - "q": { - "t": { - "q": { - "t": { - "a": 24 + t: { + a: 42, + q: { + t: { + q: { + t: { + a: 24, }, }, }, }, }, - } + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { __typename t { @@ -435,13 +478,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "__typename": "Query", @@ -485,19 +531,21 @@ describe('gateway post-processing', () => { `); const input = { - "i": [ + i: [ { - "__typename": "A", - "x": 24, + __typename: 'A', + x: 24, }, { - "__typename": "B", - "x": 42, + __typename: 'B', + x: 42, }, - ] - } + ], + }; - const operation = parseOperation(schema, ` + const operation = parseOperation( + schema, + ` { i { ... on I { @@ -506,13 +554,16 @@ describe('gateway post-processing', () => { } } } - `); + `, + ); - expect(computeResponse({ - operation, - input, - introspectionHandling, - })).toMatchInlineSnapshot(` + expect( + computeResponse({ + operation, + input, + introspectionHandling, + }), + ).toMatchInlineSnapshot(` Object { "data": Object { "i": Array [ @@ -543,12 +594,15 @@ describe('gateway post-processing', () => { included: 'world', }; - const operation = parseOperation(schema, `#graphql + const operation = parseOperation( + schema, + `#graphql query DefaultedIfCondition($if: Boolean = true) { skipped: hello @skip(if: $if) included: hello @include(if: $if) } - `); + `, + ); expect( computeResponse({ @@ -578,13 +632,16 @@ describe('gateway post-processing', () => { included: 'world', }; - const operation = parseOperation(schema, `#graphql + const operation = parseOperation( + schema, + `#graphql # note that the default conditional is inverted from the previous test query DefaultedIfCondition($if: Boolean = false) { skipped: hello @skip(if: $if) included: hello @include(if: $if) } - `); + `, + ); expect( computeResponse({ @@ -602,4 +659,4 @@ describe('gateway post-processing', () => { } `); }); -}) +}); diff --git a/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts index 2ffeb89fb..64c645dde 100644 --- a/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +++ b/gateway-js/src/datasources/__tests__/LocalGraphQLDataSource.test.ts @@ -42,7 +42,7 @@ describe('constructing requests', () => { }, incomingRequestContext: { context: { userId: 2 }, - } as GatewayGraphQLRequestContext<{userId: number}>, + } as GatewayGraphQLRequestContext<{ userId: number }>, context: { userId: 2 }, }); diff --git a/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts index 537dc798e..74482c057 100644 --- a/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +++ b/gateway-js/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts @@ -231,7 +231,11 @@ describe('constructing requests', () => { nock('https://api.example.com') .post('/foo', { query: '{ me { name } }' }) - .reply(200, { data: { me: 'james' } }, {'content-type': 'application/graphql-response+json'}); + .reply( + 200, + { data: { me: 'james' } }, + { 'content-type': 'application/graphql-response+json' }, + ); const { data } = await DataSource.process({ ...defaultProcessOptions, diff --git a/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts b/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts index 7d3f6fbc9..53b2553de 100644 --- a/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts +++ b/gateway-js/src/schema-helper/__tests__/addExtensions.test.ts @@ -4,12 +4,14 @@ import { ApolloGraphQLSchemaExtensions } from '../../typings/graphql'; const { version } = require('../../../package.json'); describe('addExtensions', () => { - it('adds gateway extensions to a schema', async () => { const schema = buildSchema('type Query { hello: String }'); expect(schema.extensions).toEqual({}); - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; - expect(actualExtensions).toEqual({ apollo: { gateway: { version: version } } }); + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; + expect(actualExtensions).toEqual({ + apollo: { gateway: { version: version } }, + }); }); it('does not delete existing extensions', async () => { @@ -19,18 +21,19 @@ describe('addExtensions', () => { foo: 'bar', apollo: { gateway: { - version: 'hello' - } - } + version: 'hello', + }, + }, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ foo: 'bar', apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); @@ -38,15 +41,16 @@ describe('addExtensions', () => { const schema = buildSchema('type Query { hello: String }'); expect(schema.extensions).toEqual({}); schema.extensions = { - apollo: undefined + apollo: undefined, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); @@ -55,16 +59,17 @@ describe('addExtensions', () => { expect(schema.extensions).toEqual({}); schema.extensions = { apollo: { - gateway: undefined - } + gateway: undefined, + }, }; - const actualExtensions: ApolloGraphQLSchemaExtensions = addExtensions(schema).extensions; + const actualExtensions: ApolloGraphQLSchemaExtensions = + addExtensions(schema).extensions; expect(actualExtensions).toEqual({ apollo: { gateway: { - version: version - } - } + version: version, + }, + }, }); }); }); diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts index b13b3a3d5..5873afd81 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts @@ -3,11 +3,17 @@ import { fixtures, fixturesWithUpdate, } from 'apollo-federation-integration-testsuite'; -import { nockBeforeEach, nockAfterEach } from '../../../__tests__/nockAssertions'; +import { + nockBeforeEach, + nockAfterEach, +} from '../../../__tests__/nockAssertions'; import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../../..'; import { IntrospectAndCompose } from '..'; import { mockAllServicesSdlQuerySuccess } from '../../../__tests__/integration/nockMocks'; -import { getTestingSupergraphSdl, wait } from '../../../__tests__/execution-utils'; +import { + getTestingSupergraphSdl, + wait, +} from '../../../__tests__/execution-utils'; import resolvable from '@josephg/resolvable'; import type { Logger } from '@apollo/utils.logger'; @@ -182,10 +188,7 @@ describe('IntrospectAndCompose', () => { }, }); - await Promise.all([ - healthCheckPromiseOnLoad, - healthCheckPromiseOnUpdate, - ]); + await Promise.all([healthCheckPromiseOnLoad, healthCheckPromiseOnUpdate]); expect(healthCheckSpy).toHaveBeenNthCalledWith( 1, diff --git a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts index c13a5d131..3fb686d73 100644 --- a/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts +++ b/gateway-js/src/supergraphManagers/IntrospectAndCompose/__tests__/loadServicesFromRemoteEndpoint.test.ts @@ -10,7 +10,7 @@ describe('loadServicesFromRemoteEndpoint', () => { loadServicesFromRemoteEndpoint({ serviceList, serviceSdlCache, - getServiceIntrospectionHeaders: async () => ({}) + getServiceIntrospectionHeaders: async () => ({}), }), ).rejects.toThrowError( "Tried to load schema for 'test' but no 'url' was specified.", diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts index 720676ba6..90672ed87 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts @@ -93,7 +93,9 @@ describe('loadSupergraphSdlFromStorage', () => { }); it('Queries alternate Uplink URL if first one times out', async () => { - mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1).delay(120_000).reply(500); + mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1) + .delay(120_000) + .reply(500); mockSupergraphSdlRequestIfAfter( 'originalId-1234', mockCloudConfigUrl2, diff --git a/package.json b/package.json index 1ed6699b5..8db1b4a95 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,11 @@ "changeset-version": "changeset version && npm i", "build-and-publish": "npm run compile && changeset publish", "spell:check": "cspell lint --no-progress --config .cspell/cspell.yml || (echo 'Add any real words to ./cspell/cspell-dict.txt.'; exit 1)", - "use-repo-git-hooks": "git config core.hooksPath .git-hooks" + "//": "This only needs to use prettier@2 for as long as jest disallows using prettier@3", + "prettier:check": "node ./node_modules/prettier-2/bin-prettier.js --check ./**/__tests__/**/*.test.ts", + "prettier:fix": "node ./node_modules/prettier-2/bin-prettier.js --write ./**/__tests__/**/*.test.ts", + "//": "Optional: run this to configure git hooks and blame ignore revs", + "git:configure": "git config core.hooksPath .git-hooks; git config blame.ignoreRevsFile .git-blame-ignore-revs" }, "engines": { "node": ">=14.15.0", diff --git a/query-planner-js/src/__tests__/allFeatures.test.ts b/query-planner-js/src/__tests__/allFeatures.test.ts index 4cfc265b2..698a21092 100644 --- a/query-planner-js/src/__tests__/allFeatures.test.ts +++ b/query-planner-js/src/__tests__/allFeatures.test.ts @@ -1,9 +1,7 @@ import fs from 'fs'; import { defineFeature, loadFeatures } from 'jest-cucumber'; import path from 'path'; -import { - QueryPlan, QueryPlanner -} from '..'; +import { QueryPlan, QueryPlanner } from '..'; import { Operation, Schema, @@ -12,7 +10,6 @@ import { } from '@apollo/federation-internals'; import { parse, validate } from 'graphql'; - // This test looks over all directories under tests/features and finds "supergraphSdl.graphql" in // each of those directories. It runs all of the .feature cases in that directory against that schema. // To add test cases against new schemas, create a sub directory under "features" with the new schema @@ -42,8 +39,12 @@ for (const directory of directories) { const supergraphSdl = fs.readFileSync(schemaPath, 'utf8'); const supergraph = Supergraph.build(supergraphSdl); schema = supergraph.schema; - const exposeDocumentNodeInFetchNode = feature.title.endsWith("(with ExposeDocumentNodeInFetchNode)"); - queryPlanner = new QueryPlanner(supergraph, { exposeDocumentNodeInFetchNode }); + const exposeDocumentNodeInFetchNode = feature.title.endsWith( + '(with ExposeDocumentNodeInFetchNode)', + ); + queryPlanner = new QueryPlanner(supergraph, { + exposeDocumentNodeInFetchNode, + }); }); feature.scenarios.forEach((scenario) => { @@ -54,7 +55,9 @@ for (const directory of directories) { const givenQuery = () => { given(/^query$/im, (operationString: string) => { operation = parseOperation(schema, operationString); - expect(validate(schema.toGraphQLJSSchema(), parse(operationString))).toStrictEqual([]); + expect( + validate(schema.toGraphQLJSSchema(), parse(operationString)), + ).toStrictEqual([]); }); }; diff --git a/query-planner-js/src/__tests__/buildPlan.defer.test.ts b/query-planner-js/src/__tests__/buildPlan.defer.test.ts index e55da6bcb..bca4a418e 100644 --- a/query-planner-js/src/__tests__/buildPlan.defer.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.defer.test.ts @@ -1,10 +1,21 @@ -import { operationFromDocument, Schema, ServiceDefinition } from '@apollo/federation-internals'; +import { + operationFromDocument, + Schema, + ServiceDefinition, +} from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { QueryPlanner } from '@apollo/query-planner'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from "./testHelper"; - -function composeAndCreatePlannerWithDefer(...services: ServiceDefinition[]): [Schema, QueryPlanner] { - return composeAndCreatePlannerWithOptions(services, { incrementalDelivery: { enableDefer : true }}); +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; + +function composeAndCreatePlannerWithDefer( + ...services: ServiceDefinition[] +): [Schema, QueryPlanner] { + return composeAndCreatePlannerWithOptions(services, { + incrementalDelivery: { enableDefer: true }, + }); } describe('handles simple @defer', () => { @@ -12,14 +23,14 @@ describe('handles simple @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -29,21 +40,24 @@ describe('handles simple @defer', () => { v1: Int v2: Int } - ` - } + `, + }; test('without defer-support enabled', () => { const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); // Without defer-support enabled, we should get the same plan than if `@defer` wasn't there. const plan = queryPlanner.buildQueryPlan(operation); @@ -80,17 +94,23 @@ describe('handles simple @defer', () => { }); test('with defer-support enabled', () => { - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -161,14 +181,14 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -182,22 +202,28 @@ describe('non-router-based-defer', () => { a: Int b: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v { - a - ... @defer { - b + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v { + a + ... @defer { + b + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // We cannot handle a @defer on value type at the query planning level, so we expect nothing to be @@ -259,15 +285,15 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id", resolvable: false) { id: ID! v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -276,20 +302,26 @@ describe('non-router-based-defer', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - v1 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... @defer { + v1 + } + v2 } - v2 } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While the @defer in the operation is on an entity, the @key in the first subgraph @@ -348,7 +380,7 @@ describe('non-router-based-defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -359,8 +391,8 @@ describe('non-router-based-defer', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -378,24 +410,30 @@ describe('non-router-based-defer', () => { type U @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v { - a - ... @defer { - u { - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v { + a + ... @defer { + u { + x + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While we cannot defer the initial resolving of `u`, we can defer the fetch of it's `x` field, @@ -478,7 +516,7 @@ test('@defer resuming in same subgraph', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -486,20 +524,23 @@ test('@defer resuming in same subgraph', () => { v0: String v1: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - v1 + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + v1 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -552,7 +593,7 @@ test('@defer multiple fields in different subgraphs', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -560,8 +601,8 @@ test('@defer multiple fields in different subgraphs', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -570,8 +611,8 @@ test('@defer multiple fields in different subgraphs', () => { id: ID! v2: String } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -580,22 +621,29 @@ test('@defer multiple fields in different subgraphs', () => { id: ID! v3: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - v1 - v2 - v3 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + v1 + v2 + v3 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -682,7 +730,7 @@ test('multiple (non-nested) @defer + label handling', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { @@ -690,8 +738,8 @@ test('multiple (non-nested) @defer + label handling', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -705,8 +753,8 @@ test('multiple (non-nested) @defer + label handling', () => { type U @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -716,29 +764,36 @@ test('multiple (non-nested) @defer + label handling', () => { x: Int y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer(label: "defer_v1") { - v1 - } - ... @defer { - v2 - } - v3 { - x - ... @defer(label: "defer_in_v3") { - y + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer(label: "defer_v1") { + v1 + } + ... @defer { + v2 + } + v3 { + x + ... @defer(label: "defer_in_v3") { + y + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -870,15 +925,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -893,30 +948,36 @@ describe('nested @defer', () => { body: String author: User } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - name - ... on User @defer { - messages { - body - author { - name - ... @defer { - messages { - body + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + name + ... on User @defer { + messages { + body + author { + name + ... @defer { + messages { + body + } } } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1028,15 +1089,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1055,25 +1116,31 @@ describe('nested @defer', () => { paragraphs: [String] lines: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - ... @defer { - messages { - ... @defer { - body { - lines + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + ... @defer { + messages { + ... @defer { + body { + lines + } } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1135,15 +1202,15 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } type User @key(fields: "id") { id: ID! name: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1153,23 +1220,29 @@ describe('nested @defer', () => { age: Int address: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - name - ... @defer { - age + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + name ... @defer { - address + age + ... @defer { + address + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1247,32 +1320,35 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - me : User + me: User } - type User { + type User { id: ID! name: String age: Int address: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - me { - name - ... @defer { - age + const operation = operationFromDocument( + api, + gql` + { + me { + name ... @defer { - address + age + ... @defer { + address + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1320,7 +1396,7 @@ describe('nested @defer', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T { @@ -1328,8 +1404,8 @@ describe('nested @defer', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1337,22 +1413,28 @@ describe('nested @defer', () => { type T @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - a + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { ... @defer { - b + a + ... @defer { + b + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that nothing can effectively be deferred, so everything is fetched in the very first fetch, but @@ -1400,7 +1482,7 @@ describe('@defer on mutation', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type Mutation { @@ -1413,9 +1495,8 @@ describe('@defer on mutation', () => { v0: String v1: String } - ` - - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1424,27 +1505,33 @@ describe('@defer on mutation', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation mut { - update1 { - v0 - ... @defer { - v1 - } - } - update2 { - v1 - ... @defer { + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + mutation mut { + update1 { v0 - v2 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // What matters here is that the updates (that go to different fields) are correctly done in sequence, @@ -1545,7 +1632,7 @@ describe('@defer on mutation', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type Mutation { @@ -1557,8 +1644,8 @@ describe('@defer on mutation', () => { v0: String v1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1571,27 +1658,33 @@ describe('@defer on mutation', () => { id: ID! v2: String } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation mut { - update1 { - v0 - ... @defer { - v1 - } - } - update2 { - v1 - ... @defer { + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + mutation mut { + update1 { v0 - v2 + ... @defer { + v1 + } + } + update2 { + v1 + ... @defer { + v0 + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // What matters here is that the updates (that go to different fields) are correctly done in sequence, @@ -1720,8 +1813,8 @@ test('multi-dependency deferred section', () => { id0: ID! v1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1731,8 +1824,8 @@ test('multi-dependency deferred section', () => { id1: ID! v2: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1742,8 +1835,8 @@ test('multi-dependency deferred section', () => { id2: ID! v3: Int } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -1753,22 +1846,30 @@ test('multi-dependency deferred section', () => { id2: ID! v4: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3, subgraph4); - let operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - ... @defer { - v4 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); + let operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + ... @defer { + v4 + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1854,16 +1955,19 @@ test('multi-dependency deferred section', () => { } `); - operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v4 + operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v4 + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); // TODO: the following plan is admittedly not as effecient as it could be, as the 2 queries to @@ -1871,7 +1975,7 @@ test('multi-dependency deferred section', () => { // key dependencies for the deferred block, so it would make more sense to defer those fetches // as well. It is however tricky to both improve this here _and_ maintain the plan generate // just above (which is admittedly optimial). More precisely, what the code currently does is - // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the + // that when it gets to a defer, then it defers the fetch that gets the deferred fields (the // fetch to subgraph 4 here), but it puts the "condition" resolution for the key of that fetch // in the non-deferred section. Here, resolving that fetch conditions is what creates the // dependency on the the fetches to subgraph 2 and 3, and so those get non-deferred. @@ -1981,8 +2085,8 @@ describe('@require', () => { id: ID! v1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1992,8 +2096,8 @@ describe('@require', () => { v2: Int @requires(fields: "v3") v3: Int @external } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2002,20 +2106,27 @@ describe('@require', () => { id: ID! v3: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2097,7 +2208,7 @@ describe('@provides', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T @provides(fields: "v2") + t: T @provides(fields: "v2") } type T @key(fields: "id") { @@ -2105,8 +2216,8 @@ describe('@provides', () => { v1: Int v2: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2115,20 +2226,26 @@ describe('@provides', () => { id: ID! v2: Int @shareable } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - v1 - ... @defer { - v2 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + ... @defer { + v2 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2182,7 +2299,7 @@ test('@defer on query root type', () => { name: 'Subgraph1', typeDefs: gql` type Query { - op1 : Int + op1: Int op2: A } @@ -2191,8 +2308,8 @@ test('@defer on query root type', () => { y: Int next: Query } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2201,25 +2318,31 @@ test('@defer on query root type', () => { op3: Int op4: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - op2 { - x - y - next { - op3 - ... @defer { - op1 - op4 + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + op2 { + x + y + next { + op3 + ... @defer { + op1 + op4 + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2302,8 +2425,8 @@ test('@defer on everything queried', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2312,20 +2435,26 @@ test('@defer on everything queried', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - ... @defer { - t { - x - y + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + ... @defer { + t { + x + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2390,8 +2519,8 @@ test('@defer everything within entity', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2400,20 +2529,26 @@ test('@defer everything within entity', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... @defer { - x - y + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... @defer { + x + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2475,13 +2610,16 @@ test('@defer everything within entity', () => { }); describe('defer with conditions', () => { - test.each([{ - name: 'without explicit label', - label: undefined, - }, { - name: 'with explicit label', - label: 'testLabel', - }])('simple @defer with condition $name', ({label}) => { + test.each([ + { + name: 'without explicit label', + label: undefined, + }, + { + name: 'with explicit label', + label: 'testLabel', + }, + ])('simple @defer with condition $name', ({ label }) => { const subgraph1 = { name: 'Subgraph1', typeDefs: gql` @@ -2493,8 +2631,8 @@ describe('defer with conditions', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2503,11 +2641,16 @@ describe('defer with conditions', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` query($cond: Boolean) { t { x @@ -2516,7 +2659,8 @@ describe('defer with conditions', () => { } } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2540,7 +2684,9 @@ describe('defer with conditions', () => { } } }, [ - Deferred(depends: [0], path: "t"${label ? `, label: "${label}"` : ''}) { + Deferred(depends: [0], path: "t"${ + label ? `, label: "${label}"` : '' + }) { { y }: @@ -2612,20 +2758,23 @@ describe('defer with conditions', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - query($cond: Boolean) { - t { - x - ... @defer(if: $cond) { - y + const operation = operationFromDocument( + api, + gql` + query ($cond: Boolean) { + t { + x + ... @defer(if: $cond) { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2704,8 +2853,8 @@ describe('defer with conditions', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2714,8 +2863,8 @@ describe('defer with conditions', () => { id: ID! y: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2724,28 +2873,35 @@ describe('defer with conditions', () => { id: ID! b: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - query($cond1: Boolean, $cond2: Boolean) { - t { - x - ... @defer(if: $cond1, label: "foo") { - y - } - ... @defer(if: $cond2, label: "bar") { - u { - a - ... @defer(if: $cond1) { - b + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + query ($cond1: Boolean, $cond2: Boolean) { + t { + x + ... @defer(if: $cond1, label: "foo") { + y + } + ... @defer(if: $cond2, label: "bar") { + u { + a + ... @defer(if: $cond1) { + b + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3080,8 +3236,8 @@ test('defer when some interface has different definitions in different subgraphs a: Int c: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3095,21 +3251,27 @@ test('defer when some interface has different definitions in different subgraphs a: Int @external b: Int @requires(fields: "a") } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query Dimensions { - i { - a - b - ... @defer { - c + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + query Dimensions { + i { + a + b + ... @defer { + c + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3180,8 +3342,8 @@ describe('named fragments', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3191,22 +3353,28 @@ describe('named fragments', () => { x: Int y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...TestFragment @defer + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...TestFragment @defer + } } - } - fragment TestFragment on T { - x - y - } - `); + fragment TestFragment on T { + x + y + } + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3264,8 +3432,8 @@ describe('named fragments', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3276,28 +3444,34 @@ describe('named fragments', () => { y: Int z: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...Fragment1 - ...Fragment2 @defer + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...Fragment1 + ...Fragment2 @defer + } } - } - fragment Fragment1 on T { - x - y - } + fragment Fragment1 on T { + x + y + } - fragment Fragment2 on T { - y - z - } - `); + fragment Fragment2 on T { + y + z + } + `, + ); // Field 'y' is queried twice, both in the deferred and non-deferred section. The spec says that // means the field is requested twice, so ensures that's what we do. @@ -3384,8 +3558,8 @@ describe('named fragments', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3394,23 +3568,29 @@ describe('named fragments', () => { id: ID! y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ...OnT @defer - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ...OnT @defer + x + } } - } - fragment OnT on T { - y - __typename - } - `); + fragment OnT on T { + y + __typename + } + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3476,8 +3656,8 @@ test('do not merge query branches with @defer', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3486,24 +3666,30 @@ test('do not merge query branches with @defer', () => { id: ID! c: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1, subgraph2); + const [api, queryPlanner] = composeAndCreatePlannerWithDefer( + subgraph1, + subgraph2, + ); // We have 2 separate @defer, so we should 2 deferred parts, not 1 defer parts with parallel fetches. - const operation = operationFromDocument(api, gql` - { - t { - a - ... @defer { - b - } - ... @defer { - c + const operation = operationFromDocument( + api, + gql` + { + t { + a + ... @defer { + b + } + ... @defer { + c + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3576,27 +3762,30 @@ test('@defer only the key of an entity', () => { name: 'Subgraph1', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id") { id: ID! v0: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - v0 - ... @defer { - id + const operation = operationFromDocument( + api, + gql` + { + t { + v0 + ... @defer { + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Making sure that the deferred part has no fetches since we only defer the key @@ -3635,7 +3824,7 @@ test('the path in @defer includes traversed fragments', () => { name: 'Subgraph1', typeDefs: gql` type Query { - i : I + i: I } interface I { @@ -3652,24 +3841,27 @@ test('the path in @defer includes traversed fragments', () => { v1: String v2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlannerWithDefer(subgraph1); - const operation = operationFromDocument(api, gql` - { - i { - ... on A { - t { - v1 - ... @defer { - v2 + const operation = operationFromDocument( + api, + gql` + { + i { + ... on A { + t { + v1 + ... @defer { + v2 + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` diff --git a/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts b/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts index 27335501e..34847a1b5 100644 --- a/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.interfaceObject.test.ts @@ -1,7 +1,7 @@ import { assert, operationFromDocument } from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { isPlanNode } from '../QueryPlan'; -import { composeAndCreatePlanner, findFetchNodes } from "./testHelper"; +import { composeAndCreatePlanner, findFetchNodes } from './testHelper'; describe('basic @key on interface/@interfaceObject handling', () => { const subgraph1 = { @@ -27,8 +27,8 @@ describe('basic @key on interface/@interfaceObject handling', () => { x: Int w: Int } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -41,21 +41,24 @@ describe('basic @key on interface/@interfaceObject handling', () => { id: ID! y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); test('can use a @key on an @interfaceObject type', () => { // Start by ensuring we can use the key on an @interfaceObject type - const operation = operationFromDocument(api, gql` - { - iFromS1 { - x - y + const operation = operationFromDocument( + api, + gql` + { + iFromS1 { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -88,17 +91,20 @@ describe('basic @key on interface/@interfaceObject handling', () => { }, } `); - }); + }); test('can use a @key on an interface "from" an @interfaceObject type', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - x - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -135,13 +141,16 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('only uses an @interfaceObject if it can', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -158,14 +167,17 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('does not rely on an @interfaceObject directly for `__typename`', () => { - const operation = operationFromDocument(api, gql` - { - iFromS2 { - __typename - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + __typename + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -205,15 +217,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { // fact that we "filter" a single implementation should act as if `__typename` was queried // (effectively, the gateway/router need that `__typename` to decide if the returned data // should be included or not. - const operation = operationFromDocument(api, gql` - { - iFromS2 { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -263,15 +278,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { }); test('can use a @key on an @interfaceObject type even for a concrete implementation', () => { - const operation = operationFromDocument(api, gql` - { - iFromS1 { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + iFromS1 { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -319,15 +337,18 @@ describe('basic @key on interface/@interfaceObject handling', () => { test('handles query of an interface field (that is not on the `@interfaceObject`) for a specific implementation when query starts on the @interfaceObject', () => { // Here, we start on S2, but `x` is only in S1. Further, while `x` is on the `I` interface, we only query it for `A`. - const operation = operationFromDocument(api, gql` - { - iFromS2 { - ... on A { - x + const operation = operationFromDocument( + api, + gql` + { + iFromS2 { + ... on A { + x + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -377,8 +398,8 @@ it('avoids buffering @interfaceObject results that may have to filtered with lis id: ID! expansiveField: String } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -396,21 +417,24 @@ it('avoids buffering @interfaceObject results that may have to filtered with lis id: ID! b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - everything { - ... on A { - a - expansiveField + const operation = operationFromDocument( + api, + gql` + { + everything { + ... on A { + a + expansiveField + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -470,8 +494,8 @@ it('handles @requires on concrete type of field provided by interface object', ( id: ID! x: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -495,20 +519,23 @@ it('handles @requires on concrete type of field provided by interface object', ( id: ID! x: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on A { - y + const operation = operationFromDocument( + api, + gql` + { + i { + ... on A { + y + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -573,8 +600,8 @@ it('handles @interfaceObject in nested entity', () => { type T { relatedIs: [I] } - ` - } + `, + }; const subgraph2 = { name: 'S2', @@ -597,22 +624,25 @@ it('handles @interfaceObject in nested entity', () => { id: ID! a: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - t { - relatedIs { - a + const operation = operationFromDocument( + api, + gql` + { + i { + t { + relatedIs { + a + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` diff --git a/query-planner-js/src/__tests__/buildPlan.subscription.test.ts b/query-planner-js/src/__tests__/buildPlan.subscription.test.ts index 3f0e6c12a..33e103bd4 100644 --- a/query-planner-js/src/__tests__/buildPlan.subscription.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.subscription.test.ts @@ -1,6 +1,9 @@ import { operationFromDocument } from '@apollo/federation-internals'; import gql from 'graphql-tag'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from './testHelper'; +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; describe('subscription query plan tests', () => { it('basic subscription query plan', () => { @@ -187,7 +190,10 @@ describe('subscription query plan tests', () => { `, }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraphA, subgraphB], { incrementalDelivery: { enableDefer: true } }); + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraphA, subgraphB], + { incrementalDelivery: { enableDefer: true } }, + ); const operation = operationFromDocument( api, gql` @@ -202,6 +208,8 @@ describe('subscription query plan tests', () => { } `, ); - expect(() => queryPlanner.buildQueryPlan(operation)).toThrow('@defer is not supported on subscriptions'); + expect(() => queryPlanner.buildQueryPlan(operation)).toThrow( + '@defer is not supported on subscriptions', + ); }); }); diff --git a/query-planner-js/src/__tests__/buildPlan.test.ts b/query-planner-js/src/__tests__/buildPlan.test.ts index f2a1e0d10..d3cba514b 100644 --- a/query-planner-js/src/__tests__/buildPlan.test.ts +++ b/query-planner-js/src/__tests__/buildPlan.test.ts @@ -1,9 +1,17 @@ import { QueryPlanner } from '@apollo/query-planner'; -import { assert, operationFromDocument, ServiceDefinition, Supergraph } from '@apollo/federation-internals'; +import { + assert, + operationFromDocument, + ServiceDefinition, + Supergraph, +} from '@apollo/federation-internals'; import gql from 'graphql-tag'; import { FetchNode, FlattenNode, SequenceNode } from '../QueryPlan'; import { FieldNode, OperationDefinitionNode, parse } from 'graphql'; -import { composeAndCreatePlanner, composeAndCreatePlannerWithOptions } from './testHelper'; +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; import { enforceQueryPlannerConfigDefaults } from '../config'; describe('shareable root fields', () => { @@ -19,8 +27,8 @@ describe('shareable root fields', () => { id: ID! prop1: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -33,18 +41,21 @@ describe('shareable root fields', () => { id: ID! prop2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - me { - prop1 - prop2 + const operation = operationFromDocument( + api, + gql` + { + me { + prop1 + prop2 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that even though we have keys, it is faster to query both @@ -82,8 +93,8 @@ describe('shareable root fields', () => { id: ID! ${fields.map((f) => `${f}: Int\n`)} } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -95,8 +106,8 @@ describe('shareable root fields', () => { type User @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -108,17 +119,24 @@ describe('shareable root fields', () => { type User @key(fields: "id") { id: ID! } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` { me { ${fields.map((f) => `${f}\n`)} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -172,8 +190,8 @@ test('pick keys that minimize fetches', () => { type Country @key(fields: "iso") { iso: String! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -193,26 +211,29 @@ test('pick keys that minimize fetches', () => { name: String! sign: String! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - transfers { - from { - currency { - name + const operation = operationFromDocument( + api, + gql` + { + transfers { + from { + currency { + name + } } - } - to { - currency { - sign + to { + currency { + sign + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // We want to make sure we use the key on Transfer just once, not 2 fetches using the keys @@ -274,7 +295,10 @@ describe('@provides', () => { typeDefs: gql` type Query { doSomething: Response - doSomethingWithProvides: Response @provides(fields: "responseValue { subResponseValue { subSubResponseValue } }") + doSomethingWithProvides: Response + @provides( + fields: "responseValue { subResponseValue { subSubResponseValue } }" + ) } type Response { @@ -289,8 +313,8 @@ describe('@provides', () => { id: ID! subSubResponseValue: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -299,21 +323,24 @@ describe('@provides', () => { id: ID! subSubResponseValue: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - doSomething { - responseValue { - subResponseValue { - subSubResponseValue + let operation = operationFromDocument( + api, + gql` + { + doSomething { + responseValue { + subResponseValue { + subSubResponseValue + } } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -353,17 +380,20 @@ describe('@provides', () => { `); // And now make sure with the provides we do only get a fetch to subgraph1 - operation = operationFromDocument(api, gql` - { - doSomethingWithProvides { - responseValue { - subResponseValue { - subSubResponseValue + operation = operationFromDocument( + api, + gql` + { + doSomethingWithProvides { + responseValue { + subResponseValue { + subSubResponseValue + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -409,8 +439,8 @@ describe('@provides', () => { id: ID! v: Value @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -429,19 +459,22 @@ describe('@provides', () => { id: ID! v: Value @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - v { - a + let operation = operationFromDocument( + api, + gql` + { + noProvides { + v { + a + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -495,15 +528,18 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only. - operation = operationFromDocument(api, gql` - { - withProvides { - v { - a + operation = operationFromDocument( + api, + gql` + { + withProvides { + v { + a + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -522,16 +558,19 @@ describe('@provides', () => { `); // Sanity check that if we query `b` however we have to got to subgraph2. - operation = operationFromDocument(api, gql` - { - withProvides { - v { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvides { + v { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -593,7 +632,8 @@ describe('@provides', () => { type Query { noProvides: U withProvidesForT1: U @provides(fields: "... on T1 { a }") - withProvidesForBoth: U @provides(fields: "... on T1 { a } ... on T2 {b}") + withProvidesForBoth: U + @provides(fields: "... on T1 { a } ... on T2 {b}") } union U = T1 | T2 @@ -608,8 +648,8 @@ describe('@provides', () => { a: Int b: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -623,23 +663,26 @@ describe('@provides', () => { id: ID! b: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - ... on T1 { - a - } - ... on T2 { - a - b + let operation = operationFromDocument( + api, + gql` + { + noProvides { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -690,18 +733,21 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only when provided. - operation = operationFromDocument(api, gql` - { - withProvidesForT1 { - ... on T1 { - a - } - ... on T2 { - a + operation = operationFromDocument( + api, + gql` + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -723,19 +769,22 @@ describe('@provides', () => { `); // But ensure that querying `b` still goes to subgraph2 if only a is provided. - operation = operationFromDocument(api, gql` - { - withProvidesForT1 { - ... on T1 { - a - } - ... on T2 { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvidesForT1 { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -776,19 +825,22 @@ describe('@provides', () => { `); // Lastly, if both are provided, ensures we only hit subgraph1. - operation = operationFromDocument(api, gql` - { - withProvidesForBoth { - ... on T1 { - a - } - ... on T2 { - a - b + operation = operationFromDocument( + api, + gql` + { + withProvidesForBoth { + ... on T1 { + a + } + ... on T2 { + a + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -837,8 +889,8 @@ describe('@provides', () => { a: Int @external b: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -853,18 +905,21 @@ describe('@provides', () => { a: Int @shareable b: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - a - b + let operation = operationFromDocument( + api, + gql` + { + noProvides { + a + b + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -916,13 +971,16 @@ describe('@provides', () => { `); // Ensuring that querying only `a` can be done with subgraph1 only. - operation = operationFromDocument(api, gql` - { - withProvidesOnA { - a + operation = operationFromDocument( + api, + gql` + { + withProvidesOnA { + a + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -944,13 +1002,16 @@ describe('@provides', () => { `); // Ensuring that for `b`, only the T2 value is provided by subgraph1. - operation = operationFromDocument(api, gql` - { - withProvidesOnB { - b + operation = operationFromDocument( + api, + gql` + { + withProvidesOnB { + b + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -990,15 +1051,18 @@ describe('@provides', () => { `); // But if we only query for T2, then no reason to go to subgraph2. - operation = operationFromDocument(api, gql` - { - withProvidesOnB { - ... on T2 { - b + operation = operationFromDocument( + api, + gql` + { + withProvidesOnB { + ... on T2 { + b + } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1045,8 +1109,8 @@ describe('@provides', () => { id: ID! a: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1071,25 +1135,28 @@ describe('@provides', () => { a: Int @shareable c: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - { - noProvides { - i { - a - ... on T1 { - b - } - ... on T2 { - c + let operation = operationFromDocument( + api, + gql` + { + noProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); // This is our sanity check: we first query _without_ the provides to make sure we _do_ need to @@ -1134,21 +1201,24 @@ describe('@provides', () => { `); // But the same operation with the provides allow to get what is provided from the first subgraph. - operation = operationFromDocument(api, gql` - { - withProvides { - i { - a - ... on T1 { - b - } - ... on T2 { - c + operation = operationFromDocument( + api, + gql` + { + withProvides { + i { + a + ... on T1 { + b + } + ... on T2 { + c + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1224,8 +1294,8 @@ describe('@requires', () => { f: Int g: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1241,17 +1311,20 @@ describe('@requires', () => { f: Int @external g: Int @requires(fields: "f") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - is { - g + const operation = operationFromDocument( + api, + gql` + { + is { + g + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly @@ -1327,8 +1400,8 @@ describe('@requires', () => { id: ID! value: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -1345,21 +1418,24 @@ describe('@requires', () => { value: String @external computed: String @requires(fields: "value") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - list { - computed - computed2 - user { + const operation = operationFromDocument( + api, + gql` + { + list { computed + computed2 + user { + computed + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // The main goal of this test is to show that the 2 @requires for `f` gets handled seemlessly @@ -1421,7 +1497,7 @@ describe('@requires', () => { }, } `); - }) + }); it('handles simple require chain (require that depends on another require)', () => { const subgraph1 = { @@ -1435,19 +1511,19 @@ describe('@requires', () => { id: ID! v: Int! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - v: Int! @external - inner: Int! @requires(fields: "v") + id: ID! + v: Int! @external + inner: Int! @requires(fields: "v") } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1457,18 +1533,25 @@ describe('@requires', () => { inner: Int! @external outer: Int! @requires(fields: "inner") } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // Ensures that if we only ask `outer`, we get everything needed in between. - let operation = operationFromDocument(api, gql` - { - t { - outer + let operation = operationFromDocument( + api, + gql` + { + t { + outer + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1523,15 +1606,18 @@ describe('@requires', () => { // (note: technically it happens to switch the order of fields in the inputs of "Subgraph2" // so the plans are not 100% the same "string", which is why we inline it in both cases, // but that's still the same plan and a perfectly valid output). - operation = operationFromDocument(api, gql` - { - t { - v - inner - outer + operation = operationFromDocument( + api, + gql` + { + t { + v + inner + outer + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -1596,19 +1682,19 @@ describe('@requires', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - v: Int! @external - inner: Int! @requires(fields: "v") + id: ID! + v: Int! @external + inner: Int! @requires(fields: "v") } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -1618,8 +1704,8 @@ describe('@requires', () => { inner: Int! @external outer: Int! @requires(fields: "inner") } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -1628,18 +1714,26 @@ describe('@requires', () => { id: ID! v: Int! } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); // Ensures that if we only ask `outer`, we get everything needed in between. - let operation = operationFromDocument(api, gql` - { - t { - outer - } - } - `); + let operation = operationFromDocument( + api, + gql` + { + t { + outer + } + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); const expectedPlan = ` @@ -1706,15 +1800,18 @@ describe('@requires', () => { expect(plan).toMatchInlineSnapshot(expectedPlan); // Ensures that manually asking for the required dependencies doesn't change anything. - operation = operationFromDocument(api, gql` - { - t { - v - inner - outer + operation = operationFromDocument( + api, + gql` + { + t { + v + inner + outer + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(expectedPlan); @@ -1732,8 +1829,8 @@ describe('@requires', () => { id: ID! v1: Int! } - ` - } + `, + }; const totalRequires = 10; const subgraphs: ServiceDefinition[] = [subgraph1]; @@ -1743,22 +1840,25 @@ describe('@requires', () => { typeDefs: gql` type T @key(fields: "id") { id: ID! - v${i-1}: Int! @external - v${i}: Int! @requires(fields: "v${i-1}") + v${i - 1}: Int! @external + v${i}: Int! @requires(fields: "v${i - 1}") } - ` + `, }); } const [api, queryPlanner] = composeAndCreatePlanner(...subgraphs); // Ensures that if we only ask `outer`, we get everything needed in between. - const operation = operationFromDocument(api, gql` + const operation = operationFromDocument( + api, + gql` { t { v${totalRequires} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); const dependentFetches: string[] = []; @@ -1768,7 +1868,7 @@ describe('@requires', () => { { ... on T { __typename - v${i-1} + v${i - 1} id } } => @@ -1778,8 +1878,7 @@ describe('@requires', () => { } } }, - },` - ); + },`); } const expectedPlan = ` QueryPlan { @@ -1813,37 +1912,37 @@ describe('@requires', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner1: Int! - inner2_required: Int! + id: ID! + inner1: Int! + inner2_required: Int! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner2_required: Int! @external - inner2: Int! @requires(fields: "inner2_required") + id: ID! + inner2_required: Int! @external + inner2: Int! @requires(fields: "inner2_required") } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner3: Inner3Type! + id: ID! + inner3: Inner3Type! } type Inner3Type @key(fields: "k3") { @@ -1854,8 +1953,8 @@ describe('@requires', () => { k4: ID! inner4_required: Int! } - ` - } + `, + }; const subgraph5 = { name: 'Subgraph5', @@ -1867,54 +1966,68 @@ describe('@requires', () => { inner3: Inner3Type! @external inner4: Inner4Type! @external inner5: Int! @external - outer: Int! @requires(fields: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5") + outer: Int! + @requires( + fields: "inner1 inner2 inner3 { inner3_nested } inner4 { inner4_nested } inner5" + ) } - type Inner3Type @key(fields: "k3") { - k3: ID! - inner3_nested: Int! - } + type Inner3Type @key(fields: "k3") { + k3: ID! + inner3_nested: Int! + } - type Inner4Type @key(fields: "k4") { - k4: ID! - inner4_nested: Int! @requires(fields: "inner4_required") - inner4_required: Int! @external - } - ` - } + type Inner4Type @key(fields: "k4") { + k4: ID! + inner4_nested: Int! @requires(fields: "inner4_required") + inner4_required: Int! @external + } + `, + }; const subgraph6 = { name: 'Subgraph6', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner4: Inner4Type! + id: ID! + inner4: Inner4Type! } type Inner4Type @key(fields: "k4") { k4: ID! } - ` - } + `, + }; const subgraph7 = { name: 'Subgraph7', typeDefs: gql` type T @key(fields: "id") { - id: ID! - inner5: Int! - } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4, subgraph5, subgraph6, subgraph7); - const operation = operationFromDocument(api, gql` - { - t { - outer + id: ID! + inner5: Int! + } + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + subgraph5, + subgraph6, + subgraph7, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + outer + } } - } - `); + `, + ); // This is a big plan, but afaict, this is optimal. That is, there is 3 main steps: // 1. it get the `id` for `T`, which is needed for anything else. @@ -2122,8 +2235,8 @@ describe('@requires', () => { v1: String v2: String } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2139,26 +2252,32 @@ describe('@requires', () => { v1: String v2: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const op1 = operationFromDocument(api, gql` - { - entity { - f2 - f3 + const op1 = operationFromDocument( + api, + gql` + { + entity { + f2 + f3 + } } - } - `); + `, + ); - const op2 = operationFromDocument(api, gql` - { - entity { - f3 + const op2 = operationFromDocument( + api, + gql` + { + entity { + f3 + } } - } - `); + `, + ); const plan1 = queryPlanner.buildQueryPlan(op2); const expectedPlan = ` @@ -2253,14 +2372,14 @@ describe('@requires', () => { name: 'A', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id1") { id1: ID! } - ` - } + `, + }; const subgraph2 = { name: 'B', @@ -2271,8 +2390,8 @@ describe('@requires', () => { v1: Int v2: Int } - ` - } + `, + }; const subgraph3 = { name: 'C', @@ -2281,8 +2400,8 @@ describe('@requires', () => { id1: ID! v3: Int } - ` - } + `, + }; const subgraph4 = { name: 'D', @@ -2292,20 +2411,28 @@ describe('@requires', () => { v3: Int @external v4: Int @requires(fields: "v3") } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); // The optimal plan should: // 1. fetch id1 from A @@ -2395,8 +2522,8 @@ describe('@requires', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2406,17 +2533,20 @@ describe('@requires', () => { a: Int @external b: Int @requires(fields: "a") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query foo($test: Boolean!){ - t @include(if: $test) { - b + const operation = operationFromDocument( + api, + gql` + query foo($test: Boolean!) { + t @include(if: $test) { + b + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2466,8 +2596,8 @@ describe('@requires', () => { id: ID! a: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2477,17 +2607,20 @@ describe('@requires', () => { a: Int @external b: Int @requires(fields: "a") } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query foo($test: Boolean!){ - t { - b @include(if: $test) + const operation = operationFromDocument( + api, + gql` + query foo($test: Boolean!) { + t { + b @include(if: $test) + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2538,8 +2671,8 @@ describe('@requires', () => { type A @key(fields: "idA") { idA: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2553,8 +2686,8 @@ describe('@requires', () => { idB: ID! required: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -2564,19 +2697,26 @@ describe('@requires', () => { c: Int @requires(fields: "required") required: Int @external } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - query foo($test1: Boolean!, $test2: Boolean!){ - a @include(if: $test1) { - b @include(if: $test2) { - c + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + query foo($test1: Boolean!, $test2: Boolean!) { + a @include(if: $test1) { + b @include(if: $test2) { + c + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2655,8 +2795,8 @@ describe('@requires', () => { a: String @inaccessible onlyIn1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -2668,20 +2808,23 @@ describe('@requires', () => { type One @key(fields: "id") { id: ID! a: String @external - b: String @requires(fields: "a" ) + b: String @requires(fields: "a") onlyIn2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - one { - b + const operation = operationFromDocument( + api, + gql` + { + one { + b + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2728,15 +2871,15 @@ describe('@requires', () => { name: 'A', typeDefs: gql` type Query { - t : T + t: T } type T @key(fields: "id1") @key(fields: "req1") { id1: ID! req1: Int } - ` - } + `, + }; const subgraph2 = { name: 'B', @@ -2747,8 +2890,8 @@ describe('@requires', () => { req2: Int @external v: Int @requires(fields: "req1 req2") } - ` - } + `, + }; const subgraph3 = { name: 'C', @@ -2757,17 +2900,24 @@ describe('@requires', () => { req1: Int req2: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - v + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2832,8 +2982,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'non-graphql-name', @@ -2842,17 +2992,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2884,7 +3037,8 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; expect(fetch.operation).toMatch(/^query myOp__non_graphql_name__1.*/i); }); @@ -2899,8 +3053,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'a-na&me-with-plen&ty-replace*ments', @@ -2909,17 +3063,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -2951,8 +3108,11 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; - expect(fetch.operation).toMatch(/^query myOp__a_name_with_plenty_replacements__1.*/i); + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; + expect(fetch.operation).toMatch( + /^query myOp__a_name_with_plenty_replacements__1.*/i, + ); }); test('handle very non-graph subgraph name', () => { @@ -2966,8 +3126,8 @@ describe('fetch operation names', () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: '42!', @@ -2976,17 +3136,20 @@ describe('fetch operation names', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query myOp { - t { - x + const operation = operationFromDocument( + api, + gql` + query myOp { + t { + x + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3018,7 +3181,8 @@ describe('fetch operation names', () => { }, } `); - const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode).node as FetchNode; + const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode) + .node as FetchNode; expect(fetch.operation).toMatch(/^query myOp___42__1.*/i); }); @@ -3032,7 +3196,8 @@ test('Correctly handle case where there is too many plans to consider', () => { // gets very large very quickly). Obviously, there is no reason to do this in practice. // Each leaf field is reachable from 2 subgraphs, so doubles the number of plans. - const defaultMaxComputedPlans = enforceQueryPlannerConfigDefaults().debug.maxEvaluatedPlans!; + const defaultMaxComputedPlans = + enforceQueryPlannerConfigDefaults().debug.maxEvaluatedPlans!; const fieldCount = Math.ceil(Math.log2(defaultMaxComputedPlans)) + 1; const fields = [...Array(fieldCount).keys()].map((i) => `f${i}`); @@ -3046,14 +3211,20 @@ test('Correctly handle case where there is too many plans to consider', () => { } `; - const [api, queryPlanner] = composeAndCreatePlanner({ name: 'S1', typeDefs }, { name: 'S2', typeDefs }); - const operation = operationFromDocument(api, gql` + const [api, queryPlanner] = composeAndCreatePlanner( + { name: 'S1', typeDefs }, + { name: 'S2', typeDefs }, + ); + const operation = operationFromDocument( + api, + gql` { t { ${fields.map((f) => `${f}\n`)} } } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a @@ -3073,9 +3244,11 @@ test('Correctly handle case where there is too many plans to consider', () => { // ... all fields // } // } - const mainSelection = (fetchOp.definitions[0] as OperationDefinitionNode).selectionSet; + const mainSelection = (fetchOp.definitions[0] as OperationDefinitionNode) + .selectionSet; const subSelection = (mainSelection.selections[0] as FieldNode).selectionSet; - const queriedFields = subSelection?.selections.map((s) => (s as FieldNode).name.value) ?? []; + const queriedFields = + subSelection?.selections.map((s) => (s as FieldNode).name.value) ?? []; fields.sort(); // Note that alphabetical order is not numerical order, hence this queriedFields.sort(); expect(queriedFields).toStrictEqual(fields); @@ -3128,19 +3301,22 @@ describe('Field covariance and type-explosion', () => { const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - { - dummy { - field { - ... on Object { - field { - __typename + const operation = operationFromDocument( + api, + gql` + { + dummy { + field { + ... on Object { + field { + __typename + } } } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3180,8 +3356,8 @@ describe('Field covariance and type-explosion', () => { field: Object @provides(fields: "x") x: Int @external } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3190,23 +3366,26 @@ describe('Field covariance and type-explosion', () => { id: ID! x: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - dummy { - field { - ... on Object { - field { - __typename + const operation = operationFromDocument( + api, + gql` + { + dummy { + field { + ... on Object { + field { + __typename + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3229,7 +3408,7 @@ describe('Field covariance and type-explosion', () => { } `); }); -}) +}); describe('handles non-intersecting fragment conditions', () => { test('with federation 1 supergraphs', () => { @@ -3275,30 +3454,33 @@ describe('handles non-intersecting fragment conditions', () => { enum join__Graph { S1 @join__graph(name: "S1" url: "") } - ` + `; const supergraph = Supergraph.build(supergraphSdl); const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - fragment OrangeYouGladIDidntSayBanana on Fruit { - ... on Banana { - inBunch - } - ... on Apple { - hasStem + const operation = operationFromDocument( + api, + gql` + fragment OrangeYouGladIDidntSayBanana on Fruit { + ... on Banana { + inBunch + } + ... on Apple { + hasStem + } } - } - query Fruitiness { - fruit { - ... on Apple { - ...OrangeYouGladIDidntSayBanana + query Fruitiness { + fruit { + ... on Apple { + ...OrangeYouGladIDidntSayBanana + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3337,28 +3519,31 @@ describe('handles non-intersecting fragment conditions', () => { type Query { fruit: Fruit! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - fragment OrangeYouGladIDidntSayBanana on Fruit { - ... on Banana { - inBunch - } - ... on Apple { - hasStem + const operation = operationFromDocument( + api, + gql` + fragment OrangeYouGladIDidntSayBanana on Fruit { + ... on Banana { + inBunch + } + ... on Apple { + hasStem + } } - } - query Fruitiness { - fruit { - ... on Apple { - ...OrangeYouGladIDidntSayBanana + query Fruitiness { + fruit { + ... on Apple { + ...OrangeYouGladIDidntSayBanana + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3402,8 +3587,8 @@ test('avoids unnecessary fetches', () => { type A @key(fields: "idA2") { idA2: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3413,12 +3598,11 @@ test('avoids unnecessary fetches', () => { u: U } - type U @key(fields: "idU") { idU: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -3426,8 +3610,8 @@ test('avoids unnecessary fetches', () => { type A @key(fields: "idA1") { idA1: ID! } - ` - } + `, + }; const subgraph4 = { name: 'Subgraph4', @@ -3436,8 +3620,8 @@ test('avoids unnecessary fetches', () => { idA1: ID! idA2: ID! } - ` - } + `, + }; const subgraph5 = { name: 'Subgraph5', @@ -3446,22 +3630,31 @@ test('avoids unnecessary fetches', () => { idU: ID! v: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3, subgraph4, subgraph5); - const operation = operationFromDocument(api, gql` - { - t { - u { - v - } - a { - idA1 + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + subgraph4, + subgraph5, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + u { + v + } + a { + idA1 + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` QueryPlan { @@ -3585,15 +3778,18 @@ describe('Fed1 supergraph handling', () => { const api = supergraph.apiSchema(); const queryPlanner = new QueryPlanner(supergraph); - const operation = operationFromDocument(api, gql` - { - t { - nodes { - id + const operation = operationFromDocument( + api, + gql` + { + t { + nodes { + id + } } } - } - `); + `, + ); const queryPlan = queryPlanner.buildQueryPlan(operation); expect(queryPlan).toMatchInlineSnapshot(` @@ -3647,48 +3843,51 @@ describe('Named fragments preservation', () => { child: Foo child2: Foo } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query { - a { - ...on A1 { - ...FooSelect - } - ...on A2 { - ...FooSelect - } - ...on A3 { - ...FooSelect + const operation = operationFromDocument( + api, + gql` + query { + a { + ... on A1 { + ...FooSelect + } + ... on A2 { + ...FooSelect + } + ... on A3 { + ...FooSelect + } } } - } - fragment FooSelect on Foo { - __typename - foo - child { - ...FooChildSelect - } - child2 { - ...FooChildSelect + fragment FooSelect on Foo { + __typename + foo + child { + ...FooChildSelect + } + child2 { + ...FooChildSelect + } } - } - fragment FooChildSelect on Foo { - __typename - foo - child { + fragment FooChildSelect on Foo { + __typename + foo child { child { - foo + child { + foo + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3757,8 +3956,8 @@ describe('Named fragments preservation', () => { b: Int c: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -3774,31 +3973,34 @@ describe('Named fragments preservation', () => { b: Int c: Int } - ` - } + `, + }; // We use a fragment which does save some on the original query, but as each // field gets to a different subgraph, the fragment would only be used one // on each sub-fetch and we make sure the fragment is not used in that case. const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - t { - v1 { - ...OnV - } - v2 { - ...OnV + let operation = operationFromDocument( + api, + gql` + query { + t { + v1 { + ...OnV + } + v2 { + ...OnV + } } } - } - fragment OnV on V { - a - b - c - } - `); + fragment OnV on V { + a + b + c + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3842,24 +4044,27 @@ describe('Named fragments preservation', () => { // But double-check that if we query 2 fields from the same subgraph, then // the fragment gets used now. - operation = operationFromDocument(api, gql` - query { - t { - v2 { - ...OnV - } - v3 { - ...OnV + operation = operationFromDocument( + api, + gql` + query { + t { + v2 { + ...OnV + } + v3 { + ...OnV + } } } - } - fragment OnV on V { - a - b - c - } - `); + fragment OnV on V { + a + b + c + } + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -3904,47 +4109,55 @@ describe('Named fragments preservation', () => { `); }); - it.each([ true, false ])('respects query planner option "reuseQueryFragments=%p"', (reuseQueryFragments: boolean) => { - const subgraph1 = { - name: 'Subgraph1', - typeDefs: gql` - type Query { - t: T - } - - type T { - a1: A - a2: A - } + it.each([true, false])( + 'respects query planner option "reuseQueryFragments=%p"', + (reuseQueryFragments: boolean) => { + const subgraph1 = { + name: 'Subgraph1', + typeDefs: gql` + type Query { + t: T + } - type A { - x: Int - y: Int - } - ` - } + type T { + a1: A + a2: A + } - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph1], { reuseQueryFragments }); - const operation = operationFromDocument(api, gql` - query { - t { - a1 { - ...Selection + type A { + x: Int + y: Int } - a2 { - ...Selection + `, + }; + + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraph1], + { reuseQueryFragments }, + ); + const operation = operationFromDocument( + api, + gql` + query { + t { + a1 { + ...Selection + } + a2 { + ...Selection + } + } } - } - } - fragment Selection on A { - x - y - } - `); + fragment Selection on A { + x + y + } + `, + ); - const plan = queryPlanner.buildQueryPlan(operation); - const withReuse = ` + const plan = queryPlanner.buildQueryPlan(operation); + const withReuse = ` QueryPlan { Fetch(service: "Subgraph1") { { @@ -3965,7 +4178,7 @@ describe('Named fragments preservation', () => { }, } `; - const withoutReuse = ` + const withoutReuse = ` QueryPlan { Fetch(service: "Subgraph1") { { @@ -3982,10 +4195,13 @@ describe('Named fragments preservation', () => { } }, } - ` + `; - expect(plan).toMatchInlineSnapshot(reuseQueryFragments ? withReuse : withoutReuse); - }); + expect(plan).toMatchInlineSnapshot( + reuseQueryFragments ? withReuse : withoutReuse, + ); + }, + ); it('works with nested fragments when only the nested fragment gets preserved', () => { const subgraph1 = { @@ -3995,7 +4211,7 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: V b: V @@ -4005,32 +4221,34 @@ describe('Named fragments preservation', () => { v1: Int v2: Int } - - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - t { - ...OnT + const operation = operationFromDocument( + api, + gql` + { + t { + ...OnT + } } - } - fragment OnT on T { - a { - ...OnV - } - b { - ...OnV + fragment OnT on T { + a { + ...OnV + } + b { + ...OnV + } } - } - fragment OnV on V { - v1 - v2 - } - `); + fragment OnV on V { + v1 + v2 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4064,28 +4282,31 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query test($if: Boolean) { - t { - id - ...OnT @include(if: $if) + const operation = operationFromDocument( + api, + gql` + query test($if: Boolean) { + t { + id + ...OnT @include(if: $if) + } } - } - fragment OnT on T { - a - b - } - `); + fragment OnT on T { + a + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4113,29 +4334,32 @@ describe('Named fragments preservation', () => { t: T } - type T @key(fields : "id") { + type T @key(fields: "id") { id: ID! a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query test($test1: Boolean, $test2: Boolean) { - t { - id - ...OnT @include(if: $test1) - ...OnT @include(if: $test2) + const operation = operationFromDocument( + api, + gql` + query test($test1: Boolean, $test2: Boolean) { + t { + id + ...OnT @include(if: $test1) + ...OnT @include(if: $test2) + } } - } - fragment OnT on T { - a - b - } - `); + fragment OnT on T { + a + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4184,8 +4408,8 @@ describe('Named fragments preservation', () => { a: Int b: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4194,28 +4418,31 @@ describe('Named fragments preservation', () => { a: Int b: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - query { - i1 { - ... on T { - ...Frag + const operation = operationFromDocument( + api, + gql` + query { + i1 { + ... on T { + ...Frag + } } - } - i2 { - ... on T { - ...Frag + i2 { + ... on T { + ...Frag + } } } - } - fragment Frag on I { - b - } - `); + fragment Frag on I { + b + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4257,7 +4484,7 @@ describe('Named fragments preservation', () => { name: 'Subgraph1', typeDefs: gql` type V @shareable { - x: Int + x: Int } interface I { @@ -4268,8 +4495,8 @@ describe('Named fragments preservation', () => { id: ID! v: V } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4280,7 +4507,7 @@ describe('Named fragments preservation', () => { } type V @shareable { - x: Int + x: Int } interface I { @@ -4298,25 +4525,36 @@ describe('Named fragments preservation', () => { inner: Inner w: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + let operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - v { x } - } - `); + fragment IFrag on I { + v { + x + } + } + `, + ); const expectedPlan = ` QueryPlan { @@ -4382,54 +4620,80 @@ describe('Named fragments preservation', () => { }, } `; - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); // We very slighly modify the operation to add an artificial indirection within `IFrag`. // This does not really change the query, and should result in the same plan, but // ensure the code handle correctly such indirection. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - ...IFragDelegate - } + fragment IFrag on I { + ...IFragDelegate + } - fragment IFragDelegate on I { - v { x } - } - `); + fragment IFragDelegate on I { + v { + x + } + } + `, + ); - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); - // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // The previous cases tests the cases where nothing in the `...IFrag` spread at the // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not // have `v` in particular), it does contains field `w` that `I` also have, so we // add that field to `IFrag` and make sure we still correctly query that field. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment OuterFrag on Outer { - ...IFrag - inner { ...IFrag } - } + fragment OuterFrag on Outer { + ...IFrag + inner { + ...IFrag + } + } - fragment IFrag on I { - v { x } - w - } - `); + fragment IFrag on I { + v { + x + } + w + } + `, + ); expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` QueryPlan { @@ -4506,7 +4770,7 @@ describe('Named fragments preservation', () => { name: 'Subgraph1', typeDefs: gql` type V @shareable { - x: Int + x: Int } union U = Outer @@ -4515,8 +4779,8 @@ describe('Named fragments preservation', () => { id: ID! v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4538,30 +4802,39 @@ describe('Named fragments preservation', () => { inner: Inner w: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - let operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + let operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ... on Outer { - v + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } } - ... on Inner { - v + + fragment UFrag on U { + ... on Outer { + v + } + ... on Inner { + v + } } - } - `); + `, + ); const expectedPlan = ` QueryPlan { @@ -4621,64 +4894,86 @@ describe('Named fragments preservation', () => { }, } `; - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); // We very slighly modify the operation to add an artificial indirection within `IFrag`. // This does not really change the query, and should result in the same plan, but // ensure the code handle correctly such indirection. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ...UFragDelegate - } + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } + } - fragment UFragDelegate on U { - ... on Outer { - v + fragment UFrag on U { + ...UFragDelegate } - ... on Inner { - v + + fragment UFragDelegate on U { + ... on Outer { + v + } + ... on Inner { + v + } } - } - `); + `, + ); - expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(expectedPlan); + expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot( + expectedPlan, + ); - // The previous cases tests the cases where nothing in the `...IFrag` spread at the + // The previous cases tests the cases where nothing in the `...IFrag` spread at the // top-level of `OuterFrag` applied at all: it all gets eliminated in the plan. But // in the schema of `Subgraph2`, while `Outer` does not implement `I` (and does not // have `v` in particular), it does contains field `w` that `I` also have, so we // add that field to `IFrag` and make sure we still correctly query that field. - operation = operationFromDocument(api, gql` - query { - outer1 { ...OuterFrag } - outer2 { ...OuterFrag } - } - - fragment OuterFrag on Outer { - ...UFrag - inner { ...UFrag } - } + operation = operationFromDocument( + api, + gql` + query { + outer1 { + ...OuterFrag + } + outer2 { + ...OuterFrag + } + } - fragment UFrag on U { - ... on Outer { - v - w + fragment OuterFrag on Outer { + ...UFrag + inner { + ...UFrag + } } - ... on Inner { - v + + fragment UFrag on U { + ... on Outer { + v + w + } + ... on Inner { + v + } } - } - `); + `, + ); expect(queryPlanner.buildQueryPlan(operation)).toMatchInlineSnapshot(` QueryPlan { @@ -4753,18 +5048,18 @@ test('works with key chains', () => { type T @key(fields: "id1") { id1: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` - type T @key(fields: "id1") @key(fields: "id2") { + type T @key(fields: "id1") @key(fields: "id2") { id1: ID! id2: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -4774,22 +5069,29 @@ test('works with key chains', () => { x: Int y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // Note: querying `id2` is only purpose, because there is 2 choice to get `id2` (either // from then 2nd or 3rd subgraph), and that create some choice in the query planning algorithm, // so excercices additional paths. - const operation = operationFromDocument(api, gql` - { - t { - id2 - x - y + const operation = operationFromDocument( + api, + gql` + { + t { + id2 + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4852,18 +5154,21 @@ describe('__typename handling', () => { id: ID! x: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - let operation = operationFromDocument(api, gql` - query { - t { - foo: __typename - x + let operation = operationFromDocument( + api, + gql` + query { + t { + foo: __typename + x + } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4879,15 +5184,18 @@ describe('__typename handling', () => { } `); - operation = operationFromDocument(api, gql` - query { - t { - foo: __typename - x - __typename + operation = operationFromDocument( + api, + gql` + query { + t { + foo: __typename + x + __typename + } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -4916,8 +5224,8 @@ describe('__typename handling', () => { type S @key(fields: "id") { id: ID } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -4930,8 +5238,8 @@ describe('__typename handling', () => { type T { x: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -4945,28 +5253,37 @@ describe('__typename handling', () => { id: ID! y: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); // This tests the patch from https://github.com/apollographql/federation/pull/2137. // Namely, the schema is such that `x` can only be fetched from one subgraph, but // technically __typename can be fetched from 2 subgraphs. However, the optimization // we test for is that we actually don't consider both choices for __typename and // instead only evaluate a single query plan (the assertion on `evaluatePlanCount`) - let operation = operationFromDocument(api, gql` - query { - s { - t { - __typename - x + let operation = operationFromDocument( + api, + gql` + query { + s { + t { + __typename + x + } } } - } - `); + `, + ); let plan = queryPlanner.buildQueryPlan(operation); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { @@ -5007,22 +5324,27 @@ describe('__typename handling', () => { // in the implementation made this example forgo the optimization of the // __typename within `t`. We make sure this is not case (that we still only // consider a single choice of plan). - operation = operationFromDocument(api, gql` - query { - s { - __typename - ... on S { - t { - __typename - x + operation = operationFromDocument( + api, + gql` + query { + s { + __typename + ... on S { + t { + __typename + x + } } } } - } - `); + `, + ); plan = queryPlanner.buildQueryPlan(operation); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { @@ -5071,8 +5393,8 @@ describe('mutations', () => { type Mutation { m1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5080,16 +5402,19 @@ describe('mutations', () => { type Mutation { m2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - mutation { - m2 - m1 - } - `); + const operation = operationFromDocument( + api, + gql` + mutation { + m2 + m1 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -5132,8 +5457,8 @@ describe('interface type-explosion', () => { type S @shareable { x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5147,19 +5472,22 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - s { - y + const operation = operationFromDocument( + api, + gql` + { + i { + s { + y + } } } - } - `); + `, + ); // The schema is constructed in such a way that we *need* to type-explode interface `I` // to be able to find field `y`. Make sure that happens. @@ -5221,8 +5549,8 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5236,19 +5564,22 @@ describe('interface type-explosion', () => { x: Int y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - s { - y + const operation = operationFromDocument( + api, + gql` + { + i { + s { + y + } } } - } - `); + `, + ); // This test is a small variation on the previous test ('handles non-matching ...'), we // we _can_ use the interface field directly and don't need to type-explode. So we @@ -5271,7 +5602,9 @@ describe('interface type-explosion', () => { }, } `); - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); }); @@ -5307,8 +5640,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5320,19 +5653,22 @@ describe('merged abstract types handling', () => { type A implements I { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on I { - v + const operation = operationFromDocument( + api, + gql` + { + u { + ... on I { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `u` and is a `I` *in the supergraph* but not in `Subgraph1`, so need to @@ -5384,8 +5720,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5395,19 +5731,22 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on I { - v + const operation = operationFromDocument( + api, + gql` + { + u { + ... on I { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While `A` is a `U` in the supergraph while not in `Subgraph1`, since the `u` @@ -5455,8 +5794,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5466,21 +5805,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on U { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + i { + ... on U { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `i` and is a `U` *in the supergraph* but not in `Subgraph1`, so need to @@ -5526,8 +5868,8 @@ describe('merged abstract types handling', () => { type C implements I { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5539,21 +5881,24 @@ describe('merged abstract types handling', () => { type A implements I { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i { - ... on U { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + i { + ... on U { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Here, `A` is a `I` in the supergraph while not in `Subgraph1`, and since the `i` operation is resolved by @@ -5598,8 +5943,8 @@ describe('merged abstract types handling', () => { type C implements I1 & I2 { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5611,19 +5956,22 @@ describe('merged abstract types handling', () => { type A implements I2 { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i1 { - ... on I2 { - v + const operation = operationFromDocument( + api, + gql` + { + i1 { + ... on I2 { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `i1` and is a `I2` *in the supergraph* but not in `Subgraph1`, so need to @@ -5677,8 +6025,8 @@ describe('merged abstract types handling', () => { type C implements I1 & I2 { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5690,19 +6038,22 @@ describe('merged abstract types handling', () => { type A implements I1 { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - i1 { - ... on I2 { - v + const operation = operationFromDocument( + api, + gql` + { + i1 { + ... on I2 { + v + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // While `A` is a `I1` in the supergraph while not in `Subgraph1`, since the `i1` @@ -5748,8 +6099,8 @@ describe('merged abstract types handling', () => { type C { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5759,21 +6110,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u1 { - ... on U2 { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + u1 { + ... on U2 { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Type `A` can be returned by `u1` and is a `U2` *in the supergraph* but not in `Subgraph1`, so need to @@ -5816,8 +6170,8 @@ describe('merged abstract types handling', () => { type C { v: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5827,21 +6181,24 @@ describe('merged abstract types handling', () => { type A { v: Int @shareable } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u1 { - ... on U2 { - ... on A { - v + const operation = operationFromDocument( + api, + gql` + { + u1 { + ... on U2 { + ... on A { + v + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Similar case than in the `interface/union` case: the whole `... on U2` sub-selection happens to be @@ -5880,12 +6237,12 @@ test('handles spread unions correctly', () => { b: Int } - type C @key(fields: "id") { + type C @key(fields: "id") { id: ID! c1: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -5905,19 +6262,22 @@ test('handles spread unions correctly', () => { id: ID! c2: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - u { - ... on C { - c1 + const operation = operationFromDocument( + api, + gql` + { + u { + ... on C { + c1 + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note: it's important that the query below DO NOT include the `... on C` part. Because in @@ -5934,7 +6294,7 @@ test('handles spread unions correctly', () => { }, } `); -}) +}); test('handles case of key chains in parallel requires', () => { const subgraph1 = { @@ -5954,18 +6314,18 @@ test('handles case of key chains in parallel requires', () => { id: ID! y: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', typeDefs: gql` - type T1 @key(fields: "id1") @key(fields: "id2") { + type T1 @key(fields: "id1") @key(fields: "id2") { id1: ID! id2: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -5980,22 +6340,29 @@ test('handles case of key chains in parallel requires', () => { y: Int @external z: Int @requires(fields: "y") } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - ... on T1 { - x - } - ... on T2 { - z + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + ... on T1 { + x + } + ... on T2 { + z + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6101,8 +6468,8 @@ test('handles types with no common supertype at the same "mergeAt"', () => { id: ID! x: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6116,26 +6483,29 @@ test('handles types with no common supertype at the same "mergeAt"', () => { id: ID! y: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t { - ... on T1 { - sub { - y + const operation = operationFromDocument( + api, + gql` + { + t { + ... on T1 { + sub { + y + } } - } - ... on T2 { - sub { - y + ... on T2 { + sub { + y + } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6213,37 +6583,40 @@ test('does not error out handling fragments when interface subtyping is involved v1: Int! v2: Int! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - a { - ...F1 - ...F2 - ...F3 + const operation = operationFromDocument( + api, + gql` + { + a { + ...F1 + ...F2 + ...F3 + } } - } - fragment F1 on A { - b { - v2 + fragment F1 on A { + b { + v2 + } } - } - fragment F2 on IA { - b { - v1 + fragment F2 on IA { + b { + v1 + } } - } - fragment F3 on IA { - b { - __typename + fragment F3 on IA { + b { + __typename + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6263,7 +6636,7 @@ test('does not error out handling fragments when interface subtyping is involved `); }); -describe("named fragments", () => { +describe('named fragments', () => { test('handles mix of fragments indirection and unions', () => { const subgraph1 = { name: 'Subgraph1', @@ -6285,38 +6658,41 @@ describe("named fragments", () => { type Cat { name: String } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - query { - parent { - ...F_indirection1_parent + const operation = operationFromDocument( + api, + gql` + query { + parent { + ...F_indirection1_parent + } } - } - - fragment F_indirection1_parent on Parent { - ...F_indirection2_catOrPerson - } - fragment F_indirection2_catOrPerson on CatOrPerson { - ...F_catOrPerson - } + fragment F_indirection1_parent on Parent { + ...F_indirection2_catOrPerson + } - fragment F_catOrPerson on CatOrPerson { - __typename - ... on Cat { - name + fragment F_indirection2_catOrPerson on CatOrPerson { + ...F_catOrPerson } - ... on Parent { - childs { - __typename - id + + fragment F_catOrPerson on CatOrPerson { + __typename + ... on Cat { + name + } + ... on Parent { + childs { + __typename + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6374,47 +6750,50 @@ describe("named fragments", () => { id1: ID! id2: ID! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - let operation = operationFromDocument(api, gql` - { - owner { - u { - ... on I { - id1 - id2 + let operation = operationFromDocument( + api, + gql` + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 } - ...Fragment1 - ...Fragment2 } } - } - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } } } - } - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } - fragment Fragment3 on OItf { - v0 - } + fragment Fragment3 on OItf { + v0 + } - fragment Fragment4 on I { - id1 - id2 - __typename - } - `); + fragment Fragment4 on I { + id1 + id2 + __typename + } + `, + ); let plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6446,42 +6825,45 @@ describe("named fragments", () => { } `); - operation = operationFromDocument(api, gql` - { - owner { - u { - ... on I { - id1 - id2 + operation = operationFromDocument( + api, + gql` + { + owner { + u { + ... on I { + id1 + id2 + } + ...Fragment1 + ...Fragment2 } - ...Fragment1 - ...Fragment2 } } - } - fragment Fragment1 on T1 { - owner { - ... on Owner { - ...Fragment3 + fragment Fragment1 on T1 { + owner { + ... on Owner { + ...Fragment3 + } } } - } - fragment Fragment2 on T2 { - ...Fragment4 - id1 - } + fragment Fragment2 on T2 { + ...Fragment4 + id1 + } - fragment Fragment3 on OItf { - v0 - } + fragment Fragment3 on OItf { + v0 + } - fragment Fragment4 on I { - id1 - id2 - } - `); + fragment Fragment4 on I { + id1 + id2 + } + `, + ); plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6534,34 +6916,35 @@ describe("named fragments", () => { other: T1! } - - type T2 implements I - { + type T2 implements I { id: ID! other: T2! } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); - const operation = operationFromDocument(api, gql` - { - t1 { - ...Fragment1 + const operation = operationFromDocument( + api, + gql` + { + t1 { + ...Fragment1 + } } - } - fragment Fragment1 on I { - other { - ... on T1 { - id - } - ... on T2 { - id + fragment Fragment1 on I { + other { + ... on T1 { + id + } + ... on T2 { + id + } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6595,8 +6978,8 @@ describe("named fragments", () => { v1: Int v2: Int } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6605,27 +6988,30 @@ describe("named fragments", () => { id: ID! v3: Int } - ` - } + `, + }; const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); - const operation = operationFromDocument(api, gql` - { - t1 { - ...allTFields - } - t2 { - ...allTFields + const operation = operationFromDocument( + api, + gql` + { + t1 { + ...allTFields + } + t2 { + ...allTFields + } } - } - fragment allTFields on T { - v0 - v1 - v2 - v3 - } - `); + fragment allTFields on T { + v0 + v1 + v2 + v3 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6699,8 +7085,8 @@ describe("named fragments", () => { type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -6709,7 +7095,6 @@ describe("named fragments", () => { id: ID! u1: U u2: U - } type U @key(fields: "id") { @@ -6717,8 +7102,8 @@ describe("named fragments", () => { v0: Int v1: Int } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -6728,29 +7113,36 @@ describe("named fragments", () => { v2: Int v3: Int } - ` - } + `, + }; - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - u1 { - ...allUFields - } - u2 { - ...allUFields + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + u1 { + ...allUFields + } + u2 { + ...allUFields + } } } - } - fragment allUFields on U { - v0 - v1 - v2 - v3 - } - `); + fragment allUFields on U { + v0 + v1 + v2 + v3 + } + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6831,7 +7223,7 @@ describe("named fragments", () => { } `); }); -}) +}); describe('`debug.maxEvaluatedPlans` configuration', () => { // Simple schema, created to force the query planner to have multiple choice. We'll build @@ -6859,10 +7251,12 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { const subgraphs = [ { name: 'Subgraph1', - typeDefs - }, { + typeDefs, + }, + { name: 'Subgraph2', - typeDefs } + typeDefs, + }, ]; test('works when unset', () => { @@ -6873,18 +7267,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { // plans are considered) and we'll have to adapt the example (find a better way to force // choices). - const config = { debug : { maxEvaluatedPlans : undefined } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: undefined } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6907,18 +7307,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('allows setting down to 1', () => { - const config = { debug : { maxEvaluatedPlans : 1 } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: 1 } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); // Note that in theory, the planner would be excused if it wasn't generated this @@ -6949,18 +7355,24 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('can be set to an arbitrary number', () => { - const config = { debug : { maxEvaluatedPlans : 10 } }; - const [api, queryPlanner] = composeAndCreatePlannerWithOptions(subgraphs, config); - const operation = operationFromDocument(api, gql` - { - t { - v1 - v2 - v3 - v4 + const config = { debug: { maxEvaluatedPlans: 10 } }; + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + subgraphs, + config, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + v1 + v2 + v3 + v4 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -6986,14 +7398,14 @@ describe('`debug.maxEvaluatedPlans` configuration', () => { }); test('cannot be set to 0 or a negative number', () => { - let config = { debug : { maxEvaluatedPlans : 0 } }; + let config = { debug: { maxEvaluatedPlans: 0 } }; expect(() => composeAndCreatePlannerWithOptions(subgraphs, config)).toThrow( - 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got 0' + 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got 0', ); - config = { debug : { maxEvaluatedPlans : -1 } }; + config = { debug: { maxEvaluatedPlans: -1 } }; expect(() => composeAndCreatePlannerWithOptions(subgraphs, config)).toThrow( - 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got -1' + 'Invalid value for query planning configuration "debug.maxEvaluatedPlans"; expected a number >= 1 but got -1', ); }); }); @@ -7023,8 +7435,8 @@ test('correctly generate plan built from some non-individually optimal branch op type T { x: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7036,8 +7448,8 @@ test('correctly generate plan built from some non-individually optimal branch op type T @key(fields: "id") { id: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7047,18 +7459,25 @@ test('correctly generate plan built from some non-individually optimal branch op x: Int @shareable y: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - x - y + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + x + y + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7107,8 +7526,8 @@ test('does not error on some complex fetch group dependencies', () => { type User { id: ID! @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7125,8 +7544,8 @@ test('does not error on some complex fetch group dependencies', () => { type Props { id: ID! @shareable } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7160,27 +7579,34 @@ test('does not error on some complex fetch group dependencies', () => { type V { x: Int } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - me { - p { - v0 - t { - v1 { - x - } - v2 { - x + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + me { + p { + v0 + t { + v1 { + x + } + v2 { + x + } } } } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7235,8 +7661,8 @@ test('does not evaluate plans relying on a key field to fetch that same field', type T @key(fields: "otherId") { otherId: ID! } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7245,8 +7671,8 @@ test('does not evaluate plans relying on a key field to fetch that same field', id: ID! otherId: ID! } - ` - } + `, + }; const subgraph3 = { name: 'Subgraph3', @@ -7254,17 +7680,24 @@ test('does not evaluate plans relying on a key field to fetch that same field', type T @key(fields: "id") { id: ID! } - ` - } - - const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2, subgraph3); - const operation = operationFromDocument(api, gql` - { - t { - id + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner( + subgraph1, + subgraph2, + subgraph3, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + id + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7307,7 +7740,9 @@ test('does not evaluate plans relying on a key field to fetch that same field', // this test ensure this is not considered anymore (considering that later plan // was not incorrect, but it was adding to the options to evaluate which in some // cases could impact query planning performance quite a bit). - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(1); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 1, + ); }); test('avoid considering indirect paths from the root when a more direct one exists', () => { @@ -7322,8 +7757,8 @@ test('avoid considering indirect paths from the root when a more direct one exis id: ID! v0: Int @shareable } - ` - } + `, + }; const subgraph2 = { name: 'Subgraph2', @@ -7337,24 +7772,30 @@ test('avoid considering indirect paths from the root when a more direct one exis v0: Int @shareable v1: Int } - ` - } + `, + }; // Each of id/v0 can have 2 options each, so that's 4 combinations. If we were to consider 2 options for each // v1 value however, that would multiple it by 2 each times, so it would 32 possibilities. We limit the number of // evaluated plans just above our expected number of 4 so that if we exceed it, the generated plan will be sub-optimal. - const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph1, subgraph2], { debug: { maxEvaluatedPlans: 6 } }); - const operation = operationFromDocument(api, gql` - { - t { - id - v0 - a0: v1 - a1: v1 - a2: v1 + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [subgraph1, subgraph2], + { debug: { maxEvaluatedPlans: 6 } }, + ); + const operation = operationFromDocument( + api, + gql` + { + t { + id + v0 + a0: v1 + a1: v1 + a2: v1 + } } - } - `); + `, + ); const plan = queryPlanner.buildQueryPlan(operation); expect(plan).toMatchInlineSnapshot(` @@ -7376,5 +7817,7 @@ test('avoid considering indirect paths from the root when a more direct one exis // As said above, we legit have 2 options for `id` and `v0`, and we cannot know which are best before we evaluate the // plans completely. But for the multiple `v1`, we should recognize that going through the 1st subgraph (and taking a // key) is never exactly a good idea. - expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe(4); + expect(queryPlanner.lastGeneratedPlanStatistics()?.evaluatedPlanCount).toBe( + 4, + ); }); diff --git a/query-planner-js/src/__tests__/generateAllPlans.test.ts b/query-planner-js/src/__tests__/generateAllPlans.test.ts index b95f50d34..5215b2f9a 100644 --- a/query-planner-js/src/__tests__/generateAllPlans.test.ts +++ b/query-planner-js/src/__tests__/generateAllPlans.test.ts @@ -1,7 +1,10 @@ import { assert } from '@apollo/federation-internals'; import { generateAllPlansAndFindBest } from '../generateAllPlans'; -function generateTestPlans(initial: string[], choices: string[][]): { best: string[], generated: string[][], evaluated: string[][] } { +function generateTestPlans( + initial: string[], + choices: string[][], +): { best: string[]; generated: string[][]; evaluated: string[][] } { const generated: string[][] = []; const evaluated: string[][] = []; const { best } = generateAllPlansAndFindBest({ @@ -28,38 +31,47 @@ function expectSamePlans(expected: string[][], actual: string[][]) { const expectedSet = new Set(expected.map((e) => normalize(e))); for (const value of actual) { const normalized = normalize(value); - assert(expectedSet.has(normalized), `Unexpected plan [${value.join(', ')}] is not in [\n${expected.map((e) => `[ ${e.join(', ')} ]`).join('\n')}\n]`); + assert( + expectedSet.has(normalized), + `Unexpected plan [${value.join(', ')}] is not in [\n${expected + .map((e) => `[ ${e.join(', ')} ]`) + .join('\n')}\n]`, + ); } const actualSet = new Set(actual.map((e) => normalize(e))); for (const value of expected) { const normalized = normalize(value); - assert(actualSet.has(normalized), `Expected plan [${value.join(', ')}] not found in [\n${actual.map((e) => `[ ${e.join(', ')} ]`).join('\n')}\n]`); + assert( + actualSet.has(normalized), + `Expected plan [${value.join(', ')}] not found in [\n${actual + .map((e) => `[ ${e.join(', ')} ]`) + .join('\n')}\n]`, + ); } } - test('Pick elements at same index first', () => { const { best, generated } = generateTestPlans( - ['I'], + ['I'], [ - [ 'A1', 'B1'], - [ 'A2', 'B2'], - [ 'A3', 'B3'], + ['A1', 'B1'], + ['A2', 'B2'], + ['A3', 'B3'], ], ); expect(best).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[0]).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[1]).toEqual(['I', 'B1', 'B2', 'B3']); -}) +}); test('Bail early for more costly elements', () => { const { best, generated } = generateTestPlans( - ['I'], + ['I'], [ - [ 'A1', 'B1VeryCostly'], - [ 'A2', 'B2Co'], - [ 'A3', 'B3'], + ['A1', 'B1VeryCostly'], + ['A2', 'B2Co'], + ['A3', 'B3'], ], ); @@ -68,29 +80,27 @@ test('Bail early for more costly elements', () => { expect(generated).toHaveLength(2); expect(generated[0]).toEqual(['I', 'A1', 'A2', 'A3']); expect(generated[1]).toEqual(['I', 'A1', 'A2', 'B3']); -}) +}); test('Handles branches of various sizes', () => { const { best, generated } = generateTestPlans( - ['I'], - [ - [ 'A1x', 'B1'], - [ 'A2', 'B2Costly', 'C2'], - [ 'A3'], - [ 'A4', 'B4' ], - ], + ['I'], + [['A1x', 'B1'], ['A2', 'B2Costly', 'C2'], ['A3'], ['A4', 'B4']], ); expect(best).toEqual(['I', 'B1', 'A2', 'A3', 'A4']); // We should generate every option, except those including `B2Costly` - expectSamePlans([ - [ 'I', 'A1x', 'A2', 'A3', 'A4' ], - [ 'I', 'A1x', 'A2', 'A3', 'B4' ], - [ 'I', 'A1x', 'C2', 'A3', 'A4' ], - [ 'I', 'A1x', 'C2', 'A3', 'B4' ], - [ 'I', 'B1', 'A2', 'A3', 'A4' ], - [ 'I', 'B1', 'A2', 'A3', 'B4' ], - [ 'I', 'B1', 'C2', 'A3', 'A4' ], - [ 'I', 'B1', 'C2', 'A3', 'B4' ], - ], generated); -}) + expectSamePlans( + [ + ['I', 'A1x', 'A2', 'A3', 'A4'], + ['I', 'A1x', 'A2', 'A3', 'B4'], + ['I', 'A1x', 'C2', 'A3', 'A4'], + ['I', 'A1x', 'C2', 'A3', 'B4'], + ['I', 'B1', 'A2', 'A3', 'A4'], + ['I', 'B1', 'A2', 'A3', 'B4'], + ['I', 'B1', 'C2', 'A3', 'A4'], + ['I', 'B1', 'C2', 'A3', 'B4'], + ], + generated, + ); +}); diff --git a/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts b/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts index 00b83a728..d9d48cf64 100644 --- a/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts +++ b/query-planner-js/src/__tests__/supergraphBackwardCompatibility.test.ts @@ -1,5 +1,12 @@ import { composeServices } from '@apollo/composition'; -import { asFed2SubgraphDocument, assert, isDefined, operationFromDocument, Schema, Supergraph } from '@apollo/federation-internals'; +import { + asFed2SubgraphDocument, + assert, + isDefined, + operationFromDocument, + Schema, + Supergraph, +} from '@apollo/federation-internals'; import fs from 'fs'; import gql from 'graphql-tag'; import path from 'path'; @@ -16,7 +23,6 @@ import { astSerializer, queryPlanSerializer, QueryPlanner } from '..'; * */ - const accounts = { name: 'accounts', typeDefs: gql` @@ -32,8 +38,8 @@ const accounts = { password: String nickname: String @override(from: "reviews") } - ` -} + `, +}; const products = { name: 'products', @@ -59,7 +65,7 @@ const products = { rating: Int @external } - type Movie implements Product @key(fields: "id") { + type Movie implements Product @key(fields: "id") { id: ID! price: Price title: String @@ -92,8 +98,8 @@ const products = { USD EUR } - ` -} + `, +}; const reviews = { name: 'reviews', @@ -130,8 +136,8 @@ const reviews = { id: ID! reviews: [Review] } - ` -} + `, +}; const testSupergraphDir = path.join(__dirname, 'testSupergraphs'); @@ -140,33 +146,47 @@ function testSupergraphPath(v: string): fs.PathLike { } export function generateTestSupergraph(v: string) { - const services = [ accounts, products, reviews ]; - const res = composeServices( - services.map((s) => ({ ...s, typeDefs: asFed2SubgraphDocument(s.typeDefs) })) + const services = [accounts, products, reviews]; + const res = composeServices( + services.map((s) => ({ + ...s, + typeDefs: asFed2SubgraphDocument(s.typeDefs), + })), + ); + assert( + !res.errors, + `Expected to compose but got errors:\n${res.errors?.join('\n\n')}`, ); - assert(!res.errors, `Expected to compose but got errors:\n${res.errors?.join('\n\n')}`); fs.writeFileSync(testSupergraphPath(v), res.supergraphSdl); } type TestSupergraph = { - version: string, - supergraph: Supergraph, - api: Schema, -} + version: string; + supergraph: Supergraph; + api: Schema; +}; function listTestSupergraphs(): TestSupergraph[] { - return fs.readdirSync(testSupergraphDir).map((file) => { - if (!file.startsWith('testSupergraph_')) { - return undefined; - } - const version = file.slice('testSupergraph_'.length, file.length - '.graphql'.length); - const supergraph = Supergraph.build(fs.readFileSync(path.join(testSupergraphDir, file), 'utf8')); - return { - version, - supergraph, - api: supergraph.apiSchema(), - }; - }).filter(isDefined); + return fs + .readdirSync(testSupergraphDir) + .map((file) => { + if (!file.startsWith('testSupergraph_')) { + return undefined; + } + const version = file.slice( + 'testSupergraph_'.length, + file.length - '.graphql'.length, + ); + const supergraph = Supergraph.build( + fs.readFileSync(path.join(testSupergraphDir, file), 'utf8'), + ); + return { + version, + supergraph, + api: supergraph.apiSchema(), + }; + }) + .filter(isDefined); } // This file is loaded by the `genTestSupergraph.ts` script to generate the test supergraph for the current version. @@ -180,27 +200,32 @@ if (typeof describe !== 'undefined') { const toTest = listTestSupergraphs(); - describe.each(toTest)(`handles supergraphs from $version`, ({supergraph, api}) => { - let qp: QueryPlanner; - - test('can extract subgraphs and build the query planner', () => { - qp = new QueryPlanner(supergraph); - }); - - test('can execute "me" query', () => { - const operation = operationFromDocument(api, gql` - { - me { - nickname - reviews { - rating + describe.each(toTest)( + `handles supergraphs from $version`, + ({ supergraph, api }) => { + let qp: QueryPlanner; + + test('can extract subgraphs and build the query planner', () => { + qp = new QueryPlanner(supergraph); + }); + + test('can execute "me" query', () => { + const operation = operationFromDocument( + api, + gql` + { + me { + nickname + reviews { + rating + } + } } - } - } - `); + `, + ); - const plan = qp.buildQueryPlan(operation); - expect(plan).toMatchInlineSnapshot(` + const plan = qp.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` QueryPlan { Parallel { Fetch(service: "accounts") { @@ -222,34 +247,37 @@ if (typeof describe !== 'undefined') { }, } `); - }); - - test('can execute "bestRatedProducts" query', () => { - const operation = operationFromDocument(api, gql` - { - bestRatedProducts(limit: 10) { - reviews { - author { - nickname + }); + + test('can execute "bestRatedProducts" query', () => { + const operation = operationFromDocument( + api, + gql` + { + bestRatedProducts(limit: 10) { + reviews { + author { + nickname + } + rating + } + price { + value + currency + } + ... on Movie { + length_minutes + } + ... on Book { + avg_rating + } } - rating - } - price { - value - currency - } - ... on Movie { - length_minutes } - ... on Book { - avg_rating - } - } - } - `); + `, + ); - const plan = qp.buildQueryPlan(operation); - expect(plan).toMatchInlineSnapshot(` + const plan = qp.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` QueryPlan { Sequence { Fetch(service: "reviews") { @@ -330,7 +358,8 @@ if (typeof describe !== 'undefined') { }, } `); - }); - }); + }); + }, + ); }); } diff --git a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts index be3e7b732..81ee9cf3c 100644 --- a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts +++ b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts @@ -862,7 +862,8 @@ describe('buildSubgraphSchema', () => { directiveDefinitions: string, typeDefinitions: string, ) => { - const schema = buildSubgraphSchema(gql`${header} + const schema = buildSubgraphSchema(gql` + ${header} type User @key(fields: "email") @tag(name: "tagOnType") { email: String @tag(name: "tagOnField") } @@ -881,8 +882,8 @@ describe('buildSubgraphSchema', () => { ? '' : ` ${header.trim()} - `) - + ` + `) + + ` ${directiveDefinitions.trim()} type User @@ -903,13 +904,15 @@ describe('buildSubgraphSchema', () => { = User ${typeDefinitions.trim()} - `); + `, + ); }; - it.each([{ - name: 'fed1', - header: '', - directiveDefinitions: ` + it.each([ + { + name: 'fed1', + header: '', + directiveDefinitions: ` directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE directive @requires(fields: _FieldSet!) on FIELD_DEFINITION @@ -922,7 +925,7 @@ describe('buildSubgraphSchema', () => { directive @extends on OBJECT | INTERFACE `, - typesDefinitions: ` + typesDefinitions: ` scalar _FieldSet scalar _Any @@ -938,14 +941,15 @@ describe('buildSubgraphSchema', () => { _service: _Service! } `, - }, { - name: 'fed2', - header: ` + }, + { + name: 'fed2', + header: ` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@tag"]) `, - directiveDefinitions: ` + directiveDefinitions: ` directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE @@ -966,7 +970,7 @@ describe('buildSubgraphSchema', () => { directive @federation__override(from: String!) on FIELD_DEFINITION `, - typesDefinitions: ` + typesDefinitions: ` enum link__Purpose { """ \`SECURITY\` features provide metadata necessary to securely resolve fields. @@ -996,9 +1000,13 @@ describe('buildSubgraphSchema', () => { _service: _Service! } `, - }])('adds it for $name schema', async ({header, directiveDefinitions, typesDefinitions}) => { - await validateTag(header, directiveDefinitions, typesDefinitions); - }); + }, + ])( + 'adds it for $name schema', + async ({ header, directiveDefinitions, typesDefinitions }) => { + await validateTag(header, directiveDefinitions, typesDefinitions); + }, + ); }); it(`fails on bad linking`, () => { @@ -1006,8 +1014,10 @@ describe('buildSubgraphSchema', () => { buildSubgraphSchema(gql` extend schema @link(url: "https://specs.apollo.dev/link/v1.0") - @link(url: "https://specs.apollo.dev/federation/v2.0", - import: [ { name: "@key", as: "@primaryKey" } ]) + @link( + url: "https://specs.apollo.dev/federation/v2.0" + import: [{ name: "@key", as: "@primaryKey" }] + ) type Query { t: T @@ -1016,10 +1026,10 @@ describe('buildSubgraphSchema', () => { type T @key(fields: "id") { id: ID! } - `); + `); } catch (e) { expect(errorCauses(e)?.map((e) => e.message)).toStrictEqual([ - 'Unknown directive "@key". If you meant the "@key" federation directive, you should use "@primaryKey" as it is imported under that name in the @link to the federation specification of this schema.' + 'Unknown directive "@key". If you meant the "@key" federation directive, you should use "@primaryKey" as it is imported under that name in the @link to the federation specification of this schema.', ]); } }); @@ -1047,9 +1057,12 @@ describe('buildSubgraphSchema', () => { { kind: Kind.OPERATION_TYPE_DEFINITION, operation: OperationTypeNode.QUERY, - type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: 'Query' } } + type: { + kind: Kind.NAMED_TYPE, + name: { kind: Kind.NAME, value: 'Query' }, + }, }, - ] + ], }, { kind: Kind.OBJECT_TYPE_DEFINITION, @@ -1060,20 +1073,19 @@ describe('buildSubgraphSchema', () => { name: { kind: Kind.NAME, value: 'test' }, type: { kind: Kind.NAMED_TYPE, - name: { kind: Kind.NAME, value: 'String' } + name: { kind: Kind.NAME, value: 'String' }, }, }, - ] + ], }, ], }; - expect(() => buildSubgraphSchema(doc)).not.toThrow(); }); it('correctly attaches the provided subscribe function to the schema object', () => { - async function* subscribeFn () { + async function* subscribeFn() { for await (const word of ['Hello', 'Bonjour', 'Ciao']) { yield word; } @@ -1123,18 +1135,20 @@ describe('buildSubgraphSchema', () => { type Query { x: Int } - `) + `); const { data, errors } = await graphql({ schema, source: query }); expect(errors).toBeUndefined(); expect((data?._service as any).sdl).toMatchString(expectedOutput); - } + }; it('expands federation 2.0 correctly', async () => { // For 2.0, we expect in particular that: // - the @composeDirective directive is *not* present // - the @shareable directive is *not* repeatable - await testVersion('2.0', ` + await testVersion( + '2.0', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1190,14 +1204,17 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.1 correctly', async () => { // For 2.1, we expect in particular that: // - the @composeDirective directive to exists // - the @shareable directive is *not* repeatable - await testVersion('2.1', ` + await testVersion( + '2.1', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1255,13 +1272,16 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.2 correctly', async () => { // For 2.2, we expect everything from 2.1 plus: // - the @shareable directive to be repeatable - await testVersion('2.2', ` + await testVersion( + '2.2', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1319,14 +1339,17 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); it('expands federation 2.3 correctly', async () => { // For 2.3, we expect in everything from 2.2 plus: // - the @interfaceObject directive // - the @tag directive to additionally have the SCHEMA location - await testVersion('2.3', ` + await testVersion( + '2.3', + ` schema @link(url: \"https://specs.apollo.dev/link/v1.0\") { @@ -1386,7 +1409,8 @@ describe('buildSubgraphSchema', () => { type _Service { sdl: String } - `); + `, + ); }); }); });