mirror of
https://github.com/vale981/apollo-server
synced 2025-03-05 09:41:40 -05:00
Refactor runQuery into GraphQLRequestProcessor
This commit is contained in:
parent
38e7b6a5b6
commit
a09d514e59
6 changed files with 314 additions and 60 deletions
|
@ -215,17 +215,16 @@ export class ApolloServerBase {
|
|||
// or cacheControl.
|
||||
this.extensions = [];
|
||||
|
||||
const debugDefault =
|
||||
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
|
||||
const debug =
|
||||
requestOptions.debug !== undefined ? requestOptions.debug : debugDefault;
|
||||
|
||||
// Error formatting should happen after the engine reporting agent, so that
|
||||
// engine gets the unmasked errors if necessary
|
||||
if (this.requestOptions.formatError) {
|
||||
this.extensions.push(
|
||||
() =>
|
||||
new FormatErrorExtension(
|
||||
this.requestOptions.formatError!,
|
||||
this.requestOptions.debug,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.extensions.push(
|
||||
() => new FormatErrorExtension(requestOptions.formatError, debug),
|
||||
);
|
||||
|
||||
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
|
||||
this.engineReportingAgent = new EngineReportingAgent(
|
||||
|
|
|
@ -2,10 +2,10 @@ import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
|
|||
import { formatApolloErrors } from 'apollo-server-errors';
|
||||
|
||||
export class FormatErrorExtension extends GraphQLExtension {
|
||||
private formatError: Function;
|
||||
private formatError?: Function;
|
||||
private debug: boolean;
|
||||
|
||||
public constructor(formatError: Function, debug: boolean = false) {
|
||||
public constructor(formatError?: Function, debug: boolean = false) {
|
||||
super();
|
||||
this.formatError = formatError;
|
||||
this.debug = debug;
|
||||
|
|
250
packages/apollo-server-core/src/requestProcessing.ts
Normal file
250
packages/apollo-server-core/src/requestProcessing.ts
Normal file
|
@ -0,0 +1,250 @@
|
|||
import { Request } from 'apollo-server-env';
|
||||
import {
|
||||
GraphQLSchema,
|
||||
GraphQLFieldResolver,
|
||||
ValidationContext,
|
||||
ASTVisitor,
|
||||
DocumentNode,
|
||||
parse,
|
||||
specifiedRules,
|
||||
validate,
|
||||
GraphQLError,
|
||||
ExecutionArgs,
|
||||
ExecutionResult,
|
||||
execute,
|
||||
getOperationAST,
|
||||
OperationDefinitionNode,
|
||||
} from 'graphql';
|
||||
import {
|
||||
GraphQLExtension,
|
||||
GraphQLExtensionStack,
|
||||
enableGraphQLExtensions,
|
||||
} from 'graphql-extensions';
|
||||
import { PersistedQueryOptions } from './';
|
||||
import {
|
||||
CacheControlExtension,
|
||||
CacheControlExtensionOptions,
|
||||
} from 'apollo-cache-control';
|
||||
import { TracingExtension } from 'apollo-tracing';
|
||||
import {
|
||||
fromGraphQLError,
|
||||
SyntaxError,
|
||||
ValidationError,
|
||||
} from 'apollo-server-errors';
|
||||
|
||||
export interface GraphQLRequest {
|
||||
query?: string;
|
||||
operationName?: string;
|
||||
variables?: { [name: string]: any };
|
||||
extensions?: object;
|
||||
httpRequest?: Pick<Request, 'url' | 'method' | 'headers'>;
|
||||
}
|
||||
|
||||
export interface GraphQLRequestOptions<TContext = any> {
|
||||
schema: GraphQLSchema;
|
||||
|
||||
rootValue?: any;
|
||||
context: TContext;
|
||||
|
||||
validationRules?: ValidationRule[];
|
||||
fieldResolver?: GraphQLFieldResolver<any, TContext>;
|
||||
|
||||
debug?: boolean;
|
||||
|
||||
extensions?: Array<() => GraphQLExtension>;
|
||||
tracing?: boolean;
|
||||
persistedQueries?: PersistedQueryOptions;
|
||||
cacheControl?: CacheControlExtensionOptions;
|
||||
|
||||
formatResponse?: Function;
|
||||
}
|
||||
|
||||
export type ValidationRule = (context: ValidationContext) => ASTVisitor;
|
||||
|
||||
export class InvalidGraphQLRequestError extends Error {}
|
||||
|
||||
export interface GraphQLResponse {
|
||||
data?: object;
|
||||
errors?: GraphQLError[];
|
||||
extensions?: object;
|
||||
}
|
||||
|
||||
export interface GraphQLRequestProcessor {
|
||||
willExecuteOperation?(operation: OperationDefinitionNode): void;
|
||||
}
|
||||
|
||||
export class GraphQLRequestProcessor {
|
||||
extensionStack!: GraphQLExtensionStack;
|
||||
|
||||
constructor(public options: GraphQLRequestOptions) {
|
||||
this.initializeExtensions();
|
||||
}
|
||||
|
||||
initializeExtensions() {
|
||||
// If custom extension factories were provided, create per-request extension
|
||||
// objects.
|
||||
const extensions = this.options.extensions
|
||||
? this.options.extensions.map(f => f())
|
||||
: [];
|
||||
|
||||
// If you're running behind an engineproxy, set these options to turn on
|
||||
// tracing and cache-control extensions.
|
||||
if (this.options.tracing) {
|
||||
extensions.push(new TracingExtension());
|
||||
}
|
||||
if (this.options.cacheControl === true) {
|
||||
extensions.push(new CacheControlExtension());
|
||||
} else if (this.options.cacheControl) {
|
||||
extensions.push(new CacheControlExtension(this.options.cacheControl));
|
||||
}
|
||||
|
||||
this.extensionStack = new GraphQLExtensionStack(extensions);
|
||||
|
||||
// We unconditionally create an extensionStack, even if there are no
|
||||
// extensions (so that we don't have to litter the rest of this function with
|
||||
// `if (extensionStack)`, but we don't instrument the schema unless there
|
||||
// actually are extensions. We do unconditionally put the stack on the
|
||||
// context, because if some other call had extensions and the schema is
|
||||
// already instrumented, that's the only way to get a custom fieldResolver to
|
||||
// work.
|
||||
if (extensions.length > 0) {
|
||||
enableGraphQLExtensions(this.options.schema);
|
||||
}
|
||||
this.options.context._extensionStack = this.extensionStack;
|
||||
}
|
||||
|
||||
async processRequest(request: GraphQLRequest): Promise<GraphQLResponse> {
|
||||
if (!request.query) {
|
||||
throw new InvalidGraphQLRequestError();
|
||||
}
|
||||
|
||||
const requestDidEnd = this.extensionStack.requestDidStart({
|
||||
request: request.httpRequest!,
|
||||
queryString: request.query,
|
||||
operationName: request.operationName,
|
||||
variables: request.variables,
|
||||
persistedQueryHit: false,
|
||||
persistedQueryRegister: false,
|
||||
});
|
||||
|
||||
try {
|
||||
let document: DocumentNode;
|
||||
try {
|
||||
document = this.parse(request.query);
|
||||
} catch (syntaxError) {
|
||||
return this.willSendResponse({
|
||||
errors: [
|
||||
fromGraphQLError(syntaxError, {
|
||||
errorClass: SyntaxError,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const validationErrors = this.validate(document);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
return this.willSendResponse({
|
||||
errors: validationErrors.map(validationError =>
|
||||
fromGraphQLError(validationError, {
|
||||
errorClass: ValidationError,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const operation = getOperationAST(document, request.operationName);
|
||||
// If we don't find an operation, we'll leave it to `buildExecutionContext`
|
||||
// to throw an appropriate error.
|
||||
if (operation && this.willExecuteOperation) {
|
||||
this.willExecuteOperation(operation);
|
||||
}
|
||||
|
||||
let response: GraphQLResponse;
|
||||
|
||||
try {
|
||||
response = (await this.execute(
|
||||
document,
|
||||
request.operationName,
|
||||
request.variables,
|
||||
)) as GraphQLResponse;
|
||||
} catch (executionError) {
|
||||
return this.willSendResponse({
|
||||
errors: [fromGraphQLError(executionError)],
|
||||
});
|
||||
}
|
||||
|
||||
const formattedExtensions = this.extensionStack.format();
|
||||
if (Object.keys(formattedExtensions).length > 0) {
|
||||
response.extensions = formattedExtensions;
|
||||
}
|
||||
|
||||
if (this.options.formatResponse) {
|
||||
response = this.options.formatResponse(response);
|
||||
}
|
||||
|
||||
return this.willSendResponse(response);
|
||||
} finally {
|
||||
requestDidEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private willSendResponse(response: GraphQLResponse): GraphQLResponse {
|
||||
return this.extensionStack.willSendResponse({
|
||||
graphqlResponse: response,
|
||||
}).graphqlResponse;
|
||||
}
|
||||
|
||||
parse(query: string): DocumentNode {
|
||||
const parsingDidEnd = this.extensionStack.parsingDidStart({
|
||||
queryString: query,
|
||||
});
|
||||
|
||||
try {
|
||||
return parse(query);
|
||||
} finally {
|
||||
parsingDidEnd();
|
||||
}
|
||||
}
|
||||
|
||||
validate(document: DocumentNode): ReadonlyArray<GraphQLError> {
|
||||
let rules = specifiedRules;
|
||||
if (this.options.validationRules) {
|
||||
rules = rules.concat(this.options.validationRules);
|
||||
}
|
||||
|
||||
const validationDidEnd = this.extensionStack.validationDidStart();
|
||||
|
||||
try {
|
||||
return validate(this.options.schema, document, rules);
|
||||
} finally {
|
||||
validationDidEnd();
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
document: DocumentNode,
|
||||
operationName: GraphQLRequest['operationName'],
|
||||
variables: GraphQLRequest['variables'],
|
||||
): Promise<ExecutionResult> {
|
||||
const executionArgs: ExecutionArgs = {
|
||||
schema: this.options.schema,
|
||||
document,
|
||||
rootValue: this.options.rootValue,
|
||||
contextValue: this.options.context,
|
||||
variableValues: variables,
|
||||
operationName,
|
||||
fieldResolver: this.options.fieldResolver,
|
||||
};
|
||||
|
||||
const executionDidEnd = this.extensionStack.executionDidStart({
|
||||
executionArgs,
|
||||
});
|
||||
|
||||
try {
|
||||
return execute(executionArgs);
|
||||
} finally {
|
||||
executionDidEnd();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,10 +3,7 @@ const sha256 = require('hash.js/lib/hash/sha/256');
|
|||
|
||||
import { CacheControlExtensionOptions } from 'apollo-cache-control';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { Request } from 'apollo-server-env';
|
||||
import { runQuery, QueryOptions } from './runQuery';
|
||||
import {
|
||||
default as GraphQLOptions,
|
||||
resolveGraphqlOptions,
|
||||
|
@ -17,6 +14,7 @@ import {
|
|||
PersistedQueryNotFoundError,
|
||||
} from 'apollo-server-errors';
|
||||
import { calculateCacheControlHeaders } from './caching';
|
||||
import { GraphQLRequestProcessor } from 'apollo-server-core/src/requestProcessing';
|
||||
|
||||
export interface HttpQueryRequest {
|
||||
method: string;
|
||||
|
@ -277,23 +275,6 @@ export async function runHttpQuery(
|
|||
throw new HttpQueryError(400, 'GraphQL queries must be strings.');
|
||||
}
|
||||
|
||||
// GET operations should only be queries (not mutations). We want to throw
|
||||
// a particular HTTP error in that case, but we don't actually parse the
|
||||
// query until we're in runQuery, so we declare the error we want to throw
|
||||
// here and pass it into runQuery.
|
||||
// TODO this could/should be added as a validation rule rather than an ad hoc error
|
||||
let nonQueryError;
|
||||
if (isGetRequest) {
|
||||
nonQueryError = new HttpQueryError(
|
||||
405,
|
||||
`GET supports only query operation`,
|
||||
false,
|
||||
{
|
||||
Allow: 'POST',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const operationName = requestParams.operationName;
|
||||
|
||||
let variables = requestParams.variables;
|
||||
|
@ -380,33 +361,46 @@ export async function runHttpQuery(
|
|||
}
|
||||
}
|
||||
|
||||
let params: QueryOptions = {
|
||||
const requestProcessor = new GraphQLRequestProcessor({
|
||||
schema: optionsObject.schema,
|
||||
queryString,
|
||||
nonQueryError,
|
||||
variables: variables,
|
||||
context,
|
||||
rootValue: optionsObject.rootValue,
|
||||
operationName: operationName,
|
||||
context,
|
||||
validationRules: optionsObject.validationRules,
|
||||
formatError: optionsObject.formatError,
|
||||
formatResponse: optionsObject.formatResponse,
|
||||
fieldResolver: optionsObject.fieldResolver,
|
||||
debug: optionsObject.debug,
|
||||
tracing: optionsObject.tracing,
|
||||
cacheControl: cacheControl
|
||||
? omit(cacheControl, [
|
||||
'calculateHttpHeaders',
|
||||
'stripFormattedExtensions',
|
||||
])
|
||||
: false,
|
||||
request: request.request,
|
||||
extensions: optionsObject.extensions,
|
||||
persistedQueryHit,
|
||||
persistedQueryRegister,
|
||||
};
|
||||
|
||||
return runQuery(params);
|
||||
extensions: optionsObject.extensions,
|
||||
tracing: optionsObject.tracing,
|
||||
cacheControl: cacheControl,
|
||||
|
||||
formatResponse: optionsObject.formatResponse,
|
||||
|
||||
debug: optionsObject.debug,
|
||||
});
|
||||
|
||||
// GET operations should only be queries (not mutations). We want to throw
|
||||
// a particular HTTP error in that case.
|
||||
if (isGetRequest) {
|
||||
requestProcessor.willExecuteOperation = operation => {
|
||||
if (operation.operation !== 'query') {
|
||||
throw new HttpQueryError(
|
||||
405,
|
||||
`GET supports only query operation`,
|
||||
false,
|
||||
{
|
||||
Allow: 'POST',
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return requestProcessor.processRequest({
|
||||
query: queryString,
|
||||
operationName,
|
||||
variables,
|
||||
extensions,
|
||||
httpRequest: request.request,
|
||||
});
|
||||
} catch (e) {
|
||||
// Populate any HttpQueryError to our handler which should
|
||||
// convert it to Http Error.
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
"outDir": "./dist",
|
||||
"noUnusedLocals": false,
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/__tests__", "**/__mocks__"]
|
||||
|
|
|
@ -33,7 +33,7 @@ export interface GraphQLResponse {
|
|||
|
||||
export class GraphQLExtension<TContext = any> {
|
||||
public requestDidStart?(o: {
|
||||
request: Request;
|
||||
request: Pick<Request, 'url' | 'method' | 'headers'>;
|
||||
queryString?: string;
|
||||
parsedQuery?: DocumentNode;
|
||||
operationName?: string;
|
||||
|
@ -71,7 +71,7 @@ export class GraphQLExtensionStack<TContext = any> {
|
|||
}
|
||||
|
||||
public requestDidStart(o: {
|
||||
request: Request;
|
||||
request: Pick<Request, 'url' | 'method' | 'headers'>;
|
||||
queryString?: string;
|
||||
parsedQuery?: DocumentNode;
|
||||
operationName?: string;
|
||||
|
@ -154,9 +154,13 @@ export class GraphQLExtensionStack<TContext = any> {
|
|||
const endHandlers: EndHandler[] = [];
|
||||
this.extensions.forEach(extension => {
|
||||
// Invoke the start handler, which may return an end handler.
|
||||
const endHandler = startInvoker(extension);
|
||||
if (endHandler) {
|
||||
endHandlers.push(endHandler);
|
||||
try {
|
||||
const endHandler = startInvoker(extension);
|
||||
if (endHandler) {
|
||||
endHandlers.push(endHandler);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
return (...errors: Array<Error>) => {
|
||||
|
@ -164,7 +168,13 @@ export class GraphQLExtensionStack<TContext = any> {
|
|||
// first handler in the stack "surrounds" the entire event's process
|
||||
// (helpful for tracing/reporting!)
|
||||
endHandlers.reverse();
|
||||
endHandlers.forEach(endHandler => endHandler(...errors));
|
||||
for (const endHandler of endHandlers) {
|
||||
try {
|
||||
endHandler(...errors);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue