Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ES modudle directly as handler #13

Closed
wants to merge 7 commits into from
Closed
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
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const createHandler = invokeHandler({
registerDependencies
})

export const handler = createHandler(parameters)
export const handler = await createHandler(parameters)
```

## Installation
Expand Down Expand Up @@ -76,7 +76,7 @@ A trivial handler that does nothing may be created like this
import { invokeHandler } from '@pureskillgg/glhf'

const createHandler = invokeHandler()
export const handler = createHandler()
export const handler = await createHandler()
```

To create a more useful handler, leverage
Expand All @@ -86,7 +86,7 @@ To create a more useful handler, leverage
import { invokeHandler } from '@pureskillgg/glhf'

const createHandler = invokeHandler({ createProcessor, registerDependencies })
export const handler = createHandler(parameters)
export const handler = await createHandler(parameters)
```

- Use `parameters` to load configuration with [AWS Config Executor].
Expand All @@ -97,12 +97,19 @@ export const handler = createHandler(parameters)
This function is registered with the [Awilix] container
as a factory function, thus it can access all dependencies registered
using `registerDependencies`.
- Register a dependency named `init` as an async function which will be called
once when the handler is created.
This allows the function to perform one-time expensive setup on cold starts
or when provisioned concurrency is used.

### Handler Factories

All exported handler functions return a new handler factory with identical signature:
1. `parameters`: The [AWS Config Executor] parameters to load.
2. `t`: The AVA `t` object (if running inside AVA).
If not using AVA, you must pass either `true` or an object with a `log` method
which takes a string message as it's only argument.
Additionally, your test running must set `NODE_ENV=test`.
3. `overrideDependencies`: A function with signature `(container, config) => void`
which will be called immediately after `registerDependencies`.

Expand All @@ -115,10 +122,11 @@ matching the signature expected by AWS Lambda.
All handlers execute these steps in order:
1. Load the config defined by the parameters.
2. Create a new [Awilix] container and register the default dependencies:
`log`, `reqId`, and `processor`.
3. Parse the event with the parser.
4. Execute the processor on the event using the configured strategy and wrapper.
5. Serialize and return the result.
`log`, `reqId`, `init`, and `processor`.
3. Await the `init` function.
4. Parse the event with the parser.
6. Execute the processor on the event using the configured strategy and wrapper.
7. Serialize and return the result.

[AWS Config Executor]: https://github.com/pureskillgg/ace
[Awilix]: https://github.com/jeffijoe/awilix
Expand All @@ -142,7 +150,7 @@ const createProcessor = () => async (event, ctx) => {

const createHandler = invokeHandler({ createProcessor })

export const handler = createHandler()
export const handler = await createHandler()
```

