jsonapi-server-mini
Version:
Minimalistic JSON:API server for Node.js and MongoDB
186 lines (161 loc) • 5.37 kB
JavaScript
/**
* @author Sander Steenhuis <info@redsandro.com> (https://www.Redsandro.com)
* @license MIT
*/
const routeFactory = require('./lib/routeFactory')
const path = require('path')
const recursive = require('recursive-readdir')
const bodyParser = require('body-parser')
const cors = require('cors')
const morgan = require('morgan')
const JSONAPIError = require('jsonapi-serializer').Error
const methodMap = {
post : 'C ',
get : ' R ',
patch : ' U ',
delete : ' D'
}
const testEnv = process.env.NODE_ENV == 'test'
const devEnv = process.env.NODE_ENV == 'development'
const prodEnv = process.env.NODE_ENV == 'production'
global.get = (obj, path, fallback) => path.split('.').every(el => ((obj = obj[el]) !== undefined)) ? obj : fallback
module.exports = async(args = {}) => {
const logger = getLogger(args)
const mongoose = await getMongoose(args)
const app = getApp(args)
const routes = getRoutes(args)
const files = await recursive(routes, [(file, stats) => stats.isFile() && file.substr(-3) !== '.js'])
files.forEach(file => {
const prefix = path.dirname(file.replace(routes, '')).replace(/^\/$/, '')
const type = path.basename(file, '.js').replace(/^\w/, c => c.toUpperCase())
const route = require(file)({mongoose})
const model = mongoose.model(type, route.schema)
const relationships = getRefs(route.schema)
const options = { model, args, relationships, ...route }
const crud = routeFactory(options)
let indexes = route.indexes
let idxOpts = {}
for (const method in crud) {
for (const path in crud[method]) {
app[method].apply(app, [`${prefix}${path}`].concat(crud[method][path]))
logger.verbose(`Added [${methodMap[method]}] ${prefix}${path}`)
}
}
/**
* While nice for development, it is recommended this behavior be disabled in production.
* @see https://mongoosejs.com/docs/guide.html#indexes
* @todo make indexing configurable
*/
if (indexes) {
if (!Array.isArray(indexes)) indexes = [indexes]
indexes.forEach(index => {
if (Array.isArray(index)) [index, idxOpts] = index
route.schema.index(index, idxOpts)
})
model.createIndexes(err => err && logger.error(err.message))
}
})
// Path does not exist
app.use((req, res, next) => {
if (!res.headersSent) {
res.status(404).json(new JSONAPIError({
status : '404',
title : 'Not Found',
detail : `Cannot ${req.method} ${req.url}`,
source : {
pointer : req.originalUrl
}
}))
}
next()
})
// An error was thrown
app.all((err, req, res, next) => {
if (res.headersSent) return next(err)
res.status(500).json(new JSONAPIError({
status : '500',
title : 'Internal Server Error',
details : err.message,
source : {
pointer : req.originalUrl
}
}))
})
if (!args.app) app.listen(args.port || 8888)
return app
}
/**
* Set up default logger
* Make sure to do this first, as other functions will use the logger.
*/
function getLogger(args) {
if (!args.logger || typeof get(args, 'logger.debug') !== 'function' || typeof get(args, 'logger.info') !== 'function') {
args.logger = require('winston')
const level = testEnv ? 'emerg' : prodEnv ? 'info' : devEnv ? 'debug' : 'silly'
args.logger.add(new args.logger.transports.Console({level}))
args.logger.verbose('Added default logger. You can specify your own winston instance using the `logger` attribute.')
}
return args.logger
}
/**
* Set up default mongoose
*/
async function getMongoose(args) {
if (!args.mongoose) {
args.mongoose = require('mongoose')
args.logger.verbose('Added default mongoose. You can specify your own mongoose instance using the `mongoose` attribute.')
}
while (args.mongoose.connection.readyState !== 1)
await connectMongoose(args)
return args.mongoose
}
async function connectMongoose(args, retryDelay = 1) {
try {
await args.mongoose.connect(args.mongoUri || 'mongodb://localhost/jsonapi-server-mini', { useNewUrlParser: true })
return args.mongoose
}
catch(err) {
args.logger.error(`Mongoose: ${err.message}`)
args.logger.warn(`Connection to mongoose failed. Retrying in ${retryDelay} seconds.`)
await new Promise(res => setTimeout(res, retryDelay * 1000))
if (retryDelay < 60) retryDelay *= 2
return await connectMongoose(args, retryDelay)
}
}
/**
* Set up default express when none was provided
*/
function getApp(args) {
if (!args.app) {
args.logger.verbose('Added default express.')
return require('express')()
.use(testEnv ? (req, res, next) => next() : morgan('dev'))
.use(cors())
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json({type: 'application/vnd.api+json'}))
}
return args.app
}
/**
* Set up default routes for testing purposes
* Or just because it's cool if the app works with zero configuration
*/
function getRoutes(args) {
if (!args.routes) {
return path.join(__dirname, 'routes')
}
return args.routes
}
/**
* Get refs/relationships from schema, so that we can have custom id's like Strings
* @param {Schema} schema
* @return {Object} Map with relationshipName:idType
*/
function getRefs(schema) {
return Object.entries(schema.paths).reduce((acc, [key, val]) => {
const options = get(val, 'options', {})
const type = get(options, 'ref') || get(options, 'type.0.ref')
if (type) acc[key] = type
return acc
}, {})
}