Skip to content

Commit

Permalink
Support gateways without executors
Browse files Browse the repository at this point in the history
Previously gateways could express their executor in two ways: as the
executor method (required to exist!) and as part of the return value
from load (which was ignored!).

Now we just return it from load, and allow it to be null.

This supports a mocking use case. Fixes #5518. It lets you do something
like:

    import { addMocksToSchema } from '@graphql-tools/mock';
    const realGateway = new ApolloGateway();
    const gateway: GatewayInterface = {
      async load(options) {
        const { schema } = await realGateway.load(options);
        return {
          schema: addMocksToSchema({ schema }),
          executor: null,
        };
      }
      stop() {
        return realGateway.stop();
      }
      onSchemaLoadOrUpdate(callback) {
        return realGateway.onSchemaLoadOrUpdate(callback);
      }
    };
    const server = new ApolloServer({ gateway });

to define a server that follows a managed federation graph but executes
queries using mocking.
  • Loading branch information
glasser committed Jul 23, 2021
1 parent bb00b8b commit 861a3a7
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 27 deletions.
15 changes: 6 additions & 9 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,14 +281,6 @@ export class ApolloServerBase<
logger: this.logger,
}),
};

// The main thing that the Gateway does is replace execution with
// its own executor. It would be awkward if you always had to pass
// `gateway: gateway, executor: gateway` to this constructor, so
// we let specifying `gateway` be a shorthand for the above.
// (We won't actually invoke the executor until after we're successfully
// called `gateway.load`.)
this.requestOptions.executor = gateway.executor;
} else {
// We construct the schema synchronously so that we can fail fast if the
// schema can't be constructed. (This used to be more important because we
Expand Down Expand Up @@ -376,10 +368,15 @@ export class ApolloServerBase<
schemaManager,
};
try {
await schemaManager.start();
const executor = await schemaManager.start();
this.toDispose.add(async () => {
await schemaManager.stop();
});
if (executor) {
// If we loaded an executor from a gateway, use it to execute
// operations.
this.requestOptions.executor = executor;
}

const schemaDerivedData = schemaManager.getSchemaDerivedData();
const service: GraphQLServiceContext = {
Expand Down
15 changes: 6 additions & 9 deletions packages/apollo-server-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import type {
ApolloConfig,
ValueOrPromise,
GraphQLExecutor,
GraphQLExecutionResult,
GraphQLRequestContextExecutionDidStart,
ApolloConfigInput,
} from 'apollo-server-types';

Expand Down Expand Up @@ -50,7 +48,7 @@ export type SchemaChangeCallback = (apiSchema: GraphQLSchema) => void;

export type GraphQLServiceConfig = {
schema: GraphQLSchema;
executor: GraphQLExecutor;
executor: GraphQLExecutor | null;
};

export interface GatewayInterface {
Expand All @@ -71,13 +69,12 @@ export interface GatewayInterface {
}) => void,
): Unsubscriber;

// Note: The `TContext` typing here is not conclusively behaving as we expect:
// https://github.com/apollographql/apollo-server/pull/3811#discussion_r387381605
executor<TContext>(
requestContext: GraphQLRequestContextExecutionDidStart<TContext>,
): Promise<GraphQLExecutionResult>;

stop(): Promise<void>;

// Note: this interface used to have an `executor` method, and also return the
// executor from `load()`. ApolloServer would only use the former. We dropped
// this method and now use the latter, which allows you to make a "mock
// gateway" that updates the schema over time but uses normal execution.
}

// This was the name used for GatewayInterface in AS2; continue to export it so
Expand Down
6 changes: 5 additions & 1 deletion packages/apollo-server-core/src/utils/schemaManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GraphQLSchema } from 'graphql';
import {
ApolloConfig,
GraphQLExecutor,
GraphQLSchemaContext,
Logger,
} from 'apollo-server-types';
Expand Down Expand Up @@ -81,8 +82,9 @@ export class SchemaManager {
* - Initialize schema-derived data.
* - Synchronously notify onSchemaLoadOrUpdate() listeners of schema load, and
* asynchronously notify them of schema updates.
* - If we started a gateway, returns the gateway's executor; otherwise null.
*/
public async start(): Promise<void> {
public async start(): Promise<GraphQLExecutor | null> {
if (this.modeSpecificState.mode === 'gateway') {
const gateway = this.modeSpecificState.gateway;
if (gateway.onSchemaLoadOrUpdate) {
Expand Down Expand Up @@ -115,10 +117,12 @@ export class SchemaManager {
if (!this.schemaDerivedData) {
this.processSchemaLoadOrUpdateEvent({ apiSchema: config.schema });
}
return config.executor;
} else {
this.processSchemaLoadOrUpdateEvent({
apiSchema: this.modeSpecificState.apiSchema,
});
return null;
}
}

Expand Down
12 changes: 4 additions & 8 deletions packages/apollo-server-integration-testsuite/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
ApolloServerBase,
PluginDefinition,
GatewayInterface,
GraphQLExecutor,
GraphQLServiceConfig,
ApolloServerPluginInlineTrace,
ApolloServerPluginUsageReporting,
Expand Down Expand Up @@ -126,11 +125,9 @@ const schema = new GraphQLSchema({
const makeGatewayMock = ({
optionsSpy = (_options) => {},
unsubscribeSpy = () => {},
executor = async () => ({}),
}: {
optionsSpy?: (_options: any) => void;
unsubscribeSpy?: () => void;
executor?: GraphQLExecutor;
} = {}) => {
let resolution: GraphQLServiceConfig | null = null;
let rejection: Error | null = null;
Expand All @@ -145,7 +142,6 @@ const makeGatewayMock = ({
};

const mockedGateway: GatewayInterface = {
executor,
load: async (options) => {
optionsSpy(options);
// Make sure it's async
Expand Down Expand Up @@ -414,7 +410,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
Promise.resolve({ data: { testString: 'hi - but federated!' } }),
);

const { gateway, triggers } = makeGatewayMock({ executor });
const { gateway, triggers } = makeGatewayMock();

triggers.resolveLoad({ schema, executor });

Expand Down Expand Up @@ -2701,7 +2697,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
? Promise.resolve({ data: { testString1: 'hello' } })
: Promise.resolve({ data: { testString2: 'aloha' } });

const { gateway, triggers } = makeGatewayMock({ executor });
const { gateway, triggers } = makeGatewayMock();

triggers.resolveLoad({
schema: makeQueryTypeWithField('testString1'),
Expand Down Expand Up @@ -2782,7 +2778,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
return { data: { testString: 'hi - but federated!' } };
};

const { gateway, triggers } = makeGatewayMock({ executor });
const { gateway, triggers } = makeGatewayMock();

triggers.resolveLoad({ schema, executor });
const { url: uri } = await createApolloServer({
Expand Down Expand Up @@ -2844,7 +2840,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
return { data: { [`testString${i}`]: `${i}` } };
};

const { gateway, triggers } = makeGatewayMock({ executor });
const { gateway, triggers } = makeGatewayMock();

triggers.resolveLoad({
schema: makeQueryTypeWithField('testString1'),
Expand Down

0 comments on commit 861a3a7

Please sign in to comment.