#### EventBridge Handler
Expand Down Expand Up @@ -181,7 +189,7 @@ const createProcessor = () => async (event, ctx) => {

const createHandler = invokeHandler({ createProcessor })

export const handler = createHandler()
export const handler = await createHandler()
```

```yaml
Expand Down Expand Up @@ -239,7 +247,7 @@ const createProcessor = () => async (event, ctx) => {

const createHandler = sqsJsonHandler({ createProcessor })

export const handler = createHandler()
export const handler = await createHandler()
```

### Advanced usage
Expand Down
40 changes: 34 additions & 6 deletions handlers/blue.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
'use strict'
import Sentry from '@sentry/serverless'
import { asFunction, asValue } from 'awilix'
import { envString } from '@pureskillgg/ace'

const Sentry = require('@sentry/serverless')
import { invokeHandler } from '../index.js'

Sentry.AWSLambda.init()

const index = import('./blue.mjs')
const createInit =
({ cache }) =>
async () => {
cache.isInit = true
}

exports.handler = Sentry.AWSLambda.wrapHandler(async (...args) => {
const { handler } = await index
return handler(...args)
const registerDependencies = (container, config) => {
container.register({ rank: asValue(config.rank) })
container.register({
cache: asValue({}),
init: asFunction(createInit).singleton()
})
}

const parameters = {
rank: envString('RANK')
}

const createProcessor =
({ rank, cache }) =>
async (event, ctx) => {
return { ...event, rank, isInit: cache.isInit }
}

export const createHandler = invokeHandler({
registerDependencies,
createProcessor
})

export const handler = Sentry.AWSLambda.wrapHandler(
await createHandler(parameters)
)
25 changes: 0 additions & 25 deletions handlers/blue.mjs

This file was deleted.

1 change: 0 additions & 1 deletion handlers/package.json

This file was deleted.

40 changes: 34 additions & 6 deletions handlers/red.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
'use strict'
import Sentry from '@sentry/serverless'
import { asClass } from 'awilix'
import { LambdaClient } from '@pureskillgg/awsjs'
import { ssmString } from '@pureskillgg/ace'

const Sentry = require('@sentry/serverless')
import { invokeHandler } from '../index.js'

Sentry.AWSLambda.init()

const index = import('./red.mjs')
const parameters = {
blueLambdaFunction: ssmString('BLUE_LAMBDA_FUNCTION_SSM_PATH')
}

exports.handler = Sentry.AWSLambda.wrapHandler(async (...args) => {
const { handler } = await index
return handler(...args)
const createProcessor =
({ blueLambdaClient, log }) =>
async (event, ctx) => {
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 = Sentry.AWSLambda.wrapHandler(
await createHandler(parameters)
)
35 changes: 0 additions & 35 deletions handlers/red.mjs

This file was deleted.

18 changes: 8 additions & 10 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { getConfig } from '@pureskillgg/ace'
import { getConfig as getAceConfig } from '@pureskillgg/ace'

export const createGetConfig =
({ parameters, cache, aliases }) =>
async (ctx, log) =>
getConfig({
parameters,
cache,
aliases,
log: log.child({ isConfigLog: true })
})
export const getConfig = async ({ parameters, cache, aliases, log }) =>
getAceConfig({
parameters,
cache,
aliases,
log: log.child({ isConfigLog: true })
})
22 changes: 20 additions & 2 deletions lib/container.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { asFunction, asValue } from 'awilix'
import { asFunction, asValue, createContainer } from 'awilix'

export { createContainer } from 'awilix'
import { getRequestId } from './request-id.js'
import { createGlobalCtx } from './ctx.js'
import { createLogger } from './logger.js'

export const createDependencies = (env, t) => {
const container = createContainer()
const ctx = createGlobalCtx(env)
const log = createLogger(ctx, t)
const reqId = getRequestId()
const init = async () => {}

container.register({
init: asValue(init),
reqId: asValue(reqId),
log: asValue(log.child({ reqId, isAppLog: false }))
})

return container
}

export const createScope = (container, { ctx, log, createProcessor }) => {
const scope = container.createScope()
Expand Down
4 changes: 4 additions & 0 deletions lib/ctx.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export const createCtx = (event, context) => {
reqId
}
}

export const createGlobalCtx = (env) => ({
functionName: env.AWS_LAMBDA_FUNCTION_NAME
})
29 changes: 17 additions & 12 deletions lib/handlers/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { identityParser } from '../parsers/index.js'
import { identitySerializer } from '../serializers/index.js'
import { createInvokeWrapper } from '../wrappers/index.js'
import { createEventStrategy } from '../strategies/index.js'
import { createContainer, createScope } from '../container.js'
import { createDependencies, createScope } from '../container.js'
import { createCache } from '../cache.js'
import { createGetConfig } from '../config.js'
import { getConfig } from '../config.js'
import { createCtx } from '../ctx.js'

const env = { ...process.env }

export const createHandler =
({
parser = identityParser,
Expand All @@ -18,27 +20,30 @@ export const createHandler =
createStrategy = createEventStrategy,
logOptions = {}
}) =>
(parameters, t, overrideDependencies = registerEmptyDependencies) => {
async (parameters, t, overrideDependencies = registerEmptyDependencies) => {
if (process.env.NODE_ENV === 'test' && !t) return async () => {}

const cache = createCache()
const container = createContainer()
const container = createDependencies(env, t)

const getConfig = createGetConfig({
const config = await getConfig({
parameters,
cache,
aliases: t ? undefined : { ...process.env }
aliases: t ? undefined : env,
log: container.resolve('log')
})

registerDependencies(container, config)
overrideDependencies(container, config)

const init = container.resolve('init')
await init()

return async (event, context = {}) => {
try {
const ctx = createCtx(event, context)
const log = createLogger(ctx, t, logOptions)
const config = await getConfig(ctx, log)

registerDependencies(container, config)
overrideDependencies(container, config)

const scope = createScope(container, { ctx, log, createProcessor })

const strategy = createStrategy(scope)
const handle = createWrapper(log, strategy, parser, serializer)
return handle(event, ctx)
Expand Down
6 changes: 3 additions & 3 deletions lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ const envBase = {
version: process.env.LOG_VERSION
}

export const createLogger = (ctx, t, options) =>
export const createLogger = (ctx, t, options = {}) =>
createMlabsLogger({
name: getName(ctx),
level,
outputMode: isDev ? 'pretty' : 'json',
t,
t: t?.log == null ? { ...t, log: () => {} } : t,
...options,
base: {
...createBase(ctx),
...options.base
...(options?.base ?? {})
}
})

Expand Down
4 changes: 2 additions & 2 deletions test/handlers/blue.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import test from 'ava'
import { localString } from '@pureskillgg/ace'

import { createHandler } from '../../handlers/blue.mjs'
import { createHandler } from '../../handlers/blue.js'

test('invoke', async (t) => {
const event = { foo: 'bar' }
const handler = createHandler(parameters, t)
const handler = await createHandler(parameters, t)
const data = await handler(event)
t.snapshot(data, 'handler')
})
Expand Down
Loading