@elsikora/nestjs-typeorm-aws-connector 1.4.1 → 2.0.0-dev.1

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 (84) hide show
  1. package/README.md +224 -140
  2. package/dist/cjs/index.d.ts +1 -0
  3. package/dist/cjs/index.js +15 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.d.ts +40 -9
  6. package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.js +498 -87
  7. package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.js.map +1 -1
  8. package/dist/cjs/modules/typeorm-aws-connector/rotator/type/index.d.ts +3 -0
  9. package/dist/cjs/modules/typeorm-aws-connector/rotator/type/mutable-driver.type.d.ts +5 -0
  10. package/dist/cjs/modules/typeorm-aws-connector/rotator/type/query-runner-mode.type.d.ts +2 -0
  11. package/dist/cjs/modules/typeorm-aws-connector/rotator/type/retiring-driver.type.d.ts +5 -0
  12. package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.d.ts +4 -4
  13. package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.js +47 -6
  14. package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.js.map +1 -1
  15. package/dist/cjs/shared/constant/typeorm-aws-connector/constant.d.ts +1 -1
  16. package/dist/cjs/shared/constant/typeorm-aws-connector/constant.js +7 -0
  17. package/dist/cjs/shared/constant/typeorm-aws-connector/constant.js.map +1 -1
  18. package/dist/cjs/shared/enum/index.d.ts +1 -0
  19. package/dist/cjs/shared/enum/typeorm-aws-connector/index.d.ts +1 -0
  20. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.d.ts +12 -0
  21. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js +16 -0
  22. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js.map +1 -0
  23. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.d.ts +6 -0
  24. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js +10 -0
  25. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js.map +1 -0
  26. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.d.ts +8 -0
  27. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.js +12 -0
  28. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.js.map +1 -0
  29. package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/index.d.ts +3 -0
  30. package/dist/cjs/shared/interface/typeorm-aws-connector/index.d.ts +1 -0
  31. package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/deferred-event.interface.d.ts +9 -0
  32. package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/drain-timeout-event.interface.d.ts +10 -0
  33. package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/generation-retired-event.interface.d.ts +8 -0
  34. package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/index.d.ts +3 -0
  35. package/dist/cjs/shared/interface/typeorm-aws-connector/ssm-lookups.interface.d.ts +1 -0
  36. package/dist/cjs/shared/provider/typeorm-aws-connector/database.provider.js.map +1 -1
  37. package/dist/cjs/shared/type/database/config-rotation.type.d.ts +7 -0
  38. package/dist/cjs/shared/type/typeorm-aws-connector/index.d.ts +1 -0
  39. package/dist/cjs/shared/type/typeorm-aws-connector/rotation/event-listener.type.d.ts +6 -0
  40. package/dist/cjs/shared/type/typeorm-aws-connector/rotation/event.type.d.ts +2 -0
  41. package/dist/cjs/shared/type/typeorm-aws-connector/rotation/index.d.ts +3 -0
  42. package/dist/cjs/shared/type/typeorm-aws-connector/rotation/resolved-config.type.d.ts +11 -0
  43. package/dist/esm/index.d.ts +1 -0
  44. package/dist/esm/index.js +3 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.d.ts +40 -9
  47. package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.js +498 -87
  48. package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.js.map +1 -1
  49. package/dist/esm/modules/typeorm-aws-connector/rotator/type/index.d.ts +3 -0
  50. package/dist/esm/modules/typeorm-aws-connector/rotator/type/mutable-driver.type.d.ts +5 -0
  51. package/dist/esm/modules/typeorm-aws-connector/rotator/type/query-runner-mode.type.d.ts +2 -0
  52. package/dist/esm/modules/typeorm-aws-connector/rotator/type/retiring-driver.type.d.ts +5 -0
  53. package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.d.ts +4 -4
  54. package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.js +47 -6
  55. package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.js.map +1 -1
  56. package/dist/esm/shared/constant/typeorm-aws-connector/constant.d.ts +1 -1
  57. package/dist/esm/shared/constant/typeorm-aws-connector/constant.js +7 -0
  58. package/dist/esm/shared/constant/typeorm-aws-connector/constant.js.map +1 -1
  59. package/dist/esm/shared/enum/index.d.ts +1 -0
  60. package/dist/esm/shared/enum/typeorm-aws-connector/index.d.ts +1 -0
  61. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.d.ts +12 -0
  62. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js +16 -0
  63. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js.map +1 -0
  64. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.d.ts +6 -0
  65. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js +10 -0
  66. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js.map +1 -0
  67. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.d.ts +8 -0
  68. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.js +12 -0
  69. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.js.map +1 -0
  70. package/dist/esm/shared/enum/typeorm-aws-connector/rotation/index.d.ts +3 -0
  71. package/dist/esm/shared/interface/typeorm-aws-connector/index.d.ts +1 -0
  72. package/dist/esm/shared/interface/typeorm-aws-connector/rotation/deferred-event.interface.d.ts +9 -0
  73. package/dist/esm/shared/interface/typeorm-aws-connector/rotation/drain-timeout-event.interface.d.ts +10 -0
  74. package/dist/esm/shared/interface/typeorm-aws-connector/rotation/generation-retired-event.interface.d.ts +8 -0
  75. package/dist/esm/shared/interface/typeorm-aws-connector/rotation/index.d.ts +3 -0
  76. package/dist/esm/shared/interface/typeorm-aws-connector/ssm-lookups.interface.d.ts +1 -0
  77. package/dist/esm/shared/provider/typeorm-aws-connector/database.provider.js.map +1 -1
  78. package/dist/esm/shared/type/database/config-rotation.type.d.ts +7 -0
  79. package/dist/esm/shared/type/typeorm-aws-connector/index.d.ts +1 -0
  80. package/dist/esm/shared/type/typeorm-aws-connector/rotation/event-listener.type.d.ts +6 -0
  81. package/dist/esm/shared/type/typeorm-aws-connector/rotation/event.type.d.ts +2 -0
  82. package/dist/esm/shared/type/typeorm-aws-connector/rotation/index.d.ts +3 -0
  83. package/dist/esm/shared/type/typeorm-aws-connector/rotation/resolved-config.type.d.ts +11 -0
  84. package/package.json +13 -7
