Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a2f051c
Set up TypeScript
seb-cr Aug 17, 2022
c22b93f
Delete src and tests
seb-cr Aug 17, 2022
e22f03a
Implement type-safe dependency injection
seb-cr Aug 17, 2022
b9a9f83
Configure Jest
seb-cr Aug 17, 2022
ef4c36e
Add tests to cover package exports
seb-cr Aug 18, 2022
5de714e
Use lambda types exported from package root
seb-cr Aug 18, 2022
15f6a96
Add configuration for SQSService
seb-cr Aug 18, 2022
76a9cfc
Update docs with dependency and config examples
seb-cr Aug 18, 2022
aa5b879
Accept partial configs in `configure`
seb-cr Aug 18, 2022
d671144
Type-check custom config keys
seb-cr Aug 19, 2022
7dbe20b
Add JSDoc to SQSService
seb-cr Aug 19, 2022
1b4d371
Remove TODO comment
seb-cr Aug 19, 2022
912887a
Migrate LoggerService
seb-cr Aug 19, 2022
b822a6c
Improve clarity in Dependencies section of readme
seb-cr Aug 19, 2022
e5b2420
Migrate TimerService and add tests
seb-cr Aug 19, 2022
5901efe
Migrate ResponseModel
seb-cr Aug 19, 2022
86732e2
Migrate RequestService
seb-cr Aug 20, 2022
1222512
Migrate StatusModel
seb-cr Aug 20, 2022
e7c9a47
Migrate SQSService
seb-cr Aug 20, 2022
012eaf6
Add type definitions for `alai` package
seb-cr Aug 22, 2022
dfa703c
Write v2 migration guide
seb-cr Aug 22, 2022
e5ffa8f
Migrate LambdaTermination
seb-cr Aug 22, 2022
1e6329e
Migrate PromisifiedDelay
seb-cr Aug 22, 2022
0743841
Migrate BaseConfigService
seb-cr Aug 22, 2022
7e5db45
Migrate HTTPService
seb-cr Aug 22, 2022
a778720
Rename core modules to match class names
seb-cr Aug 23, 2022
0cd2025
Document models that will not be migrated
seb-cr Aug 23, 2022
2a7e443
Migrate and improve missing model tests
seb-cr Aug 23, 2022
364ebd1
Test config module
seb-cr Aug 23, 2022
2914ea3
Migrate LambdaWrapper
seb-cr Aug 24, 2022
7877f84
Test LambdaWrapper
seb-cr Aug 24, 2022
1bfd96c
Simplify use of AWS event and context mocks
seb-cr Aug 24, 2022
fb21313
Mute logs via LoggerService instead of stdout
seb-cr Aug 24, 2022
1cc4a29
Add tests for public class properties
seb-cr Aug 24, 2022
f69d3fa
Consistently import stuff in examples in docs
seb-cr Aug 24, 2022
7e3bf1e
Update package export tests
seb-cr Aug 24, 2022
64bb69e
Fix typo in readme
seb-cr Aug 26, 2022
53764c8
More consistency in examples
seb-cr Aug 26, 2022
a3d12b3
Use `Object.values` to get list of dependency classes
seb-cr Dec 2, 2022
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
7 changes: 3 additions & 4 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
root: true

extends:
- '@comicrelief/eslint-config'
- '@comicrelief/eslint-config/mixins/jsdoc'

parser: '@babel/eslint-parser'
- '@comicrelief/eslint-config/mixins/base'
- '@comicrelief/eslint-config/mixins/ts'

ignorePatterns:
- node_modules
Expand All @@ -14,3 +12,4 @@ ignorePatterns:

rules:
unicorn/prefer-node-protocol: off
'@typescript-eslint/no-explicit-any': off
6 changes: 6 additions & 0 deletions .nycrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extends: '@istanbuljs/nyc-config-typescript'

