Skip to content

Commit 18afab1

Browse files
committed
Support for apps that require async setup + export types
1 parent f436513 commit 18afab1

File tree

3 files changed

+88
-36
lines changed

3 files changed

+88
-36
lines changed

README.md

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ The list of supported frameworks matches [in-process-request](https://github.com
1010
* Connect v3
1111
* Koa v2
1212

13-
1413
Inspired by [aws-serverless-express](https://github.com/awslabs/aws-serverless-express)
1514

1615
It supports `nodejs8.10` and `nodejs10.x` execution environments.
1716

1817
The main differences between this module and `aws-serverless-express` are
1918
* It's using [in-process-request](https://github.com/janaz/in-process-request) module to execute app handlers in-process without having to start background http server
2019
* Simpler setup as it doesn't require managing the internal http server
20+
* Support for applications that require asynchronous setup (for example reading config from network, or decrypting secrets from KMS)
2121
* It's faster, because it doesn't need to pass the request to the internal server through the unix socket
2222
* It's free from issues caused by limits in Node.js http module such as header size limit
2323

@@ -49,45 +49,34 @@ module.exports = { handler }
4949

5050
If the above file in your Lambda source was called `index.js` then the name of the handler in the Lambda configuration is `index.handler`
5151

52-
### Advanced example
52+
### Advanced example with asynchronous setup
5353

54-
Sometimes the application needs to read configuration from remote source before it can start processing requests. For example it may need to decrypt some secrets managed by KMS.
54+
Sometimes the application needs to read configuration from remote source before it can start processing requests. For example it may need to decrypt some secrets managed by KMS. For this use case a special helper `deferred` has been provided. It takes a factory function which returns a Promise that resolves to the app instance. The factory function will be called only once.
5555

5656
```javascript
5757
const lambdaRequestHandler = require('lambda-request-handler')
5858
const AWS = require('aws-sdk')
5959
const express = require('express')
6060

61-
const kms = new AWS.KMS()
61+
const createApp = (secret) => {
62+
const app = express();
63+
app.get('/secret', (req, res) => {
64+
res.json({
65+
secret: secret,
66+
})
67+
})
68+
}
6269

63-
const myKmsPromise = async () => {
70+
const myAppPromise = async () => {
71+
const kms = new AWS.KMS()
6472
const data = await kms.decrypt({
6573
CiphertextBlob: Buffer.from(process.env.ENCRYPTED_SECRET, 'base64')
6674
}).promise()
67-
process.env.SECRET = data.Plaintext.toString('ascii')
75+
const secret = data.Plaintext.toString('ascii')
76+
return createApp(secret);
6877
};
6978

70-
const app = express()
71-
72-
app.get('/secret', (req, res) => {
73-
res.json({
74-
secret: process.env.SECRET,
75-
})
76-
})
77-
78-
const myAppHandler = lambdaRequestHandler(app)
79-
80-
let _myKmsPromise;
81-
82-
const handler = async (event) => {
83-
if (!_myKmsPromise) {
84-
// _myKmsPromise is in global scope so that only one request to KMS is made during this Lambda lifecycle
85-
_myKmsPromise = myKmsPromise();
86-
}
87-
await _myKmsPromise;
88-
// at this point the secret is decrypted and available as process.env.SECRET to the app
89-
return myAppHandler(event)
90-
}
79+
const handler = lambdaRequestHandler.deferred(myAppPromise);
9180

9281
module.exports = { handler }
9382

src/lambda.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
import { RequestListener } from 'http';
2-
32
import inProcessRequestHandler from 'in-process-request';
4-
import { APIGatewayEvent } from './apiGateway';
3+
import * as apigw from './apiGateway';
54
import eventToRequestOptions from './eventToRequestOptions'
65
import { inProcessResponseToApiGatewayResponse, errorResponse } from './response';
76

8-
const handler = (app: RequestListener) => {
9-
const appHandler = inProcessRequestHandler(app);
10-
return (event: APIGatewayEvent) => {
11-
const reqOptions = eventToRequestOptions(event);
12-
return appHandler(reqOptions)
7+
declare namespace handler {
8+
type APIGatewayEvent = apigw.APIGatewayEvent;
9+
type APIGatewayResponse = apigw.APIGatewayResponse;
10+
type APIGatewayEventHandler = (event: handler.APIGatewayEvent) => Promise<handler.APIGatewayResponse>
11+
};
12+
13+
const handlerPromise = (appPromiseFn: () => Promise<RequestListener>): handler.APIGatewayEventHandler => {
14+
let _p: Promise<RequestListener> | null = null;
15+
return event => {
16+
if (!_p) {
17+
_p = appPromiseFn();
18+
}
19+
return _p
20+
.then(app => {
21+
const reqOptions = eventToRequestOptions(event);
22+
const appHandler = inProcessRequestHandler(app);
23+
return appHandler(reqOptions);
24+
})
1325
.then(inProcessResponseToApiGatewayResponse)
1426
.catch(e => {
1527
console.error(e);
1628
return errorResponse();
1729
});
18-
}
19-
};
30+
}
31+
}
32+
33+
const handler = (app: RequestListener): handler.APIGatewayEventHandler => handlerPromise(() => Promise.resolve(app));
34+
35+
handler.deferred = handlerPromise;
2036

2137
export = handler;

test/integration.deferred.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import app from './app';
2+
import lambda from '../src/lambda';
3+
4+
let __test = 0;
5+
const handler = lambda.deferred(() => new Promise(resolve => {
6+
__test = __test + 1;
7+
setTimeout(() => {
8+
resolve(app);
9+
}, 10);
10+
}));
11+
12+
describe('integration for deferred app', () => {
13+
it('returns static file', () => {
14+
const myEvent = {
15+
path: "/static/file.png",
16+
httpMethod: "GET",
17+
headers: {},
18+
queryStringParameters: {},
19+
isBase64Encoded: false,
20+
body: null
21+
}
22+
return handler(myEvent).then(response => {
23+
expect(response.statusCode).toEqual(200);
24+
expect(response.isBase64Encoded).toEqual(true);
25+
expect(response.headers["content-type"]).toEqual('image/png');
26+
expect(response.headers["content-length"]).toEqual('178');
27+
});
28+
});
29+
30+
it('resolves the app promise only once', () => {
31+
const myEvent = {
32+
path: "/static/file.png",
33+
httpMethod: "GET",
34+
headers: {},
35+
queryStringParameters: {},
36+
isBase64Encoded: false,
37+
body: null
38+
}
39+
return handler(myEvent)
40+
.then(() => {
41+
expect(__test).toEqual(1);
42+
return handler(myEvent);
43+
}).then(() => {
44+
expect(__test).toEqual(1);
45+
});
46+
});
47+
})

0 commit comments

Comments
 (0)