@venizia/ignis-docs 0.0.1-9 → 0.0.2
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/{v0.0.1-7-initial-architecture.md → 2025-12-16-initial-architecture.md} +20 -12
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +300 -0
- package/wiki/changelogs/2025-12-17-refactor.md +80 -12
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +28 -90
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +101 -297
- package/wiki/changelogs/index.md +20 -8
- package/wiki/changelogs/planned-schema-migrator.md +561 -0
- package/wiki/changelogs/planned-transaction-support.md +216 -0
- package/wiki/changelogs/template.md +123 -0
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -2
- package/wiki/get-started/best-practices/architectural-patterns.md +2 -2
- package/wiki/get-started/best-practices/code-style-standards.md +575 -10
- package/wiki/get-started/best-practices/common-pitfalls.md +5 -3
- package/wiki/get-started/best-practices/contribution-workflow.md +2 -0
- package/wiki/get-started/best-practices/data-modeling.md +91 -34
- package/wiki/get-started/best-practices/security-guidelines.md +3 -1
- package/wiki/get-started/building-a-crud-api.md +3 -3
- 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/persistent.md +350 -378
- package/wiki/get-started/core-concepts/services.md +21 -27
- package/wiki/references/base/bootstrapping.md +789 -0
- package/wiki/references/base/components.md +1 -1
- package/wiki/references/base/dependency-injection.md +95 -2
- package/wiki/references/base/services.md +2 -2
- package/wiki/references/components/authentication.md +4 -3
- package/wiki/references/components/index.md +1 -1
- package/wiki/references/helpers/error.md +2 -2
- package/wiki/references/src-details/boot.md +379 -0
- package/wiki/references/src-details/core.md +2 -2
- package/wiki/changelogs/v0.0.1-8-model-repo-datasource-refactor.md +0 -278
|
@@ -15,7 +15,7 @@ This package provides:
|
|
|
15
15
|
- **Prettier settings** - Consistent formatting across all Ignis projects
|
|
16
16
|
- **TypeScript configs** - Shared base and common configurations
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### Prettier Configuration
|
|
19
19
|
|
|
20
20
|
Automatic code formatting eliminates style debates.
|
|
21
21
|
|
|
@@ -50,9 +50,7 @@ bun run prettier:cli # Check formatting
|
|
|
50
50
|
bun run prettier:fix # Auto-fix
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
## ESLint Configuration
|
|
53
|
+
### ESLint Configuration
|
|
56
54
|
|
|
57
55
|
Prevents common errors and enforces best practices.
|
|
58
56
|
|
|
@@ -88,9 +86,7 @@ bun run eslint --fix # Auto-fix issues
|
|
|
88
86
|
bun run lint:fix # Run both ESLint + Prettier
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
## TypeScript Configuration
|
|
89
|
+
### TypeScript Configuration
|
|
94
90
|
|
|
95
91
|
Use the centralized TypeScript configs:
|
|
96
92
|
|
|
@@ -121,6 +117,358 @@ Use the centralized TypeScript configs:
|
|
|
121
117
|
|
|
122
118
|
See [`@venizia/dev-configs` documentation](../../references/src-details/dev-configs.md) for full details.
|
|
123
119
|
|
|
120
|
+
## Naming Conventions
|
|
121
|
+
|
|
122
|
+
### Class Names
|
|
123
|
+
|
|
124
|
+
| Type | Pattern | Example |
|
|
125
|
+
|------|---------|---------|
|
|
126
|
+
| Components | `[Feature]Component` | `HealthCheckComponent`, `AuthComponent` |
|
|
127
|
+
| Controllers | `[Feature]Controller` | `UserController`, `AuthController` |
|
|
128
|
+
| Services | `[Feature]Service` | `JWTTokenService`, `PaymentService` |
|
|
129
|
+
| Repositories | `[Feature]Repository` | `UserRepository`, `OrderRepository` |
|
|
130
|
+
| Strategies | `[Feature]Strategy` | `JWTAuthenticationStrategy` |
|
|
131
|
+
| Factories | `[Feature]Factory` | `UIProviderFactory` |
|
|
132
|
+
|
|
133
|
+
### File Names
|
|
134
|
+
|
|
135
|
+
Both styles are acceptable: `[type].ts` or `[name].[type].ts`
|
|
136
|
+
|
|
137
|
+
| Type | Single File | Multiple Files |
|
|
138
|
+
|------|-------------|----------------|
|
|
139
|
+
| Components | `component.ts` | `auth.component.ts` |
|
|
140
|
+
| Controllers | `controller.ts` | `user.controller.ts` |
|
|
141
|
+
| Services | `service.ts` | `jwt-token.service.ts` |
|
|
142
|
+
| Repositories | `repository.ts` | `user.repository.ts` |
|
|
143
|
+
| Types/Interfaces | `types.ts` | `user.types.ts` |
|
|
144
|
+
| Constants | `constants.ts` | `keys.ts`, `rest-paths.ts` |
|
|
145
|
+
| Schemas | `schema.ts` | `sign-in.schema.ts` |
|
|
146
|
+
|
|
147
|
+
**Guidelines:**
|
|
148
|
+
- Use `[type].ts` when there's only one file of that type in the folder
|
|
149
|
+
- Use `[name].[type].ts` when there are multiple files of the same type
|
|
150
|
+
- Use kebab-case for multi-word names: `jwt-token.service.ts`
|
|
151
|
+
|
|
152
|
+
### Type and Interface Prefixes
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Interfaces use 'I' prefix
|
|
156
|
+
interface IHealthCheckOptions {
|
|
157
|
+
restOptions: { path: string };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface IAuthService {
|
|
161
|
+
signIn(context: Context): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Type aliases use 'T' prefix
|
|
165
|
+
type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
166
|
+
type TRouteContext = Context<Env, Path, Input>;
|
|
167
|
+
|
|
168
|
+
// Generic constraints
|
|
169
|
+
type TTableSchemaWithId = { id: PgColumn };
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Binding Keys
|
|
173
|
+
|
|
174
|
+
Use static class with `@app/[component]/[feature]` format:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
export class HealthCheckBindingKeys {
|
|
178
|
+
static readonly HEALTH_CHECK_OPTIONS = '@app/health-check/options';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class SocketIOBindingKeys {
|
|
182
|
+
static readonly SOCKET_IO_INSTANCE = '@app/socket-io/instance';
|
|
183
|
+
static readonly SERVER_OPTIONS = '@app/socket-io/server-options';
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Directory Structure
|
|
188
|
+
|
|
189
|
+
### Component Organization
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
src/components/[feature]/
|
|
193
|
+
├── index.ts # Barrel exports
|
|
194
|
+
├── component.ts # IoC binding setup
|
|
195
|
+
├── controller.ts # Route handlers
|
|
196
|
+
└── common/
|
|
197
|
+
├── index.ts # Barrel exports
|
|
198
|
+
├── keys.ts # Binding key constants
|
|
199
|
+
├── types.ts # Interfaces and types
|
|
200
|
+
└── rest-paths.ts # Route path constants
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Complex Component (with multiple features)
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
src/components/auth/
|
|
207
|
+
├── index.ts
|
|
208
|
+
├── authenticate/
|
|
209
|
+
│ ├── index.ts
|
|
210
|
+
│ ├── component.ts
|
|
211
|
+
│ ├── common/
|
|
212
|
+
│ ├── controllers/
|
|
213
|
+
│ ├── services/
|
|
214
|
+
│ └── strategies/
|
|
215
|
+
└── models/
|
|
216
|
+
├── entities/ # Database models
|
|
217
|
+
└── requests/ # Request schemas
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Barrel Exports
|
|
221
|
+
|
|
222
|
+
Every folder should have an `index.ts` that re-exports its contents:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// components/health-check/index.ts
|
|
226
|
+
export * from './common';
|
|
227
|
+
export * from './component';
|
|
228
|
+
export * from './controller';
|
|
229
|
+
|
|
230
|
+
// components/health-check/common/index.ts
|
|
231
|
+
export * from './keys';
|
|
232
|
+
export * from './rest-paths';
|
|
233
|
+
export * from './types';
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Constants Pattern
|
|
237
|
+
|
|
238
|
+
**Prefer static classes over enums** for better tree-shaking and extensibility.
|
|
239
|
+
|
|
240
|
+
### Basic Constants
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
export class Authentication {
|
|
244
|
+
static readonly STRATEGY_BASIC = 'basic';
|
|
245
|
+
static readonly STRATEGY_JWT = 'jwt';
|
|
246
|
+
static readonly TYPE_BEARER = 'Bearer';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class HealthCheckRestPaths {
|
|
250
|
+
static readonly ROOT = '/';
|
|
251
|
+
static readonly PING = '/ping';
|
|
252
|
+
static readonly METRICS = '/metrics';
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Typed Constants with Validation
|
|
257
|
+
|
|
258
|
+
For constants that need type extraction and runtime validation, use this pattern:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { TConstValue } from '@venizia/ignis-helpers';
|
|
262
|
+
|
|
263
|
+
export class DocumentUITypes {
|
|
264
|
+
// 1. Define static readonly values
|
|
265
|
+
static readonly SWAGGER = 'swagger';
|
|
266
|
+
static readonly SCALAR = 'scalar';
|
|
267
|
+
|
|
268
|
+
// 2. Create a Set for O(1) validation lookup
|
|
269
|
+
static readonly SCHEME_SET = new Set([this.SWAGGER, this.SCALAR]);
|
|
270
|
+
|
|
271
|
+
// 3. Validation helper method
|
|
272
|
+
static isValid(value: string): boolean {
|
|
273
|
+
return this.SCHEME_SET.has(value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 4. Extract union type from class values
|
|
278
|
+
export type TDocumentUIType = TConstValue<typeof DocumentUITypes>;
|
|
279
|
+
// Result: 'swagger' | 'scalar'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Full Example with Usage:**
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { TConstValue } from '@venizia/ignis-helpers';
|
|
286
|
+
|
|
287
|
+
export class UserStatuses {
|
|
288
|
+
static readonly ACTIVE = 'active';
|
|
289
|
+
static readonly INACTIVE = 'inactive';
|
|
290
|
+
static readonly PENDING = 'pending';
|
|
291
|
+
static readonly BANNED = 'banned';
|
|
292
|
+
|
|
293
|
+
static readonly SCHEME_SET = new Set([
|
|
294
|
+
this.ACTIVE,
|
|
295
|
+
this.INACTIVE,
|
|
296
|
+
this.PENDING,
|
|
297
|
+
this.BANNED,
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
static isValid(value: string): boolean {
|
|
301
|
+
return this.SCHEME_SET.has(value);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Optional: get all values as array
|
|
305
|
+
static values(): string[] {
|
|
306
|
+
return [...this.SCHEME_SET];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Type-safe union type
|
|
311
|
+
export type TUserStatus = TConstValue<typeof UserStatuses>;
|
|
312
|
+
// Result: 'active' | 'inactive' | 'pending' | 'banned'
|
|
313
|
+
|
|
314
|
+
// Usage in interfaces
|
|
315
|
+
interface IUser {
|
|
316
|
+
id: string;
|
|
317
|
+
status: TUserStatus; // Type-safe!
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Usage with validation
|
|
321
|
+
function updateUserStatus(userId: string, status: string) {
|
|
322
|
+
if (!UserStatuses.isValid(status)) {
|
|
323
|
+
throw getError({
|
|
324
|
+
statusCode: HTTP.ResultCodes.RS_4.BadRequest,
|
|
325
|
+
message: `Invalid status: ${status}. Valid: ${UserStatuses.values().join(', ')}`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// status is validated at runtime
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Enum vs Static Class Comparison
|
|
333
|
+
|
|
334
|
+
| Aspect | Static Class | TypeScript Enum |
|
|
335
|
+
|--------|--------------|-----------------|
|
|
336
|
+
| Tree-shaking | Full support | Partial (IIFE blocks it) |
|
|
337
|
+
| Bundle size | Minimal | Larger (IIFE wrapper) |
|
|
338
|
+
| Runtime validation | O(1) with `Set` | O(n) with `Object.values()` |
|
|
339
|
+
| Type extraction | `TConstValue<typeof X>` → values | `keyof typeof X` → keys (not values!) |
|
|
340
|
+
| Add methods | Yes | Not possible |
|
|
341
|
+
| Compiled output | Clean class | IIFE wrapper |
|
|
342
|
+
|
|
343
|
+
**Compiled JavaScript:**
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Enum compiles to IIFE (not tree-shakable)
|
|
347
|
+
var UserStatus;
|
|
348
|
+
(function (UserStatus) {
|
|
349
|
+
UserStatus["ACTIVE"] = "active";
|
|
350
|
+
})(UserStatus || (UserStatus = {}));
|
|
351
|
+
|
|
352
|
+
// Static class compiles cleanly
|
|
353
|
+
class UserStatuses { }
|
|
354
|
+
UserStatuses.ACTIVE = 'active';
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Type Extraction Difference:**
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// Enum - extracts KEYS
|
|
361
|
+
type T = keyof typeof UserStatus; // 'ACTIVE' | 'INACTIVE'
|
|
362
|
+
|
|
363
|
+
// Static Class - extracts VALUES
|
|
364
|
+
type T = TConstValue<typeof UserStatuses>; // 'active' | 'inactive'
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**When to use `const enum`:** Only for numeric flags with no iteration needed (values are inlined, zero runtime). But doesn't work with `--isolatedModules`.
|
|
368
|
+
|
|
369
|
+
**Verdict:** Use Static Class for 90% of cases - better tree-shaking, easy validation, type-safe values, extensible with methods.
|
|
370
|
+
|
|
371
|
+
## Configuration Patterns
|
|
372
|
+
|
|
373
|
+
### Default Options
|
|
374
|
+
|
|
375
|
+
Every configurable class should define `DEFAULT_OPTIONS`:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const DEFAULT_OPTIONS: IHealthCheckOptions = {
|
|
379
|
+
restOptions: { path: '/health' },
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
|
|
383
|
+
identifier: 'SOCKET_IO_SERVER',
|
|
384
|
+
path: '/io',
|
|
385
|
+
cors: {
|
|
386
|
+
origin: '*',
|
|
387
|
+
methods: ['GET', 'POST'],
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Option Merging
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// In component constructor or binding
|
|
396
|
+
const extraOptions = this.application.get<Partial<IServerOptions>>({
|
|
397
|
+
key: BindingKeys.SERVER_OPTIONS,
|
|
398
|
+
isOptional: true,
|
|
399
|
+
}) ?? {};
|
|
400
|
+
|
|
401
|
+
this.options = Object.assign({}, DEFAULT_OPTIONS, extraOptions);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Constructor Validation
|
|
405
|
+
|
|
406
|
+
Validate required options in the constructor:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
constructor(options: IJWTTokenServiceOptions) {
|
|
410
|
+
super({ scope: JWTTokenService.name });
|
|
411
|
+
|
|
412
|
+
if (!options.jwtSecret) {
|
|
413
|
+
throw getError({
|
|
414
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
415
|
+
message: '[JWTTokenService] Invalid jwtSecret',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (!options.applicationSecret) {
|
|
420
|
+
throw getError({
|
|
421
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
422
|
+
message: '[JWTTokenService] Invalid applicationSecret',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.options = options;
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Logging Patterns
|
|
431
|
+
|
|
432
|
+
### Method Context Prefix
|
|
433
|
+
|
|
434
|
+
Always include class and method context in log messages:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
// Format: [ClassName][methodName] Message with %s placeholders
|
|
438
|
+
this.logger.info('[binding] Asset storage bound | Key: %s | Type: %s', key, storageType);
|
|
439
|
+
this.logger.debug('[authenticate] Token validated | User: %s', userId);
|
|
440
|
+
this.logger.warn('[register] Skipping duplicate registration | Type: %s', opts.type);
|
|
441
|
+
this.logger.error('[generate] Token generation failed | Error: %s', error.message);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Structured Data
|
|
445
|
+
|
|
446
|
+
Use format specifiers for structured logging:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// %s - string, %d - number, %j - JSON object
|
|
450
|
+
this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.email);
|
|
451
|
+
this.logger.debug('[config] Server options: %j', this.serverOptions);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Scope Naming
|
|
455
|
+
|
|
456
|
+
Every class extending a base class should set its scope using `ClassName.name`:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
export class JWTTokenService extends BaseService {
|
|
460
|
+
constructor() {
|
|
461
|
+
super({ scope: JWTTokenService.name });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export class UserController extends BaseController {
|
|
466
|
+
constructor() {
|
|
467
|
+
super({ scope: UserController.name });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
124
472
|
## Environment Variables Management
|
|
125
473
|
|
|
126
474
|
Avoid using `process.env` directly in your business logic. Instead, use the `applicationEnvironment` helper and define your keys as constants. This ensures type safety and centralized management.
|
|
@@ -147,7 +495,8 @@ const retries = applicationEnvironment.get<number>(EnvironmentKeys.APP_ENV_MAX_R
|
|
|
147
495
|
|
|
148
496
|
Use the `getError` helper and `HTTP` constants to throw consistent, formatted exceptions that the framework's error handler can process correctly.
|
|
149
497
|
|
|
150
|
-
|
|
498
|
+
### Basic Error
|
|
499
|
+
|
|
151
500
|
```typescript
|
|
152
501
|
import { getError, HTTP } from '@venizia/ignis';
|
|
153
502
|
|
|
@@ -155,9 +504,225 @@ if (!record) {
|
|
|
155
504
|
throw getError({
|
|
156
505
|
statusCode: HTTP.ResultCodes.RS_4.NotFound,
|
|
157
506
|
message: 'Record not found',
|
|
158
|
-
|
|
159
|
-
details: { id: requestedId }
|
|
507
|
+
details: { id: requestedId },
|
|
160
508
|
});
|
|
161
509
|
}
|
|
162
510
|
```
|
|
163
511
|
|
|
512
|
+
### Error with Context
|
|
513
|
+
|
|
514
|
+
Include class/method context in error messages:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// Format: [ClassName][methodName] Descriptive message
|
|
518
|
+
throw getError({
|
|
519
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
520
|
+
message: '[JWTTokenService][generate] Failed to generate token',
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
throw getError({
|
|
524
|
+
statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
|
|
525
|
+
message: '[AuthMiddleware][authenticate] Missing authorization header',
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Validation Errors
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
constructor(options: IServiceOptions) {
|
|
533
|
+
if (!options.apiKey) {
|
|
534
|
+
throw getError({
|
|
535
|
+
statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
|
|
536
|
+
message: '[PaymentService] Missing required apiKey configuration',
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### HTTP Status Code Categories
|
|
543
|
+
|
|
544
|
+
| Category | Constant | Use Case |
|
|
545
|
+
|----------|----------|----------|
|
|
546
|
+
| Success | `HTTP.ResultCodes.RS_2.Ok` | Successful response |
|
|
547
|
+
| Created | `HTTP.ResultCodes.RS_2.Created` | Resource created |
|
|
548
|
+
| Bad Request | `HTTP.ResultCodes.RS_4.BadRequest` | Invalid input |
|
|
549
|
+
| Unauthorized | `HTTP.ResultCodes.RS_4.Unauthorized` | Missing/invalid auth |
|
|
550
|
+
| Forbidden | `HTTP.ResultCodes.RS_4.Forbidden` | Insufficient permissions |
|
|
551
|
+
| Not Found | `HTTP.ResultCodes.RS_4.NotFound` | Resource not found |
|
|
552
|
+
| Internal Error | `HTTP.ResultCodes.RS_5.InternalServerError` | Server errors |
|
|
553
|
+
|
|
554
|
+
## Route Definition Patterns
|
|
555
|
+
|
|
556
|
+
Ignis supports three methods for defining routes. Choose based on your needs:
|
|
557
|
+
|
|
558
|
+
### Method 1: Config-Driven Routes
|
|
559
|
+
|
|
560
|
+
Define route configurations as constants:
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// common/rest-paths.ts
|
|
564
|
+
export class UserRestPaths {
|
|
565
|
+
static readonly ROOT = '/';
|
|
566
|
+
static readonly BY_ID = '/:id';
|
|
567
|
+
static readonly PROFILE = '/profile';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// common/route-configs.ts
|
|
571
|
+
export const ROUTE_CONFIGS = {
|
|
572
|
+
[UserRestPaths.ROOT]: {
|
|
573
|
+
method: HTTP.Methods.GET,
|
|
574
|
+
path: UserRestPaths.ROOT,
|
|
575
|
+
responses: jsonResponse({
|
|
576
|
+
[HTTP.ResultCodes.RS_2.Ok]: UserListSchema,
|
|
577
|
+
}),
|
|
578
|
+
},
|
|
579
|
+
[UserRestPaths.BY_ID]: {
|
|
580
|
+
method: HTTP.Methods.GET,
|
|
581
|
+
path: UserRestPaths.BY_ID,
|
|
582
|
+
request: {
|
|
583
|
+
params: z.object({ id: z.string().uuid() }),
|
|
584
|
+
},
|
|
585
|
+
responses: jsonResponse({
|
|
586
|
+
[HTTP.ResultCodes.RS_2.Ok]: UserSchema,
|
|
587
|
+
[HTTP.ResultCodes.RS_4.NotFound]: ErrorSchema,
|
|
588
|
+
}),
|
|
589
|
+
},
|
|
590
|
+
} as const;
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Method 2: Using `@api` Decorator
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
@controller({ path: '/users' })
|
|
597
|
+
export class UserController extends BaseController {
|
|
598
|
+
|
|
599
|
+
@api({ configs: ROUTE_CONFIGS[UserRestPaths.ROOT] })
|
|
600
|
+
list(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.ROOT]>) {
|
|
601
|
+
return context.json({ users: [] }, HTTP.ResultCodes.RS_2.Ok);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
@api({ configs: ROUTE_CONFIGS[UserRestPaths.BY_ID] })
|
|
605
|
+
getById(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.BY_ID]>) {
|
|
606
|
+
const { id } = context.req.valid('param');
|
|
607
|
+
return context.json({ id, name: 'User' }, HTTP.ResultCodes.RS_2.Ok);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Method 3: Using `bindRoute` (Programmatic)
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
@controller({ path: '/health' })
|
|
616
|
+
export class HealthCheckController extends BaseController {
|
|
617
|
+
constructor() {
|
|
618
|
+
super({ scope: HealthCheckController.name });
|
|
619
|
+
|
|
620
|
+
this.bindRoute({ configs: ROUTE_CONFIGS['/'] }).to({
|
|
621
|
+
handler: context => context.json({ status: 'ok' }),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Method 4: Using `defineRoute` (Inline)
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
@controller({ path: '/health' })
|
|
631
|
+
export class HealthCheckController extends BaseController {
|
|
632
|
+
constructor() {
|
|
633
|
+
super({ scope: HealthCheckController.name });
|
|
634
|
+
|
|
635
|
+
this.defineRoute({
|
|
636
|
+
configs: ROUTE_CONFIGS['/ping'],
|
|
637
|
+
handler: context => {
|
|
638
|
+
const { message } = context.req.valid('json');
|
|
639
|
+
return context.json({ echo: message }, HTTP.ResultCodes.RS_2.Ok);
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### OpenAPI Schema Integration
|
|
647
|
+
|
|
648
|
+
Use Zod with `.openapi()` for automatic documentation:
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
const CreateUserSchema = z.object({
|
|
652
|
+
email: z.string().email(),
|
|
653
|
+
name: z.string().min(1).max(100),
|
|
654
|
+
}).openapi({
|
|
655
|
+
description: 'Create user request body',
|
|
656
|
+
example: { email: 'user@example.com', name: 'John Doe' },
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const UserSchema = z.object({
|
|
660
|
+
id: z.string().uuid(),
|
|
661
|
+
email: z.string().email(),
|
|
662
|
+
name: z.string(),
|
|
663
|
+
createdAt: z.string().datetime(),
|
|
664
|
+
}).openapi({
|
|
665
|
+
description: 'User response',
|
|
666
|
+
});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## Type Inference Patterns
|
|
670
|
+
|
|
671
|
+
### Zod Schema to Type
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// Define schema
|
|
675
|
+
export const SignInRequestSchema = z.object({
|
|
676
|
+
email: z.string().email(),
|
|
677
|
+
password: z.string().min(8),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Infer type from schema
|
|
681
|
+
export type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Const Assertion for Literal Types
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
const ROUTE_CONFIGS = {
|
|
688
|
+
'/users': { method: 'GET', path: '/users' },
|
|
689
|
+
'/users/:id': { method: 'GET', path: '/users/:id' },
|
|
690
|
+
} as const;
|
|
691
|
+
|
|
692
|
+
// Type is now narrowed to literal values
|
|
693
|
+
type RouteKey = keyof typeof ROUTE_CONFIGS; // '/users' | '/users/:id'
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Generic Type Constraints
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
export class DefaultCRUDRepository<
|
|
700
|
+
Schema extends TTableSchemaWithId = TTableSchemaWithId
|
|
701
|
+
> {
|
|
702
|
+
// Schema is constrained to have an 'id' column
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export interface IAuthService<
|
|
706
|
+
SIRQ extends TSignInRequest = TSignInRequest,
|
|
707
|
+
SIRS = AnyObject,
|
|
708
|
+
> {
|
|
709
|
+
signIn(context: Context, opts: SIRQ): Promise<SIRS>;
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## Summary Table
|
|
714
|
+
|
|
715
|
+
| Aspect | Standard |
|
|
716
|
+
|--------|----------|
|
|
717
|
+
| Interface prefix | `I` (e.g., `IUserService`) |
|
|
718
|
+
| Type alias prefix | `T` (e.g., `TUserRequest`) |
|
|
719
|
+
| Class naming | PascalCase with suffix (e.g., `UserController`) |
|
|
720
|
+
| File naming | kebab-case (e.g., `user.controller.ts`) |
|
|
721
|
+
| Binding keys | `@app/[component]/[feature]` |
|
|
722
|
+
| Constants | Static readonly class (not enums) |
|
|
723
|
+
| Barrel exports | `index.ts` at every folder level |
|
|
724
|
+
| Error format | `[ClassName][method] Message` |
|
|
725
|
+
| Logging format | `[method] Message \| Key: %s` |
|
|
726
|
+
| Default options | `DEFAULT_OPTIONS` constant |
|
|
727
|
+
| Scope naming | `ClassName.name` |
|
|
728
|
+
|
|
@@ -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,7 +75,7 @@ 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);
|
|
@@ -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
|