Files
vue-cli/packages/@vue/cli-ui/graphql-server.js
Guillaume Chau da43343329 fix(CORS): only allow connections from the designated host (#4985)
* fix(cors): only allow localhost

* fix: use host so it's configurable

* fix: use cors options object

* feat: use a custom graphql-server instead of the one from apollo plugin

exports the httpServer instance

* fix: add CORS validation in the http upgrade request

Co-authored-by: Haoqun Jiang <haoqunjiang@gmail.com>
2020-02-03 19:35:40 +08:00

220 lines
5.9 KiB
JavaScript

// modified from vue-cli-plugin-apollo/graphql-server
// added a return value for the server() call
const http = require('http')
const { chalk } = require('@vue/cli-shared-utils')
const express = require('express')
const { ApolloServer, gql } = require('apollo-server-express')
const { PubSub } = require('graphql-subscriptions')
const merge = require('deepmerge')
function defaultValue (provided, value) {
return provided == null ? value : provided
}
function autoCall (fn, ...context) {
if (typeof fn === 'function') {
return fn(...context)
}
return fn
}
module.exports = (options, cb = null) => {
// Default options
options = merge({
integratedEngine: false
}, options)
// Express app
const app = express()
// Customize those files
let typeDefs = load(options.paths.typeDefs)
const resolvers = load(options.paths.resolvers)
const context = load(options.paths.context)
const schemaDirectives = load(options.paths.directives)
let pubsub
try {
pubsub = load(options.paths.pubsub)
} catch (e) {
if (process.env.NODE_ENV !== 'production' && !options.quiet) {
console.log(chalk.yellow('Using default PubSub implementation for subscriptions.'))
console.log(chalk.grey('You should provide a different implementation in production (for example with Redis) by exporting it in \'apollo-server/pubsub.js\'.'))
}
}
let dataSources
try {
dataSources = load(options.paths.dataSources)
} catch (e) {}
// GraphQL API Server
// Realtime subscriptions
if (!pubsub) pubsub = new PubSub()
// Customize server
try {
const serverModule = load(options.paths.server)
serverModule(app)
} catch (e) {
// No file found
}
// Apollo server options
typeDefs = processSchema(typeDefs)
let apolloServerOptions = {
typeDefs,
resolvers,
schemaDirectives,
dataSources,
tracing: true,
cacheControl: true,
engine: !options.integratedEngine,
// Resolvers context from POST
context: async ({ req, connection }) => {
let contextData
try {
if (connection) {
contextData = await autoCall(context, { connection })
} else {
contextData = await autoCall(context, { req })
}
} catch (e) {
console.error(e)
throw e
}
contextData = Object.assign({}, contextData, { pubsub })
return contextData
},
// Resolvers context from WebSocket
subscriptions: {
path: options.subscriptionsPath,
onConnect: async (connection, websocket) => {
let contextData = {}
try {
contextData = await autoCall(context, {
connection,
websocket
})
contextData = Object.assign({}, contextData, { pubsub })
} catch (e) {
console.error(e)
throw e
}
return contextData
}
}
}
// Automatic mocking
if (options.enableMocks) {
// Customize this file
apolloServerOptions.mocks = load(options.paths.mocks)
apolloServerOptions.mockEntireSchema = false
if (!options.quiet) {
if (process.env.NODE_ENV === 'production') {
console.warn('Automatic mocking is enabled, consider disabling it with the \'enableMocks\' option.')
} else {
console.log('✔️ Automatic mocking is enabled')
}
}
}
// Apollo Engine
if (options.enableEngine && options.integratedEngine) {
if (options.engineKey) {
apolloServerOptions.engine = {
apiKey: options.engineKey,
schemaTag: options.schemaTag,
...options.engineOptions || {}
}
console.log('✔️ Apollo Engine is enabled')
} else if (!options.quiet) {
console.log(chalk.yellow('Apollo Engine key not found.') + `To enable Engine, set the ${chalk.cyan('VUE_APP_APOLLO_ENGINE_KEY')} env variable.`)
console.log('Create a key at https://engine.apollographql.com/')
console.log('You may see `Error: Must provide document` errors (query persisting tries).')
}
} else {
apolloServerOptions.engine = false
}
// Final options
apolloServerOptions = merge(apolloServerOptions, defaultValue(options.serverOptions, {}))
// Apollo Server
const server = new ApolloServer(apolloServerOptions)
// Express middleware
server.applyMiddleware({
app,
path: options.graphqlPath,
cors: options.cors
// gui: {
// endpoint: graphqlPath,
// subscriptionEndpoint: graphqlSubscriptionsPath,
// },
})
// Start server
const httpServer = http.createServer(app)
httpServer.setTimeout(options.timeout)
server.installSubscriptionHandlers(httpServer)
httpServer.listen({
host: options.host || 'localhost',
port: options.port
}, () => {
if (!options.quiet) {
console.log(`✔️ GraphQL Server is running on ${chalk.cyan(`http://localhost:${options.port}${options.graphqlPath}`)}`)
if (process.env.NODE_ENV !== 'production' && !process.env.VUE_CLI_API_MODE) {
console.log(`✔️ Type ${chalk.cyan('rs')} to restart the server`)
}
}
cb && cb()
})
// added in order to let vue cli to deal with the http upgrade request
return {
apolloServer: server,
httpServer
}
}
function load (file) {
const module = require(file)
if (module.default) {
return module.default
}
return module
}
function processSchema (typeDefs) {
if (Array.isArray(typeDefs)) {
return typeDefs.map(processSchema)
}
if (typeof typeDefs === 'string') {
// Convert schema to AST
typeDefs = gql(typeDefs)
}
// Remove upload scalar (it's already included in Apollo Server)
removeFromSchema(typeDefs, 'ScalarTypeDefinition', 'Upload')
return typeDefs
}
function removeFromSchema (document, kind, name) {
const definitions = document.definitions
const index = definitions.findIndex(
def => def.kind === kind && def.name.kind === 'Name' && def.name.value === name
)
if (index !== -1) {
definitions.splice(index, 1)
}
}