@coko/server
Version:
Reusable server for use by Coko's projects
207 lines (166 loc) • 5.89 kB
JavaScript
const express = require('express')
const { promisify } = require('util')
const http = require('http')
const config = require('config')
const passport = require('passport')
const cookieParser = require('cookie-parser')
const helmet = require('helmet')
const morgan = require('morgan')
const logger = require('./logger')
const { logInit, logTask, logTaskItem } = require('./logger/internals')
const { db, migrationManager } = require('./db')
const { startJobManager, stopJobManager } = require('./jobManager')
const authentication = require('./authentication')
const healthcheck = require('./healthcheck')
const setupGraphqlServer = require('./graphql/setup')
const subscriptionManager = require('./graphql/pubsub')
const seedGlobalTeams = require('./startup/seedGlobalTeams')
const ensureTempFolderExists = require('./startup/ensureTempFolderExists')
const checkConfig = require('./startup/checkConfig')
const errorStatuses = require('./startup/errorStatuses')
const mountStatic = require('./startup/static')
const registerComponents = require('./startup/registerComponents')
const cors = require('./startup/cors')
const { checkConnections } = require('./startup/checkConnections')
const dbConnectionReporter = require('./startup/dbConnectionReporter')
const {
runCustomStartupScripts,
runCustomShutdownScripts,
} = require('./startup/customScripts')
let server
let useGraphQLServer = true
if (
config.has('useGraphQLServer') &&
config.get('useGraphQLServer') === false
) {
useGraphQLServer = false
}
const startServer = async () => {
if (server) return server
const startTime = performance.now()
logInit('Coko server init tasks')
checkConfig(config)
await ensureTempFolderExists()
await checkConnections()
await migrationManager.migrate()
await seedGlobalTeams()
await runCustomStartupScripts()
const app = express()
const port = config.port || 3000
app.set('port', port)
const httpServer = http.createServer(app)
httpServer.app = app
logTask(`Starting HTTP server`)
const startListening = promisify(httpServer.listen).bind(httpServer)
await startListening(port)
logTaskItem(`App is listening on port ${port}`)
app.use(express.json({ limit: '50mb' }))
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
/**
* Perhaps in the future, we can add a config option to make this 'same-site'
* in some cases. (eg. client running at myapp.com and server running at
* server.myapp.com can use a stricter 'same-site' policy without issues.)
* Or maybe someone is not mounting static folders at all and they want to
* restrict even further to 'same-origin'.
*/
let helmetConfig = { crossOriginResourcePolicy: { policy: 'cross-origin' } }
/**
* This makes apollo explorer work in development
* See https://docs.nestjs.com/security/helmet#use-with-express-default
*/
if (process.env.NODE_ENV === 'development' && useGraphQLServer) {
helmetConfig = {
...helmetConfig,
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
imgSrc: [
`'self'`,
'data:',
'apollo-server-landing-page.cdn.apollographql.com',
],
scriptSrc: [`'self'`, `https: 'unsafe-inline'`],
manifestSrc: [
`'self'`,
'apollo-server-landing-page.cdn.apollographql.com',
],
frameSrc: [`'self'`, 'sandbox.embed.apollographql.com'],
},
},
}
}
app.use(helmet(helmetConfig))
app.use(cors())
morgan.token('graphql', ({ body }, res, type) => {
if (!body.operationName) return ''
switch (type) {
case 'query':
return body.query.replace(/\s+/g, ' ')
case 'variables':
return JSON.stringify(body.variables)
case 'operation':
default:
return body.operationName
}
})
app.use(
morgan(
(config.has('morganLogFormat') && config.get('morganLogFormat')) ||
'combined',
{
stream: logger.stream,
},
),
)
app.use(passport.initialize())
passport.use('bearer', authentication.strategies.bearer)
passport.use('anonymous', authentication.strategies.anonymous)
passport.use('local', authentication.strategies.local)
app.get('/healthcheck', healthcheck)
mountStatic(app)
registerComponents(app)
errorStatuses(app)
if (useGraphQLServer) await setupGraphqlServer(httpServer, app, passport)
await startJobManager()
server = httpServer
const endTime = performance.now()
const durationInSeconds = (endTime - startTime) / 1000 // Convert to seconds
logInit(
`Coko server init finished in ${durationInSeconds.toFixed(4)} seconds`,
)
dbConnectionReporter()
return httpServer
}
const shutdownFn = async () => {
await runCustomShutdownScripts()
logTask('Shut down http server')
await server.close()
server = undefined
logTaskItem('Http server successfully shut down')
await stopJobManager({ destroy: true })
if (useGraphQLServer) {
logTask('Shut down subscription client')
await subscriptionManager.client.end()
logTaskItem('Subscription client successfully shut down')
}
logTask('Shut down database connection')
await db.destroy()
logTaskItem('Database connection successfully shut down')
}
const shutdown = async signal => {
logInit(`Coko server graceful shutdown after receiving signal ${signal}`)
const startTime = performance.now()
await shutdownFn()
const endTime = performance.now()
const durationInSeconds = (endTime - startTime) / 1000 // Convert to seconds
logInit(
`Coko server graceful shutdown finished in ${durationInSeconds.toFixed(
4,
)} seconds`,
)
process.exit()
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
module.exports = { startServer, shutdownFn }