@cazoo/events

Raise Cazoo business events

Usage no npm install needed!

<script type="module">
  import cazooEvents from 'https://cdn.skypack.dev/@cazoo/events';
</script>

README

@cazoo/events

Used for raising and parsing Cazoo business events. Abstracts away EventBridge, which we are using as an event bus.

Basic Usage

EventClient.put Validates all events and will fail with a validation exception at the first event that fails validation.

import { EventClient, DoNotValidator, parse } from '@cazoo/events';

export const someHandler = async (ev, context) => {
  const { name, payload } = parse(ev)

  /* Implement your Lambda logic here */

  // Raise a new event on the shared EventBridge event bus.
  const validator = new DoNotValidator();
  const client = new EventClient({ context, validator });
  const event = {
    name: "orderPlaced",
    payload: { foo: 'bar' }
  };
  await client.put(event)
}

EventClient.putEvents: validates all events and in case of failures raises a ValidationErrorMultiple exception, which maps all failed events with the outcome of their validation (publication is skipped).

import { EventClient, DoNotValidator, parse } from '@cazoo/events';

export const someHandler = async (ev, context) => {
  const { name, payload } = parse(ev)

  /* Implement your Lambda logic here */

  // Raise a new event on the shared EventBridge event bus.
  const validator = new DoNotValidator();
  const client = new EventClient({ context, validator });
  const event = {
    name: "orderPlaced",
    payload: { foo: 'bar' }
  };
  await client.putEvents(event)
}
## Targeting a custom event bus

It's possible to use an Environment Variable `CAZOO_EVENTS_TARGET_EVENT_BUS` to use a custom event bus, you can also manually use a
custom event bus using the information below.

```ts
const client = new EventClient({
  context: myContext,
  eventBusName: "custom-event-bus"
});

Validating target event bus

If you wish to provide a custom event bus as your target, you can also optionally validate the existence of the event bus by using the eventBusValidation boolean as shown below:

const client = new EventClient({
  context: myContext,
  eventBusName: "custom-event-bus",
  eventBusValidation: true
})

IAM Permissions

If you want to use event bus validation, then you will need the events:DescribeEventBus IAM permission on your Lambda.

Note: If eventBusValidation is set, then the eventBusName parameter or the environment variable CAZOO_EVENTS_TARGET_EVENT_BUS must be provided, else an error will throw

Configuring the event bridge client

You can provide your own config for the event bridge client the library uses.

const client = new EventClient({
  context: myContext,
  clientConfig: { endpoint: 'http://localhost:3213' }
});

If eventBusName parameter is not provided, the default event bus is targeted, instead.

Runtime validation

We now have support for runtime validation of events via the EventClient class. This class will dynamically fetch schemas from the Eventbridge Schema Registry and validate messages before they are raised.

// We use the ARN of the schema registry to look up event schemas
const registryArn = process.env.CAZOO_EVENT_REGISTRY_ARN || 
  "arn:aws:schemas:eu-west-1:571578941547:registry/cazoo-events"

const validator = new DynamicSchemaValidator(registryArn)
// We create a client passing a validator and the lambda context
const client = new EventClient({
  context,
  validator,
});

try {
  const result = await client.put({
    name: "deliveryCancelled",
    payload: {
      orderNumber: "abc-123"
    }
  });
} catch (e) {
  console.log(e.message);
  console.log(JSON.stringify(e.errors, null, 2))
}
/* Yields the following error

data should NOT have additional properties, data should have required property 'order_number'

[
  {
    "keyword": "additionalProperties",
    "dataPath": "",
    "schemaPath": "#/additionalProperties",
    "params": {
      "additionalProperty": "orderNumber"
    },
    "message": "should NOT have additional properties"
  },
  {
    "keyword": "required",
    "dataPath": "",
    "schemaPath": "#/required",
    "params": {
      "missingProperty": "order_number"
    },
    "message": "should have required property 'order_number'"
  }
]
*/

If no validator is passed, validation will not be enabled. You can also explicitly disable validation with the DoNotValidator.

const validator = new DoNotValidator()
const client = new EventClient({ context, validator });
// This won't throw
const result = await client.put({
  name: "anyOldRubbish",
  payload: {
    foo: "bar"
  }
});

IAM Permissions

Your lambda will require the following IAM permissions to validate schemas.

Serverless

iamRoleStatements:
  - Effect: Allow
    Action: 
      - schemas:DescribeSchema
    Resource: 
      - arn:aws:schemas:${AWS::Region}:${AWS::AccountId}:schema/REGISTRY_NAME/SCHEMA_NAME
      - ...

Terraform

variable "account_id" {}
variable "region" {}
variable "registry_name" {}
variable "schema_name {}

data "aws_iam_policy_document" "allow_describe_schema" {
  statement {
    sid = "AllowDescribeSchema"

    actions = [
      "schemas:DescribeSchema"
    ]

    resources = [
      "arn:aws:schemas:${var.region}:${var.account_id}:schema/${var.registry_name}/${var.schema_name}",
      ...
    ]
  }
}

resource "aws_iam_policy" "allow_describe_schema" {
  name   = "allow-describe-schema"
  path   = "/"
  policy = data.aws_iam_policy_document.allow_describe_schema.json
}

# attach the policy to your lambda role.
resource "aws_iam_role_policy_attachment" "attach_allow_describe_schema" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = aws_iam_policy.allow_describe_schema.arn
}

Tests

Running the acceptance tests requires authentication with cazoo-dev aws account

Releases

We strictly follow semantic versioning so please consider this when working on this library.

To create a new release follow these steps:

  • merge the PR on master
  • execute from any local machine, staying on the master/main branch, the npm version patch/minor/major

The tests will be automatically run, the tag will be pushed and the pipeline will get automatically triggered by the new pushed tag and will deploy to NPM too.