Skip to content

Commit 7500846

Browse files
authored
Merge pull request #1195 from comicrelief/beta
chore: Release v2.0.0
2 parents d2709ba + 59b2450 commit 7500846

File tree

90 files changed

+5762
-7223
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+5762
-7223
lines changed

.eslintrc.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
root: true
22

33
extends:
4-
- '@comicrelief/eslint-config'
5-
- '@comicrelief/eslint-config/mixins/jsdoc'
6-
7-
parser: '@babel/eslint-parser'
4+
- '@comicrelief/eslint-config/mixins/base'
5+
- '@comicrelief/eslint-config/mixins/ts'
86

97
ignorePatterns:
108
- node_modules
@@ -14,3 +12,4 @@ ignorePatterns:
1412

1513
rules:
1614
unicorn/prefer-node-protocol: off
15+
'@typescript-eslint/no-explicit-any': off

.github/workflows/main.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,32 @@ jobs:
4949
env:
5050
CI: true
5151

52+
type-test:
53+
name: Type test
54+
runs-on: ubuntu-latest
55+
56+
steps:
57+
- uses: actions/checkout@v2
58+
59+
- uses: actions/setup-node@v1
60+
with:
61+
node-version: 14
62+
63+
- name: Restore cache
64+
uses: actions/cache@v2
65+
with:
66+
path: '**/node_modules'
67+
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
68+
69+
- run: yarn install
70+
71+
- run: yarn test:types
72+
5273
publish:
5374
name: Publish
5475
runs-on: ubuntu-latest
5576
needs: unit-test
56-
if: github.ref == 'refs/heads/master'
77+
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/beta'
5778
steps:
5879
- name: Checkout
5980
uses: actions/checkout@v2

