apollographql / apollo-server

🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.
https://www.apollographql.com/docs/apollo-server/
MIT License
13.75k stars 2.03k forks source link

How to update schema on Apollo Server using Webpack's Hot Reload? #2481

Closed spyshower closed 5 years ago

spyshower commented 5 years ago

On Webpack, I see this:

[HMR] Updated modules:
[HMR]  - ./src/resolvers.js
[HMR] Update applied.

But nothing is updated. I have tried some other solutions I found online regarding webpack's output.publicPath. I have no idea what else to do and soon I am going to production. Restarting the server is not an option for me.

My code:


import express from 'express'
import { execute, subscribe } from 'graphql';
import { ApolloServer } from 'apollo-server-express'

import schema from './schema';

import typeDefs from './typeDefs'
import resolvers from './resolvers'

import getUserByToken from './getUserByToken';

const app = express();

const path = '/graphql';

const server = new ApolloServer({ typeDefs, resolvers, });

server.applyMiddleware({ app });

server.listen(8081, () => {
  console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
  console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`)
})

let currentApp = app

if (module.hot) {
    module.hot.accept(['./index', './resolvers'], () => {
        server.removeListener('request', currentApp);
        server.on('request', app);
        currentApp = app;
    });
}

I checked out https://github.com/apollographql/apollo-server/issues/1275 but found no solution.

DevNebulae commented 5 years ago

Have you read this article (you may need to read this in Incognito Mode)? https://medium.freecodecamp.org/build-an-apollo-graphql-server-with-typescript-and-webpack-hot-module-replacement-hmr-3c339d05184f

This worked for me nicely, but you need to have your Webpack process running in one terminal tab and executing the JS code in a secondary terminal tab.

spyshower commented 5 years ago

@DevNebulae I managed to get it running via another way!

DevNebulae commented 5 years ago

@spyshower For documentation's sake, could you please post your resolution? Don't be like DenverCoder9.

spyshower commented 5 years ago

Deleted conjecture.

resolvers.js

const resolvers = {

  Subscription: {
    ...
  },

  Query: {
    ..
  }

}

export default resolvers

typeDefs.js

const typeDefs = gql`

  type Subscription {
    addUser(name: String!): ...    
  }

export default typeDefs

index.js

import http from 'http';
import express from 'express'
const requestIp = require('request-ip');

import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'

const typeDefs = require('./typeDefs').default
const resolvers = require('./resolvers').default

import getUserByToken from './getUserByToken';

const app = express();

const PORT = 8081;

app.use(requestIp.mw())

const server = new ApolloServer({ schema,
    subscriptions: {

        onConnect: (connectionParams, webSocket) => {
            console.log("onConnect server")
            if (connectionParams.authToken) {
                return getUserByToken(connectionParams.authToken, "user")
                    .then(user => {
                        // console.log('then -> ' + user.id)
                        return {
                            currentUser: user.dataValues,
                        };
                    })
                    .catch(error => {
                        console.log('error index.js server onConnect auth: ' + error)
                    });
            }

            throw new Error('Missing auth token!');
        }
    },
    context: ({ req }) => {

        // const ip = req.clientIp;
        // console.log('ip: ' + ip)
        req.headers.ip = req.clientIp

        return req.headers
    },
});

// THE MAGIC HAPPENS HERE <3
app.use('/graphql', (req, res, next) => {

        const typeDefs = require('./typeDefs').default
        const resolvers = require('./resolvers').default

        const schema = makeExecutableSchema({ typeDefs, resolvers })
        // console.log(schema)
    server.schema = schema
    next();
})

server.applyMiddleware({ app });

const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);
httpServer.listen(PORT, () => {
  console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`)
  console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`)
})

let currentApp = app

if (module.hot) {

// add whatever file you wanna watch 
    module.hot.accept(['./index', './typeDefs', './resolvers'], () => {
        httpServer.removeListener('request', currentApp);
        httpServer.on('request', app);
        currentApp = app;
    });
}

The typeDefs/resolvers files are incomplete of course.

MartinPham commented 5 years ago

I was searching the same problem and arrived here. After some minutes of researching, reading code, I came up with a cleaner solution:

import http from 'http';
import express from 'express';
import console from 'chalk-console';
import { ApolloServer } from 'apollo-server-express';

import { LocalStorage } from 'node-localstorage';
import { PubSub } from 'apollo-server';

const localStorage = new LocalStorage('./data');
const pubsub = new PubSub();

const typeDefs = require('./schemas').default;
const resolvers = require('./resolvers').default(localStorage, pubsub);

const PORT = process.env.PORT || 8081;

const configureHttpServer = (httpServer) => {
  console.info('Creating Express app');
  const expressApp = express();

  console.info('Creating Apollo server');
  const apolloServer = new ApolloServer({
    typeDefs,
    resolvers
  });

  apolloServer.applyMiddleware({ 
    app: expressApp 
  });

  console.info('Express app created with Apollo middleware');

  httpServer.on('request', expressApp);
  apolloServer.installSubscriptionHandlers(httpServer);
}

