@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.
- package/README.md +224 -140
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.d.ts +40 -9
- package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.js +498 -87
- package/dist/cjs/modules/typeorm-aws-connector/rotator/rotator.service.js.map +1 -1
- package/dist/cjs/modules/typeorm-aws-connector/rotator/type/index.d.ts +3 -0
- package/dist/cjs/modules/typeorm-aws-connector/rotator/type/mutable-driver.type.d.ts +5 -0
- package/dist/cjs/modules/typeorm-aws-connector/rotator/type/query-runner-mode.type.d.ts +2 -0
- package/dist/cjs/modules/typeorm-aws-connector/rotator/type/retiring-driver.type.d.ts +5 -0
- package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.d.ts +4 -4
- package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.js +47 -6
- package/dist/cjs/modules/typeorm-aws-connector/typeorm-aws-connector.service.js.map +1 -1
- package/dist/cjs/shared/constant/typeorm-aws-connector/constant.d.ts +1 -1
- package/dist/cjs/shared/constant/typeorm-aws-connector/constant.js +7 -0
- package/dist/cjs/shared/constant/typeorm-aws-connector/constant.js.map +1 -1
- package/dist/cjs/shared/enum/index.d.ts +1 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.d.ts +12 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js +16 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js.map +1 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.d.ts +6 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js +10 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js.map +1 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.d.ts +8 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.js +12 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/event.enum.js.map +1 -0
- package/dist/cjs/shared/enum/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/deferred-event.interface.d.ts +9 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/drain-timeout-event.interface.d.ts +10 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/generation-retired-event.interface.d.ts +8 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/cjs/shared/interface/typeorm-aws-connector/ssm-lookups.interface.d.ts +1 -0
- package/dist/cjs/shared/provider/typeorm-aws-connector/database.provider.js.map +1 -1
- package/dist/cjs/shared/type/database/config-rotation.type.d.ts +7 -0
- package/dist/cjs/shared/type/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/cjs/shared/type/typeorm-aws-connector/rotation/event-listener.type.d.ts +6 -0
- package/dist/cjs/shared/type/typeorm-aws-connector/rotation/event.type.d.ts +2 -0
- package/dist/cjs/shared/type/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/cjs/shared/type/typeorm-aws-connector/rotation/resolved-config.type.d.ts +11 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.d.ts +40 -9
- package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.js +498 -87
- package/dist/esm/modules/typeorm-aws-connector/rotator/rotator.service.js.map +1 -1
- package/dist/esm/modules/typeorm-aws-connector/rotator/type/index.d.ts +3 -0
- package/dist/esm/modules/typeorm-aws-connector/rotator/type/mutable-driver.type.d.ts +5 -0
- package/dist/esm/modules/typeorm-aws-connector/rotator/type/query-runner-mode.type.d.ts +2 -0
- package/dist/esm/modules/typeorm-aws-connector/rotator/type/retiring-driver.type.d.ts +5 -0
- package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.d.ts +4 -4
- package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.js +47 -6
- package/dist/esm/modules/typeorm-aws-connector/typeorm-aws-connector.service.js.map +1 -1
- package/dist/esm/shared/constant/typeorm-aws-connector/constant.d.ts +1 -1
- package/dist/esm/shared/constant/typeorm-aws-connector/constant.js +7 -0
- package/dist/esm/shared/constant/typeorm-aws-connector/constant.js.map +1 -1
- package/dist/esm/shared/enum/index.d.ts +1 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.d.ts +12 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js +16 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/deferred-reason.enum.js.map +1 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.d.ts +6 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js +10 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/drain-timeout-phase.enum.js.map +1 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.d.ts +8 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.js +12 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/event.enum.js.map +1 -0
- package/dist/esm/shared/enum/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/rotation/deferred-event.interface.d.ts +9 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/rotation/drain-timeout-event.interface.d.ts +10 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/rotation/generation-retired-event.interface.d.ts +8 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/esm/shared/interface/typeorm-aws-connector/ssm-lookups.interface.d.ts +1 -0
- package/dist/esm/shared/provider/typeorm-aws-connector/database.provider.js.map +1 -1
- package/dist/esm/shared/type/database/config-rotation.type.d.ts +7 -0
- package/dist/esm/shared/type/typeorm-aws-connector/index.d.ts +1 -0
- package/dist/esm/shared/type/typeorm-aws-connector/rotation/event-listener.type.d.ts +6 -0
- package/dist/esm/shared/type/typeorm-aws-connector/rotation/event.type.d.ts +2 -0
- package/dist/esm/shared/type/typeorm-aws-connector/rotation/index.d.ts +3 -0
- package/dist/esm/shared/type/typeorm-aws-connector/rotation/resolved-config.type.d.ts +11 -0
- 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
|
-
- ♻️
|
|
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
|
|
54
|
-
|
|
55
|
-
| **Language**
|
|
56
|
-
| **Runtime**
|
|
57
|
-
| **Framework**
|
|
58
|
-
| **ORM**
|
|
59
|
-
| **Cloud Services**
|
|
60
|
-
| **Build Tool**
|
|
61
|
-
| **Linting**
|
|
62
|
-
| **CI/CD**
|
|
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
|
-
- ✨ ****
|
|
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 >=
|
|
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.
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
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.
|
|
344
|
-
7.
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
405
|
-
|
|
406
|
-
| Dual ESM/CJS module output
|
|
407
|
-
| AWS Secrets Manager credential resolution
|
|
408
|
-
| Automatic credential rotation with staged driver promotion
|
|
409
|
-
|
|
|
410
|
-
|
|
|
411
|
-
|
|
|
412
|
-
|
|
|
413
|
-
|
|
|
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
|
|
416
|
-
| Read replica support with separate SSM lookups
|
|
417
|
-
| Integration test suite with LocalStack
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
450
|
-
|
|
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>
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|