@venizia/ignis-docs 0.2.0 → 0.2.1-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 +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- 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 +27 -3
- 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 +8 -4
- 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 +247 -153
- 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 +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- 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/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- 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 +58 -218
- package/content/references/utilities/retry.md +139 -0
- 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/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -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-server/src/connectors/postgres/repositories/core/soft-deletable.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/soft-deletable.ts) - `SoftDeletableRelationalRepository` - delete/restore overrides, `isStrict` findById
|
|
14
|
+
- [`packages/core-server/src/connectors/postgres/repositories/core/index.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/connectors/postgres/repositories/core/index.ts) - compatibility alias `SoftDeletableRepository`
|
|
15
|
+
- [`packages/core-server/src/connectors/postgres/models/enrichers/tz.enricher.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/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 `where` is empty - a non-empty condition never needs `force`.
|
|
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.** Restore methods internally pass `shouldSkipDefaultFilter: true`, so they can find the soft-deleted records the default filter would otherwise hide. You don't 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,18 @@ const category = await repository.findById({
|
|
|
178
189
|
// Throws: [CategoryRepository][findById] Entity with id 123 not found (HTTP 404)
|
|
179
190
|
```
|
|
180
191
|
|
|
181
|
-
|
|
192
|
+
- **`isStrict` is the only thing the override adds.** Every other `findById` option - `retry`, `transaction`, `log`, `shouldSkipDefaultFilter`, `lock` - behaves exactly as on `DefaultCRUDRepository`. So does the `filter` argument.
|
|
193
|
+
- **Everything else is inherited unchanged.** `find`, `findOne`, `count`, `existsWith` behave exactly as on `DefaultCRUDRepository`.
|
|
194
|
+
- **The default filter excludes soft-deleted rows.** IGNIS applies `{ deletedAt: null }` automatically - pass `options: { shouldSkipDefaultFilter: true }` to include them.
|
|
182
195
|
|
|
196
|
+
You can combine `isStrict` with [read retry](/references/base/repositories/advanced#read-retry-replica-lag). The retry loop runs first, so a strict read waits out replica lag before it throws:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const category = await repository.findById({
|
|
200
|
+
id: '123',
|
|
201
|
+
options: { retry: { maxAttempts: 4 }, isStrict: true },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
183
204
|
|
|
184
205
|
## Options Reference
|
|
185
206
|
|
|
@@ -189,7 +210,7 @@ All other read operations (`find`, `findOne`, `count`, `existsWith`) work as nor
|
|
|
189
210
|
|--------|------|---------|-------------|
|
|
190
211
|
| `shouldHardDelete` | `boolean` | `false` | Bypass soft delete and physically remove the row |
|
|
191
212
|
| `shouldReturn` | `boolean` | `true` | Return the updated/deleted record |
|
|
192
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition (`deleteAll`/`deleteBy`) |
|
|
213
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition (`deleteAll`/`deleteBy`) |
|
|
193
214
|
| `transaction` | `ITransaction` | - | Transaction context |
|
|
194
215
|
| `log` | `{ use: boolean; level?: TLogLevel }` | - | Enable operation logging |
|
|
195
216
|
| `shouldSkipDefaultFilter` | `boolean` | `false` | Bypass the default filter |
|
|
@@ -199,27 +220,22 @@ All other read operations (`find`, `findOne`, `count`, `existsWith`) work as nor
|
|
|
199
220
|
| Option | Type | Default | Description |
|
|
200
221
|
|--------|------|---------|-------------|
|
|
201
222
|
| `shouldReturn` | `boolean` | `true` | Return the restored record |
|
|
202
|
-
| `force` | `boolean` | `false` | Allow empty `where` condition (`restoreAll`) |
|
|
223
|
+
| `force` | `boolean` | `false` | Allow an empty `where` condition (`restoreAll` only - `restoreById` has no `force` option) |
|
|
203
224
|
| `transaction` | `ITransaction` | - | Transaction context |
|
|
204
225
|
|
|
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
226
|
## How It Works
|
|
210
227
|
|
|
211
228
|
| Operation | SQL Behavior |
|
|
212
229
|
|-----------|-------------|
|
|
213
230
|
| `deleteById` | `UPDATE SET deletedAt = NOW() WHERE id = ?` (via `updateById`) |
|
|
214
231
|
| `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 ...` (
|
|
232
|
+
| `restoreById` | `UPDATE SET deletedAt = NULL WHERE id = ?` (with `shouldSkipDefaultFilter: true`) |
|
|
233
|
+
| `restoreAll` / `restoreBy` | `UPDATE SET deletedAt = NULL WHERE ...` (with `shouldSkipDefaultFilter: true`) |
|
|
217
234
|
| `find` / `findOne` / `count` | Default filter automatically adds `WHERE deletedAt IS NULL` |
|
|
218
|
-
| `deleteById({ shouldHardDelete: true })` | `DELETE FROM ... WHERE id = ?` (delegates to parent
|
|
235
|
+
| `deleteById({ shouldHardDelete: true })` | `DELETE FROM ... WHERE id = ?` (delegates to the parent repository) |
|
|
219
236
|
|
|
220
237
|
> [!TIP]
|
|
221
|
-
>
|
|
222
|
-
|
|
238
|
+
> `shouldHardDelete` bypasses soft delete entirely and delegates to the parent `DefaultCRUDRepository`'s delete implementation, which performs a real SQL `DELETE`.
|
|
223
239
|
|
|
224
240
|
## With Transactions
|
|
225
241
|
|
|
@@ -232,64 +248,60 @@ try {
|
|
|
232
248
|
options: { transaction: tx },
|
|
233
249
|
});
|
|
234
250
|
await tx.commit();
|
|
235
|
-
} catch {
|
|
251
|
+
} catch (error) {
|
|
236
252
|
await tx.rollback();
|
|
253
|
+
throw error;
|
|
237
254
|
}
|
|
238
255
|
```
|
|
239
256
|
|
|
240
|
-
|
|
241
257
|
## Type Constraint
|
|
242
258
|
|
|
243
259
|
`SoftDeletableRepository` enforces that the schema includes a `deletedAt` column at the type level:
|
|
244
260
|
|
|
245
261
|
```typescript
|
|
262
|
+
export type TDeletedAtColumn = AnyPgColumn<{ data: Date | string | null }>;
|
|
263
|
+
|
|
246
264
|
export type TSoftDeletableTableSchema = TTableSchemaWithId & {
|
|
247
|
-
deletedAt:
|
|
265
|
+
deletedAt: TDeletedAtColumn;
|
|
248
266
|
};
|
|
249
267
|
|
|
250
|
-
export class
|
|
268
|
+
export class SoftDeletableRelationalRepository<
|
|
251
269
|
EntitySchema extends TSoftDeletableTableSchema = TSoftDeletableTableSchema,
|
|
252
270
|
// ...
|
|
253
|
-
> extends
|
|
271
|
+
> extends DefaultRelationalRepository<EntitySchema, ...> { }
|
|
254
272
|
```
|
|
255
273
|
|
|
256
|
-
If your schema does not have a `deletedAt` column, you
|
|
257
|
-
|
|
274
|
+
If your schema does not have a `deletedAt` column, you get a TypeScript compilation error when extending `SoftDeletableRepository`.
|
|
258
275
|
|
|
259
276
|
## Class Hierarchy
|
|
260
277
|
|
|
278
|
+
`SoftDeletableRepository` is the friendly alias for `SoftDeletableRelationalRepository`, the last tier of the PostgreSQL repository chain:
|
|
279
|
+
|
|
261
280
|
```
|
|
262
281
|
AbstractRepository (engine-neutral, src/base)
|
|
263
|
-
->
|
|
264
|
-
-> ReadableRepository
|
|
265
|
-
-> PersistableRepository
|
|
266
|
-
-> DefaultCRUDRepository
|
|
267
|
-
-> SoftDeletableRepository <-- you are here
|
|
282
|
+
-> RelationalBaseRepository (PostgresBaseRepository)
|
|
283
|
+
-> ReadableRelationalRepository (ReadableRepository)
|
|
284
|
+
-> PersistableRelationalRepository (PersistableRepository)
|
|
285
|
+
-> DefaultRelationalRepository (DefaultCRUDRepository)
|
|
286
|
+
-> SoftDeletableRelationalRepository (SoftDeletableRepository) <-- you are here
|
|
268
287
|
```
|
|
269
288
|
|
|
270
|
-
|
|
271
289
|
## Quick Reference
|
|
272
290
|
|
|
273
291
|
| Want to... | Code |
|
|
274
292
|
|------------|------|
|
|
275
293
|
| Soft delete by ID | `repository.deleteById({ id })` |
|
|
276
294
|
| Hard delete by ID | `repository.deleteById({ id, options: { shouldHardDelete: true } })` |
|
|
277
|
-
| Soft delete by condition | `repository.deleteAll({ where
|
|
295
|
+
| Soft delete by condition | `repository.deleteAll({ where })` |
|
|
278
296
|
| Restore by ID | `repository.restoreById({ id })` |
|
|
279
297
|
| Restore by condition | `repository.restoreAll({ where })` |
|
|
280
298
|
| Find including deleted | `repository.find({ filter, options: { shouldSkipDefaultFilter: true } })` |
|
|
281
299
|
| Strict findById (404) | `repository.findById({ id, options: { isStrict: true } })` |
|
|
282
300
|
|
|
301
|
+
## See also
|
|
283
302
|
|
|
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
|
|
303
|
+
- [Repositories overview](/references/base/repositories/) - CRUD basics, common tasks
|
|
304
|
+
- [Advanced Features](./advanced) - transactions, hidden properties, performance
|
|
305
|
+
- [Repository Mixins (Removed)](./mixins) - where default-filter and fields-visibility behavior lives now
|
|
306
|
+
- [Default Filter](/references/base/filter-system/default-filter) - configuring `@model` default filters
|
|
307
|
+
- [Models - Full Reference](/references/base/models-reference) - `@model`, entity hierarchy, schema enrichers
|
|
@@ -0,0 +1,267 @@
|
|
|
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. It covers 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-server/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`). That's 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 instead of 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-server/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`. It 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.
|
|
201
|
+
- **On renewal fire** - `renewRaw()` extends the same lease. On renew failure or max-TTL, `fetchRaw()` mints a fresh lease and a single rotation is dispatched instead.
|
|
202
|
+
- **Rotation dispatch** - `onRotate` handlers run first. The registered rotatables for that key then run **in series**; a throwing consumer is logged and does not abort the others.
|
|
203
|
+
- **Shutdown** - clears every timer and revokes every lease via `revokeRaw()`.
|
|
204
|
+
|
|
205
|
+
The scheduler, cache, and clock are injectable, so the machinery is tested deterministically without real timers or a live vault.
|
|
206
|
+
|
|
207
|
+
## HashiCorp Vault Provider
|
|
208
|
+
|
|
209
|
+
**File:** `packages/helpers/src/modules/secrets/hashicorp/hashicorp.helper.ts`
|
|
210
|
+
|
|
211
|
+
- **Auth** - a Zod discriminated union on `method`: `token` (`token`), `app-role` (`roleId` + `secretId`, optional `mountPath`), `kubernetes` (`role`, optional `jwtPath` and `mountPath`). `configure()` logs in and stores the Vault token. `mountPath` defaults to the method name - `approle` and `kubernetes`.
|
|
212
|
+
- **KV v2** - `getBundle()` unwraps the KV-v2 `.data.data` envelope automatically.
|
|
213
|
+
- **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.
|
|
214
|
+
- **Token self-renewal** - the Vault auth token has its own TTL. The provider schedules the token for renewal in the same cadence as leases.
|
|
215
|
+
- **Re-auth on expiry** - if the token can no longer be renewed, the provider re-runs the login flow, so AppRole / Kubernetes deployments survive past the token TTL without a restart.
|
|
216
|
+
|
|
217
|
+
`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 (`ModuleUtility.load`), so importing the root package never requires it. `Bun.build`-compiled applications need no `external: ['node-vault']` workaround.
|
|
218
|
+
|
|
219
|
+
An application that does use this provider and compiles a binary must ship `node-vault` in `node_modules` next to the binary. Alternatively, inject a ready-made `client` through the helper options, or import `node-vault` statically and hand it over with `ModuleUtility.register({ modules: { 'node-vault': nodeVault } })` at startup.
|
|
220
|
+
|
|
221
|
+
## Dotenv Vault Provider
|
|
222
|
+
|
|
223
|
+
**File:** `packages/helpers/src/modules/secrets/dotenv/dotenv.helper.ts`
|
|
224
|
+
|
|
225
|
+
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.
|
|
226
|
+
|
|
227
|
+
## Rotation and the Soft-Evict Contract
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
The PostgreSQL datasource implements `onSecretRotated()` on `AbstractRelationalDataSource`:
|
|
232
|
+
|
|
233
|
+
1. Capture the current pool.
|
|
234
|
+
2. Apply the new credentials onto `this.settings` (`{ username, password }` maps to pg's `{ user, password }`).
|
|
235
|
+
3. Clear the driver / connector / client, re-run `configure()` to build a fresh pool, and re-wire the driver.
|
|
236
|
+
4. Drain the old pool with `end()`. It resolves once checked-out clients are released, so in-flight transactions finish on the old pool while new work uses the new one.
|
|
237
|
+
|
|
238
|
+
> [!WARNING] configure() must read from this.settings
|
|
239
|
+
> 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.
|
|
240
|
+
|
|
241
|
+
## Consuming the Provider
|
|
242
|
+
|
|
243
|
+
Any provider-fed code injects the singleton at `@app/config`.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import type { ISecretsHelper } from '@venizia/ignis-helpers';
|
|
247
|
+
|
|
248
|
+
@service()
|
|
249
|
+
export class PaymentService {
|
|
250
|
+
constructor(
|
|
251
|
+
@inject({ key: '@app/config' }) private secrets: ISecretsHelper,
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
async charge() {
|
|
255
|
+
const apiKey = await this.secrets.get({ path: 'secret/data/myapp/stripe', key: 'apiKey' });
|
|
256
|
+
// ...
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
For a step-by-step walkthrough of enabling each provider, see the [Secrets & Vault guide](/guides/core-concepts/secrets-vault).
|
|
262
|
+
|
|
263
|
+
## See Also
|
|
264
|
+
|
|
265
|
+
- [Secrets & Vault guide](/guides/core-concepts/secrets-vault) - practical setup for each provider
|
|
266
|
+
- [DataSources](./datasources) - the pool that rotation rebuilds
|
|
267
|
+
- [Environment Variables](/references/configuration/environment-variables) - where hydrated secrets land
|
|
@@ -8,7 +8,7 @@ difficulty: beginner
|
|
|
8
8
|
|
|
9
9
|
Technical reference for `BaseService` - the foundation for the business logic layer in IGNIS.
|
|
10
10
|
|
|
11
|
-
**File:** `packages/core/src/base/services/base.ts`
|
|
11
|
+
**File:** `packages/core-server/src/base/services/base.ts`
|
|
12
12
|
|
|
13
13
|
## Quick Reference
|
|
14
14
|
|
|
@@ -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
|
---
|
|
@@ -29,7 +29,7 @@ Technical reference for `BaseService` - the foundation for the business logic la
|
|
|
29
29
|
Abstract class that all application services must extend.
|
|
30
30
|
|
|
31
31
|
```typescript
|
|
32
|
-
// packages/core/src/base/services/base.ts
|
|
32
|
+
// packages/core-server/src/base/services/base.ts
|
|
33
33
|
import { BaseHelper } from '@venizia/ignis-helpers';
|
|
34
34
|
import { IService } from './types';
|
|
35
35
|
|
|
@@ -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())
|
|
@@ -65,7 +65,7 @@ this.service(GreeterService); // binds as 'services.GreeterService'
|
|
|
65
65
|
`this.service(Ctor)` is implemented directly on `BaseApplication`:
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
|
-
// packages/core/src/base/applications/base.ts
|
|
68
|
+
// packages/core-server/src/base/applications/base.ts
|
|
69
69
|
service<Base extends IService, Args extends AnyObject = any>(
|
|
70
70
|
ctor: TClass<Base>,
|
|
71
71
|
opts?: TMixinOpts<Args>,
|
|
@@ -244,7 +244,9 @@ this.service(OrderService);
|
|
|
244
244
|
|
|
245
245
|
### Abstract Base Services
|
|
246
246
|
|
|
247
|
-
For shared dependencies across multiple related services, define an abstract base. The container only ever `instantiate()`s the **concrete** class
|
|
247
|
+
For shared dependencies across multiple related services, define an abstract base. The container only ever `instantiate()`s the **concrete** class - `this.service(UserAuditTestService)` registers `UserAuditTestService`, never `BaseTestService`. The hard DI rule - every constructor parameter of a container-instantiated class must carry `@inject` - applies to that concrete constructor.
|
|
248
|
+
|
|
249
|
+
A `scope: string` computed from `ClassName.name` is not something the container can supply. So it cannot sit as a bare constructor parameter next to an `@inject`-decorated one. The shared repository is injected as a **property** on the base instead. The concrete subclass's constructor is left with zero parameters - nothing to decorate, nothing to violate:
|
|
248
250
|
|
|
249
251
|
```typescript
|
|
250
252
|
// Shared repository access for a group of test services - property injection,
|