@venizia/ignis-docs 0.0.3 → 0.0.4-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -2
- package/wiki/best-practices/api-usage-examples.md +591 -0
- package/wiki/best-practices/architectural-patterns.md +415 -0
- package/wiki/best-practices/architecture-decisions.md +488 -0
- package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +406 -17
- package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
- package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
- package/wiki/best-practices/data-modeling.md +376 -0
- package/wiki/best-practices/deployment-strategies.md +698 -0
- package/wiki/best-practices/index.md +27 -0
- package/wiki/best-practices/performance-optimization.md +196 -0
- package/wiki/best-practices/security-guidelines.md +218 -0
- package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
- package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
- package/wiki/changelogs/2025-12-17-refactor.md +1 -1
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +2 -2
- package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
- package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
- package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
- package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
- package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
- package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
- package/wiki/changelogs/index.md +6 -0
- package/wiki/changelogs/planned-schema-migrator.md +0 -8
- package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
- package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
- package/wiki/guides/core-concepts/components-guide.md +509 -0
- package/wiki/{get-started → guides}/core-concepts/components.md +24 -17
- package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
- package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
- package/wiki/guides/core-concepts/persistent/index.md +119 -0
- package/wiki/guides/core-concepts/persistent/models.md +241 -0
- package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
- package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
- package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
- package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
- package/wiki/guides/get-started/philosophy.md +682 -0
- package/wiki/guides/get-started/setup.md +157 -0
- package/wiki/guides/index.md +89 -0
- package/wiki/guides/reference/glossary.md +243 -0
- package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
- package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
- package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
- package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
- package/wiki/guides/tutorials/realtime-chat.md +1261 -0
- package/wiki/guides/tutorials/testing.md +723 -0
- package/wiki/index.md +176 -37
- package/wiki/references/base/application.md +27 -0
- package/wiki/references/base/bootstrapping.md +31 -26
- package/wiki/references/base/components.md +24 -7
- package/wiki/references/base/controllers.md +50 -20
- package/wiki/references/base/datasources.md +30 -0
- package/wiki/references/base/dependency-injection.md +39 -3
- package/wiki/references/base/filter-system/application-usage.md +224 -0
- package/wiki/references/base/filter-system/array-operators.md +132 -0
- package/wiki/references/base/filter-system/comparison-operators.md +109 -0
- package/wiki/references/base/filter-system/default-filter.md +428 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
- package/wiki/references/base/filter-system/index.md +127 -0
- package/wiki/references/base/filter-system/json-filtering.md +197 -0
- package/wiki/references/base/filter-system/list-operators.md +71 -0
- package/wiki/references/base/filter-system/logical-operators.md +156 -0
- package/wiki/references/base/filter-system/null-operators.md +58 -0
- package/wiki/references/base/filter-system/pattern-matching.md +108 -0
- package/wiki/references/base/filter-system/quick-reference.md +431 -0
- package/wiki/references/base/filter-system/range-operators.md +63 -0
- package/wiki/references/base/filter-system/tips.md +190 -0
- package/wiki/references/base/filter-system/use-cases.md +452 -0
- package/wiki/references/base/index.md +90 -0
- package/wiki/references/base/middlewares.md +604 -0
- package/wiki/references/base/models.md +215 -23
- package/wiki/references/base/providers.md +731 -0
- package/wiki/references/base/repositories/advanced.md +555 -0
- package/wiki/references/base/repositories/index.md +228 -0
- package/wiki/references/base/repositories/mixins.md +331 -0
- package/wiki/references/base/repositories/relations.md +486 -0
- package/wiki/references/base/repositories.md +40 -635
- package/wiki/references/base/services.md +28 -4
- package/wiki/references/components/authentication.md +22 -2
- package/wiki/references/components/health-check.md +12 -0
- package/wiki/references/components/index.md +23 -0
- package/wiki/references/components/mail.md +687 -0
- package/wiki/references/components/request-tracker.md +16 -0
- package/wiki/references/components/socket-io.md +18 -0
- package/wiki/references/components/static-asset.md +14 -26
- package/wiki/references/components/swagger.md +17 -0
- package/wiki/references/configuration/environment-variables.md +427 -0
- package/wiki/references/configuration/index.md +73 -0
- package/wiki/references/helpers/cron.md +14 -0
- package/wiki/references/helpers/crypto.md +15 -0
- package/wiki/references/helpers/env.md +16 -0
- package/wiki/references/helpers/error.md +17 -0
- package/wiki/references/helpers/index.md +14 -0
- package/wiki/references/helpers/inversion.md +24 -4
- package/wiki/references/helpers/logger.md +19 -0
- package/wiki/references/helpers/network.md +11 -0
- package/wiki/references/helpers/queue.md +19 -0
- package/wiki/references/helpers/redis.md +21 -0
- package/wiki/references/helpers/socket-io.md +24 -5
- package/wiki/references/helpers/storage.md +18 -10
- package/wiki/references/helpers/testing.md +18 -0
- package/wiki/references/helpers/types.md +16 -0
- package/wiki/references/helpers/uid.md +167 -0
- package/wiki/references/helpers/worker-thread.md +16 -0
- package/wiki/references/index.md +177 -0
- package/wiki/references/quick-reference.md +634 -0
- package/wiki/references/src-details/boot.md +3 -3
- package/wiki/references/src-details/dev-configs.md +0 -4
- package/wiki/references/src-details/docs.md +2 -2
- package/wiki/references/src-details/index.md +86 -0
- package/wiki/references/src-details/inversion.md +1 -6
- package/wiki/references/src-details/mcp-server.md +3 -15
- package/wiki/references/utilities/index.md +86 -10
- package/wiki/references/utilities/jsx.md +577 -0
- package/wiki/references/utilities/request.md +0 -2
- package/wiki/references/utilities/statuses.md +740 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
- package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
- package/wiki/get-started/best-practices/data-modeling.md +0 -177
- package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
- package/wiki/get-started/best-practices/performance-optimization.md +0 -97
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/persistent.md +0 -539
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ bun add drizzle-orm drizzle-zod pg lodash
|
|
|
145
145
|
|
|
146
146
|
**Development dependencies:**
|
|
147
147
|
```bash
|
|
148
|
-
bun add -d typescript @types/bun @venizia/dev-configs tsc-alias
|
|
148
|
+
bun add -d typescript @types/bun @venizia/dev-configs tsc-alias
|
|
149
149
|
bun add -d drizzle-kit @types/pg @types/lodash
|
|
150
150
|
```
|
|
151
151
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/ignis-docs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-1",
|
|
4
4
|
"description": "Documentation and MCP Server for Ignis Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ignis",
|
|
@@ -100,6 +100,8 @@
|
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"@mastra/core": "^0.24.6",
|
|
102
102
|
"@mastra/mcp": "^0.14.4",
|
|
103
|
+
"cytoscape": "^3.33.1",
|
|
104
|
+
"cytoscape-cose-bilkent": "^4.1.0",
|
|
103
105
|
"dayjs": "^1.11.19",
|
|
104
106
|
"debug": "^4.4.3",
|
|
105
107
|
"fast-glob": "^3.3.3",
|
|
@@ -111,7 +113,7 @@
|
|
|
111
113
|
"@braintree/sanitize-url": "^7.1.1",
|
|
112
114
|
"@types/bun": "^1.3.4",
|
|
113
115
|
"@types/glob": "^8.1.0",
|
|
114
|
-
"@venizia/dev-configs": "^0.0.
|
|
116
|
+
"@venizia/dev-configs": "^0.0.5-0",
|
|
115
117
|
"eslint": "^9.36.0",
|
|
116
118
|
"glob": "^10.4.2",
|
|
117
119
|
"prettier": "^3.6.2",
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# API Usage Examples
|
|
2
|
+
|
|
3
|
+
Practical examples for defining endpoints and working with data in Ignis applications.
|
|
4
|
+
|
|
5
|
+
## Routing Patterns
|
|
6
|
+
|
|
7
|
+
### Decorator-Based Routing (Recommended)
|
|
8
|
+
|
|
9
|
+
Use `@get`, `@post` decorators with `as const` route configs for full type safety:
|
|
10
|
+
|
|
11
|
+
**`src/controllers/test/definitions.ts`**
|
|
12
|
+
```typescript
|
|
13
|
+
import { z } from '@hono/zod-openapi';
|
|
14
|
+
import { Authentication, HTTP, jsonContent, jsonResponse } from '@venizia/ignis';
|
|
15
|
+
|
|
16
|
+
// Define route configs as const for type inference
|
|
17
|
+
export const RouteConfigs = {
|
|
18
|
+
// Use UPPER_CASE descriptive names for each route
|
|
19
|
+
GET_TEST: {
|
|
20
|
+
method: HTTP.Methods.GET,
|
|
21
|
+
path: '/test',
|
|
22
|
+
responses: jsonResponse({
|
|
23
|
+
description: 'Test decorator GET endpoint',
|
|
24
|
+
schema: z.object({ message: z.string(), method: z.string() }),
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
CREATE_ITEM: {
|
|
28
|
+
method: HTTP.Methods.POST,
|
|
29
|
+
path: '/items',
|
|
30
|
+
authStrategies: [Authentication.STRATEGY_JWT], // Secure this endpoint
|
|
31
|
+
request: {
|
|
32
|
+
body: jsonContent({
|
|
33
|
+
description: 'Request body for POST',
|
|
34
|
+
schema: z.object({ name: z.string(), age: z.number().int().positive() }),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
responses: jsonResponse({
|
|
38
|
+
description: 'Test decorator POST endpoint',
|
|
39
|
+
schema: z.object({ id: z.string(), name: z.string(), age: z.number() }),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
} as const;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then, use the decorators in your controller class. The `TRouteContext` type provides a fully typed context, including request parameters, body, and response types.
|
|
46
|
+
|
|
47
|
+
**`src/controllers/test/controller.ts`**
|
|
48
|
+
```typescript
|
|
49
|
+
import {
|
|
50
|
+
BaseController,
|
|
51
|
+
controller,
|
|
52
|
+
get,
|
|
53
|
+
post,
|
|
54
|
+
TRouteContext,
|
|
55
|
+
HTTP,
|
|
56
|
+
} from '@venizia/ignis';
|
|
57
|
+
import { RouteConfigs } from './definitions';
|
|
58
|
+
|
|
59
|
+
@controller({ path: '/test' })
|
|
60
|
+
export class TestController extends BaseController {
|
|
61
|
+
// ...
|
|
62
|
+
|
|
63
|
+
@get({ configs: RouteConfigs.GET_TEST })
|
|
64
|
+
getWithDecorator(context: TRouteContext<typeof RouteConfigs.GET_TEST>) {
|
|
65
|
+
// context is fully typed!
|
|
66
|
+
return context.json({ message: 'Hello from decorator', method: 'GET' }, HTTP.ResultCodes.RS_2.Ok);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@post({ configs: RouteConfigs.CREATE_ITEM })
|
|
70
|
+
createWithDecorator(context: TRouteContext<typeof RouteConfigs.CREATE_ITEM>) {
|
|
71
|
+
// context.req.valid('json') is automatically typed as { name: string, age: number }
|
|
72
|
+
const body = context.req.valid('json');
|
|
73
|
+
|
|
74
|
+
// The response is validated against the schema
|
|
75
|
+
return context.json(
|
|
76
|
+
{
|
|
77
|
+
id: crypto.randomUUID(),
|
|
78
|
+
name: body.name,
|
|
79
|
+
age: body.age,
|
|
80
|
+
},
|
|
81
|
+
HTTP.ResultCodes.RS_2.Ok,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Example 2: Manual Route Definition in `binding()`
|
|
88
|
+
|
|
89
|
+
You can also define routes manually within the controller's `binding()` method using `defineRoute` or `bindRoute`. This is useful for more complex scenarios or for developers who prefer a non-decorator syntax.
|
|
90
|
+
|
|
91
|
+
**`src/controllers/test/controller.ts`**
|
|
92
|
+
```typescript
|
|
93
|
+
import { BaseController, controller, HTTP, ValueOrPromise } from '@venizia/ignis';
|
|
94
|
+
import { RouteConfigs } from './definitions';
|
|
95
|
+
|
|
96
|
+
@controller({ path: '/test' })
|
|
97
|
+
export class TestController extends BaseController {
|
|
98
|
+
// ...
|
|
99
|
+
override binding(): ValueOrPromise<void> {
|
|
100
|
+
// Using 'defineRoute'
|
|
101
|
+
this.defineRoute({
|
|
102
|
+
configs: RouteConfigs.GET_HELLO,
|
|
103
|
+
handler: context => {
|
|
104
|
+
return context.json({ message: 'Hello' }, HTTP.ResultCodes.RS_2.Ok);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Using 'bindRoute' for a fluent API
|
|
109
|
+
this.bindRoute({
|
|
110
|
+
configs: RouteConfigs.GET_GREETING,
|
|
111
|
+
}).to({
|
|
112
|
+
handler: context => {
|
|
113
|
+
return context.json({ message: 'Hello 3' }, HTTP.ResultCodes.RS_2.Ok);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ...
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Example 3: Auto-Generated CRUD Controller
|
|
122
|
+
|
|
123
|
+
For standard database entities, you can use `ControllerFactory.defineCrudController` to instantly generate a controller with a full set of CRUD endpoints.
|
|
124
|
+
|
|
125
|
+
**`src/controllers/configuration.controller.ts`**
|
|
126
|
+
```typescript
|
|
127
|
+
import { Configuration } from '@/models';
|
|
128
|
+
import { ConfigurationRepository } from '@/repositories';
|
|
129
|
+
import {
|
|
130
|
+
BindingKeys,
|
|
131
|
+
BindingNamespaces,
|
|
132
|
+
controller,
|
|
133
|
+
ControllerFactory,
|
|
134
|
+
inject,
|
|
135
|
+
} from '@venizia/ignis';
|
|
136
|
+
|
|
137
|
+
const BASE_PATH = '/configurations';
|
|
138
|
+
|
|
139
|
+
// 1. The factory generates a controller class with all CRUD routes
|
|
140
|
+
const _Controller = ControllerFactory.defineCrudController({
|
|
141
|
+
repository: { name: ConfigurationRepository.name },
|
|
142
|
+
controller: {
|
|
143
|
+
name: 'ConfigurationController',
|
|
144
|
+
basePath: BASE_PATH,
|
|
145
|
+
},
|
|
146
|
+
entity: () => Configuration, // The entity is used to generate OpenAPI schemas
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 2. Extend the generated controller to inject the repository
|
|
150
|
+
@controller({ path: BASE_PATH })
|
|
151
|
+
export class ConfigurationController extends _Controller {
|
|
152
|
+
constructor(
|
|
153
|
+
@inject({
|
|
154
|
+
key: BindingKeys.build({
|
|
155
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
156
|
+
key: ConfigurationRepository.name,
|
|
157
|
+
}),
|
|
158
|
+
})
|
|
159
|
+
repository: ConfigurationRepository,
|
|
160
|
+
) {
|
|
161
|
+
super(repository);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
This automatically creates endpoints like `GET /configurations`, `POST /configurations`, `GET /configurations/:id`, etc.
|
|
166
|
+
|
|
167
|
+
## Repository (Data Access) Usage
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// In src/application.ts
|
|
173
|
+
|
|
174
|
+
// Get the repository instance from the DI container
|
|
175
|
+
const configurationRepository = this.get<ConfigurationRepository>({
|
|
176
|
+
key: BindingKeys.build({
|
|
177
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
178
|
+
key: ConfigurationRepository.name,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Find One Record ---
|
|
183
|
+
const record = await configurationRepository.findOne({
|
|
184
|
+
filter: { where: { code: 'CODE_1' } },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// --- Find Multiple Records with Relations ---
|
|
188
|
+
const records = await configurationRepository.find({
|
|
189
|
+
filter: {
|
|
190
|
+
where: { code: 'CODE_2' },
|
|
191
|
+
fields: { id: true, code: true, createdBy: true },
|
|
192
|
+
limit: 100,
|
|
193
|
+
include: [{ relation: 'creator' }], // Eager load the 'creator' relation
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- Create a Single Record ---
|
|
198
|
+
const newRecord = await configurationRepository.create({
|
|
199
|
+
data: {
|
|
200
|
+
code: 'NEW_CODE',
|
|
201
|
+
group: 'SYSTEM',
|
|
202
|
+
dataType: 'TEXT',
|
|
203
|
+
tValue: 'some value',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// --- Create Multiple Records ---
|
|
208
|
+
const newRecords = await configurationRepository.createAll({
|
|
209
|
+
data: [
|
|
210
|
+
{ code: 'CODE_A', group: 'SYSTEM' },
|
|
211
|
+
{ code: 'CODE_B', group: 'SYSTEM' },
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// --- Update a Record by ID ---
|
|
216
|
+
const updated = await configurationRepository.updateById({
|
|
217
|
+
id: 'some-uuid',
|
|
218
|
+
data: { tValue: 'new value' },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// --- Delete a Record by ID ---
|
|
222
|
+
const deleted = await configurationRepository.deleteById({
|
|
223
|
+
id: newRecord.data!.id,
|
|
224
|
+
options: { shouldReturn: true }, // Option to return the deleted record
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
## Server-Side Rendering (JSX)
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
**Usage:**
|
|
232
|
+
|
|
233
|
+
Use `defineJSXRoute` in your controller and `htmlResponse` for documentation.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { BaseController, controller, htmlResponse } from '@venizia/ignis';
|
|
237
|
+
|
|
238
|
+
@controller({ path: '/pages' })
|
|
239
|
+
export class PageController extends BaseController {
|
|
240
|
+
|
|
241
|
+
override binding(): void {
|
|
242
|
+
this.defineJSXRoute({
|
|
243
|
+
configs: {
|
|
244
|
+
method: 'get',
|
|
245
|
+
path: '/welcome',
|
|
246
|
+
description: 'Welcome Page',
|
|
247
|
+
responses: htmlResponse({ description: 'HTML Welcome Page' }),
|
|
248
|
+
},
|
|
249
|
+
handler: (c) => {
|
|
250
|
+
const title = 'Welcome to Ignis';
|
|
251
|
+
|
|
252
|
+
// Return JSX directly
|
|
253
|
+
return c.html(
|
|
254
|
+
<html>
|
|
255
|
+
<head><title>{title}</title></head>
|
|
256
|
+
<body>
|
|
257
|
+
<h1>{title}</h1>
|
|
258
|
+
<p>Server-side rendered content.</p>
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
|
261
|
+
);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Custom Middleware
|
|
269
|
+
|
|
270
|
+
Create reusable middleware using Hono's `createMiddleware` helper.
|
|
271
|
+
|
|
272
|
+
### Basic Middleware Pattern
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
import { createMiddleware } from 'hono/factory';
|
|
276
|
+
import type { MiddlewareHandler } from 'hono';
|
|
277
|
+
|
|
278
|
+
// Simple middleware with options
|
|
279
|
+
export const rateLimiter = (opts: { maxRequests: number }): MiddlewareHandler => {
|
|
280
|
+
const { maxRequests } = opts;
|
|
281
|
+
const requests = new Map<string, number>();
|
|
282
|
+
|
|
283
|
+
return createMiddleware(async (c, next) => {
|
|
284
|
+
const ip = c.req.header('x-forwarded-for') ?? 'unknown';
|
|
285
|
+
const count = requests.get(ip) ?? 0;
|
|
286
|
+
|
|
287
|
+
if (count >= maxRequests) {
|
|
288
|
+
return c.json({ error: 'Too many requests' }, 429);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
requests.set(ip, count + 1);
|
|
292
|
+
await next();
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Usage in application
|
|
297
|
+
server.use('/api/*', rateLimiter({ maxRequests: 100 }));
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Middleware with Logging
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { BaseHelper } from '@venizia/ignis';
|
|
304
|
+
import { createMiddleware } from 'hono/factory';
|
|
305
|
+
|
|
306
|
+
export const requestLogger = (): MiddlewareHandler => {
|
|
307
|
+
const helper = new BaseHelper({ scope: 'RequestLogger' });
|
|
308
|
+
|
|
309
|
+
return createMiddleware(async (c, next) => {
|
|
310
|
+
const start = performance.now();
|
|
311
|
+
const method = c.req.method;
|
|
312
|
+
const path = c.req.path;
|
|
313
|
+
|
|
314
|
+
helper.logger.info('[%s] %s - Started', method, path);
|
|
315
|
+
|
|
316
|
+
await next();
|
|
317
|
+
|
|
318
|
+
const duration = performance.now() - start;
|
|
319
|
+
helper.logger.info('[%s] %s - Completed in %dms', method, path, duration.toFixed(2));
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Middleware in Controllers
|
|
325
|
+
|
|
326
|
+
Apply middleware to specific routes in your controller:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
@controller({ path: '/admin' })
|
|
330
|
+
export class AdminController extends BaseController {
|
|
331
|
+
constructor() {
|
|
332
|
+
super({ scope: AdminController.name, path: '/admin' });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
override binding(): void {
|
|
336
|
+
// Apply middleware to all routes in this controller
|
|
337
|
+
this.getRouter().use('*', adminOnlyMiddleware());
|
|
338
|
+
|
|
339
|
+
this.defineRoute({
|
|
340
|
+
configs: { method: 'get', path: '/dashboard', /* ... */ },
|
|
341
|
+
handler: (c) => c.json({ /* ... */ }),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Service Layer Patterns
|
|
348
|
+
|
|
349
|
+
Services contain business logic and orchestrate operations across multiple repositories.
|
|
350
|
+
|
|
351
|
+
### Basic Service
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { BaseService, inject, BindingKeys, BindingNamespaces } from '@venizia/ignis';
|
|
355
|
+
|
|
356
|
+
export class UserService extends BaseService {
|
|
357
|
+
constructor(
|
|
358
|
+
@inject({
|
|
359
|
+
key: BindingKeys.build({
|
|
360
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
361
|
+
key: UserRepository.name,
|
|
362
|
+
}),
|
|
363
|
+
})
|
|
364
|
+
private userRepository: UserRepository,
|
|
365
|
+
|
|
366
|
+
@inject({
|
|
367
|
+
key: BindingKeys.build({
|
|
368
|
+
namespace: BindingNamespaces.REPOSITORY,
|
|
369
|
+
key: OrderRepository.name,
|
|
370
|
+
}),
|
|
371
|
+
})
|
|
372
|
+
private orderRepository: OrderRepository,
|
|
373
|
+
) {
|
|
374
|
+
super({ scope: UserService.name });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async getUserWithOrders(userId: string) {
|
|
378
|
+
const user = await this.userRepository.findById({ id: userId });
|
|
379
|
+
if (!user.data) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const orders = await this.orderRepository.find({
|
|
384
|
+
filter: { where: { userId } },
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
...user.data,
|
|
389
|
+
orders: orders.data,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async deactivateUser(userId: string) {
|
|
394
|
+
// Business logic: cancel pending orders before deactivating
|
|
395
|
+
await this.orderRepository.updateBy({
|
|
396
|
+
where: { userId, status: 'PENDING' },
|
|
397
|
+
data: { status: 'CANCELLED' },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return this.userRepository.updateById({
|
|
401
|
+
id: userId,
|
|
402
|
+
data: { status: 'INACTIVE' },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Using Services in Controllers
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
@controller({ path: '/users' })
|
|
412
|
+
export class UserController extends BaseController {
|
|
413
|
+
constructor(
|
|
414
|
+
@inject({
|
|
415
|
+
key: BindingKeys.build({
|
|
416
|
+
namespace: BindingNamespaces.SERVICE,
|
|
417
|
+
key: UserService.name,
|
|
418
|
+
}),
|
|
419
|
+
})
|
|
420
|
+
private userService: UserService,
|
|
421
|
+
) {
|
|
422
|
+
super({ scope: UserController.name, path: '/users' });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@get({ configs: RouteConfigs.GET_USER_WITH_ORDERS })
|
|
426
|
+
async getUserWithOrders(c: TRouteContext<typeof RouteConfigs.GET_USER_WITH_ORDERS>) {
|
|
427
|
+
const { id } = c.req.valid('param');
|
|
428
|
+
const result = await this.userService.getUserWithOrders(id);
|
|
429
|
+
|
|
430
|
+
if (!result) {
|
|
431
|
+
throw getError({ statusCode: 404, message: 'User not found' });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return c.json(result, HTTP.ResultCodes.RS_2.Ok);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Batch Operations
|
|
440
|
+
|
|
441
|
+
Use `updateBy` and `deleteBy` for bulk operations with filter conditions.
|
|
442
|
+
|
|
443
|
+
### Bulk Update
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// Update all inactive users to archived
|
|
447
|
+
const result = await userRepository.updateBy({
|
|
448
|
+
where: { status: 'INACTIVE', lastLoginAt: { lt: new Date('2024-01-01') } },
|
|
449
|
+
data: { status: 'ARCHIVED' },
|
|
450
|
+
});
|
|
451
|
+
// result.count = number of affected rows
|
|
452
|
+
|
|
453
|
+
// Update ALL records (requires force flag)
|
|
454
|
+
await userRepository.updateBy({
|
|
455
|
+
where: {}, // Empty = all records
|
|
456
|
+
data: { notificationSent: true },
|
|
457
|
+
options: { force: true }, // Required for safety
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Bulk Delete
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// Delete expired sessions
|
|
465
|
+
const result = await sessionRepository.deleteBy({
|
|
466
|
+
where: { expiresAt: { lt: new Date() } },
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Delete with return values
|
|
470
|
+
const deleted = await sessionRepository.deleteBy({
|
|
471
|
+
where: { userId: 'user-123' },
|
|
472
|
+
options: { shouldReturn: true }, // Returns deleted records
|
|
473
|
+
});
|
|
474
|
+
// deleted.data = array of deleted records
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Batch Create
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// Create multiple records at once
|
|
481
|
+
const result = await userRepository.createAll({
|
|
482
|
+
data: [
|
|
483
|
+
{ name: 'Alice', email: 'alice@example.com' },
|
|
484
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
485
|
+
{ name: 'Charlie', email: 'charlie@example.com' },
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
// result.data = array of created records with IDs
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Error Handling
|
|
492
|
+
|
|
493
|
+
Use `getError()` to throw structured errors that are automatically formatted by the framework.
|
|
494
|
+
|
|
495
|
+
### Throwing Errors
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { getError, HTTP } from '@venizia/ignis';
|
|
499
|
+
|
|
500
|
+
// Basic error
|
|
501
|
+
throw getError({ message: 'Something went wrong' });
|
|
502
|
+
// Returns: { statusCode: 400, message: 'Something went wrong' }
|
|
503
|
+
|
|
504
|
+
// With status code
|
|
505
|
+
throw getError({
|
|
506
|
+
statusCode: HTTP.ResultCodes.RS_4.NotFound,
|
|
507
|
+
message: 'User not found',
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// With message code for i18n
|
|
511
|
+
throw getError({
|
|
512
|
+
statusCode: 404,
|
|
513
|
+
message: 'User not found',
|
|
514
|
+
messageCode: 'USER_NOT_FOUND',
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Error Handling in Route Handlers
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
@get({ configs: RouteConfigs.GET_USER })
|
|
522
|
+
async getUser(c: TRouteContext<typeof RouteConfigs.GET_USER>) {
|
|
523
|
+
const { id } = c.req.valid('param');
|
|
524
|
+
|
|
525
|
+
const user = await this.userRepository.findById({ id });
|
|
526
|
+
|
|
527
|
+
if (!user.data) {
|
|
528
|
+
throw getError({
|
|
529
|
+
statusCode: 404,
|
|
530
|
+
message: `User with ID '${id}' not found`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return c.json(user.data, HTTP.ResultCodes.RS_2.Ok);
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Error Response Format
|
|
539
|
+
|
|
540
|
+
All errors are automatically formatted:
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"statusCode": 404,
|
|
545
|
+
"message": "User not found",
|
|
546
|
+
"messageCode": "USER_NOT_FOUND",
|
|
547
|
+
"requestId": "abc123"
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Try-Catch for Complex Operations
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
async processOrder(c: Context) {
|
|
555
|
+
const data = c.req.valid('json');
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const tx = await this.orderRepository.beginTransaction({
|
|
559
|
+
isolationLevel: 'READ COMMITTED',
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const order = await this.orderRepository.create({
|
|
564
|
+
data: { ...data, status: 'PENDING' },
|
|
565
|
+
options: { transaction: tx },
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await this.inventoryService.decrementStock({
|
|
569
|
+
items: data.items,
|
|
570
|
+
transaction: tx,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
await tx.commit();
|
|
574
|
+
return c.json(order.data, HTTP.ResultCodes.RS_2.Created);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
await tx.rollback();
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
} catch (error) {
|
|
580
|
+
this.logger.error('[processOrder] Failed: %s', error);
|
|
581
|
+
|
|
582
|
+
if (error instanceof ApplicationError) {
|
|
583
|
+
throw error; // Re-throw application errors
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
throw getError({
|
|
587
|
+
statusCode: 500,
|
|
588
|
+
message: 'Failed to process order',
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|