@venizia/ignis-docs 0.0.1-8 → 0.0.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/LICENSE.md +1 -0
- package/package.json +2 -2
- package/wiki/changelogs/2025-12-16-initial-architecture.md +145 -0
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
- package/wiki/changelogs/2025-12-17-refactor.md +90 -0
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +130 -0
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +249 -0
- package/wiki/changelogs/index.md +33 -0
- package/wiki/changelogs/planned-transaction-support.md +216 -0
- package/wiki/changelogs/template.md +123 -0
- package/wiki/get-started/5-minute-quickstart.md +1 -1
- package/wiki/get-started/best-practices/api-usage-examples.md +12 -10
- package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
- package/wiki/get-started/best-practices/common-pitfalls.md +7 -5
- package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
- package/wiki/get-started/best-practices/data-modeling.md +91 -40
- package/wiki/get-started/best-practices/security-guidelines.md +3 -1
- package/wiki/get-started/building-a-crud-api.md +63 -78
- package/wiki/get-started/core-concepts/application.md +72 -3
- package/wiki/get-started/core-concepts/bootstrapping.md +566 -0
- package/wiki/get-started/core-concepts/components.md +4 -2
- package/wiki/get-started/core-concepts/controllers.md +14 -14
- package/wiki/get-started/core-concepts/persistent.md +383 -431
- package/wiki/get-started/core-concepts/services.md +21 -27
- package/wiki/get-started/quickstart.md +1 -1
- package/wiki/references/base/bootstrapping.md +789 -0
- package/wiki/references/base/components.md +1 -1
- package/wiki/references/base/controllers.md +40 -16
- package/wiki/references/base/datasources.md +195 -33
- package/wiki/references/base/dependency-injection.md +98 -5
- package/wiki/references/base/models.md +398 -28
- package/wiki/references/base/repositories.md +475 -22
- package/wiki/references/base/services.md +2 -2
- package/wiki/references/components/authentication.md +228 -10
- package/wiki/references/components/health-check.md +1 -1
- package/wiki/references/components/index.md +1 -1
- package/wiki/references/components/swagger.md +1 -1
- package/wiki/references/helpers/error.md +2 -2
- package/wiki/references/helpers/inversion.md +8 -3
- package/wiki/references/src-details/boot.md +379 -0
- package/wiki/references/src-details/core.md +8 -7
- package/wiki/references/src-details/inversion.md +4 -4
- package/wiki/references/utilities/request.md +16 -7
|
@@ -66,6 +66,8 @@ export class Application extends BaseApplication {
|
|
|
66
66
|
|
|
67
67
|
- **Bad:**
|
|
68
68
|
```typescript
|
|
69
|
+
import { ApplicationError, getError } from '@venizia/ignis';
|
|
70
|
+
|
|
69
71
|
// In a Controller
|
|
70
72
|
async createUser(c: Context) {
|
|
71
73
|
const { name, email, companyName } = c.req.valid('json');
|
|
@@ -73,13 +75,13 @@ export class Application extends BaseApplication {
|
|
|
73
75
|
// Complex logic inside the controller
|
|
74
76
|
const existingUser = await this.userRepository.findByEmail(email);
|
|
75
77
|
if (existingUser) {
|
|
76
|
-
throw
|
|
78
|
+
throw getError({ message: 'Email already exists' });
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const company = await this.companyRepository.findOrCreate(companyName);
|
|
80
82
|
const user = await this.userRepository.create({ name, email, companyId: company.id });
|
|
81
83
|
|
|
82
|
-
return c.json(user);
|
|
84
|
+
return c.json(user, HTTP.ResultCodes.RS_2.Ok);
|
|
83
85
|
}
|
|
84
86
|
```
|
|
85
87
|
- **Good:**
|
|
@@ -89,7 +91,7 @@ export class Application extends BaseApplication {
|
|
|
89
91
|
const userData = c.req.valid('json');
|
|
90
92
|
// Delegate to the service
|
|
91
93
|
const newUser = await this.userService.createUser(userData);
|
|
92
|
-
return c.json(newUser);
|
|
94
|
+
return c.json(newUser, HTTP.ResultCodes.RS_2.Ok);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
// In UserService
|
|
@@ -101,7 +103,7 @@ export class Application extends BaseApplication {
|
|
|
101
103
|
}
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
## 4. Missing Environment Variables
|
|
105
107
|
|
|
106
108
|
**Pitfall:** The application fails to start or behaves unexpectedly because required environment variables are not defined in your `.env` file. The framework validates variables prefixed with `APP_ENV_` by default.
|
|
107
109
|
|
|
@@ -121,7 +123,7 @@ APP_ENV_POSTGRES_PASSWORD=password
|
|
|
121
123
|
APP_ENV_POSTGRES_DATABASE=db
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
## 5. Not Using `as const` for Route Definitions
|
|
125
127
|
|
|
126
128
|
**Pitfall:** When using the decorator-based routing with a shared `ROUTE_CONFIGS` object, you forget to add `as const` to the object definition. TypeScript will infer the types too broadly, and you will lose the benefits of type-safe contexts (`TRouteContext`).
|
|
127
129
|
|
|
@@ -56,6 +56,7 @@ The project uses a Makefile for common development tasks:
|
|
|
56
56
|
**Individual package builds:**
|
|
57
57
|
```bash
|
|
58
58
|
make core # Build @venizia/ignis (after dependencies)
|
|
59
|
+
make boot # Build @venizia/ignis-boot
|
|
59
60
|
make helpers # Build @venizia/ignis-helpers
|
|
60
61
|
make inversion # Build @venizia/ignis-inversion
|
|
61
62
|
make dev-configs # Build @venizia/dev-configs
|
|
@@ -65,6 +66,7 @@ make docs # Build documentation
|
|
|
65
66
|
**Force update individual packages:**
|
|
66
67
|
```bash
|
|
67
68
|
make update-core
|
|
69
|
+
make update-boot
|
|
68
70
|
make update-helpers
|
|
69
71
|
make update-inversion
|
|
70
72
|
make update-dev-configs
|
|
@@ -6,30 +6,24 @@ Ignis streamlines data modeling with Drizzle ORM by providing powerful helpers a
|
|
|
6
6
|
|
|
7
7
|
All entity models should extend `BaseEntity`. This provides integration with the framework's repository layer and automatic schema generation support.
|
|
8
8
|
|
|
9
|
-
**
|
|
9
|
+
The recommended pattern is to define the schema and relations as **static properties** on the class. This keeps the definition self-contained and enables powerful type inference.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
BaseEntity,
|
|
14
|
-
model,
|
|
15
|
-
TTableObject,
|
|
16
|
-
} from '@venizia/ignis';
|
|
17
|
-
import { configurationTable } from './schema'; // Your Drizzle schema
|
|
11
|
+
**Example (`src/models/entities/user.model.ts`):**
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
```typescript
|
|
14
|
+
import { BaseEntity, extraUserColumns, generateIdColumnDefs, model } from '@venizia/ignis';
|
|
15
|
+
import { pgTable } from 'drizzle-orm/pg-core';
|
|
22
16
|
|
|
23
|
-
@model({ type: 'entity'
|
|
24
|
-
export class
|
|
25
|
-
|
|
17
|
+
@model({ type: 'entity' })
|
|
18
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
19
|
+
// 1. Define schema as a static property
|
|
20
|
+
static override schema = pgTable('User', {
|
|
21
|
+
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
22
|
+
...extraUserColumns({ idType: 'string' }),
|
|
23
|
+
});
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
name: Configuration.TABLE_NAME,
|
|
30
|
-
schema: configurationTable,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
25
|
+
// 2. Define relations as a static method (return empty array if none)
|
|
26
|
+
static override relations = () => [];
|
|
33
27
|
}
|
|
34
28
|
```
|
|
35
29
|
|
|
@@ -45,7 +39,7 @@ Instead of manually defining common columns like primary keys, timestamps, or au
|
|
|
45
39
|
| `generateTzColumnDefs` | Adds timestamps | `createdAt`, `modifiedAt` (auto-updating) |
|
|
46
40
|
| `generateUserAuditColumnDefs` | Adds audit fields | `createdBy`, `modifiedBy` |
|
|
47
41
|
| `generateDataTypeColumnDefs` | Adds generic value fields | `nValue` (number), `tValue` (text), `jValue` (json), etc. |
|
|
48
|
-
| `
|
|
42
|
+
| `extraUserColumns` | Comprehensive user fields | Combines audit, timestamps, status, and type fields |
|
|
49
43
|
|
|
50
44
|
**Usage Example:**
|
|
51
45
|
|
|
@@ -86,41 +80,98 @@ export const configurationTable = pgTable(
|
|
|
86
80
|
|
|
87
81
|
## 3. Defining Relations
|
|
88
82
|
|
|
89
|
-
|
|
83
|
+
Relations are defined using the `TRelationConfig` structure within the static `relations` method of your model.
|
|
90
84
|
|
|
91
|
-
**Example:**
|
|
85
|
+
**Example (`src/models/entities/configuration.model.ts`):**
|
|
92
86
|
|
|
93
87
|
```typescript
|
|
94
|
-
import {
|
|
95
|
-
|
|
88
|
+
import {
|
|
89
|
+
BaseEntity,
|
|
90
|
+
model,
|
|
91
|
+
RelationTypes,
|
|
92
|
+
TRelationConfig,
|
|
93
|
+
} from '@venizia/ignis';
|
|
94
|
+
import { User } from './user.model';
|
|
95
|
+
|
|
96
|
+
@model({ type: 'entity' })
|
|
97
|
+
export class Configuration extends BaseEntity<typeof Configuration.schema> {
|
|
98
|
+
// ... schema definition ...
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
relations: [
|
|
100
|
+
// Define relations
|
|
101
|
+
static override relations = (): TRelationConfig[] => [
|
|
100
102
|
{
|
|
101
103
|
name: 'creator',
|
|
102
104
|
type: RelationTypes.ONE,
|
|
103
|
-
schema:
|
|
105
|
+
schema: User.schema,
|
|
104
106
|
metadata: {
|
|
105
|
-
fields: [
|
|
106
|
-
references: [
|
|
107
|
+
fields: [Configuration.schema.createdBy],
|
|
108
|
+
references: [User.schema.id],
|
|
107
109
|
},
|
|
108
110
|
},
|
|
109
|
-
]
|
|
110
|
-
}
|
|
111
|
+
];
|
|
112
|
+
}
|
|
111
113
|
```
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
## 4. Repositories and Auto-Discovery
|
|
116
|
+
|
|
117
|
+
Ignis simplifies the connection between models, repositories, and datasources.
|
|
118
|
+
|
|
119
|
+
### DataSource Auto-Discovery
|
|
120
|
+
|
|
121
|
+
DataSources automatically discover their schema from the repositories that bind to them. You **do not** need to manually register schemas in the DataSource constructor.
|
|
114
122
|
|
|
115
123
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
// src/datasources/postgres.datasource.ts
|
|
125
|
+
@datasource({ driver: 'node-postgres' })
|
|
126
|
+
export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
|
|
127
|
+
constructor() {
|
|
119
128
|
super({
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
name: PostgresDataSource.name,
|
|
130
|
+
config: { /* connection config */ },
|
|
131
|
+
// NO schema property needed - auto-discovered!
|
|
123
132
|
});
|
|
124
133
|
}
|
|
134
|
+
|
|
135
|
+
override configure(): ValueOrPromise<void> {
|
|
136
|
+
// This method automatically collects all schemas from bound repositories
|
|
137
|
+
const schema = this.getSchema();
|
|
138
|
+
this.connector = drizzle({ client: new Pool(this.settings), schema });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Repository Binding
|
|
144
|
+
|
|
145
|
+
Repositories use the `@repository` decorator to bind a **Model** to a **DataSource**. This binding is what powers the auto-discovery mechanism.
|
|
146
|
+
|
|
147
|
+
**Pattern 1: Zero Boilerplate (Recommended)**
|
|
148
|
+
|
|
149
|
+
For most repositories, you don't need a constructor. The DataSource is automatically injected.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
@repository({ model: Configuration, dataSource: PostgresDataSource })
|
|
153
|
+
export class ConfigurationRepository extends DefaultCRUDRepository<typeof Configuration.schema> {
|
|
154
|
+
// No constructor needed!
|
|
125
155
|
}
|
|
126
156
|
```
|
|
157
|
+
|
|
158
|
+
**Pattern 2: Explicit Injection (Advanced)**
|
|
159
|
+
|
|
160
|
+
If you need to perform custom initialization or inject additional dependencies, you can define a constructor. **Important:** The first parameter must be the DataSource.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
@repository({ model: User, dataSource: PostgresDataSource })
|
|
164
|
+
export class UserRepository extends ReadableRepository<typeof User.schema> {
|
|
165
|
+
constructor(
|
|
166
|
+
@inject({ key: 'datasources.PostgresDataSource' })
|
|
167
|
+
dataSource: PostgresDataSource,
|
|
168
|
+
) {
|
|
169
|
+
super(dataSource);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Custom methods
|
|
173
|
+
async findByRealm(realm: string) {
|
|
174
|
+
return this.findOne({ filter: { where: { realm } } });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
@@ -72,9 +72,11 @@ const SecureRoute = {
|
|
|
72
72
|
|
|
73
73
|
**Access user in protected routes:**
|
|
74
74
|
```typescript
|
|
75
|
+
import { Authentication, IJWTTokenPayload, ApplicationError, getError } from '@venizia/ignis';
|
|
76
|
+
|
|
75
77
|
const user = c.get(Authentication.CURRENT_USER) as IJWTTokenPayload;
|
|
76
78
|
if (!user.roles.includes('admin')) {
|
|
77
|
-
throw
|
|
79
|
+
throw getError({ statusCode: 403, message: 'Forbidden' });
|
|
78
80
|
}
|
|
79
81
|
```
|
|
80
82
|
|
|
@@ -134,12 +134,10 @@ export type TTodo = TTableObject<TTodoSchema>;
|
|
|
134
134
|
|
|
135
135
|
// 4. Create the Entity class, decorated with @model
|
|
136
136
|
@model({ type: 'entity' })
|
|
137
|
-
export class Todo extends BaseEntity<
|
|
138
|
-
static
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
super({ name: Todo.TABLE_NAME, schema: todoTable });
|
|
142
|
-
}
|
|
137
|
+
export class Todo extends BaseEntity<typeof Todo.schema> {
|
|
138
|
+
static override schema = todoTable;
|
|
139
|
+
static override relations = () => todoRelations.definitions;
|
|
140
|
+
static override TABLE_NAME = 'Todo';
|
|
143
141
|
}
|
|
144
142
|
```
|
|
145
143
|
|
|
@@ -203,7 +201,6 @@ Create `src/datasources/postgres.datasource.ts`:
|
|
|
203
201
|
|
|
204
202
|
```typescript
|
|
205
203
|
// src/datasources/postgres.datasource.ts
|
|
206
|
-
import { Todo, todoRelations, todoTable } from '@/models/todo.model';
|
|
207
204
|
import {
|
|
208
205
|
BaseDataSource,
|
|
209
206
|
datasource,
|
|
@@ -214,61 +211,61 @@ import { drizzle } from 'drizzle-orm/node-postgres';
|
|
|
214
211
|
import { Pool } from 'pg';
|
|
215
212
|
|
|
216
213
|
interface IDSConfigs {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
database?: string;
|
|
223
|
-
};
|
|
214
|
+
host: string;
|
|
215
|
+
port: number;
|
|
216
|
+
database: string;
|
|
217
|
+
user: string;
|
|
218
|
+
password: string;
|
|
224
219
|
}
|
|
225
220
|
|
|
226
|
-
|
|
221
|
+
/**
|
|
222
|
+
* PostgresDataSource with auto-discovery support.
|
|
223
|
+
*
|
|
224
|
+
* How it works:
|
|
225
|
+
* 1. @repository decorator binds model to datasource
|
|
226
|
+
* 2. When configure() is called, getSchema() auto-discovers all bound models
|
|
227
|
+
* 3. Drizzle is initialized with the auto-discovered schema
|
|
228
|
+
*/
|
|
229
|
+
@datasource({ driver: 'node-postgres' })
|
|
227
230
|
export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
|
|
228
231
|
constructor() {
|
|
229
232
|
super({
|
|
230
233
|
name: PostgresDataSource.name,
|
|
231
|
-
|
|
234
|
+
// Driver is read from @datasource decorator - no need to pass here!
|
|
232
235
|
config: {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
database: process.env.APP_ENV_POSTGRES_DATABASE,
|
|
239
|
-
},
|
|
236
|
+
host: process.env.APP_ENV_POSTGRES_HOST ?? 'localhost',
|
|
237
|
+
port: +(process.env.APP_ENV_POSTGRES_PORT ?? 5432),
|
|
238
|
+
database: process.env.APP_ENV_POSTGRES_DATABASE ?? 'todo_db',
|
|
239
|
+
user: process.env.APP_ENV_POSTGRES_USERNAME ?? 'postgres',
|
|
240
|
+
password: process.env.APP_ENV_POSTGRES_PASSWORD ?? '',
|
|
240
241
|
},
|
|
241
|
-
//
|
|
242
|
-
schema: Object.assign(
|
|
243
|
-
{},
|
|
244
|
-
{ [Todo.TABLE_NAME]: todoTable },
|
|
245
|
-
todoRelations.relations,
|
|
246
|
-
),
|
|
242
|
+
// NO schema property - auto-discovered from @repository bindings!
|
|
247
243
|
});
|
|
248
244
|
}
|
|
249
245
|
|
|
250
246
|
override configure(): ValueOrPromise<void> {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
247
|
+
// getSchema() auto-discovers models from @repository bindings
|
|
248
|
+
const schema = this.getSchema();
|
|
249
|
+
|
|
250
|
+
// Log discovered schema for debugging
|
|
251
|
+
const schemaKeys = Object.keys(schema);
|
|
252
|
+
this.logger.debug(
|
|
253
|
+
'[configure] Auto-discovered schema | Schema + Relations (%s): %o',
|
|
254
|
+
schemaKeys.length,
|
|
255
|
+
schemaKeys,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const client = new Pool(this.settings);
|
|
259
|
+
this.connector = drizzle({ client, schema });
|
|
264
260
|
}
|
|
265
261
|
}
|
|
266
262
|
```
|
|
267
263
|
|
|
268
264
|
**Key Points:**
|
|
269
|
-
-
|
|
265
|
+
- Schema is auto-discovered from `@repository` decorators - no manual registration needed
|
|
266
|
+
- Uses `getSchema()` for lazy schema resolution (resolves when all models are loaded)
|
|
270
267
|
- Uses environment variables for connection config
|
|
271
|
-
- Implements connection lifecycle methods
|
|
268
|
+
- Implements connection lifecycle methods (`connect()`, `disconnect()`)
|
|
272
269
|
|
|
273
270
|
> **Deep Dive:** See [DataSources Reference](../references/base/datasources.md) for advanced configuration and multiple database support.
|
|
274
271
|
|
|
@@ -280,26 +277,15 @@ Create `src/repositories/todo.repository.ts`:
|
|
|
280
277
|
|
|
281
278
|
```typescript
|
|
282
279
|
// src/repositories/todo.repository.ts
|
|
283
|
-
import { Todo
|
|
284
|
-
import {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
@repository
|
|
292
|
-
export class TodoRepository extends DefaultCRUDRepository<TTodoSchema> {
|
|
293
|
-
constructor(
|
|
294
|
-
@inject({ key: 'datasources.PostgresDataSource' })
|
|
295
|
-
dataSource: IDataSource,
|
|
296
|
-
) {
|
|
297
|
-
super({
|
|
298
|
-
dataSource,
|
|
299
|
-
entityClass: Todo,
|
|
300
|
-
relations: todoRelations.definitions,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
280
|
+
import { Todo } from '@/models/todo.model';
|
|
281
|
+
import { PostgresDataSource } from '@/datasources/postgres.datasource';
|
|
282
|
+
import { DefaultCRUDRepository, repository } from '@venizia/ignis';
|
|
283
|
+
|
|
284
|
+
// Both 'model' and 'dataSource' are required for schema auto-discovery
|
|
285
|
+
@repository({ model: Todo, dataSource: PostgresDataSource })
|
|
286
|
+
export class TodoRepository extends DefaultCRUDRepository<typeof Todo.schema> {
|
|
287
|
+
// No constructor needed! DataSource and relations are auto-resolved
|
|
288
|
+
// from the @repository decorator and entity's static properties
|
|
303
289
|
}
|
|
304
290
|
```
|
|
305
291
|
|
|
@@ -358,15 +344,15 @@ export class TodoController extends _Controller {
|
|
|
358
344
|
**Auto-generated Endpoints:**
|
|
359
345
|
| Method | Path | Description |
|
|
360
346
|
|--------|------|-------------|
|
|
361
|
-
| GET | `/todos` | List all todos |
|
|
362
|
-
| GET | `/todos/:id` | Get todo by ID |
|
|
363
|
-
| GET | `/todos/find-one` | Find one todo by filter |
|
|
364
|
-
| GET | `/todos/count` | Count todos |
|
|
365
|
-
| POST | `/todos` | Create todo |
|
|
366
|
-
| PATCH | `/todos/:id` | Update todo by ID |
|
|
367
|
-
| PATCH | `/todos` | Update multiple todos |
|
|
368
|
-
| DELETE | `/todos/:id` | Delete todo by ID |
|
|
369
|
-
| DELETE | `/todos` | Delete multiple todos |
|
|
347
|
+
| GET | `/todos` | List all todos (find) |
|
|
348
|
+
| GET | `/todos/:id` | Get todo by ID (findById) |
|
|
349
|
+
| GET | `/todos/find-one` | Find one todo by filter (findOne) |
|
|
350
|
+
| GET | `/todos/count` | Count todos (count) |
|
|
351
|
+
| POST | `/todos` | Create todo (create) |
|
|
352
|
+
| PATCH | `/todos/:id` | Update todo by ID (updateById) |
|
|
353
|
+
| PATCH | `/todos` | Update multiple todos by filter (updateBy) |
|
|
354
|
+
| DELETE | `/todos/:id` | Delete todo by ID (deleteById) |
|
|
355
|
+
| DELETE | `/todos` | Delete multiple todos by filter (deleteBy) |
|
|
370
356
|
|
|
371
357
|
> **Deep Dive:** See [ControllerFactory Reference](../references/base/controllers.md#controllerfactory) for customization options.
|
|
372
358
|
|
|
@@ -639,8 +625,7 @@ Now that you've built the Todo API, try building a **User** feature on your own!
|
|
|
639
625
|
|
|
640
626
|
**Challenge checklist:**
|
|
641
627
|
- [ ] Create `src/models/user.model.ts`
|
|
642
|
-
- [ ]
|
|
643
|
-
- [ ] Create `src/repositories/user.repository.ts`
|
|
628
|
+
- [ ] Create `src/repositories/user.repository.ts` (this auto-registers User with PostgresDataSource)
|
|
644
629
|
- [ ] Create `src/controllers/user.controller.ts`
|
|
645
630
|
- [ ] Register repository and controller in `application.ts`
|
|
646
631
|
- [ ] Run migration: `bun run migrate:dev`
|
|
@@ -660,7 +645,7 @@ For complex validation or business rules, create a Service layer:
|
|
|
660
645
|
|
|
661
646
|
```typescript
|
|
662
647
|
// src/services/todo.service.ts
|
|
663
|
-
import { BaseService, inject } from '@venizia/ignis';
|
|
648
|
+
import { BaseService, inject, getError } from '@venizia/ignis';
|
|
664
649
|
import { TodoRepository } from '@/repositories/todo.repository';
|
|
665
650
|
|
|
666
651
|
export class TodoService extends BaseService {
|
|
@@ -674,7 +659,7 @@ export class TodoService extends BaseService {
|
|
|
674
659
|
async createTodo(data: any) {
|
|
675
660
|
// Business logic validation
|
|
676
661
|
if (data.title.length < 3) {
|
|
677
|
-
throw
|
|
662
|
+
throw getError({ message: 'Title too short' });
|
|
678
663
|
}
|
|
679
664
|
|
|
680
665
|
// Check for duplicates
|
|
@@ -682,7 +667,7 @@ export class TodoService extends BaseService {
|
|
|
682
667
|
filter: { where: { title: data.title } },
|
|
683
668
|
});
|
|
684
669
|
if (existing) {
|
|
685
|
-
throw
|
|
670
|
+
throw getError({ message: 'Todo already exists' });
|
|
686
671
|
}
|
|
687
672
|
|
|
688
673
|
return this.todoRepository.create({ data });
|
|
@@ -42,10 +42,13 @@ export class Application extends BaseApplication {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
preConfigure(): ValueOrPromise<void> {
|
|
45
|
-
//
|
|
45
|
+
// Manual registration (traditional approach)
|
|
46
46
|
this.dataSource(MyDataSource);
|
|
47
47
|
this.service(MyService);
|
|
48
48
|
this.controller(MyController);
|
|
49
|
+
|
|
50
|
+
// Or use boot system for auto-discovery (recommended for larger apps)
|
|
51
|
+
// See Bootstrapping section below
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
postConfigure(): ValueOrPromise<void> {
|
|
@@ -75,10 +78,74 @@ The `BaseApplication` class provides several **overridable hook methods** that a
|
|
|
75
78
|
| :--- | :--- |
|
|
76
79
|
| `getAppInfo()` | **Required.** Return application metadata, usually from `package.json`. Used for OpenAPI docs. |
|
|
77
80
|
| `staticConfigure()` | Configure static file serving. |
|
|
78
|
-
| `preConfigure()` | **Most Important Hook.** Set up application resources like components, controllers, services, and datasources. |
|
|
81
|
+
| `preConfigure()` | **Most Important Hook.** Set up application resources like components, controllers, services, and datasources. Can be skipped if using boot system. |
|
|
79
82
|
| `postConfigure()` | Perform actions *after* all resources have been configured and instantiated. |
|
|
80
83
|
| `setupMiddlewares()`| Add custom application-level middlewares to the Hono instance. |
|
|
81
84
|
|
|
85
|
+
## Bootstrapping (Auto-discovery)
|
|
86
|
+
|
|
87
|
+
The boot system provides automatic artifact discovery and loading, eliminating manual registration. When enabled, it scans your project directory and automatically loads controllers, services, repositories, and datasources.
|
|
88
|
+
|
|
89
|
+
> **Detailed Guide:** See [Bootstrapping Concepts](./bootstrapping.md) for complete documentation.
|
|
90
|
+
|
|
91
|
+
### Enabling Boot System
|
|
92
|
+
|
|
93
|
+
Add `bootOptions` to your application config:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export const appConfigs: IApplicationConfigs = {
|
|
97
|
+
host: process.env.APP_ENV_SERVER_HOST,
|
|
98
|
+
port: +(process.env.APP_ENV_SERVER_PORT ?? 3000),
|
|
99
|
+
// Enable boot system
|
|
100
|
+
bootOptions: {
|
|
101
|
+
datasources: { dirs: ['datasources'] },
|
|
102
|
+
repositories: { dirs: ['repositories'] },
|
|
103
|
+
services: { dirs: ['services'] },
|
|
104
|
+
controllers: { dirs: ['controllers'] }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
With boot enabled, you can skip manual registration in `preConfigure()`:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export class Application extends BaseApplication {
|
|
113
|
+
// No need to register artifacts manually!
|
|
114
|
+
// Boot system handles it automatically
|
|
115
|
+
|
|
116
|
+
preConfigure(): ValueOrPromise<void> {
|
|
117
|
+
// Only register things that need custom configuration
|
|
118
|
+
// Everything else is auto-discovered
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Boot vs Manual Registration
|
|
124
|
+
|
|
125
|
+
| Approach | Use Case | Pros | Cons |
|
|
126
|
+
|----------|----------|------|------|
|
|
127
|
+
| **Boot System** | Apps with 10+ artifacts per type | Auto-discovery, scalable, clean code | Requires file naming conventions |
|
|
128
|
+
| **Manual Registration** | Small apps (< 5 artifacts) | Fine-grained control, explicit | Tedious, maintenance burden |
|
|
129
|
+
|
|
130
|
+
### Project Structure for Boot
|
|
131
|
+
|
|
132
|
+
Follow naming conventions for auto-discovery:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
src/
|
|
136
|
+
├── datasources/
|
|
137
|
+
│ └── postgres.datasource.js
|
|
138
|
+
├── repositories/
|
|
139
|
+
│ ├── user.repository.js
|
|
140
|
+
│ └── product.repository.js
|
|
141
|
+
├── services/
|
|
142
|
+
│ ├── auth.service.js
|
|
143
|
+
│ └── user.service.js
|
|
144
|
+
└── controllers/
|
|
145
|
+
├── auth.controller.js
|
|
146
|
+
└── user.controller.js
|
|
147
|
+
```
|
|
148
|
+
|
|
82
149
|
## Lifecycle Diagram
|
|
83
150
|
|
|
84
151
|
This diagram shows the sequence of operations during application startup. The methods you can override are highlighted.
|
|
@@ -90,7 +157,8 @@ graph TD
|
|
|
90
157
|
|
|
91
158
|
subgraph "initialize() Sequence"
|
|
92
159
|
direction TB
|
|
93
|
-
B -->
|
|
160
|
+
B --> B0["boot() - if bootOptions configured"];
|
|
161
|
+
B0 --> B1["printStartUpInfo"];
|
|
94
162
|
B1 --> B2["validateEnvs"];
|
|
95
163
|
B2 --> B3["registerDefaultMiddlewares"];
|
|
96
164
|
B3 --> B4["staticConfigure()"];
|
|
@@ -129,6 +197,7 @@ Application configuration is passed to the `BaseApplication` constructor via an
|
|
|
129
197
|
| `path.isStrict`| `boolean`| `true` | If `true`, the router is strict about trailing slashes. |
|
|
130
198
|
| `debug.showRoutes`| `boolean`| `false`| If `true`, prints all registered routes to the console on startup. |
|
|
131
199
|
| `favicon` | `string` | `'🔥'` | An emoji to be used as the application's favicon. |
|
|
200
|
+
| `bootOptions` | `IBootOptions` | `undefined` | Enable auto-discovery of artifacts. See [Bootstrapping](./bootstrapping.md). |
|
|
132
201
|
|
|
133
202
|
### Example Configuration
|
|
134
203
|
|