diff --git a/src/use/express.ts b/src/use/express.ts index 4098b52..79d8d28 100644 --- a/src/use/express.ts +++ b/src/use/express.ts @@ -14,6 +14,12 @@ import { RequestParams } from '../common'; * @category Server/express */ export interface RequestContext { + /** + * The Express response object for the current request. + * + * This can be used to manipulate the HTTP response, for example to set + * response headers or cookies. + */ res: Response; } @@ -82,16 +88,40 @@ export type HandlerOptions = * Create a GraphQL over HTTP spec compliant request handler for * the express framework. * + * The Express response is available through `req.context.res`. To expose it + * to GraphQL resolvers, return it from the `context` option. This can be used + * to manipulate the HTTP response, for example to set response headers or + * cookies. + * * ```js * import express from 'express'; // yarn add express * import { createHandler } from 'graphql-http/lib/use/express'; * import { schema } from './my-graphql-schema'; * * const app = express(); - * app.all('/graphql', createHandler({ schema })); + * + * app.all( + * '/graphql', + * createHandler({ + * schema, + * context(req) { + * return { + * res: req.context.res, + * }; + * }, + * }), + * ); * * app.listen({ port: 4000 }); - * console.log('Listening to port 4000'); + * ``` + * + * A resolver can then manipulate the response: + * + * ```js + * resolve(_, __, { res }) { + * res.setHeader('set-cookie', 'session=; Max-Age=0; Path=/'); + * return true; + * } * ``` * * @category Server/express diff --git a/tests/use.test.ts b/tests/use.test.ts index 114da4d..1df4987 100644 --- a/tests/use.test.ts +++ b/tests/use.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { GraphQLBoolean, GraphQLObjectType, GraphQLSchema } from 'graphql'; import net from 'net'; import { fetch } from '@whatwg-node/fetch'; import { serverAudits } from '../src/audits'; @@ -137,6 +138,66 @@ describe('express', () => { await dispose(); }); + + it('should allow manipulating the response from a resolver', async () => { + const responseSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + hello: { + type: GraphQLBoolean, + resolve: () => true, + }, + }, + }), + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: { + logout: { + type: GraphQLBoolean, + resolve: (_, __, context) => { + context.res.setHeader('x-test', 'test-x'); + return true; + }, + }, + }, + }), + }); + + const app = express(); + + app.all( + '/', + createExpressHandler({ + schema: responseSchema, + context(req) { + return { + res: req.context.res, + }; + }, + }), + ); + + const [url, , dispose] = startDisposableServer(app.listen(0)); + + const res = await fetch(url, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ + query: 'mutation { logout }', + }), + }); + + await expect(res.text()).resolves.toMatchInlineSnapshot( + `"{\"data\":{\"logout\":true}}"`, + ); + + expect(res.headers.get('x-test')).toBe('test-x'); + + await dispose(); + }); }); describe('fastify', () => {