mirror of
https://github.com/vale981/apollo-server
synced 2025-03-06 02:01: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.
|
// or cacheControl.
|
||||||
this.extensions = [];
|
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
|
// Error formatting should happen after the engine reporting agent, so that
|
||||||
// engine gets the unmasked errors if necessary
|
// engine gets the unmasked errors if necessary
|
||||||
if (this.requestOptions.formatError) {
|
|
||||||
this.extensions.push(
|
this.extensions.push(
|
||||||
() =>
|
() => new FormatErrorExtension(requestOptions.formatError, debug),
|
||||||
new FormatErrorExtension(
|
|
||||||
this.requestOptions.formatError!,
|
|
||||||
this.requestOptions.debug,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
|
if (engine || (engine !== false && process.env.ENGINE_API_KEY)) {
|
||||||
this.engineReportingAgent = new EngineReportingAgent(
|
this.engineReportingAgent = new EngineReportingAgent(
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
|
||||||
import { formatApolloErrors } from 'apollo-server-errors';
|
import { formatApolloErrors } from 'apollo-server-errors';
|
||||||
|
|
||||||
export class FormatErrorExtension extends GraphQLExtension {
|
export class FormatErrorExtension extends GraphQLExtension {
|
||||||
private formatError: Function;
|
private formatError?: Function;
|
||||||
private debug: boolean;
|
private debug: boolean;
|
||||||
|
|
||||||
public constructor(formatError: Function, debug: boolean = false) {
|
public constructor(formatError?: Function, debug: boolean = false) {
|
||||||
super();
|
super();
|
||||||
this.formatError = formatError;
|
this.formatError = formatError;
|
||||||
this.debug = debug;
|
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 { CacheControlExtensionOptions } from 'apollo-cache-control';
|
||||||
|
|
||||||
import { omit } from 'lodash';
|
|
||||||
|
|
||||||
import { Request } from 'apollo-server-env';
|
import { Request } from 'apollo-server-env';
|
||||||
import { runQuery, QueryOptions } from './runQuery';
|
|
||||||
import {
|
import {
|
||||||
default as GraphQLOptions,
|
default as GraphQLOptions,
|
||||||
resolveGraphqlOptions,
|
resolveGraphqlOptions,
|
||||||
|
@ -17,6 +14,7 @@ import {
|
||||||
PersistedQueryNotFoundError,
|
PersistedQueryNotFoundError,
|
||||||
} from 'apollo-server-errors';
|
} from 'apollo-server-errors';
|
||||||
import { calculateCacheControlHeaders } from './caching';
|
import { calculateCacheControlHeaders } from './caching';
|
||||||
|
import { GraphQLRequestProcessor } from 'apollo-server-core/src/requestProcessing';
|
||||||
|
|
||||||
export interface HttpQueryRequest {
|
export interface HttpQueryRequest {
|
||||||
method: string;
|
method: string;
|
||||||
|
@ -277,23 +275,6 @@ export async function runHttpQuery(
|
||||||
throw new HttpQueryError(400, 'GraphQL queries must be strings.');
|
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;
|
const operationName = requestParams.operationName;
|
||||||
|
|
||||||
let variables = requestParams.variables;
|
let variables = requestParams.variables;
|
||||||
|
@ -380,33 +361,46 @@ export async function runHttpQuery(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let params: QueryOptions = {
|
const requestProcessor = new GraphQLRequestProcessor({
|
||||||
schema: optionsObject.schema,
|
schema: optionsObject.schema,
|
||||||
queryString,
|
|
||||||
nonQueryError,
|
|
||||||
variables: variables,
|
|
||||||
context,
|
|
||||||
rootValue: optionsObject.rootValue,
|
rootValue: optionsObject.rootValue,
|
||||||
operationName: operationName,
|
context,
|
||||||
validationRules: optionsObject.validationRules,
|
validationRules: optionsObject.validationRules,
|
||||||
formatError: optionsObject.formatError,
|
|
||||||
formatResponse: optionsObject.formatResponse,
|
|
||||||
fieldResolver: optionsObject.fieldResolver,
|
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) {
|
} catch (e) {
|
||||||
// Populate any HttpQueryError to our handler which should
|
// Populate any HttpQueryError to our handler which should
|
||||||
// convert it to Http Error.
|
// convert it to Http Error.
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "../../tsconfig",
|
"extends": "../../tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist"
|
"outDir": "./dist",
|
||||||
|
"noUnusedLocals": false,
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["**/__tests__", "**/__mocks__"]
|
"exclude": ["**/__tests__", "**/__mocks__"]
|
||||||
|
|
|
@ -33,7 +33,7 @@ export interface GraphQLResponse {
|
||||||
|
|
||||||
export class GraphQLExtension<TContext = any> {
|
export class GraphQLExtension<TContext = any> {
|
||||||
public requestDidStart?(o: {
|
public requestDidStart?(o: {
|
||||||
request: Request;
|
request: Pick<Request, 'url' | 'method' | 'headers'>;
|
||||||
queryString?: string;
|
queryString?: string;
|
||||||
parsedQuery?: DocumentNode;
|
parsedQuery?: DocumentNode;
|
||||||
operationName?: string;
|
operationName?: string;
|
||||||
|
@ -71,7 +71,7 @@ export class GraphQLExtensionStack<TContext = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public requestDidStart(o: {
|
public requestDidStart(o: {
|
||||||
request: Request;
|
request: Pick<Request, 'url' | 'method' | 'headers'>;
|
||||||
queryString?: string;
|
queryString?: string;
|
||||||
parsedQuery?: DocumentNode;
|
parsedQuery?: DocumentNode;
|
||||||
operationName?: string;
|
operationName?: string;
|
||||||
|
@ -154,17 +154,27 @@ export class GraphQLExtensionStack<TContext = any> {
|
||||||
const endHandlers: EndHandler[] = [];
|
const endHandlers: EndHandler[] = [];
|
||||||
this.extensions.forEach(extension => {
|
this.extensions.forEach(extension => {
|
||||||
// Invoke the start handler, which may return an end handler.
|
// Invoke the start handler, which may return an end handler.
|
||||||
|
try {
|
||||||
const endHandler = startInvoker(extension);
|
const endHandler = startInvoker(extension);
|
||||||
if (endHandler) {
|
if (endHandler) {
|
||||||
endHandlers.push(endHandler);
|
endHandlers.push(endHandler);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return (...errors: Array<Error>) => {
|
return (...errors: Array<Error>) => {
|
||||||
// We run end handlers in reverse order of start handlers. That way, the
|
// We run end handlers in reverse order of start handlers. That way, the
|
||||||
// first handler in the stack "surrounds" the entire event's process
|
// first handler in the stack "surrounds" the entire event's process
|
||||||
// (helpful for tracing/reporting!)
|
// (helpful for tracing/reporting!)
|
||||||
endHandlers.reverse();
|
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