Skip to content

Commit

Permalink
feat: apollo server plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Chau committed Aug 19, 2019
1 parent 69e2e4b commit 1adc07a
Show file tree
Hide file tree
Showing 14 changed files with 1,006 additions and 40 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"eslint-plugin-vue-libs": "^3.0.0",
"execa": "^2.0.0",
"globby": "^8.0.1",
"graphql": "^14.4.2",
"graphql-tag": "^2.10.1",
"inquirer": "^6.2.0",
"lerna": "^3.4.3",
"minimist": "^1.2.0",
Expand Down
15 changes: 15 additions & 0 deletions packages/@nodepack/cli/src/lib/createModules/apollo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/** @type {import('../ProjectCreateJob').CreateModule} */
module.exports = api => {
api.injectFeature({
name: 'Apollo (needs Express)',
value: 'apollo',
// @ts-ignore
description: 'GraphQL Server',
link: 'https://www.apollographql.com/',
})

api.onPromptComplete((answers, preset) => {
// @ts-ignore
preset.plugins['@nodepack/plugin-apollo'] = ''
})
}
1 change: 1 addition & 0 deletions packages/@nodepack/cli/src/lib/createModules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ exports.getPromptModules = () => {
'babel',
'typescript',
'express',
'apollo',
].map(file => require(`./${file}`))
}
44 changes: 44 additions & 0 deletions packages/@nodepack/plugin-apollo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@nodepack/plugin-apollo",
"version": "0.4.2",
"description": "Nodepack plugin for Apollo Server (GraphQL)",
"author": "Guillaume Chau <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Akryum/nodepack.git"
},
"bugs": {
"url": "https://github.com/Akryum/nodepack/issues"
},
"homepage": "https://github.com/Akryum/nodepack#readme",
"publishConfig": {
"access": "public"
},
"main": "src/index.js",
"typings": "types/index.d.ts",
"scripts": {
"test": "yarn test:lint",
"test:lint": "eslint src"
},
"dependencies": {
"@types/express": "^4.11.1",
"apollo-server-express": "2.8.1",
"graphql-playground-middleware-express": "^1.7.12",
"graphql-deduplicator": "^2.0.5",
"lodash": "^4.17.15",
"mock-express-response": "^0.2.2",
"subscriptions-transport-ws": "^0.9.16"
},
"devDependencies": {
},
"peerDependencies": {
"@nodepack/app-context": "^0.4.0",
"@nodepack/service": "^0.4.0",
"@nodepack/utils": "^0.4.0",
"graphql": "^14.4.2"
},
"optionalDependencies": {
"graphql-tag": "^2.10.1"
}
}
36 changes: 36 additions & 0 deletions packages/@nodepack/plugin-apollo/src/app-migrations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/** @type {import('@nodepack/service').MigrationPlugin} */
module.exports = api => {
const schemaTemplate = `./templates/schema-${api.hasPlugin('typescript') ? 'ts' : 'js'}`

api.register({
id: 'defaultPackage',
title: 'Install dependencies',
up: (api, options) => {
api.extendPackage({
dependencies: {
graphql: '^14.4.2',
'graphql-tag': '^2.10.1',
},
})
},
down: (api, options) => {
api.extendPackage({
dependencies: {
graphql: undefined,
'graphql-tag': undefined,
},
})
},
})

api.register({
id: 'defaultSchema',
title: 'Template: default schema',
up: (api, options) => {
api.render(schemaTemplate, options)
},
down: (api, options) => {
api.unrender(schemaTemplate)
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import gql from 'graphql-tag'

export const typeDefs = gql`
type Query {
hello: String!
}
`

export const resolvers = {
Query: {
hello: (root, args, ctx, info) => `Hello world`,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import gql from 'graphql-tag'
import { IResolvers } from 'graphql-tools'
import { ApolloContext } from '@nodepack/plugin-apollo'

export const typeDefs = gql`
type Query {
hello: String!
}
`

export const resolvers: IResolvers = {
Query: {
hello: (root, args, ctx: ApolloContext, info) => `Hello world`,
},
}
8 changes: 8 additions & 0 deletions packages/@nodepack/plugin-apollo/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('@nodepack/service').ServicePlugin} */
module.exports = (api, options) => {
if (api.hasPlugin('express')) {
api.addRuntimeModule('./runtime/apollo-express.js')
} else {
throw new Error(`You need @nodepack/plugin-express with Apollo. Other options not yet supported.`)
}
}
69 changes: 69 additions & 0 deletions packages/@nodepack/plugin-apollo/src/runtime/apollo-express.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { hook, callHook } from '@nodepack/app-context'
import { loadSchema } from './schema'
import { makeExecutableSchema, ApolloServer } from 'apollo-server-express'
import MockExpressResponse from 'mock-express-response'
import expressPlayground from 'graphql-playground-middleware-express'
import { getApolloConfig, printReady } from './server'

hook('express-http', async (ctx) => {
const { config, express: app, httpServer } = ctx
const apolloConfig = config.apollo || {}

// Load schema
const fullSchema = ctx.schema = await loadSchema(ctx)

await callHook('apollo-schema', ctx)

const localSchema = makeExecutableSchema({
typeDefs: fullSchema.typeDefs,
resolvers: fullSchema.resolvers,
})

// Apollo Server
const server = ctx.server = new ApolloServer(await getApolloConfig({
ctx,
apolloConfig,
localSchema,
onSubConnect: (connection, websocket) => {
return new Promise((resolve, reject) => {
const req = websocket.upgradeReq
// @ts-ignore
app.handle(req, new MockExpressResponse(), (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
})
},
}))

// GraphQL Playground
if (apolloConfig.playground !== false) {
const playgroundRoute = ctx.playgroundRoute =
typeof apolloConfig.playground === 'string' ? apolloConfig.playground : '/playground'
app.get(playgroundRoute, expressPlayground({
endpoint: server.graphqlPath,
subscriptionEndpoint: server.subscriptionsPath,
}))
}

// Express middleware
server.applyMiddleware({
app,
cors: config.cors,
})

// Subscriptions
server.installSubscriptionHandlers(httpServer)

// Listen hook
hook('express-listen', async (ctx) => {
await callHook('apollo-listen', ctx)
})

hook('print-ready', () => {
printReady(ctx)
})
})
10 changes: 10 additions & 0 deletions packages/@nodepack/plugin-apollo/src/runtime/pubsub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PubSub } from 'apollo-server-express'
import events from 'events'

export function createDefaultPubsub () {
const eventEmitter = new events.EventEmitter()
eventEmitter.setMaxListeners(Infinity)
return new PubSub({
eventEmitter,
})
}
54 changes: 54 additions & 0 deletions packages/@nodepack/plugin-apollo/src/runtime/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import merge from 'lodash/merge'

/** @typedef {import('graphql').DocumentNode} DocumentNode */
/** @typedef {import('apollo-server-express').IResolvers} IResolvers */

/**
* @param {any} ctx
*/
export async function loadSchema (ctx) {
/** @type {DocumentNode[]} */
const typeDefs = []
/** @type {DocumentNode[]} */
const internalTypeDefs = []
/** @type {IResolvers} */
const resolvers = {}
/** @type {DocumentNode[]} */
const mergeTypeDefs = []
/** @type {IResolvers} */
const mergeResolvers = {}

const files = require.context('@', true, /^.\/schema\/.*\.[jt]sx?$/)
for (const key of files.keys()) {
let module = files(key)
if (module.default) {
module = module.default
}
if (typeof module === 'function') {
module = await module(ctx)
}
if (module.typeDefs) {
typeDefs.push(module.typeDefs)
}
if (module.internalTypeDefs) {
internalTypeDefs.push(module.internalTypeDefs)
}
if (module.mergeTypeDefs) {
mergeTypeDefs.push(module.mergeTypeDefs)
}
if (module.resolvers) {
merge(resolvers, module.resolvers)
}
if (module.mergeResolvers) {
merge(mergeResolvers, module.mergeResolvers)
}
}

return {
typeDefs,
internalTypeDefs,
resolvers,
mergeTypeDefs,
mergeResolvers,
}
}
63 changes: 63 additions & 0 deletions packages/@nodepack/plugin-apollo/src/runtime/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { callHook, createContext } from '@nodepack/app-context'
import { createDefaultPubsub } from './pubsub'

export async function getApolloConfig ({
ctx,
apolloConfig,
localSchema,
onSubConnect,
}) {
// PubSub for subscriptions
const pubsub = ctx.config.pubsub || createDefaultPubsub()

return {
schema: localSchema,
context: async (context) => {
let user
if (context.req) {
// @ts-ignore
user = context.req.user
} else if (context.connection) {
user = context.connection.context.user
}
const reqCtx = await createContext()
reqCtx.user = user
reqCtx.rawContext = context
reqCtx.req = context.req
reqCtx.res = context.res
reqCtx.connection = context.connection
reqCtx.pubsub = pubsub
await callHook('apollo-request', reqCtx)
return reqCtx
},
tracing: true,
introspection: true,
subscriptions: {
path: '/subscriptions',
/**
* @param {any} connection
*/
onConnect: async (connection, websocket) => {
// @ts-ignore
const req = websocket.upgradeReq
if (onSubConnect) {
await onSubConnect(connection, websocket)
}
return {
...connection.context,
user: req.user,
}
},
},
...apolloConfig.apolloServerOptions || {},
playground: false,
}
}

export function printReady (ctx) {
console.log(`🚀 Server ready at http://localhost:${ctx.port}${ctx.server.graphqlPath}`)
console.log(`⚡ Subs ready at ws://localhost:${ctx.port}${ctx.server.subscriptionsPath}`)
if (ctx.playgroundRoute) {
console.log(`🎮️ Playground ready at http://localhost:${ctx.port}${ctx.playgroundRoute}`)
}
}
22 changes: 22 additions & 0 deletions packages/@nodepack/plugin-apollo/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DocumentNode } from 'graphql'
import { IResolvers, PubSubEngine, ApolloServer } from 'apollo-server-express'
import { ExecutionParams } from 'subscriptions-transport-ws'
import { Request, Response } from 'express'

export interface Schema {
typeDefs: DocumentNode
resolvers: IResolvers
internalTypeDefs: DocumentNode
mergeTypeDefs: DocumentNode
mergeResolvers: IResolvers
}

export interface ApolloContext {
schema: Schema
req: Request
res: Response
connection: ExecutionParams
pubsub: PubSubEngine
user: any
server: ApolloServer
}
Loading

0 comments on commit 1adc07a

Please sign in to comment.