|
1 | 1 | 'use strict' |
2 | 2 |
|
3 | 3 | // setup |
4 | | -const http = require('http') |
5 | | -const Pg = require('pg').Pool |
6 | | -const jsonwebtoken = require('jsonwebtoken') |
7 | | -const tinyParams = require('tiny-params') |
8 | | -const port = process.env.PORT || process.env.port || 8091 |
9 | | -const handlers = { |
10 | | - search: (name, req) => findAll(name, req.params), |
11 | | - create: (name, req) => create(name, req.params), |
12 | | - read: (name, req) => find(name, req.id, req.params), |
13 | | - update: (name, req) => save(name, req.id, req.params), |
14 | | - delete: (name, req) => destroy(name, req.id, req.params) |
15 | | -} |
16 | | -const scrud = { |
17 | | - GET: 'search', |
18 | | - 'GET?': 'search', |
19 | | - POST: 'create', |
20 | | - 'POST/': 'create', |
21 | | - 'GET/': 'read', |
22 | | - 'PUT/': 'update', |
23 | | - 'DELETE/': 'delete' |
24 | | -} |
25 | | -const hasBody = {create: true, update: true} |
26 | | -const wlSign = [ |
27 | | - 'algorithm', |
28 | | - 'expiresIn', |
29 | | - 'notBefore', |
30 | | - 'audience', |
31 | | - 'issuer', |
32 | | - 'jwtid', |
33 | | - 'subject', |
34 | | - 'noTimestamp', |
35 | | - 'header' |
36 | | -] |
37 | | - |
38 | | -// globals |
39 | | -let logger |
40 | | -let pgPool |
41 | | -let jwtOpts |
42 | | -let authTrans |
43 | | -let pgPrefix = '' |
44 | | -let base = '' |
45 | | -let baseRgx = new RegExp(`^/?${base}/`) |
46 | | -let maxBodyBytes = 1e6 |
47 | | -let resources = {} |
48 | | - |
49 | | -// local helpers |
50 | | -const logIt = (e) => typeof logger === 'function' ? logger(e) : console.log(e) |
51 | | - |
52 | | -const cleanPath = (url) => { |
53 | | - return decodeURIComponent(url).replace(baseRgx, '').replace(/\/$/, '') |
54 | | -} |
55 | | - |
56 | | -const parseId = (url) => { |
57 | | - let id = (url.match(/\/(.+?)(\/|\?|$)/) || [])[1] |
58 | | - return (id || '').match(/^\d+$/) ? parseInt(id, 10) : id || null |
59 | | -} |
60 | | - |
61 | | -const callPgFunc = (name, params) => { |
62 | | - let q = `SELECT * FROM ${name}($1);` |
63 | | - if (!pgPool) return Promise.reject(new Error('no database configured')) |
64 | | - return new Promise((resolve, reject) => { |
65 | | - pgPool.connect((err, client, done) => { |
66 | | - if (err) return reject(err) |
67 | | - client.query(q, [params], (err, result) => { |
68 | | - done(err) |
69 | | - if (err) return reject(err) |
70 | | - resolve((result.rows[0] || {})[name] ? result.rows[0][name] : []) |
71 | | - }) |
72 | | - }) |
73 | | - }) |
74 | | -} |
75 | | - |
76 | | -const bodyParse = (req) => new Promise((resolve, reject) => { |
77 | | - let body = '' |
78 | | - req.on('data', (d) => { |
79 | | - body += d.toString() |
80 | | - if (body.length > maxBodyBytes) return reject(new Error('body too large')) |
81 | | - }) |
82 | | - req.on('end', () => resolve(body ? JSON.parse(body) : {})) |
83 | | -}) |
84 | | - |
85 | | -const noIdErr = () => JSON.stringify(new Error('no id passed')) |
86 | | - |
87 | | -const filterObj = (obj, ary) => { |
88 | | - let base = {} |
89 | | - ary.forEach((o) => { base[o] = obj[o] }) |
90 | | - return base |
91 | | -} |
| 4 | +const scrud = require('./scrud') |
92 | 5 |
|
93 | 6 | // exports |
94 | | -module.exports = { |
95 | | - register, |
96 | | - start, |
97 | | - sendData, |
98 | | - sendErr, |
99 | | - fourOhOne, |
100 | | - fourOhFour, |
101 | | - genToken, |
102 | | - authenticate, |
103 | | - find, |
104 | | - findAll, |
105 | | - create, |
106 | | - save, |
107 | | - destroy |
108 | | -} |
109 | | - |
110 | | -// register resource |
111 | | -function register (name, opts = {}) { |
112 | | - if (!name) return Promise.reject(new Error(`no name specified in register`)) |
113 | | - return new Promise((resolve, reject) => { |
114 | | - let r = resources[name] = Object.assign(opts, {name}) |
115 | | - if (Array.isArray(r.skipAuth)) { |
116 | | - let skippers = {} |
117 | | - r.skipAuth.forEach((a) => { skippers[a] = true }) |
118 | | - r.skipAuth = skippers |
119 | | - } |
120 | | - return resolve(r) |
121 | | - }) |
122 | | -} |
123 | | - |
124 | | -// start server |
125 | | -function start (opts = {}) { |
126 | | - if (opts.namespace) pgPrefix = `${opts.namespace.toLowerCase()}_` |
127 | | - if (opts.maxBodyBytes) maxBodyBytes = opts.maxBodyBytes |
128 | | - if (opts.jsonwebtoken) jwtOpts = opts.jsonwebtoken |
129 | | - if (opts.logger) base = opts.logger |
130 | | - if (opts.base) base = opts.base |
131 | | - if (opts.authTrans) authTrans = opts.authTrans |
132 | | - baseRgx = new RegExp(`^/?${base}/`) |
133 | | - return new Promise((resolve, reject) => { |
134 | | - let server = http.createServer(handleRequest) |
135 | | - server.listen(opts.port || port) |
136 | | - if (opts.postgres) pgPool = new Pg(opts.postgres) |
137 | | - return resolve(server) |
138 | | - }) |
139 | | -} |
140 | | - |
141 | | -// request handler |
142 | | -function handleRequest (req, res) { |
143 | | - if (!baseRgx.test(req.url)) return fourOhFour(res) |
144 | | - let url = cleanPath(req.url) |
145 | | - let matches = url.match(/^\/?(.+?)(\/|\?|$)/) || [] |
146 | | - let resource = resources[matches[1] || ''] |
147 | | - let modifier = matches[2] |
148 | | - let action = scrud[`${req.method}${modifier}`] |
149 | | - if (!resource || !action) return fourOhFour(res) |
150 | | - let name = resource.name |
151 | | - res.setHeader('Content-Type', 'application/json') |
152 | | - res.setHeader('SCRUD', `${name}:${action}`) |
153 | | - req.id = parseId(url) |
154 | | - req.params = tinyParams(url) |
155 | | - let headers = req.headers || {} |
156 | | - let connection = req.connection || {} |
157 | | - req.params.ip = headers['x-forwarded-for'] || connection.remoteAddress |
158 | | - req.once('error', (err) => sendErr(res, err)) |
159 | | - let handler = resource[action] || actionHandler |
160 | | - let jwt = (headers.authorization || '').replace(/^Bearer\s/, '') |
161 | | - let callHandler = () => { |
162 | | - if (!hasBody[action]) return handler(req, res, name, action) |
163 | | - bodyParse(req).then((body) => { |
164 | | - req.params = Object.assign(body, req.params) |
165 | | - return handler(req, res, name, action) |
166 | | - }) |
167 | | - } |
168 | | - if (resource.skipAuth && resource.skipAuth[action]) return callHandler() |
169 | | - authenticate(jwt).then((authData) => { |
170 | | - req.auth = req.params.auth = authTrans ? authTrans(authData) : authData |
171 | | - return callHandler() |
172 | | - }).catch((err) => fourOhOne(res, err)) |
173 | | -} |
174 | | - |
175 | | -function sendData (res, data = null) { |
176 | | - return new Promise((resolve, reject) => { |
177 | | - res.end(JSON.stringify({data, error: null})) |
178 | | - return resolve() |
179 | | - }) |
180 | | -} |
181 | | - |
182 | | -function sendErr (res, err = new Error(), code = 500) { |
183 | | - return new Promise((resolve, reject) => { |
184 | | - res.statusCode = code |
185 | | - logIt(err, 'fatal') |
186 | | - err = err instanceof Error ? (err.message || err.name) : err.toString() |
187 | | - res.end(JSON.stringify({data: null, error: err})) |
188 | | - return resolve() |
189 | | - }) |
190 | | -} |
191 | | - |
192 | | -function fourOhOne (res, err = new Error(`unable to auhenticate request`)) { |
193 | | - return sendErr(res, err, 401) |
194 | | -} |
195 | | - |
196 | | -function fourOhFour (res, err = new Error(`no match for requested route`)) { |
197 | | - return sendErr(res, err, 404) |
198 | | -} |
199 | | - |
200 | | -function genToken (payload = {}) { |
201 | | - let key = jwtOpts.secret || jwtOpts.privateKey |
202 | | - let noOpts = () => new Error('missing required jsonwebtoken opts') |
203 | | - if (!jwtOpts || !key) return Promise.reject(noOpts()) |
204 | | - let opts = filterObj(jwtOpts, wlSign) |
205 | | - return new Promise((resolve, reject) => { |
206 | | - jsonwebtoken.sign(payload, key, opts, (err, token) => { |
207 | | - return err ? reject(err) : resolve(token) |
208 | | - }) |
209 | | - }) |
210 | | -} |
211 | | - |
212 | | -function authenticate (jwt) { |
213 | | - let key = jwtOpts.secret || jwtOpts.publicKey |
214 | | - if (!jwtOpts || !key) return Promise.resolve() |
215 | | - return new Promise((resolve, reject) => { |
216 | | - jsonwebtoken.verify(jwt, key, jwtOpts, (err, d = {}) => { |
217 | | - return err ? reject(err) : resolve(d) |
218 | | - }) |
219 | | - }) |
220 | | -} |
221 | | - |
222 | | -// helper: find resource |
223 | | -function find (resource, id, params) { |
224 | | - if (!id && id !== 0) return Promise.reject(noIdErr()) |
225 | | - params.id_array = [id] |
226 | | - let firstRecord = (d) => Promise.resolve(d[0]) |
227 | | - return callPgFunc(`${pgPrefix}${resource}_read`, params).then(firstRecord) |
228 | | -} |
229 | | - |
230 | | -// helper: find set of resources |
231 | | -function findAll (resource, params) { |
232 | | - Object.keys(params).forEach((k) => { |
233 | | - if (!Array.isArray(params[k])) return |
234 | | - params[`${k}_array`] = params[k] |
235 | | - delete params[k] |
236 | | - }) |
237 | | - return callPgFunc(`${pgPrefix}${resource}_search`, params) |
238 | | -} |
239 | | - |
240 | | -// helper: create resource |
241 | | -function create (resource, params) { |
242 | | - let firstRecord = (d) => Promise.resolve(d[0]) |
243 | | - return callPgFunc(`${pgPrefix}${resource}_create`, params).then(firstRecord) |
244 | | -} |
245 | | - |
246 | | -// helper: update resource |
247 | | -function save (resource, id, params) { |
248 | | - if (!id && id !== 0) return Promise.reject(noIdErr()) |
249 | | - params.id = id |
250 | | - let firstRecord = (d) => Promise.resolve(d[0]) |
251 | | - return callPgFunc(`${pgPrefix}${resource}_update`, params).then(firstRecord) |
252 | | -} |
253 | | - |
254 | | -// helper: delete resource |
255 | | -function destroy (resource, id, params) { |
256 | | - if (!id && id !== 0) return Promise.reject(noIdErr()) |
257 | | - params.id = id |
258 | | - return callPgFunc(`${pgPrefix}${resource}_delete`, params) |
259 | | -} |
260 | | - |
261 | | -// default handler for all resource methods |
262 | | -function actionHandler (req, res, name, action) { |
263 | | - let bq = resources[name].beforeQuery || {} // (req, res) |
264 | | - if (typeof bq !== 'function') bq = bq[action] |
265 | | - let bs = resources[name].beforeSend || {} // (req, res, data) |
266 | | - if (typeof bs !== 'function') bs = bs[action] |
267 | | - let act = () => handlers[action](name, req) |
268 | | - let send = (d) => sendData(res, d) |
269 | | - let finish = (d) => bs ? bs(req, res, d).then((d) => send(d)) : send(d) |
270 | | - let run = () => bq ? bq(req, res).then(act).then(finish) : act().then(finish) |
271 | | - return run().catch((e) => sendErr(res, e)) |
| 7 | +module.exports = scrud |
| 8 | +module.exports.instance = () => { |
| 9 | + delete require.cache[require.resolve('./scrud')] |
| 10 | + return require('./scrud') |
272 | 11 | } |
0 commit comments