@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.
Files changed (174) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +24 -13
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +8 -4
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +247 -153
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +109 -323
  26. package/content/extensions/components/authentication/api.md +489 -602
  27. package/content/extensions/components/authentication/errors.md +135 -498
  28. package/content/extensions/components/authentication/index.md +89 -801
  29. package/content/extensions/components/authentication/usage.md +231 -955
  30. package/content/extensions/components/authorization/api.md +991 -644
  31. package/content/extensions/components/authorization/errors.md +204 -208
  32. package/content/extensions/components/authorization/getting-started.md +227 -0
  33. package/content/extensions/components/authorization/index.md +88 -795
  34. package/content/extensions/components/authorization/usage.md +219 -528
  35. package/content/extensions/components/health-check.md +77 -243
  36. package/content/extensions/components/index.md +24 -90
  37. package/content/extensions/components/mail/api.md +553 -285
  38. package/content/extensions/components/mail/errors.md +76 -62
  39. package/content/extensions/components/mail/index.md +111 -463
  40. package/content/extensions/components/mail/usage.md +139 -173
  41. package/content/extensions/components/request-tracker.md +70 -173
  42. package/content/extensions/components/socket-io/api.md +462 -785
  43. package/content/extensions/components/socket-io/errors.md +49 -51
  44. package/content/extensions/components/socket-io/index.md +69 -372
  45. package/content/extensions/components/socket-io/usage.md +212 -105
  46. package/content/extensions/components/static-asset/api.md +461 -141
  47. package/content/extensions/components/static-asset/errors.md +121 -53
  48. package/content/extensions/components/static-asset/index.md +84 -606
  49. package/content/extensions/components/static-asset/usage.md +184 -299
  50. package/content/extensions/components/template/index.md +3 -3
  51. package/content/extensions/components/websocket/api.md +311 -404
  52. package/content/extensions/components/websocket/errors.md +47 -56
  53. package/content/extensions/components/websocket/index.md +75 -407
  54. package/content/extensions/components/websocket/usage.md +120 -338
  55. package/content/extensions/helpers/cron/index.md +52 -160
  56. package/content/extensions/helpers/crypto/index.md +67 -480
  57. package/content/extensions/helpers/crypto/reference.md +528 -0
  58. package/content/extensions/helpers/env/index.md +64 -178
  59. package/content/extensions/helpers/error/index.md +286 -196
  60. package/content/extensions/helpers/index.md +61 -47
  61. package/content/extensions/helpers/inversion/index.md +76 -550
  62. package/content/extensions/helpers/inversion/reference.md +530 -0
  63. package/content/extensions/helpers/kafka/admin.md +23 -3
  64. package/content/extensions/helpers/kafka/compile-binary.md +83 -52
  65. package/content/extensions/helpers/kafka/consumer.md +65 -28
  66. package/content/extensions/helpers/kafka/examples.md +46 -253
  67. package/content/extensions/helpers/kafka/index.md +55 -617
  68. package/content/extensions/helpers/kafka/producer.md +156 -22
  69. package/content/extensions/helpers/kafka/schema-registry.md +59 -79
  70. package/content/extensions/helpers/logger/hf-logger.md +220 -0
  71. package/content/extensions/helpers/logger/index.md +78 -552
  72. package/content/extensions/helpers/logger/pino.md +105 -0
  73. package/content/extensions/helpers/logger/reference.md +937 -0
  74. package/content/extensions/helpers/network/api.md +276 -195
  75. package/content/extensions/helpers/network/index.md +87 -524
  76. package/content/extensions/helpers/queue/index.md +80 -897
  77. package/content/extensions/helpers/queue/reference.md +494 -0
  78. package/content/extensions/helpers/redis/index.md +86 -640
  79. package/content/extensions/helpers/redis/reference.md +784 -0
  80. package/content/extensions/helpers/secrets/index.md +136 -0
  81. package/content/extensions/helpers/socket-io/api.md +325 -204
  82. package/content/extensions/helpers/socket-io/index.md +79 -429
  83. package/content/extensions/helpers/storage/api.md +584 -464
  84. package/content/extensions/helpers/storage/index.md +80 -573
  85. package/content/extensions/helpers/types/index.md +75 -495
  86. package/content/extensions/helpers/types/reference.md +689 -0
  87. package/content/extensions/helpers/uid/index.md +176 -189
  88. package/content/extensions/helpers/websocket/api.md +366 -214
  89. package/content/extensions/helpers/websocket/index.md +68 -503
  90. package/content/extensions/helpers/worker-thread/index.md +62 -396
  91. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  92. package/content/extensions/index.md +38 -39
  93. package/content/extensions/src-details/mcp-server.md +96 -548
  94. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  95. package/content/guides/core-concepts/persistent/index.md +5 -1
  96. package/content/guides/core-concepts/persistent/models.md +1 -1
  97. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  98. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  99. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  100. package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
  101. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  102. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  103. package/content/guides/core-concepts/secrets-vault.md +177 -0
  104. package/content/guides/core-concepts/services.md +1 -1
  105. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  106. package/content/guides/get-started/philosophy.md +135 -670
  107. package/content/guides/get-started/setup.md +53 -74
  108. package/content/guides/migrations/redis-helpers-migration.md +5 -4
  109. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  110. package/content/guides/migrations/unified-connectors-migration.md +8 -7
  111. package/content/guides/tutorials/ecommerce-api.md +3 -8
  112. package/content/guides/tutorials/realtime-chat.md +1 -1
  113. package/content/references/base/application.md +64 -22
  114. package/content/references/base/components.md +3 -3
  115. package/content/references/base/connectors.md +79 -136
  116. package/content/references/base/controllers.md +12 -12
  117. package/content/references/base/datasources-reference.md +600 -0
  118. package/content/references/base/datasources.md +84 -444
  119. package/content/references/base/dependency-injection.md +25 -46
  120. package/content/references/base/filter-system/application-usage.md +95 -123
  121. package/content/references/base/filter-system/array-operators.md +33 -43
  122. package/content/references/base/filter-system/comparison-operators.md +55 -63
  123. package/content/references/base/filter-system/default-filter.md +144 -350
  124. package/content/references/base/filter-system/fields-order-pagination.md +116 -148
  125. package/content/references/base/filter-system/index.md +114 -253
  126. package/content/references/base/filter-system/json-filtering.md +52 -181
  127. package/content/references/base/filter-system/list-operators.md +28 -44
  128. package/content/references/base/filter-system/logical-operators.md +69 -114
  129. package/content/references/base/filter-system/null-operators.md +41 -98
  130. package/content/references/base/filter-system/pattern-matching.md +48 -51
  131. package/content/references/base/filter-system/quick-reference.md +93 -196
  132. package/content/references/base/filter-system/range-operators.md +22 -38
  133. package/content/references/base/filter-system/tips.md +70 -133
  134. package/content/references/base/filter-system/use-cases.md +173 -232
  135. package/content/references/base/grpc-controllers.md +53 -17
  136. package/content/references/base/index.md +5 -3
  137. package/content/references/base/middlewares.md +43 -28
  138. package/content/references/base/models-reference.md +886 -0
  139. package/content/references/base/models.md +81 -1452
  140. package/content/references/base/providers.md +8 -8
  141. package/content/references/base/repositories/advanced.md +281 -419
  142. package/content/references/base/repositories/index.md +84 -644
  143. package/content/references/base/repositories/mixins.md +25 -21
  144. package/content/references/base/repositories/relations.md +194 -375
  145. package/content/references/base/repositories/soft-deletable.md +67 -55
  146. package/content/references/base/secrets.md +267 -0
  147. package/content/references/base/services.md +8 -6
  148. package/content/references/configuration/environment-variables.md +73 -21
  149. package/content/references/configuration/index.md +51 -31
  150. package/content/references/index.md +1 -1
  151. package/content/references/quick-reference.md +3 -16
  152. package/content/references/utilities/crypto.md +35 -76
  153. package/content/references/utilities/date.md +33 -73
  154. package/content/references/utilities/duration.md +85 -0
  155. package/content/references/utilities/index.md +6 -2
  156. package/content/references/utilities/jsx-reference.md +298 -0
  157. package/content/references/utilities/jsx.md +82 -525
  158. package/content/references/utilities/module.md +81 -61
  159. package/content/references/utilities/parse.md +34 -64
  160. package/content/references/utilities/performance.md +33 -58
  161. package/content/references/utilities/promise.md +28 -62
  162. package/content/references/utilities/request.md +58 -218
  163. package/content/references/utilities/retry.md +139 -0
  164. package/content/references/utilities/schema.md +43 -137
  165. package/content/references/utilities/statuses-reference.md +361 -0
  166. package/content/references/utilities/statuses.md +63 -667
  167. package/dist/mcp-server/index.js +0 -0
  168. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  169. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  171. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  172. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  173. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  174. package/package.json +24 -23