all: true
check-coverage: true
include:
- src/**
219 changes: 172 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,220 @@
[![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)
[![semantic-release](https://badge.fury.io/js/%40comicrelief%2Flambda-wrapper.svg)](https://www.npmjs.com/package/@comicrelief/lambda-wrapper)

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.
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.

## Installation & usage
If you're coming from v1 and updating to v2, check out the [v2 migration guide](docs/migration/v2.md).

Install via npm:
## Getting started

```bash
npm install --save @comicrelief/lambda-wrapper
```

Or via yarn:
Install via npm or Yarn:

```bash
npm i @comicrelief/lambda-wrapper
# or
yarn add @comicrelief/lambda-wrapper
```

You can then wrap your lambdas as follows.
You can then wrap your Lambda handler functions like this:

```js
import {
LambdaWrapper,
```ts
// src/action/Hello.ts
import lambdaWrapper, {
ResponseModel,
RequestService,
} from '@comicrelief/lambda-wrapper';

export default LambdaWrapper({}, (di, request, done) => {
const response = new ResponseModel({}, 200, `hello ${request.get('name', 'nobody')}`);
done(null, response.generate());
export default lambdaWrapper.wrap(async (di) => {
const request = di.get(RequestService);
return ResponseModel.generate(
{},
200,
`hello ${request.get('name', 'nobody')}`,
);
});
```

## Serverless Offline & SQS Emulation
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:

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.
```ts
// src/config/LambdaWrapper.ts
import lambdaWrapper from '@comicrelief/lambda-wrapper';

Offline SQS behaviour can be configured by setting the `LAMBDA_WRAPPER_OFFLINE_SQS_MODE` environment variable. Available modes are:
export default lambdaWrapper.configure({
// your config goes here
});
```

- `direct` (the default): invokes the consumer function directly via an offline Lambda endpoint
- `local`: send messages to an offline SQS endpoint, such as Localstack
- `aws`: no special handling of SQS offline; messages will be sent to AWS
`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.

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.
Read the next section to see what goes inside the config object!

### Direct Lambda mode
If you want to start from scratch without the built-in dependencies, you can use the `LambdaWrapper` constructor directly.

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.
```ts
// src/config/LambdaWrapper.ts
import { LambdaWrapper } from '@comicrelief/lambda-wrapper';

To take advantage of SQS emulation, you will need to define the following in the implementing service:
export default new LambdaWrapper({
// your config goes here
});
```

**QUEUE_CONSUMERS**
## Dependencies

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.
Lambda Wrapper comes with some commonly used dependencies built in:

You will need to export `QUEUE_CONSUMERS` as part of your default export, alongside `DEFINITIONS`, `DEPENDENCIES`, `QUEUES`, `QUEUE_DEFINITIONS`, etc.
- [HTTPService](docs/services/HTTPService.md)
- [LoggerService](docs/services/LoggerService.md)
- [RequestService](docs/services/RequestService.md)
- [SQSService](docs/services/SQSService.md)
- [TimerService](docs/services/TimerService.md)

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).
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:

**process.env.SERVICE_LAMBDA_URL**
```ts
export default lambdaWrapper.wrap(async (di) => {
const request = di.get(RequestService);
const sqs = di.get(SQSService);
// ...
});
```

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.
To add your own dependencies, first extend `DependencyAwareClass`.

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:
```ts
// src/services/MyService.ts
import { DependencyAwareClass } from '@comicrelief/lambda-wrapper';

offline: Offline [http for lambda] listening on http://localhost:3002
export default class MyService extends DependencyAwareClass {
doSomething() {
// ...
}
}
```

#### Caveats
Then add it to your Lambda Wrapper configuration in the `dependencies` key.

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.
```ts
// src/config/LambdaWrapper.ts
import lambdaWrapper from '@comicrelief/lambda-wrapper';

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).
import MyService from '@/src/services/MyService';

3. If the triggered lambda incurs an exception, this will be propagated upstream, effectively killing the execution of the calling lambda.
export default lambdaWrapper.configure({
dependencies: {
MyService,
},
});
```

Now you can use it inside your handler functions and other dependencies!

```ts
// src/action/DoSomething.ts
import lambdaWrapper from '@/src/config/LambdaWrapper';
import MyService from '@/src/services/MyService';

export default lambdaWrapper.wrap(async (di) => {
di.get(MyService).doSomething();
});
```

## Service config

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.

```ts
export default lambdaWrapper.configure({
dependencies: {
// your dependencies
},
sqs: {
// your SQSService config
},
// ... other configs ...
});
```

To use config with your own dependencies, you need to do three things:

1. Define the key and type of your config object.

Using `SQSService` as an example, we have the `sqs` key which has the `SQSServiceConfig` type:

```ts
export interface SQSServiceConfig {
queues?: Record<string, string>;
queueConsumers?: Record<string, string>;
}
```

2. Define a type that can be applied to a Lambda Wrapper config.

This simply combines the key and type defined in step 1. Conventionally we name these `With...` types.

```ts
export interface WithSQSServiceConfig {
sqs?: SQSServiceConfig;
}
```

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.

3. In your dependency constructor, cast the config to this type.

```ts
export default class SQSService extends DependencyAwareClass {
constructor(di: DependencyInjection) {
super(di);

const config = (this.di.config as WithSQSServiceConfig).sqs;
// Bear in mind that because the `sqs` key is optional, the type of
// `config` will be `SQSServiceConfig | undefined`. Take care when
// accessing its properties! You can use optional chaining:
const queues = config?.queues || {};
// ...
}
}
```

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.

```ts
lambdaWrapper.configure<WithSQSServiceConfig>({
sqs: {
queues: 42 // Oops! This will be flaggeed as a type error by TypeScript
},
});
```

You can combine types for multiple dependencies if needed using `&`:

```ts
lambdaWrapper.configure<WithSQSServiceConfig & WithOtherServiceConfig>({
sqs: {
// SQSService config
},
other: {
// OtherService config
},
});
```

### Local SQS mode
## Development

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.
### Testing

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`.
Also, if you need to change the port, you can set `process.env.LAMBDA_WRAPPER_OFFLINE_SQS_PORT`.
Run `yarn test` to run the unit tests.

### AWS SQS mode
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.

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.
When adding a feature, ensure it's covered by tests that adequately define its behaviour.

In order for queue URLs to be correctly constructed, you must either:
### Linting

- set `AWS_ACCOUNT_ID` to the account ID that hosts your queue; or
- invoke offline functions via the Lambda API, passing a context that contains a realistic `invokedFunctionArn` including the account ID.
Run `yarn lint` to check code style complies to our standard. Many problems can be auto-fixed using `yarn lint --fix`.

## Semantic release
### Releases

Release management is automated using [semantic-release](https://www.npmjs.com/package/semantic-release).
13 changes: 0 additions & 13 deletions babel.config.js

This file was deleted.

Loading