Skip to content

Commit 0790ec0

Browse files
committed
Merge branch 'abernix/fix-executeOperation-context-cloning' into release-2.14.1
2 parents 49c44eb + 79c0025 commit 0790ec0

File tree

4 files changed

+131
-28
lines changed

4 files changed

+131
-28
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ The version headers in this history reflect the versions of Apollo Server itself
1111
1212
- _Nothing yet! Stay tuned!_
1313

14+
### v2.14.1
15+
16+
- `apollo-server-testing`: Ensure that user-provided context is cloned when using `createTestClient`, per the instructions in the [intergration testing]() section of the Apollo Server documentation. [Issue #4170](https://github.com/apollographql/apollo-server/issues/4170) [PR #TODO](https://github.com/apollographql/apollo-server/pull/TODO)
17+
1418
### v2.14.0
1519

1620
- `apollo-server-core` / `apollo-server-plugin-base`: Add support for `willResolveField` and corresponding end-handler within `executionDidStart`. This brings the remaining bit of functionality that was previously only available from `graphql-extensions` to the new plugin API. The `graphql-extensions` API (which was never documented) will be deprecated in Apollo Server 3.x. To see the documentation for the request pipeline API, see [its documentation](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). For more details, see the attached PR. [PR #3988](https://github.com/apollographql/apollo-server/pull/3988)

packages/apollo-server-core/src/ApolloServer.ts

+10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
CacheControlExtensionOptions,
7979
} from 'apollo-cache-control';
8080
import { getEngineApiKey, getEngineGraphVariant } from "apollo-engine-reporting/dist/agent";
81+
import { cloneObject } from "./runHttpQuery";
8182

8283
const NoIntrospection = (context: ValidationContext) => ({
8384
Field(node: FieldDefinitionNode) {
@@ -855,8 +856,17 @@ export class ApolloServerBase {
855856

856857
if (typeof options.context === 'function') {
857858
options.context = (options.context as () => never)();
859+
} else if (typeof options.context === 'object') {
860+
// FIXME: We currently shallow clone the context for every request,
861+
// but that's unlikely to be what people want.
862+
// We allow passing in a function for `context` to ApolloServer,
863+
// but this only runs once for a batched request (because this is resolved
864+
// in ApolloServer#graphQLServerOptions, before runHttpQuery is invoked).
865+
// NOTE: THIS IS DUPLICATED IN runHttpQuery.ts' buildRequestContext.
866+
options.context = cloneObject(options.context);
858867
}
859868

869+
860870
const requestCtx: GraphQLRequestContext = {
861871
logger: this.logger,
862872
schema: options.schema,

packages/apollo-server-core/src/runHttpQuery.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export async function processHTTPRequest<TContext>(
246246
// We allow passing in a function for `context` to ApolloServer,
247247
// but this only runs once for a batched request (because this is resolved
248248
// in ApolloServer#graphQLServerOptions, before runHttpQuery is invoked).
249+
// NOTE: THIS IS DUPLICATED IN ApolloServerBase.prototype.executeOperation.
249250
const context = cloneObject(options.context);
250251
return {
251252
// While `logger` is guaranteed by internal Apollo Server usage of
@@ -458,6 +459,6 @@ function prettyJSONStringify(value: any) {
458459
return JSON.stringify(value) + '\n';
459460
}
460461

461-
function cloneObject<T extends Object>(object: T): T {
462+
export function cloneObject<T extends Object>(object: T): T {
462463
return Object.assign(Object.create(Object.getPrototypeOf(object)), object);
463464
}

packages/apollo-server-integration-testsuite/src/ApolloServer.ts

+115-27
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,13 @@ export function testApolloServer<AS extends ApolloServerBase>(
566566
describe('Plugins', () => {
567567
let apolloFetch: ApolloFetch;
568568
let apolloFetchResponse: ParsedResponse;
569+
let serverInstance: ApolloServerBase;
569570

570571
const setupApolloServerAndFetchPairForPlugins = async (
571572
plugins: PluginDefinition[] = [],
572573
) => {
573-
const { url: uri } = await createApolloServer({
574+
const { url: uri, server } = await createApolloServer({
575+
context: { customContext: true },
574576
typeDefs: gql`
575577
type Query {
576578
justAField: String
@@ -579,6 +581,8 @@ export function testApolloServer<AS extends ApolloServerBase>(
579581
plugins,
580582
});
581583

584+
serverInstance = server;
585+
582586
apolloFetch = createApolloFetch({ uri })
583587
// Store the response so we can inspect it.
584588
.useAfter(({ response }, next) => {
@@ -587,6 +591,56 @@ export function testApolloServer<AS extends ApolloServerBase>(
587591
});
588592
};
589593

594+
// Test for https://github.com/apollographql/apollo-server/issues/4170
595+
it('works when using executeOperation', async () => {
596+
const encounteredFields = [];
597+
const encounteredContext = [];
598+
await setupApolloServerAndFetchPairForPlugins([
599+
{
600+
requestDidStart: () => ({
601+
executionDidStart: () => ({
602+
willResolveField({ info, context }) {
603+
encounteredFields.push(info.path);
604+
encounteredContext.push(context);
605+
},
606+
}),
607+
}),
608+
},
609+
]);
610+
611+
// The bug in 4170 (linked above) was occurring because of a failure
612+
// to the context in `executeOperation`, in the same way that occurs
613+
// in `runHttpQuery` prior to entering the request pipeline. That
614+
// resulted in the inability to attach a symbol to the context because
615+
// the symbol already existed on the context. Of course, a context
616+
// is only created after the first invocation, so we'll run this twice
617+
// to encounter the error where it was in the way when we tried to set
618+
// it the second time. While we could have tested for the property
619+
// before assigning to it, that is not the contract we have with the
620+
// context, which should have been copied on `executeOperation` (which
621+
// is meant to be used by testing, currently).
622+
await serverInstance.executeOperation({
623+
query: '{ justAField }',
624+
});
625+
await serverInstance.executeOperation({
626+
query: '{ justAField }',
627+
});
628+
629+
expect(encounteredFields).toStrictEqual([
630+
{ key: 'justAField', prev: undefined },
631+
{ key: 'justAField', prev: undefined },
632+
]);
633+
634+
// This bit is just to ensure that nobody removes `context` from the
635+
// `setupApolloServerAndFetchPairForPlugins` thinking it's unimportant.
636+
// When a custom context is not provided, a new one is initialized
637+
// on each request.
638+
expect(encounteredContext).toStrictEqual([
639+
expect.objectContaining({customContext: true}),
640+
expect.objectContaining({customContext: true}),
641+
]);
642+
});
643+
590644
it('returns correct status code for a normal operation', async () => {
591645
await setupApolloServerAndFetchPairForPlugins();
592646

@@ -1492,37 +1546,71 @@ export function testApolloServer<AS extends ApolloServerBase>(
14921546
expect(spy).toHaveBeenCalledTimes(2);
14931547
});
14941548

1495-
it('clones the context for every request', async () => {
1496-
const uniqueContext = { key: 'major' };
1497-
const spy = jest.fn(() => 'hi');
1498-
const typeDefs = gql`
1499-
type Query {
1500-
hello: String
1501-
}
1502-
`;
1503-
const resolvers = {
1504-
Query: {
1505-
hello: (_parent: any, _args: any, context: any) => {
1506-
expect(context.key).toEqual('major');
1507-
context.key = 'minor';
1508-
return spy();
1549+
describe('context cloning', () => {
1550+
it('clones the context for request pipeline requests', async () => {
1551+
const uniqueContext = { key: 'major' };
1552+
const spy = jest.fn(() => 'hi');
1553+
const typeDefs = gql`
1554+
type Query {
1555+
hello: String
1556+
}
1557+
`;
1558+
const resolvers = {
1559+
Query: {
1560+
hello: (_parent: any, _args: any, context: any) => {
1561+
expect(context.key).toEqual('major');
1562+
context.key = 'minor';
1563+
return spy();
1564+
},
15091565
},
1510-
},
1511-
};
1512-
const { url: uri } = await createApolloServer({
1513-
typeDefs,
1514-
resolvers,
1515-
context: uniqueContext,
1566+
};
1567+
const { url: uri } = await createApolloServer({
1568+
typeDefs,
1569+
resolvers,
1570+
context: uniqueContext,
1571+
});
1572+
1573+
const apolloFetch = createApolloFetch({ uri });
1574+
1575+
expect(spy).not.toBeCalled();
1576+
1577+
await apolloFetch({ query: '{hello}' });
1578+
expect(spy).toHaveBeenCalledTimes(1);
1579+
await apolloFetch({ query: '{hello}' });
1580+
expect(spy).toHaveBeenCalledTimes(2);
15161581
});
15171582

1518-
const apolloFetch = createApolloFetch({ uri });
1583+
// https://github.com/apollographql/apollo-server/issues/4170
1584+
it('for every request with executeOperation', async () => {
1585+
const uniqueContext = { key: 'major' };
1586+
const spy = jest.fn(() => 'hi');
1587+
const typeDefs = gql`
1588+
type Query {
1589+
hello: String
1590+
}
1591+
`;
1592+
const resolvers = {
1593+
Query: {
1594+
hello: (_parent: any, _args: any, context: any) => {
1595+
expect(context.key).toEqual('major');
1596+
context.key = 'minor';
1597+
return spy();
1598+
},
1599+
},
1600+
};
1601+
const { server } = await createApolloServer({
1602+
typeDefs,
1603+
resolvers,
1604+
context: uniqueContext,
1605+
});
15191606

1520-
expect(spy).not.toBeCalled();
1607+
expect(spy).not.toBeCalled();
15211608

1522-
await apolloFetch({ query: '{hello}' });
1523-
expect(spy).toHaveBeenCalledTimes(1);
1524-
await apolloFetch({ query: '{hello}' });
1525-
expect(spy).toHaveBeenCalledTimes(2);
1609+
await server.executeOperation({ query: '{hello}' });
1610+
expect(spy).toHaveBeenCalledTimes(1);
1611+
await server.executeOperation({ query: '{hello}' });
1612+
expect(spy).toHaveBeenCalledTimes(2);
1613+
});
15261614
});
15271615

15281616
describe('as a function', () => {

0 commit comments

Comments
 (0)