Database Events
Stratal automatically emits events for every database operation. The EventEmitterPlugin is auto-registered by the DatabaseModule, so no additional setup is required. These events integrate with the core event system and support the same pattern matching, priority, and blocking behavior.
Two families of events are emitted:
- Query events (
before.{Model}.{operation}/after.{Model}.{operation}) wrap the raw query and carry the operation arguments and result. - Entity mutation events (
entity.{Model}.{verb}) carry full entity snapshots taken before and after the mutation.
Query event pattern
Section titled “Query event pattern”Query events follow the pattern {phase}.{Model}.{operation}:
| Segment | Values | Example |
|---|---|---|
| Phase | before, after | before, after |
| Model | Any model name from your schema | User, Post |
| Operation | create, update, delete, findMany, count, etc. | create |
A full event name looks like after.User.create or before.Post.update. These events fire for read operations too, such as after.User.findMany.
Listening to database events
Section titled “Listening to database events”Use the @Listener() and @On() decorators from the core event system:
import { Listener, On } from 'stratal/events'import type { EventContext } from 'stratal/events'
@Listener()export class UserEventListener { @On('after.User.create') async onUserCreated(context: EventContext<'after.User.create'>) { const user = context.result // send welcome email, create audit log, etc. }
@On('before.User.delete') async onBeforeUserDelete(context: EventContext<'before.User.delete'>) { const data = context.data // archive user data before deletion }}Register the listener in a module’s providers array:
@Module({ providers: [UserEventListener],})export class UsersModule {}Wildcard patterns
Section titled “Wildcard patterns”Database events support the same hierarchical pattern matching as core events:
Model wildcard
Section titled “Model wildcard”Listen to all operations on a specific model:
@On('after.User')async onAnyUserChange(context: EventContext<'after.User'>) { // context.operation tells you which operation triggered the event console.log(`User ${context.operation} completed`)}Operation wildcard
Section titled “Operation wildcard”Listen to a specific operation across all models:
@On('after.create')async onAnyCreate(context: EventContext<'after.create'>) { // context.model tells you which model was created console.log(`${context.model} created`)}Phase wildcard
Section titled “Phase wildcard”Listen to all events in a phase:
@On('after')async onAnyAfterEvent(context: EventContext<'after'>) { console.log(`${context.model}.${context.operation} completed`)}Query event context
Section titled “Query event context”The query event context is a discriminated union based on the event pattern:
Exact events (after.User.create)
Section titled “Exact events (after.User.create)”interface ExactEventContext { data: UserCreateInput // before: mutable, after: readonly result: User // only present in the after phase}Model wildcard (after.User)
Section titled “Model wildcard (after.User)”interface ModelWildcardContext { operation: DatabaseOperation // 'create' | 'update' | 'delete' | ... data: unknown result: unknown}Operation wildcard (after.create)
Section titled “Operation wildcard (after.create)”interface OperationWildcardContext { model: ModelName // 'User' | 'Post' | ... data: unknown result: unknown}Phase wildcard (after)
Section titled “Phase wildcard (after)”interface PhaseWildcardContext { model: ModelName operation: DatabaseOperation data: unknown result: unknown}Entity mutation events
Section titled “Entity mutation events”Query events carry the raw operation arguments and result, which are not always the shape you want to react to. A delete carries the where filter, an update carries only the changed fields, and a createMany carries an array. When you want to work with complete entity rows, listen for entity mutation events instead.
Entity events follow the pattern entity.{Model}.{verb}:
| Segment | Values | Example |
|---|---|---|
| Model | Any model name from your schema | User, Post |
| Verb | created, updated, deleted | created |
A full event name looks like entity.User.created or entity.Post.updated. One event is emitted per affected row, so a bulk operation that touches many rows emits one entity event per row.
Entity event context
Section titled “Entity event context”Each entity event carries model, action, and a pair of full entity snapshots. Which snapshots are populated depends on the verb:
// entity.User.createdinterface EntityCreatedContext { model: 'User' action: 'created' before: undefined // nothing existed beforehand after: User // the created row}
// entity.User.updatedinterface EntityUpdatedContext { model: 'User' action: 'updated' before: User // the row as it was before the update after: User // the row after the update}
// entity.User.deletedinterface EntityDeletedContext { model: 'User' action: 'deleted' before: User // the row as it was before deletion after: undefined // nothing remains}@On('entity.User.created')async onUserCreated(context: EventContext<'entity.User.created'>) { const user = context.after // index the new user, send a welcome email, etc.}@On('entity.User.updated')async onUserUpdated(context: EventContext<'entity.User.updated'>) { const { before, after } = context if (before.email !== after.email) { // react to the email change with both snapshots in hand }}@On('entity.User.deleted')async onUserDeleted(context: EventContext<'entity.User.deleted'>) { const user = context.before // clean up related records, archive the removed row, etc.}Entity wildcards
Section titled “Entity wildcards”Entity events support the same hierarchical pattern matching as query events:
| Pattern | Matches |
|---|---|
entity.User.created | A specific model and verb |
entity.User | All mutations on the User model |
entity.updated | Every model that is updated |
entity | Every entity mutation across all models |
@On('entity.User')async onAnyUserMutation(context: EventContext<'entity.User'>) { // context.action is 'created', 'updated', or 'deleted' // before / after are populated according to the action}Blocking behavior
Section titled “Blocking behavior”Database events follow the same blocking rules as core events:
before.*events are always blocking. The database operation waits for all handlers to complete before proceeding.after.*events are non-blocking by default. Handlers run in the background viawaitUntil.entity.*events are blocking by default, the same as custom events.
This means before handlers can validate or modify data before it reaches the database, while after handlers run without delaying the response. As with core events, you can override any default with the blocking option on @On().
Type-safe augmentation
Section titled “Type-safe augmentation”The framework automatically augments the core CustomEventRegistry with both your query events and your entity mutation events, based on the StratalDatabase schema augmentation. Once you’ve set up database type augmentation, every database event name autocompletes and its context is fully typed. For entity events, before and after resolve to the full model type.