@venizia/ignis-docs 0.2.0 → 0.2.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) 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 +22 -11
  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 +26 -2
  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 +6 -2
  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 +182 -93
  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 +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/@venizia/ignis-docs.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@venizia/ignis-docs)
8
8
  [![License](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](https://opensource.org/licenses/MIT)
9
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
10
10
  [![MCP](https://img.shields.io/badge/MCP-Compatible-8B5CF6.svg?style=flat-square)](https://modelcontextprotocol.io/)
11
11
 
12
- VitePress-powered documentation site and an MCP server with 11 tools that gives AI assistants real-time access to IGNIS knowledge -- search docs, browse source code, and verify dependencies.
12
+ VitePress-powered documentation site and an MCP server with 10 tools that gives AI assistants real-time access to IGNIS knowledge - search docs, browse source code, and verify dependencies.
13
13
 
14
- [Installation](#installation) • [MCP Setup](#mcp-server-setup) • [Available Tools](#available-mcp-tools) • [Online Docs](https://venizia-ai.github.io/ignis)
14
+ [Installation](#installation) • [MCP Setup](#mcp-server-setup) • [Available Tools](#available-mcp-tools) • [Online Docs](https://ignis.venizia.ai)
15
15
 
16
16
  </div>
17
17
 
@@ -21,7 +21,7 @@ VitePress-powered documentation site and an MCP server with 11 tools that gives
21
21
 
22
22
  | | Feature | |
23
23
  | :---: | :--- | :--- |
24
- | **1** | **11 MCP Tools** | Search docs, browse code, verify deps from any AI assistant |
24
+ | **1** | **10 MCP Tools** | Search docs, browse code, verify deps from any AI assistant |
25
25
  | **2** | **Fuzzy Search** | Fuse.js-powered search across all documentation |
26
26
  | **3** | **VitePress Site** | Full-featured docs with guides, API references, and tutorials |
27
27
  | **4** | **CLI Binary** | Ships as `ignis-docs-mcp` for easy MCP integration |
@@ -30,11 +30,11 @@ VitePress-powered documentation site and an MCP server with 11 tools that gives
30
30
 
31
31
  ## Features
32
32
 
33
- - **VitePress Documentation Site** -- Full-featured docs with guides, API references, tutorials, and best practices
34
- - **MCP Server** -- 11 tools for AI assistants to search docs, browse source code, and verify dependencies
35
- - **Fuzzy Search** -- Fuse.js-powered search across all documentation (title weight 0.7, content weight 0.3)
36
- - **GitHub Integration** -- Browse project files, search code, and verify dependency versions directly from AI tools
37
- - **CLI Binary** -- Ships as `ignis-docs-mcp` for easy integration with Claude Desktop and other MCP-compatible clients
33
+ - **VitePress Documentation Site** - Full-featured docs with guides, API references, tutorials, and best practices
34
+ - **MCP Server** - 10 tools for AI assistants to search docs, browse source code, and verify dependencies
35
+ - **Fuzzy Search** - Fuse.js-powered search across all documentation (title weight 0.7, content weight 0.3)
36
+ - **GitHub Integration** - Browse project files, search code, and verify dependency versions directly from AI tools
37
+ - **CLI Binary** - Ships as `ignis-docs-mcp` for easy integration with Claude Desktop and other MCP-compatible clients
38
38
 
39
39
  ---
40
40
 
@@ -150,7 +150,7 @@ wiki/
150
150
 
151
151
  ### Online Documentation
152
152
 
153
- [https://venizia-ai.github.io/ignis](https://venizia-ai.github.io/ignis)
153
+ [https://ignis.venizia.ai](https://ignis.venizia.ai)
154
154
 
155
155
  ---
156
156
 
@@ -184,10 +184,10 @@ bun run mcp:rebuild
184
184
 
185
185
  ## Related Links
186
186
 
187
- - [IGNIS Framework](https://github.com/VENIZIA-AI/ignis) -- Main repository
188
- - [Online Documentation](https://venizia-ai.github.io/ignis) -- Full documentation site
189
- - [MCP Server Guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/reference/mcp-docs-server.md) -- Detailed setup guide
190
- - [Model Context Protocol](https://modelcontextprotocol.io/) -- MCP specification
187
+ - [IGNIS Framework](https://github.com/VENIZIA-AI/ignis) - Main repository
188
+ - [Online Documentation](https://ignis.venizia.ai) - Full documentation site
189
+ - [MCP Server Guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/reference/mcp-docs-server.md) - Detailed setup guide
190
+ - [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
191
191
 
192
192
  ---
193
193
 
@@ -101,18 +101,22 @@ export class TestController extends BaseRestController {
101
101
  override binding(): ValueOrPromise<void> {
102
102
  // Using 'defineRoute'
103
103
  this.defineRoute({
104
- configs: RouteConfigs.GET_HELLO,
104
+ configs: RouteConfigs.GET_TEST,
105
105
  handler: context => {
106
- return context.json({ message: 'Hello' }, HTTP.ResultCodes.RS_2.Ok);
106
+ return context.json({ message: 'Hello', method: 'GET' }, HTTP.ResultCodes.RS_2.Ok);
107
107
  },
108
108
  });
109
109
 
110
110
  // Using 'bindRoute' for a fluent API
111
111
  this.bindRoute({
112
- configs: RouteConfigs.GET_GREETING,
112
+ configs: RouteConfigs.CREATE_ITEM,
113
113
  }).to({
114
114
  handler: context => {
115
- return context.json({ message: 'Hello 3' }, HTTP.ResultCodes.RS_2.Ok);
115
+ const body = context.req.valid<{ name: string; age: number }>('json');
116
+ return context.json(
117
+ { id: crypto.randomUUID(), ...body },
118
+ HTTP.ResultCodes.RS_2.Ok,
119
+ );
116
120
  },
117
121
  });
118
122
  }
@@ -168,7 +172,7 @@ This automatically creates endpoints like `GET /configurations`, `POST /configur
168
172
 
169
173
  ## Repository (Data Access) Usage
170
174
 
171
- Repositories are used to interact with your database. The `DefaultCRUDRepository` provides a rich set of methods for data manipulation. Here are examples from the `postConfigure` method in `src/application.ts`, which demonstrates how to use an injected repository.
175
+ Repositories are used to interact with your database. The `DefaultRelationalRepository` provides a rich set of methods for data manipulation. Here are examples from the `postConfigure` method in `src/application.ts`, which demonstrates how to use an injected repository.
172
176
 
173
177
  ```typescript
174
178
  // In src/application.ts
@@ -223,10 +227,11 @@ const updated = await configurationRepository.updateById({
223
227
  // --- Delete a Record by ID ---
224
228
  const deleted = await configurationRepository.deleteById({
225
229
  id: newRecord.data!.id,
226
- options: { shouldReturn: true }, // Option to return the deleted record
227
230
  });
228
231
  ```
229
232
 
233
+ Writes return `{ count, data }` and `shouldReturn` defaults to `true`. Pass `options: { shouldReturn: false }` to skip the `RETURNING` round-trip when you only need the count.
234
+
230
235
  ## Server-Side Rendering (JSX)
231
236
 
232
237
  IGNIS supports server-side rendering using Hono's JSX middleware. This is useful for returning HTML content, such as landing pages or simple admin views.
@@ -240,7 +245,10 @@ import { BaseRestController, controller, htmlResponse } from '@venizia/ignis';
240
245
 
241
246
  @controller({ path: '/pages' })
242
247
  export class PageController extends BaseRestController {
243
-
248
+ constructor() {
249
+ super({ scope: PageController.name, path: '/pages' });
250
+ }
251
+
244
252
  override binding(): void {
245
253
  this.defineJSXRoute({
246
254
  configs: {
@@ -471,12 +479,12 @@ const result = await sessionRepository.deleteBy({
471
479
  where: { expiresAt: { lt: new Date() } },
472
480
  });
473
481
 
474
- // Delete with return values
475
- const deleted = await sessionRepository.deleteBy({
482
+ // Count only - skips the RETURNING round-trip
483
+ const purged = await sessionRepository.deleteBy({
476
484
  where: { userId: 'user-123' },
477
- options: { shouldReturn: true }, // Returns deleted records
485
+ options: { shouldReturn: false },
478
486
  });
479
- // deleted.data = array of deleted records
487
+ // purged.count = number of deleted rows; purged.data is null
480
488
  ```
481
489
 
482
490
  ### Batch Create
@@ -504,7 +512,7 @@ import { getError, HTTP } from '@venizia/ignis-helpers';
504
512
 
505
513
  // Basic error
506
514
  throw getError({ message: 'Something went wrong' });
507
- // Returns: { statusCode: 400, message: 'Something went wrong', messageCode: 'core.system_error' }
515
+ // Returns: { statusCode: 400, message: 'Something went wrong', normalized: { text: 'Something went wrong', code: 'core.system_error', args: {} } }
508
516
 
509
517
  // With status code
510
518
  throw getError({
@@ -512,14 +520,21 @@ throw getError({
512
520
  message: 'User not found',
513
521
  });
514
522
 
515
- // With message code for i18n
523
+ // Full message shape - { text, code, args } - for i18n
516
524
  throw getError({
517
- statusCode: 404,
518
- message: 'User not found',
519
- messageCode: 'core.user.not_found',
525
+ statusCode: HTTP.ResultCodes.RS_4.NotFound,
526
+ message: {
527
+ text: 'User not found',
528
+ code: 'core.user.not_found',
529
+ args: { id },
530
+ },
520
531
  });
521
532
  ```
522
533
 
534
+ The `{ text, code, args }` object is the one message shape used by error definitions, `getError` input, and the `normalized` field of every response. Read `normalized.code` on the client - there is no top-level `messageCode` in the response. Codes are lower-cased and default to `core.system_error`.
535
+
536
+ Use `isApplicationError(error)` to test identity, never `instanceof` - the class has more than one identity across packages.
537
+
523
538
  ### Error Handling in Route Handlers
524
539
 
525
540
  ```typescript
@@ -548,8 +563,13 @@ All errors are automatically formatted:
548
563
  {
549
564
  "statusCode": 404,
550
565
  "message": "User not found",
551
- "messageCode": "core.user.not_found",
552
- "requestId": "abc123"
566
+ "normalized": {
567
+ "text": "User not found",
568
+ "code": "core.user.not_found",
569
+ "args": {}
570
+ },
571
+ "requestId": "abc123",
572
+ "details": { "url": "http://localhost:3000/users/abc", "path": "/users/:id" }
553
573
  }
554
574
  ```
555
575
 
@@ -584,7 +604,7 @@ async processOrder(c: Context) {
584
604
  } catch (error) {
585
605
  this.logger.error('[processOrder] Failed: %s', error);
586
606
 
587
- if (error instanceof ApplicationError) {
607
+ if (isApplicationError(error)) {
588
608
  throw error; // Re-throw application errors
589
609
  }
590
610
 
@@ -29,7 +29,7 @@ graph TD
29
29
  |-------|---------------|---------|
30
30
  | **Controllers** | Handle HTTP/gRPC - parse requests, validate, format responses | `ConfigurationController` (REST), `GreeterController` (gRPC) |
31
31
  | **Services** | Business logic - orchestrate operations | `AuthenticationService` (auth logic) |
32
- | **Repositories** | Data access - CRUD operations | `ConfigurationRepository` (extends `DefaultCRUDRepository`) |
32
+ | **Repositories** | Data access - CRUD operations | `ConfigurationRepository` (extends `DefaultRelationalRepository`) |
33
33
  | **DataSources** | Database connections | `PostgresDataSource` (connects to PostgreSQL) |
34
34
  | **Models** | Data structure - Drizzle schemas + Entity classes | `Configuration`, `User` models |
35
35
 
@@ -205,12 +205,14 @@ IGNIS applications follow a predictable startup sequence with hooks for customiz
205
205
  │ │ - Register Components │ │
206
206
  │ └─────────────────────────────────────────────────────┘ │
207
207
  │ │
208
- │ 6. registerDataSources() - Initialize DB connections
209
- │ 7. registerComponents() - Configure all components
210
- │ 8. registerControllers() - Mount routes to router
208
+ │ 6. hydrateSecrets() - Resolve secrets into env
209
+ │ 7. registerDataSources() - Initialize DB connections
210
+ │ 8. registerComponents() - Configure all components
211
+ │ 9. wireSecretRotatables() - Attach rotation listeners │
212
+ │ 10. registerControllers() - Mount routes to router │
211
213
  │ │
212
214
  │ ┌─────────────────────────────────────────────────────┐ │
213
- │ │ 9. postConfigure() ← YOUR CODE HERE │ │
215
+ │ │ 11. postConfigure() ← YOUR CODE HERE │ │
214
216
  │ │ - Seed data │ │
215
217
  │ │ - Start background jobs │ │
216
218
  │ │ - Custom initialization │ │
@@ -218,6 +220,8 @@ IGNIS applications follow a predictable startup sequence with hooks for customiz
218
220
  └─────────────────────────────────────────────────────────────┘
219
221
  ```
220
222
 
223
+ `hydrateSecrets()` runs after `preConfigure()` so a secrets provider registered there is available, and before `registerDataSources()` so datasources read already-resolved values. `wireSecretRotatables()` runs after components, because a component may contribute the datasource a rotation lease points at.
224
+
221
225
  **Lifecycle Methods:**
222
226
 
223
227
  | Method | When | Purpose |
@@ -287,16 +291,20 @@ export class Application extends BaseApplication {
287
291
  **How registration works:**
288
292
  ```typescript
289
293
  // BaseApplication implements service() directly (no mixin composition):
290
- service<Base extends IService>(ctor: TClass<Base>): Binding<Base> {
294
+ service<Base extends IService, Args extends AnyObject = any>(
295
+ ctor: TClass<Base>,
296
+ opts?: TMixinOpts<Args>,
297
+ ): Binding<Base> {
291
298
  return this.bind<Base>({
292
- key: BindingKeys.build({
293
- namespace: BindingNamespaces.SERVICE,
294
- key: ctor.name,
295
- }),
299
+ key: BindingKeys.build(
300
+ opts?.binding ?? { namespace: BindingNamespaces.SERVICE, key: ctor.name },
301
+ ),
296
302
  }).toClass(ctor);
297
303
  }
298
304
  ```
299
305
 
306
+ Every registration method takes the same optional second argument - `opts.binding` overrides the derived `{ namespace, key }` when you need to register two classes under one contract.
307
+
300
308
  **Capability interfaces:**
301
309
 
302
310
  Each registration capability is declared as a TypeScript interface that `IRestApplication` (and therefore `BaseApplication`) implements. Reference these when you type your own application contracts:
@@ -304,8 +312,11 @@ Each registration capability is declared as a TypeScript interface that `IRestAp
304
312
  | Interface | Methods | Purpose |
305
313
  |-----------|---------|---------|
306
314
  | `IServiceMixin` | `service()` | Register service classes |
307
- | `IRepositoryMixin` | `repository()`, `dataSource()`, `registerDataSources()` | Register data layer |
315
+ | `IRepositoryMixin` | `dataSource()`, `repository()` | Register data layer |
308
316
  | `IComponentMixin` | `component()`, `registerComponents()` | Register modular components |
317
+ | `IControllerMixin` | `controller()`, `registerControllers()` | Register controllers and mount routes |
318
+ | `IServerConfigMixin` | `staticConfigure()`, `preConfigure()`, `postConfigure()`, `getApplicationVersion()` | Lifecycle hooks |
319
+ | `IStaticServeMixin` | `static()` | Serve static files |
309
320
 
310
321
  > [!NOTE]
311
322
  > Earlier releases also exported `ServiceMixin`, `RepositoryMixin`, and `ComponentMixin` as class-mixin **functions** you composed onto `AbstractApplication`. They duplicated `BaseApplication`'s own methods verbatim, drifted out of sync, and had no known consumers, so they were removed. The `IServiceMixin` / `IRepositoryMixin` / `IComponentMixin` **interfaces** remain - extend `BaseApplication` and call its registration methods directly.
@@ -26,7 +26,7 @@ export class ItemController extends BaseRestController {
26
26
  @inject({ key: 'repositories.ItemRepository' })
27
27
  private itemRepository: ItemRepository,
28
28
  ) {
29
- super({ scope: 'ItemController', path: '/items' });
29
+ super({ scope: ItemController.name, path: '/items' });
30
30
  }
31
31
 
32
32
  @get({ configs: RouteConfigs.GET_ITEM_BY_ID })
@@ -53,7 +53,7 @@ export class OrderController extends BaseRestController {
53
53
  @inject({ key: 'services.OrderService' })
54
54
  private orderService: OrderService,
55
55
  ) {
56
- super({ scope: 'OrderController', path: '/orders' });
56
+ super({ scope: OrderController.name, path: '/orders' });
57
57
  }
58
58
 
59
59
  @post({ configs: RouteConfigs.CREATE_ORDER })
@@ -132,6 +132,10 @@ export class NotificationComponent extends BaseComponent {
132
132
  // Inline: Simple, one-off, no need for abstraction
133
133
  @controller({ path: '/health' })
134
134
  export class HealthController extends BaseRestController {
135
+ constructor() {
136
+ super({ scope: HealthController.name, path: '/health' });
137
+ }
138
+
135
139
  @get({ configs: RouteConfigs.HEALTH_CHECK })
136
140
  healthCheck(c: Context) {
137
141
  return c.json({ status: 'ok', timestamp: new Date() });
@@ -152,21 +156,26 @@ export class HealthController extends BaseRestController {
152
156
 
153
157
  ### Start with Standard CRUD
154
158
 
155
- Every repository gets these methods from `DefaultCRUDRepository`:
159
+ Every repository gets these methods from `DefaultRelationalRepository`:
156
160
 
157
161
  ```typescript
158
162
  // Inherited methods (options-object API) - use these first
159
- find({ filter }) // List with filters
160
- findById({ id }) // Get by ID
161
- findOne({ filter }) // Get first match
162
- create({ data }) // Create new
163
- updateById({ id, data }) // Update existing
163
+ find({ filter }) // List with filters -> T[]
164
+ findById({ id }) // Get by ID -> T | null
165
+ findOne({ filter }) // Get first match -> T | null
166
+ count({ where }) // Count matches -> { count }
167
+ existsWith({ where }) // Existence check -> boolean
168
+
169
+ create({ data }) // Create new -> { count, data }
170
+ createAll({ data }) // Create many -> { count, data }
171
+ updateById({ id, data }) // Update existing -> { count, data }
164
172
  updateAll({ data, where }) // Bulk update (updateBy is an alias)
165
- deleteById({ id }) // Delete
173
+ deleteById({ id }) // Delete -> { count, data }
166
174
  deleteAll({ where }) // Bulk delete (deleteBy is an alias)
167
- count({ where }) // Count matches
168
175
  ```
169
176
 
177
+ Reads return the record(s) directly; writes return `{ count, data }`. Pass `options: { shouldReturn: false }` on a write to skip the `RETURNING` round-trip.
178
+
170
179
  ### Add Custom Methods When:
171
180
 
172
181
  1. **Query is complex and reusable**
@@ -175,7 +184,7 @@ count({ where }) // Count matches
175
184
 
176
185
  ```typescript
177
186
  // Custom repository methods
178
- export class OrderRepository extends DefaultCRUDRepository<typeof Order.schema> {
187
+ export class OrderRepository extends DefaultRelationalRepository<typeof Order.schema> {
179
188
  // Complex query that's used in multiple places
180
189
  async findPendingOrdersOlderThan(hours: number) {
181
190
  const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
@@ -230,8 +239,8 @@ export class UserController extends BaseRestController {
230
239
  const user = await this.userService.create(data);
231
240
  return c.json(user, 201);
232
241
  } catch (error) {
233
- // Format error for API response -- messageCode is always lower-cased by ApplicationError
234
- if (error instanceof ApplicationError && error.messageCode === 'app.user.duplicate_email') {
242
+ // Format error for API response -- the code is always lower-cased by ApplicationError
243
+ if (isApplicationError(error) && error.normalized.code === 'app.user.duplicate_email') {
235
244
  return c.json({ error: 'Email already exists' }, 400);
236
245
  }
237
246
  throw error; // Let global handler catch unknown errors
@@ -255,10 +264,14 @@ export class UserService extends BaseService {
255
264
  // Validate and throw domain-specific errors
256
265
  const existing = await this.userRepository.findByEmail(data.email);
257
266
  if (existing) {
267
+ // One message shape everywhere: { text, code, args }
258
268
  throw getError({
259
269
  statusCode: 400,
260
- messageCode: MessageCode.build({ parts: ['app', 'user', 'duplicate_email'] }),
261
- message: 'User with this email already exists',
270
+ message: {
271
+ text: 'User with this email already exists',
272
+ code: MessageCode.build({ parts: ['app', 'user', 'duplicate_email'] }),
273
+ args: { email: data.email },
274
+ },
262
275
  });
263
276
  }
264
277
 
@@ -273,7 +286,7 @@ export class UserService extends BaseService {
273
286
  ### Repository Level: Let Errors Bubble
274
287
 
275
288
  ```typescript
276
- export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
289
+ export class UserRepository extends DefaultRelationalRepository<typeof User.schema> {
277
290
  // Don't catch database errors here
278
291
  // Let them bubble up to service/controller
279
292
  async findByEmail(email: string) {
@@ -7,13 +7,13 @@ Advanced TypeScript patterns used throughout the IGNIS framework.
7
7
  Create reusable class extensions without deep inheritance:
8
8
 
9
9
  ```typescript
10
- import { LoggerFactory, TMixinTarget } from '@venizia/ignis-helpers';
10
+ import { ILogger, LoggerFactory, TMixinTarget } from '@venizia/ignis-helpers';
11
11
 
12
12
  export const LoggableMixin = <BaseClass extends TMixinTarget<object>>(
13
13
  baseClass: BaseClass,
14
14
  ) => {
15
15
  return class extends baseClass {
16
- protected logger = LoggerFactory.getLogger([this.constructor.name]);
16
+ protected logger: ILogger = LoggerFactory.getLogger([this.constructor.name]);
17
17
 
18
18
  log(message: string): void {
19
19
  this.logger.info(message);
@@ -62,37 +62,48 @@ Generate classes dynamically with configuration:
62
62
 
63
63
  ```typescript
64
64
  class ControllerFactory extends BaseHelper {
65
- /** `TDataObject`/`TPersistObject` cannot be inferred from `entity` -
66
- * pass them explicitly for typed CRUD handlers. */
67
65
  static defineCrudController<
68
- TDataObject extends object = object,
69
- TPersistObject extends object = TDataObject,
70
- >(defOpts: ICrudControllerOptions) {
71
- const { controller, entity } = defOpts;
66
+ TEntity extends AbstractEntity = AbstractEntity,
67
+ Routes extends ICustomizableRoutes = ICustomizableRoutes,
68
+ // DataObject/PersistObject default to the entity's inferred select/insert shapes
69
+ TDataObject extends object = TEntityDataObject<TEntity>,
70
+ TPersistObject extends object = TEntityPersistObject<TEntity>,
71
+ >(defOpts: ICrudControllerOptions<TEntity, Routes>) {
72
+ const { controller, entity, routes } = defOpts;
72
73
 
73
74
  // `entity` accepts a class directly or a resolver function
74
75
  const entityClass = isClass(entity) ? entity : entity();
75
76
  const entityInstance = new entityClass();
76
77
 
77
78
  // Derive request/response schemas + route configs from the entity instance
78
- const routeDefinitions = buildRouteDefinitions({ entity: entityInstance });
79
-
80
- return class extends BaseRestController {
81
- repository: AbstractRepository<TDataObject, TPersistObject>;
82
-
79
+ const definitions = defineControllerRouteConfigs({
80
+ idType: entityInstance.getIdType(),
81
+ routes,
82
+ schema: {
83
+ select: entityInstance.getSchema({ type: SchemaTypes.SELECT }),
84
+ create: entityInstance.getSchema({ type: SchemaTypes.CREATE }),
85
+ update: entityInstance.getSchema({ type: SchemaTypes.UPDATE }),
86
+ },
87
+ });
88
+
89
+ return class extends PersistableCrudController<TEntity> {
83
90
  constructor(repository: AbstractRepository<TDataObject, TPersistObject>) {
84
- super({ scope: controller.name, path: controller.basePath });
85
- this.repository = repository;
91
+ super({
92
+ scope: controller.name,
93
+ path: controller.basePath,
94
+ repository,
95
+ definitions,
96
+ });
86
97
  }
87
98
 
88
99
  /** Registers all CRUD route handlers. */
89
100
  override binding(): ValueOrPromise<void> {
90
101
  this.defineRoute({
91
- configs: routeDefinitions.FIND,
102
+ configs: definitions.FIND,
92
103
  handler: async context => this.find({ context }),
93
104
  });
94
105
  this.defineRoute({
95
- configs: routeDefinitions.FIND_BY_ID,
106
+ configs: definitions.FIND_BY_ID,
96
107
  handler: async context => this.findById({ context }),
97
108
  });
98
109
  // ... more routes (count/findOne/create/updateById/deleteById/...)
@@ -101,11 +112,8 @@ class ControllerFactory extends BaseHelper {
101
112
  }
102
113
  }
103
114
 
104
- // Usage - type parameters are explicit, entity can be a class or a resolver
105
- type TUser = typeof User.schema.$inferSelect;
106
- type TNewUser = typeof User.schema.$inferInsert;
107
-
108
- const UserCrudController = ControllerFactory.defineCrudController<TUser, TNewUser>({
115
+ // Usage - types come from the entity, entity can be a class or a resolver
116
+ const UserCrudController = ControllerFactory.defineCrudController({
109
117
  controller: { name: 'UserController', basePath: '/users' },
110
118
  repository: { name: UserRepository.name },
111
119
  entity: () => User,
@@ -117,6 +125,10 @@ export class UserController extends UserCrudController {
117
125
  }
118
126
  ```
119
127
 
128
+ > [!NOTE]
129
+ > `controller.basePath` is required - `defineCrudController` throws via `getError` when it is
130
+ > missing or left at the internal `'unknown_path'` placeholder.
131
+
120
132
  ## Value Resolver Pattern
121
133
 
122
134
  Support multiple input types that resolve to a single value:
@@ -132,16 +144,16 @@ export const resolveValue = <T>(valueOrResolver: TValueOrResolver<T>): T => {
132
144
  return valueOrResolver; // Direct value
133
145
  }
134
146
 
135
- if (isClass(valueOrResolver as Function)) {
147
+ if (isClass(valueOrResolver)) {
136
148
  return valueOrResolver as T; // Class constructor (return as-is)
137
149
  }
138
150
 
139
151
  return (valueOrResolver as TResolver<T>)(); // Function resolver
140
152
  };
141
153
 
142
- // isClass (from @venizia/ignis-inversion, re-exported by helpers) - distinguishes class
154
+ // isClass (declared in @venizia/ignis-inversion, re-exported by helpers) - distinguishes class
143
155
  // constructors from arrow/regular functions by testing the SOURCE against /^class[\s{]/,
144
- // since every non-arrow function has a prototype
156
+ // since every non-arrow function has a prototype. Sound only on ES2020+ output.
145
157
  export const isClass = <T>(target: any): target is TClass<T> => {
146
158
  if (typeof target !== 'function' || target.prototype === undefined) {
147
159
  return false;
@@ -230,17 +242,17 @@ Centralized registration of components:
230
242
 
231
243
  ```typescript
232
244
  class StrategyRegistry<T> {
233
- private strategies = new Map<string, T>();
245
+ private _strategies = new Map<string, T>();
234
246
 
235
247
  register(name: string, strategy: T): void {
236
- if (this.strategies.has(name)) {
248
+ if (this._strategies.has(name)) {
237
249
  throw getError({ message: `[register] Strategy '${name}' already registered` });
238
250
  }
239
- this.strategies.set(name, strategy);
251
+ this._strategies.set(name, strategy);
240
252
  }
241
253
 
242
254
  get(name: string): T {
243
- const strategy = this.strategies.get(name);
255
+ const strategy = this._strategies.get(name);
244
256
  if (!strategy) {
245
257
  throw getError({ message: `[get] Strategy '${name}' not found` });
246
258
  }
@@ -248,11 +260,11 @@ class StrategyRegistry<T> {
248
260
  }
249
261
 
250
262
  has(name: string): boolean {
251
- return this.strategies.has(name);
263
+ return this._strategies.has(name);
252
264
  }
253
265
 
254
266
  all(): Map<string, T> {
255
- return new Map(this.strategies);
267
+ return new Map(this._strategies);
256
268
  }
257
269
  }
258
270
 
@@ -86,7 +86,9 @@ interface IUser {
86
86
  }
87
87
 
88
88
  // Usage with validation
89
- function updateUserStatus(userId: string, status: string) {
89
+ const updateUserStatus = (opts: { userId: string; status: string }) => {
90
+ const { status } = opts;
91
+
90
92
  if (!UserStatuses.isValid(status)) {
91
93
  throw getError({
92
94
  statusCode: HTTP.ResultCodes.RS_4.BadRequest,
@@ -94,7 +96,7 @@ function updateUserStatus(userId: string, status: string) {
94
96
  });
95
97
  }
96
98
  // status is validated at runtime
97
- }
99
+ };
98
100
  ```
99
101
 
100
102
  ## Enum vs Static Class Comparison
@@ -198,7 +200,15 @@ constructor(options: IJWSTokenServiceOptions) {
198
200
 
199
201
  ## Environment Variables Management
200
202
 
201
- Avoid using `process.env` directly in your business logic. Instead, use the `applicationEnvironment` helper and define your keys as constants.
203
+ Avoid using `process.env` directly in your business logic. Instead, use the `applicationEnvironment`
204
+ helper and define your keys as constants.
205
+
206
+ Two constraints it enforces:
207
+
208
+ - Only variables whose name starts with the prefix (`APP_ENV` by default, override with
209
+ `APPLICATION_ENV_PREFIX`) are visible. Anything else reads back as `undefined`.
210
+ - Values are raw strings. A non-string type parameter is an assertion, not a conversion - pass
211
+ `transform` to actually convert.
202
212
 
203
213
  **Define Keys (`src/common/environments.ts`):**
204
214
  ```typescript
@@ -215,7 +225,12 @@ import { EnvironmentKeys } from '@/common/environments';
215
225
 
216
226
  // Correct usage
217
227
  const stripeKey = applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_STRIPE_KEY);
218
- const retries = applicationEnvironment.get<number>(EnvironmentKeys.APP_ENV_MAX_RETRIES);
228
+
229
+ // Numbers need an explicit transform - the stored value is a string
230
+ const retries = applicationEnvironment.get<number, string>(EnvironmentKeys.APP_ENV_MAX_RETRIES, {
231
+ transform: value => Number(value),
232
+ defaultValue: 3,
233
+ });
219
234
  ```
220
235
 
221
236
  ## See Also
@@ -108,20 +108,52 @@ this.logger.debug('[config] Server options: %j', this.serverOptions);
108
108
  ```
109
109
 
110
110
  > [!NOTE]
111
- > `%j` on an `Error` instance drops `message` and `stack` -- they are non-enumerable, so `JSON.stringify` never sees them. Use `%s` for errors, `%j`/`%o` for plain data objects. An object passed to `%s` is inspected up to `APP_ENV_LOGGER_INSPECT_DEPTH` levels deep (default `5`) instead of Node's hard-coded `depth: 0`, so nested fields print instead of collapsing to `[Object]`. See [Logger Helper](/extensions/helpers/logger/) for details.
111
+ > `%j` on an `Error` instance drops `message` and `stack` - they are non-enumerable, so `JSON.stringify` never sees them. Use `%s` for errors, `%j`/`%o` for plain data objects. An object passed to `%s` is inspected up to `APP_ENV_LOGGER_INSPECT_DEPTH` levels deep (default `5`) instead of Node's hard-coded `depth: 0`, so nested fields print instead of collapsing to `[Object]`. See [Logger Helper](/extensions/helpers/logger/) for details.
112
112
 
113
113
  ### Log Levels
114
114
 
115
+ Exactly five levels exist, each a direct method on `ILogger`:
116
+
115
117
  | Level | Use For |
116
118
  |-------|---------|
119
+ | `emerg` | The process cannot continue |
117
120
  | `error` | Exceptions that need attention |
118
121
  | `warn` | Recoverable issues, deprecations |
119
122
  | `info` | Important business events |
120
123
  | `debug` | Detailed debugging information |
121
124
 
125
+ > [!NOTE]
126
+ > Secret-looking keys (token, password, apiKey, ...) are redacted to `[REDACTED]` before a line
127
+ > reaches any transport. `APP_ENV_LOGGER_DO_REDACT=false` disables it - local debugging only.
128
+
129
+ ### Never Swallow a Catch
130
+
131
+ An empty or comment-only `catch` is banned. Log, then rethrow or return a defined fallback.
132
+
133
+ ```typescript
134
+ // ✅ GOOD
135
+ try {
136
+ await syncRemote();
137
+ } catch (error) {
138
+ this.logger.error('[syncRemote] Sync failed | Error: %s', error);
139
+ throw error;
140
+ }
141
+
142
+ // ❌ BAD - the failure disappears
143
+ try {
144
+ await syncRemote();
145
+ } catch {
146
+ // ignore
147
+ }
148
+ ```
149
+
122
150
  ## Standardized Error Handling
123
151
 
124
152
  Use the `getError` helper and `HTTP` constants to throw consistent, formatted exceptions.
153
+ **Never `new Error`.** Across package boundaries test identity with `isApplicationError()`, never
154
+ `instanceof` - the class has more than one identity in a monorepo.
155
+
156
+ The normalized message is always one shape: `{ text, code, args }`, read from `error.normalized`.
125
157
 
126
158
  ### Basic Error
127
159