@subsquid/apollo-server-core
Version:
Core engine for Apollo GraphQL server
296 lines (259 loc) • 9.4 kB
text/typescript
import type {
CacheHint,
WithRequired,
GraphQLRequest,
GraphQLRequestContextExecutionDidStart,
GraphQLResponse,
GraphQLRequestContextWillSendResponse,
GraphQLRequestContext,
GraphQLRequestContextDidEncounterErrors,
GraphQLRequestContextDidResolveSource,
GraphQLRequestContextParsingDidStart,
GraphQLRequestContextValidationDidStart,
SchemaHash,
BaseContext,
} from 'apollo-server-types';
import type { Logger } from '@apollo/utils.logger';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql/type';
import {
enablePluginsForSchemaResolvers,
symbolExecutionDispatcherWillResolveField,
} from './schemaInstrumentation';
import type {
ApolloServerPlugin,
GraphQLRequestExecutionListener,
GraphQLServerListener,
} from 'apollo-server-plugin-base';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import { Dispatcher } from './dispatcher';
import { getOperationAST, parse, validate as graphqlValidate } from 'graphql';
import { newCachePolicy } from '../cachePolicy';
// This test harness guarantees the presence of `query`.
type IPluginTestHarnessGraphqlRequest = WithRequired<GraphQLRequest, 'query'>;
type IPluginTestHarnessExecutionDidStart<TContext> =
GraphQLRequestContextExecutionDidStart<TContext> & {
request: IPluginTestHarnessGraphqlRequest;
};
export default async function pluginTestHarness<TContext extends BaseContext>({
pluginInstance,
schema,
logger,
graphqlRequest,
overallCachePolicy,
executor,
context = Object.create(null),
}: {
/**
* An instance of the plugin to test.
*/
pluginInstance: ApolloServerPlugin<TContext>;
/**
* The optional schema that will be received by the executor. If not
* specified, a simple default schema will be created. In either case,
* the schema will be mutated by wrapping the resolvers with the
* `willResolveField` instrumentation that will allow it to respond to
* that lifecycle hook's implementations plugins.
*/
schema?: GraphQLSchema;
/**
* An optional logger (Defaults to `console`)
*/
logger?: Logger;
/**
* The `GraphQLRequest` which will be received by the `executor`. The
* `query` is required, and this doesn't support anything more exotic,
* like automated persisted queries (APQ).
*/
graphqlRequest: IPluginTestHarnessGraphqlRequest;
/**
* Overall cache control policy.
*/
overallCachePolicy?: Required<CacheHint>;
/**
* This method will be executed to retrieve the response.
*/
executor: (
requestContext: IPluginTestHarnessExecutionDidStart<TContext>,
) => Promise<GraphQLResponse>;
/**
* (optional) To provide a user context, if necessary.
*/
context?: TContext;
}): Promise<GraphQLRequestContextWillSendResponse<TContext>> {
if (!schema) {
schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'hello world';
},
},
},
}),
});
}
let serverListener: GraphQLServerListener | undefined;
if (typeof pluginInstance.serverWillStart === 'function') {
const maybeServerListener = await pluginInstance.serverWillStart({
logger: logger || console,
schema,
schemaHash: 'deprecated' as SchemaHash,
serverlessFramework: false,
apollo: {
key: 'some-key',
graphRef: 'graph@current',
},
});
if (maybeServerListener?.serverWillStop) {
serverListener = maybeServerListener;
}
}
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
const requestContext: Mutable<GraphQLRequestContext<TContext>> = {
logger: logger || console,
schema,
schemaHash: 'deprecated' as SchemaHash,
request: graphqlRequest,
metrics: Object.create(null),
source: graphqlRequest.query,
cache: new InMemoryLRUCache(),
context,
overallCachePolicy: newCachePolicy(),
requestIsBatched: false,
};
if (requestContext.source === undefined) {
throw new Error('No source provided for test');
}
if (overallCachePolicy) {
requestContext.overallCachePolicy.replace(overallCachePolicy);
}
if (typeof pluginInstance.requestDidStart !== 'function') {
throw new Error('This test harness expects this to be defined.');
}
const listener = await pluginInstance.requestDidStart(requestContext);
const dispatcher = new Dispatcher(listener ? [listener] : []);
const executionListeners: GraphQLRequestExecutionListener<TContext>[] = [];
// Let the plugins know that we now have a STRING of what we hope will
// parse and validate into a document we can execute on. Unless we have
// retrieved this from our APQ cache, there's no guarantee that it is
// syntactically correct, so this string should not be trusted as a valid
// document until after it's parsed and validated.
await dispatcher.invokeHook(
'didResolveSource',
requestContext as GraphQLRequestContextDidResolveSource<TContext>,
);
if (!requestContext.document) {
await dispatcher.invokeDidStartHook(
'parsingDidStart',
requestContext as GraphQLRequestContextParsingDidStart<TContext>,
);
try {
requestContext.document = parse(requestContext.source, undefined);
} catch (syntaxError) {
const errorOrErrors = syntaxError;
requestContext.errors = Array.isArray(errorOrErrors)
? errorOrErrors
: [errorOrErrors];
await dispatcher.invokeHook(
'didEncounterErrors',
requestContext as GraphQLRequestContextDidEncounterErrors<TContext>,
);
await dispatcher.invokeHook(
'willSendResponse',
requestContext as GraphQLRequestContextWillSendResponse<TContext>,
);
return requestContext as GraphQLRequestContextWillSendResponse<TContext>;
}
const validationDidEnd = await dispatcher.invokeDidStartHook(
'validationDidStart',
requestContext as GraphQLRequestContextValidationDidStart<TContext>,
);
/**
* We are validating only with the default rules.
*/
const validationErrors = graphqlValidate(
requestContext.schema,
requestContext.document,
);
if (validationErrors.length !== 0) {
requestContext.errors = validationErrors;
validationDidEnd(validationErrors);
await dispatcher.invokeHook(
'didEncounterErrors',
requestContext as GraphQLRequestContextDidEncounterErrors<TContext>,
);
await dispatcher.invokeHook(
'willSendResponse',
requestContext as GraphQLRequestContextWillSendResponse<TContext>,
);
return requestContext as GraphQLRequestContextWillSendResponse<TContext>;
} else {
validationDidEnd();
}
}
const operation = getOperationAST(
requestContext.document,
requestContext.request.operationName,
);
requestContext.operation = operation || undefined;
// We'll set `operationName` to `null` for anonymous operations. Note that
// usage reporting relies on the fact that the requestContext passed
// to requestDidStart is mutated to add this field before requestDidEnd is
// called
requestContext.operationName = operation?.name?.value || null;
await dispatcher.invokeHook(
'didResolveOperation',
requestContext as GraphQLRequestContextExecutionDidStart<TContext>,
);
// This execution dispatcher logic is duplicated in the request pipeline
// right now.
(
await dispatcher.invokeHook(
'executionDidStart',
requestContext as GraphQLRequestContextExecutionDidStart<TContext>,
)
).forEach((executionListener) => {
if (executionListener) {
executionListeners.push(executionListener);
}
});
executionListeners.reverse();
const executionDispatcher = new Dispatcher(executionListeners);
if (executionDispatcher.hasHook('willResolveField')) {
// Create a callback that will trigger the execution dispatcher's
// `willResolveField` hook. We will attach this to the context on a
// symbol so it can be invoked by our `wrapField` method during execution.
const invokeWillResolveField: GraphQLRequestExecutionListener<TContext>['willResolveField'] =
(...args) =>
executionDispatcher.invokeSyncDidStartHook('willResolveField', ...args);
Object.defineProperty(
requestContext.context,
symbolExecutionDispatcherWillResolveField,
{ value: invokeWillResolveField },
);
// If the schema is already enabled, this is a no-op. Otherwise, the
// schema will be augmented so it is able to invoke willResolveField.
enablePluginsForSchemaResolvers(schema);
}
try {
// `response` is readonly, so we'll cast to `any` to assign to it.
(requestContext.response as any) = await executor(
requestContext as IPluginTestHarnessExecutionDidStart<TContext>,
);
await executionDispatcher.invokeHook('executionDidEnd');
} catch (executionErr) {
await executionDispatcher.invokeHook(
'executionDidEnd',
executionErr as Error,
);
}
await dispatcher.invokeHook(
'willSendResponse',
requestContext as GraphQLRequestContextWillSendResponse<TContext>,
);
await serverListener?.serverWillStop?.();
return requestContext as GraphQLRequestContextWillSendResponse<TContext>;
}