if(!process.httpServer)
{
  console.info('Creating HTTP server');

  process.httpServer = http.createServer();

  configureHttpServer(process.httpServer);

  process.httpServer.listen(PORT, () => {
    console.info(`HTTP server ready at http://localhost:${PORT}`);
    console.info(`Websocket server ready at ws://localhost:${PORT}`);
  });
} else {
  console.info('Reloading HTTP server');
  process.httpServer.removeAllListeners('upgrade');
  process.httpServer.removeAllListeners('request');

  configureHttpServer(process.httpServer);

  console.info('HTTP server reloaded');
}

if (module.hot) {
  module.hot.accept();
}
spyshower commented 5 years ago

Init a new Express & Apollo & WS every time

Are you sure about that? Seems overkill

MartinPham commented 5 years ago

Init a new Express & Apollo & WS every time

Are you sure about that? Seems overkill

I think it should be better than calling makeExecutableSchema every request. Here I just do it every time we reload.

spyshower commented 5 years ago

@MartinPham Gonna try that, since my approach doesn't work for apollo-server>=2.6.6

MartinPham commented 5 years ago

@MartinPham Gonna try that, since my approach doesn't work for apollo-server>=2.6.6

Let me know :) I was using the latest version btw

MartinPham commented 5 years ago

@spyshower I've used here https://github.com/MartinPham/graphql-todo/blob/master/graphql/src/index.js

castengo commented 4 years ago

Thanks @MartinPham ! I used your solution but instead of setting the httpServer on the process, I just reloaded if the file where I have the schemas changed.

const httpServer = http.createServer()
configureHttpServer(httpServer)
httpServer.listen({ port: PORT }, () => console.log(`🚀  Server ready`))

if (module.hot) {
    module.hot.accept(['./schemas'], () => {
      console.info('Reloading HTTP server');
      httpServer.removeAllListeners('request')
      configureHttpServer(httpServer)
    })
}
MartinPham commented 4 years ago

Thanks @MartinPham ! I used your solution but instead of setting the httpServer on the process, I just reloaded if the file where I have the schemas changed.

const httpServer = http.createServer()
configureHttpServer(httpServer)
httpServer.listen({ port: PORT }, () => console.log(`🚀  Server ready`))

if (module.hot) {
    module.hot.accept(['./schemas'], () => {
      console.info('Reloading HTTP server');
      httpServer.removeAllListeners('request')
      configureHttpServer(httpServer)
    })
}

but probably you'd need to reload the resolvers also?

castengo commented 4 years ago

Thanks @MartinPham ! I used your solution but instead of setting the httpServer on the process, I just reloaded if the file where I have the schemas changed.

const httpServer = http.createServer()
configureHttpServer(httpServer)
httpServer.listen({ port: PORT }, () => console.log(`🚀  Server ready`))

if (module.hot) {
    module.hot.accept(['./schemas'], () => {
      console.info('Reloading HTTP server');
      httpServer.removeAllListeners('request')
      configureHttpServer(httpServer)
    })
}

but probably you'd need to reload the resolvers also?

Yea, sorry, I didn't put all the code in the response but this is what our configureHttpServer looks like:

const configureHttpServer = async (httpServer: http.Server): Promise<void> => {
  const app = express()

  const schema = await allSchemas()
  const server = new ApolloServer({
    schema
  } as Config)

  server.applyMiddleware({ app, path: '/' })
  httpServer.on('request', app)
}

We use schema stitching and the allSchemas() function returns a graphQL schema.

akvsh-r commented 4 years ago

Can you please help? @MartinPham Getting Following:

[HMR]  - ./schema.ts
[HMR]  - ./express.ts
[HMR] Update applied.
events.js:187
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE: address already in use :::8081
Cryoware commented 4 years ago

Hi not sure if you are still having issues, but this would of helped me sooner if it was answered. Make sure to do a server.stop())

if (module.hot) {
  module.hot.accept();
  module.hot.dispose(() => server.stop());
}

Can you please help? @MartinPham Getting Following:

[HMR]  - ./schema.ts
[HMR]  - ./express.ts
[HMR] Update applied.
events.js:187
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE: address already in use :::8081
sanderkooger commented 4 years ago

@MartinPham,

I am a beginning Dev, but advanced sysadmin. I'm using this setup to learn about GQL.

I have the app up and running in webpack, But every reload fails on:

[22:06:22] [INFO]  �🧦 Websocket server ready at ws://localhost:3000/graphq
internal/process/per_thread.js:180
        throw new ERR_UNKNOWN_SIGNAL(sig);
        ^

