@venizia/ignis-docs 0.0.2 → 0.0.3
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/package.json +2 -2
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
- package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
- package/wiki/changelogs/index.md +2 -1
- package/wiki/changelogs/planned-schema-migrator.md +2 -2
- package/wiki/get-started/best-practices/code-style-standards.md +298 -222
- package/wiki/get-started/best-practices/performance-optimization.md +11 -2
- package/wiki/get-started/core-concepts/components.md +59 -42
- package/wiki/get-started/core-concepts/persistent.md +43 -47
- package/wiki/references/base/components.md +515 -31
- package/wiki/references/base/controllers.md +85 -18
- package/wiki/references/base/datasources.md +78 -5
- package/wiki/references/base/repositories.md +92 -6
- package/wiki/references/helpers/index.md +1 -0
- package/wiki/references/helpers/types.md +151 -0
- package/wiki/changelogs/planned-transaction-support.md +0 -216
|
@@ -117,6 +117,55 @@ Use the centralized TypeScript configs:
|
|
|
117
117
|
|
|
118
118
|
See [`@venizia/dev-configs` documentation](../../references/src-details/dev-configs.md) for full details.
|
|
119
119
|
|
|
120
|
+
## Directory Structure
|
|
121
|
+
|
|
122
|
+
### Component Organization
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/components/[feature]/
|
|
126
|
+
├── index.ts # Barrel exports
|
|
127
|
+
├── component.ts # IoC binding setup
|
|
128
|
+
├── controller.ts # Route handlers
|
|
129
|
+
└── common/
|
|
130
|
+
├── index.ts # Barrel exports
|
|
131
|
+
├── keys.ts # Binding key constants
|
|
132
|
+
├── types.ts # Interfaces and types
|
|
133
|
+
└── rest-paths.ts # Route path constants
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Complex Component (with multiple features)
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
src/components/auth/
|
|
140
|
+
├── index.ts
|
|
141
|
+
├── authenticate/
|
|
142
|
+
│ ├── index.ts
|
|
143
|
+
│ ├── component.ts
|
|
144
|
+
│ ├── common/
|
|
145
|
+
│ ├── controllers/
|
|
146
|
+
│ ├── services/
|
|
147
|
+
│ └── strategies/
|
|
148
|
+
└── models/
|
|
149
|
+
├── entities/ # Database models
|
|
150
|
+
└── requests/ # Request schemas
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Barrel Exports
|
|
154
|
+
|
|
155
|
+
Every folder should have an `index.ts` that re-exports its contents:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// components/health-check/index.ts
|
|
159
|
+
export * from './common';
|
|
160
|
+
export * from './component';
|
|
161
|
+
export * from './controller';
|
|
162
|
+
|
|
163
|
+
// components/health-check/common/index.ts
|
|
164
|
+
export * from './keys';
|
|
165
|
+
export * from './rest-paths';
|
|
166
|
+
export * from './types';
|
|
167
|
+
```
|
|
168
|
+
|
|
120
169
|
## Naming Conventions
|
|
121
170
|
|
|
122
171
|
### Class Names
|
|
@@ -184,53 +233,236 @@ export class SocketIOBindingKeys {
|
|
|
184
233
|
}
|
|
185
234
|
```
|
|
186
235
|
|
|
187
|
-
##
|
|
236
|
+
## Type Safety
|
|
188
237
|
|
|
189
|
-
|
|
238
|
+
To ensure long-term maintainability and catch errors at compile-time, Ignis enforces strict type safety.
|
|
239
|
+
|
|
240
|
+
### Avoid `any` and `unknown`
|
|
241
|
+
|
|
242
|
+
**Never use `any` or `unknown` as much as possible.** You must specify clear, descriptive types for all variables, parameters, and return values.
|
|
243
|
+
|
|
244
|
+
- **`any`**: Bypasses all type checking and leads to "runtime surprises". It is strictly discouraged.
|
|
245
|
+
- **`unknown`**: While safer than `any`, it still forces consumers to perform manual type checking. Prefer using generics or specific interfaces.
|
|
246
|
+
|
|
247
|
+
**Why?**
|
|
248
|
+
- **Maintenance**: Developers reading your code in the future will know exactly what the data structure is.
|
|
249
|
+
- **Refactoring**: Changing an interface automatically highlights all broken code across the monorepo.
|
|
250
|
+
- **Documentation**: Types act as a self-documenting contract for your APIs and services.
|
|
251
|
+
|
|
252
|
+
## Type Definitions
|
|
253
|
+
|
|
254
|
+
### Explicit Return Types
|
|
255
|
+
Always define explicit return types for **public methods** and **API handlers**.
|
|
256
|
+
|
|
257
|
+
**Why?**
|
|
258
|
+
- **Compiler Performance:** Speeds up TypeScript type checking in large projects.
|
|
259
|
+
- **Safety:** Prevents accidental exposure of internal types or sensitive data.
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// ✅ GOOD
|
|
263
|
+
public async findUser(id: string): Promise<User | null> { ... }
|
|
190
264
|
|
|
265
|
+
// ❌ BAD (Implicit inference)
|
|
266
|
+
public async findUser(id: string) { ... }
|
|
191
267
|
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
268
|
+
|
|
269
|
+
## Type Inference Patterns
|
|
270
|
+
|
|
271
|
+
### Zod Schema to Type
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Define schema
|
|
275
|
+
export const SignInRequestSchema = z.object({
|
|
276
|
+
email: z.string().email(),
|
|
277
|
+
password: z.string().min(8),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Infer type from schema
|
|
281
|
+
export type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
201
282
|
```
|
|
202
283
|
|
|
203
|
-
###
|
|
284
|
+
### Const Assertion for Literal Types
|
|
204
285
|
|
|
286
|
+
```typescript
|
|
287
|
+
const ROUTE_CONFIGS = {
|
|
288
|
+
'/users': { method: 'GET', path: '/users' },
|
|
289
|
+
'/users/:id': { method: 'GET', path: '/users/:id' },
|
|
290
|
+
} as const;
|
|
291
|
+
|
|
292
|
+
// Type is now narrowed to literal values
|
|
293
|
+
type RouteKey = keyof typeof ROUTE_CONFIGS; // '/users' | '/users/:id'
|
|
205
294
|
```
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
295
|
+
|
|
296
|
+
### Generic Type Constraints
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
export class DefaultCRUDRepository<
|
|
300
|
+
Schema extends TTableSchemaWithId = TTableSchemaWithId
|
|
301
|
+
> {
|
|
302
|
+
// Schema is constrained to have an 'id' column
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface IAuthService<
|
|
306
|
+
SIRQ extends TSignInRequest = TSignInRequest,
|
|
307
|
+
SIRS = AnyObject,
|
|
308
|
+
> {
|
|
309
|
+
signIn(context: Context, opts: SIRQ): Promise<SIRS>;
|
|
310
|
+
}
|
|
218
311
|
```
|
|
219
312
|
|
|
220
|
-
|
|
313
|
+
## Module Exports
|
|
221
314
|
|
|
222
|
-
|
|
315
|
+
### Prefer Named Exports
|
|
316
|
+
Avoid `export default` except for configuration files (e.g., `eslint.config.mjs`) or lazy-loaded components. Use named exports for all classes, functions, and constants.
|
|
317
|
+
|
|
318
|
+
**Why?**
|
|
319
|
+
- **Refactoring:** Renaming a symbol automatically updates imports across the monorepo.
|
|
320
|
+
- **Consistency:** Enforces consistent naming across all files importing the module.
|
|
223
321
|
|
|
224
322
|
```typescript
|
|
225
|
-
//
|
|
226
|
-
export
|
|
227
|
-
export * from './component';
|
|
228
|
-
export * from './controller';
|
|
323
|
+
// ✅ GOOD
|
|
324
|
+
export class UserController { ... }
|
|
229
325
|
|
|
230
|
-
//
|
|
231
|
-
export
|
|
232
|
-
|
|
233
|
-
|
|
326
|
+
// ❌ BAD
|
|
327
|
+
export default class UserController { ... }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Function Signatures
|
|
331
|
+
|
|
332
|
+
### The Options Object Pattern
|
|
333
|
+
Prefer using a single object parameter (`opts`) over multiple positional arguments, especially for constructors and public methods with more than 2 arguments.
|
|
334
|
+
|
|
335
|
+
**Why?**
|
|
336
|
+
- **Extensibility:** You can add new properties without breaking existing calls.
|
|
337
|
+
- **Readability:** Named keys act as documentation at the call site.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
// ✅ GOOD
|
|
341
|
+
class UserService {
|
|
342
|
+
createUser(opts: { name: string; email: string; role?: string }) { ... }
|
|
343
|
+
}
|
|
344
|
+
// Usage: service.createUser({ name: 'John', email: 'john@example.com' });
|
|
345
|
+
|
|
346
|
+
// ❌ BAD
|
|
347
|
+
class UserService {
|
|
348
|
+
createUser(name: string, email: string, role?: string) { ... }
|
|
349
|
+
}
|
|
350
|
+
// Usage: service.createUser('John', 'john@example.com');
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Route Definition Patterns
|
|
354
|
+
|
|
355
|
+
Ignis supports three methods for defining routes. Choose based on your needs:
|
|
356
|
+
|
|
357
|
+
### Method 1: Config-Driven Routes
|
|
358
|
+
|
|
359
|
+
Define route configurations as constants:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// common/rest-paths.ts
|
|
363
|
+
export class UserRestPaths {
|
|
364
|
+
static readonly ROOT = '/';
|
|
365
|
+
static readonly BY_ID = '/:id';
|
|
366
|
+
static readonly PROFILE = '/profile';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// common/route-configs.ts
|
|
370
|
+
export const ROUTE_CONFIGS = {
|
|
371
|
+
[UserRestPaths.ROOT]: {
|
|
372
|
+
method: HTTP.Methods.GET,
|
|
373
|
+
path: UserRestPaths.ROOT,
|
|
374
|
+
responses: jsonResponse({
|
|
375
|
+
[HTTP.ResultCodes.RS_2.Ok]: UserListSchema,
|
|
376
|
+
}),
|
|
377
|
+
},
|
|
378
|
+
[UserRestPaths.BY_ID]: {
|
|
379
|
+
method: HTTP.Methods.GET,
|
|
380
|
+
path: UserRestPaths.BY_ID,
|
|
381
|
+
request: {
|
|
382
|
+
params: z.object({ id: z.string().uuid() }),
|
|
383
|
+
},
|
|
384
|
+
responses: jsonResponse({
|
|
385
|
+
[HTTP.ResultCodes.RS_2.Ok]: UserSchema,
|
|
386
|
+
[HTTP.ResultCodes.RS_4.NotFound]: ErrorSchema,
|
|
387
|
+
}),
|
|
388
|
+
},
|
|
389
|
+
} as const;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Method 2: Using `@api` Decorator
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
@controller({ path: '/users' })
|
|
396
|
+
export class UserController extends BaseController {
|
|
397
|
+
|
|
398
|
+
@api({ configs: ROUTE_CONFIGS[UserRestPaths.ROOT] })
|
|
399
|
+
list(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.ROOT]>) {
|
|
400
|
+
return context.json({ users: [] }, HTTP.ResultCodes.RS_2.Ok);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@api({ configs: ROUTE_CONFIGS[UserRestPaths.BY_ID] })
|
|
404
|
+
getById(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.BY_ID]>) {
|
|
405
|
+
const { id } = context.req.valid('param');
|
|
406
|
+
return context.json({ id, name: 'User' }, HTTP.ResultCodes.RS_2.Ok);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Method 3: Using `bindRoute` (Programmatic)
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
@controller({ path: '/health' })
|
|
415
|
+
export class HealthCheckController extends BaseController {
|
|
416
|
+
constructor() {
|
|
417
|
+
super({ scope: HealthCheckController.name });
|
|
418
|
+
|
|
419
|
+
this.bindRoute({ configs: ROUTE_CONFIGS['/'] }).to({
|
|
420
|
+
handler: context => context.json({ status: 'ok' }),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Method 4: Using `defineRoute` (Inline)
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
@controller({ path: '/health' })
|
|
430
|
+
export class HealthCheckController extends BaseController {
|
|
431
|
+
constructor() {
|
|
432
|
+
super({ scope: HealthCheckController.name });
|
|
433
|
+
|
|
434
|
+
this.defineRoute({
|
|
435
|
+
configs: ROUTE_CONFIGS['/ping'],
|
|
436
|
+
handler: context => {
|
|
437
|
+
const { message } = context.req.valid('json');
|
|
438
|
+
return context.json({ echo: message }, HTTP.ResultCodes.RS_2.Ok);
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### OpenAPI Schema Integration
|
|
446
|
+
|
|
447
|
+
Use Zod with `.openapi()` for automatic documentation:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const CreateUserSchema = z.object({
|
|
451
|
+
email: z.string().email(),
|
|
452
|
+
name: z.string().min(1).max(100),
|
|
453
|
+
}).openapi({
|
|
454
|
+
description: 'Create user request body',
|
|
455
|
+
example: { email: 'user@example.com', name: 'John Doe' },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const UserSchema = z.object({
|
|
459
|
+
id: z.string().uuid(),
|
|
460
|
+
email: z.string().email(),
|
|
461
|
+
name: z.string(),
|
|
462
|
+
createdAt: z.string().datetime(),
|
|
463
|
+
}).openapi({
|
|
464
|
+
description: 'User response',
|
|
465
|
+
});
|
|
234
466
|
```
|
|
235
467
|
|
|
236
468
|
## Constants Pattern
|
|
@@ -427,6 +659,28 @@ constructor(options: IJWTTokenServiceOptions) {
|
|
|
427
659
|
}
|
|
428
660
|
```
|
|
429
661
|
|
|
662
|
+
## Environment Variables Management
|
|
663
|
+
|
|
664
|
+
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.
|
|
665
|
+
|
|
666
|
+
**Define Keys (`src/common/environments.ts`):**
|
|
667
|
+
```typescript
|
|
668
|
+
export class EnvironmentKeys {
|
|
669
|
+
static readonly APP_ENV_STRIPE_KEY = 'APP_ENV_STRIPE_KEY';
|
|
670
|
+
static readonly APP_ENV_MAX_RETRIES = 'APP_ENV_MAX_RETRIES';
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Usage:**
|
|
675
|
+
```typescript
|
|
676
|
+
import { applicationEnvironment } from '@venizia/ignis';
|
|
677
|
+
import { EnvironmentKeys } from '@/common/environments';
|
|
678
|
+
|
|
679
|
+
// Correct usage
|
|
680
|
+
const stripeKey = applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_STRIPE_KEY);
|
|
681
|
+
const retries = applicationEnvironment.get<number>(EnvironmentKeys.APP_ENV_MAX_RETRIES);
|
|
682
|
+
```
|
|
683
|
+
|
|
430
684
|
## Logging Patterns
|
|
431
685
|
|
|
432
686
|
### Method Context Prefix
|
|
@@ -451,46 +705,6 @@ this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.ema
|
|
|
451
705
|
this.logger.debug('[config] Server options: %j', this.serverOptions);
|
|
452
706
|
```
|
|
453
707
|
|
|
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
|
-
|
|
472
|
-
## Environment Variables Management
|
|
473
|
-
|
|
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.
|
|
475
|
-
|
|
476
|
-
**Define Keys (`src/common/environments.ts`):**
|
|
477
|
-
```typescript
|
|
478
|
-
export class EnvironmentKeys {
|
|
479
|
-
static readonly APP_ENV_STRIPE_KEY = 'APP_ENV_STRIPE_KEY';
|
|
480
|
-
static readonly APP_ENV_MAX_RETRIES = 'APP_ENV_MAX_RETRIES';
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
**Usage:**
|
|
485
|
-
```typescript
|
|
486
|
-
import { applicationEnvironment } from '@venizia/ignis';
|
|
487
|
-
import { EnvironmentKeys } from '@/common/environments';
|
|
488
|
-
|
|
489
|
-
// Correct usage
|
|
490
|
-
const stripeKey = applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_STRIPE_KEY);
|
|
491
|
-
const retries = applicationEnvironment.get<number>(EnvironmentKeys.APP_ENV_MAX_RETRIES);
|
|
492
|
-
```
|
|
493
|
-
|
|
494
708
|
## Standardized Error Handling
|
|
495
709
|
|
|
496
710
|
Use the `getError` helper and `HTTP` constants to throw consistent, formatted exceptions that the framework's error handler can process correctly.
|
|
@@ -551,165 +765,24 @@ constructor(options: IServiceOptions) {
|
|
|
551
765
|
| Not Found | `HTTP.ResultCodes.RS_4.NotFound` | Resource not found |
|
|
552
766
|
| Internal Error | `HTTP.ResultCodes.RS_5.InternalServerError` | Server errors |
|
|
553
767
|
|
|
554
|
-
##
|
|
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
|
-
```
|
|
768
|
+
## Scope Naming
|
|
611
769
|
|
|
612
|
-
|
|
770
|
+
Every class extending a base class should set its scope using `ClassName.name`:
|
|
613
771
|
|
|
614
772
|
```typescript
|
|
615
|
-
|
|
616
|
-
export class HealthCheckController extends BaseController {
|
|
773
|
+
export class JWTTokenService extends BaseService {
|
|
617
774
|
constructor() {
|
|
618
|
-
super({ scope:
|
|
619
|
-
|
|
620
|
-
this.bindRoute({ configs: ROUTE_CONFIGS['/'] }).to({
|
|
621
|
-
handler: context => context.json({ status: 'ok' }),
|
|
622
|
-
});
|
|
775
|
+
super({ scope: JWTTokenService.name });
|
|
623
776
|
}
|
|
624
777
|
}
|
|
625
|
-
```
|
|
626
778
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
@controller({ path: '/health' })
|
|
631
|
-
export class HealthCheckController extends BaseController {
|
|
779
|
+
export class UserController extends BaseController {
|
|
632
780
|
constructor() {
|
|
633
|
-
super({ scope:
|
|
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
|
-
});
|
|
781
|
+
super({ scope: UserController.name });
|
|
642
782
|
}
|
|
643
783
|
}
|
|
644
784
|
```
|
|
645
785
|
|
|
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
786
|
## Summary Table
|
|
714
787
|
|
|
715
788
|
| Aspect | Standard |
|
|
@@ -724,5 +797,8 @@ export interface IAuthService<
|
|
|
724
797
|
| Error format | `[ClassName][method] Message` |
|
|
725
798
|
| Logging format | `[method] Message \| Key: %s` |
|
|
726
799
|
| Default options | `DEFAULT_OPTIONS` constant |
|
|
800
|
+
| Type safety | No `any` or `unknown` allowed |
|
|
727
801
|
| Scope naming | `ClassName.name` |
|
|
728
|
-
|
|
802
|
+
| Arguments | Options object (`opts`) |
|
|
803
|
+
| Exports | Named exports only |
|
|
804
|
+
| Return types | Explicitly defined |
|
|
@@ -40,17 +40,26 @@ Prevent blocking the event loop with Worker Threads:
|
|
|
40
40
|
|-----------|---------|--------|
|
|
41
41
|
| **Select specific fields** | `fields: { id: true, name: true }` | Reduce data transfer |
|
|
42
42
|
| **Use indexes** | Create indexes on WHERE/JOIN columns | 10-100x faster queries |
|
|
43
|
+
| **Mandatory Limit** | `limit: 20` | Prevent fetching massive datasets |
|
|
43
44
|
| **Paginate results** | `limit: 20, offset: 0` | Prevent memory overflow |
|
|
44
45
|
| **Eager load relations** | `include: [{ relation: 'creator' }]` | Solve N+1 problem |
|
|
45
46
|
|
|
47
|
+
> [!TIP]
|
|
48
|
+
> **Avoid Deep Nesting:** While Ignis supports deeply nested `include` filters, each level adds significant overhead to query construction and result mapping. We strongly recommend a **maximum of 2 levels** (e.g., `User -> Orders -> Items`). For more complex data fetching, consider separate queries.
|
|
49
|
+
|
|
46
50
|
**Example:**
|
|
47
51
|
```typescript
|
|
48
52
|
await userRepository.find({
|
|
49
53
|
filter: {
|
|
50
54
|
fields: { id: true, name: true, email: true }, // ✅ Specific fields
|
|
51
55
|
where: { status: 'ACTIVE' },
|
|
52
|
-
limit: 20, // ✅
|
|
53
|
-
include: [{
|
|
56
|
+
limit: 20, // ✅ Mandatory limit
|
|
57
|
+
include: [{
|
|
58
|
+
relation: 'orders',
|
|
59
|
+
scope: {
|
|
60
|
+
include: [{ relation: 'items' }] // ✅ Level 2 (Recommended limit)
|
|
61
|
+
}
|
|
62
|
+
}],
|
|
54
63
|
},
|
|
55
64
|
});
|
|
56
65
|
```
|