Code Lambda for fun and profit.
This library trivializes creating powerful AWS Lambda handler functions with minimal boilerplate.
This creates a handler that invokes another AWS Lambda function:
import { asClass } from 'awilix'
import { LambdaClient } from '@pureskillgg/awsjs'
import { ssmString } from '@pureskillgg/ace'
import { invokeHandler } from '@pureskillgg/glhf'
const parameters = {
blueLambdaFunction: ssmString('BLUE_LAMBDA_FUNCTION_SSM_PATH')
}
const createProcessor = ({ blueLambdaClient, log }) => async (
event,
container
) => {
return blueLambdaClient.invokeJson(event)
}
const registerDependencies = (container, config) => {
container.register(
'blueLambdaClient',
asClass(LambdaClient).inject(() => ({
name: 'blue',
functionName: config.blueLambdaFunction,
AwsLambdaClient: undefined,
params: undefined
}))
)
}
export const createHandler = invokeHandler({
parameters,
createProcessor,
registerDependencies
})
export const handler = createHandler(parameters)
Add this as a dependency to your project using npm with
$ npm install @pureskillgg/glhf
or using Yarn with
$ yarn add @pureskillgg/glhf
A trivial handler that does nothing may be created like this
import { invokeHandler } from '@pureskillgg/glhf'
const createHandler = invokeHandler()
export const handler = createHandler()
To create a more useful handler, leverage
parameters
, createProcessor
, and registerDependencies
,
import { invokeHandler } from '@pureskillgg/glhf'
const createHandler = invokeHandler({ createProcessor, registerDependencies })
export const handler = createHandler(parameters)
- Use
parameters
to load configuration with AWS Config Executor. - Use
registerDependencies
to register things with side-effects that should be mocked out in tests. This function has access to the configuration loaded viaparameters
. - Use
createProcessor
to define the business logic for handling each event. This function is registered with the Awilix container as a factory function, thus it can access all dependencies registered usingregisterDependencies
.
All exported handler functions return a new handler factory with identical signature:
parameters
: The AWS Config Executor parameters to load.t
: The AVAt
object (if running inside AVA).overrideDependencies
: A function with signature(container, config) => void
which will be called immediately afterregisterDependencies
.
These arguments are all designed to facilitate testing.
See handlers
and test/handlers
.
The async function returned by the factory has a signature (event, context) => any
matching the signature expected by AWS Lambda.
All handlers execute these steps in order:
- Load the config defined by the parameters.
- Create a new Awilix container and register the default dependencies:
log
,reqId
, andprocessor
. - Parse the event with the parser.
- Execute the processor on the event using the configured strategy and wrapper.
- Serialize and return the result.
The invokeHandler
handles AWS Lambda invocation events.
An invocation event is a plain JSON serializable JavaScript object. This handler may thus be used to handle any invocation event if a more specific handler is not provided.
import { invokeHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return { hello: 'world' }
}
const createHandler = invokeHandler({ createProcessor })
export const handler = createHandler()
The eventbridgeHandler
handles EventBridge events.
Additionally, this handler supports redriving events from dead-letter
queues connected to both onFailure
and event rules.
The full lifecycle of an event is explained below:
- EventBridge invokes the function as an Asynchronous invocation.
- If EventBridge cannot invoke the target function, it will put the event in the DLQ after exhausting the allotted retry attempts.
- If EventBridge can invoke the function it will consider the event delivered, even if the function invocation throws an error.
- If the asynchronous function invocation throws an error, it will put the invocation record in the DLQ after exhausting the allotted retry attempts.
- If the DLQ message is from EventBridge, the message body is the event record.
- If the DLQ message is from Lambda, the message body contains
the event record inside the
requestPayload
attribute.
The example below shows how to configure a Serverless function that will deliver failed messages to a DLQ and allow redriving failed messages with the same function.
import { eventbridgeHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return { type: event['detail-type'] }
}
const createHandler = invokeHandler({ createProcessor })
export const handler = createHandler()
event:
handler: handlers/event.handler
destinations:
onFailure: ${ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
events:
- eventBridge:
eventBus: ${ssm:${self:custom.ssmPrefix}/eventbus_arn}
deadLetterQueueArn: {ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
- sqs:
enabled: false
arn: ${ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
batchSize: 1
The sqsHandler
handler handles SQS events.
Since SQS events contain multiple messages, the default strategy for this handler will execute the processor on each message in parallel. Each message will be processed in a child Awilix scope.
The sqsJsonHandler
behaves like the sqsHandler
except
it will parse the SQS message body as JSON.
The httpHandler
handler handles API Gateway Proxy events.
The handler will catch all processor errors, wrap them with Boom, and return a basic status code response. If the processor throws a Boom error, its status code will be respected.
The httpJsonHandler
behaves like the httpHandler
except
it will parse the request body as JSON, and if the processor
returns an object with a body
property, it will
serialize that to JSON and add any missing response properties.
import { sqsJsonHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return event.body
}
const createHandler = sqsJsonHandler({ createProcessor })
export const handler = createHandler()
The handler functions take additional options:
logOptions
, parser
, serializer
, createWrapper
, and createStrategy
.
These are advanced features which are stable, but not yet fully documented.
They are used internally to create the included handler factories.
Please refer to the code for how they may be used.
A parser is a synchronous function which transforms the raw AWS Lambda event before it is passed to the processor.
A serializer is a synchronous function which transforms the output of the processor into the final return value of the AWS Lambda function.
Parsers and serializers should be agnostic to details of user input and response content. They are not expected to throw runtime errors. If a parser or serializer throws, it indicates a bug in its implementation, or a bad configuration (e.g., trying to parse payloads for the wrong event type).
A wrapper function must call a strategy with the event and context. It may optionally call the logger, parser, and serializer.
A strategy has access to the Awilix container scoped to the event. It should resolve and call the processor and optionally handle errors.
$ git clone https://github.com/pureskillgg/glhf.git
$ cd glhf
$ nvm install
$ yarn install
Run the command below in a separate terminal window:
$ yarn run test:watch
Primary development tasks are defined under scripts
in package.json
and available via yarn run
.
View them with
$ yarn run
The source code is hosted on GitHub. Clone the project with
$ git clone [email protected]:pureskillgg/glhf.git
You will need Node.js with npm, Yarn, and a Node.js debugging client.
Be sure that all commands run under the correct Node version, e.g., if using nvm, install the correct version with
$ nvm install
Set the active version for each shell session with
$ nvm use
Install the development dependencies with
$ yarn install
Use the npm version
command to release a new version.
This will push a new git tag which will trigger a GitHub action.
Publishing may be triggered using on the web using a version workflow_dispatch on GitHub Actions.
Serverless deployment is triggered by a release repository_dispatch on GitHub Actions.
Deployment may be triggered using on the web using a release workflow_dispatch on GitHub Actions.
GitHub Actions should already be configured: this section is for reference only.
The following repository secrets must be set on GitHub Actions:
NPM_TOKEN
: npm token for publishing packages.AWS_DEFAULT_REGION
: The AWS region Serverless will deploy to.AWS_ACCESS_KEY_ID
: AWS access key ID.AWS_SECRET_ACCESS_KEY
: AWS secret access key.GH_TOKEN
: A personal access token that can trigger workflows.GRAFANA_API_ORIGIN
: The Grafana origin to push annotations.GRAFANA_API_KEY
: The Grafana key key for pushing annotations.DISCORD_WEBHOOK
: The Discord webhook to notify on deploy.SENTRY_AUTH_TOKEN
: The Sentry auth token.
These must be set manually.
The version and format GitHub actions require a user with write access to the repository. including access to trigger workflows and read and write packages.
Set these additional secrets to enable the action:
GH_USER
: The GitHub user's username.GH_TOKEN
: A personal access token for the user.GIT_USER_NAME
: The GitHub user's real name.GIT_USER_EMAIL
: The GitHub user's email.GPG_PRIVATE_KEY
: The GitHub user's GPG private key.GPG_PASSPHRASE
: The GitHub user's GPG passphrase.
Please submit and comment on bug reports and feature requests.
To submit a patch:
- Fork it (https://github.com/pureskillgg/glhf/fork).
- Create your feature branch (
git checkout -b my-new-feature
). - Make changes.
- Commit your changes (
git commit -am 'Add some feature'
). - Push to the branch (
git push origin my-new-feature
). - Create a new Pull Request.
This npm package is licensed under the MIT license.
Portions of code in this project were taken and adapted from @meltwater/jackalambda which is licensed under the MIT license.
This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.