@nestjs-transactional/outbox-microservices 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.
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/externalizer/microservices-event-externalizer.d.ts +37 -0
- package/dist/externalizer/microservices-event-externalizer.js +133 -0
- package/dist/externalizer/microservices-event-externalizer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/module/outbox-microservices.module.d.ts +60 -0
- package/dist/module/outbox-microservices.module.js +95 -0
- package/dist/module/outbox-microservices.module.js.map +1 -0
- package/dist/types/options.d.ts +40 -0
- package/dist/types/options.js +11 -0
- package/dist/types/options.js.map +1 -0
- package/package.json +79 -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,311 @@
|
|
|
1
|
+
# @nestjs-transactional/outbox-microservices
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nestjs-transactional/outbox-microservices)
|
|
4
|
+
[](https://github.com/igorgolovanov/nestjs-transactional/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
> Spring Modulith `@Externalized` parity for NestJS — durable,
|
|
7
|
+
> retryable delivery of outbox events to external message brokers via
|
|
8
|
+
> the `@nestjs/microservices` `ClientProxy` abstraction.
|
|
9
|
+
|
|
10
|
+
`MicroservicesEventExternalizer` plugs into `@nestjs-transactional/outbox`
|
|
11
|
+
as the concrete `EventExternalizer` implementation and reuses the
|
|
12
|
+
existing `ClientsModule` registration in your application — one
|
|
13
|
+
package covers every transport `@nestjs/microservices` already
|
|
14
|
+
supports (Kafka, RabbitMQ, NATS, JMS, gRPC, custom).
|
|
15
|
+
|
|
16
|
+
## Architectural foundation
|
|
17
|
+
|
|
18
|
+
- [ADR-015 — event externalization architecture](../../docs/adr/015-event-externalization-architecture.md)
|
|
19
|
+
explains the design (single-package strategy, structural-port SPI,
|
|
20
|
+
reuse of `ClientsModule`, atomicity / ordering rules).
|
|
21
|
+
- [ADR-016 — externalization reliability semantics with `@nestjs/microservices`](../../docs/adr/016-externalization-reliability-semantics.md)
|
|
22
|
+
documents the silent-success limitation that scopes what this
|
|
23
|
+
package can guarantee out of the box, and the three production
|
|
24
|
+
mitigation strategies.
|
|
25
|
+
- [`docs/architecture/event-externalization.md`](../../docs/architecture/event-externalization.md)
|
|
26
|
+
has diagrams, the end-to-end sequence, the failure-mode table, and
|
|
27
|
+
the Spring Modulith mapping.
|
|
28
|
+
|
|
29
|
+
### Spring Modulith mapping (at a glance)
|
|
30
|
+
|
|
31
|
+
This package is the NestJS analogue of Spring Modulith's
|
|
32
|
+
`@Externalized` plus its four broker-specific artefacts
|
|
33
|
+
(`spring-modulith-events-kafka`, `-amqp`, `-jms`, `-messaging`)
|
|
34
|
+
collapsed into one. `@Externalized` (in `outbox`) lifts directly
|
|
35
|
+
from Spring's annotation; the broker setup moves from a Spring
|
|
36
|
+
auto-configuration to the user's own `ClientsModule.register()`.
|
|
37
|
+
Function-based `routingKey` and `headers` callbacks replace SpEL
|
|
38
|
+
expressions; bring your own type system. Full table in the
|
|
39
|
+
architecture doc above.
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
Alpha. Public API may change between 0.x releases. Headers /
|
|
44
|
+
`routingKey` are accepted on `@Externalized` but not yet applied to
|
|
45
|
+
the wire payload — see *Limitations* below. End-to-end coverage
|
|
46
|
+
lives in the externalization examples (`externalization-kafka`,
|
|
47
|
+
`externalization-multi-broker`, `externalization-multi-datasource`,
|
|
48
|
+
`externalization-with-fallback`) and the
|
|
49
|
+
[`e-commerce-orders`](../../examples/e-commerce-orders) flagship —
|
|
50
|
+
see [Worked examples](#worked-examples).
|
|
51
|
+
|
|
52
|
+
## Important: reliability semantics (read before production use)
|
|
53
|
+
|
|
54
|
+
**Read this before adopting the package in production.** The
|
|
55
|
+
`@nestjs/microservices` `ClientProxy.emit()` API this package depends
|
|
56
|
+
on (per [DD-017](../../docs/dd/017-reuse-clients-module.md)) does NOT propagate broker-side
|
|
57
|
+
delivery failures in a way the externalizer can observe. In
|
|
58
|
+
fire-and-forget mode the Observable returned by `emit()` completes
|
|
59
|
+
when the proxy considers the dispatch *handed off to the transport*,
|
|
60
|
+
not when the broker has *durably acknowledged* the message.
|
|
61
|
+
|
|
62
|
+
Concretely, this means:
|
|
63
|
+
|
|
64
|
+
- A `ClientKafka` configured against an unreachable broker can resolve
|
|
65
|
+
`emit()` successfully, the externalizer reports success, and the
|
|
66
|
+
outbox publication is finalised as `COMPLETED` — even though no
|
|
67
|
+
message ever reached a broker.
|
|
68
|
+
- The same applies to RabbitMQ in default fire-and-forget mode and to
|
|
69
|
+
any other transport `@nestjs/microservices` supports.
|
|
70
|
+
- Silent broker failures bypass the outbox retry / staleness /
|
|
71
|
+
resubmit machinery: there is nothing to retry, because as far as
|
|
72
|
+
this layer is concerned delivery succeeded.
|
|
73
|
+
|
|
74
|
+
The outbox still gives you crash-consistent **enqueueing** of events
|
|
75
|
+
and at-least-once **local listener** delivery. What it does NOT give
|
|
76
|
+
you, in this version, is at-least-once **broker-side** delivery
|
|
77
|
+
through `ClientProxy`.
|
|
78
|
+
|
|
79
|
+
[ADR-016](../../docs/adr/016-externalization-reliability-semantics.md)
|
|
80
|
+
documents the finding in full and lays out the future path
|
|
81
|
+
(broker-aware externalizers using native producers under the same
|
|
82
|
+
`EVENT_EXTERNALIZER` SPI).
|
|
83
|
+
[ADR-015](../../docs/adr/015-event-externalization-architecture.md)
|
|
84
|
+
records why this trade-off is acceptable for the v1 scope —
|
|
85
|
+
[`docs/architecture/event-externalization.md`](../../docs/architecture/event-externalization.md)
|
|
86
|
+
has the full sequence diagram and the failure-mode table.
|
|
87
|
+
|
|
88
|
+
### Mitigation strategies for production
|
|
89
|
+
|
|
90
|
+
1. **Configure the underlying `ClientProxy` for stronger
|
|
91
|
+
acknowledgment.** Kafka: `producer.acks: 'all'` plus
|
|
92
|
+
`producer.idempotent: true`. RabbitMQ: confirm-channel via
|
|
93
|
+
`amqp-connection-manager`. NATS: JetStream with explicit ack. The
|
|
94
|
+
package reuses whatever proxy you registered (DD-017) — it does
|
|
95
|
+
not interfere with this configuration.
|
|
96
|
+
|
|
97
|
+
2. **Combine with consumer-side acknowledgment / inbox patterns.**
|
|
98
|
+
Track processed message ids on the receiving system and surface
|
|
99
|
+
gaps to operators. The outbox publication's listener id plus the
|
|
100
|
+
domain event id is enough to deduplicate.
|
|
101
|
+
|
|
102
|
+
3. **Wait for the broker-aware externalizer iteration** when neither
|
|
103
|
+
of the above is feasible. The
|
|
104
|
+
[`EVENT_EXTERNALIZER` SPI](../outbox/src/externalization/event-externalizer.ts)
|
|
105
|
+
is stable; native adapters will plug into the same place without
|
|
106
|
+
client-side changes.
|
|
107
|
+
|
|
108
|
+
## Installation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm add @nestjs-transactional/outbox-microservices @nestjs-transactional/outbox @nestjs/microservices
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`@nestjs-transactional/core`, `@nestjs/common`, `@nestjs/core`,
|
|
115
|
+
`reflect-metadata`, and `rxjs` are peer dependencies (already present
|
|
116
|
+
in any NestJS application).
|
|
117
|
+
|
|
118
|
+
## Prerequisites
|
|
119
|
+
|
|
120
|
+
This package does NOT register `ClientProxy` instances — that is your
|
|
121
|
+
job (DD-017). Configure them through the standard
|
|
122
|
+
`@nestjs/microservices` `ClientsModule`:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
126
|
+
|
|
127
|
+
@Module({
|
|
128
|
+
imports: [
|
|
129
|
+
ClientsModule.register([
|
|
130
|
+
{
|
|
131
|
+
name: 'KAFKA_CLIENT',
|
|
132
|
+
transport: Transport.KAFKA,
|
|
133
|
+
options: {
|
|
134
|
+
client: { brokers: ['localhost:9092'] },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
]),
|
|
138
|
+
],
|
|
139
|
+
})
|
|
140
|
+
export class AppModule {}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' })`
|
|
144
|
+
then resolves the same proxy via `ModuleRef.get(token, { strict: false })`
|
|
145
|
+
when an outbox publication is ready to be externalized — no parallel
|
|
146
|
+
connection pool, no second mental model.
|
|
147
|
+
|
|
148
|
+
## Basic example
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { Module } from '@nestjs/common';
|
|
152
|
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
153
|
+
import { TransactionalModule } from '@nestjs-transactional/core';
|
|
154
|
+
import { Externalized, OutboxModule } from '@nestjs-transactional/outbox';
|
|
155
|
+
import { OutboxMicroservicesModule } from '@nestjs-transactional/outbox-microservices';
|
|
156
|
+
|
|
157
|
+
@Externalized<OrderPlacedEvent>({
|
|
158
|
+
target: 'orders.placed',
|
|
159
|
+
routingKey: (e) => e.tenantId, // see Limitations — logged, not yet applied
|
|
160
|
+
})
|
|
161
|
+
export class OrderPlacedEvent {
|
|
162
|
+
constructor(
|
|
163
|
+
readonly orderId: string,
|
|
164
|
+
readonly tenantId: string,
|
|
165
|
+
) {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Module({
|
|
169
|
+
imports: [
|
|
170
|
+
TransactionalModule.forRoot({ isGlobal: true }),
|
|
171
|
+
ClientsModule.register([
|
|
172
|
+
{
|
|
173
|
+
name: 'KAFKA_CLIENT',
|
|
174
|
+
transport: Transport.KAFKA,
|
|
175
|
+
options: { client: { brokers: ['localhost:9092'] } },
|
|
176
|
+
},
|
|
177
|
+
]),
|
|
178
|
+
OutboxModule.forRoot({}),
|
|
179
|
+
OutboxModule.forFeature([OrderPlacedEvent]),
|
|
180
|
+
OutboxMicroservicesModule.forRoot({
|
|
181
|
+
defaultClient: 'KAFKA_CLIENT',
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
})
|
|
185
|
+
export class AppModule {}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When an `OrderPlacedEvent` flows through the outbox the local
|
|
189
|
+
listeners run first; once they succeed the externalizer calls
|
|
190
|
+
`KAFKA_CLIENT.emit('orders.placed', event)`. Failures (broker down,
|
|
191
|
+
client misconfigured, ...) mark the publication `FAILED` and surface
|
|
192
|
+
through `FailedEventPublications.resubmit()` — single-unit atomicity
|
|
193
|
+
per [DD-019](../../docs/dd/019-hybrid-delivery-atomicity.md).
|
|
194
|
+
|
|
195
|
+
## Multiple clients
|
|
196
|
+
|
|
197
|
+
Register every transport you need under distinct tokens, then point
|
|
198
|
+
each event at its broker via the `client` option on `@Externalized`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
ClientsModule.register([
|
|
202
|
+
{ name: 'KAFKA_CLIENT', transport: Transport.KAFKA, options: { ... } },
|
|
203
|
+
{ name: 'AMQP_CLIENT', transport: Transport.RMQ, options: { ... } },
|
|
204
|
+
]),
|
|
205
|
+
|
|
206
|
+
@Externalized({ target: 'orders.placed', client: 'KAFKA_CLIENT' })
|
|
207
|
+
class OrderPlacedEvent { /* ... */ }
|
|
208
|
+
|
|
209
|
+
@Externalized({ target: 'audit', client: 'AMQP_CLIENT' })
|
|
210
|
+
class AuditableEvent { /* ... */ }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A `defaultClient` configured on the module is used when an event
|
|
214
|
+
omits the per-event `client`. Set neither and the externalizer
|
|
215
|
+
rejects the publication with a clear `ExternalizationError` — the row
|
|
216
|
+
is recorded as `FAILED` and the operator can fix the configuration
|
|
217
|
+
and resubmit.
|
|
218
|
+
|
|
219
|
+
## Multi-dataSource setups
|
|
220
|
+
|
|
221
|
+
This package needs no special configuration when the application
|
|
222
|
+
runs with multiple `OutboxModule.forRoot()` calls (one per
|
|
223
|
+
dataSource — ADR-019 multi-forRoot pattern). A single
|
|
224
|
+
`MicroservicesEventExternalizer` instance covers every dataSource;
|
|
225
|
+
per-DS processors all dispatch through it. Per-broker routing is
|
|
226
|
+
already handled by the per-event `client` parameter shown in
|
|
227
|
+
[*Multiple clients*](#multiple-clients) above — `@Externalized` is
|
|
228
|
+
dataSource-agnostic by design (an event class that lives in the
|
|
229
|
+
`'billing'` dataSource via `OutboxModule.forFeature([Event], { dataSource: 'billing' })`
|
|
230
|
+
still uses the same `client:` token resolution as a default-DS
|
|
231
|
+
event). See [`@nestjs-transactional/outbox` README](../outbox/README.md)
|
|
232
|
+
for the multi-`forRoot` pattern.
|
|
233
|
+
|
|
234
|
+
The module is registered as `@Global()` so the bound
|
|
235
|
+
`EVENT_EXTERNALIZER` is visible to `OutboxModule`'s sibling-imported
|
|
236
|
+
per-DS processors without an explicit import chain.
|
|
237
|
+
|
|
238
|
+
## Async configuration
|
|
239
|
+
|
|
240
|
+
For `defaultClient` that must be resolved from a `ConfigService`:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
OutboxMicroservicesModule.forRootAsync({
|
|
244
|
+
imports: [ConfigModule],
|
|
245
|
+
inject: [ConfigService],
|
|
246
|
+
useFactory: (config: ConfigService) => ({
|
|
247
|
+
defaultClient: config.getOrThrow<string>('outbox.defaultClient'),
|
|
248
|
+
}),
|
|
249
|
+
}),
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Bootstrap validation
|
|
253
|
+
|
|
254
|
+
By default the module resolves `defaultClient` once at
|
|
255
|
+
`OnApplicationBootstrap` and throws a descriptive error if the token
|
|
256
|
+
is unbound — the misconfiguration surfaces before the first event is
|
|
257
|
+
processed. Disable with `validateOnBootstrap: false` when the
|
|
258
|
+
`ClientProxy` registration is wired by an asynchronous factory that
|
|
259
|
+
finishes after the outbox bootstrap (the lookup is then deferred to
|
|
260
|
+
the first `externalize()` call).
|
|
261
|
+
|
|
262
|
+
## Limitations
|
|
263
|
+
|
|
264
|
+
- **Headers and `routingKey` are accepted but not applied** to the
|
|
265
|
+
wire payload yet. The `@nestjs/microservices` `ClientProxy.emit`
|
|
266
|
+
API has no unified headers / routing-key parameter — handling is
|
|
267
|
+
transport-specific (Kafka headers, AMQP properties, NATS subject
|
|
268
|
+
suffixes, ...). For now the externalizer logs resolved values at
|
|
269
|
+
debug level for visibility; the broker-aware message-construction
|
|
270
|
+
iteration ships in a later release. Wrap the event in a
|
|
271
|
+
transport-specific envelope inside your own code if you need them
|
|
272
|
+
before then.
|
|
273
|
+
- **Real-broker integration tests** (Postgres + Kafka / RabbitMQ via
|
|
274
|
+
testcontainers) are not bundled with this package — the unit and
|
|
275
|
+
module specs use a mock `ClientProxy` and cover the SPI contract
|
|
276
|
+
end-to-end without a live broker. The
|
|
277
|
+
[`externalization-with-fallback`](../../examples/externalization-with-fallback/)
|
|
278
|
+
example demonstrates a real-broker setup via docker-compose with
|
|
279
|
+
the ADR-016 silent-success limitation observable end-to-end.
|
|
280
|
+
|
|
281
|
+
## Testing
|
|
282
|
+
|
|
283
|
+
The package's own tests use a mock `ClientProxy` directly. To exercise
|
|
284
|
+
the externalizer in your application's tests, register a stub provider
|
|
285
|
+
under your client's token before the module imports:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const moduleRef = await Test.createTestingModule({
|
|
289
|
+
imports: [
|
|
290
|
+
/* ... ClientsModule.register([{ name: 'KAFKA_CLIENT', ... }]) */
|
|
291
|
+
OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' }),
|
|
292
|
+
],
|
|
293
|
+
})
|
|
294
|
+
.overrideProvider('KAFKA_CLIENT')
|
|
295
|
+
.useValue({ emit: jest.fn(() => of(undefined)) })
|
|
296
|
+
.compile();
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Worked examples
|
|
300
|
+
|
|
301
|
+
- [`externalization-kafka`](../../examples/externalization-kafka) — single DataSource + single Kafka broker, the canonical baseline.
|
|
302
|
+
- [`externalization-multi-broker`](../../examples/externalization-multi-broker) — Kafka + RabbitMQ + Redis pub/sub routed per event via `@Externalized({ client })`.
|
|
303
|
+
- [`externalization-multi-datasource`](../../examples/externalization-multi-datasource) — two physical Postgres × two `ClientProxy` registrations on a single broker.
|
|
304
|
+
- [`externalization-with-fallback`](../../examples/externalization-with-fallback) — ADR-016 silent-success demo + the three production mitigation patterns + `FailedEventPublications.resubmit` recovery flow.
|
|
305
|
+
- [`e-commerce-orders`](../../examples/e-commerce-orders) — flagship application; externalization is the terminal step of the order saga.
|
|
306
|
+
|
|
307
|
+
Full catalogue: [examples/README.md](../../examples/README.md).
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT — see [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
import { type EventExternalizer, type ExternalizationMetadata } from '@nestjs-transactional/outbox';
|
|
4
|
+
import { type OutboxMicroservicesOptions } from '../types/options';
|
|
5
|
+
/**
|
|
6
|
+
* {@link EventExternalizer} implementation backed by `@nestjs/microservices`
|
|
7
|
+
* `ClientProxy`. Routes externalized events to whichever transport
|
|
8
|
+
* the configured `ClientProxy` was registered with — Kafka, RabbitMQ,
|
|
9
|
+
* NATS, JMS, gRPC, or any custom transport.
|
|
10
|
+
*
|
|
11
|
+
* Per DD-017 this externalizer does NOT register clients itself;
|
|
12
|
+
* users register them via standard `ClientsModule.register()` /
|
|
13
|
+
* `ClientsModule.registerAsync()` and pass the token through
|
|
14
|
+
* `OutboxMicroservicesModule.forRoot({ defaultClient })`. Per-event
|
|
15
|
+
* overrides via `@Externalized({ client })` resolve through the same
|
|
16
|
+
* `ModuleRef.get(token, { strict: false })` lookup.
|
|
17
|
+
*
|
|
18
|
+
* **Headers / routingKey limitation (Phase 11.3):** the
|
|
19
|
+
* `@nestjs/microservices` `ClientProxy.emit` API has no unified
|
|
20
|
+
* headers / routing-key parameter — handling is transport-specific
|
|
21
|
+
* (Kafka headers, AMQP properties, NATS subject suffixes, ...). For
|
|
22
|
+
* the first version we log resolved headers and routing keys at debug
|
|
23
|
+
* level for visibility but do not apply them to the wire payload —
|
|
24
|
+
* users that need them can wrap the event in a transport-specific
|
|
25
|
+
* envelope inside their own code or wait for the broker-aware message
|
|
26
|
+
* construction iteration.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MicroservicesEventExternalizer implements EventExternalizer, OnApplicationBootstrap {
|
|
29
|
+
private readonly moduleRef;
|
|
30
|
+
private readonly options;
|
|
31
|
+
private readonly logger;
|
|
32
|
+
constructor(moduleRef: ModuleRef, options: OutboxMicroservicesOptions);
|
|
33
|
+
onApplicationBootstrap(): void;
|
|
34
|
+
externalize(event: unknown, metadata: ExternalizationMetadata): Promise<void>;
|
|
35
|
+
private resolveClient;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=microservices-event-externalizer.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
var MicroservicesEventExternalizer_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.MicroservicesEventExternalizer = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const core_1 = require("@nestjs/core");
|
|
19
|
+
const outbox_1 = require("@nestjs-transactional/outbox");
|
|
20
|
+
const rxjs_1 = require("rxjs");
|
|
21
|
+
const options_1 = require("../types/options");
|
|
22
|
+
/**
|
|
23
|
+
* {@link EventExternalizer} implementation backed by `@nestjs/microservices`
|
|
24
|
+
* `ClientProxy`. Routes externalized events to whichever transport
|
|
25
|
+
* the configured `ClientProxy` was registered with — Kafka, RabbitMQ,
|
|
26
|
+
* NATS, JMS, gRPC, or any custom transport.
|
|
27
|
+
*
|
|
28
|
+
* Per DD-017 this externalizer does NOT register clients itself;
|
|
29
|
+
* users register them via standard `ClientsModule.register()` /
|
|
30
|
+
* `ClientsModule.registerAsync()` and pass the token through
|
|
31
|
+
* `OutboxMicroservicesModule.forRoot({ defaultClient })`. Per-event
|
|
32
|
+
* overrides via `@Externalized({ client })` resolve through the same
|
|
33
|
+
* `ModuleRef.get(token, { strict: false })` lookup.
|
|
34
|
+
*
|
|
35
|
+
* **Headers / routingKey limitation (Phase 11.3):** the
|
|
36
|
+
* `@nestjs/microservices` `ClientProxy.emit` API has no unified
|
|
37
|
+
* headers / routing-key parameter — handling is transport-specific
|
|
38
|
+
* (Kafka headers, AMQP properties, NATS subject suffixes, ...). For
|
|
39
|
+
* the first version we log resolved headers and routing keys at debug
|
|
40
|
+
* level for visibility but do not apply them to the wire payload —
|
|
41
|
+
* users that need them can wrap the event in a transport-specific
|
|
42
|
+
* envelope inside their own code or wait for the broker-aware message
|
|
43
|
+
* construction iteration.
|
|
44
|
+
*/
|
|
45
|
+
let MicroservicesEventExternalizer = MicroservicesEventExternalizer_1 = class MicroservicesEventExternalizer {
|
|
46
|
+
moduleRef;
|
|
47
|
+
options;
|
|
48
|
+
logger = new common_1.Logger(MicroservicesEventExternalizer_1.name);
|
|
49
|
+
constructor(moduleRef, options) {
|
|
50
|
+
this.moduleRef = moduleRef;
|
|
51
|
+
this.options = options;
|
|
52
|
+
}
|
|
53
|
+
onApplicationBootstrap() {
|
|
54
|
+
if (this.options.validateOnBootstrap === false) {
|
|
55
|
+
this.logger.log('Bootstrap validation disabled — defaultClient will be resolved on first externalize() call');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (this.options.defaultClient === undefined) {
|
|
59
|
+
this.logger.log('No defaultClient configured — every @Externalized event must specify its own client');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
this.resolveClient(this.options.defaultClient);
|
|
64
|
+
this.logger.log(`Externalization configured with default client: ${formatToken(this.options.defaultClient)}`);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
68
|
+
throw new Error(`OutboxMicroservicesModule: defaultClient '${formatToken(this.options.defaultClient)}' ` +
|
|
69
|
+
`is not registered in the DI container. Register the ClientProxy via ` +
|
|
70
|
+
`ClientsModule.register() / ClientsModule.registerAsync(), or pass ` +
|
|
71
|
+
`validateOnBootstrap: false to defer resolution to the first event. ` +
|
|
72
|
+
`Original error: ${cause}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async externalize(event, metadata) {
|
|
76
|
+
const clientToken = metadata.client ?? this.options.defaultClient;
|
|
77
|
+
if (clientToken === undefined) {
|
|
78
|
+
throw new outbox_1.ExternalizationError(`No ClientProxy specified for event ${metadata.eventType}. ` +
|
|
79
|
+
`Set defaultClient on OutboxMicroservicesModule.forRoot() or pass ` +
|
|
80
|
+
`'client' in the @Externalized() decorator options.`, metadata.eventType, metadata.target);
|
|
81
|
+
}
|
|
82
|
+
let client;
|
|
83
|
+
try {
|
|
84
|
+
client = this.resolveClient(clientToken);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const cause = err instanceof Error ? err : undefined;
|
|
88
|
+
const causeMessage = err instanceof Error ? err.message : String(err);
|
|
89
|
+
throw new outbox_1.ExternalizationError(`ClientProxy '${formatToken(clientToken)}' not found in the DI container. ` +
|
|
90
|
+
`Ensure it is registered via ClientsModule.register() / registerAsync(). ` +
|
|
91
|
+
`Original error: ${causeMessage}`, metadata.eventType, metadata.target, cause);
|
|
92
|
+
}
|
|
93
|
+
if (metadata.headers !== undefined || metadata.routingKey !== undefined) {
|
|
94
|
+
// Phase 11.3 limitation — ClientProxy.emit has no unified
|
|
95
|
+
// headers / routing-key parameter. Logged for visibility; the
|
|
96
|
+
// broker-aware message construction iteration will route them
|
|
97
|
+
// through transport-specific envelopes.
|
|
98
|
+
this.logger.debug(`${metadata.eventType}: headers/routingKey are not applied to the wire payload in this version (Phase 11.3 limitation): ${JSON.stringify({ headers: metadata.headers, routingKey: metadata.routingKey })}`);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await (0, rxjs_1.firstValueFrom)(client.emit(metadata.target, event));
|
|
102
|
+
this.logger.debug(`Externalized ${metadata.eventType} → ${metadata.target} (client: ${formatToken(clientToken)})`);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const cause = err instanceof Error ? err : undefined;
|
|
106
|
+
const causeMessage = err instanceof Error ? err.message : String(err);
|
|
107
|
+
throw new outbox_1.ExternalizationError(`Failed to publish ${metadata.eventType} to ${metadata.target}: ${causeMessage}`, metadata.eventType, metadata.target, cause);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
resolveClient(token) {
|
|
111
|
+
const proxy = this.moduleRef.get(token, { strict: false });
|
|
112
|
+
if (proxy === null || proxy === undefined) {
|
|
113
|
+
throw new Error('Resolved provider is null/undefined');
|
|
114
|
+
}
|
|
115
|
+
return proxy;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.MicroservicesEventExternalizer = MicroservicesEventExternalizer;
|
|
119
|
+
exports.MicroservicesEventExternalizer = MicroservicesEventExternalizer = MicroservicesEventExternalizer_1 = __decorate([
|
|
120
|
+
(0, common_1.Injectable)(),
|
|
121
|
+
__param(1, (0, common_1.Inject)(options_1.OUTBOX_MICROSERVICES_OPTIONS)),
|
|
122
|
+
__metadata("design:paramtypes", [core_1.ModuleRef, Object])
|
|
123
|
+
], MicroservicesEventExternalizer);
|
|
124
|
+
function formatToken(token) {
|
|
125
|
+
if (typeof token === 'symbol') {
|
|
126
|
+
return token.toString();
|
|
127
|
+
}
|
|
128
|
+
if (typeof token === 'function') {
|
|
129
|
+
return token.name || '<anonymous function token>';
|
|
130
|
+
}
|
|
131
|
+
return String(token);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=microservices-event-externalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microservices-event-externalizer.js","sourceRoot":"","sources":["../../src/externalizer/microservices-event-externalizer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { MicroservicesEventExternalizer } from './externalizer/microservices-event-externalizer';
|
|
2
|
+
export { OutboxMicroservicesModule, type OutboxMicroservicesAsyncOptions, } from './module/outbox-microservices.module';
|
|
3
|
+
export { OUTBOX_MICROSERVICES_OPTIONS, type OutboxMicroservicesOptions, } from './types/options';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTBOX_MICROSERVICES_OPTIONS = exports.OutboxMicroservicesModule = exports.MicroservicesEventExternalizer = void 0;
|
|
4
|
+
var microservices_event_externalizer_1 = require("./externalizer/microservices-event-externalizer");
|
|
5
|
+
Object.defineProperty(exports, "MicroservicesEventExternalizer", { enumerable: true, get: function () { return microservices_event_externalizer_1.MicroservicesEventExternalizer; } });
|
|
6
|
+
var outbox_microservices_module_1 = require("./module/outbox-microservices.module");
|
|
7
|
+
Object.defineProperty(exports, "OutboxMicroservicesModule", { enumerable: true, get: function () { return outbox_microservices_module_1.OutboxMicroservicesModule; } });
|
|
8
|
+
var options_1 = require("./types/options");
|
|
9
|
+
Object.defineProperty(exports, "OUTBOX_MICROSERVICES_OPTIONS", { enumerable: true, get: function () { return options_1.OUTBOX_MICROSERVICES_OPTIONS; } });
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,oGAAiG;AAAxF,kJAAA,8BAA8B,OAAA;AACvC,oFAG8C;AAF5C,wIAAA,yBAAyB,OAAA;AAG3B,2CAGyB;AAFvB,uHAAA,4BAA4B,OAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type DynamicModule, type InjectionToken, type ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import { type OutboxMicroservicesOptions } from '../types/options';
|
|
3
|
+
/**
|
|
4
|
+
* Async-options shape for {@link OutboxMicroservicesModule.forRootAsync}.
|
|
5
|
+
* `imports` follows NestJS convention so the factory can pull values
|
|
6
|
+
* from a `ConfigModule` (or similar) without leaking the module
|
|
7
|
+
* reference into the factory signature.
|
|
8
|
+
*/
|
|
9
|
+
export interface OutboxMicroservicesAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
10
|
+
readonly useFactory: (...args: never[]) => Promise<OutboxMicroservicesOptions> | OutboxMicroservicesOptions;
|
|
11
|
+
readonly inject?: readonly InjectionToken[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* NestJS module that wires the
|
|
15
|
+
* {@link MicroservicesEventExternalizer} as the
|
|
16
|
+
* `EventExternalizer` for `outbox`. Reuses the user's existing
|
|
17
|
+
* `@nestjs/microservices` `ClientsModule` registration (DD-017) — the
|
|
18
|
+
* package does NOT register clients itself.
|
|
19
|
+
*
|
|
20
|
+
* Typical wiring:
|
|
21
|
+
* ```ts
|
|
22
|
+
* @Module({
|
|
23
|
+
* imports: [
|
|
24
|
+
* ClientsModule.register([
|
|
25
|
+
* { name: 'KAFKA_CLIENT', transport: Transport.KAFKA, options: { ... } },
|
|
26
|
+
* ]),
|
|
27
|
+
* OutboxModule.forRoot({}),
|
|
28
|
+
* OutboxModule.forFeature([OrderPlacedEvent]),
|
|
29
|
+
* OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' }),
|
|
30
|
+
* ],
|
|
31
|
+
* })
|
|
32
|
+
* export class AppModule {}
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The module binds {@link EVENT_EXTERNALIZER} via `useExisting` so
|
|
36
|
+
* `OutboxModule`'s `EventPublicationProcessor` picks the externalizer
|
|
37
|
+
* up through its `@Optional()` injection (DD-018). Both the SPI
|
|
38
|
+
* binding and the concrete class are exported so consumers can inject
|
|
39
|
+
* either.
|
|
40
|
+
*
|
|
41
|
+
* **Multi-dataSource setups**: a single externalizer covers every
|
|
42
|
+
* dataSource. Per-broker routing — when different events should land
|
|
43
|
+
* on different transports — happens via the per-event
|
|
44
|
+
* `@Externalized({ client })` parameter (Phase 11.3), not via a
|
|
45
|
+
* dataSource-keyed externalizer Map. See `outbox` README for the
|
|
46
|
+
* multi-`OutboxModule.forRoot()` pattern (ADR-019); each per-DS
|
|
47
|
+
* processor injects this same externalizer via `EVENT_EXTERNALIZER`.
|
|
48
|
+
*
|
|
49
|
+
* The module is registered as `@Global()` (since Phase 14.6) so the
|
|
50
|
+
* `EVENT_EXTERNALIZER` binding is visible to `OutboxModule`'s
|
|
51
|
+
* sibling-imported per-DS processors without an explicit import
|
|
52
|
+
* chain. Pre-Phase-14.6 the module was non-global, which silently
|
|
53
|
+
* broke the documented usage pattern in multi-module trees — fixed
|
|
54
|
+
* in Phase 14.6 verification work.
|
|
55
|
+
*/
|
|
56
|
+
export declare class OutboxMicroservicesModule {
|
|
57
|
+
static forRoot(options?: OutboxMicroservicesOptions): DynamicModule;
|
|
58
|
+
static forRootAsync(options: OutboxMicroservicesAsyncOptions): DynamicModule;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=outbox-microservices.module.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
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 OutboxMicroservicesModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.OutboxMicroservicesModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const outbox_1 = require("@nestjs-transactional/outbox");
|
|
13
|
+
const microservices_event_externalizer_1 = require("../externalizer/microservices-event-externalizer");
|
|
14
|
+
const options_1 = require("../types/options");
|
|
15
|
+
/**
|
|
16
|
+
* NestJS module that wires the
|
|
17
|
+
* {@link MicroservicesEventExternalizer} as the
|
|
18
|
+
* `EventExternalizer` for `outbox`. Reuses the user's existing
|
|
19
|
+
* `@nestjs/microservices` `ClientsModule` registration (DD-017) — the
|
|
20
|
+
* package does NOT register clients itself.
|
|
21
|
+
*
|
|
22
|
+
* Typical wiring:
|
|
23
|
+
* ```ts
|
|
24
|
+
* @Module({
|
|
25
|
+
* imports: [
|
|
26
|
+
* ClientsModule.register([
|
|
27
|
+
* { name: 'KAFKA_CLIENT', transport: Transport.KAFKA, options: { ... } },
|
|
28
|
+
* ]),
|
|
29
|
+
* OutboxModule.forRoot({}),
|
|
30
|
+
* OutboxModule.forFeature([OrderPlacedEvent]),
|
|
31
|
+
* OutboxMicroservicesModule.forRoot({ defaultClient: 'KAFKA_CLIENT' }),
|
|
32
|
+
* ],
|
|
33
|
+
* })
|
|
34
|
+
* export class AppModule {}
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* The module binds {@link EVENT_EXTERNALIZER} via `useExisting` so
|
|
38
|
+
* `OutboxModule`'s `EventPublicationProcessor` picks the externalizer
|
|
39
|
+
* up through its `@Optional()` injection (DD-018). Both the SPI
|
|
40
|
+
* binding and the concrete class are exported so consumers can inject
|
|
41
|
+
* either.
|
|
42
|
+
*
|
|
43
|
+
* **Multi-dataSource setups**: a single externalizer covers every
|
|
44
|
+
* dataSource. Per-broker routing — when different events should land
|
|
45
|
+
* on different transports — happens via the per-event
|
|
46
|
+
* `@Externalized({ client })` parameter (Phase 11.3), not via a
|
|
47
|
+
* dataSource-keyed externalizer Map. See `outbox` README for the
|
|
48
|
+
* multi-`OutboxModule.forRoot()` pattern (ADR-019); each per-DS
|
|
49
|
+
* processor injects this same externalizer via `EVENT_EXTERNALIZER`.
|
|
50
|
+
*
|
|
51
|
+
* The module is registered as `@Global()` (since Phase 14.6) so the
|
|
52
|
+
* `EVENT_EXTERNALIZER` binding is visible to `OutboxModule`'s
|
|
53
|
+
* sibling-imported per-DS processors without an explicit import
|
|
54
|
+
* chain. Pre-Phase-14.6 the module was non-global, which silently
|
|
55
|
+
* broke the documented usage pattern in multi-module trees — fixed
|
|
56
|
+
* in Phase 14.6 verification work.
|
|
57
|
+
*/
|
|
58
|
+
let OutboxMicroservicesModule = OutboxMicroservicesModule_1 = class OutboxMicroservicesModule {
|
|
59
|
+
static forRoot(options = {}) {
|
|
60
|
+
const providers = [
|
|
61
|
+
{ provide: options_1.OUTBOX_MICROSERVICES_OPTIONS, useValue: options },
|
|
62
|
+
microservices_event_externalizer_1.MicroservicesEventExternalizer,
|
|
63
|
+
{ provide: outbox_1.EVENT_EXTERNALIZER, useExisting: microservices_event_externalizer_1.MicroservicesEventExternalizer },
|
|
64
|
+
];
|
|
65
|
+
return {
|
|
66
|
+
module: OutboxMicroservicesModule_1,
|
|
67
|
+
global: true,
|
|
68
|
+
providers,
|
|
69
|
+
exports: [outbox_1.EVENT_EXTERNALIZER, microservices_event_externalizer_1.MicroservicesEventExternalizer],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
static forRootAsync(options) {
|
|
73
|
+
const providers = [
|
|
74
|
+
{
|
|
75
|
+
provide: options_1.OUTBOX_MICROSERVICES_OPTIONS,
|
|
76
|
+
useFactory: options.useFactory,
|
|
77
|
+
inject: options.inject ? [...options.inject] : undefined,
|
|
78
|
+
},
|
|
79
|
+
microservices_event_externalizer_1.MicroservicesEventExternalizer,
|
|
80
|
+
{ provide: outbox_1.EVENT_EXTERNALIZER, useExisting: microservices_event_externalizer_1.MicroservicesEventExternalizer },
|
|
81
|
+
];
|
|
82
|
+
return {
|
|
83
|
+
module: OutboxMicroservicesModule_1,
|
|
84
|
+
global: true,
|
|
85
|
+
imports: options.imports ?? [],
|
|
86
|
+
providers,
|
|
87
|
+
exports: [outbox_1.EVENT_EXTERNALIZER, microservices_event_externalizer_1.MicroservicesEventExternalizer],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
exports.OutboxMicroservicesModule = OutboxMicroservicesModule;
|
|
92
|
+
exports.OutboxMicroservicesModule = OutboxMicroservicesModule = OutboxMicroservicesModule_1 = __decorate([
|
|
93
|
+
(0, common_1.Module)({})
|
|
94
|
+
], OutboxMicroservicesModule);
|
|
95
|
+
//# sourceMappingURL=outbox-microservices.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbox-microservices.module.js","sourceRoot":"","sources":["../../src/module/outbox-microservices.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAMwB;AACxB,yDAAkE;AAElE,uGAAkG;AAClG,8CAG0B;AAe1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEI,IAAM,yBAAyB,iCAA/B,MAAM,yBAAyB;IACpC,MAAM,CAAC,OAAO,CAAC,UAAsC,EAAE;QACrD,MAAM,SAAS,GAAe;YAC5B,EAAE,OAAO,EAAE,sCAA4B,EAAE,QAAQ,EAAE,OAAO,EAAE;YAC5D,iEAA8B;YAC9B,EAAE,OAAO,EAAE,2BAAkB,EAAE,WAAW,EAAE,iEAA8B,EAAE;SAC7E,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,2BAAyB;YACjC,MAAM,EAAE,IAAI;YACZ,SAAS;YACT,OAAO,EAAE,CAAC,2BAAkB,EAAE,iEAA8B,CAAC;SAC9D,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,OAAwC;QAC1D,MAAM,SAAS,GAAe;YAC5B;gBACE,OAAO,EAAE,sCAA4B;gBACrC,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;aACzD;YACD,iEAA8B;YAC9B,EAAE,OAAO,EAAE,2BAAkB,EAAE,WAAW,EAAE,iEAA8B,EAAE;SAC7E,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,2BAAyB;YACjC,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;YAC9B,SAAS;YACT,OAAO,EAAE,CAAC,2BAAkB,EAAE,iEAA8B,CAAC;SAC9D,CAAC;IACJ,CAAC;CACF,CAAA;AAnCY,8DAAyB;oCAAzB,yBAAyB;IADrC,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,yBAAyB,CAmCrC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type InjectionToken } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration accepted by
|
|
4
|
+
* {@link OutboxMicroservicesModule.forRoot} (and the resolved factory
|
|
5
|
+
* result of `forRootAsync`). Per DD-017 the package does NOT register
|
|
6
|
+
* `ClientProxy` instances itself — register them via the standard
|
|
7
|
+
* `ClientsModule.register()` / `ClientsModule.registerAsync()` from
|
|
8
|
+
* `@nestjs/microservices` and pass the token here.
|
|
9
|
+
*/
|
|
10
|
+
export interface OutboxMicroservicesOptions {
|
|
11
|
+
/**
|
|
12
|
+
* DI token of the default `ClientProxy` used when an `@Externalized`
|
|
13
|
+
* mapping does not specify its own `client` override. Resolved via
|
|
14
|
+
* `ModuleRef.get(token, { strict: false })` so the proxy can live in
|
|
15
|
+
* any module (including a globally-imported `ClientsModule`).
|
|
16
|
+
*
|
|
17
|
+
* If omitted, every `@Externalized` event must carry a `client` of
|
|
18
|
+
* its own — otherwise the externalizer rejects the publication and
|
|
19
|
+
* the row is recorded as `FAILED` (DD-019).
|
|
20
|
+
*/
|
|
21
|
+
readonly defaultClient?: InjectionToken;
|
|
22
|
+
/**
|
|
23
|
+
* When `true` (default), the externalizer's `OnApplicationBootstrap`
|
|
24
|
+
* hook resolves `defaultClient` immediately and fails the bootstrap
|
|
25
|
+
* if the binding is missing — surfacing a misconfiguration before
|
|
26
|
+
* the first event is processed. Set `false` to defer resolution
|
|
27
|
+
* until the first call to `externalize()` (useful when the
|
|
28
|
+
* `ClientProxy` registration is itself wired by an asynchronous
|
|
29
|
+
* factory that has not finished by the time bootstrap runs).
|
|
30
|
+
*/
|
|
31
|
+
readonly validateOnBootstrap?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* DI token for the resolved {@link OutboxMicroservicesOptions} object.
|
|
35
|
+
* Internal — consumers configure the module via
|
|
36
|
+
* `OutboxMicroservicesModule.forRoot()` rather than injecting the
|
|
37
|
+
* token directly.
|
|
38
|
+
*/
|
|
39
|
+
export declare const OUTBOX_MICROSERVICES_OPTIONS: unique symbol;
|
|
40
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OUTBOX_MICROSERVICES_OPTIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* DI token for the resolved {@link OutboxMicroservicesOptions} object.
|
|
6
|
+
* Internal — consumers configure the module via
|
|
7
|
+
* `OutboxMicroservicesModule.forRoot()` rather than injecting the
|
|
8
|
+
* token directly.
|
|
9
|
+
*/
|
|
10
|
+
exports.OUTBOX_MICROSERVICES_OPTIONS = Symbol('OUTBOX_MICROSERVICES_OPTIONS');
|
|
11
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/types/options.ts"],"names":[],"mappings":";;;AAmCA;;;;;GAKG;AACU,QAAA,4BAA4B,GAAG,MAAM,CAAC,8BAA8B,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nestjs-transactional/outbox-microservices",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
|
+
"description": "Event externalization for @nestjs-transactional/outbox via @nestjs/microservices ClientProxy — Spring Modulith @Externalized parity",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Igor Golovanov",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/igorgolovanov/nestjs-transactional.git",
|
|
10
|
+
"directory": "packages/outbox-microservices"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/igorgolovanov/nestjs-transactional/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/igorgolovanov/nestjs-transactional/tree/main/packages/outbox-microservices#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nestjs",
|
|
18
|
+
"microservices",
|
|
19
|
+
"outbox",
|
|
20
|
+
"event-publication-registry",
|
|
21
|
+
"event-externalization",
|
|
22
|
+
"spring-modulith",
|
|
23
|
+
"kafka",
|
|
24
|
+
"rabbitmq",
|
|
25
|
+
"nats"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.11.0"
|
|
29
|
+
},
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/**/*.js",
|
|
34
|
+
"dist/**/*.d.ts",
|
|
35
|
+
"dist/**/*.js.map",
|
|
36
|
+
"!dist/**/*.spec.*"
|
|
37
|
+
],
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": true
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
50
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
51
|
+
"@nestjs/microservices": "^10.0.0 || ^11.0.0",
|
|
52
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0",
|
|
53
|
+
"rxjs": "^7.0.0",
|
|
54
|
+
"@nestjs-transactional/outbox": "^1.0.0-alpha.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@nestjs/common": "^11.0.0",
|
|
58
|
+
"@nestjs/core": "^11.0.0",
|
|
59
|
+
"@nestjs/microservices": "^11.0.0",
|
|
60
|
+
"@nestjs/testing": "^11.0.0",
|
|
61
|
+
"@types/jest": "^29.5.14",
|
|
62
|
+
"jest": "^29.7.0",
|
|
63
|
+
"reflect-metadata": "^0.2.2",
|
|
64
|
+
"rxjs": "^7.8.1",
|
|
65
|
+
"ts-jest": "^29.2.5",
|
|
66
|
+
"typescript": "^5.5.4",
|
|
67
|
+
"@nestjs-transactional/core": "1.0.0-alpha.0",
|
|
68
|
+
"@nestjs-transactional/outbox": "1.0.0-alpha.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsc -p tsconfig.build.json",
|
|
72
|
+
"clean": "rimraf dist *.tsbuildinfo coverage",
|
|
73
|
+
"test": "jest",
|
|
74
|
+
"test:watch": "jest --watch",
|
|
75
|
+
"test:cov": "jest --coverage",
|
|
76
|
+
"type-check": "tsc --noEmit",
|
|
77
|
+
"lint": "eslint \"src/**/*.ts\""
|
|
78
|
+
}
|
|
79
|
+
}
|