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,
|
|
|
|
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-04-23 16:24:08 -07:00
|
|
|
internalFormatError,
|
2018-04-23 15:09:36 -07:00
|
|
|
ValidationError,
|
|
|
|
SyntaxError,
|
|
|
|
} from './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-09-10 17:28:38 -05:00
|
|
|
export enum LogAction {
|
2018-01-09 00:08:01 +01:00
|
|
|
request,
|
|
|
|
parse,
|
|
|
|
validation,
|
|
|
|
execute,
|
2016-09-10 17:28:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum LogStep {
|
2018-01-09 00:08:01 +01:00
|
|
|
start,
|
|
|
|
end,
|
|
|
|
status,
|
2016-09-10 17:28:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface LogMessage {
|
|
|
|
action: LogAction;
|
|
|
|
step: LogStep;
|
|
|
|
key?: string;
|
2018-05-01 14:20:21 -07:00
|
|
|
data?: any;
|
2016-09-10 17:28:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface LogFunction {
|
|
|
|
(message: LogMessage);
|
|
|
|
}
|
|
|
|
|
2016-06-18 10:19:51 -07:00
|
|
|
export interface QueryOptions {
|
2018-01-09 00:08:01 +01:00
|
|
|
schema: GraphQLSchema;
|
|
|
|
query: string | DocumentNode;
|
|
|
|
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;
|
2016-06-18 10:19:51 -07:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-04-17 21:19:44 -07:00
|
|
|
function format(
|
|
|
|
errors: Array<Error>,
|
|
|
|
options?: {
|
|
|
|
formatter?: Function;
|
|
|
|
debug?: boolean;
|
|
|
|
},
|
|
|
|
): Array<Error> {
|
|
|
|
const { formatter, debug } = options;
|
2018-04-23 16:24:08 -07:00
|
|
|
return errors.map(error => internalFormatError(error, debug)).map(error => {
|
2018-03-12 20:25:12 +01:00
|
|
|
if (formatter !== undefined) {
|
|
|
|
try {
|
|
|
|
return formatter(error);
|
|
|
|
} catch (err) {
|
|
|
|
console.error('Error in formatError function:', err);
|
2018-04-17 21:19:44 -07:00
|
|
|
const newError: GraphQLError = fromGraphQLError(
|
|
|
|
new GraphQLError('Internal server error'),
|
|
|
|
);
|
2018-04-23 16:24:08 -07:00
|
|
|
return internalFormatError(newError, debug);
|
2018-03-12 20:25:12 +01:00
|
|
|
}
|
|
|
|
} else {
|
2018-04-17 21:19:44 -07:00
|
|
|
return error;
|
2018-03-12 20:25:12 +01:00
|
|
|
}
|
|
|
|
}) as Array<Error>;
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
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 || {};
|
|
|
|
let extensions = [];
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
const qry =
|
|
|
|
typeof options.query === 'string' ? options.query : print(options.query);
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'query',
|
|
|
|
data: qry,
|
|
|
|
});
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'variables',
|
|
|
|
data: options.variables,
|
|
|
|
});
|
|
|
|
logFunction({
|
|
|
|
action: LogAction.request,
|
|
|
|
step: LogStep.status,
|
|
|
|
key: 'operationName',
|
|
|
|
data: options.operationName,
|
|
|
|
});
|
|
|
|
|
|
|
|
// if query is already an AST, don't parse or validate
|
|
|
|
// XXX: This refers the operations-store flow.
|
|
|
|
if (typeof options.query === 'string') {
|
2016-06-24 16:57:52 -04:00
|
|
|
try {
|
2018-01-09 00:08:01 +01:00
|
|
|
logFunction({ action: LogAction.parse, step: LogStep.start });
|
|
|
|
documentAST = parse(options.query as string);
|
|
|
|
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-04-23 15:09:36 -07:00
|
|
|
errors: format(
|
|
|
|
[
|
|
|
|
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 {
|
|
|
|
documentAST = options.query as DocumentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
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-04-17 21:19:44 -07:00
|
|
|
errors: format(
|
|
|
|
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,
|
|
|
|
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-04-17 21:19:44 -07:00
|
|
|
response.errors = format(result.errors, {
|
|
|
|
formatter: options.formatError,
|
|
|
|
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
|
|
|
|
errors: format([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
|
|
|
}
|