UNPKG

6.85 kBJavaScriptView Raw
1const { Router } = require('express')
2const _ = require('lodash')
3const path = require('path')
4const glob = require('glob')
5const numParser = require('num-parser')
6
7const mainRouter = Router()
8const routes = require(`${process.cwd()}/config/routes`)
9const {
10 controllerPath
11} = require('../constants')
12const {
13 logger
14} = require('./middleware')
15
16// Keep a reference of project controllers.
17let controllers = {}
18_.each(glob.sync(`${process.cwd()}/${controllerPath}/*`), file => {
19 let name = path.basename(file, '.js')
20 // Require each controller file and keep reference as its file name.
21 let Controller = require(file)
22 controllers[name] = new Controller
23})
24
25let genResourceRoutes = root => {
26 return [
27 [root, [
28 [`get /`, 'fetchAll'],
29 [`get /:id`, 'fetch'],
30 [`post /`, 'create'],
31 [`patch /:id`, 'update'],
32 [`put /:id`, 'replace'],
33 [`delete /:id`, 'destroy']
34 ]]
35 ]
36}
37
38let buildRoute = (router, verb, url, to, root) => {
39 // Check how action function is referenced.
40 let cName, action
41 if(/.+#.+/.test(to))
42 // If '#' present, it splits the controller name and the action method name.
43 [cName, action] = to.split('#')
44 else
45 // Otherwise, controller name is the root path or first section of the URL as
46 // the controller name and `to` as the action method name.
47 [cName, action] = [(root || url.split('/')[0]), to]
48
49 // Set controller to be use.
50 let controller
51 // Controller name must not be null and not be defined as a URL parameter.
52 if(!cName || /:.+/.test(cName))
53 throw new Error('A controller must be specified.')
54 else {
55 // Controllers should be defined with '_controller' attached to the end of the file name.
56 let fileName = `${cName}_controller`,
57 className = fileName
58 .replace(/^[a-z]/, c => c.toUpperCase())
59 .replace(/_([a-z])/g, (m, c) => c.toUpperCase())
60 controller = controllers[fileName]
61 if(!controller)
62 throw new Error(`${className} does not exist for route "${path}".`)
63 }
64
65 // Arguments to be passed in route initilizaion function.
66 let args = [`/${url.replace(/^(\/)/, '')}`, logger]
67 // Add any before filters to the middleware stack.
68 args = args.concat(getFilterMethods('before', controller, action))
69 // Parse sent variables if they are numbers.
70 args.push(numParser)
71 // Add the main action method and after filters.
72 args.push((req, res) => {
73 // Redefine `res.json` to call before filters and proxy call original `res.json`
74 // response method.
75 let jsonProxy = res.json.bind(res)
76 res.json = body => {
77 // Allow each after filter to manipulate the reponse in respective order.
78 _.each(getFilterMethods('after', controller, action), afterFilter => {
79 try {
80 let response = afterFilter(req, body)
81 body = response || body
82 } catch(err) {
83 jsonProxy({
84 'Error': err.mess
85 })
86 }
87 })
88 jsonProxy(body)
89 }
90
91 // Check the existance of the action method. Else return error response.
92 let mainAction = controller[action]
93 if(mainAction) {
94 mainAction.call(controller, req, res)
95 } else {
96 jsonProxy('Action does not exist.').status(404)
97 }
98 })
99 router[verb].apply(router, args)
100}
101let buildRoutes = (router, routes, root) => {
102 // Loop each route defined.
103 // NOTE: Routes must be defined in an array for proper initilizaion order.
104 _.each(routes, route => {
105 let [ path, toAction ] = route
106
107 // If `toAction` is an Array, expect it to be subroutes.
108 if(Array.isArray(toAction)) {
109 let baseRoot
110 if(root) {
111 baseRoot = root
112 } else {
113 if(!/^\w+$/.test(path))
114 throw new Error('Routes that contain subroutes must only be described by a single word.')
115 baseRoot = path
116 }
117
118 // Build subroutes.
119 let subRouter = Router({mergeParams: true})
120 buildRoutes(subRouter, toAction, baseRoot)
121
122 // Attach subrouter to parent router.
123 router.use(`/${path}`, subRouter)
124 } else {
125 let [ verb, url ] = path.split(' ')
126
127 if(verb === 'resources') {
128 if(!/^\w+$/.test(url))
129 throw new Error(`Resource routes must only be described by a single word. ("${url}")`)
130
131 // Build all resource routes.
132 buildRoutes(router, genResourceRoutes(url), root)
133 } else {
134 if(!toAction)
135 throw new Error(`No action method or subroutes are defined for route "${path}". Please check proper route definitions.`)
136
137 // Build single route.
138 buildRoute(router, verb, url, toAction, root)
139 }
140 }
141 })
142}
143// Initilize routes.
144buildRoutes(mainRouter, routes)
145
146module.exports = mainRouter
147
148// Get list of filter methods to be applied to a specific action.
149function getFilterMethods(type, controller, action) {
150 // Get all filters for the action.
151 let filters = getFilters(type, controller, action)
152 // Get filter method names to be skipped.
153 type = `skip${type[0].toUpperCase() + type.slice(1)}`
154 let skipFilterMethods = _.map(getFilters(type, controller, action), 'action')
155
156 // Remove filters that should be skipped.
157 filters = _.filter(filters, f => {
158 for(let method of skipFilterMethods)
159 if(method === f.action)
160 return false
161 return true
162 })
163
164 // Return final list of filter methods.
165 return _.map(filters, f => {
166 let { action } = f
167 action = typeof action === 'string' ? controller[action] : action
168 return action.bind(controller)
169 })
170}
171
172// Get list of filters based on the type name to be applied to a specific action.
173function getFilters(type, controller, action) {
174 // User can set 'only' and 'except' rules on filters to be used on one
175 // or many action methods.
176 let useOptions = { only: true, except: false }
177
178 // Only return action filter methods that apply.
179 return _.filter(controller[`__${type}Filters`], filter => {
180 // An action must be defined in each filter.
181 if(!('action' in filter) || !filter.action)
182 throw new Error(`No action defined in filter for [${controller.constructor.name}.${type}: ${JSON.stringify(filter)}].`)
183
184 // Filter cannot contain both options.
185 let containsBoth = _.every(_.keys(useOptions), o => o in filter)
186 if(containsBoth)
187 throw new Error(`${type[0].toUpperCase() + type.slice(1)} filter cannot have both \'only\' and \'except\' keys.`)
188
189 let option = _.first(_.intersection(_.keys(useOptions), _.keys(filter)))
190 // If no option is defined, use for all actions.
191 if(!option)
192 return true
193
194 let useActions, use = useOptions[option]
195 useActions = typeof (useActions = filter[option]) === 'string' ? [useActions] : useActions
196 // Determine if filter can be used for this action.
197 for(let ua of useActions)
198 if(ua === action)
199 return use
200
201 return !use
202 })
203}