Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix handling of aliases and variables in introspection queries #2506

Merged
merged 1 commit into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/itchy-ants-lay.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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 @@ -68,13 +71,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 @@ -87,14 +110,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 @@ -181,11 +211,17 @@ export async function executeQueryPlan(
let data;
try {
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.expandAllFragments().toSelectionNode()),
introspectionHandling: (f) => executeIntrospection(
operationContext.schema,
f.expandAllFragments().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
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.