123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- import type { IncomingMessage, ServerResponse } from 'http';
- import type {
- ASTVisitor,
- DocumentNode,
- ValidationRule,
- ValidationContext,
- ExecutionArgs,
- ExecutionResult,
- FormattedExecutionResult,
- GraphQLError,
- GraphQLSchema,
- GraphQLFieldResolver,
- GraphQLTypeResolver,
- GraphQLFormattedError,
- } from 'graphql';
- import accepts from 'accepts';
- import httpError from 'http-errors';
- import {
- Source,
- parse,
- validate,
- execute,
- formatError,
- validateSchema,
- getOperationAST,
- specifiedRules,
- } from 'graphql';
- import type { GraphiQLOptions, GraphiQLData } from './renderGraphiQL';
- import { parseBody } from './parseBody';
- import { renderGraphiQL } from './renderGraphiQL';
- type $Request = IncomingMessage;
- type $Response = ServerResponse & {| json?: (data: mixed) => void |};
- type MaybePromise<T> = Promise<T> | T;
- export type Options =
- | ((
- request: $Request,
- response: $Response,
- params?: GraphQLParams,
- ) => MaybePromise<OptionsData>)
- | MaybePromise<OptionsData>;
- export type OptionsData = {|
-
- schema: GraphQLSchema,
-
- context?: mixed,
-
- rootValue?: mixed,
-
- pretty?: boolean,
-
- validationRules?: $ReadOnlyArray<(ValidationContext) => ASTVisitor>,
-
- customValidateFn?: (
- schema: GraphQLSchema,
- documentAST: DocumentNode,
- rules: $ReadOnlyArray<ValidationRule>,
- ) => $ReadOnlyArray<GraphQLError>,
-
- customExecuteFn?: (args: ExecutionArgs) => MaybePromise<ExecutionResult>,
-
- customFormatErrorFn?: (error: GraphQLError) => GraphQLFormattedError,
-
- customParseFn?: (source: Source) => DocumentNode,
-
- formatError?: (error: GraphQLError) => GraphQLFormattedError,
-
- extensions?: (
- info: RequestInfo,
- ) => MaybePromise<void | { [key: string]: mixed, ... }>,
-
- graphiql?: boolean | GraphiQLOptions,
-
- fieldResolver?: GraphQLFieldResolver<mixed, mixed>,
-
- typeResolver?: GraphQLTypeResolver<mixed, mixed>,
- |};
- export type RequestInfo = {|
-
- document: DocumentNode,
-
- variables: { +[name: string]: mixed, ... } | null,
-
- operationName: string | null,
-
- result: ExecutionResult,
-
- context?: mixed,
- |};
- type Middleware = (request: $Request, response: $Response) => Promise<void>;
- export function graphqlHTTP(options: Options): Middleware {
- if (!options) {
- throw new Error('GraphQL middleware requires options.');
- }
- return async function graphqlMiddleware(
- request: $Request,
- response: $Response,
- ): Promise<void> {
-
- let params: GraphQLParams;
- let showGraphiQL = false;
- let graphiqlOptions;
- let formatErrorFn = formatError;
- let pretty = false;
- let result: ExecutionResult;
- try {
-
- try {
- params = (await getGraphQLParams(request): GraphQLParams);
- } catch (error) {
-
-
- const optionsData = await resolveOptions();
- pretty = optionsData.pretty ?? false;
- formatErrorFn =
- optionsData.customFormatErrorFn ??
- optionsData.formatError ??
- formatErrorFn;
- throw error;
- }
-
- const optionsData: OptionsData = await resolveOptions(params);
-
- const schema = optionsData.schema;
- const rootValue = optionsData.rootValue;
- const validationRules = optionsData.validationRules ?? [];
- const fieldResolver = optionsData.fieldResolver;
- const typeResolver = optionsData.typeResolver;
- const graphiql = optionsData.graphiql ?? false;
- const extensionsFn = optionsData.extensions;
- const context = optionsData.context ?? request;
- const parseFn = optionsData.customParseFn ?? parse;
- const executeFn = optionsData.customExecuteFn ?? execute;
- const validateFn = optionsData.customValidateFn ?? validate;
- pretty = optionsData.pretty ?? false;
- formatErrorFn =
- optionsData.customFormatErrorFn ??
- optionsData.formatError ??
- formatErrorFn;
-
- if (schema == null) {
- throw httpError(
- 500,
- 'GraphQL middleware options must contain a schema.',
- );
- }
-
- if (request.method !== 'GET' && request.method !== 'POST') {
- throw httpError(405, 'GraphQL only supports GET and POST requests.', {
- headers: { Allow: 'GET, POST' },
- });
- }
-
- const { query, variables, operationName } = params;
- showGraphiQL = canDisplayGraphiQL(request, params) && graphiql !== false;
- if (typeof graphiql !== 'boolean') {
- graphiqlOptions = graphiql;
- }
-
-
- if (query == null) {
- if (showGraphiQL) {
- return respondWithGraphiQL(response, graphiqlOptions);
- }
- throw httpError(400, 'Must provide query string.');
- }
-
- const schemaValidationErrors = validateSchema(schema);
- if (schemaValidationErrors.length > 0) {
-
- throw httpError(500, 'GraphQL schema validation error.', {
- graphqlErrors: schemaValidationErrors,
- });
- }
-
- let documentAST;
- try {
- documentAST = parseFn(new Source(query, 'GraphQL request'));
- } catch (syntaxError) {
-
- throw httpError(400, 'GraphQL syntax error.', {
- graphqlErrors: [syntaxError],
- });
- }
-
- const validationErrors = validateFn(schema, documentAST, [
- ...specifiedRules,
- ...validationRules,
- ]);
- if (validationErrors.length > 0) {
-
- throw httpError(400, 'GraphQL validation error.', {
- graphqlErrors: validationErrors,
- });
- }
-
- if (request.method === 'GET') {
-
- const operationAST = getOperationAST(documentAST, operationName);
- if (operationAST && operationAST.operation !== 'query') {
-
-
-
- if (showGraphiQL) {
- return respondWithGraphiQL(response, graphiqlOptions, params);
- }
-
- throw httpError(
- 405,
- `Can only perform a ${operationAST.operation} operation from a POST request.`,
- { headers: { Allow: 'POST' } },
- );
- }
- }
-
- try {
- result = await executeFn({
- schema,
- document: documentAST,
- rootValue,
- contextValue: context,
- variableValues: variables,
- operationName,
- fieldResolver,
- typeResolver,
- });
- } catch (contextError) {
-
- throw httpError(400, 'GraphQL execution context error.', {
- graphqlErrors: [contextError],
- });
- }
-
-
- if (extensionsFn) {
- const extensions = await extensionsFn({
- document: documentAST,
- variables,
- operationName,
- result,
- context,
- });
- if (extensions != null) {
- result = { ...result, extensions };
- }
- }
- } catch (error) {
-
- response.statusCode = error.status ?? 500;
- if (error.headers != null) {
- for (const [key, value] of Object.entries(error.headers)) {
- (response: any).setHeader(key, value);
- }
- }
- result = { data: undefined, errors: error.graphqlErrors ?? [error] };
- }
-
-
-
-
-
- if (response.statusCode === 200 && result.data == null) {
- response.statusCode = 500;
- }
-
- const formattedResult: FormattedExecutionResult = {
- ...result,
- errors: result.errors?.map(formatErrorFn),
- };
-
- if (showGraphiQL) {
- return respondWithGraphiQL(
- response,
- graphiqlOptions,
- params,
- formattedResult,
- );
- }
-
-
-
- if (!pretty && typeof response.json === 'function') {
- response.json(formattedResult);
- } else {
- const payload = JSON.stringify(formattedResult, null, pretty ? 2 : 0);
- sendResponse(response, 'application/json', payload);
- }
- async function resolveOptions(
- requestParams?: GraphQLParams,
- ): Promise<OptionsData> {
- const optionsResult = await Promise.resolve(
- typeof options === 'function'
- ? options(request, response, requestParams)
- : options,
- );
-
- if (optionsResult == null || typeof optionsResult !== '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 (optionsResult.formatError) {
-
- console.warn(
- '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.',
- );
- }
- return optionsResult;
- }
- };
- }
- function respondWithGraphiQL(
- response: $Response,
- options?: GraphiQLOptions,
- params?: GraphQLParams,
- result?: FormattedExecutionResult,
- ): void {
- const data: GraphiQLData = {
- query: params?.query,
- variables: params?.variables,
- operationName: params?.operationName,
- result,
- };
- const payload = renderGraphiQL(data, options);
- return sendResponse(response, 'text/html', payload);
- }
- export type GraphQLParams = {|
- query: string | null,
- variables: { +[name: string]: mixed, ... } | null,
- operationName: string | null,
- raw: boolean,
- |};
- export async function getGraphQLParams(
- request: $Request,
- ): Promise<GraphQLParams> {
- const urlData = new URLSearchParams(request.url.split('?')[1]);
- const bodyData = await parseBody(request);
-
- let query = urlData.get('query') ?? bodyData.query;
- if (typeof query !== 'string') {
- query = null;
- }
-
- let variables = urlData.get('variables') ?? bodyData.variables;
- if (typeof variables === 'string') {
- try {
- variables = JSON.parse(variables);
- } catch (error) {
- throw httpError(400, 'Variables are invalid JSON.');
- }
- } else if (typeof variables !== 'object') {
- variables = null;
- }
-
- let operationName = urlData.get('operationName') || bodyData.operationName;
- if (typeof operationName !== 'string') {
- operationName = null;
- }
- const raw = urlData.get('raw') != null || bodyData.raw !== undefined;
- return { query, variables, operationName, raw };
- }
- function canDisplayGraphiQL(request: $Request, params: GraphQLParams): boolean {
-
-
- return !params.raw && accepts(request).types(['json', 'html']) === 'html';
- }
- 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);
- }
|