@cqrs-ddd/pipeline-deadletter 0.5.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 (56) hide show
  1. package/COMMERCIAL_LICENSE.txt +34 -0
  2. package/LICENSE +661 -0
  3. package/README.md +31 -0
  4. package/dist/dead-letter.behavior.d.ts +58 -0
  5. package/dist/dead-letter.behavior.d.ts.map +1 -0
  6. package/dist/dead-letter.behavior.js +173 -0
  7. package/dist/dead-letter.behavior.js.map +1 -0
  8. package/dist/dead-letter.redriver.d.ts +71 -0
  9. package/dist/dead-letter.redriver.d.ts.map +1 -0
  10. package/dist/dead-letter.redriver.js +81 -0
  11. package/dist/dead-letter.redriver.js.map +1 -0
  12. package/dist/errors/dead-letter-redrive.error.d.ts +13 -0
  13. package/dist/errors/dead-letter-redrive.error.d.ts.map +1 -0
  14. package/dist/errors/dead-letter-redrive.error.js +18 -0
  15. package/dist/errors/dead-letter-redrive.error.js.map +1 -0
  16. package/dist/helpers/build-attributes.d.ts +36 -0
  17. package/dist/helpers/build-attributes.d.ts.map +1 -0
  18. package/dist/helpers/build-attributes.js +42 -0
  19. package/dist/helpers/build-attributes.js.map +1 -0
  20. package/dist/helpers/build-record.d.ts +15 -0
  21. package/dist/helpers/build-record.d.ts.map +1 -0
  22. package/dist/helpers/build-record.js +63 -0
  23. package/dist/helpers/build-record.js.map +1 -0
  24. package/dist/helpers/dead-letter.intent.d.ts +18 -0
  25. package/dist/helpers/dead-letter.intent.d.ts.map +1 -0
  26. package/dist/helpers/dead-letter.intent.js +18 -0
  27. package/dist/helpers/dead-letter.intent.js.map +1 -0
  28. package/dist/helpers/redrive-scope.d.ts +9 -0
  29. package/dist/helpers/redrive-scope.d.ts.map +1 -0
  30. package/dist/helpers/redrive-scope.js +16 -0
  31. package/dist/helpers/redrive-scope.js.map +1 -0
  32. package/dist/index.d.ts +13 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +12 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces/dead-letter-options.interface.d.ts +70 -0
  37. package/dist/interfaces/dead-letter-options.interface.d.ts.map +1 -0
  38. package/dist/interfaces/dead-letter-options.interface.js +3 -0
  39. package/dist/interfaces/dead-letter-options.interface.js.map +1 -0
  40. package/dist/interfaces/dead-letter-transport.interface.d.ts +103 -0
  41. package/dist/interfaces/dead-letter-transport.interface.d.ts.map +1 -0
  42. package/dist/interfaces/dead-letter-transport.interface.js +3 -0
  43. package/dist/interfaces/dead-letter-transport.interface.js.map +1 -0
  44. package/dist/transports/bullmq.transport.d.ts +43 -0
  45. package/dist/transports/bullmq.transport.d.ts.map +1 -0
  46. package/dist/transports/bullmq.transport.js +36 -0
  47. package/dist/transports/bullmq.transport.js.map +1 -0
  48. package/dist/transports/postgres.transport.d.ts +51 -0
  49. package/dist/transports/postgres.transport.d.ts.map +1 -0
  50. package/dist/transports/postgres.transport.js +125 -0
  51. package/dist/transports/postgres.transport.js.map +1 -0
  52. package/dist/transports/rabbitmq.transport.d.ts +56 -0
  53. package/dist/transports/rabbitmq.transport.d.ts.map +1 -0
  54. package/dist/transports/rabbitmq.transport.js +82 -0
  55. package/dist/transports/rabbitmq.transport.js.map +1 -0
  56. package/package.json +71 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-record.js","sourceRoot":"","sources":["../../src/helpers/build-record.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { PipelineBehaviorTuple } from '@cqrs-ddd/pipeline';
