-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.mjs
206 lines (172 loc) · 7.16 KB
/
index.mjs
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
export class RouteError extends Error {
constructor({statusCode, message}) {
super(message)
this.statusCode = statusCode
}
}
export const Route = (config, ...handlerParams) => (request, context, lambdaCallback) => {
let cfg = Object.assign({}, {
resources: null,
headers: {},
paths: null,
debug: false
}, config)
let _debugLog = (cfg.debug) ? (msg) => console.log(`ROUTER: ${msg}`) : () => {};
_debugLog(`request: ${JSON.stringify(request)}`)
if (request.httpMethod.toUpperCase() == 'OPTIONS') {
_debugLog(`Response to OPTIONS request`)
return lambdaCallback(null, {
statusCode: 204,
headers: cfg.headers,
body: ''
})
} else {
let genericInternalErrorResponse = {
statusCode:500,
headers: cfg.headers,
body:JSON.stringify({ message: "Interval Server Error" })
}
let handler = _getRequestHandlerFile(cfg, request)
if (handler) {
try {
if (cfg.debug) {
if (handler.type == 'resource') {
_debugLog(`[method: ${request.httpMethod}, resource: ${request.resource}] Executing matching resource handler ${cfg.resources[handler.key]}`)
} else {
_debugLog(`[method: ${request.httpMethod}, path: ${request.path}] Executing matching path handler ${cfg.paths[handler.key]}`)
}
}
if (request.headers) {
let requestContentType = request.headers[Object.keys(request.headers).find(header => header.toLowerCase() == 'content-type')]
if (requestContentType && typeof requestContentType == 'string' && requestContentType.search(/^application\/json(;|$)/) === 0) {
request.body = JSON.parse(request.body)
}
}
_runImplementation((handler.type == 'resource') ? cfg.resources[handler.key] : cfg.paths[handler.key])
.then(response => {
_debugLog(`handler success: ${JSON.stringify(response)}`)
// apply headers from config and augment with headers from handler response
if (!('headers' in response)) {
response.headers = cfg.headers
}
else {
response.headers = Object.assign({}, cfg.headers, response.headers)
}
if (response.body && typeof response.body == 'object') {
response.body = JSON.stringify(response.body)
response.headers['Content-Type'] = 'application/json'
} else {
response.body = response.body || ''
}
return lambdaCallback(null, response)
})
.catch(err => {
_debugLog(`Error: ${err.message}`)
if (err instanceof RouteError) {
return lambdaCallback(null,{
statusCode: err.statusCode,
headers: cfg.headers,
body:JSON.stringify({ message: err.message })
})
} else {
return lambdaCallback(null, genericInternalErrorResponse)
}
})
function _runImplementation(handler) {
if (Array.isArray(handler)) {
return handler.reduce((p, stepFile) => p.then(previousStepOutput => {
return import(stepFile).then((handlerMod) => {
return _runImplementationStep(handlerMod, previousStepOutput)
})
}), Promise.resolve(null))
} else {
return import(handler).then((handlerMod) => {
return _runImplementationStep(handlerMod)
})
}
}
function _runImplementationStep(step, previousStepOutput=null) {
// validate request parameters
if ('validate' in step) {
for (let part in step.validate) {
_debugLog(`Validating request.${part}`)
let {error, value} = step.validate[part].validate(request[part] || {});
if (error) {
// parts of the request isn't valid
_debugLog(`Validation error: ${JSON.stringify(error.details)}`)
return Promise.reject(new RouteError({
statusCode: 400,
message: error.details.map(e => e.message).join('. ')
}))
} else {
request[part] = value
}
}
}
// handle the API request
if (previousStepOutput) {
return step.handler(request, context, previousStepOutput, ...handlerParams)
} else {
return step.handler(request, context, ...handlerParams)
}
}
} catch (err) {
_debugLog(`handler error:`, err.message)
return lambdaCallback(null, genericInternalErrorResponse)
}
} else {
_debugLog(`No handler defined for method ${request.httpMethod} on resource ${request.resource} or path ${request.path}`)
// If there is at least one method defined for the requested resource
// or path we'll return a 405 response. Otherwise, we'll return a 404
let exists = cfg.resources && Object.keys(cfg.resources).some(r => {
let [method, resource] = r.split(':')
return (resource === request.resource)
})
if (!exists && cfg.paths) exists = Object.keys(cfg.paths).some(p => {
return _pathMatchesPattern(request.path, p.split(':').pop())
})
let response = (exists) ? [405, 'Method Not Allowed'] : [404, 'Not Found']
return lambdaCallback(null, {
statusCode: response[0],
headers: cfg.headers,
body:JSON.stringify({ message: response[1] })
})
}
}
}
function _getRequestHandlerFile(cfg, request) {
// Attempt to find matching resource handler for request.resource
let resourceHandlerFileKey = [request.httpMethod.toUpperCase(), request.resource.toLowerCase()].join(':')
if (cfg.resources && resourceHandlerFileKey in cfg.resources) {
return { type: 'resource', key: resourceHandlerFileKey }
} else if (cfg.paths) {
// Go through configured paths to find one that matches request.path
// Configured paths may define parameters, we must capture those values
let reqPathParts = request.path.toLowerCase().split('/')
let matchingPathKey = Object.keys(cfg.paths).find((pathPattern) => {
let [method,pattern] = pathPattern.split(':')
return (request.httpMethod.toUpperCase() == method.toUpperCase() && _pathMatchesPattern(reqPathParts,pattern))
})
if (matchingPathKey) {
let originalReqPathParts = request.path.split('/')
if (!('pathParameters' in request)) request.pathParameters = {}
matchingPathKey.split(':').pop().split('/').forEach((part, idx) => {
if (part.substring(0,1) == '{' && part.substring(part.length - 1) == '}') {
request.pathParameters[part.slice(1,-1)] = decodeURIComponent(originalReqPathParts[idx])
}
})
return { type: 'path', key: matchingPathKey }
} else {
return null
}
} else {
return null
}
}
function _pathMatchesPattern(path, pattern) {
let _path = (Array.isArray(path)) ? path : path.split('/')
let _parts = pattern.split('/')
return _parts.length == _path.length && _parts.every((part, idx) => {
return (part == _path[idx] || (part.substring(0,1) == '{' && part.substring(part.length - 1) == '}'))
})
}