Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
spyshower opened this issue Mar 22, 2019 · 16 comments
Closed

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

spyshower opened this issue Mar 22, 2019 · 16 comments

Comments

@spyshower
Copy link

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 #1275 but found no solution.

@DevNebulae
Copy link

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
Copy link
Author

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

@DevNebulae
Copy link

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

@spyshower
Copy link
Author

spyshower commented Apr 5, 2019

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
Copy link

MartinPham commented Aug 9, 2019

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

  • Keep httpServer between HMR module updating loops
  • Remove Apollo HTTP listener and WS listener & Init a new Express & Apollo & WS every time reload module
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
Copy link
Author

Init a new Express & Apollo & WS every time

Are you sure about that? Seems overkill

@MartinPham
Copy link

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
Copy link
Author

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

@MartinPham
Copy link

@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
Copy link

@castengo
Copy link

castengo commented Jan 6, 2020

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
Copy link

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
Copy link

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
Copy link

akvsh-r commented Feb 10, 2020

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
Copy link

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
Copy link

sanderkooger commented Apr 28, 2020

@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 ;)

@abernix abernix pinned this issue Apr 28, 2020
@abernix abernix unpinned this issue Apr 28, 2020
@apollographql apollographql locked and limited conversation to collaborators Apr 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants