vertx-graphql
Version:
Create a GraphQL HTTP Server with Vert.x
286 lines (259 loc) • 8.76 kB
JavaScript
/*
* Copyright 2018 vertx-graphql
*
* Vertx-GraphQL licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/// <reference types="@vertx/web" />
// @ts-check
import { BodyHandler, } from '@vertx/web';
import { graphql, buildSchema } from 'graphql';
import { renderGraphiQL } from './bin/render-graphiql';
import { renderPlayground } from './bin/render-playground';
import { parseBody } from './bin/parse-body';
import {
themesGraphiQL,
themesPlayground
} from './bin/themes';
const getGraphQLParams = (ctx) => {
return parseBody(ctx)
.then(({ error, data }) => {
return { error, data };
});
}
const graphqlVertx = ({ schema, resolvers, context }, introspection) => {
return (ctx) => {
const response = ctx.response();
return getGraphQLParams(ctx)
.then((params) => {
return params;
})
.then(({ error, data }) => {
if (error) {
return response
.putHeader('content-type', 'application/json')
.setChunked(true)
.write(JSON.stringify({
data: null,
errors: [
{
message: 'A query attribute must be specified and must be a string.'
}
]
}))
.end();
}
const requestString = data.query;
const contextValue = context;
const variableValues = data.variables;
const operationName = data.operationName;
var rootValue = {
...resolvers.Query,
...resolvers.Mutation,
...resolvers.Subscription
};
graphql(
schema,
requestString,
rootValue,
contextValue,
variableValues,
operationName
).then((resolvedData) => {
if (resolvedData.errors) {
return response
.putHeader('content-type', 'application/json')
.setChunked(true)
.write(JSON.stringify({
errors: resolvedData.errors
}))
.end();
}
if (introspection === false) {
if (resolvedData &&
resolvedData.data &&
(resolvedData.data.__schema || resolvedData.data.__type)) {
return response
.putHeader('content-type', 'application/json')
.setChunked(true)
.write()
.end();
} else {
return response
.putHeader('content-type', 'application/json')
.setChunked(true)
.write(JSON.stringify(resolvedData))
.end();
}
} else {
return response
.putHeader('content-type', 'application/json')
.setChunked(true)
.write(JSON.stringify(resolvedData))
.end();
}
});
});
}
}
const graphiqlHandler = (options) => {
if (!options || typeof options !== 'object') {
throw new Error('GraphQL middleware requires options.');
}
const html = renderGraphiQL({
ENDPOINT: options.endpoint,
SUBSCRIPTIONS: options.subscriptions,
THEME: options.theme,
INTROSPECTION: options.introspection,
DEFAULT: options.defaultQuery
});
return (ctx) => {
return ctx.response()
.putHeader('content-type', 'text/html')
.setChunked(true)
.write(html)
.end()
}
}
const playgroundHandler = (options) => {
if (!options || typeof options !== 'object') {
throw new Error('GraphQL middleware requires options.');
}
const html = renderPlayground({
ENDPOINT: options.endpoint,
SUBSCRIPTIONS: options.subscriptions,
THEME: options.theme,
TABS: JSON.stringify(options.tabs, null, 2),
INTROSPECTION: options.introspection
});
return (ctx) => {
return ctx.response()
.putHeader('content-type', 'text/html')
.setChunked(true)
.write(html)
.end()
}
}
class GraphQLServer {
constructor({
typeDefs,
resolvers,
context,
introspection,
playground,
graphiql,
tracing
}) {
this.schema = buildSchema(typeDefs);
this.resolvers = resolvers;
this.context = context;
this.graphqlPath = '';
this.subscriptionsPath = '';
this.introspection = introspection;
this.playground = playground;
this.graphiql = graphiql;
this.tracing = tracing;
}
applyMiddleware({ app, path = '/graphql' }) {
if (!app) {
throw new Error('app is not defined')
}
this.graphqlPath = path;
this.subscriptionsPath = '/subscriptions';
if (typeof path === 'string') {
path = (path[0] === '/')
? path
: `/${path}`;
} else {
path = '/graphql';
}
const env = JSON.parse(JSON.stringify(process.env));
const playground = this.playground;
const graphiql = this.graphiql;
const introspection = this.introspection;
app.post(path).handler(BodyHandler.create().handle);
app.post(path).handler(graphqlVertx({
schema: this.schema,
resolvers: this.resolvers,
context: this.context
}, introspection));
if (graphiql && !playground) {
app.get(path).handler(graphiqlHandler({
endpoint: path,
subscriptions: this.subscriptionsPath || '',
theme: graphiql &&
graphiql.settings &&
themesGraphiQL.includes(graphiql.settings['editor.theme']) ? graphiql.settings['editor.theme']: null,
introspection: this.introspection === false ? null: undefined,
defaultQuery: graphiql &&
graphiql.defaultQuery ? graphiql.defaultQuery: null
}));
} else if (playground) {
app.get(path).handler(playgroundHandler({
endpoint: path,
subscriptions: this.subscriptionsPath || '',
theme: playground &&
playground.settings &&
themesPlayground.includes(playground.settings['editor.theme']) ? playground.settings['editor.theme']: null,
tabs: playground &&
playground.tabs &&
Array.isArray(playground.tabs) &&
playground.tabs.length ? playground.tabs: null,
introspection: this.introspection === false ? null: undefined
}));
} else {
if (env.ES4X_ENV !== 'production') {
if (playground !== false) {
app.get(path).handler(playgroundHandler({
endpoint: path,
subscriptions: this.subscriptionsPath || '',
theme: playground &&
playground.settings &&
themesPlayground.includes(playground.settings['editor.theme']) ? playground.settings['editor.theme']: null,
tabs: playground &&
playground.tabs &&
Array.isArray(playground.tabs) &&
playground.tabs.length ? playground.tabs: null,
introspection: this.introspection === false ? null: undefined
}));
} else if (graphiql !== false) {
app.get(path).handler(graphiqlHandler({
endpoint: path,
subscriptions: this.subscriptionsPath || '',
theme: graphiql &&
graphiql.settings &&
graphiql.settings['editor.theme'] &&
themesGraphiQL.includes(graphiql.settings['editor.theme']) ? graphiql.settings['editor.theme']: null,
introspection: this.introspection === false ? null: undefined,
defaultQuery: graphiql &&
graphiql.defaultQuery ? graphiql.defaultQuery: null
}));
}
}
}
}
installSubscriptionHandlers(app) {
if (typeof this.subscriptionsPath === 'string') {
app.route(this.subscriptionsPath).handler((ctx) => {
const request = ctx.request();
const response = request.response();
const headers = response.headers();
headers.set("Sec-Websocket-Protocol", "graphql-subscriptions");
request.response().putHeader('Sec-Websocket-Protocol', 'graphql-subscriptions');
request.upgrade();
});
}
}
}
module.exports = {
GraphQLServer
};