Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,28 @@ This repository wraps the default lambda handler so it can be invoked by ALB, AP
import {lf, LambdaHttpResponse} from '@linzjs/lambda';

// This works for Cloud front, ALB or API Gateway events
export const handler = lf.http(async (req) => {
if (req.method !== 'POST') throw new LambdaHttpResponse(400, 'Invalid method');
return new LambdaHttpResponse(200, 'Ok');
export const handler = lf.http();

handler.router.get('/v1/ping', () => new LambdaHttpResponse(200, 'Ok'));
handler.router.get<{ Params: { style: string } }>(
'/v1/style/:style.json',
(req) => new LambdaHttpResponse(200, 'Style: ' + req.params.style),
);

// Handle all requests
handler.router.all('*', () => new LambdaHttpResponse(404, 'Not found'));


// create middleware to validate api key on all requests
handler.router.all('*', (req) => {
const isApiValid = validateApiKey(req.query.get('api'));
// Bail early
if (!isApiValid) return new LambdaHttpResponse(400, 'Invalid api key');

// Continue
return;
});

```

#### Lambda
Expand Down Expand Up @@ -75,11 +93,11 @@ function doRequest(req) {

This can be overridden at either the wrapper
```typescript
export const handler = LambdaFunction.wrap(doRequest, myOwnLogger)
export const handler = lf.wrap(doRequest, myOwnLogger)
```

of set a different default logger
```typescript
LambdaFunction.logger = myOwnLogger;
export const handler = LambdaFunction.wrap(doRequest)
lf.logger = myOwnLogger;
export const handler = lf.wrap(doRequest)
```
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"devDependencies": {
"@linzjs/style": "^3.1.0",
"@types/aws-lambda": "^8.10.85",
"@linzjs/style": "^3.7.0",
"@types/aws-lambda": "^8.10.93",
"@types/node": "^16.4.13",
"@types/ospec": "^4.0.2",
"@types/sinon": "^10.0.6",
"conventional-changelog-cli": "^2.1.1",
"@types/ospec": "^4.0.3",
"@types/pino": "^7.0.5",
"@types/sinon": "^10.0.11",
"conventional-changelog-cli": "^2.2.2",
"conventional-github-releaser": "^3.1.5",
"ospec": "^4.1.1",
"sinon": "^12.0.1",
"sinon": "^13.0.1",
"source-map-support": "^0.5.21"
},
"scripts": {
Expand All @@ -39,9 +40,8 @@
"build/src/**"
],
"dependencies": {
"@linzjs/metrics": "^6.0.0",
"@types/pino": "^6.3.11",
"pino": "^7.5.0",
"@linzjs/metrics": "^6.21.1",
"pino": "^7.9.1",
"ulid": "^2.3.0"
}
}
6 changes: 3 additions & 3 deletions src/__test__/alb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ o.spec('AlbGateway', () => {

o('should extract methods', () => {
const req = new LambdaAlbRequest(AlbExample, fakeContext, fakeLog);
o(req.method).equals('POST');
o(req.method).equals('GET');
});

o('should upper case method', () => {
const newReq = clone(AlbExample);
newReq.httpMethod = 'get';
newReq.httpMethod = 'post';
const req = new LambdaAlbRequest(newReq, fakeContext, fakeLog);
o(req.method).equals('GET');
o(req.method).equals('POST');
});

o('should extract query parameters', () => {
Expand Down
6 changes: 3 additions & 3 deletions src/__test__/api.gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ o.spec('ApiGateway', () => {

o('should extract methods', () => {
const req = new LambdaApiGatewayRequest(ApiGatewayExample, fakeContext, fakeLog);
o(req.method).equals('POST');
o(req.method).equals('GET');
});

o('should upper case method', () => {
const newReq = clone(ApiGatewayExample);
newReq.httpMethod = 'get';
newReq.httpMethod = 'post';
const req = new LambdaApiGatewayRequest(newReq, fakeContext, fakeLog);
o(req.method).equals('GET');
o(req.method).equals('POST');
});

o('should extract query parameters', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/__test__/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ALBEvent, APIGatewayProxyEvent, CloudFrontRequestEvent } from 'aws-lamb
export const ApiGatewayExample: APIGatewayProxyEvent = {
body: 'eyJ0ZXN0IjoiYm9keSJ9',
resource: '/{proxy+}',
path: '/path/to/resource',
httpMethod: 'POST',
path: '/v1/tiles/aerial/EPSG:3857/6/3/41.json',
httpMethod: 'GET',
isBase64Encoded: true,
queryStringParameters: {
foo: 'bar',
Expand Down Expand Up @@ -104,7 +104,7 @@ export const CloudfrontExample: CloudFrontRequestEvent = {
},
request: {
body: undefined,
uri: '/test',
uri: '/v1/tiles/aerial/EPSG:3857/6/3/41.json',
method: 'GET',
clientIp: '2001:cdba::3257:9652',
querystring: '?foo=bar',
Expand All @@ -129,7 +129,7 @@ export const AlbExample: ALBEvent = {
'arn:aws:elasticloadbalancing:ap-southeast-2:000000000:targetgroup/Serve-LBHtt-1OHAJAJC2EOCV/c7cdb5edeadbeefa9',
},
},
httpMethod: 'POST',
httpMethod: 'GET',
path: '/v1/tiles/aerial/EPSG:3857/6/3/41.json',
queryStringParameters: {
api: 'abc123',
Expand Down
25 changes: 22 additions & 3 deletions src/__test__/readme.example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,26 @@ export const handlerB = lf.handler<S3Event>(async (req) => {
});

// This works for Cloud front, ALB or API Gateway events
export const handlerC = lf.http(async (req) => {
if (req.method !== 'POST') throw new LambdaHttpResponse(400, 'Invalid method');
return new LambdaHttpResponse(200, 'Ok');
export const handler = lf.http();

handler.router.get('/v1/ping', () => new LambdaHttpResponse(200, 'Ok'));
handler.router.get<{ Params: { style: string } }>(
'/v1/style/:style.json',
(req) => new LambdaHttpResponse(200, 'Style: ' + req.params.style),
);

// Handle all requests
handler.router.all('*', () => new LambdaHttpResponse(404, 'Not found'));

function validateApiKey(s?: string | null): boolean {
return s != null;
}
// create middleware to validate api key on all requests
handler.router.all('*', (req) => {
const isApiValid = validateApiKey(req.query.get('api'));
// Bail early
if (!isApiValid) return new LambdaHttpResponse(400, 'Invalid api key');

// Continue
return;
});
87 changes: 78 additions & 9 deletions src/__test__/wrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { lf } from '../function.js';
import { LambdaRequest } from '../request.js';
import { LambdaHttpRequest } from '../request.http.js';
import { LambdaHttpResponse } from '../response.http.js';
import { AlbExample, ApiGatewayExample, CloudfrontExample } from './examples.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample } from './examples.js';
import { fakeLog } from './log.js';
import { HttpMethods } from '../router.js';

function assertAlbResult(x: unknown): asserts x is ALBResult {}
function assertCloudfrontResult(x: unknown): asserts x is CloudFrontResultResponse {}
Expand All @@ -29,8 +30,71 @@ o.spec('LambdaWrap', () => {
fakeLog.logs = [];
});

o('should handle middleware', async () => {
const fn = lf.http(fakeLog);
fn.router.all('*', (req): LambdaHttpResponse | void => {
if (req.path.includes('fail')) return new LambdaHttpResponse(500, 'Failed');
});
fn.router.get('/v1/ping', () => new LambdaHttpResponse(200, 'Ok'));

const newReq = clone(ApiGatewayExample);
newReq.path = '/v1/ping';
const ret = await new Promise((resolve) => fn(newReq, fakeContext, (a, b) => resolve(b)));
assertAlbResult(ret);

o(ret.statusCode).equals(200);

newReq.path = '/v1/ping/fail';
const retB = await new Promise((resolve) => fn(newReq, fakeContext, (a, b) => resolve(b)));
assertAlbResult(retB);
o(retB.statusCode).equals(500);
});

o('should wrap all http methods', async () => {
const fn = lf.http(fakeLog);
const methods: string[] = [];

function bind(r: string) {
return (): LambdaHttpResponse => {
methods.push(r.toUpperCase());
return new LambdaHttpResponse(200, 'Ok');
};
}
fn.router.get('*', bind('get'));
fn.router.delete('*', bind('delete'));
fn.router.head('*', bind('head'));
fn.router.options('*', bind('options'));
fn.router.post('*', bind('post'));
fn.router.patch('*', bind('patch'));
fn.router.put('*', bind('put'));

const headers: Record<HttpMethods, number> = {
DELETE: 1,
ALL: 0,
GET: 1,
OPTIONS: 1,
HEAD: 1,
POST: 1,
PATCH: 1,
PUT: 1,
};

const requests = Object.entries(headers)
.filter((f) => f[1] === 1)
.map((f) => f[0]);

for (const req of requests) {
const newReq = clone(ApiGatewayExample);
newReq.httpMethod = req;
await new Promise((resolve) => fn(newReq, fakeContext, (a, b) => resolve(b)));
}

o(methods).deepEquals(requests);
});

o('should log a metalog at the end of the request', async () => {
const fn = lf.http(fakeLambda, fakeLog);
const fn = lf.http(fakeLog);
fn.router.get('/v1/tiles/:tileSet/:projection/:z/:x/:y.json', fakeLambda);
await new Promise((resolve) => fn(AlbExample, fakeContext, (a, b) => resolve(b)));

o(fakeLog.logs.length).equals(1);
Expand All @@ -39,15 +103,16 @@ o.spec('LambdaWrap', () => {
o(firstLog['@type']).equals('report');
o(typeof firstLog['duration'] === 'number').equals(true);
o(firstLog['status']).equals(200);
o(firstLog['method']).equals('POST');
o(firstLog['method']).equals('GET');
o(firstLog['path']).equals('/v1/tiles/aerial/EPSG:3857/6/3/41.json');
o(firstLog['id']).equals(requests[0].id);
o(firstLog['setTest']).equals(requests[0].id);
o(firstLog['correlationId']).equals(requests[0].correlationId);
});

o('should respond to alb events', async () => {
const fn = lf.http(fakeLambda, fakeLog);
const fn = lf.http(fakeLog);
fn.router.get('/v1/tiles/:tileSet/:projection/:z/:x/:y.json', fakeLambda);
const ret = await new Promise((resolve) => fn(AlbExample, fakeContext, (a, b) => resolve(b)));

assertAlbResult(ret);
Expand All @@ -64,7 +129,8 @@ o.spec('LambdaWrap', () => {
});

o('should respond to cloudfront events', async () => {
const fn = lf.http(fakeLambda);
const fn = lf.http(fakeLog);
fn.router.get('/v1/tiles/:tileSet/:projection/:z/:x/:y.json', fakeLambda);
const ret = await new Promise((resolve) => fn(CloudfrontExample, fakeContext, (a, b) => resolve(b)));

assertCloudfrontResult(ret);
Expand All @@ -79,7 +145,8 @@ o.spec('LambdaWrap', () => {
});

o('should respond to api gateway events', async () => {
const fn = lf.http(fakeLambda);
const fn = lf.http(fakeLog);
fn.router.get('/v1/tiles/:tileSet/:projection/:z/:x/:y.json', fakeLambda);
const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b)));

assertsApiGatewayResult(ret);
Expand All @@ -94,8 +161,9 @@ o.spec('LambdaWrap', () => {
});

o('should handle http exceptions', async () => {
const fn = lf.http(() => {
throw new Error('Fake');
const fn = lf.http(fakeLog);
fn.router.all('*', () => {
throw new Error('Error');
});
const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b)));

Expand Down Expand Up @@ -152,7 +220,8 @@ o.spec('LambdaWrap', () => {
o('should disable "server" header if no server name set', async () => {
const serverName = lf.ServerName;
lf.ServerName = null;
const fn = lf.http(fakeLambda);
const fn = lf.http();
fn.router.all('*', fakeLambda);
const ret = await new Promise((resolve) => fn(ApiGatewayExample, fakeContext, (a, b) => resolve(b)));

lf.ServerName = serverName;
Expand Down
12 changes: 7 additions & 5 deletions src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LambdaApiGatewayRequest } from './request.api.gateway.js';
import { LambdaCloudFrontRequest } from './request.cloudfront.js';
import { HttpRequestEvent, HttpResponse, LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';
import { Router } from './router.js';

export interface HttpStatus {
statusCode: string;
Expand Down Expand Up @@ -47,7 +48,7 @@ async function runFunction<T extends LambdaRequest, K>(
}
}

async function execute<T extends LambdaRequest, K>(
export async function execute<T extends LambdaRequest, K>(
req: T,
fn: (req: T) => K | Promise<K>,
): Promise<K | LambdaHttpResponse> {
Expand Down Expand Up @@ -160,15 +161,15 @@ export class lf {
return handler;
}
/**
* Wrap a lambda function to provide extra functionality
* Create a route lambda function to provide extra functionality
*
* - Log metadata about the call on every request
* - Catch errors and log them before exiting
*
* @param fn Function to wrap
* @param logger optional logger to use for the request @see lf.Logger
*/
public static http(fn: LambdaWrappedFunctionHttp, logger?: LogType): LambdaHandler<HttpRequestEvent, HttpResponse> {
public static http(logger?: LogType): LambdaHandler<HttpRequestEvent, HttpResponse> & { router: Router } {
const router = new Router();
function httpHandler(event: HttpRequestEvent, context: Context, callback: Callback<HttpResponse>): void {
const req = lf.request(event, context, logger ?? lf.Logger);

Expand All @@ -180,7 +181,7 @@ export class lf {
req.set('method', req.method);
req.set('path', req.path);

execute(req, fn).then((res: LambdaHttpResponse) => {
router.handle(req).then((res: LambdaHttpResponse) => {
// Do not cache http 500 errors
if (res.status === 500) res.header(HttpHeader.CacheControl, 'no-store');
res.header(HttpHeaderRequestId.RequestId, req.id);
Expand All @@ -198,6 +199,7 @@ export class lf {
callback(null, req.toResponse(res));
});
}
httpHandler.router = router;
return httpHandler;
}
}
Loading