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

Commit

Permalink
Proxy integration improvements (#36)
Browse files Browse the repository at this point in the history
* improve Proxy integration return values
* fix ProxyIntegrationEvent body type
* consistently use statusCode
* add aws-lambda as dep
* improve README
  • Loading branch information
tdt17 authored Jan 24, 2020
1 parent c061453 commit 59c66ff
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 137 deletions.
54 changes: 44 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
## Features

* 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
* Simplifies writing lambda handlers (in nodejs)
* Simplifies writing lambda handlers (in nodejs > 8)
* Lambda Proxy Resource support for AWS API Gateway
* Enable CORS for requests
* No external dependencies
Expand All @@ -19,6 +19,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
* SNS
* SQS
* S3
* Compatibility with Typescript >= 3.5

## Installation

Expand Down Expand Up @@ -54,7 +55,7 @@ export const handler = router.handler({
}
]
}
}
})
```

## Proxy path support (work in progress)
Expand Down Expand Up @@ -94,6 +95,36 @@ exports.handler = router.handler({
}
]
}
})
```

Typescript example:
```ts
import * as router from 'aws-lambda-router'
import { ProxyIntegrationEvent } from 'aws-lambda-router/lib/proxyIntegration'

export const handler = router.handler({
proxyIntegration: {
routes: [
{
path: '/saveExample',
method: 'POST',
// request.body needs type assertion, because it defaults to type unknown (user input should be checked):
action: (request, context) => {
const { text } = request.body as { text: string }
return `You called me with: ${text}`
}
},
{
path: '/saveExample2',
method: 'POST',
// it's also possible to set a type (no type check):
action: (request: ProxyIntegrationEvent<{ text: string }>, context) => {
return `You called me with: ${request.body.text}`
}
}
]
}
}
```
Expand All @@ -120,7 +151,7 @@ export const handler = router.handler({
}
]
}
});
})
```
If CORS is activated, these default headers will be sent on every response:
Expand Down Expand Up @@ -152,7 +183,7 @@ export const handler = router.handler({
'ServerError': 500
}
}
});
})

function doThrowAnException(body) {
throw {reason: 'MyCustomError', message: 'Throw an error for this example'}
Expand Down Expand Up @@ -187,7 +218,7 @@ export const handler = router.handler({
}
]
}
});
})
```
## SQS to Lambda Integrations
Expand All @@ -214,7 +245,7 @@ export const handler = router.handler({
}
]
}
});
})
```
An SQS message always contains an array of records. In each SQS record there is the message in the body JSON key.
Expand Down Expand Up @@ -291,7 +322,7 @@ export const handler = router.handler({
],
debug: true
}
});
})
```
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.
Expand Down Expand Up @@ -328,13 +359,16 @@ See here: https://yarnpkg.com/en/docs/cli/link
## Release History
* 0.7.2
* 0.8.0
* fix: changed ProxyIntegrationEvent body type to be generic but defaults to unknown
* fix: changed @types/aws-lambda from devDependency to dependency
* **breaking**: error response objects (thrown or rejected) now need to set `statusCode` instead of `status` (consistent with response)
* 0.7.1
* code style cleanup
* fix: hosted package on npmjs should now worked
* 0.7.0
* migrate to typescript
* using aws-lambda typings
* using @types/aws-lambda typings
* proxyIntegration: cors is now optional (default: false)
* removed use of aws lambda handler callback function (using Promise instead)
* experimental _proxy path support_ (thanks to [@swaner](https://github.com/swaner))
Expand Down
57 changes: 30 additions & 27 deletions lib/proxyIntegration.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { ProcessMethod } from './EventProcessor'

export type ProxyIntegrationEvent = APIGatewayProxyEvent
type ProxyIntegrationParams = {
paths?: { [paramId: string]: string }
}
export type ProxyIntegrationEventWithParams = APIGatewayProxyEvent & ProxyIntegrationParams
type ProxyIntegrationBody<T = unknown> = {
body: T
}
export type ProxyIntegrationEvent<T = unknown> = Omit<APIGatewayProxyEvent, 'body'> & ProxyIntegrationParams & ProxyIntegrationBody<T>
export type ProxyIntegrationResult = Omit<APIGatewayProxyResult, 'statusCode'> & { statusCode?: APIGatewayProxyResult['statusCode'] }

export interface ProxyIntegrationRoute {
path: string
method: string
action: (
request: ProxyIntegrationEventWithParams,
request: ProxyIntegrationEvent<unknown>,
context: APIGatewayEventRequestContext
) => APIGatewayProxyResult | Promise<APIGatewayProxyResult>
) => ProxyIntegrationResult | Promise<ProxyIntegrationResult> | string | Promise<string>
}

export type ProxyIntegrationErrorMapping = {
[reason: string]: APIGatewayProxyResult['statusCode']
}

export type ProxyIntegrationError = {
status: APIGatewayProxyResult['statusCode'],
statusCode: APIGatewayProxyResult['statusCode'],
message: string
} | {
reason: string,
Expand All @@ -37,7 +40,7 @@ export interface ProxyIntegrationConfig {
proxyPath?: string
}

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

const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEventWithParams,
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {
const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute, 'action'>, event: ProxyIntegrationEvent,
context: APIGatewayEventRequestContext, headers: APIGatewayProxyResult['headers']) => {

const res = await actionConfig.action(event, context)
if (!res || !res.body) {
const consolidateBody = res && JSON.stringify(res) || '{}'

if (!res || typeof res !== 'object' || typeof res.body !== 'string') {
return {
statusCode: 200,
headers,
body: consolidateBody
body: JSON.stringify(res) || '{}'
}
}

Expand All @@ -75,7 +76,7 @@ const processActionAndReturn = async (actionConfig: Pick<ProxyIntegrationRoute,
}
}

export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEventWithParams, APIGatewayEventRequestContext, APIGatewayProxyResult> =
export const process: ProcessMethod<ProxyIntegrationConfig, APIGatewayProxyEvent, APIGatewayEventRequestContext, APIGatewayProxyResult> =
(proxyIntegrationConfig, event, context) => {

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

if (proxyIntegrationConfig.proxyPath) {
console.log('proxy path is set: ' + proxyIntegrationConfig.proxyPath)
event.path = (event.pathParameters || {})[proxyIntegrationConfig.proxyPath]
console.log('proxy path with event path: ' + event.path)

if (proxyIntegrationConfig.debug) {
console.log('proxy path is set: ' + proxyIntegrationConfig.proxyPath)
console.log('proxy path with event path: ' + event.path)
}
} else {
event.path = normalizeRequestPath(event)
}
Expand All @@ -126,10 +128,12 @@ export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEven
paths: undefined
}

event.paths = actionConfig.paths
const proxyEvent: ProxyIntegrationEvent = event

proxyEvent.paths = actionConfig.paths
if (event.body) {
try {
event.body = JSON.parse(event.body)
proxyEvent.body = JSON.parse(event.body)
} catch (parseError) {
console.log(`Could not parse body as json: ${event.body}`, parseError)
return {
Expand All @@ -139,7 +143,7 @@ export const process: ProcessMethod<ProxyIntegrationConfig, ProxyIntegrationEven
}
}
}
return processActionAndReturn(actionConfig, event, context, headers).catch(error => {
return processActionAndReturn(actionConfig, proxyEvent, context, headers).catch(error => {
console.log('Error while handling action function.', error)
return convertError(error, errorMapping, headers)
})
Expand All @@ -154,20 +158,20 @@ const normalizeRequestPath = (event: APIGatewayProxyEvent): string => {
return event.path
}

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

return event.path
}

const hasReason = (error: any): error is { reason: string } => typeof error.reason === 'string'
const hasStatus = (error: any): error is { status: number } => typeof error.status === 'number'
const hasStatus = (error: any): error is { statusCode: number } => typeof error.statusCode === 'number'

const convertError = (error: ProxyIntegrationError | Error, errorMapping?: ProxyIntegrationErrorMapping, headers?: APIGatewayProxyResult['headers']) => {
if (hasReason(error) && errorMapping && errorMapping[error.reason]) {
Expand All @@ -178,8 +182,8 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
}
} else if (hasStatus(error)) {
return {
statusCode: error.status,
body: JSON.stringify({ message: error.message, error: error.status }),
statusCode: error.statusCode,
body: JSON.stringify({ message: error.message, error: error.statusCode }),
headers: addCorsHeaders({})
}
}
Expand All @@ -189,12 +193,11 @@ const convertError = (error: ProxyIntegrationError | Error, errorMapping?: Proxy
body: JSON.stringify({ error: 'ServerError', message: `Generic error:${JSON.stringify(error)}` }),
headers: addCorsHeaders({})
}
} catch (stringifyError) {
}
} catch (stringifyError) { }

return {
statusCode: 500,
body: JSON.stringify({ error: 'ServerError', message: `Generic error` })
body: JSON.stringify({ error: 'ServerError', message: 'Generic error' })
}
}

Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aws-lambda-router",
"version": "0.7.2",
"version": "0.8.0",
"description": "AWS lambda router",
"main": "index.js",
"types": "index.d.ts",
Expand Down Expand Up @@ -30,13 +30,14 @@
},
"homepage": "https://github.com/spring-media/aws-lambda-router#readme",
"devDependencies": {
"@types/aws-lambda": "^8.10.39",
"@types/jest": "^24.0.25",
"@types/jest": "^24.9.0",
"@types/node": "^8.10.59",
"codecov": "^3.6.1",
"codecov": "^3.6.2",
"jest": "24.9.0",
"ts-jest": "24.2.0",
"typescript": "3.7.4"
"typescript": "3.7.5"
},
"dependencies": {}
"dependencies": {
"@types/aws-lambda": "^8.10.40"
}
}
21 changes: 16 additions & 5 deletions test/proxyIntegration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { process as proxyIntegration, ProxyIntegrationConfig } from '../lib/proxyIntegration'
import { APIGatewayProxyEvent } from 'aws-lambda'

