@nestjs-transactional/outbox 1.0.0-alpha.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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/dist/api/completed-event-publications.d.ts +31 -0
  4. package/dist/api/completed-event-publications.js +58 -0
  5. package/dist/api/completed-event-publications.js.map +1 -0
  6. package/dist/api/failed-event-publications.d.ts +36 -0
  7. package/dist/api/failed-event-publications.js +75 -0
  8. package/dist/api/failed-event-publications.js.map +1 -0
  9. package/dist/api/incomplete-event-publications.d.ts +28 -0
  10. package/dist/api/incomplete-event-publications.js +72 -0
  11. package/dist/api/incomplete-event-publications.js.map +1 -0
  12. package/dist/decorators/inject-decorators.d.ts +85 -0
  13. package/dist/decorators/inject-decorators.js +100 -0
  14. package/dist/decorators/inject-decorators.js.map +1 -0
  15. package/dist/decorators/outbox-events-handler.decorator.d.ts +97 -0
  16. package/dist/decorators/outbox-events-handler.decorator.js +58 -0
  17. package/dist/decorators/outbox-events-handler.decorator.js.map +1 -0
  18. package/dist/dispatcher/data-source-outbox-publisher.d.ts +69 -0
  19. package/dist/dispatcher/data-source-outbox-publisher.js +127 -0
  20. package/dist/dispatcher/data-source-outbox-publisher.js.map +1 -0
  21. package/dist/dispatcher/event-publication-processor.d.ts +69 -0
  22. package/dist/dispatcher/event-publication-processor.js +192 -0
  23. package/dist/dispatcher/event-publication-processor.js.map +1 -0
  24. package/dist/dispatcher/outbox-event-publisher.d.ts +129 -0
  25. package/dist/dispatcher/outbox-event-publisher.js +172 -0
  26. package/dist/dispatcher/outbox-event-publisher.js.map +1 -0
  27. package/dist/dispatcher/processor-options.d.ts +24 -0
  28. package/dist/dispatcher/processor-options.js +15 -0
  29. package/dist/dispatcher/processor-options.js.map +1 -0
  30. package/dist/externalization/errors.d.ts +21 -0
  31. package/dist/externalization/errors.js +30 -0
  32. package/dist/externalization/errors.js.map +1 -0
  33. package/dist/externalization/event-externalizer.d.ts +49 -0
  34. package/dist/externalization/event-externalizer.js +14 -0
  35. package/dist/externalization/event-externalizer.js.map +1 -0
  36. package/dist/externalization/externalization-registry.d.ts +56 -0
  37. package/dist/externalization/externalization-registry.js +103 -0
  38. package/dist/externalization/externalization-registry.js.map +1 -0
  39. package/dist/externalization/externalized.decorator.d.ts +121 -0
  40. package/dist/externalization/externalized.decorator.js +87 -0
  41. package/dist/externalization/externalized.decorator.js.map +1 -0
  42. package/dist/externalization/types.d.ts +39 -0
  43. package/dist/externalization/types.js +3 -0
  44. package/dist/externalization/types.js.map +1 -0
  45. package/dist/index.d.ts +38 -0
  46. package/dist/index.js +58 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/interfaces/outbox-event-handler.interface.d.ts +32 -0
  49. package/dist/interfaces/outbox-event-handler.interface.js +3 -0
  50. package/dist/interfaces/outbox-event-handler.interface.js.map +1 -0
  51. package/dist/module/outbox-processing.module.d.ts +29 -0
  52. package/dist/module/outbox-processing.module.js +66 -0
  53. package/dist/module/outbox-processing.module.js.map +1 -0
  54. package/dist/module/outbox.module.d.ts +196 -0
  55. package/dist/module/outbox.module.js +604 -0
  56. package/dist/module/outbox.module.js.map +1 -0
  57. package/dist/recovery/staleness-monitor.d.ts +39 -0
  58. package/dist/recovery/staleness-monitor.js +118 -0
  59. package/dist/recovery/staleness-monitor.js.map +1 -0
  60. package/dist/recovery/startup-recovery.d.ts +37 -0
  61. package/dist/recovery/startup-recovery.js +55 -0
  62. package/dist/recovery/startup-recovery.js.map +1 -0
  63. package/dist/registry/event-publication-registry.d.ts +70 -0
  64. package/dist/registry/event-publication-registry.js +138 -0
  65. package/dist/registry/event-publication-registry.js.map +1 -0
  66. package/dist/registry/listener-registry.d.ts +52 -0
  67. package/dist/registry/listener-registry.js +67 -0
  68. package/dist/registry/listener-registry.js.map +1 -0
  69. package/dist/registry/multi-ds-listener-registrar.d.ts +78 -0
  70. package/dist/registry/multi-ds-listener-registrar.js +112 -0
  71. package/dist/registry/multi-ds-listener-registrar.js.map +1 -0
  72. package/dist/registry/outbox-listener-scanner.d.ts +66 -0
  73. package/dist/registry/outbox-listener-scanner.js +192 -0
  74. package/dist/registry/outbox-listener-scanner.js.map +1 -0
  75. package/dist/repository/event-publication-repository.d.ts +112 -0
  76. package/dist/repository/event-publication-repository.js +6 -0
  77. package/dist/repository/event-publication-repository.js.map +1 -0
  78. package/dist/serialization/event-serializer.d.ts +28 -0
  79. package/dist/serialization/event-serializer.js +6 -0
  80. package/dist/serialization/event-serializer.js.map +1 -0
  81. package/dist/serialization/event-type-registry.d.ts +41 -0
  82. package/dist/serialization/event-type-registry.js +79 -0
  83. package/dist/serialization/event-type-registry.js.map +1 -0
  84. package/dist/serialization/event-type-resolver.d.ts +43 -0
  85. package/dist/serialization/event-type-resolver.js +66 -0
  86. package/dist/serialization/event-type-resolver.js.map +1 -0
  87. package/dist/serialization/json-event-serializer.d.ts +20 -0
  88. package/dist/serialization/json-event-serializer.js +64 -0
  89. package/dist/serialization/json-event-serializer.js.map +1 -0
  90. package/dist/testing/assertable-published-events.d.ts +71 -0
  91. package/dist/testing/assertable-published-events.js +124 -0
  92. package/dist/testing/assertable-published-events.js.map +1 -0
  93. package/dist/testing/in-memory-repository.d.ts +52 -0
  94. package/dist/testing/in-memory-repository.js +230 -0
  95. package/dist/testing/in-memory-repository.js.map +1 -0
  96. package/dist/testing/index.d.ts +4 -0
  97. package/dist/testing/index.js +20 -0
  98. package/dist/testing/index.js.map +1 -0
  99. package/dist/testing/published-events.d.ts +62 -0
  100. package/dist/testing/published-events.js +110 -0
  101. package/dist/testing/published-events.js.map +1 -0
  102. package/dist/tokens/index.d.ts +2 -0
  103. package/dist/tokens/index.js +14 -0
  104. package/dist/tokens/index.js.map +1 -0
  105. package/dist/tokens/token-utils.d.ts +68 -0
  106. package/dist/tokens/token-utils.js +98 -0
  107. package/dist/tokens/token-utils.js.map +1 -0
  108. package/dist/types/completion-mode.d.ts +16 -0
  109. package/dist/types/completion-mode.js +20 -0
  110. package/dist/types/completion-mode.js.map +1 -0
  111. package/dist/types/errors.d.ts +50 -0
  112. package/dist/types/errors.js +64 -0
  113. package/dist/types/errors.js.map +1 -0
  114. package/dist/types/event-publication.d.ts +47 -0
  115. package/dist/types/event-publication.js +3 -0
  116. package/dist/types/event-publication.js.map +1 -0
  117. package/dist/types/publication-status.d.ts +26 -0
  118. package/dist/types/publication-status.js +30 -0
  119. package/dist/types/publication-status.js.map +1 -0
  120. package/dist/types/resubmission-options.d.ts +37 -0
  121. package/dist/types/resubmission-options.js +62 -0
  122. package/dist/types/resubmission-options.js.map +1 -0
  123. package/dist/types/staleness-config.d.ts +18 -0
  124. package/dist/types/staleness-config.js +10 -0
  125. package/dist/types/staleness-config.js.map +1 -0
  126. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Igor Golovanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,396 @@
