From 4175f1b9cd4e0dea85d6404f9ab7c24889f7e264 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Thu, 20 Sep 2018 14:47:40 -0400 Subject: [PATCH] Allow an optional function to resolve the rootValue (#1555) * Allow an optional function to resolve the rootValue Passes the parsed DocumentNode AST to determine the root value, useful when providing a different rootValue for query vs mutation * Add API docs for rootValue --- CHANGELOG.md | 1 + docs/source/api/apollo-server.md | 15 ++++++++ .../src/__tests__/runQuery.test.ts | 17 +++++++++ .../apollo-server-core/src/graphqlOptions.ts | 8 +++-- packages/apollo-server-core/src/runQuery.ts | 7 ++-- .../src/index.ts | 36 +++++++++++++++++++ 6 files changed, 79 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0450d225737..62a586a1d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All of the packages in the `apollo-server` repo are released with the same versi ### vNEXT +- Allow an optional function to resolve the `rootValue`, passing the `DocumentNode` AST to determine the value. [PR #1555](https://github.com/apollographql/apollo-server/pull/1555) - Follow-up on the work in [PR #1516](https://github.com/apollographql/apollo-server/pull/1516) to also fix missing insertion cursor/caret when a custom GraphQL configuration is specified which doesn't specify its own `cursorShape` property. [PR #1607](https://github.com/apollographql/apollo-server/pull/1607) ### v2.1.0 diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index b0db2db34aa..3b39fc456f6 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -51,6 +51,21 @@ new ApolloServer({ authScope: getScope(req.headers.authorization) }), }); +``` + + * `rootValue`: <`Any`> | <`Function`> + + A value or function called with the parsed `Document`, creating the root value passed to the graphql executor. The function is useful if you wish to provide a different root value based on the query operation type. + +```js +new ApolloServer({ + typeDefs, + resolvers, + rootValue: (documentAST) => ({ + const op = getOperationAST(documentNode) + return op === 'mutation' ? mutationRoot : queryRoot; + }) +}); ``` * `mocks`: <`Object`> | <`Boolean`> diff --git a/packages/apollo-server-core/src/__tests__/runQuery.test.ts b/packages/apollo-server-core/src/__tests__/runQuery.test.ts index f2b6729eda4..5052984d386 100644 --- a/packages/apollo-server-core/src/__tests__/runQuery.test.ts +++ b/packages/apollo-server-core/src/__tests__/runQuery.test.ts @@ -8,6 +8,7 @@ import { GraphQLInt, GraphQLNonNull, parse, + DocumentNode, } from 'graphql'; import { runQuery } from '../runQuery'; @@ -175,6 +176,22 @@ describe('runQuery', () => { }); }); + it('correctly evaluates a rootValue function', () => { + const query = `{ testRootValue }`; + const expected = { testRootValue: 'it also works' }; + return runQuery({ + schema, + queryString: query, + rootValue: (doc: DocumentNode) => { + expect(doc.kind).toEqual('Document'); + return 'it also'; + }, + request: new MockReq(), + }).then(res => { + expect(res.data).toEqual(expected); + }); + }); + it('correctly passes in the context', () => { const query = `{ testContextValue }`; const expected = { testContextValue: 'it still works' }; diff --git a/packages/apollo-server-core/src/graphqlOptions.ts b/packages/apollo-server-core/src/graphqlOptions.ts index 4f45e08d57e..4c0673515bd 100644 --- a/packages/apollo-server-core/src/graphqlOptions.ts +++ b/packages/apollo-server-core/src/graphqlOptions.ts @@ -2,6 +2,7 @@ import { GraphQLSchema, ValidationContext, GraphQLFieldResolver, + DocumentNode, } from 'graphql'; import { GraphQLExtension } from 'graphql-extensions'; import { CacheControlExtensionOptions } from 'apollo-cache-control'; @@ -13,7 +14,7 @@ import { DataSource } from 'apollo-datasource'; * * - schema: an executable GraphQL schema used to fulfill requests. * - (optional) formatError: Formatting function applied to all errors before response is sent - * - (optional) rootValue: rootValue passed to GraphQL execution + * - (optional) rootValue: rootValue passed to GraphQL execution, or a function to resolving the rootValue from the DocumentNode * - (optional) context: the context passed to GraphQL execution * - (optional) validationRules: extra validation rules applied to requests * - (optional) formatResponse: a function applied to each graphQL execution result @@ -25,11 +26,12 @@ import { DataSource } from 'apollo-datasource'; export interface GraphQLServerOptions< TContext = | (() => Promise> | Record) - | Record + | Record, + TRootVal = ((parsedQuery: DocumentNode) => any) | any > { schema: GraphQLSchema; formatError?: Function; - rootValue?: any; + rootValue?: TRootVal; context?: TContext; validationRules?: Array<(context: ValidationContext) => any>; formatResponse?: Function; diff --git a/packages/apollo-server-core/src/runQuery.ts b/packages/apollo-server-core/src/runQuery.ts index 4eabf053a36..12920ab6d61 100644 --- a/packages/apollo-server-core/src/runQuery.ts +++ b/packages/apollo-server-core/src/runQuery.ts @@ -50,7 +50,7 @@ export interface QueryOptions { // a mutation), throw this error. nonQueryError?: Error; - rootValue?: any; + rootValue?: ((parsedQuery: DocumentNode) => any) | any; context?: any; variables?: { [key: string]: any }; operationName?: string; @@ -222,7 +222,10 @@ function doRunQuery(options: QueryOptions): Promise { const executionArgs: ExecutionArgs = { schema: options.schema, document: documentAST, - rootValue: options.rootValue, + rootValue: + typeof options.rootValue === 'function' + ? options.rootValue(documentAST) + : options.rootValue, contextValue: context, variableValues: options.variables, operationName: options.operationName, diff --git a/packages/apollo-server-integration-testsuite/src/index.ts b/packages/apollo-server-integration-testsuite/src/index.ts index 36d7325780b..e73e77a9493 100644 --- a/packages/apollo-server-integration-testsuite/src/index.ts +++ b/packages/apollo-server-integration-testsuite/src/index.ts @@ -12,6 +12,8 @@ import { GraphQLScalarType, introspectionQuery, BREAK, + DocumentNode, + getOperationAST, } from 'graphql'; import request = require('supertest'); @@ -842,6 +844,40 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => { }); }); + it('passes the rootValue function result to the resolver', async () => { + const expectedQuery = 'query: it passes rootValue'; + const expectedMutation = 'mutation: it passes rootValue'; + app = await createApp({ + graphqlOptions: { + schema, + rootValue: (documentNode: DocumentNode) => { + const op = getOperationAST(documentNode, undefined); + return op.operation === 'query' + ? expectedQuery + : expectedMutation; + }, + }, + }); + const queryReq = request(app) + .post('/graphql') + .send({ + query: 'query test{ testRootValue }', + }); + return queryReq.then(res => { + expect(res.status).toEqual(200); + expect(res.body.data.testRootValue).toEqual(expectedQuery); + }); + const mutationReq = request(app) + .post('/graphql') + .send({ + query: 'mutation test{ testMutation(echo: "ping") }', + }); + return mutationReq.then(res => { + expect(res.status).toEqual(200); + expect(res.body.data.testRootValue).toEqual(expectedMutation); + }); + }); + it('returns errors', async () => { const expected = 'Secret error message'; app = await createApp({