graphql-yoga
Version: 
<div align="center"><img src="https://raw.githubusercontent.com/graphql-hive/graphql-yoga/refs/heads/main/website/public/cover.png" width="720" /></div>
409 lines (408 loc) • 18.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YogaServer = void 0;
exports.createYoga = createYoga;
const tslib_1 = require("tslib");
/* eslint-disable @typescript-eslint/no-explicit-any */
const graphql_1 = require("graphql");
const core_1 = require("@envelop/core");
const instrumentation_1 = require("@envelop/instrumentation");
const executor_1 = require("@graphql-tools/executor");
const logger_1 = require("@graphql-yoga/logger");
const defaultFetchAPI = tslib_1.__importStar(require("@whatwg-node/fetch"));
const promise_helpers_1 = require("@whatwg-node/promise-helpers");
const server_1 = require("@whatwg-node/server");
const error_js_1 = require("./error.js");
const allowed_headers_js_1 = require("./plugins/allowed-headers.js");
const get_js_1 = require("./plugins/request-parser/get.js");
const post_form_url_encoded_js_1 = require("./plugins/request-parser/post-form-url-encoded.js");
const post_graphql_string_js_1 = require("./plugins/request-parser/post-graphql-string.js");
const post_json_js_1 = require("./plugins/request-parser/post-json.js");
const post_multipart_js_1 = require("./plugins/request-parser/post-multipart.js");
const use_check_graphql_query_params_js_1 = require("./plugins/request-validation/use-check-graphql-query-params.js");
const use_check_method_for_graphql_js_1 = require("./plugins/request-validation/use-check-method-for-graphql.js");
const use_http_validation_error_js_1 = require("./plugins/request-validation/use-http-validation-error.js");
const use_limit_batching_js_1 = require("./plugins/request-validation/use-limit-batching.js");
const use_prevent_mutation_via_get_js_1 = require("./plugins/request-validation/use-prevent-mutation-via-get.js");
const use_graphiql_js_1 = require("./plugins/use-graphiql.js");
const use_health_check_js_1 = require("./plugins/use-health-check.js");
const use_parser_and_validation_cache_js_1 = require("./plugins/use-parser-and-validation-cache.js");
const use_request_parser_js_1 = require("./plugins/use-request-parser.js");
const use_result_processor_js_1 = require("./plugins/use-result-processor.js");
const use_schema_js_1 = require("./plugins/use-schema.js");
const use_unhandled_route_js_1 = require("./plugins/use-unhandled-route.js");
const process_request_js_1 = require("./process-request.js");
const mask_error_js_1 = require("./utils/mask-error.js");
/**
 * Base class that can be extended to create a GraphQL server with any HTTP server framework.
 * @internal
 */
