@venizia/ignis-docs 0.2.0 → 0.2.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
|
@@ -6,16 +6,19 @@ difficulty: intermediate
|
|
|
6
6
|
|
|
7
7
|
# SoftDeletableRepository
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Reference for `SoftDeletableRepository` - delete methods set a `deletedAt` timestamp instead of removing the row, with matching restore methods. For the common tasks, start with the [Repositories overview](/references/base/repositories/).
|
|
10
10
|
|
|
11
|
-
**
|
|
11
|
+
**Files:**
|
|
12
12
|
|
|
13
|
+
- [`packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository` - delete/restore overrides, `isStrict` findById
|
|
14
|
+
- [`packages/core/src/connectors/postgres/repositories/core/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/repositories/core/index.ts) - compatibility alias `SoftDeletableRepository`
|
|
15
|
+
- [`packages/core/src/connectors/postgres/models/enrichers/tz.enricher.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/connectors/postgres/models/enrichers/tz.enricher.ts) - `generateTzColumnDefs` - adds the `deletedAt` column
|
|
13
16
|
|
|
14
17
|
## Setup
|
|
15
18
|
|
|
16
|
-
### 1. Define
|
|
19
|
+
### 1. Define the model with a `deletedAt` column
|
|
17
20
|
|
|
18
|
-
Use the `generateTzColumnDefs` enricher with the `deleted` option enabled
|
|
21
|
+
Use the `generateTzColumnDefs` enricher with the `deleted` option enabled:
|
|
19
22
|
|
|
20
23
|
```typescript
|
|
21
24
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
@@ -45,12 +48,12 @@ export class Category extends BaseEntity<typeof Category.schema> {
|
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
> [!IMPORTANT]
|
|
48
|
-
> - The model **must** have a `deletedAt` column
|
|
51
|
+
> - The model **must** have a `deletedAt` column. `SoftDeletableRepository` requires `TSoftDeletableTableSchema`, which enforces `{ deletedAt: AnyPgColumn<{ data: Date | string | null }> }`.
|
|
49
52
|
> - Set `defaultFilter: { where: { deletedAt: null } }` in `@model` settings so soft-deleted records are excluded by default.
|
|
50
53
|
> - Optionally add `deletedAt` to `hiddenProperties` to hide it from API responses.
|
|
51
54
|
> - Use `generateTzColumnDefs` with `deleted: { enable: true, ... }` to add the column, or define it manually with `timestamp('deleted_at', { mode: 'date', withTimezone: true })`.
|
|
52
55
|
|
|
53
|
-
### 2.
|
|
56
|
+
### 2. Extend `SoftDeletableRepository`
|
|
54
57
|
|
|
55
58
|
```typescript
|
|
56
59
|
import { repository, SoftDeletableRepository } from '@venizia/ignis';
|
|
@@ -61,10 +64,10 @@ import { PostgresDataSource } from '@/datasources/postgres.datasource';
|
|
|
61
64
|
export class CategoryRepository extends SoftDeletableRepository<typeof Category.schema> {}
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
|
|
65
67
|
## Delete Operations
|
|
66
68
|
|
|
67
|
-
All delete methods set `deletedAt = new Date()` instead of removing the row
|
|
69
|
+
- **Delete sets a timestamp, not a `DELETE`.** All delete methods set `deletedAt = new Date()` instead of removing the row - `deleteById` calls `updateById` internally, `deleteAll`/`deleteBy` call `updateAll`.
|
|
70
|
+
- **Bypass with `shouldHardDelete`.** Pass `options.shouldHardDelete: true` to skip soft delete and delegate to the parent's real `DELETE`.
|
|
68
71
|
|
|
69
72
|
### deleteById
|
|
70
73
|
|
|
@@ -88,24 +91,33 @@ const result = await repository.deleteById({
|
|
|
88
91
|
|
|
89
92
|
### deleteAll
|
|
90
93
|
|
|
94
|
+
`where` only needs `options.force: true` when it is empty - a non-empty condition never needs it.
|
|
95
|
+
|
|
91
96
|
```typescript
|
|
92
|
-
// Soft delete
|
|
97
|
+
// Soft delete matching records - non-empty where, no force needed
|
|
93
98
|
const result = await repository.deleteAll({
|
|
94
99
|
where: { status: 'archived' },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Soft delete every row - empty where requires force
|
|
103
|
+
const result = await repository.deleteAll({
|
|
104
|
+
where: {},
|
|
95
105
|
options: { force: true },
|
|
96
106
|
});
|
|
97
107
|
|
|
98
|
-
// Hard delete
|
|
108
|
+
// Hard delete matching records
|
|
99
109
|
const result = await repository.deleteAll({
|
|
100
110
|
where: { status: 'archived' },
|
|
101
|
-
options: { shouldHardDelete: true
|
|
111
|
+
options: { shouldHardDelete: true },
|
|
102
112
|
});
|
|
103
113
|
```
|
|
104
114
|
|
|
105
115
|
### deleteBy
|
|
106
116
|
|
|
117
|
+
Same behavior as `deleteAll`, but `where` is a required parameter instead of optional.
|
|
118
|
+
|
|
107
119
|
```typescript
|
|
108
|
-
// Soft delete by where condition
|
|
120
|
+
// Soft delete by where condition
|
|
109
121
|
const result = await repository.deleteBy({
|
|
110
122
|
where: { name: 'Obsolete' },
|
|
111
123
|
});
|
|
@@ -117,10 +129,10 @@ const result = await repository.deleteBy({
|
|
|
117
129
|
});
|
|
118
130
|
```
|
|
119
131
|
|
|
120
|
-
|
|
121
132
|
## Restore Operations
|
|
122
133
|
|
|
123
|
-
Restore methods set `deletedAt = null
|
|
134
|
+
- **Restore clears the timestamp.** Restore methods set `deletedAt = null`.
|
|
135
|
+
- **Default filter is bypassed automatically.** They internally pass `shouldSkipDefaultFilter: true` so they can find the soft-deleted records the default filter would otherwise hide - you do not need to set this yourself.
|
|
124
136
|
|
|
125
137
|
### restoreById
|
|
126
138
|
|
|
@@ -138,7 +150,7 @@ const result = await repository.restoreById({
|
|
|
138
150
|
### restoreAll
|
|
139
151
|
|
|
140
152
|
```typescript
|
|
141
|
-
// Restore all soft-deleted records (requires force
|
|
153
|
+
// Restore all soft-deleted records (empty where requires force)
|
|
142
154
|
const result = await repository.restoreAll({
|
|
143
155
|
where: {},
|
|
144
156
|
options: { force: true },
|
|
@@ -153,13 +165,12 @@ const result = await repository.restoreAll({
|
|
|
153
165
|
### restoreBy
|
|
154
166
|
|
|
155
167
|
```typescript
|
|
156
|
-
//
|
|
168
|
+
// Calls restoreAll internally
|
|
157
169
|
const result = await repository.restoreBy({
|
|
158
170
|
where: { status: 'archived' },
|
|
159
171
|
});
|
|
160
172
|
```
|
|
161
173
|
|
|
162
|
-
|
|
163
174
|
## Read Operations
|
|
164
175
|
|
|
165
176
|
### findById with isStrict
|
|
@@ -178,8 +189,8 @@ const category = await repository.findById({
|
|
|
178
189
|
// Throws: [CategoryRepository][findById] Entity with id 123 not found (HTTP 404)
|
|
179
190
|
```
|
|
180
191
|
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
- **Everything else is inherited unchanged.** `find`, `findOne`, `count`, `existsWith` behave exactly as on `DefaultCRUDRepository`.
|
|
193
|
+
- **The default filter excludes soft-deleted rows.** `{ deletedAt: null }` is applied automatically - pass `options: { shouldSkipDefaultFilter: true }` to include them.
|
|
183
194
|
|
|
184
195
|
## Options Reference
|
|
185
196
|
|
|
@@ -189,7 +200,7 @@ All other read operations (`find`, `findOne`, `count`, `existsWith`) work as nor
|
|
|
189
200
|
|--------|------|---------|-------------|
|
|
190
201
|
| `shouldHardDelete` | `boolean` | `false` | Bypass soft delete and physically remove the row |
|
|
191
202
|
| `shouldReturn` | `boolean` | `true` | Return the updated/deleted record |
|
|
192
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition (`deleteAll`/`deleteBy`) |
|
|
203
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition (`deleteAll`/`deleteBy`) |
|
|
193
204
|
| `transaction` | `ITransaction` | - | Transaction context |
|
|
194
205
|
| `log` | `{ use: boolean; level?: TLogLevel }` | - | Enable operation logging |
|
|
195
206
|
| `shouldSkipDefaultFilter` | `boolean` | `false` | Bypass the default filter |
|
|
@@ -199,27 +210,22 @@ All other read operations (`find`, `findOne`, `count`, `existsWith`) work as nor
|
|
|
199
210
|
| Option | Type | Default | Description |
|
|
200
211
|
|--------|------|---------|-------------|
|
|
201
212
|
| `shouldReturn` | `boolean` | `true` | Return the restored record |
|
|
202
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition (`restoreAll`) |
|
|
213
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition (`restoreAll` only - `restoreById` has no `force` option) |
|
|
203
214
|
| `transaction` | `ITransaction` | - | Transaction context |
|
|
204
215
|
|
|
205
|
-
> [!NOTE]
|
|
206
|
-
> Restore operations automatically set `shouldSkipDefaultFilter: true` internally so they can find soft-deleted records that the default filter would normally hide. You do not need to set this yourself.
|
|
207
|
-
|
|
208
|
-
|
|
209
216
|
## How It Works
|
|
210
217
|
|
|
211
218
|
| Operation | SQL Behavior |
|
|
212
219
|
|-----------|-------------|
|
|
213
220
|
| `deleteById` | `UPDATE SET deletedAt = NOW() WHERE id = ?` (via `updateById`) |
|
|
214
221
|
| `deleteAll` / `deleteBy` | `UPDATE SET deletedAt = NOW() WHERE ...` (via `updateAll`) |
|
|
215
|
-
| `restoreById` | `UPDATE SET deletedAt = NULL WHERE id = ?` (
|
|
216
|
-
| `restoreAll` / `restoreBy` | `UPDATE SET deletedAt = NULL WHERE ...` (
|
|
222
|
+
| `restoreById` | `UPDATE SET deletedAt = NULL WHERE id = ?` (with `shouldSkipDefaultFilter: true`) |
|
|
223
|
+
| `restoreAll` / `restoreBy` | `UPDATE SET deletedAt = NULL WHERE ...` (with `shouldSkipDefaultFilter: true`) |
|
|
217
224
|
| `find` / `findOne` / `count` | Default filter automatically adds `WHERE deletedAt IS NULL` |
|
|
218
|
-
| `deleteById({ shouldHardDelete: true })` | `DELETE FROM ... WHERE id = ?` (delegates to parent
|
|
225
|
+
| `deleteById({ shouldHardDelete: true })` | `DELETE FROM ... WHERE id = ?` (delegates to the parent repository) |
|
|
219
226
|
|
|
220
227
|
> [!TIP]
|
|
221
|
-
>
|
|
222
|
-
|
|
228
|
+
> `shouldHardDelete` bypasses soft delete entirely and delegates to the parent `DefaultCRUDRepository`'s delete implementation, which performs a real SQL `DELETE`.
|
|
223
229
|
|
|
224
230
|
## With Transactions
|
|
225
231
|
|
|
@@ -232,64 +238,60 @@ try {
|
|
|
232
238
|
options: { transaction: tx },
|
|
233
239
|
});
|
|
234
240
|
await tx.commit();
|
|
235
|
-
} catch {
|
|
241
|
+
} catch (error) {
|
|
236
242
|
await tx.rollback();
|
|
243
|
+
throw error;
|
|
237
244
|
}
|
|
238
245
|
```
|
|
239
246
|
|
|
240
|
-
|
|
241
247
|
## Type Constraint
|
|
242
248
|
|
|
243
249
|
`SoftDeletableRepository` enforces that the schema includes a `deletedAt` column at the type level:
|
|
244
250
|
|
|
245
251
|
```typescript
|
|
252
|
+
export type TDeletedAtColumn = AnyPgColumn<{ data: Date | string | null }>;
|
|
253
|
+
|
|
246
254
|
export type TSoftDeletableTableSchema = TTableSchemaWithId & {
|
|
247
|
-
deletedAt:
|
|
255
|
+
deletedAt: TDeletedAtColumn;
|
|
248
256
|
};
|
|
249
257
|
|
|
250
|
-
export class
|
|
258
|
+
export class SoftDeletableRelationalRepository<
|
|
251
259
|
EntitySchema extends TSoftDeletableTableSchema = TSoftDeletableTableSchema,
|
|
252
260
|
// ...
|
|
253
|
-
> extends
|
|
261
|
+
> extends DefaultRelationalRepository<EntitySchema, ...> { }
|
|
254
262
|
```
|
|
255
263
|
|
|
256
|
-
If your schema does not have a `deletedAt` column, you
|
|
257
|
-
|
|
264
|
+
If your schema does not have a `deletedAt` column, you get a TypeScript compilation error when extending `SoftDeletableRepository`.
|
|
258
265
|
|
|
259
266
|
## Class Hierarchy
|
|
260
267
|
|
|
268
|
+
`SoftDeletableRepository` is the friendly alias for `SoftDeletableRelationalRepository`, the last tier of the PostgreSQL repository chain:
|
|
269
|
+
|
|
261
270
|
```
|
|
262
271
|
AbstractRepository (engine-neutral, src/base)
|
|
263
|
-
->
|
|
264
|
-
-> ReadableRepository
|
|
265
|
-
-> PersistableRepository
|
|
266
|
-
-> DefaultCRUDRepository
|
|
267
|
-
-> SoftDeletableRepository <-- you are here
|
|
272
|
+
-> RelationalBaseRepository (PostgresBaseRepository)
|
|
273
|
+
-> ReadableRelationalRepository (ReadableRepository)
|
|
274
|
+
-> PersistableRelationalRepository (PersistableRepository)
|
|
275
|
+
-> DefaultRelationalRepository (DefaultCRUDRepository)
|
|
276
|
+
-> SoftDeletableRelationalRepository (SoftDeletableRepository) <-- you are here
|
|
268
277
|
```
|
|
269
278
|
|
|
270
|
-
|
|
271
279
|
## Quick Reference
|
|
272
280
|
|
|
273
281
|
| Want to... | Code |
|
|
274
282
|
|------------|------|
|
|
275
283
|
| Soft delete by ID | `repository.deleteById({ id })` |
|
|
276
284
|
| Hard delete by ID | `repository.deleteById({ id, options: { shouldHardDelete: true } })` |
|
|
277
|
-
| Soft delete by condition | `repository.deleteAll({ where
|
|
285
|
+
| Soft delete by condition | `repository.deleteAll({ where })` |
|
|
278
286
|
| Restore by ID | `repository.restoreById({ id })` |
|
|
279
287
|
| Restore by condition | `repository.restoreAll({ where })` |
|
|
280
288
|
| Find including deleted | `repository.find({ filter, options: { shouldSkipDefaultFilter: true } })` |
|
|
281
289
|
| Strict findById (404) | `repository.findById({ id, options: { isStrict: true } })` |
|
|
282
290
|
|
|
291
|
+
## See also
|
|
283
292
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
- [
|
|
287
|
-
- [
|
|
288
|
-
- [
|
|
289
|
-
|
|
290
|
-
## See Also
|
|
291
|
-
|
|
292
|
-
- **Related Concepts:**
|
|
293
|
-
- [Repositories Overview](./index) - Core repository operations
|
|
294
|
-
- [Default Filter](../filter-system/default-filter) - Automatic filtering
|
|
295
|
-
- [Models](/guides/core-concepts/persistent/models) - Entity definitions with enrichers
|
|
293
|
+
- [Repositories overview](/references/base/repositories/) - CRUD basics, common tasks
|
|
294
|
+
- [Advanced Features](./advanced) - transactions, hidden properties, performance
|
|
295
|
+
- [Repository Mixins (Removed)](./mixins) - where default-filter and fields-visibility behavior lives now
|
|
296
|
+
- [Default Filter](/references/base/filter-system/default-filter) - configuring `@model` default filters
|
|
297
|
+
- [Models - Full Reference](/references/base/models-reference) - `@model`, entity hierarchy, schema enrichers
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Secrets & Vault Reference
|
|
3
|
+
description: Technical reference for the Secrets provider family, boot-time hydration, dynamic leases, and credential rotation
|
|
4
|
+
difficulty: advanced
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Deep Dive: Secrets & Vault
|
|
8
|
+
|
|
9
|
+
Technical reference for the `Secrets` provider family - how IGNIS loads configuration and credentials from a vault instead of reading `process.env` directly, hydrates static secrets at boot, and rotates dynamic database credentials into a live connection pool without a restart.
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT] Dormant by default
|
|
12
|
+
> An application that does not override `registerSecrets()` gets the `system-envs` provider, which reads `process.env` exactly as before. The whole subsystem is additive - existing apps behave identically until they opt in.
|
|
13
|
+
|
|
14
|
+
**Files:** `packages/helpers/src/modules/secrets/*.ts` (provider family) and `packages/core/src/base/applications/base.ts` (`hydrateSecrets()` lifecycle, DI binding, rotation wiring)
|
|
15
|
+
|
|
16
|
+
## Quick Reference
|
|
17
|
+
|
|
18
|
+
| Class / Interface | Purpose | Key Members |
|
|
19
|
+
|-------------------|---------|-------------|
|
|
20
|
+
| **ISecretsHelper** | Contract every provider implements | `get()`, `getBundle()`, `lease()`, `onRotate()`, `registerRotatable()`, `shutdown()`, `configure()` |
|
|
21
|
+
| **AbstractSecretsHelper** | Provider-agnostic machinery | TTL cache, lease registry, renewal scheduler, `onRotate` dispatch; abstract `fetchRaw`/`renewRaw`/`revokeRaw` |
|
|
22
|
+
| **SystemEnvsHelper** | Default, static, zero-dependency | Reads `process.env`; `lease()` throws NotSupported |
|
|
23
|
+
| **HashiCorpVaultHelper** | KV v2 + dynamic leases + rotation | Token / AppRole / Kubernetes auth; token self-renewal; `node-vault` optional peer |
|
|
24
|
+
| **DotenvVaultHelper** | Static, encrypted `.env.vault` | Decrypts via `@dotenvx/dotenvx`; `lease()` throws NotSupported |
|
|
25
|
+
| **SecretProviders** | Provider const-class | `SYSTEM_ENVS`, `HASHICORP_VAULT`, `DOTENV_VAULT`, `SCHEME_SET`, `isValid()` |
|
|
26
|
+
| **VaultAuthMethods** | Auth const-class | `TOKEN`, `APP_ROLE`, `KUBERNETES`, `SCHEME_SET`, `isValid()` |
|
|
27
|
+
| **ISecretsRegistration** | What `registerSecrets()` returns | `provider`, `config`, `hydrate[]`, `lease[]`, `renewBeforeRatio`, `cacheTtlSeconds` |
|
|
28
|
+
| **ISecretRotatable** | Opt-in consumer hook | `onSecretRotated({ key, secret })` |
|
|
29
|
+
|
|
30
|
+
## Two Paths: Hydrate and Provider
|
|
31
|
+
|
|
32
|
+
Secrets reach the application through two independent channels.
|
|
33
|
+
|
|
34
|
+
- **Hydrate (static)** - during boot, the framework fetches secrets from the vault and merges them into `Envs` and `process.env`. Any code that already reads `process.env.APP_ENV_*` keeps working unchanged. Set once, never rotated.
|
|
35
|
+
- **Provider (dynamic)** - the resolved provider is bound in the container at the key `@app/config`. Code that needs on-demand reads, dynamic credentials, or rotation injects that provider and calls `await secrets.get(...)`.
|
|
36
|
+
|
|
37
|
+
A single registration drives both. The hydrate path is a convenience over the provider path - the same provider produces both.
|
|
38
|
+
|
|
39
|
+
## Provider Tiers
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
AbstractSecretsHelper (BaseHelper; TTL cache, lease registry, renewal scheduler, onRotate dispatch)
|
|
43
|
+
├── SystemEnvsHelper // 'system-envs' - process.env / Envs. Static, zero deps, default.
|
|
44
|
+
├── HashiCorpVaultHelper // 'hashicorp-vault' - full KV + dynamic + lease + rotation.
|
|
45
|
+
└── DotenvVaultHelper // 'dotenv-vault' - encrypted .env.vault, static only.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The renewal scheduler, TTL cache, lease registry, and `onRotate` dispatch all live in `AbstractSecretsHelper`. A concrete provider implements only the raw calls against its backend (`fetchRaw`, `renewRaw`, `revokeRaw`), which is why the two static providers are thin and why only HashiCorp exercises the lease machinery.
|
|
49
|
+
|
|
50
|
+
## `ISecretsHelper` Interface
|
|
51
|
+
|
|
52
|
+
**File:** `packages/helpers/src/modules/secrets/common/types.ts`
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
interface ISecretsHelper extends IConfigurable {
|
|
56
|
+
// One-shot read of a static / KV value (TTL-cached).
|
|
57
|
+
get<TValue = string>(opts: { path: string; key?: string; defaultValue?: TValue }): Promise<TValue>;
|
|
58
|
+
|
|
59
|
+
// Whole KV bundle at a path - hydration merges this into Envs.
|
|
60
|
+
getBundle(opts: { path: string }): Promise<Record<string, string>>;
|
|
61
|
+
|
|
62
|
+
// Lease-bearing dynamic secret. The provider tracks the lease, schedules renewal,
|
|
63
|
+
// and emits onRotate when the backend issues fresh credentials.
|
|
64
|
+
lease(opts: { path: string; key: string }): Promise<ISecretLease>;
|
|
65
|
+
|
|
66
|
+
// Rotation event - mirrors the onInitialized / onConnected / onError vocabulary.
|
|
67
|
+
onRotate(handler: (event: ISecretRotationEvent) => ValueOrPromise<void>): void;
|
|
68
|
+
|
|
69
|
+
// Connect a live consumer (a pool holder) to a lease key.
|
|
70
|
+
registerRotatable(opts: { key: string; target: ISecretRotatable }): void;
|
|
71
|
+
|
|
72
|
+
// Stop renewal timers and revoke outstanding leases.
|
|
73
|
+
shutdown(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ISecretLease {
|
|
77
|
+
value: Record<string, string>; // e.g. { username, password }
|
|
78
|
+
leaseId: string;
|
|
79
|
+
ttlSeconds: number;
|
|
80
|
+
renewable: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ISecretRotationEvent {
|
|
84
|
+
key: string; // logical key, e.g. 'datasources.PostgresDataSource'
|
|
85
|
+
lease: ISecretLease;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Opt-in consumer hook. Soft-evict, NOT a hard teardown.
|
|
89
|
+
interface ISecretRotatable {
|
|
90
|
+
onSecretRotated(opts: { key: string; secret: Record<string, string> }): Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> [!NOTE] Static vs dynamic secrets
|
|
95
|
+
> `get()` / `getBundle()` return static values (KV, env) and are TTL-cached. `lease()` returns a dynamic, lease-bearing secret and is supported only by `HashiCorpVaultHelper`; the static providers throw an explicit NotSupported error rather than returning a fake lease.
|
|
96
|
+
|
|
97
|
+
## Registration
|
|
98
|
+
|
|
99
|
+
An application opts in by overriding `registerSecrets()` on its `BaseApplication` subclass. The method returns an `ISecretsRegistration` and runs before datasources are built.
|
|
100
|
+
|
|
101
|
+
**File:** `packages/helpers/src/modules/secrets/common/types.ts`
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface ISecretsRegistration {
|
|
105
|
+
provider: TSecretProvider; // SecretProviders.*
|
|
106
|
+
config?: AnyObject; // provider-specific (endpoint, auth, ...)
|
|
107
|
+
hydrate?: Array<{ // static KV -> merged into Envs at boot
|
|
108
|
+
path: string;
|
|
109
|
+
prefix?: string; // prepend to every merged key
|
|
110
|
+
keys?: Record<string, string>; // explicit vaultKey -> envKey (wins over prefix)
|
|
111
|
+
}>;
|
|
112
|
+
lease?: Array<{ // dynamic leases -> renewed + rotated
|
|
113
|
+
key: string; // binding key of the consuming datasource
|
|
114
|
+
path: string;
|
|
115
|
+
}>;
|
|
116
|
+
renewBeforeRatio?: number; // renew a lease at ttl * ratio (default 0.66)
|
|
117
|
+
cacheTtlSeconds?: number; // TTL for the get/getBundle cache (default 300)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `registerSecrets()` default
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// packages/core/src/base/applications/base.ts
|
|
125
|
+
registerSecrets(): ValueOrPromise<ISecretsRegistration> {
|
|
126
|
+
return { provider: SecretProviders.SYSTEM_ENVS };
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Because the default names `system-envs`, an app that never overrides it hydrates nothing, leases nothing, and reads `process.env` as always.
|
|
131
|
+
|
|
132
|
+
## Const-classes
|
|
133
|
+
|
|
134
|
+
**File:** `packages/helpers/src/modules/secrets/common/constants.ts`
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export class SecretProviders {
|
|
138
|
+
static readonly SYSTEM_ENVS = 'system-envs';
|
|
139
|
+
static readonly HASHICORP_VAULT = 'hashicorp-vault';
|
|
140
|
+
static readonly DOTENV_VAULT = 'dotenv-vault';
|
|
141
|
+
|
|
142
|
+
static readonly SCHEME_SET = new Set([this.SYSTEM_ENVS, this.HASHICORP_VAULT, this.DOTENV_VAULT]);
|
|
143
|
+
static isValid(value: string): value is TSecretProvider {
|
|
144
|
+
return this.SCHEME_SET.has(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class VaultAuthMethods {
|
|
149
|
+
static readonly TOKEN = 'token';
|
|
150
|
+
static readonly APP_ROLE = 'app-role';
|
|
151
|
+
static readonly KUBERNETES = 'kubernetes';
|
|
152
|
+
|
|
153
|
+
static readonly SCHEME_SET = new Set([this.TOKEN, this.APP_ROLE, this.KUBERNETES]);
|
|
154
|
+
static isValid(value: string): value is TVaultAuthMethod {
|
|
155
|
+
return this.SCHEME_SET.has(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Lifecycle Integration
|
|
161
|
+
|
|
162
|
+
The registration is consumed by a new async boot phase, `hydrateSecrets()`, inserted between `preConfigure()` and `registerDataSources()`.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
validateEnvs → staticConfigure → preConfigure
|
|
166
|
+
→ hydrateSecrets() ← resolve provider, hydrate Envs, bind @app/config, set up leases
|
|
167
|
+
→ registerDataSources
|
|
168
|
+
→ wireSecretRotatables() ← connect each lease key to its datasource
|
|
169
|
+
→ registerComponents → registerControllers → postConfigure
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`hydrateSecrets()` does four things:
|
|
173
|
+
|
|
174
|
+
1. Calls `registerSecrets()` and builds the provider via `createSecretsHelper({ provider })`.
|
|
175
|
+
2. Runs `provider.configure()` (authentication), then merges every `hydrate` entry into `Envs` + `process.env`, and opens every `lease`.
|
|
176
|
+
3. Binds the live provider at `CoreBindings.APPLICATION_CONFIG` (`@app/config`) as a singleton.
|
|
177
|
+
4. Registers a post-stop hook (`secrets.shutdown`) so `provider.shutdown()` runs on teardown, revoking outstanding leases.
|
|
178
|
+
|
|
179
|
+
`wireSecretRotatables()` runs after datasources are registered: for each `lease` entry it resolves the datasource at `entry.key` and, if the instance implements `onSecretRotated`, calls `provider.registerRotatable({ key, target })`. A datasource that does not implement the hook is skipped.
|
|
180
|
+
|
|
181
|
+
> [!TIP] Why a lifecycle phase and not a Component
|
|
182
|
+
> Components register *after* datasources, but secrets must be resolved *before* datasources build their pools. Hydration is therefore a dedicated phase, not a component.
|
|
183
|
+
|
|
184
|
+
## Failure Mode
|
|
185
|
+
|
|
186
|
+
`hydrateSecrets()` is fail-closed in production and forgiving in development, keyed on `Environment.DEVELOPMENT_ENVS` (`local`, `debug`, `development`, `dev`, `sit`).
|
|
187
|
+
|
|
188
|
+
| Environment | Vault unreachable / auth fails / secret missing |
|
|
189
|
+
|-------------|-------------------------------------------------|
|
|
190
|
+
| Development set | Log a warning, fall back to a `system-envs` provider, continue booting |
|
|
191
|
+
| Everything else (prod, staging, and any unrecognized name) | Throw `ApplicationError`, crash the boot |
|
|
192
|
+
|
|
193
|
+
The fallback in development builds a fresh `SystemEnvsHelper` and shuts down the partially-built provider first, so a half-authenticated Vault client leaves no renewal timer or lease behind.
|
|
194
|
+
|
|
195
|
+
## `AbstractSecretsHelper` Machinery
|
|
196
|
+
|
|
197
|
+
**File:** `packages/helpers/src/modules/secrets/base/abstract.helper.ts`
|
|
198
|
+
|
|
199
|
+
- **TTL cache** - `get()` / `getBundle()` cache each path for `cacheTtlSeconds` (default 300) and re-fetch on expiry.
|
|
200
|
+
- **Renewal scheduler** - each lease schedules a renewal at `ttlSeconds × renewBeforeRatio` (default 0.66) through an injectable timer seam. On fire, `renewRaw()` extends the same lease; on renew failure or max-TTL, `fetchRaw()` mints a fresh lease and a single rotation is dispatched.
|
|
201
|
+
- **Rotation dispatch** - `onRotate` handlers run first, then the registered rotatables for that key run **in series**; a throwing consumer is logged and does not abort the others.
|
|
202
|
+
- **Shutdown** - clears every timer and revokes every lease via `revokeRaw()`.
|
|
203
|
+
|
|
204
|
+
The scheduler, cache, and clock are injectable, so the machinery is tested deterministically without real timers or a live vault.
|
|
205
|
+
|
|
206
|
+
## HashiCorp Vault Provider
|
|
207
|
+
|
|
208
|
+
**File:** `packages/helpers/src/modules/secrets/hashicorp/hashicorp.helper.ts`
|
|
209
|
+
|
|
210
|
+
- **Auth** - a Zod discriminated union on `method`: `token`, `app-role` (`roleId` + `secretId`), `kubernetes` (`role`, optional `jwtPath`). `configure()` logs in and stores the Vault token.
|
|
211
|
+
- **KV v2** - `getBundle()` unwraps the KV-v2 `.data.data` envelope automatically.
|
|
212
|
+
- **Dynamic secrets** - a read against a dynamic engine (for example `database/creds/...`) returns a lease (`lease_id`, `lease_duration`, `renewable`), which drives the renewal scheduler.
|
|
213
|
+
- **Token self-renewal** - the Vault auth token has its own TTL. The provider schedules the token for renewal in the same cadence and re-runs the login flow (re-auth) if the token can no longer be renewed, so AppRole / Kubernetes deployments survive past the token TTL without a restart.
|
|
214
|
+
|
|
215
|
+
`node-vault` is an optional peer. It is reached only through the `@venizia/ignis-helpers/hashicorp-vault` sub-path and a bundler-invisible dynamic import (`importOptionalModule`), so importing the root package never requires it - and `Bun.build`-compiled applications need no `external: ['node-vault']` workaround. An application that does use this provider and compiles a binary must ship `node-vault` in `node_modules` next to the binary, or inject a ready-made `client` through the helper options.
|
|
216
|
+
|
|
217
|
+
## Dotenv Vault Provider
|
|
218
|
+
|
|
219
|
+
**File:** `packages/helpers/src/modules/secrets/dotenv/dotenv.helper.ts`
|
|
220
|
+
|
|
221
|
+
Decrypts a committed `.env.vault` file with a per-environment `DOTENV_KEY` (via `@dotenvx/dotenvx`) into a flat key-value map, which the hydrate path merges into `Envs`. Static only - `lease()` throws NotSupported. `@dotenvx/dotenvx` is an optional peer reached through the `@venizia/ignis-helpers/dotenv-vault` sub-path.
|
|
222
|
+
|
|
223
|
+
## Rotation and the Soft-Evict Contract
|
|
224
|
+
|
|
225
|
+
When a dynamic credential rotates, a live connection pool holding the old credentials must rebuild. IGNIS uses an event plus an opt-in rebuild callback, mirroring the Spring Cloud Vault + HikariCP pattern.
|
|
226
|
+
|
|
227
|
+
The PostgreSQL datasource implements `onSecretRotated()` on `AbstractRelationalDataSource`:
|
|
228
|
+
|
|
229
|
+
1. Capture the current pool.
|
|
230
|
+
2. Apply the new credentials onto `this.settings` (`{ username, password }` maps to pg's `{ user, password }`).
|
|
231
|
+
3. Clear the driver / connector / client, re-run `configure()` to build a fresh pool, and re-wire the driver.
|
|
232
|
+
4. Drain the old pool with `end()` - which resolves once checked-out clients are released, so in-flight transactions finish on the old pool while new work uses the new one.
|
|
233
|
+
|
|
234
|
+
> [!WARNING] configure() must read from this.settings
|
|
235
|
+
> Rotation applies new credentials by writing them onto `this.settings` and re-running `configure()`. A `configure()` that builds its pool from a hard-coded connection string, or reads `Envs` directly, will rebuild with **stale** credentials.
|
|
236
|
+
|
|
237
|
+
## Consuming the Provider
|
|
238
|
+
|
|
239
|
+
Any provider-fed code injects the singleton at `@app/config`.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import type { ISecretsHelper } from '@venizia/ignis-helpers';
|
|
243
|
+
|
|
244
|
+
@service()
|
|
245
|
+
export class PaymentService {
|
|
246
|
+
constructor(
|
|
247
|
+
@inject({ key: '@app/config' }) private secrets: ISecretsHelper,
|
|
248
|
+
) {}
|
|
249
|
+
|
|
250
|
+
async charge() {
|
|
251
|
+
const apiKey = await this.secrets.get({ path: 'secret/data/myapp/stripe', key: 'apiKey' });
|
|
252
|
+
// ...
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
For a step-by-step walkthrough of enabling each provider, see the [Secrets & Vault guide](/guides/core-concepts/secrets-vault).
|
|
258
|
+
|
|
259
|
+
## See Also
|
|
260
|
+
|
|
261
|
+
- [Secrets & Vault guide](/guides/core-concepts/secrets-vault) - practical setup for each provider
|
|
262
|
+
- [DataSources](./datasources) - the pool that rotation rebuilds
|
|
263
|
+
- [Environment Variables](/references/configuration/environment-variables) - where hydrated secrets land
|
|
@@ -19,7 +19,7 @@ Technical reference for `BaseService` - the foundation for the business logic la
|
|
|
19
19
|
| **Logging** | `this.logger` (scoped to constructor `scope`) |
|
|
20
20
|
| **Registration** | `this.service(MyService)` in application lifecycle |
|
|
21
21
|
| **Binding key** | `services.{ClassName}` (e.g., `services.AuthenticationService`) |
|
|
22
|
-
| **DI decorator** |
|
|
22
|
+
| **DI decorator** | None on the class itself - only `@inject` on constructor parameters or properties |
|
|
23
23
|
| **CRUD service** | Removed - use `DefaultCRUDRepository` for data access |
|
|
24
24
|
|
|
25
25
|
---
|
|
@@ -54,7 +54,7 @@ export interface IService {}
|
|
|
54
54
|
|
|
55
55
|
## Registering a Service
|
|
56
56
|
|
|
57
|
-
Services are registered imperatively in an application lifecycle method.
|
|
57
|
+
Services are registered imperatively in an application lifecycle method. No class decorator is involved - `this.service()` creates the binding and handles everything.
|
|
58
58
|
|
|
59
59
|
```typescript
|
|
60
60
|
// In your Application class (e.g., in preConfigure())
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# Environment Variables Reference
|
|
2
2
|
|
|
3
|
-
Complete reference of all environment variables used by IGNIS framework.
|
|
3
|
+
Complete reference of all environment variables used by the IGNIS framework, grouped by category with defaults and required/optional status.
|
|
4
4
|
|
|
5
|
+
**Files:**
|
|
6
|
+
|
|
7
|
+
- [`packages/core/src/common/environments.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/common/environments.ts) - `EnvironmentKeys`
|
|
8
|
+
- [`packages/helpers/src/modules/env/app-env.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/env/app-env.ts) - `applicationEnvironment`, `Environment`
|
|
9
|
+
- [`packages/core/src/base/applications/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/applications/abstract.ts) - `validateEnvs()`, host/port resolution priority
|
|
10
|
+
- [`packages/core/src/base/applications/base.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/applications/base.ts) - `registerSecrets()`, `hydrateSecrets()`
|
|
11
|
+
- [`packages/helpers/src/modules/secrets/common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/secrets/common/constants.ts) - `SecretProviders`, `VaultAuthMethods`
|
|
12
|
+
- [`packages/helpers/src/modules/secrets/hashicorp/hashicorp.helper.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/helpers/src/modules/secrets/hashicorp/hashicorp.helper.ts) - HashiCorp Vault helper
|
|
5
13
|
|
|
6
14
|
## Overview
|
|
7
15
|
|
|
@@ -189,7 +197,9 @@ export class PostgresDataSource extends BaseDataSource {
|
|
|
189
197
|
|
|
190
198
|
### Security Requirements
|
|
191
199
|
|
|
192
|
-
The `AuthenticateComponent` receives its secrets programmatically via the `jwtOptions` binding (`jwtSecret`, `getTokenExpiresFn`)
|
|
200
|
+
- **Not read directly.** The `AuthenticateComponent` receives its secrets programmatically via the `jwtOptions` binding (`jwtSecret`, `getTokenExpiresFn`) - it never reads these environment variables itself.
|
|
201
|
+
- **`EnvironmentKeys` is the convention, not a requirement.** These constants are the conventional way for your application to supply those values into the binding.
|
|
202
|
+
- **A missing or placeholder secret fails the boot.** The component throws at startup if `jwtSecret` is missing or left at the placeholder value - wiring it from an unset environment variable fails the same way.
|
|
193
203
|
|
|
194
204
|
### Generate Strong Secrets
|
|
195
205
|
|
|
@@ -221,9 +231,11 @@ APP_ENV_JWT_EXPIRES_IN=86400
|
|
|
221
231
|
|
|
222
232
|
| Variable | Required | Default | Description |
|
|
223
233
|
|----------|----------|---------|-------------|
|
|
224
|
-
| `APP_ENV_LOGGER_FOLDER_PATH` | No |
|
|
234
|
+
| `APP_ENV_LOGGER_FOLDER_PATH` | No | _(unset)_ | Directory for log files; file logging is OFF when unset |
|
|
225
235
|
| `APP_ENV_LOGGER_FORMAT` | No | `text` | Log output format |
|
|
236
|
+
| `APP_ENV_LOGGER_LEVEL` | No | `debug` | Logger-level floor; transports without their own level inherit it |
|
|
226
237
|
| `APP_ENV_LOGGER_INSPECT_DEPTH` | No | `5` | How deep a `%s` argument is inspected. Node hard-codes depth `0` for `%s`, which prints `[Object]` one level in; IGNIS widens it. Non-negative integers only - an absent, invalid or negative value falls back to `5` |
|
|
238
|
+
| `APP_ENV_LOGGER_DO_REDACT` | No | `true` | Secret redaction in logged values. ONLY the literal `false` disables it (reveals raw credentials in log lines); anything else keeps redaction ON. Never disable in production |
|
|
227
239
|
| `APP_ENV_LOGGER_FILE_FREQUENCY` | No | `1h` | Log file rotation frequency |
|
|
228
240
|
| `APP_ENV_LOGGER_FILE_MAX_SIZE` | No | `100m` | Max size per log file |
|
|
229
241
|
| `APP_ENV_LOGGER_FILE_MAX_FILES` | No | `5d` | Log file retention |
|
|
@@ -329,7 +341,9 @@ APP_ENV_MAIL_REFRESH_TOKEN=your-oauth2-refresh-token
|
|
|
329
341
|
| `NODE_ENV` | No | `development` | Environment mode. One of `local`, `debug`, `development`, `dev`, `sit`, `uat`, `alpha`, `beta`, `staging`, `production` |
|
|
330
342
|
| `ALLOW_EMPTY_ENV_VALUE` | No | `false` | Allow empty env values |
|
|
331
343
|
|
|
332
|
-
|
|
344
|
+
- **Fail-closed by default.** An environment IGNIS does not recognize is treated as production, so error responses are sanitized.
|
|
345
|
+
- **Development environments expose error detail.** `local`, `debug`, `development`, `dev`, and `sit` are the development set - only these show internal error detail.
|
|
346
|
+
- **Everything else stays sanitized.** `alpha`, `beta`, `staging`, and `production` never expose internal detail, matching production behavior.
|
|
333
347
|
|
|
334
348
|
### Example
|
|
335
349
|
|
|
@@ -343,6 +357,28 @@ NODE_ENV=production
|
|
|
343
357
|
```
|
|
344
358
|
|
|
345
359
|
|
|
360
|
+
## Secrets & Vault
|
|
361
|
+
|
|
362
|
+
- **A `.env` file is one option, not a requirement.** IGNIS can load these variables from a vault (HashiCorp Vault, an encrypted `.env.vault`, or plain `process.env`) and **hydrate** them into the same `APP_ENV_*` keys at boot. Code that reads `process.env.APP_ENV_*` keeps working unchanged - the values simply arrive from the vault instead of a file.
|
|
363
|
+
- **Hydration runs before datasources are configured** (after `preConfigure()`, before `registerDataSources()`), so a hydrated `APP_ENV_DS_PASSWORD` is available exactly where a file-based one would be.
|
|
364
|
+
- **Vault values take precedence over `process.env`** when the provider is live - a hydrated key overwrites whatever was already in `process.env`.
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Store the key in the vault already named APP_ENV_... and it merges as-is.
|
|
368
|
+
override registerSecrets() {
|
|
369
|
+
return {
|
|
370
|
+
provider: SecretProviders.HASHICORP_VAULT,
|
|
371
|
+
config: { endpoint, auth: { method: VaultAuthMethods.APP_ROLE, roleId, secretId } },
|
|
372
|
+
hydrate: [{ path: 'secret/data/myapp/config' }],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
> [!NOTE] Failure policy
|
|
378
|
+
> If the vault is unreachable, development environments (`local`, `debug`, `development`, `dev`, `sit`) fall back to `process.env`; every other environment fails the boot rather than starting with missing secrets.
|
|
379
|
+
|
|
380
|
+
See the [Secrets & Vault guide](/guides/core-concepts/secrets-vault) for setup and the [Secrets & Vault reference](/references/base/secrets) for the full API.
|
|
381
|
+
|
|
346
382
|
## Environment-Specific Files
|
|
347
383
|
|
|
348
384
|
Create environment-specific `.env` files:
|
|
@@ -444,3 +480,11 @@ APP_ENV_LOGGER_FOLDER_PATH=./logs
|
|
|
444
480
|
# APP_ENV_MAIL_CLIENT_SECRET=
|
|
445
481
|
# APP_ENV_MAIL_REFRESH_TOKEN=
|
|
446
482
|
```
|
|
483
|
+
|
|
484
|
+
## See also
|
|
485
|
+
|
|
486
|
+
- [Configuration Reference](./index.md) - `EnvironmentKeys` constants and the `applicationEnvironment` helper
|
|
487
|
+
- [Secrets & Vault Guide](/guides/core-concepts/secrets-vault) - setup walkthrough for vault-backed secrets
|
|
488
|
+
- [Secrets & Vault Reference](/references/base/secrets) - full provider API (`registerSecrets`, `SecretProviders`, `VaultAuthMethods`)
|
|
489
|
+
- [DataSources Guide](/guides/core-concepts/persistent/datasources) - wiring `APP_ENV_POSTGRES_*` into a DataSource
|
|
490
|
+
- [Logger Helper](/extensions/helpers/logger/) - `APP_ENV_LOGGER_*` variables in depth
|