.nycrc.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extends: '@istanbuljs/nyc-config-typescript'
2+
3+
all: true
4+
check-coverage: true
5+
include:
6+
- src/**

README.md

Lines changed: 206 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,97 +4,254 @@
44
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
55
[![semantic-release](https://badge.fury.io/js/%40comicrelief%2Flambda-wrapper.svg)](https://www.npmjs.com/package/@comicrelief/lambda-wrapper)
66

7-
When writing Serverless endpoints, we have found ourselves replicating a lot of boiler plate code to do basic actions, such as reading request variables or writing to SQS. The aim of this package is to provide a wrapper for our Lambda functions, to provide some level of dependency and configuration injection and to reduce time spent on project setup.
7+
When writing Serverless applications, we have found ourselves replicating a lot of boilerplate code to do basic actions, such as reading request data or sending messages to SQS. The aim of this package is to provide a wrapper for our Lambda functions, to provide some level of dependency and configuration injection and to reduce time spent on project setup.
88

9-
> 🚀 [Lambda Wrapper v2 is now available in beta!](https://github.com/comicrelief/lambda-wrapper/tree/beta) This major release includes TypeScript support and some significant design changes.
9+
If you're coming from v1 and updating to v2, check out the [v2 migration guide](docs/migration/v2.md).
1010

11-
## Installation & usage
11+
## Getting started
1212

13-
Install via npm:
14-
15-
```bash
16-
npm install --save @comicrelief/lambda-wrapper
17-
```
18-
19-
Or via yarn:
13+
Install via npm or Yarn:
2014

2115
```bash
16+
npm i @comicrelief/lambda-wrapper
17+
# or
2218
yarn add @comicrelief/lambda-wrapper
2319
```
2420

25-
You can then wrap your lambdas as follows.
21+
You can then wrap your Lambda handler functions like this:
2622

27-
```js
28-
import {
29-
LambdaWrapper,
23+
```ts
24+
// src/action/Hello.ts
25+
import lambdaWrapper, {
3026
ResponseModel,
3127
RequestService,
3228
} from '@comicrelief/lambda-wrapper';
3329

34-
export default LambdaWrapper({}, (di, request, done) => {
35-
const response = new ResponseModel({}, 200, `hello ${request.get('name', 'nobody')}`);
36-
done(null, response.generate());
30+
export default lambdaWrapper.wrap(async (di) => {
31+
const request = di.get(RequestService);
32+
return ResponseModel.generate(
33+
{},
34+
200,
35+
`hello ${request.get('name', 'nobody')}`,
36+
);
37+
});
38+
```
39+
40+
Here we've used the default export `lambdaWrapper` which is a preconfigured instance that can be used out of the box. You'll likely want to add your own dependencies and service config using the `configure` method:
41+
42+
```ts
43+
// src/config/LambdaWrapper.ts
44+
import lambdaWrapper from '@comicrelief/lambda-wrapper';
45+
46+
export default lambdaWrapper.configure({
47+
// your config goes here
48+
});
49+
```
50+
51+
`configure` returns a new Lambda Wrapper instance with the given configuration. You'll want to export it and then use this when wrapping your handler functions.
52+
53+
Read the next section to see what goes inside the config object!
54+
55+
If you want to start from scratch without the built-in dependencies, you can use the `LambdaWrapper` constructor directly.
56+
57+
```ts
58+
// src/config/LambdaWrapper.ts
59+
import { LambdaWrapper } from '@comicrelief/lambda-wrapper';
60+
61+
export default new LambdaWrapper({
62+
// your config goes here
63+
});
64+
```
65+
66+
## Dependencies
67+
68+
Lambda Wrapper comes with some commonly used dependencies built in:
69+
70+
- [HTTPService](docs/services/HTTPService.md)
71+
- [LoggerService](docs/services/LoggerService.md)
72+
- [RequestService](docs/services/RequestService.md)
73+
- [SQSService](docs/services/SQSService.md)
74+
- [TimerService](docs/services/TimerService.md)
75+
76+
Access these via dependency injection. You've already seen an example of this where we got `RequestService`. Pass the dependency class to `di.get()` to get its instance:
77+
78+
```ts
79+
export default lambdaWrapper.wrap(async (di) => {
80+
const request = di.get(RequestService);
81+
const sqs = di.get(SQSService);
82+
// ...
83+
});
84+
```
85+
86+
To add your own dependencies, first extend `DependencyAwareClass`.
87+
88+
```ts
89+
// src/services/MyService.ts
90+
import { DependencyAwareClass } from '@comicrelief/lambda-wrapper';
91+
92+
export default class MyService extends DependencyAwareClass {
93+
doSomething() {
94+
// ...
95+
}
96+
}
97+
```
98+
99+
If you need to override the constructor, it must take a `DependencyInjection` instance and pass it to `super`.
100+
101+
```ts
102+
export default class MyService extends DependencyAwareClass {
103+
constructor(di: DependencyInjection) {
104+
super(di);
105+
// now do your other constructor stuff
106+
}
107+
}
108+
```
109+
110+
Then add it to your Lambda Wrapper configuration in the `dependencies` key.
111+
112+
```ts
113+
// src/config/LambdaWrapper.ts
114+
import lambdaWrapper from '@comicrelief/lambda-wrapper';
115+
116+
import MyService from '@/src/services/MyService';
117+
118+
export default lambdaWrapper.configure({
119+
dependencies: {
120+
MyService,
121+
},
122+
});
123+
```
124+
125+
Now you can use it inside your handler functions and other dependencies!
126+
127+
```ts
128+
// src/action/DoSomething.ts
129+
import lambdaWrapper from '@/src/config/LambdaWrapper';
130+
import MyService from '@/src/services/MyService';
131+
132+
export default lambdaWrapper.wrap(async (di) => {
133+
di.get(MyService).doSomething();
37134
});
38135
```
39136

40-
## Serverless Offline & SQS Emulation
137+
## Service config
41138

42-
Serverless Offline only emulates API Gateway and Lambda, so publishing an SQS message would use the real SQS queue and trigger the consumer function (if any) in AWS. When working with offline code, you often want the local functions to be invoked instead.
139+
Some dependencies need their own config. This goes in per-service keys within your Lambda Wrapper config. For an example, see [SQSService](docs/services/SQSService.md) which uses the `sqs` key.
43140

44-
Offline SQS behaviour can be configured by setting the `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` environment variable. Available modes are:
141+
```ts
142+
export default lambdaWrapper.configure({
143+
dependencies: {
144+
// your dependencies
145+
},
146+
sqs: {
147+
// your SQSService config
148+
},
149+
// ... other configs ...
150+
});
151+
```
152+
153+
To use config with your own dependencies, you need to do three things:
154+
155+
1. Define the key and type of your config object.
156+
157+
Using `SQSService` as an example, we have the `sqs` key which has the `SQSServiceConfig` type:
158+
159+
```ts
160+
export interface SQSServiceConfig {
161+
queues?: Record<string, string>;
162+
queueConsumers?: Record<string, string>;
163+
}
164+
```
165+
166+
2. Define a type that can be applied to a Lambda Wrapper config.
45167

46-
- `direct` (the default): invokes the consumer function directly via an offline Lambda endpoint
47-
- `local`: send messages to an offline SQS endpoint, such as Localstack
48-
- `aws`: no special handling of SQS offline; messages will be sent to AWS
168+
This simply combines the key and type defined in step 1. Conventionally we name these `With...` types.
49169

50-
Details of each mode are documented in the sections below. When you send a message using `SQSService.prototype.publish`, it will check which mode to use and dispatch the message appropriately. These modes take effect only when running offline (as defined by `DependencyInjection.prototype.isOffline`). In a deployed environment, SQS messages will always be sent to AWS SQS.
170+
```ts
171+
export interface WithSQSServiceConfig {
172+
sqs?: SQSServiceConfig;
173+
}
174+
```
51175

52-
### Direct Lambda mode
176+
In the case of `SQSService`, the `sqs` key is optional because this dependency is included by default and not all applications need it. If your dependency requires config in order to work, you can make this a required key.
53177

54-
This is the default mode if `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` is not set. A Lambda client will be created and the message will be delivered to the offline Lambda endpoint, effectively running the consumer function _immediately_ as part of the original Lambda invocation. This works very well in the offline environment because invoking a Lambda function will trigger its whole (local) execution tree.
178+
3. In your dependency constructor, cast the config to this type.
55179

56-
To take advantage of SQS emulation, you will need to define the following in the implementing service:
180+
```ts
181+
export default class SQSService extends DependencyAwareClass {
182+
constructor(di: DependencyInjection) {
183+
super(di);
57184

58-
**QUEUE_CONSUMERS**
185+
const config = (this.di.config as WithSQSServiceConfig).sqs;
186+
// Bear in mind that because the `sqs` key is optional, the type of
187+
// `config` will be `SQSServiceConfig | undefined`. Take care when
188+
// accessing its properties! You can use optional chaining:
189+
const queues = config?.queues || {};
190+
// ...
191+
}
192+
}
193+
```
59194

60-
In your `src/Config/Configuration` define a `QUEUE_CONSUMERS` object. `QUEUE_CONSUMERS` will map the queue name to the fully qualified `FunctionName` that we want to trigger when messages are published to that queue.
195+
When you go to configure your Lambda Wrapper, you can now include your dependency's config type in the generic for `configure` to get IntelliSense completions and type checking for your config keys.
61196

62-
You will need to export `QUEUE_CONSUMERS` as part of your default export, alongside `DEFINITIONS`, `DEPENDENCIES`, `QUEUES`, `QUEUE_DEFINITIONS`, etc.
197+
```ts
198+
lambdaWrapper.configure<WithSQSServiceConfig>({
199+
sqs: {
200+
queues: 42 // Oops! This will be flagged as a type error by TypeScript
201+
},
202+
});
203+
```
63204

64-
A `Configuration` example can be found in the `serverless-prize-platform` repository [here](https://github.com/comicrelief/serverless-prize-platform/blob/master/src/Config/Configuration.js).
205+
You can combine types for multiple dependencies if needed using `&`:
65206

66-
**process.env.SERVICE_LAMBDA_URL**
207+
```ts
208+
lambdaWrapper.configure<WithSQSServiceConfig & WithOtherServiceConfig>({
209+
sqs: {
210+
// SQSService config
211+
},
212+
other: {
213+
// OtherService config
214+
},
215+
});
216+
```
67217

68-
While creating the Lambda client, we need to point it to our offline environment. LambdaWrapper will take care of the specifics, but it will need to know the Lambda endpoint URL. This _can_ and _must_ be specified via the `SERVICE_LAMBDA_URL` environment variable.
218+
## Monitoring
69219

70-
The URL is likely to be your localhost URL and the next available port from the offline API Gateway. So, if you are running Serverless Offline on `http://localhost:3001`, the Lambda URL is likely to be `http://localhost:3002`. You can check the port in the output during Serverless Offline startup by looking for the following line:
220+
At Comic Relief we use [Lumigo](https://lumigo.io/) for monitoring and observability of our deployed services. Lambda Wrapper includes the Lumigo tracer to allow us to tag traces with custom labels and metrics ([execution tags](https://docs.lumigo.io/docs/execution-tags)).
71221

72-
offline: Offline [http for lambda] listening on http://localhost:3002
222+
Lumigo integration works out-of-the-box with Lumigo's [auto-trace feature](https://docs.lumigo.io/docs/serverless-applications#automatic-instrumentation). If you prefer manual tracing, enable it by setting `LUMIGO_TRACER_TOKEN` in your Lambda environment variables.
73223

74-
#### Caveats
224+
And if you don't use Lumigo, don't worry, their tracer will not be instantiated in your functions and no calls will be made to their servers unless `LUMIGO_TRACER_TOKEN` is set.
75225

76-
1. You will be running the SQS-triggered lambdas in the same Serverless Offline context as your triggering lambda. Expect logs from both lambdas in the Serverless Offline output.
226+
## Notes
77227

78-
2. If you await `sqs.publish` you will effectively wait until all SQS-triggered lambdas (and possibly their own SQS-triggered lambdas) have all completed. This is necessary to avoid any pending execution (i.e. the lambda terminating before its async processes are completed).
228+
Lambda Wrapper's dependency injection relies on class names being preserved. If your build process includes minifying or uglifying your code, you'll need to disable these transformations.
79229

80-
3. If the triggered lambda incurs an exception, this will be propagated upstream, effectively killing the execution of the calling lambda.
230+
In many of our projects we use `serverless-webpack` to bundle service code prior to deployment. To disable name mangling, set `optimization.minimize` to `false` in your webpack config:
231+
232+
```js
233+
// webpack.config.js
234+
module.exports = {
235+
// ...
236+
optimization: {
237+
minimize: false,
238+
},
239+
```
81240
82-
### Local SQS mode
241+
## Development
83242
84-
Use this mode by setting `LAMBDA_WRAPPER_OFFLINE_SQS_MODE=local`. Messages will still be sent to an SQS queue, but using a locally simulated version instead of AWS. This allows you to test your service using a tool like Localstack.
243+
### Testing
85244
86-
By default, messages will be sent to a SQS service running on `localhost:4576`. If you need to change the hostname, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_HOST`.
87-
Also, if you need to change the port, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_PORT`.
245+
Run `yarn test` to run the unit tests, and `yarn test:types` to run the type tests.
88246
89-
### AWS SQS mode
247+
When writing a bugfix, start by writing a test that reproduces the problem. It should fail with the current version of Lambda Wrapper, and pass once you've implemented the fix.
90248
91-
Use this mode by setting `LAMBDA_WRAPPER_OFFLINE_SQS_MODE=aws`. Messages will be sent to the real queue in AWS. This mode is useful when a queue is consumed by an external service, rather than another function in the service under test.
249+
When adding a feature, ensure it's covered by tests that adequately define its behaviour.
92250
93-
In order for queue URLs to be correctly constructed, you must either:
251+
### Linting
94252
95-
- set `AWS_ACCOUNT_ID` to the account ID that hosts your queue; or
96-
- invoke offline functions via the Lambda API, passing a context that contains a realistic `invokedFunctionArn` including the account ID.
253+
Run `yarn lint` to check code style complies to our standard. Many problems can be auto-fixed using `yarn lint --fix`.
97254
98-
## Semantic release
255+
### Releases
99256
100257
Release management is automated using [semantic-release](https://www.npmjs.com/package/semantic-release).

babel.config.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)