@othree.io/chisel-sdk
v7.2.1
Published
Utility functions for Chisel
Maintainers
Readme
@othree.io/chisel-sdk
SDK for building event-sourced microservices with Chisel on AWS. Provides pre-wired write-side actors, read-side projections, and CDK infrastructure stacks out of the box.
Installation
npm install @othree.io/chisel-sdkExports
import { aws, iac } from '@othree.io/chisel-sdk'| Namespace | Purpose |
|-----------|---------|
| aws | Runtime Lambda handlers for write-side (actor) and read-side (projections) |
| iac | CDK stack factories for the full CQRS infrastructure |
Write Side (aws.createActor)
Creates a fully wired Chisel actor with DynamoDB event persistence, sharded SNS publishing, and X-Ray tracing.
import { aws } from '@othree.io/chisel-sdk'
const actor = aws.createActor<MyAggregate, MyEvent>({
boundedContext: 'Orders',
now: () => Date.now(),
getInitialState: async (contextId) => Optional(emptyOrder),
handleCommand: myCommandHandler,
reduceEvent: myReducer,
})
// Lambda exports
export const handleCommand = actor.handleCommand
export const getState = actor.getStateEnvironment variables (read automatically):
| Variable | Description |
|----------|-------------|
| EVENTS_TABLE | DynamoDB events table name |
| EVENTS_TOPICS | Comma-separated SNS topic ARNs |
| LOG_LEVEL | Logging level (default: info) |
Returns a LambdaActor<AggregateRoot> with:
handleCommand- processes aLambdaCommandand returns anInvocationResult<CommandResult<AggregateRoot>>getState- loads and returns the current aggregate state asInvocationResult<AggregateRoot>bycontextId
Read Side (aws.getProjectionServices)
Creates a fully wired projection processor with DynamoDB upsert/delete, SQS batch processing, and DLQ routing.
import { aws } from '@othree.io/chisel-sdk'
const projection = aws.getProjectionServices<MyAggregate, MyEvent>({
boundedContext: 'Orders',
now: () => Date.now(),
})
// Lambda exports
export const updateProjection = projection.sqs.updateProjection
export const getById = projection.services.getByIdEnvironment variables (read automatically):
| Variable | Description |
|----------|-------------|
| PROJECTION_TABLE | DynamoDB projection table name |
| PROJECTION_DLQ | Application DLQ URL |
| PROJECTION_PK | Projection table partition key name |
| DELETE_ON_EVENTS | Comma-separated event types that trigger deletion |
| LOG_LEVEL | Logging level (default: info) |
Returns:
sqs.updateProjection- SQS batch handler that upserts or deletes projections based on event typeservices.getById- queries the projection table by ID
Standalone Functions
These are also exported for direct use or custom wiring:
| Function | Description |
|----------|-------------|
| aws.getProcessInput | Parses an SQS record containing an SNS-wrapped Chisel event |
| aws.processEvent | Routes to delete or upsert based on deleteOnEvents configuration |
| aws.getProjectionById | Queries the projection table and throws NotFoundError if empty |
| aws.getProjectionServiceHandler | Wraps a projection query function as a Lambda service handler |
| aws.getConfiguration | Reads projection configuration from environment variables |
Read Side — RDS (aws.getRdsProjectionServices)
The DynamoDB read side above upserts the reduced aggregate as a single row. RDS
projections are typed and multi-table, so the SDK provides a transactional
projector harness instead: it owns the SQS handler, the stethoscope
retry/DLQ wrapping, and one Data-API transaction per event — your bounded
context supplies a project(event, state, tx) function that performs the typed
multi-table writes. getById/snapshot are owned by the bounded context (they
compose multiple tables over @othree.io/awsome's rdsData wrapper) and are
intentionally not provided by the SDK for RDS.
import { aws } from '@othree.io/chisel-sdk'
import { rdsData } from '@othree.io/awsome'
import { Optional } from '@othree.io/optional'
// The bounded context implements this. The harness opens the transaction,
// commits on success, and rolls back (then lets stethoscope retry) if you
// return an empty/errored Optional or throw. event.eventId and
// event.metadata?.client_request_id are the idempotency keys — write idempotent
// SQL (INSERT ... ON CONFLICT) so at-least-once re-delivery is safe.
const project = async (
event: TriggeredEvent<RestaurantEvent>,
state: Restaurant,
tx: rdsData.Transaction,
): Promise<Optional<Restaurant>> => {
await tx.query(
`INSERT INTO restaurant.restaurants (id, name, market_id)
VALUES (:id, :name, :market_id)
ON CONFLICT (id) DO UPDATE SET name = :name`,
{ id: state.id, name: state.name, market_id: state.marketId },
)
return Optional(state)
}
const projection = aws.getRdsProjectionServices<Restaurant, RestaurantEvent>({
boundedContext: 'Restaurant',
now: () => Date.now(),
project,
})
// Lambda export — only the SQS consumer; getById is bounded-context-owned.
export const updateProjection = projection.sqs.updateProjectionEnvironment variables (read automatically):
| Variable | Description |
|----------|-------------|
| DATA_CLUSTER_ARN | Aurora cluster ARN (from the 00.04 SSM data/cluster-arn export) |
| DATA_SECRET_ARN | Cluster credentials secret ARN (data/secret-arn) |
| DATA_DATABASE | Database name (data/database) |
| PROJECTION_SCHEMA | Per-context Postgres schema (e.g. restaurant) — read by your project SQL |
| PROJECTION_DLQ | Application DLQ URL |
| LOG_LEVEL | Logging level (default: info) |
The harness makes no single-table / single-PK / state-blob assumption; every
write goes through the injected tx.query (the @othree.io/awsome rdsData
primitive), which binds values as typed parameters — never interpolated SQL.
Infrastructure (iac.createChiselStacks)
Creates all CDK stacks for a Chisel microservice in a single call.
import { App } from 'aws-cdk-lib'
import { iac } from '@othree.io/chisel-sdk'
const app = new App()
const stacks = iac.createChiselStacks(app, {
boundedContext: 'Orders',
version: 'v1',
projectMetadata: {
project: 'my-project',
environment: 'prod',
owner: 'team-a',
},
context: {
project: { env: 'prod', naming: { prefix: 'myapp' } },
lambda: { functions: [] },
chisel: { numberOfShards: 3 },
},
writeSide: {
codePath: 'dist/write',
},
readSide: {
codePath: 'dist/read',
deleteOnEvents: ['OrderDeleted'],
},
})The context is validated against ChiselServiceContextConstraints at runtime using Zod.
Generated Stacks
| Stack | Description | Output |
|-------|-------------|--------|
| EventBus | FIFO SNS topics (one per shard) | stacks.topics.stack, stacks.topics.topics |
| WritePersistence | DynamoDB events table | stacks.writeSide.persistence.eventsTable |
| WriteServices | Command handler and get-state Lambda functions | stacks.writeSide.services.handleCommandArn, stacks.writeSide.services.getStateArn |
| TopicSubscribers | SQS queues subscribed to SNS topics with DLQ and redrive | stacks.readSide.subscribers.eventQueues, stacks.readSide.subscribers.dlq |
| ProjectionPersistence | DynamoDB projection table with optional GSIs | stacks.readSide.persistence.projectionTable |
| ReadServices | Update-projection and get-by-id Lambda functions | stacks.readSide.servicesStack.stack |
The read side is optional. Omit readSide from the input to create a write-only service.
When readSide.store === 'RDS', ProjectionPersistence is skipped (no DynamoDB
table) and ReadServices contains only the SQS projector Lambda (no get-by-id
Lambda) with rds-data + Secrets-Manager grants instead of table grants.
Context Configuration
The ChiselServiceContext controls stack behavior:
type ChiselServiceContext = Readonly<{
project: {
env: string // Environment name (used as resource suffix)
naming?: { prefix?: string } // Optional resource name prefix
}
lambda: {
functions: Array<{
name: string
reservedConcurrentExecutions?: number
}>
}
chisel: {
numberOfShards: number // Number of FIFO SNS topics
}
}>Read Side Options
readSide is a discriminated union on store. A missing store defaults to
'Dynamo' (backward compatible).
type ReadSideInput = DynamoReadSideInput | RdsReadSideInput
type DynamoReadSideInput = Readonly<{
store?: 'Dynamo'
codePath: string // Lambda code path
tablePk?: string // Projection table PK name (default: 'id')
globalSecondaryIndexes?: Array<GSIInput> // Optional GSIs
deleteOnEvents: Array<string> // Event types that trigger row deletion
}>
type RdsReadSideInput = Readonly<{
store: 'RDS'
codePath: string // Lambda code path
clusterArn: string // Aurora cluster ARN (SSM data/cluster-arn)
secretArn: string // Credentials secret ARN (SSM data/secret-arn)
database: string // Database name (SSM data/database)
schema: string // Per-context Postgres schema, e.g. 'restaurant'
}>For the RDS variant, createChiselStacks provisions the projector Lambda on the
SQS event source and grants rds-data:* (scoped to the cluster ARN) plus
secretsmanager:GetSecretValue (scoped to the secret ARN). It does not create
the Aurora cluster/VPC, define tables, run migrations, or create a
projection-persistence stack or a get-by-id Lambda — those are owned externally
(the cluster by infrastructure, the tables/queries by the bounded context). With
the default aws/secretsmanager KMS key no extra grant is needed; a
customer-managed key would require an additional kms:Decrypt grant (not wired at
MVP).
const stacks = iac.createChiselStacks(app, {
boundedContext: 'Restaurant',
version: 'v1',
projectMetadata: { project: 'chinchilete', environment: 'dev', owner: 'platform' },
context: {
project: { env: 'dev' },
lambda: { functions: [] },
chisel: { numberOfShards: 3 },
},
writeSide: { codePath: 'dist/write' },
readSide: {
store: 'RDS',
codePath: 'dist/read',
clusterArn: process.env.DATA_CLUSTER_ARN!,
secretArn: process.env.DATA_SECRET_ARN!,
database: 'chinchilete',
schema: 'restaurant',
},
})
// On the RDS path stacks.readSide.persistence is undefined (no projection table).Peer Dependencies
{
"@othree.io/auditor": "^5.0.0",
"@othree.io/awsome": "^5.0.0",
"@othree.io/cdk": "^6.0.0",
"@othree.io/chisel": "^7.0.0",
"@othree.io/chisel-aws": "^7.0.0",
"@othree.io/excuses": "^2.0.0",
"@othree.io/journal": "^3.0.0",
"@othree.io/stethoscope": "^5.0.0",
"aws-cdk-lib": "^2.239.0",
"aws-lambda": "^1.0.7",
"aws-xray-sdk-core": "^3.12.0",
"uuid": "^13.0.0"
}Development
npm install
npm run build # Dual CJS/ESM output
npm test # Vitest with coverageLicense
ISC
