forked from mansona/express-autoroute-json
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
185 lines (161 loc) · 5.24 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/**
* @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
}, {})
}