@@ -6,16 +6,19 @@ difficulty: intermediate
6
6
 
7
7
  # SoftDeletableRepository
8
8
 
9
- A repository that overrides delete operations to set a `deletedAt` timestamp instead of physically removing records. Extends `DefaultCRUDRepository` with restore capabilities.
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
- **File:** `packages/core/src/connectors/postgres/repositories/core/soft-deletable.ts` (PostgreSQL connector - soft delete via `deletedAt` is a Drizzle/SQL-specific pattern, not part of the engine-neutral `AbstractRepository`)
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 Model with Soft Delete
19
+ ### 1. Define the model with a `deletedAt` column
17
20
 
18
- Use the `generateTzColumnDefs` enricher with the `deleted` option enabled to add a `deletedAt` column:
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 (`Date | null`). The `SoftDeletableRepository` requires `TSoftDeletableTableSchema` which enforces `{ deletedAt: AnyPgColumn<{ data: Date | null }> }`.
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. Create Repository
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. They internally call the corresponding `update` method (`updateById` or `updateAll`).
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 all matching records
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 all matching records
108
+ // Hard delete matching records
99
109
  const result = await repository.deleteAll({
100
110
  where: { status: 'archived' },
101
- options: { shouldHardDelete: true, force: 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 (alias for deleteAll)
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` and automatically use `shouldSkipDefaultFilter: true` to find soft-deleted records.
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 for empty where)
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
- // Alias for restoreAll
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
- All other read operations (`find`, `findOne`, `count`, `existsWith`) work as normal. The default filter (`{ deletedAt: null }`) automatically excludes soft-deleted records. Use `shouldSkipDefaultFilter: true` to include them.
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 = ?` (via `updateById` with `shouldSkipDefaultFilter: true`) |
216
- | `restoreAll` / `restoreBy` | `UPDATE SET deletedAt = NULL WHERE ...` (via `updateAll` with `shouldSkipDefaultFilter: true`) |
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 `PersistableRepository`) |
235
+ | `deleteById({ shouldHardDelete: true })` | `DELETE FROM ... WHERE id = ?` (delegates to the parent repository) |
219
236
 
220
237
  > [!TIP]
221
- > The `shouldHardDelete` option bypasses soft delete entirely and delegates to the parent `DefaultCRUDRepository`'s delete implementation, which performs a real SQL `DELETE`.
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: AnyPgColumn<{ data: Date | null }>;
265
+ deletedAt: TDeletedAtColumn;
248
266
  };
249
267
 
250
- export class SoftDeletableRepository<
268
+ export class SoftDeletableRelationalRepository<
251
269
  EntitySchema extends TSoftDeletableTableSchema = TSoftDeletableTableSchema,
252
270
  // ...
253
- > extends DefaultCRUDRepository<EntitySchema, ...> { }
271
+ > extends DefaultRelationalRepository<EntitySchema, ...> { }
254
272
  ```
255
273
 
256
- If your schema does not have a `deletedAt` column, you will get a TypeScript compilation error when extending `SoftDeletableRepository`.
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
- -> PostgresBaseRepository (connectors/postgres)
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, options: { force: true } })` |
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
- ## Next Steps
285
-
286
- - [Advanced Features](./advanced.md) - Transactions, hidden properties
287
- - [Repository Mixins (Removed)](./mixins.md) - Where default-filter and fields-visibility behavior lives now
288
- - [Repository Overview](./index.md) - Repository basics
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** | `@injectable` is NOT used on services - registration is imperative only |
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. There is no `@injectable` or `@service` decorator used on service classes in application code - `this.service()` handles everything.
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 (`this.service(UserAuditTestService)` registers `UserAuditTestService`, never `BaseTestService`), and the hard DI rule - every constructor parameter of a container-instantiated class must carry `@inject` - applies to that concrete constructor. A `scope: string` computed from `ClassName.name` is not something the container can supply anyway, 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, and the concrete subclass's constructor is left with zero parameters - nothing to decorate, nothing to violate:
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,