1
+ # @nestjs-transactional/outbox
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40nestjs-transactional%2Foutbox/alpha?style=flat-square&label=npm)](https://www.npmjs.com/package/@nestjs-transactional/outbox)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
5
+
6
+ Persistent Event Publication Registry for NestJS — an ORM-agnostic core
7
+ that brings Spring Modulith-equivalent delivery guarantees to the
8
+ `@nestjs-transactional` family of packages.
9
+
10
+ ## Overview
11
+
12
+ `@TransactionalEventsHandler` from `@nestjs-transactional/cqrs` provides
13
+ phase-based dispatching (like Spring Framework core): handlers fire
14
+ `AFTER_COMMIT`, `BEFORE_COMMIT`, `AFTER_ROLLBACK`, or `AFTER_COMPLETION`.
15
+ That covers a lot, but it is purely in-memory — if the process dies
16
+ between commit and handler invocation, the event is lost.
17
+
18
+ `outbox` closes that gap. It gives you:
19
+
20
+ - A persistent **Event Publication Registry** — every handler
21
+ invocation is logged atomically with the business transaction.
22
+ - **Retry on process restart** — publications that were not acknowledged
23
+ before shutdown are replayed on next startup.
24
+ - **Lifecycle states**: `PUBLISHED`, `PROCESSING`, `COMPLETED`, `FAILED`,
25
+ `RESUBMITTED`.
26
+ - **Staleness monitor** — detects publications stuck in `PROCESSING`.
27
+ - **Failed / Incomplete / Completed** query APIs for operators.
28
+ - **Completion modes**: `UPDATE`, `DELETE`, `ARCHIVE`.
29
+
30
+ This package only defines types, the repository SPI, the in-memory
31
+ reference implementation (for tests), and the Nest module wiring. It
32
+ does **not** ship a production persistence backend — that lives in a
33
+ sibling package such as `@nestjs-transactional/outbox-typeorm`.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pnpm add @nestjs-transactional/core @nestjs-transactional/outbox
39
+ # plus a persistence backend, e.g.:
40
+ pnpm add @nestjs-transactional/outbox-typeorm
41
+ ```
42
+
43
+ Peer dependencies: `@nestjs/common`, `@nestjs/core`, `reflect-metadata`,
44
+ `rxjs`.
45
+
46
+ ## Usage
47
+
48
+ ### 1. Module wiring
49
+
50
+ `OutboxModule.forRoot()` provides global infrastructure (registry,
51
+ serializer, processor, recovery). `OutboxModule.forFeature([...])`
52
+ declares the event classes a feature module owns — matches
53
+ `TypeOrmModule.forFeature(...)` ergonomics.
54
+
55
+ ```typescript
56
+ import { Module } from '@nestjs/common';
57
+ import { TransactionalModule } from '@nestjs-transactional/core';
58
+ import { OutboxModule, OutboxProcessingModule } from '@nestjs-transactional/outbox';
59
+
60
+ @Module({
61
+ imports: [
62
+ // isGlobal: true is REQUIRED so outbox providers can see
63
+ // TransactionManager across module boundaries.
64
+ TransactionalModule.forRoot({
65
+ isGlobal: true,
66
+ adapters: [/* your adapter registrations */],
67
+ }),
68
+ OutboxModule.forRoot({
69
+ republishOnStartup: true,
70
+ processor: { pollingInterval: 1000, batchSize: 100 },
71
+ staleness: { processing: 60_000, monitorInterval: 30_000 },
72
+ // repository: { provide: EVENT_PUBLICATION_REPOSITORY, useClass: TypeOrmEventPublicationRepository },
73
+ }),
74
+ // Import OutboxProcessingModule ONLY in worker processes — not in
75
+ // API-only apps that merely publish events.
76
+ OutboxProcessingModule,
77
+ // Register the event classes this module emits / consumes.
78
+ OutboxModule.forFeature([OrderPlacedEvent, OrderShippedEvent]),
79
+ ],
80
+ })
81
+ export class AppModule {}
82
+ ```
83
+
84
+ #### Modular pattern (multiple feature modules)
85
+
86
+ Distribute event registrations to the modules that own them:
87
+
88
+ ```typescript
89
+ @Module({
90
+ imports: [OutboxModule.forFeature([OrderPlacedEvent, OrderCancelledEvent])],
91
+ providers: [OrderService],
92
+ })
93
+ export class OrderModule {}
94
+
95
+ @Module({
96
+ imports: [OutboxModule.forFeature([InventoryReservedEvent])],
97
+ providers: [InventoryService],
98
+ })
99
+ export class InventoryModule {}
100
+
101
+ @Module({
102
+ imports: [
103
+ TransactionalModule.forRoot({ isGlobal: true /* ... */ }),
104
+ OutboxModule.forRoot({ republishOnStartup: true /* ... */ }),
105
+ OrderModule,
106
+ InventoryModule,
107
+ ],
108
+ })
109
+ export class AppModule {}
110
+ ```
111
+
112
+ Multiple `forFeature` calls accumulate into the singleton
113
+ `EventTypeRegistry` provided by `forRoot`. Each event class can only
114
+ be registered once — duplicates throw at bootstrap with the offending
115
+ class name:
116
+
117
+ ```
118
+ Error: Event type 'OrderPlacedEvent' already registered.
119
+ Each event type can only be registered once — check for duplicate
120
+ entries across OutboxModule.forFeature() calls.
121
+ ```
122
+
123
+ `OutboxModule.forFeature([])` is accepted as a no-op (matches
124
+ `TypeOrmModule.forFeature([])`).
125
+
126
+ ### 2. Declaring a handler
127
+
128
+ ```typescript
129
+ import { Injectable } from '@nestjs/common';
130
+ import {
131
+ type IOutboxEventHandler,
132
+ OutboxEventsHandler,
133
+ } from '@nestjs-transactional/outbox';
134
+
135
+ @Injectable()
136
+ @OutboxEventsHandler(OrderPlacedEvent)
137
+ export class InventoryReservationHandler
138
+ implements IOutboxEventHandler<OrderPlacedEvent>
139
+ {
140
+ async handle(event: OrderPlacedEvent): Promise<void> {
141
+ // Runs inside a fresh REQUIRES_NEW transaction, only after the
142
+ // publishing transaction has committed. Retried on exception,
143
+ // resumable across restarts.
144
+ }
145
+ }
146
+ ```
147
+
148
+ The decorator accepts either rest params or an options object:
149
+
150
+ ```typescript
151
+ // Short form — defaults (newTransaction: true).
152
+ @OutboxEventsHandler(OrderPlacedEvent, OrderCancelledEvent)
153
+
154
+ // Long form — explicit options.
155
+ @OutboxEventsHandler({
156
+ events: [OrderPlacedEvent],
157
+ id: 'inventory.reservation', // stable base id, see "Listener ids" below
158
+ newTransaction: false, // skip the REQUIRES_NEW wrapper
159
+ })
160
+ ```
161
+
162
+ See ADR-014 for the rationale behind the class-level shape.
163
+
164
+ ### 3. Publishing events
165
+
166
+ ```typescript
167
+ import { Injectable } from '@nestjs/common';
168
+ import { Transactional } from '@nestjs-transactional/core';
169
+ import { OutboxEventPublisher } from '@nestjs-transactional/outbox';
170
+
171
+ @Injectable()
172
+ export class PlaceOrderHandler {
173
+ constructor(private readonly outbox: OutboxEventPublisher) {}
174
+
175
+ @Transactional()
176
+ async handle(command: PlaceOrder): Promise<void> {
177
+ // ...persist business data in the same transaction...
178
+ await this.outbox.publish(new OrderPlacedEvent(command.orderId));
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### 4. Operator APIs
184
+
185
+ ```typescript
186
+ const failed = await this.failedEventPublications.findAll();
187
+ await this.failedEventPublications.resubmit(
188
+ ResubmissionOptions.defaults().withBatchSize(50).withMaxAttempts(3),
189
+ );
190
+ await this.completedEventPublications.purge(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
191
+ ```
192
+
193
+ ## Listener ids
194
+
195
+ Every stored publication carries a `listenerId` string that identifies
196
+ which handler class should deliver it. The scanner composes the id as:
197
+
198
+ ```
199
+ ${baseId}#${EventName}
200
+ ```
201
+
202
+ - **No explicit `id`**: baseId is the class name. Example:
203
+ `InventoryReservationHandler#OrderPlacedEvent`.
204
+ - **With `options.id`**: baseId is the supplied string. Example:
205
+ `inventory.reservation#OrderPlacedEvent`.
206
+
207
+ A class that handles multiple event types produces one id per event.
208
+ Renaming the class without supplying an explicit `id` breaks delivery
209
+ for every stored publication under the old id — set `options.id` when
210
+ you want stability across renames.
211
+
212
+ ## Externalization (advanced)
213
+
214
+ > The `EventExternalizer` SPI lives in this package. The
215
+ > `@nestjs/microservices` `ClientProxy`-backed implementation ships
216
+ > as a separate package —
217
+ > [`@nestjs-transactional/outbox-microservices`](../outbox-microservices).
218
+ > Without an externalizer bound, `@Externalized` mappings are
219
+ > recorded but never delivered to a broker — local outbox listeners
220
+ > still run.
221
+ >
222
+ > Architecture and design rationale:
223
+ > [ADR-015](../../docs/adr/015-event-externalization-architecture.md),
224
+ > [`docs/architecture/event-externalization.md`](../../docs/architecture/event-externalization.md).
225
+ > Reliability caveat for the `@nestjs/microservices`-backed
226
+ > implementation:
227
+ > [ADR-016](../../docs/adr/016-externalization-reliability-semantics.md).
228
+
229
+ `@Externalized` marks an event class for delivery to an external
230
+ message broker (Kafka topic, RabbitMQ exchange, NATS subject, ...) in
231
+ addition to local outbox listeners. The processor invokes the bound
232
+ `EventExternalizer` AFTER the local listener has succeeded — single
233
+ unit atomicity (DD-019): if either step fails, the publication is
234
+ recorded as `FAILED` and can be resubmitted via `FailedEventPublications`.
235
+
236
+ ```typescript
237
+ import { Externalized } from '@nestjs-transactional/outbox';
238
+
239
+ @Externalized<OrderPlacedEvent>({
240
+ target: 'orders.placed', // broker-side destination
241
+ routingKey: (e) => e.tenantId, // optional, brokers that support it
242
+ headers: (e) => ({ 'x-tenant': e.tenantId }), // static record OR callback
243
+ client: 'KAFKA_CLIENT', // optional ClientProxy token
244
+ })
245
+ export class OrderPlacedEvent {
246
+ constructor(
247
+ readonly orderId: string,
248
+ readonly tenantId: string,
249
+ ) {}
250
+ }
251
+ ```
252
+
253
+ Bind a concrete externalizer under the `EVENT_EXTERNALIZER` token —
254
+ this is normally done by an extension package (`outbox-microservices`).
255
+ Custom implementations can register the same way:
256
+
257
+ ```typescript
258
+ import { EVENT_EXTERNALIZER, type EventExternalizer } from '@nestjs-transactional/outbox';
259
+
260
+ @Module({
261
+ providers: [
262
+ {
263
+ provide: EVENT_EXTERNALIZER,
264
+ useClass: MyCustomExternalizer, // implements EventExternalizer
265
+ },
266
+ ],
267
+ })
268
+ export class MyAppModule {}
269
+ ```
270
+
271
+ The processor only invokes the externalizer when BOTH a binding under
272
+ `EVENT_EXTERNALIZER` exists AND the event class carries an
273
+ `@Externalized` mapping — keep one without the other if you only need
274
+ half the contract (e.g. metadata-only mappings during a rollout, or a
275
+ generic externalizer that resolves targets some other way). Inspect
276
+ the resolved mapping at runtime via `ExternalizationRegistry`:
277
+
278
+ ```typescript
279
+ import { ExternalizationRegistry } from '@nestjs-transactional/outbox';
280
+
281
+ @Injectable()
282
+ export class MyDiagnostics {
283
+ constructor(private readonly externalization: ExternalizationRegistry) {}
284
+
285
+ isExternalized(typeName: string): boolean {
286
+ return this.externalization.has(typeName);
287
+ }
288
+ }
289
+ ```
290
+
291
+ Errors raised by the externalizer are wrapped in `ExternalizationError`
292
+ (carrying `eventType`, `target`, and the underlying cause) and surface
293
+ on the publication's `failureReason` for operator visibility.
294
+
295
+ ## Testing utilities
296
+
297
+ Exported via the `/testing` subpath for assertions about which events
298
+ the code under test published. Mirrors Spring Modulith's
299
+ `PublishedEvents` / `AssertablePublishedEvents`.
300
+
301
+ ```typescript
302
+ import {
303
+ PublishedEvents,
304
+ AssertablePublishedEvents,
305
+ } from '@nestjs-transactional/outbox/testing';
306
+
307
+ describe('PlaceOrder', () => {
308
+ let app: TestingModule;
309
+ let assertablePublishedEvents: AssertablePublishedEvents;
310
+
311
+ beforeEach(async () => {
312
+ app = await Test.createTestingModule({
313
+ imports: [
314
+ TransactionalModule.forRoot({ isGlobal: true /* ... */ }),
315
+ OutboxModule.forRoot({}),
316
+ OutboxModule.forFeature([OrderPlacedEvent]),
317
+ ],
318
+ providers: [
319
+ PlaceOrderService,
320
+ PublishedEvents,
321
+ AssertablePublishedEvents,
322
+ ],
323
+ }).compile();
324
+ await app.init();
325
+ assertablePublishedEvents = app.get(AssertablePublishedEvents);
326
+ });
327
+
328
+ it('publishes OrderPlacedEvent for the placed order', async () => {
329
+ const service = app.get(PlaceOrderService);
330
+ await service.place('order-123');
331
+
332
+ const view = await assertablePublishedEvents.contains(OrderPlacedEvent);
333
+ view.matching((e) => e.orderId, 'order-123').hasSize(1);
334
+ });
335
+
336
+ it('publishes nothing when validation rejects the command', async () => {
337
+ const service = app.get(PlaceOrderService);
338
+ await service.placeInvalid().catch(() => undefined);
339
+
340
+ await assertablePublishedEvents.doesNotContain(OrderPlacedEvent);
341
+ });
342
+ });
343
+ ```
344
+
345
+ `PublishedEvents.ofType(EventType)` returns a fluent `PublishedEventsView`
346
+ for read-only inspection (`.get()`, `.count()`,
347
+ `.matching(predicate)`, `.matching(getter, expected)`).
348
+ `AssertablePublishedEvents.contains(EventType)` is the assertion-first
349
+ counterpart — it throws `PublishedEventsAssertionError` when zero
350
+ events match, and returns an `AssertionView` whose `.matching(...)`
351
+ and `.hasSize(...)` operate synchronously over the already-fetched
352
+ events.
353
+
354
+ The utilities read through the wired `EventPublicationRepository`
355
+ implementation, so the default `InMemoryEventPublicationRepository`
356
+ works out of the box for unit-level tests. Integration tests that use
357
+ `@nestjs-transactional/outbox-typeorm` with a real Postgres get the
358
+ same API for free.
359
+
360
+ ## Worked examples
361
+
362
+ - [`basic-outbox`](../../examples/basic-outbox) — `@OutboxEventsHandler` + `OutboxEventPublisher`, in-memory test repository.
363
+ - [`basic-typeorm-outbox`](../../examples/basic-typeorm-outbox) — production-shape outbox with Postgres, atomicity proven via testcontainers.
364
+ - [`multi-datasource-outbox`](../../examples/multi-datasource-outbox) — per-DS `event_publication` tables (ADR-019 multi-`forRoot`).
365
+ - [`shared-database-modular-monolith`](../../examples/shared-database-modular-monolith) — one Postgres, multi-schema, per-module outbox stacks.
366
+ - [`saga-pattern`](../../examples/saga-pattern) — choreographed multi-step business saga over outbox events.
367
+ - [`audit-logging`](../../examples/audit-logging) — cross-DS audit trail with idempotency on the consumer.
368
+ - [`e-commerce-orders`](../../examples/e-commerce-orders) — three-bounded-context flagship wired through outbox integration events.
369
+
370
+ Full catalogue: [examples/README.md](../../examples/README.md).
371
+
372
+ ## Status
373
+
374
+ Alpha. Public API may change between 0.x releases. Current
375
+ functionality is exercised end-to-end through the
376
+ [Tier 1–5 example library](../../examples/README.md).
377
+
378
+ Design notes:
379
+ [`docs/roadmap/README.md`](../../docs/roadmap/README.md),
380
+ [`docs/adr/006-outbox-pattern.md`](../../docs/adr/006-outbox-pattern.md).
381
+
382
+ ## Inspired by Spring Modulith
383
+
384
+ The design follows
385
+ [Spring Modulith's Event Publication Registry](https://docs.spring.io/spring-modulith/reference/events.html)
386
+ closely — lifecycle states, `@ApplicationModuleListener` semantics,
387
+ completion modes, and staleness monitoring all map one-to-one. The
388
+ deviations from Spring Modulith are limited to what is needed to fit
389
+ the Node.js / NestJS runtime (async workers instead of thread pools,
390
+ AsyncLocalStorage for transaction context, NestJS DI conventions,
391
+ class-level handler decorators aligned with `@nestjs/cqrs`
392
+ conventions — see ADR-014).
393
+
394
+ ## License
395
+
396
+ MIT
@@ -0,0 +1,31 @@
1
+ import { type EventPublicationRepository } from '../repository/event-publication-repository';
2
+ import type { EventPublication } from '../types/event-publication';
3
+ /**
4
+ * Operator-facing query + purge API for publications that have
5
+ * reached the terminal `COMPLETED` state. Equivalent to Spring
6
+ * Modulith's `CompletedEventPublications`.
7
+ *
8
+ * Relevant when the completion mode is `UPDATE` (the default) — the
9
+ * row is kept for audit and needs to be purged eventually.
10
+ */
11
+ export declare class CompletedEventPublications {
12
+ private readonly repository;
13
+ constructor(repository: EventPublicationRepository);
14
+ /**
15
+ * Return completed publications, optionally filtered by
16
+ * `completionDate` and capped by `limit`.
17
+ */
18
+ findAll(options?: {
19
+ readonly olderThan?: Date;
20
+ readonly limit?: number;
21
+ }): Promise<EventPublication[]>;
22
+ /** How many publications are currently in `COMPLETED`. */
23
+ count(): Promise<number>;
24
+ /**
25
+ * Delete completed publications. When `olderThan` is provided, only
26
+ * publications whose `completionDate` is strictly before it are
27
+ * removed. Returns the number of rows deleted.
28
+ */
29
+ purge(olderThan?: Date): Promise<number>;
30
+ }
31
+ //# sourceMappingURL=completed-event-publications.d.ts.map
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.CompletedEventPublications = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const event_publication_repository_1 = require("../repository/event-publication-repository");
18
+ /**
19
+ * Operator-facing query + purge API for publications that have
20
+ * reached the terminal `COMPLETED` state. Equivalent to Spring
21
+ * Modulith's `CompletedEventPublications`.
22
+ *
23
+ * Relevant when the completion mode is `UPDATE` (the default) — the
24
+ * row is kept for audit and needs to be purged eventually.
25
+ */
26
+ let CompletedEventPublications = class CompletedEventPublications {
27
+ repository;
28
+ constructor(repository) {
29
+ this.repository = repository;
30
+ }
31
+ /**
32
+ * Return completed publications, optionally filtered by
33
+ * `completionDate` and capped by `limit`.
34
+ */
35
+ async findAll(options) {
36
+ return this.repository.findCompleted(options);
37
+ }
38
+ /** How many publications are currently in `COMPLETED`. */
39
+ async count() {
40
+ const completed = await this.repository.findCompleted();
41
+ return completed.length;
42
+ }
43
+ /**
44
+ * Delete completed publications. When `olderThan` is provided, only
45
+ * publications whose `completionDate` is strictly before it are
46
+ * removed. Returns the number of rows deleted.
47
+ */
48
+ async purge(olderThan) {
49
+ return this.repository.deleteCompleted(olderThan);
50
+ }
51
+ };
52
+ exports.CompletedEventPublications = CompletedEventPublications;
53
+ exports.CompletedEventPublications = CompletedEventPublications = __decorate([
54
+ (0, common_1.Injectable)(),
55
+ __param(0, (0, common_1.Inject)(event_publication_repository_1.EVENT_PUBLICATION_REPOSITORY)),
56
+ __metadata("design:paramtypes", [Object])
57
+ ], CompletedEventPublications);
58
+ //# sourceMappingURL=completed-event-publications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completed-event-publications.js","sourceRoot":"","sources":["../../src/api/completed-event-publications.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,6FAGoD;AAGpD;;;;;;;GAOG;AAEI,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IAGlB;IAFnB,YAEmB,UAAsC;QAAtC,eAAU,GAAV,UAAU,CAA4B;IACtD,CAAC;IAEJ;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,OAGb;QACC,OAAO,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,KAAK;QACT,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK,CAAC,SAAgB;QAC1B,OAAO,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF,CAAA;AA/BY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,2DAA4B,CAAC,CAAA;;GAF5B,0BAA0B,CA+BtC"}
@@ -0,0 +1,36 @@
1
+ import { type EventPublicationRepository } from '../repository/event-publication-repository';
2
+ import type { EventPublication } from '../types/event-publication';
3
+ import { ResubmissionOptions } from '../types/resubmission-options';
4
+ /**
5
+ * Operator-facing query + resubmit API for publications currently in
6
+ * the `FAILED` state. Equivalent to Spring Modulith's
7
+ * `FailedEventPublications`.
8
+ */
9
+ export declare class FailedEventPublications {
10
+ private readonly repository;
11
+ constructor(repository: EventPublicationRepository);
12
+ /**
13
+ * Return every failed publication matching the optional filters
14
+ * (minimum age in ms since `publicationDate`, cap on
15
+ * `completionAttempts`).
16
+ */
17
+ findAll(options?: {
18
+ readonly minAge?: number;
19
+ readonly maxAttempts?: number;
20
+ }): Promise<EventPublication[]>;
21
+ /** How many publications are currently in `FAILED`. */
22
+ count(): Promise<number>;
23
+ /**
24
+ * Transition selected failed publications to `RESUBMITTED` so the
25
+ * processor will pick them up again. Returns the number of rows
26
+ * transitioned.
27
+ *
28
+ * Selection order:
29
+ * 1. Pull failed publications matching the `minAge` /
30
+ * `maxCompletionAttempts` filters.
31
+ * 2. Apply the user-supplied `filter` predicate, if any.
32
+ * 3. Keep at most `batchSize` publications.
33
+ */
34
+ resubmit(options?: ResubmissionOptions): Promise<number>;
35
+ }
36
+ //# sourceMappingURL=failed-event-publications.d.ts.map
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FailedEventPublications = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const event_publication_repository_1 = require("../repository/event-publication-repository");
18
+ const publication_status_1 = require("../types/publication-status");
19
+ const resubmission_options_1 = require("../types/resubmission-options");
20
+ /**
21
+ * Operator-facing query + resubmit API for publications currently in
22
+ * the `FAILED` state. Equivalent to Spring Modulith's
23
+ * `FailedEventPublications`.
24
+ */
25
+ let FailedEventPublications = class FailedEventPublications {
26
+ repository;
27
+ constructor(repository) {
28
+ this.repository = repository;
29
+ }
30
+ /**
31
+ * Return every failed publication matching the optional filters
32
+ * (minimum age in ms since `publicationDate`, cap on
33
+ * `completionAttempts`).
34
+ */
35
+ async findAll(options) {
36
+ return this.repository.findFailed(options);
37
+ }
38
+ /** How many publications are currently in `FAILED`. */
39
+ async count() {
40
+ const failed = await this.repository.findFailed();
41
+ return failed.length;
42
+ }
43
+ /**
44
+ * Transition selected failed publications to `RESUBMITTED` so the
45
+ * processor will pick them up again. Returns the number of rows
46
+ * transitioned.
47
+ *
48
+ * Selection order:
49
+ * 1. Pull failed publications matching the `minAge` /
50
+ * `maxCompletionAttempts` filters.
51
+ * 2. Apply the user-supplied `filter` predicate, if any.
52
+ * 3. Keep at most `batchSize` publications.
53
+ */
54
+ async resubmit(options = resubmission_options_1.ResubmissionOptions.defaults()) {
55
+ const failed = await this.repository.findFailed({
56
+ minAge: options.minAge,
57
+ maxAttempts: options.maxCompletionAttempts ?? undefined,
58
+ });
59
+ const filtered = options.filter !== null ? failed.filter(options.filter) : failed;
60
+ const toProcess = filtered.slice(0, options.batchSize);
61
+ for (const pub of toProcess) {
62
+ await this.repository.updateStatus(pub.id, publication_status_1.PublicationStatus.RESUBMITTED, {
63
+ lastResubmissionDate: new Date(),
64
+ });
65
+ }
66
+ return toProcess.length;
67
+ }
68
+ };
69
+ exports.FailedEventPublications = FailedEventPublications;
70
+ exports.FailedEventPublications = FailedEventPublications = __decorate([
71
+ (0, common_1.Injectable)(),
72
+ __param(0, (0, common_1.Inject)(event_publication_repository_1.EVENT_PUBLICATION_REPOSITORY)),
73
+ __metadata("design:paramtypes", [Object])
74
+ ], FailedEventPublications);
75
+ //# sourceMappingURL=failed-event-publications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failed-event-publications.js","sourceRoot":"","sources":["../../src/api/failed-event-publications.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AAEpD,6FAGoD;AAEpD,oEAAgE;AAChE,wEAAoE;AAGpE;;;;GAIG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAGf;IAFnB,YAEmB,UAAsC;QAAtC,eAAU,GAAV,UAAU,CAA4B;IACtD,CAAC;IAEJ;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,OAGb;QACC,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,KAAK;QACT,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;QAClD,OAAO,MAAM,CAAC,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,QAAQ,CACZ,UAA+B,0CAAmB,CAAC,QAAQ,EAAE;QAE7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;YAC9C,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,WAAW,EAAE,OAAO,CAAC,qBAAqB,IAAI,SAAS;SACxD,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAClF,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAEvD,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,sCAAiB,CAAC,WAAW,EAAE;gBACxE,oBAAoB,EAAE,IAAI,IAAI,EAAE;aACjC,CAAC,CAAC;QACL,CAAC;QAED,OAAO,SAAS,CAAC,MAAM,CAAC;IAC1B,CAAC;CACF,CAAA;AAtDY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,2DAA4B,CAAC,CAAA;;GAF5B,uBAAuB,CAsDnC"}
@@ -0,0 +1,28 @@
1
+ import { type EventPublicationRepository } from '../repository/event-publication-repository';
2
+ import type { EventPublication } from '../types/event-publication';
3
+ import { ResubmissionOptions } from '../types/resubmission-options';
4
+ /**
5
+ * Operator-facing query + resubmit API for every publication that has
6
+ * not yet reached the terminal `COMPLETED` state. Equivalent to Spring
7
+ * Modulith's `IncompleteEventPublications`.
8
+ *
9
+ * Typical use case: startup / restart recovery — query the backlog,
10
+ * bulk-resubmit retriable rows, let the processor drain.
11
+ */
12
+ export declare class IncompleteEventPublications {
13
+ private readonly repository;
14
+ constructor(repository: EventPublicationRepository);
15
+ /** Return every publication whose status is not `COMPLETED`. */
16
+ findAll(): Promise<EventPublication[]>;
17
+ /** How many publications are currently not `COMPLETED`. */
18
+ count(): Promise<number>;
19
+ /**
20
+ * Scan every non-completed publication and transition retriable
21
+ * ones (`FAILED`, `PUBLISHED`) to `RESUBMITTED`. Publications
22
+ * currently `PROCESSING` or `RESUBMITTED` are left untouched —
23
+ * `PROCESSING` is owned by a worker and `RESUBMITTED` is already in
24
+ * the retry queue. Returns the number of rows transitioned.
25
+ */
26
+ resubmitIncompletePublications(options?: ResubmissionOptions): Promise<number>;
27
+ }
28
+ //# sourceMappingURL=incomplete-event-publications.d.ts.map