Skip to content

Commit

Permalink
Fix handling of aliases and variables in introspection queries (#2506)
Browse files Browse the repository at this point in the history
Fixes #2500, #2503
  • Loading branch information
Sylvain Lebresne authored and Sylvain Lebresne committed May 16, 2023
1 parent 2894a1e commit ef5c817
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changeset/itchy-ants-lay.md
@@ -0,0 +1,6 @@
---
"@apollo/gateway": patch
---

Fix handling of aliases and variables in introspection queries.

140 changes: 140 additions & 0 deletions gateway-js/src/__tests__/executeQueryPlan.introspection.test.ts
@@ -0,0 +1,140 @@
import gql from 'graphql-tag';
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 { buildOperationContext } from '../operationContext';
import { executeQueryPlan } from '../executeQueryPlan';

function buildRequestContext(variables: Record<string, any>): GatewayGraphQLRequestContext {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return {
cache: undefined as any,
context: {},
request: {
variables,
},
metrics: {},
};
}

async function executePlan(
queryPlan: QueryPlan,
operation: Operation,
schema: Schema,
serviceMap: { [serviceName: string]: LocalGraphQLDataSource },
variables: Record<string, any> = {},
): Promise<GatewayExecutionResult> {
const apiSchema = schema.toAPISchema();
const operationContext = buildOperationContext({
schema: apiSchema.toGraphQLJSSchema(),
operationDocument: gql`${operation.toString()}`,
});
return executeQueryPlan(
queryPlan,
serviceMap,
buildRequestContext(variables),
operationContext,
schema.toGraphQLJSSchema(),
apiSchema,
);
}

describe('handling of introspection queries', () => {
const typeDefs: ServiceDefinitionModule[] = [
{
name: 'S1',
typeDefs: gql`
type Query {
t: [T]
}
interface T {
id: ID!
}
type T1 implements T @key(fields: "id") {
id: ID!
a1: Int
}
type T2 implements T @key(fields: "id") {
id: ID!
a2: Int
}
`,
},
];
const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema(typeDefs);

it('it handles aliases on introspection fields', async () => {
const operation = parseOperation(schema, `
{
myAlias: __type(name: "T1") {
kind
name
}
}
`);

const queryPlan = queryPlanner.buildQueryPlan(operation);
const response = await executePlan(queryPlan, operation, schema, serviceMap);
expect(response.errors).toBeUndefined();
expect(response.data).toMatchInlineSnapshot(`
Object {
"myAlias": Object {
"kind": "OBJECT",
"name": "T1",
},
}
`);
});

it('it handles aliases inside introspection fields', async () => {
const operation = parseOperation(schema, `
{
__type(name: "T1") {
myKind: kind
name
}
}
`);

const queryPlan = queryPlanner.buildQueryPlan(operation);
const response = await executePlan(queryPlan, operation, schema, serviceMap);
expect(response.errors).toBeUndefined();
expect(response.data).toMatchInlineSnapshot(`
Object {
"__type": Object {
"myKind": "OBJECT",
"name": "T1",
},
}
`);
});

it('it handles variables passed to introspection fields', async () => {
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" });
expect(response.errors).toBeUndefined();
expect(response.data).toMatchInlineSnapshot(`
Object {
"__type": Object {
"kind": "OBJECT",
"name": "T1",
},
}
`);
});
});
48 changes: 42 additions & 6 deletions gateway-js/src/executeQueryPlan.ts
Expand Up @@ -11,6 +11,9 @@ import {
executeSync,
OperationTypeNode,
FieldNode,
visit,
ASTNode,
VariableDefinitionNode,
} from 'graphql';
import { Trace, google } from '@apollo/usage-reporting-protobuf';
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
Expand Down Expand Up @@ -66,13 +69,33 @@ interface ExecutionContext {
errors: GraphQLError[];
}

function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): DocumentNode {
function collectUsedVariables(node: ASTNode): Set<string> {
const usedVariables = new Set<string>();
visit(node, {
Variable: ({ name }) => {
usedVariables.add(name.value);
}
});
return usedVariables;
}

function makeIntrospectionQueryDocument(
introspectionSelection: FieldNode,
variableDefinitions?: readonly VariableDefinitionNode[],
): DocumentNode {
const usedVariables = collectUsedVariables(introspectionSelection);
const usedVariableDefinitions = variableDefinitions?.filter((def) => usedVariables.has(def.variable.name.value));
assert(
usedVariables.size === (usedVariableDefinitions?.length ?? 0),
() => `Should have found all used variables ${[...usedVariables]} in definitions ${JSON.stringify(variableDefinitions)}`,
);
return {
kind: Kind.DOCUMENT,
definitions: [
{
kind: Kind.OPERATION_DEFINITION,
operation: OperationTypeNode.QUERY,
variableDefinitions: usedVariableDefinitions,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [ introspectionSelection ],
Expand All @@ -85,14 +108,21 @@ function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): Docu
function executeIntrospection(
schema: GraphQLSchema,
introspectionSelection: FieldNode,
variableDefinitions: ReadonlyArray<VariableDefinitionNode> | undefined,
variableValues: Record<string, any> | undefined,
): any {
const { data } = executeSync({
const { data, errors } = executeSync({
schema,
document: makeIntrospectionQueryDocument(introspectionSelection),
document: makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions),
rootValue: {},
variableValues,
});
assert(
!errors || errors.length === 0,
() => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed but got ${JSON.stringify(errors)}`
);
assert(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`);
return data[introspectionSelection.name.value];
return data[introspectionSelection.alias?.value ?? introspectionSelection.name.value];
}

export async function executeQueryPlan(
Expand Down Expand Up @@ -159,11 +189,17 @@ export async function executeQueryPlan(
);

let postProcessingErrors: GraphQLError[];
const variables = requestContext.request.variables;
({ data, errors: postProcessingErrors } = computeResponse({
operation,
variables: requestContext.request.variables,
variables,
input: unfilteredData,
introspectionHandling: (f) => executeIntrospection(operationContext.schema, f.expandFragments().toSelectionNode()),
introspectionHandling: (f) => executeIntrospection(
operationContext.schema,
f.expandFragments().toSelectionNode(),
operationContext.operation.variableDefinitions,
variables,
),
}));

// If we have errors during the post-processing, we ignore them if any other errors have been thrown during
Expand Down

0 comments on commit ef5c817

Please sign in to comment.