@venizia/ignis-docs 0.0.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. package/content/references/base/middleware.md +0 -347
@@ -1,6 +1,6 @@
1
1
  # Performance Optimization
2
2
 
3
- Optimize your Ignis application for speed and scalability.
3
+ Optimize your IGNIS application for speed and scalability.
4
4
 
5
5
  ## 1. Measure Performance
6
6
 
@@ -46,7 +46,7 @@ Prevent blocking the event loop with Worker Threads:
46
46
 
47
47
  ### Query Operators Reference
48
48
 
49
- Ignis supports extensive query operators for filtering:
49
+ IGNIS supports extensive query operators for filtering:
50
50
 
51
51
  | Operator | Description | Example |
52
52
  |----------|-------------|---------|
@@ -68,7 +68,7 @@ Ignis supports extensive query operators for filtering:
68
68
  **Complex Filter Example:**
69
69
 
70
70
  ```typescript
71
- await repo.find({
71
+ await repository.find({
72
72
  filter: {
73
73
  where: {
74
74
  and: [
@@ -87,22 +87,22 @@ await repo.find({
87
87
 
88
88
  ### JSON Path Filtering
89
89
 
90
- Filter by nested JSON/JSONB fields using PostgreSQL's `#>` operator:
90
+ Filter and sort by nested JSON/JSONB fields using PostgreSQL path extraction:
91
91
 
92
92
  ```typescript
93
93
  // Order by nested JSON path
94
- await repo.find({
94
+ await repository.find({
95
95
  filter: {
96
96
  order: ['metadata.nested[0].field ASC'],
97
97
  },
98
98
  });
99
99
 
100
- // The framework uses PostgreSQL #> operator for path extraction
101
- // metadata #> '{nested,0,field}'
100
+ // Ordering uses the #> operator: metadata #> '{nested,0,field}'
101
+ // Where clauses on JSON paths use #>> (text extraction)
102
102
  ```
103
103
 
104
104
  > [!TIP]
105
- > **Avoid Deep Nesting:** While Ignis supports deeply nested `include` filters, each level adds significant overhead to query construction and result mapping. We strongly recommend a **maximum of 2 levels** (e.g., `User -> Orders -> Items`). For more complex data fetching, consider separate queries.
105
+ > **Avoid Deep Nesting:** While IGNIS supports deeply nested `include` filters, each level adds significant overhead to query construction and result mapping. We strongly recommend a **maximum of 2 levels** (e.g., `User -> Orders -> Items`). For more complex data fetching, consider separate queries.
106
106
 
107
107
  **Example:**
108
108
  ```typescript
@@ -132,11 +132,15 @@ Reduce database load with caching:
132
132
 
133
133
  **Example:**
134
134
  ```typescript
135
- // Cache expensive query results
136
- const cached = await redis.get('users:active');
135
+ // Cache expensive query results (RedisHelper uses an options-object API)
136
+ const cached = await redis.getObject<TUser[]>({ key: 'users:active' });
137
137
  if (!cached) {
138
- const users = await userRepository.find({ where: { active: true } });
139
- await redis.set('users:active', users, 300); // 5 min TTL
138
+ const users = await userRepository.find({ filter: { where: { active: true } } });
139
+ await redis.set({
140
+ key: 'users:active',
141
+ value: users,
142
+ options: { expiresIn: 5 * 60 * 1000 }, // 5 min TTL (milliseconds)
143
+ });
140
144
  }
141
145
  ```
142
146
 
@@ -203,14 +207,20 @@ Connection pooling significantly improves performance by reusing database connec
203
207
 
204
208
  ```typescript
205
209
  import { Pool } from 'pg';
