@auriclabs/events 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @auriclabs/events@0.1.0 build /home/runner/work/packages/packages/packages/events
2
+ > @auriclabs/events@0.2.0 build /home/runner/work/packages/packages/packages/events
3
3
  > tsdown src/index.ts --format cjs,esm --dts --no-hash
4
4
 
5
5
  [tsdown] Node.js v20.20.1 is deprecated. Support will be removed in the next minor release. Please upgrade to Node.js 22.18.0 or later.
@@ -13,27 +13,27 @@
13
13
  See more at https://tsdown.dev/options/dependencies#deps-onlybundle
14
14
  Detected dependencies in bundle:
15
15
  - @types/aws-lambda
16
- ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
17
- See more at https://tsdown.dev/options/dependencies#deps-onlybundle
18
- Detected dependencies in bundle:
19
- - @types/aws-lambda
20
16
  ℹ [ESM] dist/index.mjs 10.31 kB │ gzip: 3.19 kB
21
- ℹ [ESM] dist/index.mjs.map 23.73 kB │ gzip: 6.75 kB
17
+ ℹ [ESM] dist/index.mjs.map 23.71 kB │ gzip: 6.74 kB
22
18
  ℹ [ESM] dist/index.d.mts.map  7.93 kB │ gzip: 2.65 kB
23
19
  ℹ [ESM] dist/index.d.mts 15.74 kB │ gzip: 4.28 kB
24
- ℹ [ESM] 4 files, total: 57.71 kB
20
+ ℹ [ESM] 4 files, total: 57.69 kB
25
21
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
26
- - tsdown:external (41%)
27
- - rolldown-plugin-dts:generate (39%)
22
+ - rolldown-plugin-dts:generate (67%)
23
+ - rolldown-plugin-dts:resolver (24%)
28
24
  See https://rolldown.rs/options/checks#plugintimings for more details.
29
25
 
30
- ✔ Build complete in 5273ms
26
+ ✔ Build complete in 4816ms
27
+ ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
28
+ See more at https://tsdown.dev/options/dependencies#deps-onlybundle
29
+ Detected dependencies in bundle:
30
+ - @types/aws-lambda
31
31
  ℹ [CJS] dist/index.d.cts.map  7.93 kB │ gzip: 2.65 kB
32
32
  ℹ [CJS] dist/index.d.cts 15.74 kB │ gzip: 4.28 kB
33
33
  ℹ [CJS] 2 files, total: 23.67 kB
34
34
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
35
35
  - rolldown-plugin-dts:resolver (57%)
36
- - rolldown-plugin-dts:generate (25%)
36
+ - rolldown-plugin-dts:generate (28%)
37
37
  See https://rolldown.rs/options/checks#plugintimings for more details.
38
38
 
