UNPKG

11.3 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.graphqlHTTP = graphqlHTTP;
7exports.getGraphQLParams = getGraphQLParams;
8
9var _accepts = _interopRequireDefault(require("accepts"));
10
11var _httpErrors = _interopRequireDefault(require("http-errors"));
12
13var _graphql = require("graphql");
14
15var _parseBody = require("./parseBody");
16
17var _renderGraphiQL = require("./renderGraphiQL");
18
19function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
21/**
22 * Middleware for express; takes an options object or function as input to
23 * configure behavior, and returns an express middleware.
24 */
25function graphqlHTTP(options) {
26 if (!options) {
27 throw new Error('GraphQL middleware requires options.');
28 }
29
30 return async function graphqlMiddleware(request, response) {
31 // Higher scoped variables are referred to at various stages in the asynchronous state machine below.
32 let params;
33 let showGraphiQL = false;
34 let graphiqlOptions;
35 let formatErrorFn = _graphql.formatError;
36 let pretty = false;
37 let result;
38
39 try {
40 // Parse the Request to get GraphQL request parameters.
41 try {
42 params = await getGraphQLParams(request);
43 } catch (error) {
44 // When we failed to parse the GraphQL parameters, we still need to get
45 // the options object, so make an options call to resolve just that.
46 const optionsData = await resolveOptions();
47 pretty = optionsData.pretty ?? false;
48 formatErrorFn = optionsData.customFormatErrorFn ?? optionsData.formatError ?? formatErrorFn;
49 throw error;
50 } // Then, resolve the Options to get OptionsData.
51
52
53 const optionsData = await resolveOptions(params); // Collect information from the options data object.
54
55 const schema = optionsData.schema;
56 const rootValue = optionsData.rootValue;
57 const validationRules = optionsData.validationRules ?? [];
58 const fieldResolver = optionsData.fieldResolver;
59 const typeResolver = optionsData.typeResolver;
60 const graphiql = optionsData.graphiql ?? false;
61 const extensionsFn = optionsData.extensions;
62 const context = optionsData.context ?? request;
63 const parseFn = optionsData.customParseFn ?? _graphql.parse;
64 const executeFn = optionsData.customExecuteFn ?? _graphql.execute;
65 const validateFn = optionsData.customValidateFn ?? _graphql.validate;
66 pretty = optionsData.pretty ?? false;
67 formatErrorFn = optionsData.customFormatErrorFn ?? optionsData.formatError ?? formatErrorFn; // Assert that schema is required.
68
69 if (schema == null) {
70 throw (0, _httpErrors.default)(500, 'GraphQL middleware options must contain a schema.');
71 } // GraphQL HTTP only supports GET and POST methods.
72
73
74 if (request.method !== 'GET' && request.method !== 'POST') {
75 throw (0, _httpErrors.default)(405, 'GraphQL only supports GET and POST requests.', {
76 headers: {
77 Allow: 'GET, POST'
78 }
79 });
80 } // Get GraphQL params from the request and POST body data.
81
82
83 const {
84 query,
85 variables,
86 operationName
87 } = params;
88 showGraphiQL = canDisplayGraphiQL(request, params) && graphiql !== false;
89
90 if (typeof graphiql !== 'boolean') {
91 graphiqlOptions = graphiql;
92 } // If there is no query, but GraphiQL will be displayed, do not produce
93 // a result, otherwise return a 400: Bad Request.
94
95
96 if (query == null) {
97 if (showGraphiQL) {
98 return respondWithGraphiQL(response, graphiqlOptions);
99 }
100
101 throw (0, _httpErrors.default)(400, 'Must provide query string.');
102 } // Validate Schema
103
104
105 const schemaValidationErrors = (0, _graphql.validateSchema)(schema);
106
107 if (schemaValidationErrors.length > 0) {
108 // Return 500: Internal Server Error if invalid schema.
109 throw (0, _httpErrors.default)(500, 'GraphQL schema validation error.', {
110 graphqlErrors: schemaValidationErrors
111 });
112 } // Parse source to AST, reporting any syntax error.
113
114
115 let documentAST;
116
117 try {
118 documentAST = parseFn(new _graphql.Source(query, 'GraphQL request'));
119 } catch (syntaxError) {
120 // Return 400: Bad Request if any syntax errors errors exist.
121 throw (0, _httpErrors.default)(400, 'GraphQL syntax error.', {
122 graphqlErrors: [syntaxError]
123 });
124 } // Validate AST, reporting any errors.
125
126
127 const validationErrors = validateFn(schema, documentAST, [..._graphql.specifiedRules, ...validationRules]);
128
129 if (validationErrors.length > 0) {
130 // Return 400: Bad Request if any validation errors exist.
131 throw (0, _httpErrors.default)(400, 'GraphQL validation error.', {
132 graphqlErrors: validationErrors
133 });
134 } // Only query operations are allowed on GET requests.
135
136
137 if (request.method === 'GET') {
138 // Determine if this GET request will perform a non-query.
139 const operationAST = (0, _graphql.getOperationAST)(documentAST, operationName);
140
141 if (operationAST && operationAST.operation !== 'query') {
142 // If GraphiQL can be shown, do not perform this query, but
143 // provide it to GraphiQL so that the requester may perform it
144 // themselves if desired.
145 if (showGraphiQL) {
146 return respondWithGraphiQL(response, graphiqlOptions, params);
147 } // Otherwise, report a 405: Method Not Allowed error.
148
149
150 throw (0, _httpErrors.default)(405, `Can only perform a ${operationAST.operation} operation from a POST request.`, {
151 headers: {
152 Allow: 'POST'
153 }
154 });
155 }
156 } // Perform the execution, reporting any errors creating the context.
157
158
159 try {
160 result = await executeFn({
161 schema,
162 document: documentAST,
163 rootValue,
164 contextValue: context,
165 variableValues: variables,
166 operationName,
167 fieldResolver,
168 typeResolver
169 });
170 } catch (contextError) {
171 // Return 400: Bad Request if any execution context errors exist.
172 throw (0, _httpErrors.default)(400, 'GraphQL execution context error.', {
173 graphqlErrors: [contextError]
174 });
175 } // Collect and apply any metadata extensions if a function was provided.
176 // https://graphql.github.io/graphql-spec/#sec-Response-Format
177
178
179 if (extensionsFn) {
180 const extensions = await extensionsFn({
181 document: documentAST,
182 variables,
183 operationName,
184 result,
185 context
186 });
187
188 if (extensions != null) {
189 result = { ...result,
190 extensions
191 };
192 }
193 }
194 } catch (error) {
195 // If an error was caught, report the httpError status, or 500.
196 response.statusCode = error.status ?? 500;
197
198 if (error.headers != null) {
199 for (const [key, value] of Object.entries(error.headers)) {
200 response.setHeader(key, value);
201 }
202 }
203
204 result = {
205 data: undefined,
206 errors: error.graphqlErrors ?? [error]
207 };
208 } // If no data was included in the result, that indicates a runtime query
209 // error, indicate as such with a generic status code.
210 // Note: Information about the error itself will still be contained in
211 // the resulting JSON payload.
212 // https://graphql.github.io/graphql-spec/#sec-Data
213
214
215 if (response.statusCode === 200 && result.data == null) {
216 response.statusCode = 500;
217 } // Format any encountered errors.
218
219
220 if (result.errors) {
221 result.errors = result.errors.map(formatErrorFn);
222 } // If allowed to show GraphiQL, present it instead of JSON.
223
224
225 if (showGraphiQL) {
226 return respondWithGraphiQL(response, graphiqlOptions, params, result);
227 } // If "pretty" JSON isn't requested, and the server provides a
228 // response.json method (express), use that directly.
229 // Otherwise use the simplified sendResponse method.
230
231
232 if (!pretty && typeof response.json === 'function') {
233 response.json(result);
234 } else {
235 const payload = JSON.stringify(result, null, pretty ? 2 : 0);
236 sendResponse(response, 'application/json', payload);
237 }
238
239 async function resolveOptions(requestParams) {
240 const optionsResult = await Promise.resolve(typeof options === 'function' ? options(request, response, requestParams) : options); // Assert that optionsData is in fact an Object.
241
242 if (optionsResult == null || typeof optionsResult !== 'object') {
243 throw new Error('GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.');
244 }
245
246 if (optionsResult.formatError) {
247 // eslint-disable-next-line no-console
248 console.warn('`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.');
249 }
250
251 return optionsResult;
252 }
253 };
254}
255
256function respondWithGraphiQL(response, options, params, result) {
257 const data = {
258 query: params?.query,
259 variables: params?.variables,
260 operationName: params?.operationName,
261 result
262 };
263 const payload = (0, _renderGraphiQL.renderGraphiQL)(data, options);
264 return sendResponse(response, 'text/html', payload);
265}
266
267/**
268 * Provided a "Request" provided by express or connect (typically a node style
269 * HTTPClientRequest), Promise the GraphQL request parameters.
270 */
271async function getGraphQLParams(request) {
272 const urlData = new URLSearchParams(request.url.split('?')[1]);
273 const bodyData = await (0, _parseBody.parseBody)(request); // GraphQL Query string.
274
275 let query = urlData.get('query') ?? bodyData.query;
276
277 if (typeof query !== 'string') {
278 query = null;
279 } // Parse the variables if needed.
280
281
282 let variables = urlData.get('variables') ?? bodyData.variables;
283
284 if (typeof variables === 'string') {
285 try {
286 variables = JSON.parse(variables);
287 } catch (error) {
288 throw (0, _httpErrors.default)(400, 'Variables are invalid JSON.');
289 }
290 } else if (typeof variables !== 'object') {
291 variables = null;
292 } // Name of GraphQL operation to execute.
293
294
295 let operationName = urlData.get('operationName') || bodyData.operationName;
296
297 if (typeof operationName !== 'string') {
298 operationName = null;
299 }
300
301 const raw = urlData.get('raw') != null || bodyData.raw !== undefined;
302 return {
303 query,
304 variables,
305 operationName,
306 raw
307 };
308}
309/**
310 * Helper function to determine if GraphiQL can be displayed.
311 */
312
313
314function canDisplayGraphiQL(request, params) {
315 // If `raw` false, GraphiQL mode is not enabled.
316 // Allowed to show GraphiQL if not requested as raw and this request prefers HTML over JSON.
317 return !params.raw && (0, _accepts.default)(request).types(['json', 'html']) === 'html';
318}
319/**
320 * Helper function for sending a response using only the core Node server APIs.
321 */
322
323
324function sendResponse(response, type, data) {
325 const chunk = Buffer.from(data, 'utf8');
326 response.setHeader('Content-Type', type + '; charset=utf-8');
327 response.setHeader('Content-Length', String(chunk.length));
328 response.end(chunk);
329}