graphql-yoga
Version:
441 lines (440 loc) • 19.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createYoga = exports.YogaServer = void 0;
const graphql_1 = require("graphql");
const executor_1 = require("@graphql-tools/executor");
const core_1 = require("@envelop/core");
const validation_cache_1 = require("@envelop/validation-cache");
const parser_cache_1 = require("@envelop/parser-cache");
const fetch_1 = require("@whatwg-node/fetch");
const server_1 = require("@whatwg-node/server");
const process_request_js_1 = require("./process-request.js");
const logger_js_1 = require("./logger.js");
const useCORS_js_1 = require("./plugins/useCORS.js");
const useHealthCheck_js_1 = require("./plugins/useHealthCheck.js");
const useGraphiQL_js_1 = require("./plugins/useGraphiQL.js");
const useRequestParser_js_1 = require("./plugins/useRequestParser.js");
const GET_js_1 = require("./plugins/requestParser/GET.js");
const POSTJson_js_1 = require("./plugins/requestParser/POSTJson.js");
const POSTMultipart_js_1 = require("./plugins/requestParser/POSTMultipart.js");
const POSTGraphQLString_js_1 = require("./plugins/requestParser/POSTGraphQLString.js");
const useResultProcessor_js_1 = require("./plugins/useResultProcessor.js");
const regular_js_1 = require("./plugins/resultProcessor/regular.js");
const push_js_1 = require("./plugins/resultProcessor/push.js");
const multipart_js_1 = require("./plugins/resultProcessor/multipart.js");
const POSTFormUrlEncoded_js_1 = require("./plugins/requestParser/POSTFormUrlEncoded.js");
const error_js_1 = require("./error.js");
const useCheckMethodForGraphQL_js_1 = require("./plugins/requestValidation/useCheckMethodForGraphQL.js");
const useCheckGraphQLQueryParams_js_1 = require("./plugins/requestValidation/useCheckGraphQLQueryParams.js");
const useHTTPValidationError_js_1 = require("./plugins/requestValidation/useHTTPValidationError.js");
const usePreventMutationViaGET_js_1 = require("./plugins/requestValidation/usePreventMutationViaGET.js");
const useUnhandledRoute_js_1 = require("./plugins/useUnhandledRoute.js");
const yoga_default_format_error_js_1 = require("./utils/yoga-default-format-error.js");
const useSchema_js_1 = require("./plugins/useSchema.js");
const useLimitBatching_js_1 = require("./plugins/requestValidation/useLimitBatching.js");
const overlapping_fields_can_be_merged_js_1 = require("./validations/overlapping-fields-can-be-merged.js");
const defer_stream_directive_on_root_field_js_1 = require("./validations/defer-stream-directive-on-root-field.js");
const defer_stream_directive_label_js_1 = require("./validations/defer-stream-directive-label.js");
const stream_directive_on_list_field_js_1 = require("./validations/stream-directive-on-list-field.js");
/**
* Base class that can be extended to create a GraphQL server with any HTTP server framework.
* @internal
*/
class YogaServer {
constructor(options) {
this.handle = async (request, serverContext) => {
try {
const response = await this.getResponse(request, serverContext);
for (const onResponseHook of this.onResponseHooks) {
await onResponseHook({
request,
response,
serverContext,
});
}
return response;
}
catch (e) {
this.logger.error(e);
return new this.fetchAPI.Response('Internal Server Error', {
status: 500,
});
}
};
this.id = options?.id ?? 'yoga';
this.fetchAPI =
options?.fetchAPI ??
(0, fetch_1.createFetch)({
useNodeFetch: true,
});
const logger = options?.logging != null ? options.logging : true;
this.logger =
typeof logger === 'boolean'
? logger === true
? logger_js_1.defaultYogaLogger
: {
/* eslint-disable */
debug: () => { },
error: () => { },
warn: () => { },
info: () => { },
/* eslint-enable */
}
: logger;
this.maskedErrorsOpts =
options?.maskedErrors === false
? null
: {
maskError: (error, message) => (0, yoga_default_format_error_js_1.yogaDefaultFormatError)({
error,
message,
isDev: this.maskedErrorsOpts?.isDev ?? false,
}),
errorMessage: 'Unexpected error.',
...(typeof options?.maskedErrors === 'object'
? options.maskedErrors
: {}),
};
const maskedErrors = this.maskedErrorsOpts != null ? this.maskedErrorsOpts : null;
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.filter(
// We do not want to use the default one cause it does not account for `@defer` and `@stream`
({ name }) => !['OverlappingFieldsCanBeMergedRule'].includes(name)),
overlapping_fields_can_be_merged_js_1.OverlappingFieldsCanBeMergedRule,
defer_stream_directive_on_root_field_js_1.DeferStreamDirectiveOnRootFieldRule,
defer_stream_directive_label_js_1.DeferStreamDirectiveLabelRule,
stream_directive_on_list_field_js_1.StreamDirectiveOnListFieldRule,
],
}),
// Use the schema provided by the user
!!options?.schema && (0, useSchema_js_1.useSchema)(options.schema),
// Performance things
options?.parserCache !== false &&
(0, parser_cache_1.useParserCache)(typeof options?.parserCache === 'object'
? options.parserCache
: undefined),
options?.validationCache !== false &&
(0, validation_cache_1.useValidationCache)({
cache: typeof options?.validationCache === 'object'
? options.validationCache
: undefined,
}),
// Log events - useful for debugging purposes
logger !== false &&
(0, core_1.useLogger)({
skipIntrospection: true,
logFn: (eventName, events) => {
switch (eventName) {
case 'execute-start':
case 'subscribe-start':
this.logger.debug((0, logger_js_1.titleBold)('Execution start'));
// eslint-disable-next-line no-case-declarations
const {
// the `params` might be missing in cases where the user provided
// malformed context to getEnveloped (like `yoga.getEnveloped({})`)
params: { query, operationName, variables, extensions } = {}, } = events.args.contextValue;
this.logger.debug((0, logger_js_1.titleBold)('Received GraphQL operation:'));
this.logger.debug({
query,
operationName,
variables,
extensions,
});
break;
case 'execute-end':
case 'subscribe-end':
this.logger.debug((0, logger_js_1.titleBold)('Execution end'));
this.logger.debug({
result: events.result,
});
break;
}
},
}),
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, useHealthCheck_js_1.useHealthCheck)({
id: this.id,
logger: this.logger,
endpoint: options?.healthCheckEndpoint,
}),
options?.cors !== false && (0, useCORS_js_1.useCORS)(options?.cors),
options?.graphiql !== false &&
(0, useGraphiQL_js_1.useGraphiQL)({
graphqlEndpoint: this.graphqlEndpoint,
options: options?.graphiql,
render: options?.renderGraphiQL,
logger: this.logger,
}),
// Middlewares before the GraphQL execution
(0, useCheckMethodForGraphQL_js_1.useCheckMethodForGraphQL)(),
(0, useRequestParser_js_1.useRequestParser)({
match: GET_js_1.isGETRequest,
parse: GET_js_1.parseGETRequest,
}),
(0, useRequestParser_js_1.useRequestParser)({
match: POSTJson_js_1.isPOSTJsonRequest,
parse: POSTJson_js_1.parsePOSTJsonRequest,
}),
options?.multipart !== false &&
(0, useRequestParser_js_1.useRequestParser)({
match: POSTMultipart_js_1.isPOSTMultipartRequest,
parse: POSTMultipart_js_1.parsePOSTMultipartRequest,
}),
(0, useRequestParser_js_1.useRequestParser)({
match: POSTGraphQLString_js_1.isPOSTGraphQLStringRequest,
parse: POSTGraphQLString_js_1.parsePOSTGraphQLStringRequest,
}),
(0, useRequestParser_js_1.useRequestParser)({
match: POSTFormUrlEncoded_js_1.isPOSTFormUrlEncodedRequest,
parse: POSTFormUrlEncoded_js_1.parsePOSTFormUrlEncodedRequest,
}),
// Middlewares after the GraphQL execution
(0, useResultProcessor_js_1.useResultProcessor)({
mediaTypes: ['multipart/mixed'],
processResult: multipart_js_1.processMultipartResult,
}),
(0, useResultProcessor_js_1.useResultProcessor)({
mediaTypes: ['text/event-stream'],
processResult: push_js_1.processPushResult,
}),
(0, useResultProcessor_js_1.useResultProcessor)({
mediaTypes: ['application/graphql-response+json', 'application/json'],
processResult: regular_js_1.processRegularResult,
}),
...(options?.plugins ?? []),
(0, useLimitBatching_js_1.useLimitBatching)(batchingLimit),
(0, useCheckGraphQLQueryParams_js_1.useCheckGraphQLQueryParams)(),
(0, useUnhandledRoute_js_1.useUnhandledRoute)({
graphqlEndpoint,
showLandingPage: options?.landingPage ?? true,
}),
// We make sure that the user doesn't send a mutation with GET
(0, usePreventMutationViaGET_js_1.usePreventMutationViaGET)(),
// To make sure those are called at the end
{
onPluginInit({ addPlugin }) {
if (maskedErrors) {
addPlugin((0, core_1.useMaskedErrors)(maskedErrors));
}
addPlugin(
// We handle validation errors at the end
(0, useHTTPValidationError_js_1.useHTTPValidationError)());
},
},
];
this.getEnveloped = (0, core_1.envelop)({
plugins: this.plugins,
});
this.onRequestHooks = [];
this.onRequestParseHooks = [];
this.onParamsHooks = [];
this.onResultProcessHooks = [];
this.onResponseHooks = [];
for (const plugin of this.plugins) {
if (plugin) {
if (plugin.onRequest) {
this.onRequestHooks.push(plugin.onRequest);
}
if (plugin.onRequestParse) {
this.onRequestParseHooks.push(plugin.onRequestParse);
}
if (plugin.onParams) {
this.onParamsHooks.push(plugin.onParams);
}
if (plugin.onResultProcess) {
this.onResultProcessHooks.push(plugin.onResultProcess);
}
if (plugin.onResponse) {
this.onResponseHooks.push(plugin.onResponse);
}
}
}
}
async getResultForParams({ params, request, },
// eslint-disable-next-line @typescript-eslint/ban-types
...args) {
try {
let result;
for (const onParamsHook of this.onParamsHooks) {
await onParamsHook({
params,
request,
setParams(newParams) {
params = newParams;
},
setResult(newResult) {
result = newResult;
},
fetchAPI: this.fetchAPI,
});
}
if (result == null) {
const serverContext = args[0];
const initialContext = {
...serverContext,
request,
params,
};
const enveloped = this.getEnveloped(initialContext);
this.logger.debug(`Processing GraphQL Parameters`);
result = await (0, process_request_js_1.processRequest)({
params,
enveloped,
});
}
return result;
}
catch (error) {
const errors = (0, error_js_1.handleError)(error, this.maskedErrorsOpts);
const result = {
errors,
};
return result;
}
}
async getResponse(request, serverContext) {
const url = new URL(request.url, 'http://localhost');
for (const onRequestHook of this.onRequestHooks) {
let response;
await onRequestHook({
request,
serverContext,
fetchAPI: this.fetchAPI,
url,
endResponse(newResponse) {
response = newResponse;
},
});
if (response) {
return response;
}
}
let requestParser;
const onRequestParseDoneList = [];
let result;
try {
for (const onRequestParse of this.onRequestParseHooks) {
const onRequestParseResult = await onRequestParse({
request,
requestParser,
serverContext,
setRequestParser(parser) {
requestParser = parser;
},
});
if (onRequestParseResult?.onRequestParseDone != null) {
onRequestParseDoneList.push(onRequestParseResult.onRequestParseDone);
}
}
this.logger.debug(`Parsing request to extract GraphQL parameters`);
if (!requestParser) {
return new this.fetchAPI.Response('Request is not valid', {
status: 400,
statusText: 'Bad Request',
});
}
let requestParserResult = await requestParser(request);
for (const onRequestParseDone of onRequestParseDoneList) {
await onRequestParseDone({
requestParserResult,
setRequestParserResult(newParams) {
requestParserResult = newParams;
},
});
}
result = (await (Array.isArray(requestParserResult)
? Promise.all(requestParserResult.map((params) => this.getResultForParams({
params,
request,
}, serverContext)))
: this.getResultForParams({
params: requestParserResult,
request,
}, serverContext)));
}
catch (error) {
const errors = (0, error_js_1.handleError)(error, this.maskedErrorsOpts);
result = {
errors,
};
}
const response = await (0, process_request_js_1.processResult)({
request,
result,
fetchAPI: this.fetchAPI,
onResultProcessHooks: this.onResultProcessHooks,
});
return response;
}
/**
* Testing utility to mock http request for GraphQL endpoint
*
*
* Example - Test a simple query
* ```ts
* const { response, executionResult } = await yoga.inject({
* document: "query { ping }",
* })
* expect(response.status).toBe(200)
* expect(executionResult.data.ping).toBe('pong')
* ```
**/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async inject({ document, variables, operationName, headers, serverContext, }) {
const request = new this.fetchAPI.Request('http://localhost' + this.graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({
query: document &&
(typeof document === 'string' ? document : (0, graphql_1.print)(document)),
variables,
operationName,
}),
});
const response = await this.handle(request, serverContext);
let executionResult = null;
if (response.headers.get('content-type') === 'application/json') {
executionResult = await response.json();
}
return {
response,
executionResult,
};
}
}
exports.YogaServer = YogaServer;
function createYoga(options) {
const server = new YogaServer(options);
return (0, server_1.createServerAdapter)(server, server.fetchAPI.Request);
}
exports.createYoga = createYoga;