2016-06-10 20:48:21 -04:00
|
|
|
import {
|
2018-01-09 00:08:01 +01:00
|
|
|
GraphQLSchema,
|
|
|
|
GraphQLFieldResolver,
|
|
|
|
ExecutionResult,
|
|
|
|
DocumentNode,
|
|
|
|
parse,
|
|
|
|
validate,
|
|
|
|
execute,
|
2018-05-16 17:44:51 -07:00
|
|
|
ExecutionArgs,
|
2018-05-16 18:47:44 -07:00
|
|
|
getOperationAST,
|
2018-01-09 00:08:01 +01:00
|
|
|
GraphQLError,
|
|
|
|
specifiedRules,
|
|
|
|
ValidationContext,
|
2016-06-10 20:48:21 -04:00
|
|
|
} from 'graphql';
|
2016-06-10 17:05:39 -07:00
|
|
|
|
2018-01-09 00:08:01 +01:00
|
|
|
import {
|
|
|
|
enableGraphQLExtensions,
|
|
|
|
GraphQLExtension,
|
|
|
|
GraphQLExtensionStack,
|
|
|
|
} from 'graphql-extensions';
|
2017-10-23 19:01:02 -07:00
|
|
|
import { TracingExtension } from 'apollo-tracing';
|
2018-06-21 13:29:14 -07:00
|
|
|
import {
|
|
|
|
CacheControlExtension,
|
|
|
|
CacheControlExtensionOptions,
|
|
|
|
} from 'apollo-cache-control';
|
2017-08-09 16:57:17 +02:00
|
|
|
|
2018-04-23 15:09:36 -07:00
|
|
|
import {
|
|
|
|
fromGraphQLError,
|
2018-05-02 16:40:03 -07:00
|
|
|
formatApolloErrors,
|
2018-04-23 15:09:36 -07:00
|
|
|
ValidationError,
|
|
|
|
SyntaxError,
|
2018-06-20 14:04:22 +02:00
|
|
|
} from 'apollo-server-errors';
|
2018-04-17 21:19:44 -07:00
|
|
|
|
2017-08-09 16:57:17 +02:00
|
|
|
export interface GraphQLResponse {
|
|
|
|
data?: object;
|
|
|
|
errors?: Array<GraphQLError & object>;
|
|
|
|
extensions?: object;
|
2016-06-10 17:05:39 -07:00
|
|
|
}
|
|
|
|
|
2016-06-18 10:19:51 -07:00
|
|
|
export interface QueryOptions {
|
2018-01-09 00:08:01 +01:00
|
|
|
schema: GraphQLSchema;
|
2018-05-16 18:47:44 -07:00
|
|
|
// Specify exactly one of these. parsedQuery is primarily for use by
|
|
|
|
// OperationStore.
|
|
|
|
queryString?: string;
|
|
|
|
parsedQuery?: DocumentNode;
|
|
|
|
|
|
|
|
// If this is specified and the given GraphQL query is not a "query" (eg, it's
|
|
|
|
// a mutation), throw this error.
|
|
|
|
nonQueryError?: Error;
|
|
|
|
|
2018-01-09 00:08:01 +01:00
|
|
|
rootValue?: any;
|
|
|
|
context?: any;
|
|
|
|
variables?: { [key: string]: any };
|
|
|
|
operationName?: string;
|
|
|
|
validationRules?: Array<(context: ValidationContext) => any>;
|
|
|
|
fieldResolver?: GraphQLFieldResolver<any, any>;
|
|
|
|
// WARNING: these extra validation rules are only applied to queries
|
|
|
|
// submitted as string, not those submitted as Document!
|
|
|
|
|
|
|
|
formatError?: Function;
|
|
|
|
formatResponse?: Function;
|
|
|
|
debug?: boolean;
|
|
|
|
tracing?: boolean;
|
2018-06-21 13:29:14 -07:00
|
|
|
cacheControl?: boolean | CacheControlExtensionOptions;
|
2018-05-30 17:59:03 -07:00
|
|
|
request: Pick<Request, 'url' | 'method' | 'headers'>;
|
2018-05-16 17:44:51 -07:00
|
|
|
extensions?: Array<() => GraphQLExtension>;
|
2018-06-22 17:25:41 -07:00
|
|
|
persistedQueryHit?: boolean;
|
|
|
|
persistedQueryRegister?: boolean;
|
2016-06-18 10:19:51 -07:00
|
|
|
}
|
|
|
|
|
2018-05-16 18:47:44 -07:00
|
|
|
function isQueryOperation(query: DocumentNode, operationName: string) {
|
|
|
|
const operationAST = getOperationAST(query, operationName);
|
|
|
|
return operationAST.operation === 'query';
|
|
|
|
}
|
|
|
|
|
2018-03-12 20:25:12 +01:00
|
|
|
export function runQuery(options: QueryOptions): Promise<GraphQLResponse> {
|
2018-01-09 00:08:01 +01:00
|
|
|
// Fiber-aware Promises run their .then callbacks in Fibers.
|
|
|
|
return Promise.resolve().then(() => doRunQuery(options));
|
2016-08-14 03:19:31 -04:00
|
|
|
}
|
|
|
|
|
2017-08-09 16:57:17 +02:00
|
|
|
function doRunQuery(options: QueryOptions): Promise<GraphQLResponse> {
|
2018-05-16 18:47:44 -07:00
|
|
|
if (options.queryString && options.parsedQuery) {
|
|
|
|
throw new Error('Only supply one of queryString and parsedQuery');
|
|
|
|
}
|
|
|
|
if (!(options.queryString || options.parsedQuery)) {
|
|
|
|
throw new Error('Must supply one of queryString and parsedQuery');
|
|
|
|
}
|
|
|
|
|
2018-01-09 00:08:01 +01:00
|
|
|
const debugDefault =
|
|
|
|
process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test';
|
2018-03-12 20:25:12 +01:00
|
|
|
const debug = options.debug !== undefined ? options.debug : debugDefault;
|
2018-01-09 00:08:01 +01:00
|
|
|
|
|
|
|
const context = options.context || {};
|
2018-05-16 17:44:51 -07:00
|
|
|
|
2018-06-01 21:16:25 -07:00
|
|
|
// If custom extension factories were provided, create per-request extension
|
|
|
|
// objects.
|
2018-05-16 17:44:51 -07:00
|
|
|
const extensions = options.extensions ? options.extensions.map(f => f()) : [];
|
|
|
|
|
2018-06-13 12:28:36 -07:00
|
|
|
// If you're running behind an engineproxy, set these options to turn on
|
|
|
|
// tracing and cache-control extensions.
|
2018-01-09 00:08:01 +01:00
|
|
|
if (options.tracing) {
|
2018-05-16 17:44:51 -07:00
|
|
|
extensions.push(new TracingExtension());
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
2018-03-28 13:56:22 -07:00
|
|
|
if (options.cacheControl === true) {
|
2018-05-16 17:44:51 -07:00
|
|
|
extensions.push(new CacheControlExtension());
|
2018-03-28 13:56:22 -07:00
|
|
|
} else if (options.cacheControl) {
|
|
|
|
extensions.push(new CacheControlExtension(options.cacheControl));
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
const extensionStack = new GraphQLExtensionStack(extensions);
|
|
|
|
|
2018-06-01 21:16:25 -07:00
|
|
|
// 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.
|
2018-05-16 17:44:51 -07:00
|
|
|
if (extensions.length > 0) {
|
2018-01-09 00:08:01 +01:00
|
|
|
enableGraphQLExtensions(options.schema);
|
|
|
|
}
|
2018-06-01 21:16:25 -07:00
|
|
|
context._extensionStack = extensionStack;
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
const requestDidEnd = extensionStack.requestDidStart({
|
2018-05-30 17:59:03 -07:00
|
|
|
// Since the Request interfacess are not the same between node-fetch and
|
|
|
|
// typescript's lib dom, we should limit the fields that need to be passed
|
|
|
|
// into requestDidStart to only the ones we need, currently just the
|
|
|
|
// headers, method, and url
|
|
|
|
request: options.request as any,
|
2018-06-01 21:16:25 -07:00
|
|
|
queryString: options.queryString,
|
|
|
|
parsedQuery: options.parsedQuery,
|
|
|
|
operationName: options.operationName,
|
|
|
|
variables: options.variables,
|
2018-06-22 17:25:41 -07:00
|
|
|
persistedQueryHit: options.persistedQueryHit,
|
|
|
|
persistedQueryRegister: options.persistedQueryRegister,
|
2018-01-09 00:08:01 +01:00
|
|
|
});
|
2018-05-16 17:44:51 -07:00
|
|
|
return Promise.resolve()
|
|
|
|
.then(() => {
|
|
|
|
// Parse the document.
|
|
|
|
let documentAST: DocumentNode;
|
|
|
|
if (options.parsedQuery) {
|
|
|
|
documentAST = options.parsedQuery;
|
|
|
|
} else if (!options.queryString) {
|
|
|
|
throw new Error('Must supply one of queryString and parsedQuery');
|
|
|
|
} else {
|
|
|
|
const parsingDidEnd = extensionStack.parsingDidStart({
|
|
|
|
queryString: options.queryString,
|
|
|
|
});
|
|
|
|
let graphqlParseErrors;
|
|
|
|
try {
|
|
|
|
documentAST = parse(options.queryString);
|
|
|
|
} catch (syntaxError) {
|
|
|
|
graphqlParseErrors = formatApolloErrors(
|
|
|
|
[
|
|
|
|
fromGraphQLError(syntaxError, {
|
|
|
|
errorClass: SyntaxError,
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
{
|
|
|
|
formatter: options.formatError,
|
|
|
|
debug,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
parsingDidEnd(...(graphqlParseErrors || []));
|
|
|
|
if (graphqlParseErrors) {
|
|
|
|
return Promise.resolve({ errors: graphqlParseErrors });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-17 21:19:44 -07:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
if (
|
|
|
|
options.nonQueryError &&
|
|
|
|
!isQueryOperation(documentAST, options.operationName)
|
|
|
|
) {
|
|
|
|
// XXX this goes to requestDidEnd, is that correct or should it be
|
|
|
|
// validation?
|
|
|
|
throw options.nonQueryError;
|
|
|
|
}
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
let rules = specifiedRules;
|
|
|
|
if (options.validationRules) {
|
|
|
|
rules = rules.concat(options.validationRules);
|
|
|
|
}
|
|
|
|
const validationDidEnd = extensionStack.validationDidStart();
|
|
|
|
let validationErrors;
|
|
|
|
try {
|
|
|
|
validationErrors = validate(options.schema, documentAST, rules);
|
|
|
|
} catch (validationThrewError) {
|
|
|
|
// Catch errors thrown by validate, not just those returned by it.
|
|
|
|
validationErrors = [validationThrewError];
|
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
if (validationErrors) {
|
|
|
|
validationErrors = formatApolloErrors(
|
|
|
|
validationErrors.map(err =>
|
|
|
|
fromGraphQLError(err, { errorClass: ValidationError }),
|
|
|
|
),
|
|
|
|
{
|
|
|
|
formatter: options.formatError,
|
|
|
|
debug,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
validationDidEnd(...(validationErrors || []));
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
if (validationErrors && validationErrors.length) {
|
|
|
|
return Promise.resolve({
|
|
|
|
errors: validationErrors,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
const executionArgs: ExecutionArgs = {
|
|
|
|
schema: options.schema,
|
|
|
|
document: documentAST,
|
|
|
|
rootValue: options.rootValue,
|
|
|
|
contextValue: context,
|
|
|
|
variableValues: options.variables,
|
|
|
|
operationName: options.operationName,
|
|
|
|
fieldResolver: options.fieldResolver,
|
2018-01-09 00:08:01 +01:00
|
|
|
};
|
2018-05-16 17:44:51 -07:00
|
|
|
const executionDidEnd = extensionStack.executionDidStart({
|
|
|
|
executionArgs,
|
|
|
|
});
|
|
|
|
return Promise.resolve()
|
|
|
|
.then(() => execute(executionArgs))
|
|
|
|
.catch(executionError => {
|
|
|
|
return {
|
|
|
|
// These errors will get passed through formatApolloErrors in the
|
|
|
|
// `then` below.
|
|
|
|
// TODO accurate code for this error, which describes this error, which
|
|
|
|
// can occur when:
|
|
|
|
// * variables incorrectly typed/null when nonnullable
|
|
|
|
// * unknown operation/operation name invalid
|
|
|
|
// * operation type is unsupported
|
|
|
|
// Options: PREPROCESSING_FAILED, GRAPHQL_RUNTIME_CHECK_FAILED
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
errors: [fromGraphQLError(executionError)],
|
|
|
|
} as ExecutionResult;
|
|
|
|
})
|
|
|
|
.then(result => {
|
|
|
|
let response: GraphQLResponse = {
|
|
|
|
data: result.data,
|
|
|
|
};
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
if (result.errors) {
|
|
|
|
response.errors = formatApolloErrors([...result.errors], {
|
|
|
|
formatter: options.formatError,
|
|
|
|
debug,
|
|
|
|
});
|
|
|
|
}
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-06-11 13:06:31 +02:00
|
|
|
executionDidEnd(...(result.errors || []));
|
2018-05-16 17:44:51 -07:00
|
|
|
|
|
|
|
const formattedExtensions = extensionStack.format();
|
|
|
|
if (Object.keys(formattedExtensions).length > 0) {
|
|
|
|
response.extensions = formattedExtensions;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.formatResponse) {
|
|
|
|
response = options.formatResponse(response, options);
|
|
|
|
}
|
2018-01-09 00:08:01 +01:00
|
|
|
|
2018-05-16 17:44:51 -07:00
|
|
|
return response;
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
// Handle the case of an internal server failure (or nonQueryError) ---
|
|
|
|
// we're not returning a GraphQL response so we don't call
|
|
|
|
// willSendResponse.
|
|
|
|
requestDidEnd(err);
|
|
|
|
throw err;
|
|
|
|
})
|
|
|
|
.then(graphqlResponse => {
|
|
|
|
extensionStack.willSendResponse({ graphqlResponse });
|
|
|
|
requestDidEnd();
|
|
|
|
return graphqlResponse;
|
2018-01-09 00:08:01 +01:00
|
|
|
});
|
2016-06-10 17:05:39 -07:00
|
|
|
}
|