UNPKG

7.74 kBJavaScriptView Raw
1const isPromise = require('./isPromise')
2const once = require('once')
3
4/**
5 * @typedef middy
6 * @type function
7 * @param {Object} event - the AWS Lambda event from the original handler
8 * @param {Object} context - the AWS Lambda context from the original handler
9 * @param {function} callback - the AWS Lambda callback from the original handler
10 * @property {useFunction} use - attach one or more new middlewares
11 * @property {applyMiddlewareFunction} applyMiddleware - attach a new middleware
12 * @property {middlewareAttachFunction} before - attach a new *before-only* middleware
13 * @property {middlewareAttachFunction} after - attach a new *after-only* middleware
14 * @property {middlewareAttachFunction} onError - attach a new *error-handler-only* middleware
15 * @property {Object} __middlewares - contains the list of all the attached
16 * middlewares organised by type (`before`, `after`, `onError`). To be used only
17 * for testing and debugging purposes
18 */
19
20/**
21 * @typedef useFunction
22 * @type {function}
23 * @param {middlewareObject|middlewareObject[]} - the middleware object or array of middleware objects to attach
24 * @return {middy}
25 */
26
27/**
28 * @typedef applyMiddlewareFunction
29 * @type {function}
30 * @param {middlewareObject} - the middleware object to attach
31 * @return {middy}
32 */
33
34/**
35 * @typedef middlewareAttachFunction
36 * @type {function}
37 * @param {middlewareFunction} - the middleware function to attach
38 * @return {middy}
39 */
40
41/**
42 * @typedef middlewareNextFunction
43 * @type {function}
44 * @param {error} error - An optional error object to pass in case an error occurred
45 */
46
47/**
48 * @typedef middlewareFunction
49 * @type {function}
50 * @param {function} handler - the original handler function.
51 * It will expose properties `event`, `context`, `response`, `error` and `callback` that can
52 * be used to interact with the middleware lifecycle
53 * @param {middlewareNextFunction} next - the callback to invoke to pass the control to the next middleware
54 * @return {void|Promise} - A middleware can return a Promise instead of using the `next` function as a callback.
55 * In this case middy will wait for the promise to resolve (or reject) and it will automatically
56 * propagate the result to the next middleware.
57 */
58
59/**
60 * @typedef middlewareObject
61 * @type Object
62 * @property {middlewareFunction} before - the middleware function to attach as *before* middleware
63 * @property {middlewareFunction} after - the middleware function to attach as *after* middleware
64 * @property {middlewareFunction} onError - the middleware function to attach as *error* middleware
65 */
66
67const runMiddlewares = (middlewares, request, done) => {
68 const stack = Array.from(middlewares)
69 const runNext = (err) => {
70 try {
71 if (err) {
72 return done(err)
73 }
74
75 const nextMiddleware = stack.shift()
76
77 if (nextMiddleware) {
78 const retVal = nextMiddleware(request, runNext)
79
80 if (retVal) {
81 if (!isPromise(retVal)) {
82 throw new Error('Unexpected return value in middleware')
83 }
84
85 retVal
86 .then(runNext)
87 .catch(done)
88 }
89
90 return
91 }
92
93 return done()
94 } catch (err) {
95 return done(err)
96 }
97 }
98
99 runNext()
100}
101
102const runErrorMiddlewares = (middlewares, request, done) => {
103 const stack = Array.from(middlewares)
104 request.__handledError = false
105 const runNext = (err) => {
106 try {
107 if (!err) {
108 request.__handledError = true
109 }
110
111 const nextMiddleware = stack.shift()
112
113 if (nextMiddleware) {
114 const retVal = nextMiddleware(request, runNext)
115
116 if (retVal) {
117 if (!isPromise(retVal)) {
118 const invalidMiddlewareReturnError = new Error('Unexpected return value in onError middleware')
119 // embed original error to avoid swallowing the real exception
120 invalidMiddlewareReturnError.originalError = err
121 throw invalidMiddlewareReturnError
122 }
123
124 retVal
125 .then(runNext)
126 .catch(done)
127 }
128
129 return
130 }
131
132 return done(request.__handledError ? null : err)
133 } catch (err) {
134 return done(err)
135 }
136 }
137
138 runNext(request.error)
139}
140
141/**
142 * Middy factory function. Use it to wrap your existing handler to enable middlewares on it.
143 * @param {function} handler - your original AWS Lambda function
144 * @return {middy} - a `middy` instance
145 */
146const middy = (handler) => {
147 const beforeMiddlewares = []
148 const afterMiddlewares = []
149 const errorMiddlewares = []
150
151 const instance = (event, context, callback) => {
152 const request = {}
153 request.event = event
154 request.context = context
155 request.callback = callback
156 request.response = null
157 request.error = null
158
159 const middyPromise = new Promise((resolve, reject) => {
160 const terminate = (err) => {
161 if (err) {
162 return callback ? callback(err) : reject(err)
163 }
164
165 return callback ? callback(null, request.response) : resolve(request.response)
166 }
167
168 const errorHandler = err => {
169 request.error = err
170 return runErrorMiddlewares(errorMiddlewares, request, terminate)
171 }
172
173 runMiddlewares(beforeMiddlewares, request, (err) => {
174 if (err) return errorHandler(err)
175
176 const onHandlerError = once((err) => {
177 request.response = null
178 errorHandler(err)
179 })
180
181 const onHandlerSuccess = once((response) => {
182 request.response = response
183 runMiddlewares(afterMiddlewares, request, (err) => {
184 if (err) return errorHandler(err)
185
186 terminate()
187 })
188 })
189
190 const handlerReturnValue = handler.call(request, request.event, request.context, (err, response) => {
191 if (err) return onHandlerError(err)
192 onHandlerSuccess(response)
193 })
194
195 // support for async/await promise return in handler
196 if (handlerReturnValue) {
197 if (!isPromise(handlerReturnValue)) {
198 throw new Error('Unexpected return value in handler')
199 }
200
201 handlerReturnValue
202 .then(onHandlerSuccess)
203 .catch(onHandlerError)
204 }
205 })
206 })
207 if (!request.callback) return middyPromise
208 }
209
210 instance.use = (middlewares) => {
211 if (Array.isArray(middlewares)) {
212 middlewares.forEach(middleware => instance.applyMiddleware(middleware))
213 return instance
214 } else if (typeof middlewares === 'object') {
215 return instance.applyMiddleware(middlewares)
216 } else {
217 throw new Error('Middy.use() accepts an object or an array of objects')
218 }
219 }
220
221 instance.applyMiddleware = (middleware) => {
222 if (typeof middleware !== 'object') {
223 throw new Error('Middleware must be an object')
224 }
225
226 const { before, after, onError } = middleware
227
228 if (!before && !after && !onError) {
229 throw new Error('Middleware must contain at least one key among "before", "after", "onError"')
230 }
231
232 if (before) {
233 instance.before(before)
234 }
235
236 if (after) {
237 instance.after(after)
238 }
239
240 if (onError) {
241 instance.onError(onError)
242 }
243
244 return instance
245 }
246
247 instance.before = (beforeMiddleware) => {
248 beforeMiddlewares.push(beforeMiddleware)
249
250 return instance
251 }
252
253 instance.after = (afterMiddleware) => {
254 afterMiddlewares.unshift(afterMiddleware)
255
256 return instance
257 }
258
259 instance.onError = (errorMiddleware) => {
260 errorMiddlewares.push(errorMiddleware)
261
262 return instance
263 }
264
265 instance.__middlewares = {
266 before: beforeMiddlewares,
267 after: afterMiddlewares,
268 onError: errorMiddlewares
269 }
270
271 return instance
272}
273
274module.exports = middy