class YogaServer {
    /**
     * Instance of envelop
     */
    getEnveloped;
    logger;
    graphqlEndpoint;
    fetchAPI;
    plugins;
    instrumentation;
    onRequestParseHooks;
    onParamsHooks;
    onExecutionResultHooks;
    onResultProcessHooks;
    maskedErrorsOpts;
    id;
    version = '5.16.0';
    constructor(options) {
        this.id = options?.id ?? 'yoga';
        this.fetchAPI = {
            ...defaultFetchAPI,
        };
        if (options?.fetchAPI) {
            for (const key in options.fetchAPI) {
                if (options.fetchAPI[key]) {
                    this.fetchAPI[key] = options.fetchAPI[key];
                }
            }
        }
        const logger = options?.logging == null ? true : options.logging;
        this.logger =
            typeof logger === 'boolean'
                ? logger === true
                    ? (0, logger_1.createLogger)()
                    : (0, logger_1.createLogger)('silent')
                : typeof logger === 'string'
                    ? (0, logger_1.createLogger)(logger)
                    : logger;
        const maskErrorFn = (typeof options?.maskedErrors === 'object' && options.maskedErrors.maskError) || mask_error_js_1.maskError;
        const maskedErrorSet = new WeakSet();
        this.maskedErrorsOpts =
            options?.maskedErrors === false
                ? null
                : {
                    errorMessage: 'Unexpected error.',
                    ...(typeof options?.maskedErrors === 'object' ? options.maskedErrors : {}),
                    maskError: (error, message) => {
                        if (maskedErrorSet.has(error)) {
                            return error;
                        }
                        const newError = maskErrorFn(error, message, this.maskedErrorsOpts?.isDev);
                        if (newError !== error) {
                            this.logger.error(error);
                        }
                        maskedErrorSet.add(newError);
                        return newError;
                    },
                };
        const maskedErrors = this.maskedErrorsOpts == null ? null : this.maskedErrorsOpts;
        let batchingLimit = 0;
        if (options?.batching) {
            if (typeof options.batching === 'boolean') {
                batchingLimit = 10;
            }
            else {
                batchingLimit = options.batching.limit ?? 10;
            }
        }
        this.graphqlEndpoint = options?.graphqlEndpoint || '/graphql';
        const graphqlEndpoint = this.graphqlEndpoint;
        this.plugins = [
            (0, core_1.useEngine)({
                parse: graphql_1.parse,
                validate: graphql_1.validate,
                execute: executor_1.normalizedExecutor,
                subscribe: executor_1.normalizedExecutor,
                specifiedRules: graphql_1.specifiedRules,
            }),
            // Use the schema provided by the user
            !!options?.schema && (0, use_schema_js_1.useSchema)(options.schema),
            options?.allowedHeaders?.request != null &&
                (0, allowed_headers_js_1.useAllowedRequestHeaders)(options.allowedHeaders.request),
            options?.context != null &&
                (0, core_1.useExtendContext)(initialContext => {
                    if (options?.context) {
                        if (typeof options.context === 'function') {
                            return options.context(initialContext);
                        }
                        return options.context;
                    }
                    return {};
                }),
            // Middlewares before processing the incoming HTTP request
            (0, use_health_check_js_1.useHealthCheck)({
                id: this.id,
                logger: this.logger,
                endpoint: options?.healthCheckEndpoint,
            }),
            options?.cors !== false && (0, server_1.useCORS)(options?.cors),
            options?.graphiql !== false &&
                (0, use_graphiql_js_1.useGraphiQL)({
                    graphqlEndpoint,
                    options: options?.graphiql,
                    render: options?.renderGraphiQL,
                    logger: this.logger,
                }),
            // Middlewares before the GraphQL execution
            (0, use_request_parser_js_1.useRequestParser)({
                match: get_js_1.isGETRequest,
                parse: get_js_1.parseGETRequest,
            }),
            (0, use_request_parser_js_1.useRequestParser)({
                match: post_json_js_1.isPOSTJsonRequest,
                parse: post_json_js_1.parsePOSTJsonRequest,
            }),
            options?.multipart !== false &&
                (0, use_request_parser_js_1.useRequestParser)({
                    match: post_multipart_js_1.isPOSTMultipartRequest,
                    parse: post_multipart_js_1.parsePOSTMultipartRequest,
                }),
            (0, use_request_parser_js_1.useRequestParser)({
                match: post_graphql_string_js_1.isPOSTGraphQLStringRequest,
                parse: post_graphql_string_js_1.parsePOSTGraphQLStringRequest,
            }),
            (0, use_request_parser_js_1.useRequestParser)({
                match: post_form_url_encoded_js_1.isPOSTFormUrlEncodedRequest,
                parse: post_form_url_encoded_js_1.parsePOSTFormUrlEncodedRequest,
            }),
            // Middlewares after the GraphQL execution
            (0, use_result_processor_js_1.useResultProcessors)(),
            ...(options?.plugins ?? []),
            options?.parserAndValidationCache !== false &&
                (0, use_parser_and_validation_cache_js_1.useParserAndValidationCache)(!options?.parserAndValidationCache || options?.parserAndValidationCache === true
                    ? {}
                    : options?.parserAndValidationCache),
            (0, use_limit_batching_js_1.useLimitBatching)(batchingLimit),
            (0, use_check_graphql_query_params_js_1.useCheckGraphQLQueryParams)(options?.extraParamNames),
            (0, use_unhandled_route_js_1.useUnhandledRoute)({
                graphqlEndpoint,
                showLandingPage: options?.landingPage !== false,
                landingPageRenderer: typeof options?.landingPage === 'function' ? options.landingPage : undefined,
            }),
            // We check the method after user-land plugins because the plugin might support more methods (like graphql-sse).
            (0, use_check_method_for_graphql_js_1.useCheckMethodForGraphQL)(),
            // We make sure that the user doesn't send a mutation with GET
            (0, use_prevent_mutation_via_get_js_1.usePreventMutationViaGET)(),
            // Make sure we always throw AbortError instead of masking it!
            maskedErrors !== null && {
                onSubscribe() {
                    return {
                        onSubscribeError({ error }) {
                            if ((0, error_js_1.isAbortError)(error)) {
                                throw error;
                            }
                        },
                    };
                },
            },
            maskedErrors !== null && (0, core_1.useMaskedErrors)(maskedErrors),
            options?.allowedHeaders?.response != null &&
                (0, allowed_headers_js_1.useAllowedResponseHeaders)(options.allowedHeaders.response),
            // We handle validation errors at the end
            (0, use_http_validation_error_js_1.useHTTPValidationError)(),
        ];
        this.getEnveloped = (0, core_1.envelop)({
            plugins: this.plugins,
        });
        this.plugins = this.getEnveloped._plugins;
        this.onRequestParseHooks = [];
        this.onParamsHooks = [];
        this.onExecutionResultHooks = [];
        this.onResultProcessHooks = [];
        for (const plugin of this.plugins) {
            if (plugin) {
                if (plugin.onYogaInit) {
                    plugin.onYogaInit({
                        yoga: this,
                    });
                }
                if (plugin.onRequestParse) {
                    this.onRequestParseHooks.push(plugin.onRequestParse);
                }
                if (plugin.onParams) {
                    this.onParamsHooks.push(plugin.onParams);
                }
                if (plugin.onExecutionResult) {
                    this.onExecutionResultHooks.push(plugin.onExecutionResult);
                }
                if (plugin.onResultProcess) {
                    this.onResultProcessHooks.push(plugin.onResultProcess);
                }
                if (plugin.instrumentation) {
                    this.instrumentation = this.instrumentation
                        ? (0, instrumentation_1.chain)(this.instrumentation, plugin.instrumentation)
                        : plugin.instrumentation;
                }
            }
        }
    }
    handleParams = ({ request, context, params }) => {
        const additionalContext = context['request'] === request
            ? {
                params,
            }
            : {
                request,
                params,
            };
        Object.assign(context, additionalContext);
        const enveloped = this.getEnveloped(context);
        this.logger.debug(`Processing GraphQL Parameters`);
        return (0, promise_helpers_1.handleMaybePromise)(() => (0, promise_helpers_1.handleMaybePromise)(() => (0, process_request_js_1.processRequest)({ params, enveloped }), result => {
            this.logger.debug(`Processing GraphQL Parameters done.`);
            return result;
        }, error => {
            const errors = (0, error_js_1.handleError)(error, this.maskedErrorsOpts, this.logger);
            return {
                errors,
            };
        }), result => {
            if ((0, core_1.isAsyncIterable)(result)) {
                result = (0, promise_helpers_1.mapAsyncIterator)(result, v => v, (error) => {
                    if (error.name === 'AbortError') {
                        this.logger.debug(`Request aborted`);
                        throw error;
                    }
                    const errors = (0, error_js_1.handleError)(error, this.maskedErrorsOpts, this.logger);
                    return {
                        errors,
                    };
                });
            }
            return result;
        });
    };
    getResultForParams = ({ params, request, }, context) => {
        let result;
        let paramsHandler = this.handleParams;
        return (0, promise_helpers_1.handleMaybePromise)(() => (0, promise_helpers_1.iterateAsync)(this.onParamsHooks, onParamsHook => onParamsHook({
            params,
            request,
            setParams(newParams) {
                params = newParams;
            },
            paramsHandler,
            setParamsHandler(newHandler) {
                paramsHandler = newHandler;
            },
            setResult(newResult) {
                result = newResult;
            },
            fetchAPI: this.fetchAPI,
            context,
        })), () => (0, promise_helpers_1.handleMaybePromise)(() => result ||
            paramsHandler({
                request,
                params,
                context: context,
            }), result => (0, promise_helpers_1.handleMaybePromise)(() => (0, promise_helpers_1.iterateAsync)(this.onExecutionResultHooks, onExecutionResult => onExecutionResult({
            result,
            setResult(newResult) {
                result = newResult;
            },
            request,
            context: context,
        })), () => result)));
    };
    parseRequest = (request, serverContext) => {
        let url = new Proxy({}, {
            get: (_target, prop, _receiver) => {
                url = new this.fetchAPI.URL(request.url, 'http://localhost');
                return Reflect.get(url, prop, url);
            },
        });
        let requestParser;
        const onRequestParseDoneList = [];
        return (0, promise_helpers_1.handleMaybePromise)(() => (0, promise_helpers_1.iterateAsync)(this.onRequestParseHooks, onRequestParse => (0, promise_helpers_1.handleMaybePromise)(() => onRequestParse({
            request,
            url,
            requestParser,
            serverContext,
            setRequestParser(parser) {
                requestParser = parser;
            },
        }), requestParseHookResult => requestParseHookResult?.onRequestParseDone), onRequestParseDoneList), () => {
            this.logger.debug(`Parsing request to extract GraphQL parameters`);
            if (!requestParser) {
                return {
                    response: new this.fetchAPI.Response(null, {
                        status: 415,
                        statusText: 'Unsupported Media Type',
                    }),
                };
            }
            return (0, promise_helpers_1.handleMaybePromise)(() => requestParser(request), requestParserResult => {
                return (0, promise_helpers_1.handleMaybePromise)(() => (0, promise_helpers_1.iterateAsyncVoid)(onRequestParseDoneList, onRequestParseDone => onRequestParseDone({
                    requestParserResult,
                    setRequestParserResult(newParams) {
                        requestParserResult = newParams;
                    },
                })), () => ({
                    requestParserResult,
                }));
            });
        });
    };
    handle = (request, serverContext) => {
        const instrumented = this.instrumentation && (0, instrumentation_1.getInstrumented)({ request });
        const parseRequest = this.instrumentation?.requestParse
            ? instrumented.asyncFn(this.instrumentation?.requestParse, this.parseRequest)
            : this.parseRequest;
        return (0, promise_helpers_1.unfakePromise)((0, promise_helpers_1.fakePromise)()
            .then(() => parseRequest(request, serverContext))
            .then(({ response, requestParserResult }) => {
            if (response) {
                return response;
            }
            const getResultForParams = this.instrumentation?.operation
                ? (payload, context) => {
                    const instrumented = (0, instrumentation_1.getInstrumented)({ context, request: payload.request });
                    const tracedHandler = instrumented.asyncFn(this.instrumentation?.operation, this.getResultForParams);
                    return tracedHandler(payload, context);
                }
                : this.getResultForParams;
            return (0, promise_helpers_1.handleMaybePromise)(() => (Array.isArray(requestParserResult)
                ? Promise.all(requestParserResult.map(params => getResultForParams({
                    params,
                    request,
                }, Object.create(serverContext))))
                : getResultForParams({
                    params: requestParserResult,
                    request,
                }, serverContext)), result => {
                const tracedProcessResult = this.instrumentation?.resultProcess
                    ? instrumented.asyncFn(this.instrumentation.resultProcess, (process_request_js_1.processResult))
                    : process_request_js_1.processResult;
                return tracedProcessResult({
                    request,
                    result,
                    fetchAPI: this.fetchAPI,
                    onResultProcessHooks: this.onResultProcessHooks,
                    serverContext,
                });
            });
        })
            .catch(error => {
            const errors = (0, error_js_1.handleError)(error, this.maskedErrorsOpts, this.logger);
            const result = {
                errors,
            };
            return (0, process_request_js_1.processResult)({
                request,
                result,
                fetchAPI: this.fetchAPI,
                onResultProcessHooks: this.onResultProcessHooks,
                serverContext,
            });
        }));
    };
}
exports.YogaServer = YogaServer;
function createYoga(options) {
    const server = new YogaServer(options);
    return (0, server_1.createServerAdapter)(server, {
        fetchAPI: server.fetchAPI,
        plugins: server['plugins'],
        disposeOnProcessTerminate: options.disposeOnProcessTerminate,
    });
}