2016-06-10 20:48:21 -04:00
|
|
|
import {
|
2018-01-09 00:08:01 +01:00
|
|
|
GraphQLSchema,
|
|
|
|
GraphQLFieldResolver,
|
|
|
|
ExecutionResult,
|
|
|
|
DocumentNode,
|
|
|
|
parse,
|
|
|
|
print,
|
|
|
|
validate,
|
|
|
|
execute,
|
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-05-01 06:09:48 -07:00
|
|
|
import { CacheControlExtension } 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,
|
|
|
|
} from './errors';
|
2018-04-17 21:19:44 -07:00
|
|
|
|
2018-05-02 16:27:33 -07:00
|
|
|
import { LogStep, LogAction, LogMessage, LogFunction } from './logging';
|
|
|
|
|
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;
|
|
|
|
logFunction?: LogFunction;
|
|
|
|
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-05-01 06:09:48 -07:00
|
|
|
// cacheControl?: boolean | CacheControlExtensionOptions;
|
|
|
|
cacheControl?: boolean | any;
|
2018-05-29 21:37:38 -07:00
|
|
|
request: Request;
|
2018-04-02 22:49:43 -03:00
|
|
|
extensions?: Array<typeof GraphQLExtension | GraphQLExtension>;
|
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-01-09 00:08:01 +01:00
|
|
|
let documentAST: DocumentNode;
|
|
|
|
|
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 logFunction =
|
|
|
|
options.logFunction ||
|
|
|
|
function() {
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
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
|
|
|
|
|
|
|
logFunction({ action: LogAction.request, step: LogStep.start });
|
|
|
|
|
|
|
|
const context = options.context || {};
|
2018-04-02 22:49:43 -03:00
|
|
|
let extensions = options.extensions !== undefined ? options.extensions : [];
|
2018-01-09 00:08:01 +01:00
|
|
|
if (options.tracing) {
|
|
|
|
extensions.push(TracingExtension);
|
|
|
|
}
|
2018-03-28 13:56:22 -07:00
|
|
|
if (options.cacheControl === true) {
|
2018-01-09 00:08:01 +01:00
|
|
|
extensions.push(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
|
|
|
}
|
|
|
|
const extensionStack =
|
|
|
|
extensions.length > 0 && new GraphQLExtensionStack(extensions);
|
|
|
|
|
|
|
|
if (extensionStack) {
|
|
|
|
context._extensionStack = extensionStack;
|
|
|
|
enableGraphQLExtensions(options.schema);
|
|
|
|
|
|
|
|
extensionStack.requestDidStart();
|
|
|
|
}
|
|
|
|
|
2018-05-16 18:47:44 -07:00
|
|
|
const loggedQuery = options.queryString || print(options.parsedQuery);
|
2018-01-09 00:08:01 +01:00
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'query',
|
2018-05-16 18:47:44 -07:00
|
|
|
data: loggedQuery,
|
2018-01-09 00:08:01 +01:00
|
|
|
});
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'variables',
|
|
|
|
data: options.variables,
|
|
|
|
});
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'operationName',
|
|
|
|
data: options.operationName,
|
|
|
|
});
|
|
|
|
|
2018-05-16 18:47:44 -07:00
|
|
|
// Parse and validate the query, unless it is already an AST (eg, if using
|
|
|
|
// OperationStore with formatParams).
|
|
|
|
if (options.queryString) {
|
2016-06-24 16:57:52 -04:00
|
|
|
try {
|
2018-01-09 00:08:01 +01:00
|
|
|
logFunction({ action: LogAction.parse, step: LogStep.start });
|
2018-05-16 18:47:44 -07:00
|
|
|
documentAST = parse(options.queryString);
|
2018-01-09 00:08:01 +01:00
|
|
|
logFunction({ action: LogAction.parse, step: LogStep.end });
|
|
|
|
} catch (syntaxError) {
|
|
|
|
logFunction({ action: LogAction.parse, step: LogStep.end });
|
2018-03-12 20:25:12 +01:00
|
|
|
return Promise.resolve({
|
2018-05-02 16:40:03 -07:00
|
|
|
errors: formatApolloErrors(
|
2018-04-23 15:09:36 -07:00
|
|
|
[
|
|
|
|
fromGraphQLError(syntaxError, {
|
|
|
|
errorClass: SyntaxError,
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
{
|
|
|
|
formatter: options.formatError,
|
|
|
|
debug,
|
|
|
|
},
|
|
|
|
),
|
2018-03-12 20:25:12 +01:00
|
|
|
});
|
2016-06-24 16:57:52 -04:00
|
|
|
}
|
2018-01-09 00:08:01 +01:00
|
|
|
} else {
|
2018-05-16 18:47:44 -07:00
|
|
|
documentAST = options.parsedQuery;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
options.nonQueryError &&
|
|
|
|
!isQueryOperation(documentAST, options.operationName)
|
|
|
|
) {
|
|
|
|
throw options.nonQueryError;
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let rules = specifiedRules;
|
|
|
|
if (options.validationRules) {
|
|
|
|
rules = rules.concat(options.validationRules);
|
|
|
|
}
|
|
|
|
logFunction({ action: LogAction.validation, step: LogStep.start });
|
|
|
|
const validationErrors = validate(options.schema, documentAST, rules);
|
|
|
|
logFunction({ action: LogAction.validation, step: LogStep.end });
|
2018-04-17 21:19:44 -07:00
|
|
|
|
2018-01-09 00:08:01 +01:00
|
|
|
if (validationErrors.length) {
|
2018-03-12 20:25:12 +01:00
|
|
|
return Promise.resolve({
|
2018-05-02 16:40:03 -07:00
|
|
|
errors: formatApolloErrors(
|
2018-04-17 21:19:44 -07:00
|
|
|
validationErrors.map(err =>
|
2018-04-23 15:09:36 -07:00
|
|
|
fromGraphQLError(err, { errorClass: ValidationError }),
|
2018-04-17 21:19:44 -07:00
|
|
|
),
|
|
|
|
{
|
|
|
|
formatter: options.formatError,
|
2018-05-02 16:40:03 -07:00
|
|
|
logFunction,
|
2018-04-17 21:19:44 -07:00
|
|
|
debug,
|
|
|
|
},
|
|
|
|
),
|
2018-03-12 20:25:12 +01:00
|
|
|
});
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (extensionStack) {
|
|
|
|
extensionStack.executionDidStart();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
logFunction({ action: LogAction.execute, step: LogStep.start });
|
|
|
|
return Promise.resolve(
|
|
|
|
execute(
|
|
|
|
options.schema,
|
|
|
|
documentAST,
|
|
|
|
options.rootValue,
|
|
|
|
context,
|
|
|
|
options.variables,
|
|
|
|
options.operationName,
|
|
|
|
options.fieldResolver,
|
|
|
|
),
|
|
|
|
).then(result => {
|
|
|
|
logFunction({ action: LogAction.execute, step: LogStep.end });
|
|
|
|
|
|
|
|
let response: GraphQLResponse = {
|
|
|
|
data: result.data,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (result.errors) {
|
2018-05-21 15:41:36 -07:00
|
|
|
response.errors = formatApolloErrors([...result.errors], {
|
2018-04-17 21:19:44 -07:00
|
|
|
formatter: options.formatError,
|
2018-05-02 16:40:03 -07:00
|
|
|
logFunction,
|
2018-04-17 21:19:44 -07:00
|
|
|
debug,
|
|
|
|
});
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (extensionStack) {
|
|
|
|
extensionStack.executionDidEnd();
|
|
|
|
extensionStack.requestDidEnd();
|
|
|
|
response.extensions = extensionStack.format();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.formatResponse) {
|
|
|
|
response = options.formatResponse(response, options);
|
|
|
|
}
|
|
|
|
|
2018-05-01 14:20:21 -07:00
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.end,
|
|
|
|
key: 'response',
|
|
|
|
data: response,
|
|
|
|
});
|
|
|
|
|
2018-01-09 00:08:01 +01:00
|
|
|
return response;
|
|
|
|
});
|
|
|
|
} catch (executionError) {
|
|
|
|
logFunction({ action: LogAction.execute, step: LogStep.end });
|
|
|
|
logFunction({ action: LogAction.request, step: LogStep.end });
|
2018-03-12 20:25:12 +01:00
|
|
|
return Promise.resolve({
|
2018-04-23 15:09:36 -07:00
|
|
|
//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-05-02 16:40:03 -07:00
|
|
|
errors: formatApolloErrors([fromGraphQLError(executionError)], {
|
2018-04-17 21:19:44 -07:00
|
|
|
formatter: options.formatError,
|
|
|
|
debug,
|
|
|
|
}),
|
2018-03-12 20:25:12 +01:00
|
|
|
});
|
2018-01-09 00:08:01 +01:00
|
|
|
}
|
2016-06-10 17:05:39 -07:00
|
|
|
}
|