TypeError [ERR_UNKNOWN_SIGNAL]: Unknown signal: SIGUSR2
    at StartServerPlugin.afterEmit (D:\1. Repos\tif-graphql-api-server\node_modules\start-server-webpack-plugin\dist\StartServerPlugin.js:85:17)    
    at AsyncSeriesHook.eval [as callAsync] (eval at create (D:\1. Repos\tif-graphql-api-server\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:7:1)
    at asyncLib.forEachLimit.err (D:\1. Repos\tif-graphql-api-server\node_modules\webpack\lib\Compiler.js:482:27)
    at D:\1. Repos\tif-graphql-api-server\node_modules\neo-async\async.js:2818:7
    at done (D:\1. Repos\tif-graphql-api-server\node_modules\neo-async\async.js:3522:9)
    at AsyncSeriesHook.eval [as callAsync] (eval at create (D:\1. Repos\tif-graphql-api-server\node_modules\tapable\lib\HookCodeFactory.js:33:10), <anonymous>:6:1)
    at outputFileSystem.writeFile.err (D:\1. Repos\tif-graphql-api-server\node_modules\webpack\lib\Compiler.js:464:33)
    at D:\1. Repos\tif-graphql-api-server\node_modules\graceful-fs\graceful-fs.js:57:14
    at FSReqCallback.args [as oncomplete] (fs.js:145:20)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

My Index.js

require('dotenv').config()
import http from 'http'
import express from 'express'
import console from 'chalk-console'
import { gql, ApolloServer } from 'apollo-server-express'
const books = require('./domains/books')
const magazines = require('./domains/magazines')

import { LocalStorage } from 'node-localstorage'
import { PubSub } from 'apollo-server-express'

const localStorage = new LocalStorage('./data')
const pubsub = new PubSub()

//const typeDefs = require('./schemas').default
//const resolvers = require('./resolvers').default(localStorage, pubsub)

const typeDef = gql`
  type Query
`

var PORT = process.env.PORT || 4000

const configureHttpServer = (httpServer) => {
  console.info('Creating Express app')
  const expressApp = express()

  console.info('Creating Apollo server')
  const apolloServer = new ApolloServer({
    typeDefs: [typeDef, books.typeDef, magazines.typeDef],
    resolvers: [books.resolvers, magazines.resolvers],
    playground: process.env.NODE_ENV !== 'production'
  })

  apolloServer.applyMiddleware({
    app: expressApp
  })

  console.info('Express app created with Apollo middleware')

  httpServer.on('request', expressApp)
  apolloServer.installSubscriptionHandlers(httpServer)
}

if (!process.httpServer) {
  console.info('Creating HTTP server')

  process.httpServer = http.createServer()

  configureHttpServer(process.httpServer)

  process.httpServer.listen(PORT, () => {
    console.info(` 🚀 HTTP server ready at http://localhost:${PORT}/graphql  !`)
    console.info(` 🧦 Websocket server ready at ws://localhost:${PORT}/graphql !`)
  })
} else {
  console.info('⌛ Reloading HTTP server')
  process.httpServer.removeAllListeners('upgrade')
  process.httpServer.removeAllListeners('request')

  configureHttpServer(process.httpServer)

  console.info('👍 HTTP server reloaded')
}

if (module.hot) {
  module.hot.accept()
  module.hot.dispose(() => server.stop())
}

My package.json:

{
  "name": "tif-graphql-api-server",
  "version": "0.0.1",
  "description": "GRaphQL server for This Is Fashion apps",
  "main": "index.js",
  "repository": "https://gitlab.com/thisisfashion/tif-graphql-api-server.git",
  "author": "Sander Kooger <sander@thisisfashion.tv>",
  "license": "UNLICENCE",
  "private": true,
  "scripts": {
    "build": "webpack --config webpack.production.js",
    "dev": "webpack --config webpack.development.js",
    "start": "node -r esm src",
    "start:webpack": "node dist/server.js",
    "lint": "eslint \"./**/*.{js,jsx,json}\" ",
    "prettier": "prettier --write \"./**/*.{js,jsx,json,css}\""
  },
  "husky": {
    "hooks": {
      "pre-commit": "cross-env lint-staged",
      "pre-push": "cross-env lint-staged"
    }
  },
  "lint-staged": {
    "*.{css,js,jsx,json}": [
      "prettier --write"
    ]
  },
  "engines": {
    "node": ">=6"
  },
  "dependencies": {
    "cross-env": "^7.0.2",
    "dotenv": "^8.2.0",
    "apollo-server-express": "^2.12.0",
    "body-parser": "^1.19.0",
    "chalk-console": "^1.1.0",
    "cross-env": "^7.0.2",
    "dotenv": "^8.2.0",
    "esm": "^3.2.25",
    "express": "^4.17.1",
    "graphql": "^15.0.0",
    "graphql-tools": "^5.0.0",
    "http": "^0.0.1-security",
    "node-localstorage": "^2.1.6"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "husky": "^4.2.5",
    "lint-staged": "^10.1.3",
    "prettier": "^2.0.5",
    "start-server-webpack-plugin": "^2.2.5",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-merge": "^4.2.2",
    "webpack-node-externals": "^1.7.2"
  }
}

I was building this boilerplate for internal user in an application later. But i have an idea. I think a lot of people are looking for an apollo-server that does:

HMR Domain based schema building (EG a ./src/domain directory that contains different folders per datatype you need resolvers etc for) getUserByToken (Have data that the server spits out without/ and data only visible for authenticated users. ) @spyshower thank you for the inspiration.

Maybe its a good idea to build something like this, Kind of like NextJs for GQL? I am more than willing to contribute, and use it in production / Help out with documentation.

I'm even willing to be the Idiot, so we can make it idiot-proof ;)