39
- ✔ Build complete in 5275ms
39
+ ✔ Build complete in 4824ms
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @auriclabs/events
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eeb513b: Add @auriclabs/events package with event dispatching, event listeners, stream handler,
8
+ and event service for DynamoDB-backed event sourcing.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [eeb513b]
13
+ - @auriclabs/api-core@0.1.1
14
+ - @auriclabs/logger@0.1.1
15
+ - @auriclabs/pagination@1.0.1
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/event-service.ts","../src/init.ts","../src/context.ts","../src/dispatch-event.ts","../src/dispatch-events.ts","../src/create-dispatch.ts","../src/create-event-listener.ts","../src/stream-handler.ts"],"sourcesContent":["import { normalizePaginationResponse, PaginationResponse } from '@auriclabs/pagination';\nimport { DynamoDBClient, ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n TransactWriteCommand,\n GetCommand,\n QueryCommand,\n} from '@aws-sdk/lib-dynamodb';\n\nimport type {\n EventRecord,\n AggregateHead,\n AggregatePK,\n EventId,\n AggregateId,\n AggregateType,\n EventSK,\n Source,\n} from './types';\n\nconst ddb = DynamoDBDocumentClient.from(new DynamoDBClient(), {\n marshallOptions: {\n removeUndefinedValues: true,\n },\n});\n\nconst pad = (n: number, w = 9): EventId => String(n).padStart(w, '0') as EventId;\nconst pkFor = (aggregateType: string, aggregateId: string): AggregatePK =>\n `AGG#${aggregateType}#${aggregateId}`;\n\nexport interface AppendArgs<P = unknown> {\n aggregateType: string;\n aggregateId: string;\n source: string;\n /** Version you observed before appending (0 for brand new) */\n expectedVersion: number;\n /** Required for idempotent retries (e.g., the command id) */\n idempotencyKey: string;\n\n // Event properties (flattened)\n eventId: string; // ULID/UUID – must be stable across retries\n eventType: string;\n occurredAt?: string; // default: now ISO\n payload?: Readonly<P>;\n schemaVersion?: number; // optional but recommended\n\n // Optional metadata\n correlationId?: string;\n causationId?: string;\n actorId?: string;\n}\n\nexport interface AppendEventResult {\n pk: string;\n sk: string;\n version: number;\n}\n\nexport interface EventService {\n appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult>;\n getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined>;\n getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined>;\n listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>>;\n}\n\nexport function createEventService(tableName: string): EventService {\n const TABLE = tableName;\n\n return {\n async appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult> {\n const {\n aggregateType,\n aggregateId,\n expectedVersion,\n idempotencyKey,\n eventId,\n eventType,\n occurredAt,\n source,\n payload,\n schemaVersion,\n correlationId,\n causationId,\n actorId,\n } = args;\n\n const pk = pkFor(aggregateType, aggregateId);\n const nextVersion = expectedVersion + 1;\n const sk = `EVT#${pad(nextVersion)}` as EventSK;\n const nowIso = new Date().toISOString();\n const eventOccurredAt = occurredAt ?? nowIso;\n\n try {\n await ddb.send(\n new TransactWriteCommand({\n TransactItems: [\n {\n Update: {\n TableName: TABLE,\n Key: { pk, sk: 'HEAD' },\n UpdateExpression:\n 'SET currentVersion = :next, lastEventId = :eid, lastIdemKey = :idem, updatedAt = :now, aggregateId = if_not_exists(aggregateId, :aid), aggregateType = if_not_exists(aggregateType, :atype)',\n ConditionExpression:\n '(attribute_not_exists(currentVersion) AND :expected = :zero) ' +\n 'OR currentVersion = :expected ' +\n 'OR lastIdemKey = :idem',\n ExpressionAttributeValues: {\n ':zero': 0,\n ':expected': expectedVersion,\n ':next': nextVersion,\n ':eid': eventId,\n ':idem': idempotencyKey,\n ':now': nowIso,\n ':aid': aggregateId,\n ':atype': aggregateType,\n },\n },\n },\n {\n Put: {\n TableName: TABLE,\n Item: {\n pk,\n sk,\n itemType: 'event',\n source: source as Source,\n aggregateId: aggregateId as AggregateId,\n aggregateType: aggregateType as AggregateType,\n version: nextVersion,\n\n eventId: eventId as EventId,\n eventType: eventType,\n schemaVersion: schemaVersion ?? 1,\n occurredAt: eventOccurredAt,\n\n correlationId,\n causationId,\n actorId,\n\n payload: payload as Readonly<unknown>,\n } satisfies EventRecord,\n ConditionExpression: 'attribute_not_exists(pk) OR eventId = :eid',\n ExpressionAttributeValues: { ':eid': eventId },\n },\n },\n ],\n }),\n );\n } catch (err) {\n if (err instanceof ConditionalCheckFailedException) {\n throw new Error(\n `OCC failed for aggregate ${aggregateType}/${aggregateId}: expectedVersion=${expectedVersion}`,\n );\n }\n throw err;\n }\n\n return { pk, sk, version: nextVersion };\n },\n\n async getHead(\n aggregateType: string,\n aggregateId: string,\n ): Promise<AggregateHead | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk: 'HEAD' } }));\n return res.Item as AggregateHead | undefined;\n },\n\n async getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const sk = `EVT#${pad(version)}`;\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk } }));\n return res.Item as EventRecord | undefined;\n },\n\n async listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>> {\n const pk = pkFor(params.aggregateType, params.aggregateId);\n const fromSk =\n params.fromVersionExclusive != null\n ? `EVT#${pad(params.fromVersionExclusive + 1)}`\n : 'EVT#000000000';\n const toSk =\n params.toVersionInclusive != null\n ? `EVT#${pad(params.toVersionInclusive)}`\n : 'EVT#999999999';\n\n const res = await ddb.send(\n new QueryCommand({\n TableName: TABLE,\n KeyConditionExpression: 'pk = :pk AND sk BETWEEN :from AND :to',\n ExpressionAttributeValues: {\n ':pk': pk,\n ':from': fromSk,\n ':to': toSk,\n },\n ScanIndexForward: true,\n Limit: params.limit,\n }),\n );\n\n return normalizePaginationResponse({\n data: (res.Items ?? []) as EventRecord[],\n cursor: res.LastEvaluatedKey && (res.LastEvaluatedKey as { pk: string; sk: string }).sk,\n });\n },\n };\n}\n","import { createEventService, EventService } from './event-service';\n\nlet _eventService: EventService | undefined;\n\nexport function initEvents(config: { tableName: string }): void {\n _eventService = createEventService(config.tableName);\n}\n\nexport function getEventService(): EventService {\n if (!_eventService) {\n throw new Error('Call initEvents() before using events');\n }\n return _eventService;\n}\n","import { AppendArgs } from './event-service';\n\nexport type EventContext = Partial<AppendArgs>;\n\nlet context: EventContext = {};\n\nexport const setEventContext = (newContext: EventContext) => {\n context = { ...newContext };\n};\n\nexport const getEventContext = () => context;\n\nexport const resetEventContext = () => {\n context = {};\n};\n\nexport const appendEventContext = (event: EventContext) => {\n context = { ...context, ...event };\n};\n","import { retry } from '@auriclabs/api-core';\nimport { logger } from '@auriclabs/logger';\nimport { ulid } from 'ulid';\n\nimport { AppendArgs, AppendEventResult } from './event-service';\nimport { getEventService } from './init';\nimport { getEventContext } from './context';\n\nexport type DispatchEventArgs = Omit<\n AppendArgs,\n 'eventId' | 'expectedVersion' | 'schemaVersion' | 'occurredAt' | 'idempotencyKey'\n> &\n Partial<Pick<AppendArgs, 'idempotencyKey' | 'eventId'>>;\n\nexport const dispatchEvent = async (event: DispatchEventArgs): Promise<AppendEventResult> => {\n const eventService = getEventService();\n const eventId = event.eventId ?? `evt-${ulid()}`;\n const occurredAt = new Date().toISOString();\n const idempotencyKey = event.idempotencyKey ?? eventId;\n\n return retry(async () => {\n const head = await eventService.getHead(event.aggregateType, event.aggregateId);\n logger.debug({ event }, 'Dispatching event');\n return eventService.appendEvent({\n ...getEventContext(),\n ...event,\n eventId,\n expectedVersion: head?.currentVersion ?? 0,\n schemaVersion: 1,\n occurredAt,\n idempotencyKey,\n });\n });\n};\n","import { dispatchEvent, DispatchEventArgs } from './dispatch-event';\n\nexport interface DispatchEventsArgs {\n inOrder?: boolean;\n}\n\nexport const dispatchEvents = async (\n events: DispatchEventArgs[],\n { inOrder = false }: DispatchEventsArgs = {},\n) => {\n if (inOrder) {\n for (const event of events) {\n await dispatchEvent(event);\n }\n } else {\n await Promise.all(events.map((event) => dispatchEvent(event)));\n }\n};\n","import { ulid } from 'ulid';\n\nimport { EventContext, getEventContext } from './context';\nimport { dispatchEvent, DispatchEventArgs } from './dispatch-event';\nimport { AppendEventResult } from './event-service';\n\nexport type MakePartial<T, O> = Omit<T, keyof O> & Partial<O>;\n\nexport type DispatchRecord<\n Options extends Partial<DispatchEventArgs> = Partial<DispatchEventArgs>,\n> = Record<\n string,\n (\n ...args: any[]\n ) =>\n | ValueOrFactoryRecord<MakePartial<DispatchEventArgs, Options>>\n | DispatchEventArgsFactory<Options>\n>;\n\nexport type ValueOrFactory<T> = T | ((context: EventContext) => T);\nexport type ValueOrFactoryRecord<T> = {\n [K in keyof T]: ValueOrFactory<T[K]>;\n};\n\nexport type DispatchEventArgsFactory<Options extends Partial<DispatchEventArgs>> = (\n context: EventContext,\n) => MakePartial<DispatchEventArgs, Options>;\n\nexport type DispatchRecordResponse<\n R extends DispatchRecord<O>,\n O extends Partial<DispatchEventArgs>,\n> = {\n [K in keyof R]: (...args: Parameters<R[K]>) => Promise<AppendEventResult>;\n};\n\nexport function createDispatch<DR extends DispatchRecord<O>, O extends Partial<DispatchEventArgs>>(\n record: DR,\n optionsOrFactory?: ValueOrFactoryRecord<O> | ((context: EventContext) => O),\n): DispatchRecordResponse<DR, O> {\n return Object.fromEntries(\n Object.entries(record).map(([key, value]) => [\n key,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (...args: any[]) => {\n const eventId = `evt-${ulid()}`;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n const result = value(...args);\n const context: EventContext = { eventId, ...getEventContext() };\n const executeValueFn = (value: any) =>\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n typeof value === 'function' ? value(context) : value;\n const parseResponse = (result: any) =>\n Object.fromEntries(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n Object.entries(result).map(([key, value]) => [key, executeValueFn(value)]),\n );\n Object.assign(\n context,\n typeof result === 'function' ? result(context) : parseResponse(result),\n );\n Object.assign(\n context,\n typeof optionsOrFactory === 'function'\n ? optionsOrFactory(context)\n : parseResponse(optionsOrFactory),\n );\n return dispatchEvent(context as DispatchEventArgs);\n },\n ]),\n ) as DispatchRecordResponse<DR, O>;\n}\n","import { logger } from '@auriclabs/logger';\nimport { SQSBatchResponse, SQSEvent } from 'aws-lambda';\n\nimport { setEventContext } from './context';\nimport { EventHandlers, EventRecord } from './types';\n\nexport interface CreateEventListenerOptions {\n debug?: boolean;\n}\n\nexport const createEventListener =\n (eventHandlers: EventHandlers, { debug = false }: CreateEventListenerOptions = {}) =>\n async (sqsEvent: SQSEvent) => {\n const response: SQSBatchResponse = {\n batchItemFailures: [],\n };\n let hasFailed = false;\n for (const record of sqsEvent.Records) {\n // skip the job if it has failed\n if (hasFailed) {\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n continue;\n }\n\n let event: EventRecord | undefined;\n try {\n event = JSON.parse(record.body) as EventRecord;\n if (debug) {\n logger.debug({ event }, 'Processing event');\n }\n let handler = eventHandlers[event.eventType];\n while (typeof handler === 'string') {\n handler = eventHandlers[handler];\n }\n if (typeof handler === 'function') {\n setEventContext({\n causationId: event.eventId,\n correlationId: event.correlationId,\n actorId: event.actorId,\n });\n await handler(event);\n }\n } catch (error) {\n hasFailed = true;\n logger.error({ error, event, body: record.body }, 'Error processing event');\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n }\n }\n return response;\n };\n","import { logger } from '@auriclabs/logger';\nimport { AttributeValue } from '@aws-sdk/client-dynamodb';\nimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';\nimport { SendMessageBatchCommand, SQSClient } from '@aws-sdk/client-sqs';\nimport { unmarshall } from '@aws-sdk/util-dynamodb';\nimport { DynamoDBStreamEvent } from 'aws-lambda';\nimport { kebabCase } from 'lodash-es';\n\nimport { AggregateHead, EventRecord } from './types';\n\nconst BATCH_SIZE = 10;\n\nexport interface CreateStreamHandlerConfig {\n busName: string;\n queueUrls: string[];\n}\n\n/**\n * Creates a Lambda handler for DynamoDB stream events.\n * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.\n */\nexport function createStreamHandler(config: CreateStreamHandlerConfig) {\n const sqsClient = new SQSClient();\n const eventBridge = new EventBridgeClient({});\n\n function chunkArray<T>(array: T[], chunkSize: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n }\n\n async function sendToQueuesBatch(eventRecords: EventRecord[]) {\n await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));\n }\n\n async function sendToQueueBatch(eventRecords: EventRecord[], queue: string) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord, index) => ({\n Id: `${eventRecord.eventId}-${index}`,\n MessageBody: JSON.stringify(eventRecord),\n MessageGroupId: eventRecord.aggregateId,\n MessageDeduplicationId: eventRecord.eventId,\n }));\n\n await sqsClient.send(\n new SendMessageBatchCommand({\n QueueUrl: queue,\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch, queue }, 'Error sending batch to queue');\n throw error;\n }\n }\n }\n\n async function sendToBusBatch(eventRecords: EventRecord[]) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const source = eventRecord.source ?? kebabCase(eventRecord.aggregateType.split('.')[0]);\n return {\n Source: source,\n DetailType: eventRecord.eventType,\n Detail: JSON.stringify(eventRecord),\n EventBusName: config.busName,\n };\n });\n\n await eventBridge.send(\n new PutEventsCommand({\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch }, 'Error sending batch to bus');\n throw error;\n }\n }\n }\n\n return async (event: DynamoDBStreamEvent): Promise<void> => {\n const eventRecords = event.Records.filter((record) => record.eventName === 'INSERT')\n .map((record) => {\n try {\n const data = record.dynamodb?.NewImage;\n return unmarshall(data as Record<string, AttributeValue>) as EventRecord | AggregateHead;\n } catch (error) {\n logger.error({ error, record }, 'Error unmarshalling event record');\n return undefined;\n }\n })\n .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');\n\n if (eventRecords.length > 0) {\n await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/event-service.ts","../src/init.ts","../src/context.ts","../src/dispatch-event.ts","../src/dispatch-events.ts","../src/create-dispatch.ts","../src/create-event-listener.ts","../src/stream-handler.ts"],"sourcesContent":["import { normalizePaginationResponse, PaginationResponse } from '@auriclabs/pagination';\nimport { DynamoDBClient, ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n TransactWriteCommand,\n GetCommand,\n QueryCommand,\n} from '@aws-sdk/lib-dynamodb';\n\nimport type {\n EventRecord,\n AggregateHead,\n AggregatePK,\n EventId,\n AggregateId,\n AggregateType,\n EventSK,\n Source,\n} from './types';\n\nconst ddb = DynamoDBDocumentClient.from(new DynamoDBClient(), {\n marshallOptions: {\n removeUndefinedValues: true,\n },\n});\n\nconst pad = (n: number, w = 9): EventId => String(n).padStart(w, '0') as EventId;\nconst pkFor = (aggregateType: string, aggregateId: string): AggregatePK =>\n `AGG#${aggregateType}#${aggregateId}`;\n\nexport interface AppendArgs<P = unknown> {\n aggregateType: string;\n aggregateId: string;\n source: string;\n /** Version you observed before appending (0 for brand new) */\n expectedVersion: number;\n /** Required for idempotent retries (e.g., the command id) */\n idempotencyKey: string;\n\n // Event properties (flattened)\n eventId: string; // ULID/UUID – must be stable across retries\n eventType: string;\n occurredAt?: string; // default: now ISO\n payload?: Readonly<P>;\n schemaVersion?: number; // optional but recommended\n\n // Optional metadata\n correlationId?: string;\n causationId?: string;\n actorId?: string;\n}\n\nexport interface AppendEventResult {\n pk: string;\n sk: string;\n version: number;\n}\n\nexport interface EventService {\n appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult>;\n getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined>;\n getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined>;\n listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>>;\n}\n\nexport function createEventService(tableName: string): EventService {\n const TABLE = tableName;\n\n return {\n async appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult> {\n const {\n aggregateType,\n aggregateId,\n expectedVersion,\n idempotencyKey,\n eventId,\n eventType,\n occurredAt,\n source,\n payload,\n schemaVersion,\n correlationId,\n causationId,\n actorId,\n } = args;\n\n const pk = pkFor(aggregateType, aggregateId);\n const nextVersion = expectedVersion + 1;\n const sk = `EVT#${pad(nextVersion)}` as EventSK;\n const nowIso = new Date().toISOString();\n const eventOccurredAt = occurredAt ?? nowIso;\n\n try {\n await ddb.send(\n new TransactWriteCommand({\n TransactItems: [\n {\n Update: {\n TableName: TABLE,\n Key: { pk, sk: 'HEAD' },\n UpdateExpression:\n 'SET currentVersion = :next, lastEventId = :eid, lastIdemKey = :idem, updatedAt = :now, aggregateId = if_not_exists(aggregateId, :aid), aggregateType = if_not_exists(aggregateType, :atype)',\n ConditionExpression:\n '(attribute_not_exists(currentVersion) AND :expected = :zero) ' +\n 'OR currentVersion = :expected ' +\n 'OR lastIdemKey = :idem',\n ExpressionAttributeValues: {\n ':zero': 0,\n ':expected': expectedVersion,\n ':next': nextVersion,\n ':eid': eventId,\n ':idem': idempotencyKey,\n ':now': nowIso,\n ':aid': aggregateId,\n ':atype': aggregateType,\n },\n },\n },\n {\n Put: {\n TableName: TABLE,\n Item: {\n pk,\n sk,\n itemType: 'event',\n source: source as Source,\n aggregateId: aggregateId as AggregateId,\n aggregateType: aggregateType as AggregateType,\n version: nextVersion,\n\n eventId: eventId as EventId,\n eventType: eventType,\n schemaVersion: schemaVersion ?? 1,\n occurredAt: eventOccurredAt,\n\n correlationId,\n causationId,\n actorId,\n\n payload: payload as Readonly<unknown>,\n } satisfies EventRecord,\n ConditionExpression: 'attribute_not_exists(pk) OR eventId = :eid',\n ExpressionAttributeValues: { ':eid': eventId },\n },\n },\n ],\n }),\n );\n } catch (err) {\n if (err instanceof ConditionalCheckFailedException) {\n throw new Error(\n `OCC failed for aggregate ${aggregateType}/${aggregateId}: expectedVersion=${expectedVersion}`,\n );\n }\n throw err;\n }\n\n return { pk, sk, version: nextVersion };\n },\n\n async getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk: 'HEAD' } }));\n return res.Item as AggregateHead | undefined;\n },\n\n async getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const sk = `EVT#${pad(version)}`;\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk } }));\n return res.Item as EventRecord | undefined;\n },\n\n async listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>> {\n const pk = pkFor(params.aggregateType, params.aggregateId);\n const fromSk =\n params.fromVersionExclusive != null\n ? `EVT#${pad(params.fromVersionExclusive + 1)}`\n : 'EVT#000000000';\n const toSk =\n params.toVersionInclusive != null\n ? `EVT#${pad(params.toVersionInclusive)}`\n : 'EVT#999999999';\n\n const res = await ddb.send(\n new QueryCommand({\n TableName: TABLE,\n KeyConditionExpression: 'pk = :pk AND sk BETWEEN :from AND :to',\n ExpressionAttributeValues: {\n ':pk': pk,\n ':from': fromSk,\n ':to': toSk,\n },\n ScanIndexForward: true,\n Limit: params.limit,\n }),\n );\n\n return normalizePaginationResponse({\n data: (res.Items ?? []) as EventRecord[],\n cursor: res.LastEvaluatedKey && (res.LastEvaluatedKey as { pk: string; sk: string }).sk,\n });\n },\n };\n}\n","import { createEventService, EventService } from './event-service';\n\nlet _eventService: EventService | undefined;\n\nexport function initEvents(config: { tableName: string }): void {\n _eventService = createEventService(config.tableName);\n}\n\nexport function getEventService(): EventService {\n if (!_eventService) {\n throw new Error('Call initEvents() before using events');\n }\n return _eventService;\n}\n","import { AppendArgs } from './event-service';\n\nexport type EventContext = Partial<AppendArgs>;\n\nlet context: EventContext = {};\n\nexport const setEventContext = (newContext: EventContext) => {\n context = { ...newContext };\n};\n\nexport const getEventContext = () => context;\n\nexport const resetEventContext = () => {\n context = {};\n};\n\nexport const appendEventContext = (event: EventContext) => {\n context = { ...context, ...event };\n};\n","import { retry } from '@auriclabs/api-core';\nimport { logger } from '@auriclabs/logger';\nimport { ulid } from 'ulid';\n\nimport { getEventContext } from './context';\nimport { AppendArgs, AppendEventResult } from './event-service';\nimport { getEventService } from './init';\n\nexport type DispatchEventArgs = Omit<\n AppendArgs,\n 'eventId' | 'expectedVersion' | 'schemaVersion' | 'occurredAt' | 'idempotencyKey'\n> &\n Partial<Pick<AppendArgs, 'idempotencyKey' | 'eventId'>>;\n\nexport const dispatchEvent = async (event: DispatchEventArgs): Promise<AppendEventResult> => {\n const eventService = getEventService();\n const eventId = event.eventId ?? `evt-${ulid()}`;\n const occurredAt = new Date().toISOString();\n const idempotencyKey = event.idempotencyKey ?? eventId;\n\n return retry(async () => {\n const head = await eventService.getHead(event.aggregateType, event.aggregateId);\n logger.debug({ event }, 'Dispatching event');\n return eventService.appendEvent({\n ...getEventContext(),\n ...event,\n eventId,\n expectedVersion: head?.currentVersion ?? 0,\n schemaVersion: 1,\n occurredAt,\n idempotencyKey,\n });\n });\n};\n","import { dispatchEvent, DispatchEventArgs } from './dispatch-event';\n\nexport interface DispatchEventsArgs {\n inOrder?: boolean;\n}\n\nexport const dispatchEvents = async (\n events: DispatchEventArgs[],\n { inOrder = false }: DispatchEventsArgs = {},\n) => {\n if (inOrder) {\n for (const event of events) {\n await dispatchEvent(event);\n }\n } else {\n await Promise.all(events.map((event) => dispatchEvent(event)));\n }\n};\n","import { ulid } from 'ulid';\n\nimport { EventContext, getEventContext } from './context';\nimport { dispatchEvent, DispatchEventArgs } from './dispatch-event';\nimport { AppendEventResult } from './event-service';\n\nexport type MakePartial<T, O> = Omit<T, keyof O> & Partial<O>;\n\nexport type DispatchRecord<\n Options extends Partial<DispatchEventArgs> = Partial<DispatchEventArgs>,\n> = Record<\n string,\n (\n ...args: any[]\n ) =>\n | ValueOrFactoryRecord<MakePartial<DispatchEventArgs, Options>>\n | DispatchEventArgsFactory<Options>\n>;\n\nexport type ValueOrFactory<T> = T | ((context: EventContext) => T);\nexport type ValueOrFactoryRecord<T> = {\n [K in keyof T]: ValueOrFactory<T[K]>;\n};\n\nexport type DispatchEventArgsFactory<Options extends Partial<DispatchEventArgs>> = (\n context: EventContext,\n) => MakePartial<DispatchEventArgs, Options>;\n\nexport type DispatchRecordResponse<\n R extends DispatchRecord<O>,\n O extends Partial<DispatchEventArgs>,\n> = {\n [K in keyof R]: (...args: Parameters<R[K]>) => Promise<AppendEventResult>;\n};\n\nexport function createDispatch<DR extends DispatchRecord<O>, O extends Partial<DispatchEventArgs>>(\n record: DR,\n optionsOrFactory?: ValueOrFactoryRecord<O> | ((context: EventContext) => O),\n): DispatchRecordResponse<DR, O> {\n return Object.fromEntries(\n Object.entries(record).map(([key, value]) => [\n key,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (...args: any[]) => {\n const eventId = `evt-${ulid()}`;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n const result = value(...args);\n const context: EventContext = { eventId, ...getEventContext() };\n const executeValueFn = (value: any) =>\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n typeof value === 'function' ? value(context) : value;\n const parseResponse = (result: any) =>\n Object.fromEntries(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n Object.entries(result).map(([key, value]) => [key, executeValueFn(value)]),\n );\n Object.assign(\n context,\n typeof result === 'function' ? result(context) : parseResponse(result),\n );\n Object.assign(\n context,\n typeof optionsOrFactory === 'function'\n ? optionsOrFactory(context)\n : parseResponse(optionsOrFactory),\n );\n return dispatchEvent(context as DispatchEventArgs);\n },\n ]),\n ) as DispatchRecordResponse<DR, O>;\n}\n","import { logger } from '@auriclabs/logger';\nimport { SQSBatchResponse, SQSEvent } from 'aws-lambda';\n\nimport { setEventContext } from './context';\nimport { EventHandlers, EventRecord } from './types';\n\nexport interface CreateEventListenerOptions {\n debug?: boolean;\n}\n\nexport const createEventListener =\n (eventHandlers: EventHandlers, { debug = false }: CreateEventListenerOptions = {}) =>\n async (sqsEvent: SQSEvent) => {\n const response: SQSBatchResponse = {\n batchItemFailures: [],\n };\n let hasFailed = false;\n for (const record of sqsEvent.Records) {\n // skip the job if it has failed\n if (hasFailed) {\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n continue;\n }\n\n let event: EventRecord | undefined;\n try {\n event = JSON.parse(record.body) as EventRecord;\n if (debug) {\n logger.debug({ event }, 'Processing event');\n }\n let handler = eventHandlers[event.eventType];\n while (typeof handler === 'string') {\n handler = eventHandlers[handler];\n }\n if (typeof handler === 'function') {\n setEventContext({\n causationId: event.eventId,\n correlationId: event.correlationId,\n actorId: event.actorId,\n });\n await handler(event);\n }\n } catch (error) {\n hasFailed = true;\n logger.error({ error, event, body: record.body }, 'Error processing event');\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n }\n }\n return response;\n };\n","import { logger } from '@auriclabs/logger';\nimport { AttributeValue } from '@aws-sdk/client-dynamodb';\nimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';\nimport { SendMessageBatchCommand, SQSClient } from '@aws-sdk/client-sqs';\nimport { unmarshall } from '@aws-sdk/util-dynamodb';\nimport { DynamoDBStreamEvent } from 'aws-lambda';\nimport { kebabCase } from 'lodash-es';\n\nimport { AggregateHead, EventRecord } from './types';\n\nconst BATCH_SIZE = 10;\n\nexport interface CreateStreamHandlerConfig {\n busName: string;\n queueUrls: string[];\n}\n\n/**\n * Creates a Lambda handler for DynamoDB stream events.\n * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.\n */\nexport function createStreamHandler(config: CreateStreamHandlerConfig) {\n const sqsClient = new SQSClient();\n const eventBridge = new EventBridgeClient({});\n\n function chunkArray<T>(array: T[], chunkSize: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n }\n\n async function sendToQueuesBatch(eventRecords: EventRecord[]) {\n await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));\n }\n\n async function sendToQueueBatch(eventRecords: EventRecord[], queue: string) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord, index) => ({\n Id: `${eventRecord.eventId}-${index}`,\n MessageBody: JSON.stringify(eventRecord),\n MessageGroupId: eventRecord.aggregateId,\n MessageDeduplicationId: eventRecord.eventId,\n }));\n\n await sqsClient.send(\n new SendMessageBatchCommand({\n QueueUrl: queue,\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch, queue }, 'Error sending batch to queue');\n throw error;\n }\n }\n }\n\n async function sendToBusBatch(eventRecords: EventRecord[]) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const source = eventRecord.source ?? kebabCase(eventRecord.aggregateType.split('.')[0]);\n return {\n Source: source,\n DetailType: eventRecord.eventType,\n Detail: JSON.stringify(eventRecord),\n EventBusName: config.busName,\n };\n });\n\n await eventBridge.send(\n new PutEventsCommand({\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch }, 'Error sending batch to bus');\n throw error;\n }\n }\n }\n\n return async (event: DynamoDBStreamEvent): Promise<void> => {\n const eventRecords = event.Records.filter((record) => record.eventName === 'INSERT')\n .map((record) => {\n try {\n const data = record.dynamodb?.NewImage;\n return unmarshall(data as Record<string, AttributeValue>) as EventRecord | AggregateHead;\n } catch (error) {\n logger.error({ error, record }, 'Error unmarshalling event record');\n return undefined;\n }\n })\n .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');\n\n if (eventRecords.length > 0) {\n await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);\n }\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import nodeTsConfig from '@auriclabs/eslint-config/node-ts';
2
+
3
+ export default nodeTsConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auriclabs/events",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Event sourcing runtime utilities for DynamoDB-backed event stores",
5
5
  "prettier": "@auriclabs/prettier-config",
6
6
  "main": "dist/index.cjs",
@@ -24,14 +24,14 @@
24
24
  "devDependencies": {
25
25
  "@types/aws-lambda": "^8.10.148",
26
26
  "@types/lodash-es": "4.17.12",
27
- "@auriclabs/api-core": "0.1.0",
28
- "@auriclabs/logger": "0.1.0",
29
- "@auriclabs/pagination": "1.0.0"
27
+ "@auriclabs/api-core": "0.1.1",
28
+ "@auriclabs/logger": "0.1.1",
29
+ "@auriclabs/pagination": "1.0.1"
30
30
  },
31
31
  "peerDependencies": {
32
- "@auriclabs/api-core": "^0.1.0",
33
- "@auriclabs/logger": "^0.1.0",
34
- "@auriclabs/pagination": "^0.1.0",
32
+ "@auriclabs/api-core": "^0.1.1",
33
+ "@auriclabs/logger": "^0.1.1",
34
+ "@auriclabs/pagination": "^1.0.1",
35
35
  "@aws-sdk/client-dynamodb": "^3.0.0",
36
36
  "@aws-sdk/lib-dynamodb": "^3.0.0",
37
37
  "@aws-sdk/client-eventbridge": "^3.0.0",
@@ -10,8 +10,10 @@ vi.mock('./context', () => ({
10
10
  setEventContext: mockSetEventContext,
11
11
  }));
12
12
 
13
- import { createEventListener } from './create-event-listener';
14
13
  import { logger } from '@auriclabs/logger';
14
+
15
+ import { createEventListener } from './create-event-listener';
16
+
15
17
  import type { SQSEvent } from 'aws-lambda';
16
18
 
17
19
  const makeRecord = (body: object, messageId = 'msg-1') => ({
@@ -27,9 +27,10 @@ vi.mock('ulid', () => ({
27
27
  ulid: mockUlid,
28
28
  }));
29
29
 
30
- import { dispatchEvent } from './dispatch-event';
31
30
  import { retry } from '@auriclabs/api-core';
32
31
 
32
+ import { dispatchEvent } from './dispatch-event';
33
+
33
34
  describe('dispatchEvent', () => {
34
35
  beforeEach(() => {
35
36
  vi.clearAllMocks();
@@ -2,9 +2,9 @@ import { retry } from '@auriclabs/api-core';
2
2
  import { logger } from '@auriclabs/logger';
3
3
  import { ulid } from 'ulid';
4
4
 
5
+ import { getEventContext } from './context';
5
6
  import { AppendArgs, AppendEventResult } from './event-service';
6
7
  import { getEventService } from './init';
7
- import { getEventContext } from './context';
8
8
 
9
9
  export type DispatchEventArgs = Omit<
10
10
  AppendArgs,
@@ -61,9 +61,7 @@ describe('dispatchEvents', () => {
61
61
  });
62
62
 
63
63
  it('defaults inOrder to false when options not provided', async () => {
64
- const events = [
65
- { aggregateType: 'order', aggregateId: '1', source: 'test', eventType: 'A' },
66
- ];
64
+ const events = [{ aggregateType: 'order', aggregateId: '1', source: 'test', eventType: 'A' }];
67
65
 
68
66
  await dispatchEvents(events);
69
67
 
@@ -23,9 +23,10 @@ vi.mock('@auriclabs/pagination', () => ({
23
23
  normalizePaginationResponse: vi.fn((input: unknown) => input),
24
24
  }));
25
25
 
26
- import { createEventService } from './event-service';
27
26
  import { TransactWriteCommand, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
28
27
 
28
+ import { createEventService } from './event-service';
29
+
29
30
  describe('event-service', () => {
30
31
  const TABLE_NAME = 'test-events';
31
32
 
@@ -168,10 +168,7 @@ export function createEventService(tableName: string): EventService {
168
168
  return { pk, sk, version: nextVersion };
169
169
  },
170
170
 
171
- async getHead(
172
- aggregateType: string,
173
- aggregateId: string,
174
- ): Promise<AggregateHead | undefined> {
171
+ async getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined> {
175
172
  const pk = pkFor(aggregateType, aggregateId);
176
173
  const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk: 'HEAD' } }));
177
174
  return res.Item as AggregateHead | undefined;
@@ -26,10 +26,12 @@ vi.mock('lodash-es', () => ({
26
26
  kebabCase: vi.fn((s: string) => s.toLowerCase().replace(/[.\s]+/g, '-')),
27
27
  }));
28
28
 
29
- import { createStreamHandler } from './stream-handler';
30
- import { SendMessageBatchCommand } from '@aws-sdk/client-sqs';
31
- import { PutEventsCommand } from '@aws-sdk/client-eventbridge';
32
29
  import { logger } from '@auriclabs/logger';
30
+ import { PutEventsCommand } from '@aws-sdk/client-eventbridge';
31
+ import { SendMessageBatchCommand } from '@aws-sdk/client-sqs';
32
+
33
+ import { createStreamHandler } from './stream-handler';
34
+
33
35
  import type { DynamoDBStreamEvent } from 'aws-lambda';
34
36
 
35
37
  const makeEventRecord = (overrides = {}) => ({
@@ -48,10 +50,7 @@ const makeEventRecord = (overrides = {}) => ({
48
50
  ...overrides,
49
51
  });
50
52
 
51
- const makeStreamRecord = (
52
- eventName: string,
53
- newImage: object | undefined = {},
54
- ) => ({
53
+ const makeStreamRecord = (eventName: string, newImage: object | undefined = {}) => ({
55
54
  eventID: '1',
56
55
  eventVersion: '1.1',
57
56
  dynamodb: {
@@ -104,9 +103,7 @@ describe('stream-handler', () => {
104
103
  const headRecord = { pk: 'AGG#order#o-1', sk: 'HEAD', itemType: 'head' };
105
104
  const eventRecord = makeEventRecord();
106
105
 
107
- mockUnmarshall
108
- .mockReturnValueOnce(headRecord)
109
- .mockReturnValueOnce(eventRecord);
106
+ mockUnmarshall.mockReturnValueOnce(headRecord).mockReturnValueOnce(eventRecord);
110
107
 
111
108
  const handler = createStreamHandler(config);
112
109
  const event: DynamoDBStreamEvent = {
@@ -122,7 +119,7 @@ describe('stream-handler', () => {
122
119
  expect(SendMessageBatchCommand).toHaveBeenCalledTimes(1);
123
120
  const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
124
121
  expect(sqsInput.Entries).toHaveLength(1);
125
- expect(JSON.parse(sqsInput.Entries![0].MessageBody!)).toEqual(eventRecord);
122
+ expect(JSON.parse(sqsInput.Entries![0].MessageBody)).toEqual(eventRecord);
126
123
  });
127
124
 
128
125
  it('sends to all configured queues', async () => {
@@ -168,7 +165,7 @@ describe('stream-handler', () => {
168
165
  expect(ebInput.Entries![0].EventBusName).toBe('test-bus');
169
166
  expect(ebInput.Entries![0].DetailType).toBe('CreditAdded');
170
167
  expect(ebInput.Entries![0].Source).toBe('billing');
171
- expect(JSON.parse(ebInput.Entries![0].Detail!)).toEqual(eventRecord);
168
+ expect(JSON.parse(ebInput.Entries![0].Detail)).toEqual(eventRecord);
172
169
  });
173
170
 
174
171
  it('uses kebabCase of aggregateType as source fallback when source is undefined', async () => {
@@ -230,9 +227,7 @@ describe('stream-handler', () => {
230
227
 
231
228
  const handler = createStreamHandler(config);
232
229
  const event: DynamoDBStreamEvent = {
233
- Records: records.map((_, i) =>
234
- makeStreamRecord('INSERT', { idx: { N: String(i) } }),
235
- ) as any,
230
+ Records: records.map((_, i) => makeStreamRecord('INSERT', { idx: { N: String(i) } })) as any,
236
231
  };
237
232
 
238
233
  await handler(event);
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src", "vitest.config.ts"],
7
+ "exclude": ["dist", "node_modules"]
8
+ }