Skip to content
This repository was archived by the owner on Aug 8, 2024. It is now read-only.

Commit 59c66ff

Browse files
authored
Proxy integration improvements (#36)
* improve Proxy integration return values * fix ProxyIntegrationEvent body type * consistently use statusCode * add aws-lambda as dep * improve README
1 parent c061453 commit 59c66ff

File tree

5 files changed

+239
-137
lines changed

5 files changed

+239
-137
lines changed

README.md

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
1010
## Features
1111

1212
* Easy Handling of [ANY method](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-settings-method-request.html#setup-method-add-http-method) in API Gateways
13-
* Simplifies writing lambda handlers (in nodejs)
13+
* Simplifies writing lambda handlers (in nodejs > 8)
1414
* Lambda Proxy Resource support for AWS API Gateway
1515
* Enable CORS for requests
1616
* No external dependencies
@@ -19,6 +19,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
1919
* SNS
2020
* SQS
2121
* S3
22+
* Compatibility with Typescript >= 3.5
2223

2324
## Installation
2425

@@ -54,7 +55,7 @@ export const handler = router.handler({
5455
}
5556
]
5657
}
57-
}
58+
})
5859
```
5960

6061
## Proxy path support (work in progress)
@@ -94,6 +95,36 @@ exports.handler = router.handler({
9495
}
9596
]
9697
}
98+
})
99+
```
100+
101+
Typescript example:
102+
```ts
103+
import * as router from 'aws-lambda-router'
104+
import { ProxyIntegrationEvent } from 'aws-lambda-router/lib/proxyIntegration'
105+
106+
export const handler = router.handler({
107+
proxyIntegration: {
108+
routes: [
109+
{
110+
path: '/saveExample',
111+
method: 'POST',
112+
// request.body needs type assertion, because it defaults to type unknown (user input should be checked):
113+
action: (request, context) => {
114+
const { text } = request.body as { text: string }
115+
return `You called me with: ${text}`
116+
}
117+
},
118+
{
119+
path: '/saveExample2',
120+
method: 'POST',
121+
// it's also possible to set a type (no type check):
122+
action: (request: ProxyIntegrationEvent<{ text: string }>, context) => {
123+
return `You called me with: ${request.body.text}`
124+
}
125+
}
126+
]
127+
}
97128
}
98129
```
99130
@@ -120,7 +151,7 @@ export const handler = router.handler({
120151
}
121152
]
122153
}
123-
});
154+
})
124155
```
125156
126157
If CORS is activated, these default headers will be sent on every response:
@@ -152,7 +183,7 @@ export const handler = router.handler({
152183
'ServerError': 500
153184
}
154185
}
155-
});
186+
})
156187

157188
function doThrowAnException(body) {
158189
throw {reason: 'MyCustomError', message: 'Throw an error for this example'}
@@ -187,7 +218,7 @@ export const handler = router.handler({
187218
}
188219
]
189220
}
190-
});
221+
})
191222
```
192223
193224
## SQS to Lambda Integrations
@@ -214,7 +245,7 @@ export const handler = router.handler({
214245
}
215246
]
216247
}
217-
});
248+
})
218249
```
219250
220251
An SQS message always contains an array of records. In each SQS record there is the message in the body JSON key.
@@ -291,7 +322,7 @@ export const handler = router.handler({
291322
],
292323
debug: true
293324
}
294-
});
325+
})
295326
```
296327
297328
Per s3 event there can be several records per event. The action methods are called one after the other record. The result of the action method is an array with objects insides.
@@ -328,13 +359,16 @@ See here: https://yarnpkg.com/en/docs/cli/link
328359
329360
330361
## Release History
331-
332-
* 0.7.2
362+
* 0.8.0
363+
* fix: changed ProxyIntegrationEvent body type to be generic but defaults to unknown
364+
* fix: changed @types/aws-lambda from devDependency to dependency
365+
* **breaking**: error response objects (thrown or rejected) now need to set `statusCode` instead of `status` (consistent with response)
366+
* 0.7.1
333367
* code style cleanup
334368
* fix: hosted package on npmjs should now worked
335369
* 0.7.0
336370
* migrate to typescript
337-
* using aws-lambda typings
371+
* using @types/aws-lambda typings
338372
* proxyIntegration: cors is now optional (default: false)
339373
* removed use of aws lambda handler callback function (using Promise instead)
340374
* experimental _proxy path support_ (thanks to [@swaner](https://github.com/swaner))

lib/proxyIntegration.ts

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
22
import { ProcessMethod } from './EventProcessor'
33

4-
export type ProxyIntegrationEvent = APIGatewayProxyEvent
54
type ProxyIntegrationParams = {
65
paths?: { [paramId: string]: string }
76
}
8-
export type ProxyIntegrationEventWithParams = APIGatewayProxyEvent & ProxyIntegrationParams
7+
type ProxyIntegrationBody<T = unknown> = {
8+
body: T
9+
}
10+
export type ProxyIntegrationEvent<T = unknown> = Omit<APIGatewayProxyEvent, 'body'> & ProxyIntegrationParams & ProxyIntegrationBody<T>
11+
export type ProxyIntegrationResult = Omit<APIGatewayProxyResult, 'statusCode'> & { statusCode?: APIGatewayProxyResult['statusCode'] }
912

1013
export interface ProxyIntegrationRoute {
1114
path: string
1215
method: string
1316
action: (
14-
request: ProxyIntegrationEventWithParams,
17+
request: ProxyIntegrationEvent<unknown>,
1518
context: APIGatewayEventRequestContext
16-
) => APIGatewayProxyResult | Promise<APIGatewayProxyResult>
19+
) => ProxyIntegrationResult | Promise<ProxyIntegrationResult> | string | Promise<string>
1720
}
1821

1922
export type ProxyIntegrationErrorMapping = {
2023
[reason: string]: APIGatewayProxyResult['statusCode']
2124
}
2225

2326
export type ProxyIntegrationError = {
24-
status: APIGatewayProxyResult['statusCode'],
27+
statusCode: APIGatewayProxyResult['statusCode'],
2528
message: string
2629
} | {
2730
reason: string,
@@ -37,7 +40,7 @@ export interface ProxyIntegrationConfig {
3740
proxyPath?: string
3841
}
3942

40-
const NO_MATCHING_ACTION = (request: APIGatewayProxyEvent) => {
43+
const NO_MATCHING_ACTION = (request: ProxyIntegrationEvent) => {
4144
throw {
4245
reason: 'NO_MATCHING_ACTION',
4346
message: `Could not find matching action for ${request.path} and method ${request.httpMethod}`
@@ -51,17 +54,15 @@ const addCorsHeaders = (toAdd: APIGatewayProxyResult['headers'] = {}) => {
5154
return toAdd
5255
}
5356

54-
const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEventWithParams,
55-
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {
57+
const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEvent,
58+
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {
5659

5760
const res = await actionConfig.action(event, context)
58-
if (!res || !res.body) {
59-
const consolidateBody = res && JSON.stringify(res) || '{}'
60-
61+
if (!res || typeof res !== 'object' || typeof res.body !== 'string') {
6162
return {
6263
statusCode: 200,
6364
headers,
64-
body: consolidateBody
65+
body: JSON.stringify(res) || '{}'
6566
}
6667
}
6768

@@ -75,7 +76,7 @@ const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute,
7576
}
7677
}
7778

78-
export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEventWithParams, APIGatewayEventRequestContext, APIGatewayProxyResult> =
79+
export const process: ProcessMethod<ProxyIntegrationConfig, APIGatewayProxyEvent, APIGatewayEventRequestContext, APIGatewayProxyResult> =
7980
(proxyIntegrationConfig, event, context) => {
8081

8182
if (proxyIntegrationConfig.debug) {
@@ -112,10 +113,11 @@ export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEven
112113
errorMapping['NO_MATCHING_ACTION'] = 404
113114

114115
if (proxyIntegrationConfig.proxyPath) {
115-
console.log('proxy path is set: ' + proxyIntegrationConfig.proxyPath)
116116
event.path = (event.pathParameters || {})[proxyIntegrationConfig.proxyPath]
117-
console.log('proxy path with event path: ' + event.path)
118-
117+
if (proxyIntegrationConfig.debug) {
118+
console.log('proxy path is set: ' + proxyIntegrationConfig.proxyPath)
119+
console.log('proxy path with event path: ' + event.path)
120+
}
119121
} else {
120122
event.path = normalizeRequestPath(event)
121123
}
@@ -126,10 +128,12 @@ export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEven
126128
paths: undefined
127129
}
128130

129-
event.paths = actionConfig.paths
131+
const proxyEvent: ProxyIntegrationEvent = event
132+
133+
proxyEvent.paths = actionConfig.paths
130134
if (event.body) {
131135
try {
132-
event.body = JSON.parse(event.body)
136+
proxyEvent.body = JSON.parse(event.body)
133137
} catch (parseError) {
134138
console.log(`Could not parse body as json: ${event.body}`, parseError)
135139
return {
@@ -139,7 +143,7 @@ export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEven
139143
}
140144
}
141145
}
142-
return processActionAndReturn(actionConfig, event, context, headers).catch(error => {
146+
return processActionAndReturn(actionConfig, proxyEvent, context, headers).catch(error => {
143147
console.log('Error while handling action function.', error)
144148
return convertError(error, errorMapping, headers)
145149
})
@@ -154,20 +158,20 @@ const normalizeRequestPath = (event: APIGatewayProxyEvent): string => {
154158
return event.path
155159
}
156160

157-
// ugly hack: if host is from API-Gateway 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/';
161+
// ugly hack: if host is from API-Gateway 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/'
158162
// if host is from amazonaws.com, then event.path is just '/resource-path':
159163
const apiId = event.requestContext ? event.requestContext.apiId : null // the apiId that is the first part of the amazonaws.com-host
160164
if ((apiId && event.headers && event.headers.Host && event.headers.Host.substring(0, apiId.length) !== apiId)) {
161165
// remove first path element:
162-
const groups: any = /\/[^\/]+(.*)/.exec(event.path) || [null, null]
166+
const groups = /\/[^\/]+(.*)/.exec(event.path) || [null, null]
163167
return groups[1] || '/'
164168
}
165169

166170
return event.path
167171
}
168172

169173
const hasReason = (error: any): error is { reason: string } => typeof error.reason === 'string'
170-
const hasStatus = (error: any): error is { status: number } => typeof error.status === 'number'
174+
const hasStatus = (error: any): error is { statusCode: number } => typeof error.statusCode === 'number'
171175

172176
const convertError = (error: ProxyIntegrationError | Error, errorMapping?: ProxyIntegrationErrorMapping, headers?: APIGatewayProxyResult['headers']) => {
173177
if (hasReason(error) && errorMapping && errorMapping[error.reason]) {
@@ -178,8 +182,8 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
178182
}
179183
} else if (hasStatus(error)) {
180184
return {
181-
statusCode: error.status,
182-
body: JSON.stringify({ message: error.message, error: error.status }),
185+
statusCode: error.statusCode,
186+
body: JSON.stringify({ message: error.message, error: error.statusCode }),
183187
headers: addCorsHeaders({})
184188
}
185189
}
@@ -189,12 +193,11 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
189193
body: JSON.stringify({ error: 'ServerError', message: `Generic error:${JSON.stringify(error)}` }),
190194
headers: addCorsHeaders({})
191195
}
192-
} catch (stringifyError) {
193-
}
196+
} catch (stringifyError) { }
194197

195198
return {
196199
statusCode: 500,
197-
body: JSON.stringify({ error: 'ServerError', message: `Generic error` })
200+
body: JSON.stringify({ error: 'ServerError', message: 'Generic error' })
198201
}
199202
}
200203

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aws-lambda-router",
3-
"version": "0.7.2",
3+
"version": "0.8.0",
44
"description": "AWS lambda router",
55
"main": "index.js",
66
"types": "index.d.ts",
@@ -30,13 +30,14 @@
3030
},
3131
"homepage": "https://github.com/spring-media/aws-lambda-router#readme",
3232
"devDependencies": {
33-
"@types/aws-lambda": "^8.10.39",
34-
"@types/jest": "^24.0.25",
33+
"@types/jest": "^24.9.0",
3534
"@types/node": "^8.10.59",
36-
"codecov": "^3.6.1",
35+
"codecov": "^3.6.2",
3736
"jest": "24.9.0",
3837
"ts-jest": "24.2.0",
39-
"typescript": "3.7.4"
38+
"typescript": "3.7.5"
4039
},
41-
"dependencies": {}
40+
"dependencies": {
41+
"@types/aws-lambda": "^8.10.40"
42+
}
4243
}

test/proxyIntegration.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { process as proxyIntegration, ProxyIntegrationConfig } from '../lib/proxyIntegration'
33
import { APIGatewayProxyEvent } from 'aws-lambda'
44

5-
function forEach (arrayOfArrays: any) {
5+
function forEach(arrayOfArrays: any) {
66
return {
77
it: (description: string, testCaseFunction: (...args: any[]) => void | Promise<void>) => {
88
arrayOfArrays.forEach((innerArray: any) => {
@@ -374,7 +374,7 @@ describe('proxyIntegration.routeHandler', () => {
374374
})
375375

376376
it('should pass through error statuscode', async () => {
377-
const statusCodeError = { status: 666, message: { reason: 'oops' } }
377+
const statusCodeError = { statusCode: 666, message: { reason: 'oops' } }
378378
const routeConfig = {
379379
routes: [
380380
{
@@ -515,10 +515,21 @@ describe('proxyIntegration.routeHandler.returnvalues', () => {
515515
})
516516
})
517517

518-
it('should return async result', async () => {
518+
forEach([
519+
[{ foo: 'bar' }, JSON.stringify({ foo: 'bar' })],
520+
[{ body: 1234 }, JSON.stringify({ body: 1234 })],
521+
[{ body: '1234' }, '1234'],
522+
['', '""'],
523+
['abc', '"abc"'],
524+
[false, 'false'],
525+
[true, 'true'],
526+
[null, 'null'],
527+
[1234, '1234'],
528+
[undefined, '{}']
529+
]).it('should return async result', async (returnValue, expectedBody) => {
519530
const routeConfig = {
520531
routes: [
521-
{ method: 'GET', path: '/', action: () => Promise.resolve({ foo: 'bar' } as any) }
532+
{ method: 'GET', path: '/', action: () => Promise.resolve(returnValue) }
522533
]
523534
}
524535
const result = await proxyIntegration(routeConfig, {
@@ -528,7 +539,7 @@ describe('proxyIntegration.routeHandler.returnvalues', () => {
528539
expect(result).toEqual({
529540
statusCode: 200,
530541
headers: jasmine.anything(),
531-
body: JSON.stringify({ foo: 'bar' })
542+
body: expectedBody
532543
})
533544
})
534545

0 commit comments

Comments
 (0)