@venizia/ignis-docs 0.2.0 → 0.2.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +22 -11
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +6 -2
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +182 -93
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +57 -218
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@venizia/ignis-docs)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
10
10
|
[](https://modelcontextprotocol.io/)
|
|
11
11
|
|
|
12
|
-
VitePress-powered documentation site and an MCP server with
|
|
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
|
|
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** | **
|
|
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**
|
|
34
|
-
- **MCP Server**
|
|
35
|
-
- **Fuzzy Search**
|
|
36
|
-
- **GitHub Integration**
|
|
37
|
-
- **CLI Binary**
|
|
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
|
|
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)
|
|
188
|
-
- [Online Documentation](https://venizia
|
|
189
|
-
- [MCP Server Guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/reference/mcp-docs-server.md)
|
|
190
|
-
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
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.
|
|
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.
|
|
112
|
+
configs: RouteConfigs.CREATE_ITEM,
|
|
113
113
|
}).to({
|
|
114
114
|
handler: context => {
|
|
115
|
-
|
|
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 `
|
|
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
|
-
//
|
|
475
|
-
const
|
|
482
|
+
// Count only - skips the RETURNING round-trip
|
|
483
|
+
const purged = await sessionRepository.deleteBy({
|
|
476
484
|
where: { userId: 'user-123' },
|
|
477
|
-
options: { shouldReturn:
|
|
485
|
+
options: { shouldReturn: false },
|
|
478
486
|
});
|
|
479
|
-
//
|
|
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',
|
|
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
|
-
//
|
|
523
|
+
// Full message shape - { text, code, args } - for i18n
|
|
516
524
|
throw getError({
|
|
517
|
-
statusCode:
|
|
518
|
-
message:
|
|
519
|
-
|
|
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
|
-
"
|
|
552
|
-
|
|
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
|
|
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 `
|
|
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.
|
|
209
|
-
│ 7.
|
|
210
|
-
│ 8.
|
|
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
|
-
│ │
|
|
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
|
|
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
|
-
|
|
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` | `
|
|
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:
|
|
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:
|
|
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 `
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
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 --
|
|
234
|
-
if (error
|
|
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
|
-
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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({
|
|
85
|
-
|
|
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:
|
|
102
|
+
configs: definitions.FIND,
|
|
92
103
|
handler: async context => this.find({ context }),
|
|
93
104
|
});
|
|
94
105
|
this.defineRoute({
|
|
95
|
-
configs:
|
|
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 -
|
|
105
|
-
|
|
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
|
|
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 (
|
|
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
|
|
245
|
+
private _strategies = new Map<string, T>();
|
|
234
246
|
|
|
235
247
|
register(name: string, strategy: T): void {
|
|
236
|
-
if (this.
|
|
248
|
+
if (this._strategies.has(name)) {
|
|
237
249
|
throw getError({ message: `[register] Strategy '${name}' already registered` });
|
|
238
250
|
}
|
|
239
|
-
this.
|
|
251
|
+
this._strategies.set(name, strategy);
|
|
240
252
|
}
|
|
241
253
|
|
|
242
254
|
get(name: string): T {
|
|
243
|
-
const strategy = this.
|
|
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.
|
|
263
|
+
return this._strategies.has(name);
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
all(): Map<string, T> {
|
|
255
|
-
return new Map(this.
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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`
|
|
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
|
|