Skip to main content

Events

We generate events for all system events such as the creation / modification of data. These events are published to an AWS event bridge and can be consumed by client apps.

The schemas for events are available in the @hectare/platform.modules.[service].contracts component, so if you are consuming a customers event from the Trading service you would reference the schema from the @hectare/platform.modules.customers.contracts component

Events

All events should derive from the BaseEvent class here components/common/types/base-event.ts \ All event payloads should derive from the BaseEventPayload class here components/common/types/base-event.ts

You can publish events via the eventPublisher on the Context class. The eventPublisher implements the IEventPublisher interface which exposes the following methods

publishAfterCommit<T extends BaseEventPayload>(event: BaseEvent<T>): void
publishBatchAfterCommit<T extends BaseEventPayload>(events: BaseEvent<T>[]): void
publish<T extends BaseEventPayload>(event: BaseEvent<T>): Promise<void>
publishBatch<T extends BaseEventPayload>(events: BaseEvent<T>[]): Promise<void>

This allows you to publish one event or a batch of events. You can publish the events immediately (publish & publishBatch) or, more commonly you can publish the events only after the database session has been successfully committed (publishAfterCommit & publishBatchAfterCommit)

Schema generation process

We looked into the EventBridge schema discovery feature and took some inspiration from this process, but decided it would be more useful to be in control of this process from within our codebase.

The BaseEvent class (components/common/types/base-event.ts) has a public method called schema() which should be overridden in derived classed and a sample schema returned from this method

We have unit tests to ensure a sample schema is provided for all events so this is mandatory, the schema returned should return a sample value for all filds on the event model.

We use factores to centralise generation of example types for tests so often it makes sense to re-use these factories for the event schema generation to reduce code duplication, example of an event schema generated from factories...

import { BaseEventPayload } from '@hectare/platform.components.common'
import { Context, ContextFactory } from '@hectare/platform.components.context'
import { BusinessUnit, BusinessUnits } from '../../models'
import { BusinessUnitsFactory } from '../../factories'

export class BusinessUnitsCreated extends BaseEventPayload {
businessUnits: BusinessUnits

constructor(context: Context, businessUnits: BusinessUnits) {
super(context.event.correlationId, context.user.id, context.user.organisationId)
this.businessUnits = businessUnits
}

public static schema(): string {
return JSON.stringify(new BusinessUnitsCreated(ContextFactory.build(), BusinessUnitsFactory.build()))
}
}

We are using https://quicktype.io/ for interface generation, this is run as part of the pnpm run schemas command

Consuming Events from AWS

Events are consumed by creating an EventBridge rule to route any instances of the event to a Lambda function. There are two types of events we currently support, those triggered by an event and those triggered on a schedule. the Cloudformation is largely the same other thanb the Trigger configuration.

  HandleTradeInventoryAllocated:
Type: AWS::Serverless::Function
Properties:
CodeUri: dist/trade-allocated
Handler: index.handler
TimeOut: 15
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:DescribeLogStreams
Resource: '*'
Events:
Trigger:
Type: EventBridgeRule
Properties:
EventBusName: !ImportValue platform-infrastructure-services-event-bus-name
Pattern:
source:
- trading
detail-type:
- TradeInventoryAllocated

Above is a snippet of a lambda that consumes an event. You need to add an EventBus to consume from our custom event bus. If not supplied, the lambda will consume from the default event bus (which only contains AWS events).To see the different event types SAM Lambda could support, see here.

Consuming Events Locally

Locally we swap the IEventPublisher component so insteaad of publishing events to the AWS EventBridge we just execute them. This allows us to operate the application locally without needing to run any simulation of AWS infrastructure.

This will just work, you can debug events which will generally be triggered after the session on a given request has committed.

See the components/api/events/event-publisher.ts component for details around how we handle events locally

Event Handler Naming Conventions

Each modules events component exports a manifest with a HashMap of all the events handled by the module, for example.

const eventHandlers = EventHandlersBuilder.create()
.addHandler('TradingOrganisationCreated', OrganisationCreated)
.addHandler('TradingOrganisationUpdated', OrganisationUpdated)
.done()

const manifest: IEventsManifest = {
database: Databases.Inventory,
name: 'inventory',
events: new EventsHandler(eventHandlers, 'inventory'),
models
}

The EventHandlersBuilder component ensure we dont have duplicate keys for the event handlers hash map.

The key for each handler is important, there are rules governing this. The key must end with the name of the event that is fired, in the above example we have 2 handlers, one handling the OrganisationCreated event and the other handling the OrganisationUpdated event.

In order to allow us to have multiple handlers for the same event we support the ability to prefix the key with an arbirary string, in this case Trading.

You can then connect a handler to an event raised from the entry point file for the Lambda function, the LambdaHandler is the component we use for this, so when defining the entry point for a Lambda you can specifyt the prefic for the handler key as follows.

import { manifest } from '@hectare/platform.modules.customers.events'
import * as Sentry from '@sentry/node'
import { LambdaHandler } from '@hectare/platform.components.context'

// Trading is the prefix for the handler key, so in this example the event being
// handled is the OrganisationCreated event, but the handler we should invoke has
// a key of TradingOrganisationCreated
const lambdaHandler = new LambdaHandler(manifest, null, 'Trading')

Sentry.init(lambdaHandler.sentry())

export const handler = async (event: unknown, context: unknown) => {
await lambdaHandler.handle(event, context)
}

Tailing events

You can tail the event bus in real time via the terminal. This can be a very useful addition to your development workflow as well as for debugging. An easy way to achieve is this is by using the lumgio-cli. It forwards the events onto a SQS queue which it then tails for you.

function tailPlatformEventBus { lumigo-cli tail-eventbridge-bus -r eu-west-2 -n platform-infrastructure-event-bus }
function tailDefaultEventBus { lumigo-cli tail-eventbridge-bus -r eu-west-2 }

Add the above aliases to your bash profile of choice. You can then tail the bus after installing the cli as per the snippet below.

npm i -g lumigo-cli
export AWS_PROFILE=platform.dev
tailPlatformEventBus
// or tailDefaultEventBus