@othree.io/chisel-lens
v3.0.0
Published
Integration testing utilities for event-sourced projections
Readme
@othree.io/chisel-lens
Integration testing utilities for event-sourced projections. Provides a fluent API for sending commands (or SNS messages), waiting for eventual consistency, validating projection state, and cleaning up test data.
Supported Data Sources
- DynamoDB
- OpenSearch
- RDS Aurora Serverless (with auto-pause warm-up)
Installation
npm install @othree.io/chisel-lensvitest is a peer dependency and must be installed separately.
API
when -- Command-driven projection tests
Sends commands to Lambda command handlers, waits for the projection to materialize, validates the result, then cleans up both the projection and the triggered events. The .and() method accepts a function (contextId: Optional<string>) => TestCommand, where contextId is extracted from the previous command's result events — useful when chained commands need to target the same aggregate created by the first command (e.g. auto-generated IDs).
import { when, ProjectionTestConfiguration } from '@othree.io/chisel-lens'
const config: ProjectionTestConfiguration<MyProjection> = {
eventualConsistencyTimeout: 10000,
pollingInterval: 2000,
dataSourceConfiguration: {
type: ProjectionDataSourceType.Dynamo,
configuration: { tableName: 'my-projection-table' }
},
commandHandlers: [{
bc: 'users',
commandHandlerArn: 'arn:aws:lambda:...:my-command-handler',
eventsTableName: 'my-events-table'
}]
}
const sendCommand = when<MyProjection>(config, credentials)
// Validate projection exists with expected values
await sendCommand({ bc: 'users', command: syncNameCommand })
.and((_contextId) => ({ bc: 'users', command: syncLastNameCommand }))
.expectProjection({ name: 'Chuck', lastName: 'Norris' })
.toExist()
// Use the previous command's contextId for chained commands (e.g. auto-generated IDs)
await sendCommand({ bc: 'users', command: createUserCommand })
.and((contextId) => ({ bc: 'users', command: { ...updateNameCommand, contextId: contextId.orElse('') } }))
.expectProjection({ name: 'Chuck', lastName: 'Norris' })
.toExist()
// Validate projection was NOT created
await sendCommand({ bc: 'users', command: invalidCommand })
.toBeEmpty()whenSNS -- SNS message-driven projection tests
Publishes arbitrary messages to an SNS topic, waits for the projection to materialize, validates the result, then cleans up the projection. Designed for projections that subscribe directly to SNS topics rather than being triggered by command handler events.
import { whenSNS, SNSProjectionTestConfiguration } from '@othree.io/chisel-lens'
const config: SNSProjectionTestConfiguration<MyProjection, MyMessage> = {
eventualConsistencyTimeout: 10000,
topicArn: 'arn:aws:sns:...:my-topic',
getKeys: (messages) => ({ id: messages[0].id }),
dataSourceConfiguration: {
type: ProjectionDataSourceType.Dynamo,
configuration: { tableName: 'my-projection-table' }
},
// Optional FIFO topic support:
getMessageGroupId: (msg) => msg.id,
getMessageDeduplicationId: (msg) => msg.eventId,
getMessageAttributes: (msg) => ({
eventType: { DataType: 'String', StringValue: msg.type }
})
}
const sendMessage = whenSNS<MyProjection, MyMessage>(config, credentials)
await sendMessage({ id: '001', type: 'Created', body: { name: 'Chuck' } })
.and({ id: '001', type: 'Updated', body: { lastName: 'Norris' } })
.expectProjection({ name: 'Chuck', lastName: 'Norris' })
.toExist()warmUp
Warms up the data source before running tests. For RDS Aurora Serverless, this handles auto-pause resume with retry logic. For DynamoDB and OpenSearch, this is a no-op.
import { warmUp } from '@othree.io/chisel-lens'
await warmUp(config, credentials)()waitForProjection
Polls the data source until a projection appears (and optionally matches expected values).
import { waitForProjection } from '@othree.io/chisel-lens'
const wait = waitForProjection<MyProjection>(config, credentials)
const projection = await wait({ id: '001' }, expectedProjection)pollForResult
Generic polling utility. Polls an async function until the result satisfies a predicate, or until timeout.
import { pollForResult } from '@othree.io/chisel-lens'
const result = await pollForResult(
() => fetchSomething(),
(value) => value.status === 'READY',
10000,
2000
)Low-level API
All low-level building blocks are available under the lowLevel namespace for custom compositions:
import { lowLevel } from '@othree.io/chisel-lens'
// lowLevel.when, lowLevel.whenMessage, lowLevel.runAndValidate,
// lowLevel.runAndValidateMessage, lowLevel.pollForProjection, etc.Development
npm install # Install dependencies
npm run build # Compile TypeScript
npm test # Run tests (vitest) with 100% coverage thresholds
npm run docs # Generate JSDoc documentationArchitecture
src/
index.ts # High-level API (when, whenSNS, warmUp, etc.)
projection-utils.ts # Core builder pattern, polling, run-and-validate, cleanup logic
datasource-utils.ts # Data source adapters (DynamoDB, OpenSearch, RDS)
test/
projection-utils.test.ts
datasource-utils.test.ts
index.test.ts