2
+ import { DeadLetterBehavior } from '../dead-letter.behavior.js';
3
+ import type { DeadLetterBehaviorOptions } from '../interfaces/dead-letter-options.interface.js';
4
+ export type DeadLetterIntentOptions = DeadLetterBehaviorOptions;
5
+ /**
6
+ * Returns a dead-letter behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
7
+ * @param options Per-handler dead-letter options; omitted fields use behavior defaults.
8
+ * @returns The behavior class and options tuple.
9
+ * @example
10
+ * ```ts
11
+ * export const onUserCreated = pipeline.wrap(
12
+ * { name: 'onUserCreated', kind: 'event' },
13
+ * deadLetter({ redactKeys: ['refreshToken'], rethrow: false }),
14
+ * )(sendWelcomeEmail);
15
+ * ```
16
+ */
17
+ export declare function deadLetter(options?: DeadLetterIntentOptions): PipelineBehaviorTuple<DeadLetterBehavior, DeadLetterBehaviorOptions>;
18
+ //# sourceMappingURL=dead-letter.intent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.intent.d.ts","sourceRoot":"","sources":["../../src/helpers/dead-letter.intent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gDAAgD,CAAC;AAEhG,MAAM,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAEhE;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,OAAO,GAAE,uBAA4B,GACpC,qBAAqB,CAAC,kBAAkB,EAAE,yBAAyB,CAAC,CAEtE"}
@@ -0,0 +1,18 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { DeadLetterBehavior } from '../dead-letter.behavior.js';
3
+ /**
4
+ * Returns a dead-letter behavior entry for `pipeline.wrap()` or `createPipeline({ globalBehaviors })`.
5
+ * @param options Per-handler dead-letter options; omitted fields use behavior defaults.
6
+ * @returns The behavior class and options tuple.
7
+ * @example
8
+ * ```ts
9
+ * export const onUserCreated = pipeline.wrap(
10
+ * { name: 'onUserCreated', kind: 'event' },
11
+ * deadLetter({ redactKeys: ['refreshToken'], rethrow: false }),
12
+ * )(sendWelcomeEmail);
13
+ * ```
14
+ */
15
+ export function deadLetter(options = {}) {
16
+ return [DeadLetterBehavior, options];
17
+ }
18
+ //# sourceMappingURL=dead-letter.intent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter.intent.js","sourceRoot":"","sources":["../../src/helpers/dead-letter.intent.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAKhE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CACxB,OAAO,GAA4B,EAAE;IAErC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Runs `fn` as the redrive of dead letter `id`. Inside it, `DeadLetterBehavior`
3
+ * neither captures a failure again nor swallows it: the redriver records the
4
+ * attempt on the existing record instead.
5
+ */
6
+ export declare function runAsRedrive<T>(id: string, fn: () => T): T;
7
+ /** The id of the dead letter being redriven, if the current execution is a redrive. */
8
+ export declare function currentRedriveId(): string | undefined;
9
+ //# sourceMappingURL=redrive-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redrive-scope.d.ts","sourceRoot":"","sources":["../../src/helpers/redrive-scope.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE1D;AAED,uFAAuF;AACvF,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAErD"}
@@ -0,0 +1,16 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
+ const redriving = new AsyncLocalStorage();
4
+ /**
5
+ * Runs `fn` as the redrive of dead letter `id`. Inside it, `DeadLetterBehavior`
6
+ * neither captures a failure again nor swallows it: the redriver records the
7
+ * attempt on the existing record instead.
8
+ */
9
+ export function runAsRedrive(id, fn) {
10
+ return redriving.run(id, fn);
11
+ }
12
+ /** The id of the dead letter being redriven, if the current execution is a redrive. */
13
+ export function currentRedriveId() {
14
+ return redriving.getStore();
15
+ }
16
+ //# sourceMappingURL=redrive-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redrive-scope.js","sourceRoot":"","sources":["../../src/helpers/redrive-scope.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,SAAS,GAAG,IAAI,iBAAiB,EAAU,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAI,EAAU,EAAE,EAAW;IACrD,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,gBAAgB;IAC9B,OAAO,SAAS,CAAC,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,13 @@
1
+ export { DEFAULT_REDACT_KEYS, REDACTED, redactValue, } from '@cqrs-ddd/safe-stringify';
2
+ export { DEAD_LETTER_ITEM, DEAD_LETTER_ITEM_TOKEN, DeadLetterBehavior, } from './dead-letter.behavior.js';
3
+ export { type DeadLetterDispatch, type DeadLetterRedriveResult, DeadLetterRedriver, type DeadLetterRedriverOptions, } from './dead-letter.redriver.js';
4
+ export { DeadLetterRedriveError } from './errors/dead-letter-redrive.error.js';
5
+ export { buildDeadLetterAttributes } from './helpers/build-attributes.js';
6
+ export { buildDeadLetterRecord } from './helpers/build-record.js';
7
+ export { type DeadLetterIntentOptions, deadLetter, } from './helpers/dead-letter.intent.js';
8
+ export type { DeadLetterBehaviorOptions, DeadLetterMetadataFactory, } from './interfaces/dead-letter-options.interface.js';
9
+ export type { DeadLetterError, DeadLetterListFilter, DeadLetterRecord, DeadLetterRequestKind, DeadLetterStatus, DeadLetterStore, DeadLetterTransport, } from './interfaces/dead-letter-transport.interface.js';
10
+ export { BullMqDeadLetterTransport, type BullMqDeadLetterTransportOptions, type BullMqQueueLike, } from './transports/bullmq.transport.js';
11
+ export { createDeadLetterTableSql, PostgresDeadLetterTransport, type PostgresDeadLetterTransportOptions, type PostgresQueryableLike, } from './transports/postgres.transport.js';
12
+ export { type RabbitMqConfirmChannelLike, RabbitMqDeadLetterTransport, type RabbitMqDeadLetterTransportOptions, } from './transports/rabbitmq.transport.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,mBAAmB,EACnB,QAAQ,EACR,WAAW,GACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAC5B,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EACL,KAAK,uBAAuB,EAC5B,UAAU,GACX,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,yBAAyB,EACzB,yBAAyB,GAC1B,MAAM,+CAA+C,CAAC;AACvD,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EACL,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,wBAAwB,EACxB,2BAA2B,EAC3B,KAAK,kCAAkC,EACvC,KAAK,qBAAqB,GAC3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,KAAK,0BAA0B,EAC/B,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,oCAAoC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export { DEFAULT_REDACT_KEYS, REDACTED, redactValue, } from '@cqrs-ddd/safe-stringify';
3
+ export { DEAD_LETTER_ITEM, DEAD_LETTER_ITEM_TOKEN, DeadLetterBehavior, } from './dead-letter.behavior.js';
4
+ export { DeadLetterRedriver, } from './dead-letter.redriver.js';
5
+ export { DeadLetterRedriveError } from './errors/dead-letter-redrive.error.js';
6
+ export { buildDeadLetterAttributes } from './helpers/build-attributes.js';
7
+ export { buildDeadLetterRecord } from './helpers/build-record.js';
8
+ export { deadLetter, } from './helpers/dead-letter.intent.js';
9
+ export { BullMqDeadLetterTransport, } from './transports/bullmq.transport.js';
10
+ export { createDeadLetterTableSql, PostgresDeadLetterTransport, } from './transports/postgres.transport.js';
11
+ export { RabbitMqDeadLetterTransport, } from './transports/rabbitmq.transport.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EACL,mBAAmB,EACnB,QAAQ,EACR,WAAW,GACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAGL,kBAAkB,GAEnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAEL,UAAU,GACX,MAAM,iCAAiC,CAAC;AAczC,OAAO,EACL,yBAAyB,GAG1B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,wBAAwB,EACxB,2BAA2B,GAG5B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAEL,2BAA2B,GAE5B,MAAM,oCAAoC,CAAC"}
@@ -0,0 +1,70 @@
1
+ import type { Constructor, IPipelineContext } from '@cqrs-ddd/pipeline';
2
+ import type { DeadLetterRequestKind } from './dead-letter-transport.interface.js';
3
+ /**
4
+ * Factory producing extra, request-aware metadata to attach to a dead letter
5
+ * (e.g. tenant id, user id, attempt count read from `context.items`).
6
+ */
7
+ export type DeadLetterMetadataFactory = (context: IPipelineContext) => Record<string, unknown>;
8
+ /**
9
+ * Per-handler (and constructor-default) options for {@link DeadLetterBehavior}.
10
+ *
11
+ * Supplied per handler via
12
+ * `pipeline.wrap(options, [DeadLetterBehavior, { ... }])`, shallow-merged over
13
+ * the constructor defaults (handler keys win), except that `ignoreErrors` and
14
+ * `redactKeys` set at both levels are combined.
15
+ *
16
+ * @example Fire-and-forget event capture
17
+ * ```ts
18
+ * class UserCreatedHandler {
19
+ * @pipeline.wrap({ kind: 'event' }, [DeadLetterBehavior, {
20
+ * captureKinds: ['event'],
21
+ * rethrow: false,
22
+ * redactKeys: ['token'],
23
+ * }])
24
+ * async handle(event: UserCreatedEvent) {}
25
+ * }
26
+ * ```
27
+ */
28
+ export interface DeadLetterBehaviorOptions {
29
+ /**
30
+ * Whether to re-throw the original error after dead-lettering.
31
+ *
32
+ * - `true` (default) — propagate after the capture decision; the caller sees the
33
+ * failure (HTTP 5xx, command rejection, …). Use for commands/queries.
34
+ * - `false` — on an **event** handler only, swallow the error when it is
35
+ * captured and the transport delivers it; the pipeline then resolves to
36
+ * `undefined` and the swallow is logged at `error` level with the record id.
37
+ * On a command or query handler it is a contract diagnostic and is ignored.
38
+ */
39
+ rethrow?: boolean;
40
+ /**
41
+ * Include the error stack trace in the record. Default `true`.
42
+ * Set `false` to keep records lean or avoid leaking internals downstream.
43
+ */
44
+ includeStack?: boolean;
45
+ /**
46
+ * Request kinds to capture. Default `['event']`: a command's or query's
47
+ * caller already receives the error. Example: `['command', 'event']` to also
48
+ * keep failed commands.
49
+ */
50
+ captureKinds?: DeadLetterRequestKind[];
51
+ /**
52
+ * Filter predicate or error types to ignore.
53
+ * Matching errors are re-thrown without being sent to the dead-letter transport.
54
+ * Useful for ignoring expected client validation errors (e.g. ZodValidationError).
55
+ */
56
+ ignoreErrors?: Array<Constructor<unknown> | (abstract new (...args: never[]) => unknown)> | ((error: unknown, context: IPipelineContext) => boolean);
57
+ /** Produce extra metadata to merge into the dead-letter record. */
58
+ metadata?: DeadLetterMetadataFactory;
59
+ /**
60
+ * Custom redactor function for the captured request payload.
61
+ * When supplied, takes precedence over {@link redactKeys}.
62
+ */
63
+ redact?: (payload: unknown) => unknown;
64
+ /**
65
+ * Field names to mask with `[REDACTED]` in the captured request payload.
66
+ * Case-insensitive matching. Merged on top of {@link DEFAULT_REDACT_KEYS}.
67
+ */
68
+ redactKeys?: string[];
69
+ }
70
+ //# sourceMappingURL=dead-letter-options.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-options.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/dead-letter-options.interface.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAElF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,gBAAgB,KACtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAEvC;;;;OAIG;IACH,YAAY,CAAC,EACT,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,CAAC,GAC1E,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC;IAE7D,mEAAmE;IACnE,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IAErC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;IAEvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB"}
@@ -0,0 +1,3 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export {};
3
+ //# sourceMappingURL=dead-letter-options.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-options.interface.js","sourceRoot":"","sources":["../../src/interfaces/dead-letter-options.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
@@ -0,0 +1,103 @@
1
+ /** Request kinds as classified by the pipeline. */
2
+ export type DeadLetterRequestKind = 'command' | 'query' | 'event' | 'unknown';
3
+ /** Lifecycle of a dead letter. */
4
+ export type DeadLetterStatus = 'open' | 'resolved';
5
+ /** Serializable description of the failure that produced a dead letter. */
6
+ export interface DeadLetterError {
7
+ /** Error class name (e.g. `TimeoutError`), or `'unknown'` for non-Error throws. */
8
+ name: string;
9
+ /** Error message. */
10
+ message: string;
11
+ /** Stack trace, unless suppressed via `includeStack: false`. */
12
+ stack?: string;
13
+ }
14
+ /**
15
+ * A single dead-letter record for a pipeline request that failed (after any
16
+ * retries) and was forwarded to a
17
+ * {@link DeadLetterTransport} for inspection / replay.
18
+ *
19
+ * The shape is transport-neutral, but `payload` and `metadata` are supplied by
20
+ * the application and must satisfy the configured transport's serialization
21
+ * requirements.
22
+ */
23
+ export interface DeadLetterRecord {
24
+ /**
25
+ * Unique id of this dead letter (UUIDv7, so ids sort in capture order), used
26
+ * to redrive or resolve it.
27
+ */
28
+ id: string;
29
+ /** Correlation ID of the failed pipeline run (for cross-system tracing). */
30
+ correlationId: string;
31
+ /** Active tenant identifier if execution occurred within a multi-tenant context. */
32
+ tenantId?: string;
33
+ /** Whether the failed request was a command, query, event, or unknown. */
34
+ requestKind: DeadLetterRequestKind;
35
+ /** Request class name, e.g. `CreateUserCommand`. */
36
+ requestName: string;
37
+ /** Handler class name, e.g. `CreateUserHandler`. */
38
+ handlerName: string;
39
+ /** The original request payload (the command/query/event instance). */
40
+ payload: unknown;
41
+ /** Details of the error that caused the failure. */
42
+ error: DeadLetterError;
43
+ /** ISO-8601 timestamp of when the dead letter was produced. */
44
+ failedAt: string;
45
+ /** Metadata from the `metadata` factory, plus `tenantId` when present. */
46
+ metadata?: Record<string, unknown>;
47
+ /** Redrive attempts made so far; `0` when captured. */
48
+ attempts: number;
49
+ /** `'open'` until a redrive succeeds or it is resolved by hand. */
50
+ status: DeadLetterStatus;
51
+ /**
52
+ * `true` when redaction changed the payload (or a custom `redact` ran): the
53
+ * stored payload is not the original request, so it is not redriven as is.
54
+ */
55
+ payloadRedacted: boolean;
56
+ /** Error of the last failed redrive attempt, if any. */
57
+ lastError?: DeadLetterError;
58
+ /** ISO-8601 timestamp of when the record was resolved. */
59
+ resolvedAt?: string;
60
+ }
61
+ /**
62
+ * Transport-agnostic sink for dead letters.
63
+ *
64
+ * Implement this once per backend; the bundled implementations are
65
+ * {@link BullMqDeadLetterTransport}, {@link RabbitMqDeadLetterTransport},
66
+ * and {@link PostgresDeadLetterTransport}. Because the {@link DeadLetterBehavior}
67
+ * depends only on this interface, swapping the backend is a one-line change:
68
+ * the transport passed to its constructor.
69
+ */
70
+ export interface DeadLetterTransport {
71
+ /**
72
+ * Persist / publish a single dead-letter record.
73
+ *
74
+ * Implementations should be resilient and fast; a throw here is caught by the
75
+ * behavior and logged, but never masks the original handler error.
76
+ *
77
+ * @param record - The failed request snapshot to forward.
78
+ */
79
+ send(record: DeadLetterRecord): Promise<void>;
80
+ }
81
+ /** Filter for {@link DeadLetterStore.list}. */
82
+ export interface DeadLetterListFilter {
83
+ status?: DeadLetterStatus;
84
+ requestName?: string;
85
+ /** Most records to return, oldest first. Default `100`. */
86
+ limit?: number;
87
+ }
88
+ /**
89
+ * A transport that also keeps its records, so they can be listed, redriven
90
+ * with {@link DeadLetterRedriver}, and resolved. `PostgresDeadLetterTransport`
91
+ * implements it; queue transports rely on their broker's own dead-letter tooling.
92
+ */
93
+ export interface DeadLetterStore extends DeadLetterTransport {
94
+ /** The record with this id, or `undefined`. */
95
+ get(id: string): Promise<DeadLetterRecord | undefined>;
96
+ /** Records matching the filter, oldest first. */
97
+ list(filter?: DeadLetterListFilter): Promise<DeadLetterRecord[]>;
98
+ /** Counts one failed redrive attempt and keeps its error. */
99
+ recordAttempt(id: string, error: DeadLetterError): Promise<void>;
100
+ /** Closes the record. */
101
+ markResolved(id: string): Promise<void>;
102
+ }
103
+ //# sourceMappingURL=dead-letter-transport.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-transport.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/dead-letter-transport.interface.ts"],"names":[],"mappings":"AAEA,mDAAmD;AACnD,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9E,kCAAkC;AAClC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnD,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,WAAW,EAAE,qBAAqB,CAAC;IACnC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,KAAK,EAAE,eAAe,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;OAGG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,+CAA+C;AAC/C,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAgB,SAAQ,mBAAmB;IAC1D,+CAA+C;IAC/C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;IACvD,iDAAiD;IACjD,IAAI,CAAC,MAAM,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACjE,6DAA6D;IAC7D,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,yBAAyB;IACzB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC"}
@@ -0,0 +1,3 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ export {};
3
+ //# sourceMappingURL=dead-letter-transport.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-transport.interface.js","sourceRoot":"","sources":["../../src/interfaces/dead-letter-transport.interface.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
@@ -0,0 +1,43 @@
1
+ import type { DeadLetterRecord, DeadLetterTransport } from '../interfaces/dead-letter-transport.interface.js';
2
+ /**
3
+ * Minimal structural shape of a BullMQ `Queue`. Declared locally so this package
4
+ * does not hard-depend on `bullmq` — a real `Queue` satisfies it. Add the queue
5
+ * as a peer in your app: `pnpm add bullmq`.
6
+ */
7
+ export interface BullMqQueueLike {
8
+ add(name: string, data: unknown, opts?: unknown): Promise<unknown>;
9
+ }
10
+ /** Options for {@link BullMqDeadLetterTransport}. */
11
+ export interface BullMqDeadLetterTransportOptions {
12
+ /** Job name added to the queue for each dead letter. Default `'dead-letter'`. */
13
+ jobName?: string;
14
+ /**
15
+ * BullMQ `JobsOptions` for the dead-letter job. Defaults keep failures
16
+ * inspectable: `{ removeOnComplete: false, removeOnFail: false, attempts: 1 }`.
17
+ */
18
+ jobOptions?: unknown;
19
+ }
20
+ /**
21
+ * Bundled {@link DeadLetterTransport} backed by a **BullMQ** queue.
22
+ *
23
+ * Each dead letter is added as a job; a worker (or Bull Board) can then inspect,
24
+ * alert on, or replay it. The constructor takes any object matching
25
+ * {@link BullMqQueueLike}, so a real `bullmq` `Queue` works directly. Nothing
26
+ * selects this transport implicitly; pass an instance to the
27
+ * {@link DeadLetterBehavior} constructor.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * import { Queue } from 'bullmq';
32
+ * const queue = new Queue('dead-letters', { connection: { host, port } });
33
+ * const transport = new BullMqDeadLetterTransport(queue);
34
+ * ```
35
+ */
36
+ export declare class BullMqDeadLetterTransport implements DeadLetterTransport {
37
+ private readonly queue;
38
+ private readonly jobName;
39
+ private readonly jobOptions;
40
+ constructor(queue: BullMqQueueLike, options?: BullMqDeadLetterTransportOptions);
41
+ send(record: DeadLetterRecord): Promise<void>;
42
+ }
43
+ //# sourceMappingURL=bullmq.transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq.transport.d.ts","sourceRoot":"","sources":["../../src/transports/bullmq.transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,kDAAkD,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,qDAAqD;AACrD,MAAM,WAAW,gCAAgC;IAC/C,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAQD;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IAKjE,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IAErC,YACmB,KAAK,EAAE,eAAe,EACvC,OAAO,GAAE,gCAAqC,EAI/C;IAEK,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAElD;CACF"}
@@ -0,0 +1,36 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ const DEFAULT_JOB_OPTIONS = {
3
+ removeOnComplete: false,
4
+ removeOnFail: false,
5
+ attempts: 1,
6
+ };
7
+ /**
8
+ * Bundled {@link DeadLetterTransport} backed by a **BullMQ** queue.
9
+ *
10
+ * Each dead letter is added as a job; a worker (or Bull Board) can then inspect,
11
+ * alert on, or replay it. The constructor takes any object matching
12
+ * {@link BullMqQueueLike}, so a real `bullmq` `Queue` works directly. Nothing
13
+ * selects this transport implicitly; pass an instance to the
14
+ * {@link DeadLetterBehavior} constructor.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { Queue } from 'bullmq';
19
+ * const queue = new Queue('dead-letters', { connection: { host, port } });
20
+ * const transport = new BullMqDeadLetterTransport(queue);
21
+ * ```
22
+ */
23
+ export class BullMqDeadLetterTransport {
24
+ queue;
25
+ jobName;
26
+ jobOptions;
27
+ constructor(queue, options = {}) {
28
+ this.queue = queue;
29
+ this.jobName = options.jobName ?? 'dead-letter';
30
+ this.jobOptions = options.jobOptions ?? DEFAULT_JOB_OPTIONS;
31
+ }
32
+ async send(record) {
33
+ await this.queue.add(this.jobName, record, this.jobOptions);
34
+ }
35
+ }
36
+ //# sourceMappingURL=bullmq.transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq.transport.js","sourceRoot":"","sources":["../../src/transports/bullmq.transport.ts"],"names":[],"mappings":"AAAA,sEAAsE;AA2BtE,MAAM,mBAAmB,GAAG;IAC1B,gBAAgB,EAAE,KAAK;IACvB,YAAY,EAAE,KAAK;IACnB,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,yBAAyB;IAKjB,KAAK;IAJP,OAAO,CAAS;IAChB,UAAU,CAAU;IAErC,YACmB,KAAsB,EACvC,OAAO,GAAqC,EAAE;qBAD7B,KAAK;QAGtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC;QAChD,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAwB;QACjC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,51 @@
1
+ import type { DeadLetterError, DeadLetterListFilter, DeadLetterRecord, DeadLetterStore } from '../interfaces/dead-letter-transport.interface.js';
2
+ /**
3
+ * Minimal structural shape of a `pg` `Pool` / `Client`. Declared locally so this
4
+ * package does not hard-depend on `pg` — a real `Pool` or `Client` satisfies it.
5
+ * Add it in your app: `pnpm add pg`.
6
+ */
7
+ export interface PostgresQueryableLike {
8
+ query(text: string, values?: unknown[]): Promise<unknown>;
9
+ }
10
+ /** Options for {@link PostgresDeadLetterTransport}. */
11
+ export interface PostgresDeadLetterTransportOptions {
12
+ /**
13
+ * Destination table, optionally schema-qualified (e.g. `audit.dead_letters`).
14
+ * Default `'dead_letters'`. Validated as a safe SQL identifier.
15
+ */
16
+ table?: string;
17
+ }
18
+ /**
19
+ * SQL to create the dead-letter table. Run once in a migration.
20
+ *
21
+ * @param table - Table name (validated). Default `'dead_letters'`.
22
+ */
23
+ export declare function createDeadLetterTableSql(table?: string): string;
24
+ /**
25
+ * {@link DeadLetterStore} backed by **Postgres** (`pg`): inserts each dead
26
+ * letter as a row and keeps it, so it can be listed, redriven with
27
+ * `DeadLetterRedriver`, and resolved. Create the table once with
28
+ * {@link createDeadLetterTableSql}.
29
+ *
30
+ * The table name is validated as a plain SQL identifier (it is interpolated,
31
+ * not parameterized); all values are bound parameters.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * await pool.query(createDeadLetterTableSql());
36
+ * const store = new PostgresDeadLetterTransport(pool);
37
+ * const open = await store.list({ status: 'open' });
38
+ * ```
39
+ */
40
+ export declare class PostgresDeadLetterTransport implements DeadLetterStore {
41
+ private readonly db;
42
+ private readonly table;
43
+ constructor(db: PostgresQueryableLike, options?: PostgresDeadLetterTransportOptions);
44
+ send(record: DeadLetterRecord): Promise<void>;
45
+ get(id: string): Promise<DeadLetterRecord | undefined>;
46
+ list(filter?: DeadLetterListFilter): Promise<DeadLetterRecord[]>;
47
+ recordAttempt(id: string, error: DeadLetterError): Promise<void>;
48
+ markResolved(id: string): Promise<void>;
49
+ private rows;
50
+ }
51
+ //# sourceMappingURL=postgres.transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.transport.d.ts","sourceRoot":"","sources":["../../src/transports/postgres.transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EAChB,MAAM,kDAAkD,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3D;AAED,uDAAuD;AACvD,MAAM,WAAW,kCAAkC;IACjD;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAeD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,SAAiB,GAAG,MAAM,CAkBvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,2BAA4B,YAAW,eAAe;IAI/D,OAAO,CAAC,QAAQ,CAAC,EAAE;IAHrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B,YACmB,EAAE,EAAE,qBAAqB,EAC1C,OAAO,GAAE,kCAAuC,EAGjD;IAEK,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBlD;IAEK,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAK3D;IAEK,IAAI,CAAC,MAAM,GAAE,oBAAyB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CASzE;IAEK,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;IAEK,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK5C;YAEa,IAAI;CASnB"}
@@ -0,0 +1,125 @@
1
+ /* Copyright (C) 2026-present Aristotelis — see repository license. */
2
+ import { toPostgresJson } from '@cqrs-ddd/pipeline';
3
+ /** Allows `table` or `schema.table` made of unquoted SQL identifiers only. */
4
+ const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
5
+ function assertSafeTable(table) {
6
+ if (!SAFE_IDENTIFIER.test(table)) {
7
+ throw new Error(`Invalid dead-letter table name "${table}". ` +
8
+ 'Use an unquoted identifier like "dead_letters" or "schema.dead_letters".');
9
+ }
10
+ return table;
11
+ }
12
+ /**
13
+ * SQL to create the dead-letter table. Run once in a migration.
14
+ *
15
+ * @param table - Table name (validated). Default `'dead_letters'`.
16
+ */
17
+ export function createDeadLetterTableSql(table = 'dead_letters') {
18
+ const name = assertSafeTable(table);
19
+ return `CREATE TABLE IF NOT EXISTS ${name} (
20
+ id UUID PRIMARY KEY,
21
+ correlation_id TEXT NOT NULL,
22
+ request_kind TEXT NOT NULL,
23
+ request_name TEXT NOT NULL,
24
+ handler_name TEXT NOT NULL,
25
+ payload JSONB NOT NULL,
26
+ error JSONB NOT NULL,
27
+ metadata JSONB,
28
+ failed_at TIMESTAMPTZ NOT NULL,
29
+ attempts INTEGER NOT NULL DEFAULT 0,
30
+ status TEXT NOT NULL DEFAULT 'open',
31
+ payload_redacted BOOLEAN NOT NULL DEFAULT false,
32
+ last_error JSONB,
33
+ resolved_at TIMESTAMPTZ
34
+ );`;
35
+ }
36
+ /**
37
+ * {@link DeadLetterStore} backed by **Postgres** (`pg`): inserts each dead
38
+ * letter as a row and keeps it, so it can be listed, redriven with
39
+ * `DeadLetterRedriver`, and resolved. Create the table once with
40
+ * {@link createDeadLetterTableSql}.
41
+ *
42
+ * The table name is validated as a plain SQL identifier (it is interpolated,
43
+ * not parameterized); all values are bound parameters.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * await pool.query(createDeadLetterTableSql());
48
+ * const store = new PostgresDeadLetterTransport(pool);
49
+ * const open = await store.list({ status: 'open' });
50
+ * ```
51
+ */
52
+ export class PostgresDeadLetterTransport {
53
+ db;
54
+ table;
55
+ constructor(db, options = {}) {
56
+ this.db = db;
57
+ this.table = assertSafeTable(options.table ?? 'dead_letters');
58
+ }
59
+ async send(record) {
60
+ await this.db.query(`INSERT INTO ${this.table}
61
+ (id, correlation_id, request_kind, request_name, handler_name, payload,
62
+ error, metadata, failed_at, attempts, status, payload_redacted)
63
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, [
64
+ record.id,
65
+ record.correlationId,
66
+ record.requestKind,
67
+ record.requestName,
68
+ record.handlerName,
69
+ json(record.payload ?? null),
70
+ json(record.error),
71
+ record.metadata ? json(record.metadata) : null,
72
+ record.failedAt,
73
+ record.attempts,
74
+ record.status,
75
+ record.payloadRedacted,
76
+ ]);
77
+ }
78
+ async get(id) {
79
+ const rows = await this.rows(`SELECT * FROM ${this.table} WHERE id = $1`, [
80
+ id,
81
+ ]);
82
+ return rows[0];
83
+ }
84
+ async list(filter = {}) {
85
+ return this.rows(`SELECT * FROM ${this.table}
86
+ WHERE ($1::text IS NULL OR status = $1)
87
+ AND ($2::text IS NULL OR request_name = $2)
88
+ ORDER BY id
89
+ LIMIT $3`, [filter.status ?? null, filter.requestName ?? null, filter.limit ?? 100]);
90
+ }
91
+ async recordAttempt(id, error) {
92
+ await this.db.query(`UPDATE ${this.table} SET attempts = attempts + 1, last_error = $2 WHERE id = $1`, [id, json(error)]);
93
+ }
94
+ async markResolved(id) {
95
+ await this.db.query(`UPDATE ${this.table} SET status = 'resolved', resolved_at = now() WHERE id = $1`, [id]);
96
+ }
97
+ async rows(sql, values) {
98
+ const result = (await this.db.query(sql, values));
99
+ return (result.rows ?? []).map(toRecord);
100
+ }
101
+ }
102
+ function json(value) {
103
+ return toPostgresJson(JSON.stringify(value));
104
+ }
105
+ function toRecord(row) {
106
+ const tenantId = row.metadata?.tenantId;
107
+ return {
108
+ id: row.id,
109
+ correlationId: row.correlation_id,
110
+ ...(typeof tenantId === 'string' ? { tenantId } : {}),
111
+ requestKind: row.request_kind,
112
+ requestName: row.request_name,
113
+ handlerName: row.handler_name,
114
+ payload: row.payload,
115
+ error: row.error,
116
+ failedAt: row.failed_at.toISOString(),
117
+ ...(row.metadata ? { metadata: row.metadata } : {}),
118
+ attempts: row.attempts,
119
+ status: row.status,
120
+ payloadRedacted: row.payload_redacted,
121
+ ...(row.last_error ? { lastError: row.last_error } : {}),
122
+ ...(row.resolved_at ? { resolvedAt: row.resolved_at.toISOString() } : {}),
123
+ };
124
+ }
125
+ //# sourceMappingURL=postgres.transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.transport.js","sourceRoot":"","sources":["../../src/transports/postgres.transport.ts"],"names":[],"mappings":"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"}