123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- // @flow strict
- import { type IncomingMessage, type ServerResponse } from 'http';
- import url from 'url';
- import accepts from 'accepts';
- import httpError from 'http-errors';
- import {
- Source,
- parse,
- validate,
- execute,
- formatError,
- validateSchema,
- getOperationAST,
- specifiedRules,
- type ASTVisitor,
- type DocumentNode,
- type ValidationRule,
- type ValidationContext,
- type ExecutionArgs,
- type ExecutionResult,
- type GraphQLError,
- type GraphQLSchema,
- type GraphQLFieldResolver,
- type GraphQLTypeResolver,
- } from 'graphql';
- import { parseBody } from './parseBody';
- import { renderGraphiQL, type GraphiQLOptions } from './renderGraphiQL';
- type $Request = IncomingMessage;
- type $Response = ServerResponse & {| json?: ?(data: mixed) => void |};
- /**
- * Used to configure the graphqlHTTP middleware by providing a schema
- * and other configuration options.
- *
- * Options can be provided as an Object, a Promise for an Object, or a Function
- * that returns an Object or a Promise for an Object.
- */
- export type Options =
- | ((
- request: $Request,
- response: $Response,
- params?: GraphQLParams,
- ) => OptionsResult)
- | OptionsResult;
- export type OptionsResult = OptionsData | Promise<OptionsData>;
- export type OptionsData = {|
- /**
- * A GraphQL schema from graphql-js.
- */
- schema: GraphQLSchema,
- /**
- * A value to pass as the context to this middleware.
- */
- context?: ?mixed,
- /**
- * An object to pass as the rootValue to the graphql() function.
- */
- rootValue?: ?mixed,
- /**
- * A boolean to configure whether the output should be pretty-printed.
- */
- pretty?: ?boolean,
- /**
- * An optional array of validation rules that will be applied on the document
- * in additional to those defined by the GraphQL spec.
- */
- validationRules?: ?$ReadOnlyArray<(ValidationContext) => ASTVisitor>,
- /**
- * An optional function which will be used to validate instead of default `validate`
- * from `graphql-js`.
- */
- customValidateFn?: ?(
- schema: GraphQLSchema,
- documentAST: DocumentNode,
- rules: $ReadOnlyArray<ValidationRule>,
- ) => $ReadOnlyArray<GraphQLError>,
- /**
- * An optional function which will be used to execute instead of default `execute`
- * from `graphql-js`.
- */
- customExecuteFn?: ?(args: ExecutionArgs) => Promise<ExecutionResult>,
- /**
- * An optional function which will be used to format any errors produced by
- * fulfilling a GraphQL operation. If no function is provided, GraphQL's
- * default spec-compliant `formatError` function will be used.
- */
- customFormatErrorFn?: ?(error: GraphQLError) => mixed,
- /**
- * An optional function which will be used to create a document instead of
- * the default `parse` from `graphql-js`.
- */
- customParseFn?: ?(source: Source) => DocumentNode,
- /**
- * `formatError` is deprecated and replaced by `customFormatErrorFn`. It will
- * be removed in version 1.0.0.
- */
- formatError?: ?(error: GraphQLError) => mixed,
- /**
- * An optional function for adding additional metadata to the GraphQL response
- * as a key-value object. The result will be added to "extensions" field in
- * the resulting JSON. This is often a useful place to add development time
- * info such as the runtime of a query or the amount of resources consumed.
- *
- * Information about the request is provided to be used.
- *
- * This function may be async.
- */
- extensions?: ?(info: RequestInfo) => { [key: string]: mixed, ... },
- /**
- * A boolean to optionally enable GraphiQL mode.
- * Alternatively, instead of `true` you can pass in an options object.
- */
- graphiql?: ?boolean | ?GraphiQLOptions,
- /**
- * A resolver function to use when one is not provided by the schema.
- * If not provided, the default field resolver is used (which looks for a
- * value or method on the source value with the field's name).
- */
- fieldResolver?: ?GraphQLFieldResolver<mixed, mixed>,
- /**
- * A type resolver function to use when none is provided by the schema.
- * If not provided, the default type resolver is used (which looks for a
- * `__typename` field or alternatively calls the `isTypeOf` method).
- */
- typeResolver?: ?GraphQLTypeResolver<mixed, mixed>,
- |};
- /**
- * All information about a GraphQL request.
- */
- export type RequestInfo = {|
- /**
- * The parsed GraphQL document.
- */
- document: ?DocumentNode,
- /**
- * The variable values used at runtime.
- */
- variables: ?{ +[name: string]: mixed, ... },
- /**
- * The (optional) operation name requested.
- */
- operationName: ?string,
- /**
- * The result of executing the operation.
- */
- result: ?mixed,
- /**
- * A value to pass as the context to the graphql() function.
- */
- context?: ?mixed,
- |};
- type Middleware = (request: $Request, response: $Response) => Promise<void>;
- /**
- * Middleware for express; takes an options object or function as input to
- * configure behavior, and returns an express middleware.
- */
- module.exports = graphqlHTTP;
- function graphqlHTTP(options: Options): Middleware {
- if (!options) {
- throw new Error('GraphQL middleware requires options.');
- }
- return function graphqlMiddleware(request: $Request, response: $Response) {
- // Higher scoped variables are referred to at various stages in the
- // asynchronous state machine below.
- let context;
- let params;
- let pretty;
- let formatErrorFn = formatError;
- let validateFn = validate;
- let executeFn = execute;
- let parseFn = parse;
- let extensionsFn;
- let showGraphiQL = false;
- let query;
- let documentAST;
- let variables;
- let operationName;
- // Promises are used as a mechanism for capturing any thrown errors during
- // the asynchronous process below.
- // Parse the Request to get GraphQL request parameters.
- return getGraphQLParams(request)
- .then(
- graphQLParams => {
- params = graphQLParams;
- // Then, resolve the Options to get OptionsData.
- return resolveOptions(params);
- },
- error => {
- // When we failed to parse the GraphQL parameters, we still need to get
- // the options object, so make an options call to resolve just that.
- const dummyParams = {
- query: null,
- variables: null,
- operationName: null,
- raw: null,
- };
- return resolveOptions(dummyParams).then(() => Promise.reject(error));
- },
- )
- .then(optionsData => {
- // Assert that schema is required.
- if (!optionsData.schema) {
- throw new Error('GraphQL middleware options must contain a schema.');
- }
- // Collect information from the options data object.
- const schema = optionsData.schema;
- const rootValue = optionsData.rootValue;
- const fieldResolver = optionsData.fieldResolver;
- const typeResolver = optionsData.typeResolver;
- const validationRules = optionsData.validationRules || [];
- const graphiql = optionsData.graphiql;
- context = optionsData.context || request;
- // GraphQL HTTP only supports GET and POST methods.
- if (request.method !== 'GET' && request.method !== 'POST') {
- response.setHeader('Allow', 'GET, POST');
- throw httpError(405, 'GraphQL only supports GET and POST requests.');
- }
- // Get GraphQL params from the request and POST body data.
- query = params.query;
- variables = params.variables;
- operationName = params.operationName;
- showGraphiQL = canDisplayGraphiQL(request, params) && graphiql;
- // If there is no query, but GraphiQL will be displayed, do not produce
- // a result, otherwise return a 400: Bad Request.
- if (!query) {
- if (showGraphiQL) {
- return null;
- }
- throw httpError(400, 'Must provide query string.');
- }
- // Validate Schema
- const schemaValidationErrors = validateSchema(schema);
- if (schemaValidationErrors.length > 0) {
- // Return 500: Internal Server Error if invalid schema.
- response.statusCode = 500;
- return { errors: schemaValidationErrors };
- }
- // GraphQL source.
- const source = new Source(query, 'GraphQL request');
- // Parse source to AST, reporting any syntax error.
- try {
- documentAST = parseFn(source);
- } catch (syntaxError) {
- // Return 400: Bad Request if any syntax errors errors exist.
- response.statusCode = 400;
- return { errors: [syntaxError] };
- }
- // Validate AST, reporting any errors.
- const validationErrors = validateFn(schema, documentAST, [
- ...specifiedRules,
- ...validationRules,
- ]);
- if (validationErrors.length > 0) {
- // Return 400: Bad Request if any validation errors exist.
- response.statusCode = 400;
- return { errors: validationErrors };
- }
- // Only query operations are allowed on GET requests.
- if (request.method === 'GET') {
- // Determine if this GET request will perform a non-query.
- const operationAST = getOperationAST(documentAST, operationName);
- if (operationAST && operationAST.operation !== 'query') {
- // If GraphiQL can be shown, do not perform this query, but
- // provide it to GraphiQL so that the requester may perform it
- // themselves if desired.
- if (showGraphiQL) {
- return null;
- }
- // Otherwise, report a 405: Method Not Allowed error.
- response.setHeader('Allow', 'POST');
- throw httpError(
- 405,
- `Can only perform a ${operationAST.operation} operation from a POST request.`,
- );
- }
- }
- // Perform the execution, reporting any errors creating the context.
- try {
- return executeFn({
- schema,
- document: documentAST,
- rootValue,
- contextValue: context,
- variableValues: variables,
- operationName,
- fieldResolver,
- typeResolver,
- });
- } catch (contextError) {
- // Return 400: Bad Request if any execution context errors exist.
- response.statusCode = 400;
- return { errors: [contextError] };
- }
- })
- .then(result => {
- // Collect and apply any metadata extensions if a function was provided.
- // https://graphql.github.io/graphql-spec/#sec-Response-Format
- if (result && extensionsFn) {
- return Promise.resolve(
- extensionsFn({
- document: documentAST,
- variables,
- operationName,
- result,
- context,
- }),
- ).then(extensions => {
- if (extensions && typeof extensions === 'object') {
- (result: any).extensions = extensions;
- }
- return result;
- });
- }
- return result;
- })
- .catch(error => {
- // If an error was caught, report the httpError status, or 500.
- response.statusCode = error.status || 500;
- return { errors: [error] };
- })
- .then(result => {
- // If no data was included in the result, that indicates a runtime query
- // error, indicate as such with a generic status code.
- // Note: Information about the error itself will still be contained in
- // the resulting JSON payload.
- // https://graphql.github.io/graphql-spec/#sec-Data
- if (response.statusCode === 200 && result && !result.data) {
- response.statusCode = 500;
- }
- // Format any encountered errors.
- if (result && result.errors) {
- (result: any).errors = result.errors.map(formatErrorFn);
- }
- // If allowed to show GraphiQL, present it instead of JSON.
- if (showGraphiQL) {
- const payload = renderGraphiQL({
- query,
- variables,
- operationName,
- result,
- options: typeof showGraphiQL !== 'boolean' ? showGraphiQL : {},
- });
- return sendResponse(response, 'text/html', payload);
- }
- // At this point, result is guaranteed to exist, as the only scenario
- // where it will not is when showGraphiQL is true.
- if (!result) {
- throw httpError(500, 'Internal Error');
- }
- // If "pretty" JSON isn't requested, and the server provides a
- // response.json method (express), use that directly.
- // Otherwise use the simplified sendResponse method.
- if (!pretty && typeof response.json === 'function') {
- response.json(result);
- } else {
- const payload = JSON.stringify(result, null, pretty ? 2 : 0);
- sendResponse(response, 'application/json', payload);
- }
- });
- async function resolveOptions(requestParams) {
- const optionsResult =
- typeof options === 'function'
- ? options(request, response, requestParams)
- : options;
- const optionsData = await optionsResult;
- // Assert that optionsData is in fact an Object.
- if (!optionsData || typeof optionsData !== 'object') {
- throw new Error(
- 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.',
- );
- }
- if (optionsData.formatError) {
- // eslint-disable-next-line no-console
- console.warn(
- '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.',
- );
- }
- validateFn = optionsData.customValidateFn || validateFn;
- executeFn = optionsData.customExecuteFn || executeFn;
- parseFn = optionsData.customParseFn || parseFn;
- formatErrorFn =
- optionsData.customFormatErrorFn ||
- optionsData.formatError ||
- formatErrorFn;
- extensionsFn = optionsData.extensions;
- pretty = optionsData.pretty;
- return optionsData;
- }
- };
- }
- export type GraphQLParams = {|
- query: ?string,
- variables: ?{ +[name: string]: mixed, ... },
- operationName: ?string,
- raw: ?boolean,
- |};
- /**
- * Provided a "Request" provided by express or connect (typically a node style
- * HTTPClientRequest), Promise the GraphQL request parameters.
- */
- module.exports.getGraphQLParams = getGraphQLParams;
- async function getGraphQLParams(request: $Request): Promise<GraphQLParams> {
- const bodyData = await parseBody(request);
- const urlData = (request.url && url.parse(request.url, true).query) || {};
- return parseGraphQLParams(urlData, bodyData);
- }
- /**
- * Helper function to get the GraphQL params from the request.
- */
- function parseGraphQLParams(
- urlData: { [param: string]: mixed, ... },
- bodyData: { [param: string]: mixed, ... },
- ): GraphQLParams {
- // GraphQL Query string.
- let query = urlData.query || bodyData.query;
- if (typeof query !== 'string') {
- query = null;
- }
- // Parse the variables if needed.
- let variables = urlData.variables || bodyData.variables;
- if (variables && typeof variables === 'string') {
- try {
- variables = JSON.parse(variables);
- } catch (error) {
- throw httpError(400, 'Variables are invalid JSON.');
- }
- } else if (typeof variables !== 'object') {
- variables = null;
- }
- // Name of GraphQL operation to execute.
- let operationName = urlData.operationName || bodyData.operationName;
- if (typeof operationName !== 'string') {
- operationName = null;
- }
- const raw = urlData.raw !== undefined || bodyData.raw !== undefined;
- return { query, variables, operationName, raw };
- }
- /**
- * Helper function to determine if GraphiQL can be displayed.
- */
- function canDisplayGraphiQL(request: $Request, params: GraphQLParams): boolean {
- // If `raw` exists, GraphiQL mode is not enabled.
- // Allowed to show GraphiQL if not requested as raw and this request
- // prefers HTML over JSON.
- return !params.raw && accepts(request).types(['json', 'html']) === 'html';
- }
- /**
- * Helper function for sending a response using only the core Node server APIs.
- */
- function sendResponse(response: $Response, type: string, data: string): void {
- const chunk = Buffer.from(data, 'utf8');
- response.setHeader('Content-Type', type + '; charset=utf-8');
- response.setHeader('Content-Length', String(chunk.length));
- response.end(chunk);
- }
|