function forEach (arrayOfArrays: any) {
function forEach(arrayOfArrays: any) {
return {
it: (description: string, testCaseFunction: (...args: any[]) => void | Promise<void>) => {
arrayOfArrays.forEach((innerArray: any) => {
Expand Down Expand Up @@ -374,7 +374,7 @@ describe('proxyIntegration.routeHandler', () => {
})

it('should pass through error statuscode', async () => {
const statusCodeError = { status: 666, message: { reason: 'oops' } }
const statusCodeError = { statusCode: 666, message: { reason: 'oops' } }
const routeConfig = {
routes: [
{
Expand Down Expand Up @@ -515,10 +515,21 @@ describe('proxyIntegration.routeHandler.returnvalues', () => {
})
})

it('should return async result', async () => {
forEach([
[{ foo: 'bar' }, JSON.stringify({ foo: 'bar' })],
[{ body: 1234 }, JSON.stringify({ body: 1234 })],
[{ body: '1234' }, '1234'],
['', '""'],
['abc', '"abc"'],
[false, 'false'],
[true, 'true'],
[null, 'null'],
[1234, '1234'],
[undefined, '{}']
]).it('should return async result', async (returnValue, expectedBody) => {
const routeConfig = {
routes: [
{ method: 'GET', path: '/', action: () => Promise.resolve({ foo: 'bar' } as any) }
{ method: 'GET', path: '/', action: () => Promise.resolve(returnValue) }
]
}
const result = await proxyIntegration(routeConfig, {
Expand All @@ -528,7 +539,7 @@ describe('proxyIntegration.routeHandler.returnvalues', () => {
expect(result).toEqual({
statusCode: 200,
headers: jasmine.anything(),
body: JSON.stringify({ foo: 'bar' })
body: expectedBody
})
})

Expand Down
Loading

0 comments on commit 59c66ff

Please sign in to comment.