|
| 1 | +// modified from vue-cli-plugin-apollo/graphql-server |
| 2 | +// added a return value for the server() call |
| 3 | + |
| 4 | +const http = require('http') |
| 5 | +const { chalk } = require('@vue/cli-shared-utils') |
| 6 | +const express = require('express') |
| 7 | +const { ApolloServer, gql } = require('apollo-server-express') |
| 8 | +const { PubSub } = require('graphql-subscriptions') |
| 9 | +const merge = require('deepmerge') |
| 10 | + |
| 11 | +function defaultValue (provided, value) { |
| 12 | + return provided == null ? value : provided |
| 13 | +} |
| 14 | + |
| 15 | +function autoCall (fn, ...context) { |
| 16 | + if (typeof fn === 'function') { |
| 17 | + return fn(...context) |
| 18 | + } |
| 19 | + return fn |
| 20 | +} |
| 21 | + |
| 22 | +module.exports = (options, cb = null) => { |
| 23 | + // Default options |
| 24 | + options = merge({ |
| 25 | + integratedEngine: false |
| 26 | + }, options) |
| 27 | + |
| 28 | + // Express app |
| 29 | + const app = express() |
| 30 | + |
| 31 | + // Customize those files |
| 32 | + let typeDefs = load(options.paths.typeDefs) |
| 33 | + const resolvers = load(options.paths.resolvers) |
| 34 | + const context = load(options.paths.context) |
| 35 | + const schemaDirectives = load(options.paths.directives) |
| 36 | + let pubsub |
| 37 | + try { |
| 38 | + pubsub = load(options.paths.pubsub) |
| 39 | + } catch (e) { |
| 40 | + if (process.env.NODE_ENV !== 'production' && !options.quiet) { |
| 41 | + console.log(chalk.yellow('Using default PubSub implementation for subscriptions.')) |
| 42 | + console.log(chalk.grey('You should provide a different implementation in production (for example with Redis) by exporting it in \'apollo-server/pubsub.js\'.')) |
| 43 | + } |
| 44 | + } |
| 45 | + let dataSources |
| 46 | + try { |
| 47 | + dataSources = load(options.paths.dataSources) |
| 48 | + } catch (e) {} |
| 49 | + |
| 50 | + // GraphQL API Server |
| 51 | + |
| 52 | + // Realtime subscriptions |
| 53 | + if (!pubsub) pubsub = new PubSub() |
| 54 | + |
| 55 | + // Customize server |
| 56 | + try { |
| 57 | + const serverModule = load(options.paths.server) |
| 58 | + serverModule(app) |
| 59 | + } catch (e) { |
| 60 | + // No file found |
| 61 | + } |
| 62 | + |
| 63 | + // Apollo server options |
| 64 | + |
| 65 | + typeDefs = processSchema(typeDefs) |
| 66 | + |
| 67 | + let apolloServerOptions = { |
| 68 | + typeDefs, |
| 69 | + resolvers, |
| 70 | + schemaDirectives, |
| 71 | + dataSources, |
| 72 | + tracing: true, |
| 73 | + cacheControl: true, |
| 74 | + engine: !options.integratedEngine, |
| 75 | + // Resolvers context from POST |
| 76 | + context: async ({ req, connection }) => { |
| 77 | + let contextData |
| 78 | + try { |
| 79 | + if (connection) { |
| 80 | + contextData = await autoCall(context, { connection }) |
| 81 | + } else { |
| 82 | + contextData = await autoCall(context, { req }) |
| 83 | + } |
| 84 | + } catch (e) { |
| 85 | + console.error(e) |
| 86 | + throw e |
| 87 | + } |
| 88 | + contextData = Object.assign({}, contextData, { pubsub }) |
| 89 | + return contextData |
| 90 | + }, |
| 91 | + // Resolvers context from WebSocket |
| 92 | + subscriptions: { |
| 93 | + path: options.subscriptionsPath, |
| 94 | + onConnect: async (connection, websocket) => { |
| 95 | + let contextData = {} |
| 96 | + try { |
| 97 | + contextData = await autoCall(context, { |
| 98 | + connection, |
| 99 | + websocket |
| 100 | + }) |
| 101 | + contextData = Object.assign({}, contextData, { pubsub }) |
| 102 | + } catch (e) { |
| 103 | + console.error(e) |
| 104 | + throw e |
| 105 | + } |
| 106 | + return contextData |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + // Automatic mocking |
| 112 | + if (options.enableMocks) { |
| 113 | + // Customize this file |
| 114 | + apolloServerOptions.mocks = load(options.paths.mocks) |
| 115 | + apolloServerOptions.mockEntireSchema = false |
| 116 | + |
| 117 | + if (!options.quiet) { |
| 118 | + if (process.env.NODE_ENV === 'production') { |
| 119 | + console.warn('Automatic mocking is enabled, consider disabling it with the \'enableMocks\' option.') |
| 120 | + } else { |
| 121 | + console.log('✔️ Automatic mocking is enabled') |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + // Apollo Engine |
| 127 | + if (options.enableEngine && options.integratedEngine) { |
| 128 | + if (options.engineKey) { |
| 129 | + apolloServerOptions.engine = { |
| 130 | + apiKey: options.engineKey, |
| 131 | + schemaTag: options.schemaTag, |
| 132 | + ...options.engineOptions || {} |
| 133 | + } |
| 134 | + console.log('✔️ Apollo Engine is enabled') |
| 135 | + } else if (!options.quiet) { |
| 136 | + console.log(chalk.yellow('Apollo Engine key not found.') + `To enable Engine, set the ${chalk.cyan('VUE_APP_APOLLO_ENGINE_KEY')} env variable.`) |
| 137 | + console.log('Create a key at https://engine.apollographql.com/') |
| 138 | + console.log('You may see `Error: Must provide document` errors (query persisting tries).') |
| 139 | + } |
| 140 | + } else { |
| 141 | + apolloServerOptions.engine = false |
| 142 | + } |
| 143 | + |
| 144 | + // Final options |
| 145 | + apolloServerOptions = merge(apolloServerOptions, defaultValue(options.serverOptions, {})) |
| 146 | + |
| 147 | + // Apollo Server |
| 148 | + const server = new ApolloServer(apolloServerOptions) |
| 149 | + |
| 150 | + // Express middleware |
| 151 | + server.applyMiddleware({ |
| 152 | + app, |
| 153 | + path: options.graphqlPath, |
| 154 | + cors: options.cors |
| 155 | + // gui: { |
| 156 | + // endpoint: graphqlPath, |
| 157 | + // subscriptionEndpoint: graphqlSubscriptionsPath, |
| 158 | + // }, |
| 159 | + }) |
| 160 | + |
| 161 | + // Start server |
| 162 | + const httpServer = http.createServer(app) |
| 163 | + httpServer.setTimeout(options.timeout) |
| 164 | + server.installSubscriptionHandlers(httpServer) |
| 165 | + |
| 166 | + httpServer.listen({ |
| 167 | + host: options.host || 'localhost', |
| 168 | + port: options.port |
| 169 | + }, () => { |
| 170 | + if (!options.quiet) { |
| 171 | + console.log(`✔️ GraphQL Server is running on ${chalk.cyan(`http://localhost:${options.port}${options.graphqlPath}`)}`) |
| 172 | + if (process.env.NODE_ENV !== 'production' && !process.env.VUE_CLI_API_MODE) { |
| 173 | + console.log(`✔️ Type ${chalk.cyan('rs')} to restart the server`) |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + cb && cb() |
| 178 | + }) |
| 179 | + |
| 180 | + // added in order to let vue cli to deal with the http upgrade request |
| 181 | + return { |
| 182 | + apolloServer: server, |
| 183 | + httpServer |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +function load (file) { |
| 188 | + const module = require(file) |
| 189 | + if (module.default) { |
| 190 | + return module.default |
| 191 | + } |
| 192 | + return module |
| 193 | +} |
| 194 | + |
| 195 | +function processSchema (typeDefs) { |
| 196 | + if (Array.isArray(typeDefs)) { |
| 197 | + return typeDefs.map(processSchema) |
| 198 | + } |
| 199 | + |
| 200 | + if (typeof typeDefs === 'string') { |
| 201 | + // Convert schema to AST |
| 202 | + typeDefs = gql(typeDefs) |
| 203 | + } |
| 204 | + |
| 205 | + // Remove upload scalar (it's already included in Apollo Server) |
| 206 | + removeFromSchema(typeDefs, 'ScalarTypeDefinition', 'Upload') |
| 207 | + |
| 208 | + return typeDefs |
| 209 | +} |
| 210 | + |
| 211 | +function removeFromSchema (document, kind, name) { |
| 212 | + const definitions = document.definitions |
| 213 | + const index = definitions.findIndex( |
| 214 | + def => def.kind === kind && def.name.kind === 'Name' && def.name.value === name |
| 215 | + ) |
| 216 | + if (index !== -1) { |
| 217 | + definitions.splice(index, 1) |
| 218 | + } |
| 219 | +} |
0 commit comments