diff --git a/.changeset/witty-toes-grin.md b/.changeset/witty-toes-grin.md
new file mode 100644
index 00000000000..b2361fdc400
--- /dev/null
+++ b/.changeset/witty-toes-grin.md
@@ -0,0 +1,5 @@
+---
+'@apollo/server': patch
+---
+
+Add generics for response data and variables to server.executeOperation; allow inference from TypedQueryDocumentNode.
diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx
index 183c95676a5..5e6d606c06e 100644
--- a/docs/source/api/apollo-server.mdx
+++ b/docs/source/api/apollo-server.mdx
@@ -631,7 +631,7 @@ Below are the available fields for the first argument of `executeOperation`:
-**Required**. The GraphQL operation to run. Note that you must use the `query` field even if the operation is a mutation.
+**Required**. The GraphQL operation to run. Note that you must use the `query` field even if the operation is a mutation. This may also be a [`TypedQueryDocumentNode`](https://github.com/graphql/graphql-js/blob/main/src/utilities/typedQueryDocumentNode.ts), in which case TypeScript types for the `variables` option and `response.body.singleResult.data` will be automatically inferred.
|
diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts
index 9ba05ff80eb..6fc4330f0b2 100644
--- a/packages/server/src/ApolloServer.ts
+++ b/packages/server/src/ApolloServer.ts
@@ -11,6 +11,7 @@ import {
GraphQLSchema,
ParseOptions,
print,
+ TypedQueryDocumentNode,
ValidationContext,
ValidationRule,
} from 'graphql';
@@ -64,7 +65,10 @@ import type { ApolloServerOptionsWithStaticSchema } from './externalTypes/constr
import type { GatewayExecutor } from '@apollo/server-gateway-interface';
import type { GraphQLExperimentalIncrementalExecutionResults } from './incrementalDeliveryPolyfill.js';
import { HeaderMap } from './utils/HeaderMap.js';
-import type { ExecuteOperationOptions } from './externalTypes/graphql.js';
+import type {
+ ExecuteOperationOptions,
+ VariableValues,
+} from './externalTypes/graphql.js';
const NoIntrospection: ValidationRule = (context: ValidationContext) => ({
Field(node) {
@@ -1080,26 +1084,44 @@ export class ApolloServer {
*
* The second object is an optional options object which includes the optional
* `contextValue` object available in resolvers.
+ *
+ * You may specify the TData and TVariables generic types when calling this
+ * method; Apollo Server does not validate that the returned data actually
+ * matches the structure of TData. (Typically these types are created by a
+ * code generation tool.) Note that this does not enforce that `variables` is
+ * provided at all, just that it has the right type if provided.
*/
- public async executeOperation(
+ public async executeOperation<
+ TData = Record,
+ TVariables extends VariableValues = VariableValues,
+ >(
this: ApolloServer,
- request: Omit & {
- query?: string | DocumentNode;
+ request: Omit, 'query'> & {
+ query?: string | DocumentNode | TypedQueryDocumentNode;
},
- ): Promise;
- public async executeOperation(
- request: Omit & {
- query?: string | DocumentNode;
+ ): Promise>;
+ public async executeOperation<
+ TData = Record,
+ TVariables extends VariableValues = VariableValues,
+ >(
+ request: Omit, 'query'> & {
+ query?: string | DocumentNode | TypedQueryDocumentNode;
},
options?: ExecuteOperationOptions,
- ): Promise;
-
- async executeOperation(
- request: Omit & {
- query?: string | DocumentNode;
+ ): Promise>;
+
+ async executeOperation<
+ TData = Record,
+ TVariables extends VariableValues = VariableValues,
+ >(
+ request: Omit, 'query'> & {
+ // We should consider supporting TypedDocumentNode from
+ // `@graphql-typed-document-node/core` as well, as it is more popular than
+ // the newer built-in type.
+ query?: string | DocumentNode | TypedQueryDocumentNode;
},
options: ExecuteOperationOptions = {},
- ): Promise {
+ ): Promise> {
// Since this function is mostly for testing, you don't need to explicitly
// start your server before calling it. (That also means you can use it with
// `apollo-server` which doesn't support `start()`.)
@@ -1121,7 +1143,7 @@ export class ApolloServer {
: request.query,
};
- return await internalExecuteOperation(
+ const response: GraphQLResponse = await internalExecuteOperation(
{
server: this,
graphQLRequest,
@@ -1130,6 +1152,10 @@ export class ApolloServer {
},
options,
);
+
+ // It's your job to set an appropriate TData (perhaps using codegen); we
+ // don't validate it.
+ return response as GraphQLResponse;
}
}
diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts
index 4bdc1b30c58..dd113979e77 100644
--- a/packages/server/src/__tests__/ApolloServer.test.ts
+++ b/packages/server/src/__tests__/ApolloServer.test.ts
@@ -1,6 +1,12 @@
import { ApolloServer, HeaderMap } from '..';
import type { ApolloServerOptions } from '..';
-import { FormattedExecutionResult, GraphQLError, GraphQLSchema } from 'graphql';
+import {
+ FormattedExecutionResult,
+ GraphQLError,
+ GraphQLSchema,
+ parse,
+ TypedQueryDocumentNode,
+} from 'graphql';
import type { ApolloServerPlugin, BaseContext } from '../externalTypes';
import { ApolloServerPluginCacheControlDisabled } from '../plugin/disabled/index.js';
import { ApolloServerPluginUsageReporting } from '../plugin/usageReporting/index.js';
@@ -567,3 +573,51 @@ describe('ApolloServer addPlugin', () => {
await server.stop();
});
});
+
+it('TypedQueryDocumentNode', async () => {
+ const server = new ApolloServer({
+ typeDefs: `type Query {
+ foo(arg: Int!): String
+ }`,
+ resolvers: {
+ Query: {
+ foo(_, { arg }) {
+ return `yay ${arg}`;
+ },
+ },
+ },
+ });
+ await server.start();
+
+ const query = parse(
+ 'query Q($a: Int!) {foo(arg: $a)}',
+ ) as TypedQueryDocumentNode<{ foo: string | null }, { a: number }>;
+
+ const response = await server.executeOperation({
+ query,
+ variables: { a: 1 },
+ });
+ assert(response.body.kind === 'single');
+ expect(response.body.singleResult.data?.foo).toBe('yay 1');
+ // bar is not part of the data type.
+ // @ts-expect-error
+ expect(response.body.singleResult.data?.bar).toBe(undefined);
+
+ if (1 + 1 === 3) {
+ // We just want to type-check this part, not run it.
+ await server.executeOperation({
+ query,
+ // @ts-expect-error
+ variables: { a: 'asdf' },
+ });
+ await server.executeOperation({
+ query,
+ // @ts-expect-error
+ variables: {},
+ });
+
+ // Note that we don't detect providing extra variables or not providing
+ // variables at all when some are required, though it would be nice to fix
+ // that.
+ }
+});
diff --git a/packages/server/src/externalTypes/graphql.ts b/packages/server/src/externalTypes/graphql.ts
index d7408da3555..534aa470887 100644
--- a/packages/server/src/externalTypes/graphql.ts
+++ b/packages/server/src/externalTypes/graphql.ts
@@ -17,10 +17,12 @@ import type {
GraphQLExperimentalFormattedSubsequentIncrementalExecutionResult,
} from './incrementalDeliveryPolyfill.js';
-export interface GraphQLRequest {
+export interface GraphQLRequest<
+ TVariables extends VariableValues = VariableValues,
+> {
query?: string;
operationName?: string;
- variables?: VariableValues;
+ variables?: TVariables;
extensions?: Record;
http?: HTTPGraphQLRequest;
}
@@ -32,10 +34,10 @@ export type VariableValues = { [name: string]: any };
// GraphQL operation uses incremental delivery directives such as `@defer` or
// `@stream`. Note that incremental delivery currently requires using a
// pre-release of graphql-js v17.
-export type GraphQLResponseBody =
+export type GraphQLResponseBody> =
| {
kind: 'single';
- singleResult: FormattedExecutionResult;
+ singleResult: FormattedExecutionResult;
}
| {
kind: 'incremental';
@@ -43,12 +45,15 @@ export type GraphQLResponseBody =
subsequentResults: AsyncIterable;
};
-export type GraphQLInProgressResponse = {
+export type GraphQLInProgressResponse> = {
http: HTTPGraphQLHead;
- body?: GraphQLResponseBody;
+ body?: GraphQLResponseBody;
};
-export type GraphQLResponse = WithRequired;
+export type GraphQLResponse> = WithRequired<
+ GraphQLInProgressResponse,
+ 'body'
+>;
export interface GraphQLRequestMetrics {
// It would be more accurate to call this fieldLevelInstrumentation (it is