206
- import { drizzle } from 'drizzle-orm/node-postgres';
207
-
208
- export class PostgresDataSource extends AbstractDataSource {
209
- override connect(): void {
210
- const pool = new Pool({
210
+ import { datasource } from '@venizia/ignis';
211
+ import { BasePostgresDataSource } from '@venizia/ignis/postgres';
212
+ import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
213
+
214
+ // IDataSourceConfigs: your settings interface (host/port/user/password/database)
215
+ @datasource({ driver: NodePostgresDriver })
216
+ export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
217
+ override configure(): void {
218
+ // Keep the pool on `this.client` - NodePostgresDriver above wires the driver and Drizzle
219
+ // connector from it lazily, on first getConnector()/beginTransaction()
220
+ this.client = new Pool({
211
221
  host: this.settings.host,
212
222
  port: this.settings.port,
213
- user: this.settings.username,
223
+ user: this.settings.user,
214
224
  password: this.settings.password,
215
225
  database: this.settings.database,
216
226
 
@@ -221,8 +231,6 @@ export class PostgresDataSource extends AbstractDataSource {
221
231
  connectionTimeoutMillis: 5000, // Fail if can't connect in 5s
222
232
  maxUses: 7500, // Close connection after 7500 queries
223
233
  });
224
-
225
- this.connector = drizzle({ client: pool, schema: this.schema });
226
234
  }
227
235
  }
228
236
  ```
@@ -282,14 +290,14 @@ export const User = pgTable('User', {
282
290
  ### Avoid N+1 Queries
283
291
 
284
292
  ```typescript
285
- // ❌ BAD - N+1 queries
286
- const users = await userRepo.find({ filter: { limit: 100 } });
287
- for (const user of users.data) {
288
- user.posts = await postRepo.find({ filter: { where: { authorId: user.id } } });
293
+ // ❌ BAD - N+1 queries (find returns a plain array)
294
+ const users = await userRepository.find({ filter: { limit: 100 } });
295
+ for (const user of users) {
296
+ user.posts = await postRepository.find({ filter: { where: { authorId: user.id } } });
289
297
  }
290
298
 
291
299
  // ✅ GOOD - Single query with relations
292
- const users = await userRepo.find({
300
+ const users = await userRepository.find({
293
301
  filter: {
294
302
  limit: 100,
295
303
  include: [{ relation: 'posts' }],
@@ -302,11 +310,11 @@ const users = await userRepo.find({
302
310
  ```typescript
303
311
  // ❌ BAD - Many individual inserts
304
312
  for (const item of items) {
305
- await repo.create({ data: item });
313
+ await repository.create({ data: item });
306
314
  }
307
315
 
308
316
  // ✅ GOOD - Batch insert
309
- await repo.createMany({ data: items });
317
+ await repository.createAll({ data: items });
310
318
  ```
311
319
 
312
320
  ## 9. Memory Management
@@ -315,7 +323,7 @@ await repo.createMany({ data: items });
315
323
 
316
324
  ```typescript
317
325
  // ❌ BAD - Load all records into memory
318
- const allUsers = await userRepo.find({ filter: { limit: 100000 } });
326
+ const allUsers = await userRepository.find({ filter: { limit: 100000 } });
319
327
 
320
328
  // ✅ GOOD - Process in batches
321
329
  const batchSize = 1000;
@@ -323,15 +331,16 @@ let offset = 0;
323
331
  let hasMore = true;
324
332
 
325
333
  while (hasMore) {
326
- const batch = await userRepo.find({
334
+ // find returns a plain array
335
+ const batch = await userRepository.find({
327
336
  filter: { limit: batchSize, offset },
328
337
  });
329
338
 
330
- for (const user of batch.data) {
339
+ for (const user of batch) {
331
340
  await processUser(user);
332
341
  }
333
342
 
334
- hasMore = batch.data.length === batchSize;
343
+ hasMore = batch.length === batchSize;
335
344
  offset += batchSize;
336
345
  }
337
346
  ```
@@ -1,6 +1,6 @@
1
1
  # Security Guidelines
2
2
 
3
- Critical security practices to protect your Ignis application.
3
+ Critical security practices to protect your IGNIS application.
4
4
 
5
5
  ## 1. Secret Management
6
6
 
@@ -23,9 +23,27 @@ APP_ENV_POSTGRES_PASSWORD=database_password_here
23
23
  node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
24
24
  ```
25
25
 
26
+ ### Redaction at Log Time
27
+
28
+ Never hard-coding a secret is not enough on its own -- an options object holding one still ends up in a log line the moment it is passed to `logger.info('...: %s', opts)`. `@venizia/ignis-helpers` provides two primitives for exactly this:
29
+
30
+ | Function | Use For | Behavior |
31
+ |----------|---------|----------|
32
+ | `redactSecrets(value)` | Any object/array being logged | Recursively replaces every value whose key matches a secret-looking name (`password`, `token`, `apiKey`, `authorization`, HTTP header spellings like `x-api-key`/`cookie`/`proxy-authorization`, etc., case-insensitive) with `'[REDACTED]'` |
33
+ | `redactUrlCredentials(url)` | A connection/broker URL string | Strips the password out of a URL's authority section (`mqtts://user:hunter2@broker:8883` becomes `mqtts://user:[REDACTED]@broker:8883`); a value that doesn't parse as a URL is returned unchanged |
34
+
35
+ ```typescript
36
+ import { redactSecrets, redactUrlCredentials } from '@venizia/ignis-helpers';
37
+
38
+ this.logger.info('[connect] Options: %s', redactSecrets(connectionOptions));
39
+ this.logger.info('[connect] Broker: %s', redactUrlCredentials(brokerUrl));
40
+ ```
41
+
42
+ These are the primitives the framework itself uses -- outbound HTTP request configs (`NodeFetcher`/`AxiosFetcher`, see [Network Helper](/extensions/helpers/network/)) and MQTT broker URLs (`MQTTClientHelper`, see [Queue Helper](/extensions/helpers/queue/)) are both redacted this way before they reach a log line.
43
+
26
44
  ## 2. Input Validation
27
45
 
28
- **Always validate incoming data** with Zod schemas. Ignis automatically rejects invalid requests.
46
+ **Always validate incoming data** with Zod schemas. IGNIS automatically rejects invalid requests.
29
47
 
30
48
  ```typescript
31
49
  import { z } from '@hono/zod-openapi';
@@ -63,7 +81,7 @@ this.component(AuthenticateComponent);
63
81
  ```typescript
64
82
  const SecureRoute = {
65
83
  path: '/admin/users',
66
- authStrategies: [Authentication.STRATEGY_JWT], // Requires JWT
84
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] }, // Requires JWT
67
85
  // ...
68
86
  };
69
87
  ```
@@ -73,10 +91,10 @@ const SecureRoute = {
73
91
  **Access user in protected routes:**
74
92
  ```typescript
75
93
  import { Authentication, IJWTTokenPayload } from '@venizia/ignis';
76
- import { ApplicationError, getError } from '@venizia/ignis-helpers';
94
+ import { getError } from '@venizia/ignis-helpers';
77
95
 
78
96
  const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload;
79
- if (!user.roles.includes('admin')) {
97
+ if (!user.roles.some(role => role.identifier === 'admin')) {
80
98
  throw getError({ statusCode: 403, message: 'Forbidden' });
81
99
  }
82
100
  ```
@@ -92,7 +110,7 @@ Configure model properties that should **never be returned** through repository
92
110
  hiddenProperties: ['password', 'apiSecret', 'internalToken'],
93
111
  },
94
112
  })
95
- export class User extends BaseEntity<typeof User.schema> {
113
+ export class User extends BasePostgresEntity<typeof User.schema> {
96
114
  static override schema = pgTable('User', {
97
115
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
98
116
  email: text('email').notNull(),
@@ -113,7 +131,7 @@ export class User extends BaseEntity<typeof User.schema> {
113
131
 
114
132
  ```typescript
115
133
  // For authentication - access password via connector
116
- const connector = userRepo.getConnector();
134
+ const connector = userRepository.getConnector();
117
135
  const [user] = await connector
118
136
  .select({ id: User.schema.id, password: User.schema.password })
119
137
  .from(User.schema)
@@ -227,14 +245,14 @@ Configure Cross-Origin Resource Sharing to control which domains can access your
227
245
  import { cors } from 'hono/cors';
228
246
 
229
247
  // Allow all origins (ONLY for development)
230
- this.server.use('*', cors());
248
+ this.getServer().use('*', cors());
231
249
  ```
232
250
 
233
251
  **Production (Restrictive):**
234
252
  ```typescript
235
253
  import { cors } from 'hono/cors';
236
254
 
237
- this.server.use('/api/*', cors({
255
+ this.getServer().use('/api/*', cors({
238
256
  origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
239
257
  allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
240
258
  allowHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
@@ -246,7 +264,7 @@ this.server.use('/api/*', cors({
246
264
 
247
265
  **Dynamic Origin Validation:**
248
266
  ```typescript
249
- this.server.use('/api/*', cors({
267
+ this.getServer().use('/api/*', cors({
250
268
  origin: (origin) => {
251
269
  const allowedDomains = ['yourdomain.com', 'yourdomain.io'];
252
270
  try {
@@ -296,9 +314,10 @@ const rateLimiter = (opts: { windowMs: number; max: number }) => {
296
314
  };
297
315
 
298
316
  // Apply to sensitive endpoints
299
- this.server.use('/api/auth/login', rateLimiter({ windowMs: 15 * 60 * 1000, max: 5 }));
300
- this.server.use('/api/auth/register', rateLimiter({ windowMs: 60 * 60 * 1000, max: 10 }));
301
- this.server.use('/api/*', rateLimiter({ windowMs: 60 * 1000, max: 100 }));
317
+ const server = this.getServer();
318
+ server.use('/api/auth/login', rateLimiter({ windowMs: 15 * 60 * 1000, max: 5 }));
319
+ server.use('/api/auth/register', rateLimiter({ windowMs: 60 * 60 * 1000, max: 10 }));
320
+ server.use('/api/*', rateLimiter({ windowMs: 60 * 1000, max: 100 }));
302
321
  ```
303
322
 
304
323
  **Recommended Limits:**
@@ -313,16 +332,22 @@ this.server.use('/api/*', rateLimiter({ windowMs: 60 * 1000, max: 100 }));
313
332
  **Production Recommendation:** Use Redis-backed rate limiting for distributed deployments:
314
333
 
315
334
  ```typescript
316
- import { RedisHelper } from '@venizia/ignis-helpers';
335
+ import { RedisSingleHelper } from '@venizia/ignis-helpers';
317
336
 
318
337
  // Rate limiter with Redis for multi-instance deployments
319
- const distributedRateLimiter = async (key: string, max: number, windowSec: number) => {
320
- const redis = RedisHelper.getClient();
321
- const current = await redis.incr(key);
338
+ const redisHelper = new RedisSingleHelper({
339
+ name: 'rate-limiter',
340
+ host: process.env.APP_ENV_REDIS_HOST ?? 'localhost',
341
+ port: process.env.APP_ENV_REDIS_PORT ?? '6379',
342
+ password: process.env.APP_ENV_REDIS_PASSWORD,
343
+ });
344
+
345
+ const distributedRateLimiter = async (opts: { key: string; max: number; windowSeconds: number }) => {
346
+ const current = await redisHelper.incr({ key: opts.key });
322
347
  if (current === 1) {
323
- await redis.expire(key, windowSec);
348
+ await redisHelper.expire({ key: opts.key, seconds: opts.windowSeconds });
324
349
  }
325
- return current <= max;
350
+ return current <= opts.max;
326
351
  };
327
352
  ```
328
353
 
@@ -369,7 +394,7 @@ Add security headers to protect against common attacks:
369
394
  import { secureHeaders } from 'hono/secure-headers';
370
395
 
371
396
  // Add security headers to all responses
372
- this.server.use('*', secureHeaders({
397
+ this.getServer().use('*', secureHeaders({
373
398
  // Prevent clickjacking
374
399
  xFrameOptions: 'DENY',
375
400
  // Prevent MIME type sniffing
@@ -396,13 +421,13 @@ Prevent denial of service through large payloads:
396
421
  import { bodyLimit } from 'hono/body-limit';
397
422
 
398
423
  // Limit request body size
399
- this.server.use('/api/*', bodyLimit({
424
+ this.getServer().use('/api/*', bodyLimit({
400
425
  maxSize: 1024 * 1024, // 1MB for general API
401
426
  onError: (c) => c.json({ message: 'Request body too large' }, 413),
402
427
  }));
403
428
 
404
429
  // Allow larger uploads for file endpoints
405
- this.server.use('/api/upload/*', bodyLimit({
430
+ this.getServer().use('/api/upload/*', bodyLimit({
406
431
  maxSize: 50 * 1024 * 1024, // 50MB for file uploads
407
432
  }));
408
433
  ```
@@ -413,13 +438,14 @@ Log security-relevant events for monitoring and forensics:
413
438
 
414
439
  ```typescript
415
440
  import { BaseService } from '@venizia/ignis';
441
+ import { getError } from '@venizia/ignis-helpers';
416
442
 
417
443
  export class AuthService extends BaseService {
418
444
  async login(email: string, password: string, context: Context) {
419
445
  const ip = context.req.header('x-forwarded-for') ?? 'unknown';
420
446
  const userAgent = context.req.header('user-agent') ?? 'unknown';
421
447
 
422
- const user = await this.userRepo.findByEmail(email);
448
+ const user = await this.userRepository.findByEmail(email);
423
449
 
424
450
  if (!user || !await this.verifyPassword(password, user.password)) {
425
451
  // Log failed attempt
@@ -444,6 +470,9 @@ export class AuthService extends BaseService {
444
470
  - Suspicious activity (rate limit hits, invalid tokens)
445
471
  - Admin actions
446
472
 
473
+ > [!NOTE]
474
+ > When logging a request or connection config that might carry credentials, wrap it in `redactSecrets()` (or `redactUrlCredentials()` for a URL) rather than logging it raw -- see [Redaction at Log Time](#redaction-at-log-time) above. This is what the framework's own outbound HTTP and MQTT logging already does automatically.
475
+
447
476
  ## Security Checklist
448
477
 
449
478
  Before deploying to production, verify:
@@ -1,6 +1,6 @@
1
1
  # Testing Strategies
2
2
 
3
- Comprehensive testing guide for Ignis applications using Bun's built-in test runner.
3
+ Comprehensive testing guide for IGNIS applications using Bun's built-in test runner.
4
4
 
5
5
  ## Testing Philosophy
6
6
 
@@ -46,13 +46,16 @@ afterAll(async () => {
46
46
 
47
47
  **`test/helpers/test-database.ts`:**
48
48
  ```typescript
49
+ import { sql } from 'drizzle-orm';
49
50
  import { drizzle } from 'drizzle-orm/node-postgres';
50
51
  import { Pool } from 'pg';
52
+ import { PostgresDataSource } from '@/datasources';
51
53
  import * as schema from '@/models';
52
54
 
53
55
  export class TestDatabase {
54
56
  private static pool: Pool;
55
57
  private static db: ReturnType<typeof drizzle>;
58
+ private static dataSource: PostgresDataSource;
56
59
 
57
60
  static async initialize() {
58
61
  this.pool = new Pool({
@@ -63,12 +66,20 @@ export class TestDatabase {
63
66
  database: process.env.TEST_DB_NAME ?? 'ignis_test',
64
67
  });
65
68
  this.db = drizzle({ client: this.pool, schema });
69
+
70
+ // Application DataSource - repositories take a DataSource, not a raw Drizzle instance
71
+ this.dataSource = new PostgresDataSource();
72
+ await this.dataSource.configure();
66
73
  }
67
74
 
68
75
  static getDb() {
69
76
  return this.db;
70
77
  }
71
78
 
79
+ static getDataSource() {
80
+ return this.dataSource;
81
+ }
82
+
72
83
  static async truncateAll() {
73
84
  const tables = Object.keys(schema);
74
85
  for (const table of tables) {
@@ -116,19 +127,20 @@ import type { IUserRepository } from '@/repositories';
116
127
 
117
128
  describe('UserService', () => {
118
129
  let service: UserService;
119
- let mockRepo: IUserRepository;
130
+ let mockRepository: IUserRepository;
120
131
 
121
132
  beforeEach(() => {
122
133
  // Create mock repository
123
- mockRepo = {
124
- findById: mock(() => Promise.resolve({ data: null })),
125
- findOne: mock(() => Promise.resolve({ data: null })),
126
- create: mock((opts) => Promise.resolve({ data: { id: 'new-id', ...opts.data }, count: 1 })),
127
- updateById: mock(() => Promise.resolve({ data: null, count: 0 })),
134
+ // find/findOne/findById return the rows directly; create/updateById return { count, data }
135
+ mockRepository = {
136
+ findById: mock(() => Promise.resolve(null)),
137
+ findOne: mock(() => Promise.resolve(null)),
138
+ create: mock((opts) => Promise.resolve({ count: 1, data: { id: 'new-id', ...opts.data } })),
139
+ updateById: mock(() => Promise.resolve({ count: 0, data: null })),
128
140
  } as unknown as IUserRepository;
129
141
 
130
142
  // Inject mock
131
- service = new UserService(mockRepo);
143
+ service = new UserService(mockRepository);
132
144
  });
133
145
 
134
146
  describe('createUser', () => {
@@ -142,13 +154,13 @@ describe('UserService', () => {
142
154
  email: 'test@example.com',
143
155
  name: 'Test User',
144
156
  });
145
- expect(mockRepo.create).toHaveBeenCalledTimes(1);
157
+ expect(mockRepository.create).toHaveBeenCalledTimes(1);
146
158
  });
147
159
 
148
160
  it('should throw error for duplicate email', async () => {
149
- mockRepo.findOne = mock(() => Promise.resolve({
150
- data: { id: 'existing', email: 'test@example.com' },
151
- }));
161
+ mockRepository.findOne = mock(() =>
162
+ Promise.resolve({ id: 'existing', email: 'test@example.com' }),
163
+ );
152
164
 
153
165
  await expect(
154
166
  service.createUser({ email: 'test@example.com', name: 'Test' })
@@ -160,7 +172,7 @@ describe('UserService', () => {
160
172
 
161
173
  await service.createUser(userData);
162
174
 
163
- const createCall = (mockRepo.create as ReturnType<typeof mock>).mock.calls[0][0];
175
+ const createCall = (mockRepository.create as ReturnType<typeof mock>).mock.calls[0][0];
164
176
  expect(createCall.data.password).not.toBe('secret123');
165
177
  expect(createCall.data.password).toMatch(/^\$2[aby]?\$/); // bcrypt hash
166
178
  });
@@ -168,7 +180,7 @@ describe('UserService', () => {
168
180
 
169
181
  describe('updateUser', () => {
170
182
  it('should throw NotFound when user does not exist', async () => {
171
- mockRepo.findById = mock(() => Promise.resolve({ data: null }));
183
+ mockRepository.findById = mock(() => Promise.resolve(null));
172
184
 
173
185
  await expect(
174
186
  service.updateUser('nonexistent', { name: 'New Name' })
@@ -176,17 +188,17 @@ describe('UserService', () => {
176
188
  });
177
189
 
178
190
  it('should only update provided fields', async () => {
179
- mockRepo.findById = mock(() => Promise.resolve({
180
- data: { id: '1', email: 'old@test.com', name: 'Old Name' },
181
- }));
182
- mockRepo.updateById = mock((opts) => Promise.resolve({
183
- data: { ...opts.data, id: opts.id },
191
+ mockRepository.findById = mock(() =>
192
+ Promise.resolve({ id: '1', email: 'old@test.com', name: 'Old Name' }),
193
+ );
194
+ mockRepository.updateById = mock((opts) => Promise.resolve({
184
195
  count: 1,
196
+ data: { ...opts.data, id: opts.id },
185
197
  }));
186
198
 
187
199
  await service.updateUser('1', { name: 'New Name' });
188
200
 
189
- const updateCall = (mockRepo.updateById as ReturnType<typeof mock>).mock.calls[0][0];
201
+ const updateCall = (mockRepository.updateById as ReturnType<typeof mock>).mock.calls[0][0];
190
202
  expect(updateCall.data).toEqual({ name: 'New Name' });
191
203
  expect(updateCall.data.email).toBeUndefined();
192
204
  });
@@ -206,11 +218,11 @@ import { TestDatabase } from '@test/helpers/test-database';
206
218
  import { User } from '@/models';
207
219
 
208
220
  describe('UserRepository', () => {
209
- let repo: UserRepository;
221
+ let repository: UserRepository;
210
222
 
211
223
  beforeEach(async () => {
212
- const db = TestDatabase.getDb();
213
- repo = new UserRepository(db);
224
+ // Repositories take the application DataSource, not a raw Drizzle instance
225
+ repository = new UserRepository(TestDatabase.getDataSource());
214
226
  });
215
227
 
216
228
  afterEach(async () => {
@@ -219,7 +231,7 @@ describe('UserRepository', () => {
219
231
 
220
232
  describe('create', () => {
221
233
  it('should create a user and return with generated id', async () => {
222
- const result = await repo.create({
234
+ const result = await repository.create({
223
235
  data: { email: 'test@example.com', name: 'Test User' },
224
236
  });
225
237
 
@@ -232,12 +244,12 @@ describe('UserRepository', () => {
232
244
  });
233
245
 
234
246
  it('should enforce unique email constraint', async () => {
235
- await repo.create({
247
+ await repository.create({
236
248
  data: { email: 'test@example.com', name: 'First' },
237
249
  });
238
250
 
239
251
  await expect(
240
- repo.create({ data: { email: 'test@example.com', name: 'Second' } })
252
+ repository.create({ data: { email: 'test@example.com', name: 'Second' } })
241
253
  ).rejects.toThrow(); // Unique constraint violation
242
254
  });
243
255
  });
@@ -245,7 +257,7 @@ describe('UserRepository', () => {
245
257
  describe('find', () => {
246
258
  beforeEach(async () => {
247
259
  // Seed test data
248
- await repo.createMany({
260
+ await repository.createAll({
249
261
  data: [
250
262
  { email: 'alice@test.com', name: 'Alice', status: 'ACTIVE' },
251
263
  { email: 'bob@test.com', name: 'Bob', status: 'ACTIVE' },
@@ -255,31 +267,32 @@ describe('UserRepository', () => {
255
267
  });
256
268
 
257
269
  it('should filter by status', async () => {
258
- const result = await repo.find({
270
+ // find() returns the rows directly (an array)
271
+ const users = await repository.find({
259
272
  filter: { where: { status: 'ACTIVE' } },
260
273
  });
261
274
 
262
- expect(result.data).toHaveLength(2);
263
- expect(result.data.map(u => u.name)).toContain('Alice');
264
- expect(result.data.map(u => u.name)).toContain('Bob');
275
+ expect(users).toHaveLength(2);
276
+ expect(users.map(user => user.name)).toContain('Alice');
277
+ expect(users.map(user => user.name)).toContain('Bob');
265
278
  });
266
279
 
267
280
  it('should support pagination', async () => {
268
- const page1 = await repo.find({
281
+ const page1 = await repository.find({
269
282
  filter: { limit: 2, offset: 0, order: ['name ASC'] },
270
283
  });
271
- const page2 = await repo.find({
284
+ const page2 = await repository.find({
272
285
  filter: { limit: 2, offset: 2, order: ['name ASC'] },
273
286
  });
274
287
 
275
- expect(page1.data).toHaveLength(2);
276
- expect(page1.data[0].name).toBe('Alice');
277
- expect(page2.data).toHaveLength(1);
278
- expect(page2.data[0].name).toBe('Charlie');
288
+ expect(page1).toHaveLength(2);
289
+ expect(page1[0].name).toBe('Alice');
290
+ expect(page2).toHaveLength(1);
291
+ expect(page2[0].name).toBe('Charlie');
279
292
  });
280
293
 
281
294
  it('should support complex filters', async () => {
282
- const result = await repo.find({
295
+ const users = await repository.find({
283
296
  filter: {
284
297
  where: {
285
298
  or: [
@@ -290,34 +303,35 @@ describe('UserRepository', () => {
290
303
  },
291
304
  });
292
305
 
293
- expect(result.data).toHaveLength(2);
294
- expect(result.data.map(u => u.name)).toContain('Alice');
295
- expect(result.data.map(u => u.name)).toContain('Charlie');
306
+ expect(users).toHaveLength(2);
307
+ expect(users.map(user => user.name)).toContain('Alice');
308
+ expect(users.map(user => user.name)).toContain('Charlie');
296
309
  });
297
310
  });
298
311
 
299
312
  describe('relations', () => {
300
313
  it('should load related entities', async () => {
301
314
  // Assuming User has Posts relation
302
- const user = await repo.create({
315
+ const created = await repository.create({
303
316
  data: { email: 'author@test.com', name: 'Author' },
304
317
  });
305
318
 
306
319
  // Create posts for the user
307
- const postRepo = new PostRepository(TestDatabase.getDb());
308
- await postRepo.createMany({
320
+ const postRepository = new PostRepository(TestDatabase.getDataSource());
321
+ await postRepository.createAll({
309
322
  data: [
310
- { title: 'Post 1', authorId: user.data!.id },
311
- { title: 'Post 2', authorId: user.data!.id },
323
+ { title: 'Post 1', authorId: created.data!.id },
324
+ { title: 'Post 2', authorId: created.data!.id },
312
325
  ],
313
326
  });
314
327
 
315
- const result = await repo.findById({
316
- id: user.data!.id,
328
+ // findById() returns the entity directly (or null)
329
+ const author = await repository.findById({
330
+ id: created.data!.id,
317
331
  filter: { include: [{ relation: 'posts' }] },
318
332
  });
319
333
 
320
- expect(result.data?.posts).toHaveLength(2);
334
+ expect(author?.posts).toHaveLength(2);
321
335
  });
322
336
  });
323
337
  });
@@ -341,8 +355,8 @@ describe('UserController E2E', () => {
341
355
  beforeAll(async () => {
342
356
  await TestDatabase.initialize();
343
357
  app = new Application();
344
- await app.boot();
345
- client = testClient(app.server);
358
+ await app.initialize();
359
+ client = testClient(app.getServer());
346
360
  });
347
361
 
348
362
  afterEach(async () => {