package/README.md CHANGED
@@ -16,11 +16,13 @@
16
16
  ## 💡 Highlights
17
17
 
18
18
  - 🔐 Zero-credential codebase — resolves all database config from AWS SSM Parameter Store & Secrets Manager at runtime
19
- - ♻️ Built-in automatic credential rotation with health checks, staged driver promotion, and graceful old-pool drainage
19
+ - ♻️ Bounded automatic credential rotation with health checks, transaction affinity, and at most one retiring pool
20
+ - ⏱️ Bounded shutdown with explicit drain timeout, aggregate failure, and package-neutral lifecycle events
20
21
  - 🏗️ Split-source architecture — infra teams own host/port/secrets, app teams own pool/timeout/sync settings independently
21
22
  - 📦 Dual ESM/CJS module output with full TypeScript declarations and broad NestJS compatibility (v8–v11)
22
23
 
23
24
  ## 📚 Table of Contents
25
+
24
26
  - [Description](#-description)
25
27
  - [Tech Stack](#-tech-stack)
26
28
  - [Features](#-features)
@@ -29,15 +31,17 @@
29
31
  - [Prerequisites](#-prerequisites)
30
32
  - [Installation](#-installation)
31
33
  - [Usage](#-usage)
34
+ - [Migrating to 2.0](#migrating-from-1x)
32
35
  - [Roadmap](#-roadmap)
33
36
  - [FAQ](#-faq)
34
37
  - [License](#-license)
35
38
  - [Acknowledgments](#-acknowledgments)
36
39
 
37
40
  ## 📖 Description
41
+
38
42
  **NestJS-TypeORM-AWS-Connector** is a production-grade NestJS module that eliminates the pain of managing database configuration in AWS-hosted applications. Instead of hardcoding credentials or juggling environment variables, this connector resolves your entire TypeORM `DataSourceOptions` from **AWS Systems Manager Parameter Store** and **AWS Secrets Manager** — with built-in credential rotation.
39
43
 
40
- In modern cloud-native architectures, database credentials are rotated regularly for security compliance. Traditional approaches break connections during rotation, causing downtime. This connector avoids destructive mid-run `DataSource` teardown by validating a replacement connection first, promoting the new driver for future query runners, and retiring the previous pool only after existing query runners drain.
44
+ In modern cloud-native architectures, database credentials are rotated regularly for security compliance. Traditional approaches break connections during rotation, causing downtime. This connector avoids destructive mid-run `DataSource` teardown by validating a replacement connection first, promoting the new driver for future query runners, and retiring the previous pool only after existing query runners drain. It permits at most one current and one retiring generation; later intervals defer instead of growing the pool count.
41
45
 
42
46
  ### Real-World Use Cases
43
47
 
@@ -50,22 +54,26 @@ The connector uses a **split-source model**: infrastructure-owned parameters (ho
50
54
 
51
55
  ## 🛠️ Tech Stack
52
56
 
53
- | Category | Technologies |
54
- |----------|-------------|
55
- | **Language** | TypeScript |
56
- | **Runtime** | Node.js |
57
- | **Framework** | NestJS |
58
- | **ORM** | TypeORM |
59
- | **Cloud Services** | AWS SSM Parameter Store, AWS Secrets Manager |
60
- | **Build Tool** | Rollup |
61
- | **Linting** | ESLint, Prettier |
62
- | **CI/CD** | GitHub Actions, Semantic Release |
63
- | **Package Manager** | npm |
57
+ | Category | Technologies |
58
+ | ------------------- | -------------------------------------------- |
59
+ | **Language** | TypeScript |
60
+ | **Runtime** | Node.js |
61
+ | **Framework** | NestJS |
62
+ | **ORM** | TypeORM |
63
+ | **Cloud Services** | AWS SSM Parameter Store, AWS Secrets Manager |
64
+ | **Build Tool** | Rollup |
65
+ | **Linting** | ESLint, Prettier |
66
+ | **CI/CD** | GitHub Actions, Semantic Release |
67
+ | **Package Manager** | npm |
64
68
 
65
69
  ## 🚀 Features
70
+
66
71
  - ✨ ****AWS Parameter Store Integration** — Resolves database host, port, name, type, and tuning parameters from structured SSM paths with hierarchical namespace support**
67
72
  - ✨ ****AWS Secrets Manager Integration** — Fetches username/password credentials from Secrets Manager with proper error handling for missing or malformed secrets**
68
- - ✨ ****Automatic Credential Rotation** — Configurable interval-based rotation that validates a replacement connection before promoting it for future query runners**
73
+ - ✨ ****Bounded Credential Rotation** — Validates a replacement before promotion and permits only the current pool plus one retiring pool**
74
+ - ✨ ****Transaction Affinity** — Query runners and manager-bound transactions stay on the generation where they started**
75
+ - ✨ ****Bounded Shutdown** — Stops new work, waits for active generations, then reports timeout or close failures as one aggregate error**
76
+ - ✨ ****Rotation Events** — Reports deferred rotation, retired generations, and drain timeouts without a Nest EventEmitter dependency**
69
77
  - ✨ ****Emergency Recovery** — After 3 consecutive rotation failures, automatically retries recovery without destructively tearing down the live `DataSource`**
70
78
  - ✨ ****Connection Health Validation** — Validates both existing and new connections with `SELECT 1` queries before and after rotation**
71
79
  - ✨ ****Split-Source Configuration Model** — Infrastructure-owned settings (host, port, secret-id) use canonical AWS namespaces; app-owned settings use your module namespace**
@@ -140,11 +148,16 @@ sequenceDiagram
140
148
  NestJS-TypeORM-AWS-Connector/
141
149
  ├── .github/
142
150
  │ ├── workflows/
151
+ │ │ ├── mirror-docs-to-docviewer.yml
143
152
  │ │ ├── mirror-to-codecommit.yml
144
153
  │ │ ├── qodana-quality-scan.yml
145
154
  │ │ ├── release.yml
146
155
  │ │ └── snyk-security-scan.yml
147
156
  │ └── dependabot.yml
157
+ ├── docs/
158
+ │ ├── api-reference/
159
+ │ ├── getting-started/
160
+ │ └── guides/
148
161
  ├── src/
149
162
  │ ├── modules/
150
163
  │ │ └── typeorm-aws-connector/
@@ -175,33 +188,33 @@ NestJS-TypeORM-AWS-Connector/
175
188
 
176
189
  ## 📋 Prerequisites
177
190
 
178
- - Node.js >= 18.0.0
191
+ - Node.js >= 20.0.0
179
192
  - npm >= 9.0.0
180
193
  - @nestjs/common ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
181
194
  - @aws-sdk/client-ssm ^3.535.0
182
195
  - typeorm ^0.3.20
183
- - @elsikora/nestjs-aws-parameter-store-config ^2.0.1 (installed automatically)
196
+ - @elsikora/nestjs-aws-parameter-store-config ^2.0.2 (installed automatically)
184
197
  - AWS credentials configured (IAM role, environment variables, or AWS CLI profile)
185
198
 
186
199
  ## 🛠 Installation
200
+
201
+ These source docs describe the Connector 2 contract. Use a 2.x-only range so installation cannot silently fall back to Connector 1.x. Before the first 2.0 prerelease is published, this command intentionally fails.
202
+
187
203
  ```bash
188
204
  # Install the connector and its required peer dependencies
189
- npm install @elsikora/nestjs-typeorm-aws-connector @aws-sdk/client-ssm @nestjs/common typeorm
190
-
191
- # The following are installed automatically as dependencies:
192
- # @aws-sdk/client-secrets-manager
193
- # @elsikora/nestjs-aws-parameter-store-config
194
- # @nestjs/config
195
- # @nestjs/schedule
205
+ npm install @elsikora/nestjs-typeorm-aws-connector@^2.0.0-0 @aws-sdk/client-ssm @nestjs/common typeorm
206
+ ```
196
207
 
208
+ The connector installs `@aws-sdk/client-secrets-manager`, `@elsikora/nestjs-aws-parameter-store-config`, `@nestjs/config`, and `@nestjs/schedule` as direct dependencies.
197
209
 
198
210
  ### Verify Installation
199
211
 
200
-
212
+ ```bash
201
213
  npm ls @elsikora/nestjs-typeorm-aws-connector
202
214
  ```
203
215
 
204
216
  ## 💡 Usage
217
+
205
218
  ### Prerequisites: Register Parameter Store Config Module
206
219
 
207
220
  Before using the connector, register `ParameterStoreConfigModule` from `@elsikora/nestjs-aws-parameter-store-config`:
@@ -212,21 +225,21 @@ import { ConfigModule, ConfigService } from "@nestjs/config";
212
225
  import { ENamespace, ParameterStoreConfigModule } from "@elsikora/nestjs-aws-parameter-store-config";
213
226
 
214
227
  @Module({
215
- imports: [
216
- ConfigModule.forRoot(),
217
- ParameterStoreConfigModule.registerAsync({
218
- imports: [ConfigModule],
219
- inject: [ConfigService],
220
- useFactory: (configService: ConfigService) => ({
221
- application: configService.getOrThrow<string>("APPLICATION"),
222
- config: { region: configService.getOrThrow<string>("AWS_REGION") },
223
- environment: configService.getOrThrow<string>("ENVIRONMENT"),
224
- instanceName: "my-api",
225
- namespace: ENamespace.AWS_ECS_FARGATE,
226
- shouldDecryptParameters: true,
227
- }),
228
- }),
229
- ],
228
+ imports: [
229
+ ConfigModule.forRoot(),
230
+ ParameterStoreConfigModule.registerAsync({
231
+ imports: [ConfigModule],
232
+ inject: [ConfigService],
233
+ useFactory: (configService: ConfigService) => ({
234
+ application: configService.getOrThrow<string>("APPLICATION"),
235
+ config: { region: configService.getOrThrow<string>("AWS_REGION") },
236
+ environment: configService.getOrThrow<string>("ENVIRONMENT"),
237
+ instanceName: "my-api",
238
+ namespace: ENamespace.AWS_ECS_FARGATE,
239
+ shouldDecryptParameters: true,
240
+ }),
241
+ }),
242
+ ],
230
243
  })
231
244
  export class AppModule {}
232
245
  ```
@@ -238,26 +251,22 @@ export class AppModule {}
238
251
  ```typescript
239
252
  import { Module } from "@nestjs/common";
240
253
  import { TypeOrmModule } from "@nestjs/typeorm";
241
- import {
242
- TypeOrmAwsConnectorModule,
243
- TypeOrmAwsConnectorService,
244
- } from "@elsikora/nestjs-typeorm-aws-connector";
254
+ import { TypeOrmAwsConnectorModule, TypeOrmAwsConnectorService } from "@elsikora/nestjs-typeorm-aws-connector";
245
255
  import { UserEntity } from "./entities/user.entity";
246
256
 
247
257
  @Module({
248
- imports: [
249
- // Register the connector with your entities
250
- TypeOrmAwsConnectorModule.register({
251
- entities: [UserEntity],
252
- }),
253
- // Wire it into TypeOrmModule
254
- TypeOrmModule.forRootAsync({
255
- imports: [TypeOrmAwsConnectorModule],
256
- inject: [TypeOrmAwsConnectorService],
257
- useFactory: async (connector: TypeOrmAwsConnectorService) =>
258
- connector.getTypeOrmOptions(),
259
- }),
260
- ],
258
+ imports: [
259
+ // Register the connector with your entities
260
+ TypeOrmAwsConnectorModule.register({
261
+ entities: [UserEntity],
262
+ }),
263
+ // Wire it into TypeOrmModule
264
+ TypeOrmModule.forRootAsync({
265
+ imports: [TypeOrmAwsConnectorModule],
266
+ inject: [TypeOrmAwsConnectorService],
267
+ useFactory: async (connector: TypeOrmAwsConnectorService) => connector.getTypeOrmOptions(),
268
+ }),
269
+ ],
261
270
  })
262
271
  export class DatabaseModule {}
263
272
  ```
@@ -273,28 +282,31 @@ import { ENamespace } from "@elsikora/nestjs-aws-parameter-store-config";
273
282
  import { TypeOrmAwsConnectorModule } from "@elsikora/nestjs-typeorm-aws-connector";
274
283
 
275
284
  TypeOrmAwsConnectorModule.register({
276
- entities: [UserEntity],
277
- ssmLookupDefaults: {
278
- instanceName: "my-api",
279
- namespace: ENamespace.AWS_ECS_FARGATE,
280
- },
281
- ssmLookups: {
282
- secretId: {
283
- instanceName: "database",
284
- namespace: ENamespace.AWS_SECRETS_MANAGER,
285
- path: ["secret-id"],
286
- },
287
- host: {
288
- instanceName: "aurora-postgres",
289
- namespace: ENamespace.AWS_RDS,
290
- path: ["host"],
291
- },
292
- port: {
293
- instanceName: "aurora-postgres",
294
- namespace: ENamespace.AWS_RDS,
295
- path: ["port"],
296
- },
297
- },
285
+ entities: [UserEntity],
286
+ ssmLookupDefaults: {
287
+ instanceName: "my-api",
288
+ namespace: ENamespace.AWS_ECS_FARGATE,
289
+ },
290
+ ssmLookups: {
291
+ secretId: {
292
+ instanceName: "database",
293
+ namespace: ENamespace.AWS_SECRETS_MANAGER,
294
+ path: ["secret-id"],
295
+ },
296
+ host: {
297
+ instanceName: "aurora-postgres",
298
+ namespace: ENamespace.AWS_RDS,
299
+ path: ["host"],
300
+ },
301
+ port: {
302
+ instanceName: "aurora-postgres",
303
+ namespace: ENamespace.AWS_RDS,
304
+ path: ["port"],
305
+ },
306
+ rotationShutdownDrainTimeoutMs: {
307
+ path: ["database", "rotation", "shutdown-drain-timeout-ms"],
308
+ },
309
+ },
298
310
  });
299
311
  ```
300
312
 
@@ -308,13 +320,13 @@ Bypass AWS entirely for local development:
308
320
  import { EDatabaseType } from "@elsikora/nestjs-typeorm-aws-connector";
309
321
 
310
322
  TypeOrmAwsConnectorModule.register({
311
- entities: [UserEntity],
312
- host: "127.0.0.1",
313
- port: 5432,
314
- username: "local-user",
315
- password: "local-password",
316
- databaseName: "mydb",
317
- type: EDatabaseType.POSTGRES,
323
+ entities: [UserEntity],
324
+ host: "127.0.0.1",
325
+ port: 5432,
326
+ username: "local-user",
327
+ password: "local-password",
328
+ databaseName: "mydb",
329
+ type: EDatabaseType.POSTGRES,
318
330
  });
319
331
  ```
320
332
 
@@ -325,25 +337,76 @@ TypeOrmAwsConnectorModule.register({
325
337
  Enable automatic credential rotation for long-running services:
326
338
 
327
339
  ```typescript
340
+ import { Logger } from "@nestjs/common";
341
+ import { ETypeOrmAwsConnectorRotationEvent, TypeOrmAwsConnectorModule } from "@elsikora/nestjs-typeorm-aws-connector";
342
+
343
+ const rotationLogger = new Logger("DatabaseRotation");
344
+
328
345
  TypeOrmAwsConnectorModule.register({
329
- entities: [UserEntity],
330
- rotation: {
331
- isEnabled: true,
332
- intervalMs: 3_600_000, // Rotate every hour
333
- },
346
+ entities: [UserEntity],
347
+ rotation: {
348
+ isEnabled: true,
349
+ intervalMs: 3_600_000, // Rotate every hour
350
+ shutdownDrainTimeoutMs: 15_000,
351
+ onEvent: (event) => {
352
+ switch (event.type) {
353
+ case ETypeOrmAwsConnectorRotationEvent.DRAIN_TIMEOUT: {
354
+ rotationLogger.error("Database rotation drain timed out", event);
355
+ break;
356
+ }
357
+ case ETypeOrmAwsConnectorRotationEvent.GENERATION_RETIRED: {
358
+ rotationLogger.log("Database generation retired", event);
359
+ break;
360
+ }
361
+ case ETypeOrmAwsConnectorRotationEvent.ROTATION_DEFERRED: {
362
+ rotationLogger.warn("Database rotation deferred", event);
363
+ break;
364
+ }
365
+ }
366
+ },
367
+ },
334
368
  });
335
369
  ```
336
370
 
371
+ When rotation is enabled, `shutdownDrainTimeoutMs` is required and must be a positive integer. Provide it directly or through the canonical SSM path `typeorm/rotation/shutdown-drain-timeout-ms`; the connector does not invent a shutdown default.
372
+
337
373
  The rotation service will:
374
+
338
375
  1. Validate current connection health
339
376
  2. Fetch fresh credentials from AWS Secrets Manager
340
- 3. Create and initialize a replacement `DataSource` with the updated credentials
377
+ 3. Create a replacement `DataSource` with schema synchronization, migrations, schema drop, and extension installation disabled
341
378
  4. Verify the replacement connection with a `SELECT 1` query
342
379
  5. Promote the replacement driver for future query runners without destroying the live `DataSource`
343
- 6. Dispose the previous pool only after query runners created before the promotion are released
344
- 7. Attempt emergency recovery after 3 consecutive failures
380
+ 6. Keep query runners and manager-bound transactions on the generation where they started
381
+ 7. Defer later intervals until the one retiring generation drains and closes
382
+ 8. Attempt emergency recovery through the same bounded rotation gate after 3 consecutive failures
383
+
384
+ At most two database pools exist during rotation: the current generation and one retiring generation. Overlapping intervals coalesce instead of creating queued replacements.
385
+
386
+ `onEvent` is an optional, package-neutral observability callback. It can return `void` or `Promise<void>`; its returned promise is not awaited, and its synchronous body should stay small. It must not own correctness or database writes. Listener failures are logged and isolated from the rotation lifecycle. Event objects are frozen and contain no credentials.
387
+
388
+ | Event | Meaning |
389
+ | -------------------- | ----------------------------------------------------------------------------------------- |
390
+ | `ROTATION_DEFERRED` | Rotation was skipped because another rotation, retirement, or shutdown prevents promotion |
391
+ | `GENERATION_RETIRED` | A current or retiring generation closed successfully |
392
+ | `DRAIN_TIMEOUT` | Active work did not drain, or generation close did not settle, in time |
393
+
394
+ Rotation is intended for long-lived services. For one-shot jobs, migrations, Bootstrap, or explicit CLI tasks, keep `rotation.isEnabled` disabled so the connector does not register a background interval.
345
395
 
346
- Rotation is intended for long-lived services. For one-shot jobs, migrations, or explicit CLI tasks, keep `rotation.isEnabled` disabled so the connector does not register a background interval.
396
+ ### Shutdown Lifecycle
397
+
398
+ Enable Nest shutdown hooks in long-running applications so `SIGTERM` reaches the connector:
399
+
400
+ ```typescript
401
+ const app = await NestFactory.create(AppModule);
402
+
403
+ app.enableShutdownHooks();
404
+ await app.listen(3000);
405
+ ```
406
+
407
+ During shutdown, the connector removes its interval, refuses new query runners, waits up to `shutdownDrainTimeoutMs` for active work, and then closes the current, pending, and retiring generations. If drain or close does not settle, it emits `DRAIN_TIMEOUT`, performs best-effort cleanup, and rejects with one `AggregateError`; callers and orchestrators must treat that shutdown as unclean.
408
+
409
+ Drain and generation close each have a bounded phase, so the host termination budget must exceed approximately `2 × shutdownDrainTimeoutMs` plus the surrounding Nest shutdown work and an operational margin.
347
410
 
348
411
  ---
349
412
 
@@ -351,18 +414,29 @@ Rotation is intended for long-lived services. For one-shot jobs, migrations, or
351
414
 
352
415
  ```typescript
353
416
  TypeOrmAwsConnectorModule.registerAsync({
354
- imports: [ConfigModule],
355
- inject: [ConfigService],
356
- useFactory: (configService: ConfigService) => ({
357
- entities: [UserEntity],
358
- rotation: {
359
- isEnabled: configService.get<boolean>("DB_ROTATION_ENABLED", false),
360
- intervalMs: configService.get<number>("DB_ROTATION_INTERVAL", 3_600_000),
361
- },
362
- }),
417
+ imports: [ConfigModule],
418
+ inject: [ConfigService],
419
+ useFactory: (configService: ConfigService) => {
420
+ const isRotationEnabled = configService.get<boolean>("DB_ROTATION_ENABLED", false);
421
+
422
+ return {
423
+ entities: [UserEntity],
424
+ rotation: {
425
+ intervalMs: configService.get<number>("DB_ROTATION_INTERVAL", 3_600_000),
426
+ isEnabled: isRotationEnabled,
427
+ ...(isRotationEnabled
428
+ ? {
429
+ shutdownDrainTimeoutMs: configService.getOrThrow<number>("DB_ROTATION_SHUTDOWN_DRAIN_TIMEOUT"),
430
+ }
431
+ : {}),
432
+ },
433
+ };
434
+ },
363
435
  });
364
436
  ```
365
437
 
438
+ Omit the raw `shutdownDrainTimeoutMs` property when the enabled service should resolve it from Parameter Store instead of `ConfigService`.
439
+
366
440
  ---
367
441
 
368
442
  ### Multiple Database Modules In One App
@@ -373,12 +447,12 @@ Register the connector separately inside each Nest database module that owns a s
373
447
  import { getDataSourceToken } from "@nestjs/typeorm";
374
448
 
375
449
  TypeOrmAwsConnectorModule.registerAsync({
376
- dataSourceToken: getDataSourceToken("provider"),
377
- imports: [ConfigModule],
378
- inject: [ConfigService],
379
- useFactory: (configService: ConfigService) => ({
380
- entities: [ProviderEntity],
381
- }),
450
+ dataSourceToken: getDataSourceToken("provider"),
451
+ imports: [ConfigModule],
452
+ inject: [ConfigService],
453
+ useFactory: (configService: ConfigService) => ({
454
+ entities: [ProviderEntity],
455
+ }),
382
456
  });
383
457
  ```
384
458
 
@@ -394,27 +468,35 @@ For each configuration field, values are resolved in this order:
394
468
  2. **SSM Parameter Store lookup** (via structured lookup)
395
469
  3. **Built-in default** (only for optional tuning fields like `poolSize`, `connectionTimeoutMs`)
396
470
 
397
- Required fields (`host`, `port`, `databaseName`, `type`, `secretId`) throw explicit errors if not resolvable.
471
+ Required fields (`host`, `port`, `databaseName`, `type`, `secretId`) throw explicit errors if not resolvable. When rotation resolves as enabled, `rotation.shutdownDrainTimeoutMs` is also required. Explicit `rotation` properties take priority over their structured SSM lookups.
472
+
473
+ ### Migrating from 1.x
474
+
475
+ Connector 2.0 intentionally removes the unbounded rotation and force-only shutdown contract. Existing applications that enable rotation must add a positive `rotation.shutdownDrainTimeoutMs`, enable Nest shutdown hooks for process signals, and decide how to surface the new structured rotation events.
476
+
477
+ See the complete [Migrating to 2.0 guide](docs/guides/migrating-to-2-0/page.mdx) before upgrading.
398
478
 
399
479
  ## 🛣 Roadmap
400
480
 
401
481
  <details>
402
482
  <summary>Click to expand</summary>
403
483
 
404
- | Task / Feature | Status |
405
- |---|---|
406
- | Dual ESM/CJS module output | ✅ Done |
407
- | AWS Secrets Manager credential resolution | ✅ Done |
408
- | Automatic credential rotation with staged driver promotion | ✅ Done |
409
- | Emergency recovery after consecutive rotation failures | ✅ Done |
410
- | Split-source configuration model (infra vs app namespaces) | ✅ Done |
411
- | Raw value overrides for local development | ✅ Done |
412
- | Async module registration with factory pattern | ✅ Done |
413
- | Semantic release with prerelease channel | ✅ Done |
484
+ | Task / Feature | Status |
485
+ | ------------------------------------------------------------------ | -------------- |
486
+ | Dual ESM/CJS module output | ✅ Done |
487
+ | AWS Secrets Manager credential resolution | ✅ Done |
488
+ | Automatic credential rotation with staged driver promotion | ✅ Done |
489
+ | Bounded current-plus-one-retiring generation lifecycle | ✅ Done |
490
+ | Structured package-neutral rotation lifecycle events | ✅ Done |
491
+ | Emergency recovery after consecutive rotation failures | ✅ Done |
492
+ | Split-source configuration model (infra vs app namespaces) | ✅ Done |
493
+ | Raw value overrides for local development | ✅ Done |
494
+ | Async module registration with factory pattern | ✅ Done |
495
+ | Semantic release with prerelease channel | ✅ Done |
414
496
  | Support for additional database types (e.g., MariaDB, CockroachDB) | 🚧 In Progress |
415
- | Connection pool monitoring and metrics export | 🚧 In Progress |
416
- | Read replica support with separate SSM lookups | 🚧 In Progress |
417
- | Integration test suite with LocalStack | 🚧 In Progress |
497
+ | Connection pool monitoring and metrics export | 🚧 In Progress |
498
+ | Read replica support with separate SSM lookups | 🚧 In Progress |
499
+ | Integration test suite with LocalStack | 🚧 In Progress |
418
500
 
419
501
  </details>
420
502
 
@@ -425,36 +507,38 @@ Required fields (`host`, `port`, `databaseName`, `type`, `secretId`) throw expli
425
507
 
426
508
  ### ❓ Frequently Asked Questions
427
509
 
428
- **Q: Do I need AWS credentials to use this locally?**
429
- A: No! Use raw value overrides to bypass AWS entirely during local development. Set `host`, `port`, `username`, `password`, `databaseName`, and `type` directly in the config.
510
+ **Q: Do I need AWS credentials to use this locally?** A: No! Use raw value overrides to bypass AWS entirely during local development. Set `host`, `port`, `username`, `password`, `databaseName`, and `type` directly in the config.
511
+
512
+ **Q: Which databases are supported?** A: Currently PostgreSQL (`EDatabaseType.POSTGRES`) and MySQL (`EDatabaseType.MYSQL`). The `type` field is resolved from SSM or set directly.
513
+
514
+ **Q: What happens if AWS Secrets Manager is unreachable during rotation?** A: The rotation service catches the error, increments a failure counter, and logs it. After 3 consecutive failures, it attempts an emergency recovery with a fresh replacement connection. Failed replacements are discarded, and the currently promoted pool stays in place until a later rotation succeeds.
515
+
516
+ **Q: Can I use this without `@elsikora/nestjs-aws-parameter-store-config`?** A: No. The connector depends on `ParameterStoreConfigService.get()` for SSM lookups. You must register `ParameterStoreConfigModule` first. However, if you provide raw values for all fields, SSM lookups won't actually be invoked.
430
517
 
431
- **Q: Which databases are supported?**
432
- A: Currently PostgreSQL (`EDatabaseType.POSTGRES`) and MySQL (`EDatabaseType.MYSQL`). The `type` field is resolved from SSM or set directly.
518
+ **Q: How does the split-source model work?** A: Infrastructure-owned fields like `host`, `port`, and `secretId` default to canonical AWS namespaces (e.g., `aws-rds/aurora-postgres`). Application-owned fields like `poolSize` and `connectionTimeoutMs` use your app's namespace (e.g., `aws-ecs-fargate/my-api`). You can override any lookup.
433
519
 
434
- **Q: What happens if AWS Secrets Manager is unreachable during rotation?**
435
- A: The rotation service catches the error, increments a failure counter, and logs it. After 3 consecutive failures, it attempts an emergency recovery with a fresh replacement connection. Failed replacements are discarded, and the currently promoted pool stays in place until a later rotation succeeds.
520
+ **Q: Is this compatible with NestJS v8?** A: Yes. The peer dependency accepts `@nestjs/common ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0`.
436
521
 
437
- **Q: Can I use this without `@elsikora/nestjs-aws-parameter-store-config`?**
438
- A: No. The connector depends on `ParameterStoreConfigService.get()` for SSM lookups. You must register `ParameterStoreConfigModule` first. However, if you provide raw values for all fields, SSM lookups won't actually be invoked.
522
+ **Q: How do I disable credential rotation?** A: Don't set the `rotation` config, or explicitly set `rotation.isEnabled: false`. The `RotatorService` will skip interval registration entirely. This is the recommended mode for one-shot jobs, migrations, and other short-lived processes.
439
523
 
440
- **Q: How does the split-source model work?**
441
- A: Infrastructure-owned fields like `host`, `port`, and `secretId` default to canonical AWS namespaces (e.g., `aws-rds/aurora-postgres`). Application-owned fields like `poolSize` and `connectionTimeoutMs` use your app's namespace (e.g., `aws-ecs-fargate/my-api`). You can override any lookup.
524
+ **Q: What's the default rotation interval?** A: 1 hour (3,600,000 ms). Override it via `rotation.intervalMs` or the SSM path `typeorm/rotation/interval-ms`.
442
525
 
443
- **Q: Is this compatible with NestJS v8?**
444
- A: Yes. The peer dependency accepts `@nestjs/common ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0`.
526
+ **Q: Is there a default shutdown drain timeout?** A: No. Rotation-enabled services must provide a positive integer through `rotation.shutdownDrainTimeoutMs` or `typeorm/rotation/shutdown-drain-timeout-ms`. One-shot services with rotation disabled do not require it.
445
527
 
446
- **Q: How do I disable credential rotation?**
447
- A: Don't set the `rotation` config, or explicitly set `rotation.isEnabled: false`. The `RotatorService` will skip interval registration entirely. This is the recommended mode for one-shot jobs, migrations, and other short-lived processes.
528
+ **Q: Can several rotations create several old pools?** A: No. The connector permits one current and at most one retiring generation. Further intervals emit `ROTATION_DEFERRED` until the retiring generation closes.
448
529
 
449
- **Q: What's the default rotation interval?**
450
- A: 1 hour (3,600,000 ms). Override it via `rotation.intervalMs` or the SSM path `typeorm/rotation/interval-ms`.
530
+ **Q: What happens to a transaction during credential rotation?** A: Its query runner remains attached to the generation where it started. New query runners use the promoted generation, and ordinary rotation waits for the old pool's runners to release without applying the shutdown timeout. Forced release after `shutdownDrainTimeoutMs` occurs only during process shutdown.
531
+
532
+ **Q: Does `onEvent` use Nest EventEmitter?** A: No. It is an optional callback in the rotation configuration. Use it to bridge structured events into your logger, metrics, or error reporting system without adding `@nestjs/event-emitter`.
451
533
 
452
534
  </details>
453
535
 
454
536
  ## 🔒 License
537
+
455
538
  This project is licensed under **MIT**.
456
539
 
457
540
  ## 🙏 Acknowledgments
541
+
458
542
  - **[NestJS](https://nestjs.com/)** — The progressive Node.js framework that makes building server-side applications a joy
459
543
  - **[TypeORM](https://typeorm.io/)** — The ORM that powers the database layer with decorator-based entity definitions
460
544
  - **[AWS SDK for JavaScript v3](https://github.com/aws/aws-sdk-js-v3)** — Modular AWS SDK used for SSM and Secrets Manager client operations
@@ -467,4 +551,4 @@ This project is licensed under **MIT**.
467
551
 
468
552
  <p align="center">
469
553
  <a href="#top">Back to Top</a>
470
- </p>
554
+ </p>
@@ -5,3 +5,4 @@ export type * from './shared/interface/aws/index';
5
5
  export type * from './shared/interface/typeorm-aws-connector/index';
6
6
  export * from './shared/provider/typeorm-aws-connector/index';
7
7
  export type * from './shared/type/database/index';
8
+ export type * from './shared/type/typeorm-aws-connector/index';
package/dist/cjs/index.js CHANGED
@@ -6,6 +6,9 @@ var typeormAwsConnector_service = require('./modules/typeorm-aws-connector/typeo
6
6
  var constant = require('./shared/constant/typeorm-aws-connector/constant.js');
7
7
  var type_enum = require('./shared/enum/database/type.enum.js');
8
8
  var relationLoadStrategy_enum = require('./shared/enum/relation-load-strategy.enum.js');
9
+ var deferredReason_enum = require('./shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js');
10
+ var drainTimeoutPhase_enum = require('./shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js');
11
+ var event_enum = require('./shared/enum/typeorm-aws-connector/rotation/event.enum.js');
9
12
  var database_provider = require('./shared/provider/typeorm-aws-connector/database.provider.js');
10
13
 
11
14
 
@@ -32,6 +35,18 @@ Object.defineProperty(exports, "ERelationLoadStrategy", {
32
35
  enumerable: true,
33
36
  get: function () { return relationLoadStrategy_enum.ERelationLoadStrategy; }
34
37
  });
38
+ Object.defineProperty(exports, "ETypeOrmAwsConnectorRotationDeferredReason", {
39
+ enumerable: true,
40
+ get: function () { return deferredReason_enum.ETypeOrmAwsConnectorRotationDeferredReason; }
41
+ });
42
+ Object.defineProperty(exports, "ETypeOrmAwsConnectorDrainTimeoutPhase", {
43
+ enumerable: true,
44
+ get: function () { return drainTimeoutPhase_enum.ETypeOrmAwsConnectorDrainTimeoutPhase; }
45
+ });
46
+ Object.defineProperty(exports, "ETypeOrmAwsConnectorRotationEvent", {
47
+ enumerable: true,
48
+ get: function () { return event_enum.ETypeOrmAwsConnectorRotationEvent; }
49
+ });
35
50
  exports.createDatabaseConfigProvider = database_provider.createDatabaseConfigProvider;
36
51
  exports.toTypeOrmAwsConnectorConfig = database_provider.toTypeOrmAwsConnectorConfig;
37
52
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}