@venizia/ignis-docs 0.0.4-0 → 0.0.4-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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/api-usage-examples.md +1 -0
  3. package/wiki/best-practices/code-style-standards/advanced-patterns.md +259 -0
  4. package/wiki/best-practices/code-style-standards/constants-configuration.md +225 -0
  5. package/wiki/best-practices/code-style-standards/control-flow.md +245 -0
  6. package/wiki/best-practices/code-style-standards/documentation.md +221 -0
  7. package/wiki/best-practices/code-style-standards/function-patterns.md +142 -0
  8. package/wiki/best-practices/code-style-standards/index.md +110 -0
  9. package/wiki/best-practices/code-style-standards/naming-conventions.md +174 -0
  10. package/wiki/best-practices/code-style-standards/route-definitions.md +150 -0
  11. package/wiki/best-practices/code-style-standards/tooling.md +155 -0
  12. package/wiki/best-practices/code-style-standards/type-safety.md +165 -0
  13. package/wiki/best-practices/common-pitfalls.md +164 -3
  14. package/wiki/best-practices/contribution-workflow.md +1 -1
  15. package/wiki/best-practices/data-modeling.md +102 -2
  16. package/wiki/best-practices/error-handling.md +468 -0
  17. package/wiki/best-practices/index.md +204 -21
  18. package/wiki/best-practices/performance-optimization.md +180 -0
  19. package/wiki/best-practices/security-guidelines.md +249 -0
  20. package/wiki/best-practices/testing-strategies.md +620 -0
  21. package/wiki/changelogs/2026-01-05-range-queries-content-range.md +184 -0
  22. package/wiki/changelogs/2026-01-06-basic-authentication.md +103 -0
  23. package/wiki/changelogs/2026-01-07-controller-route-customization.md +209 -0
  24. package/wiki/changelogs/index.md +3 -0
  25. package/wiki/guides/core-concepts/components-guide.md +1 -1
  26. package/wiki/guides/core-concepts/persistent/models.md +10 -0
  27. package/wiki/guides/tutorials/complete-installation.md +1 -1
  28. package/wiki/guides/tutorials/testing.md +1 -1
  29. package/wiki/references/base/bootstrapping.md +4 -3
  30. package/wiki/references/base/components.md +47 -29
  31. package/wiki/references/base/controllers.md +220 -24
  32. package/wiki/references/base/filter-system/fields-order-pagination.md +84 -0
  33. package/wiki/references/base/middlewares.md +37 -3
  34. package/wiki/references/base/models.md +40 -2
  35. package/wiki/references/base/providers.md +1 -2
  36. package/wiki/references/base/repositories/index.md +3 -1
  37. package/wiki/references/base/services.md +2 -2
  38. package/wiki/references/components/authentication.md +261 -247
  39. package/wiki/references/helpers/index.md +1 -1
  40. package/wiki/references/helpers/socket-io.md +1 -1
  41. package/wiki/references/quick-reference.md +2 -2
  42. package/wiki/references/src-details/core.md +1 -1
  43. package/wiki/references/utilities/statuses.md +4 -4
  44. package/wiki/best-practices/code-style-standards.md +0 -1193
@@ -1,1193 +0,0 @@
1
- # Code Style Standards
2
-
3
- Maintain consistent code style using **Prettier** (formatting) and **ESLint** (code quality). Ignis provides centralized configurations via the `@venizia/dev-configs` package.
4
-
5
- ## Using @venizia/dev-configs
6
-
7
- Install the centralized development configurations:
8
-
9
- ```bash
10
- bun add -d @venizia/dev-configs
11
- ```
12
-
13
- This package provides:
14
- - **ESLint rules** - Pre-configured for Node.js/TypeScript projects
15
- - **Prettier settings** - Consistent formatting across all Ignis projects
16
- - **TypeScript configs** - Shared base and common configurations
17
-
18
- ### Prettier Configuration
19
-
20
- Automatic code formatting eliminates style debates.
21
-
22
- **`.prettierrc.mjs`:**
23
- ```javascript
24
- import { prettierConfigs } from '@venizia/dev-configs';
25
-
26
- export default prettierConfigs;
27
- ```
28
-
29
- **Default Settings:**
30
- - `bracketSpacing: true` - `{ foo: bar }`
31
- - `singleQuote: false` - `"string"` (double quotes)
32
- - `printWidth: 100` - Maximum line length
33
- - `trailingComma: 'all'` - `[1, 2, 3,]`
34
- - `arrowParens: 'avoid'` - `x => x` not `(x) => x`
35
- - `semi: true` - Semicolons required
36
-
37
- **Customization:**
38
- ```javascript
39
- import { prettierConfigs } from '@venizia/dev-configs';
40
-
41
- export default {
42
- ...prettierConfigs,
43
- printWidth: 120, // Override specific settings
44
- };
45
- ```
46
-
47
- **Usage:**
48
- ```bash
49
- bun run prettier:cli # Check formatting
50
- bun run prettier:fix # Auto-fix
51
- ```
52
-
53
- ### ESLint Configuration
54
-
55
- Prevents common errors and enforces best practices.
56
-
57
- **`eslint.config.mjs`:**
58
- ```javascript
59
- import { eslintConfigs } from '@venizia/dev-configs';
60
-
61
- export default eslintConfigs;
62
- ```
63
-
64
- **Includes:**
65
- - Pre-configured rules for Node.js/TypeScript (via `@minimaltech/eslint-node`)
66
- - Disables `@typescript-eslint/no-explicit-any` by default
67
-
68
- **Customization:**
69
- ```javascript
70
- import { eslintConfigs } from '@venizia/dev-configs';
71
-
72
- export default [
73
- ...eslintConfigs,
74
- {
75
- rules: {
76
- 'no-console': 'warn', // Add project-specific rules
77
- },
78
- },
79
- ];
80
- ```
81
-
82
- **Usage:**
83
- ```bash
84
- bun run eslint # Check for issues
85
- bun run eslint --fix # Auto-fix issues
86
- bun run lint:fix # Run both ESLint + Prettier
87
- ```
88
-
89
- ### TypeScript Configuration
90
-
91
- Use the centralized TypeScript configs:
92
-
93
- **`tsconfig.json`:**
94
- ```json
95
- {
96
- "$schema": "http://json.schemastore.org/tsconfig",
97
- "extends": "@venizia/dev-configs/tsconfig.common.json",
98
- "compilerOptions": {
99
- "outDir": "dist",
100
- "rootDir": "src",
101
- "baseUrl": "src",
102
- "paths": {
103
- "@/*": ["./*"]
104
- }
105
- },
106
- "include": ["src"],
107
- "exclude": ["node_modules", "dist"]
108
- }
109
- ```
110
-
111
- **What's Included:**
112
- - `target: ES2022` - Modern JavaScript features
113
- - `experimentalDecorators: true` - Required for Ignis decorators
114
- - `emitDecoratorMetadata: true` - Metadata reflection for DI
115
- - `strict: true` - Strict type checking with selective relaxations
116
- - `skipLibCheck: true` - Faster compilation
117
-
118
- See [`@venizia/dev-configs` documentation](../references/src-details/dev-configs.md) for full details.
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
-
169
- ## Naming Conventions
170
-
171
- ### Class Names
172
-
173
- | Type | Pattern | Example |
174
- |------|---------|---------|
175
- | Components | `[Feature]Component` | `HealthCheckComponent`, `AuthComponent` |
176
- | Controllers | `[Feature]Controller` | `UserController`, `AuthController` |
177
- | Services | `[Feature]Service` | `JWTTokenService`, `PaymentService` |
178
- | Repositories | `[Feature]Repository` | `UserRepository`, `OrderRepository` |
179
- | Strategies | `[Feature]Strategy` | `JWTAuthenticationStrategy` |
180
- | Factories | `[Feature]Factory` | `UIProviderFactory` |
181
-
182
- ### File Names
183
-
184
- Both styles are acceptable: `[type].ts` or `[name].[type].ts`
185
-
186
- | Type | Single File | Multiple Files |
187
- |------|-------------|----------------|
188
- | Components | `component.ts` | `auth.component.ts` |
189
- | Controllers | `controller.ts` | `user.controller.ts` |
190
- | Services | `service.ts` | `jwt-token.service.ts` |
191
- | Repositories | `repository.ts` | `user.repository.ts` |
192
- | Types/Interfaces | `types.ts` | `user.types.ts` |
193
- | Constants | `constants.ts` | `keys.ts`, `rest-paths.ts` |
194
- | Schemas | `schema.ts` | `sign-in.schema.ts` |
195
-
196
- **Guidelines:**
197
- - Use `[type].ts` when there's only one file of that type in the folder
198
- - Use `[name].[type].ts` when there are multiple files of the same type
199
- - Use kebab-case for multi-word names: `jwt-token.service.ts`
200
-
201
- ### Type and Interface Prefixes
202
-
203
- ```typescript
204
- // Interfaces use 'I' prefix
205
- interface IHealthCheckOptions {
206
- restOptions: { path: string };
207
- }
208
-
209
- interface IAuthService {
210
- signIn(context: Context): Promise<void>;
211
- }
212
-
213
- // Type aliases use 'T' prefix
214
- type TSignInRequest = z.infer<typeof SignInRequestSchema>;
215
- type TRouteContext = Context<Env, Path, Input>;
216
-
217
- // Generic constraints
218
- type TTableSchemaWithId = { id: PgColumn };
219
- ```
220
-
221
- ### Binding Keys
222
-
223
- Use static class with `@app/[component]/[feature]` format:
224
-
225
- ```typescript
226
- export class HealthCheckBindingKeys {
227
- static readonly HEALTH_CHECK_OPTIONS = '@app/health-check/options';
228
- }
229
-
230
- export class SocketIOBindingKeys {
231
- static readonly SOCKET_IO_INSTANCE = '@app/socket-io/instance';
232
- static readonly SERVER_OPTIONS = '@app/socket-io/server-options';
233
- }
234
- ```
235
-
236
- ## Private Field Naming Convention
237
-
238
- Use underscore prefix (`_`) for private and protected class fields to distinguish them from public fields and method parameters.
239
-
240
- ```typescript
241
- class MyRepository extends BaseRepository {
242
- // Private fields with underscore prefix
243
- private _dataSource: IDataSource;
244
- private _entity: BaseEntity;
245
- private _hiddenProperties: Set<string> | null = null;
246
-
247
- // Protected fields also use underscore prefix
248
- protected _schemaFactory?: ReturnType<typeof createSchemaFactory>;
249
-
250
- constructor(dataSource: IDataSource) {
251
- // 'dataSource' (param) vs '_dataSource' (field)
252
- this._dataSource = dataSource;
253
- }
254
- }
255
- ```
256
-
257
- **Benefits:**
258
- - Clear distinction between fields and parameters
259
- - Avoids naming conflicts in constructors
260
- - Consistent with TypeScript community conventions
261
-
262
- ## Sentinel Value Pattern for Caching
263
-
264
- Use `null` to distinguish "not computed" from "computed as undefined" for lazy-initialized cached values.
265
-
266
- ```typescript
267
- class Repository {
268
- // null = not computed yet, undefined = computed but no value
269
- private _visibleProperties: Record<string, any> | null | undefined = null;
270
-
271
- get visibleProperties(): Record<string, any> | undefined {
272
- if (this._visibleProperties !== null) {
273
- return this._visibleProperties;
274
- }
275
- // Compute once and cache (may be undefined)
276
- this._visibleProperties = this.computeVisibleProperties();
277
- return this._visibleProperties;
278
- }
279
- }
280
- ```
281
-
282
- **Why not just `undefined`?**
283
- - `undefined` can be a valid computed result
284
- - `null` clearly indicates "never computed"
285
- - Prevents redundant re-computation
286
-
287
- ## Type Safety
288
-
289
- To ensure long-term maintainability and catch errors at compile-time, Ignis enforces strict type safety.
290
-
291
- ### Avoid `any` and `unknown`
292
-
293
- **Never use `any` or `unknown` as much as possible.** You must specify clear, descriptive types for all variables, parameters, and return values.
294
-
295
- - **`any`**: Bypasses all type checking and leads to "runtime surprises". It is strictly discouraged.
296
- - **`unknown`**: While safer than `any`, it still forces consumers to perform manual type checking. Prefer using generics or specific interfaces.
297
-
298
- **Why?**
299
- - **Maintenance**: Developers reading your code in the future will know exactly what the data structure is.
300
- - **Refactoring**: Changing an interface automatically highlights all broken code across the monorepo.
301
- - **Documentation**: Types act as a self-documenting contract for your APIs and services.
302
-
303
- ## Type Definitions
304
-
305
- ### Explicit Return Types
306
- Always define explicit return types for **public methods** and **API handlers**.
307
-
308
- **Why?**
309
- - **Compiler Performance:** Speeds up TypeScript type checking in large projects.
310
- - **Safety:** Prevents accidental exposure of internal types or sensitive data.
311
-
312
- ```typescript
313
- // ✅ GOOD
314
- public async findUser(id: string): Promise<User | null> { ... }
315
-
316
- // ❌ BAD (Implicit inference)
317
- public async findUser(id: string) { ... }
318
- ```
319
-
320
- ## Type Inference Patterns
321
-
322
- ### Zod Schema to Type
323
-
324
- ```typescript
325
- // Define schema
326
- export const SignInRequestSchema = z.object({
327
- email: z.string().email(),
328
- password: z.string().min(8),
329
- });
330
-
331
- // Infer type from schema
332
- export type TSignInRequest = z.infer<typeof SignInRequestSchema>;
333
- ```
334
-
335
- ### Const Assertion for Literal Types
336
-
337
- ```typescript
338
- const RouteConfigs = {
339
- GET_USERS: { method: 'GET', path: '/users' },
340
- GET_USER_BY_ID: { method: 'GET', path: '/users/:id' },
341
- } as const;
342
-
343
- // Type is now narrowed to literal values
344
- type RouteKey = keyof typeof RouteConfigs; // 'GET_USERS' | 'GET_USER_BY_ID'
345
- ```
346
-
347
- ### Generic Type Constraints
348
-
349
- ```typescript
350
- export class DefaultCRUDRepository<
351
- Schema extends TTableSchemaWithId = TTableSchemaWithId
352
- > {
353
- // Schema is constrained to have an 'id' column
354
- }
355
-
356
- export interface IAuthService<
357
- SIRQ extends TSignInRequest = TSignInRequest,
358
- SIRS = AnyObject,
359
- > {
360
- signIn(context: Context, opts: SIRQ): Promise<SIRS>;
361
- }
362
- ```
363
-
364
- ### Method Overloading for Conditional Returns
365
-
366
- Use TypeScript method overloads when return types depend on input options:
367
-
368
- ```typescript
369
- class Repository<T, R> {
370
- // Overload 1: shouldReturn: false → data is null
371
- create(opts: { data: T; options: { shouldReturn: false } }): Promise<{ count: number; data: null }>;
372
- // Overload 2: shouldReturn: true (default) → data is R
373
- create(opts: { data: T; options?: { shouldReturn?: true } }): Promise<{ count: number; data: R }>;
374
- // Implementation signature
375
- create(opts: { data: T; options?: { shouldReturn?: boolean } }): Promise<{ count: number; data: R | null }> {
376
- // implementation
377
- }
378
- }
379
-
380
- // Usage
381
- const result1 = await repo.create({ data: user, options: { shouldReturn: false } });
382
- // result1.data is typed as null
383
-
384
- const result2 = await repo.create({ data: user });
385
- // result2.data is typed as R (the entity type)
386
- ```
387
-
388
- **When to use:**
389
- - Return type varies based on boolean flag
390
- - API with optional "return data" behavior
391
- - Methods with conditional processing
392
-
393
- ## Module Exports
394
-
395
- ### Prefer Named Exports
396
- 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.
397
-
398
- **Why?**
399
- - **Refactoring:** Renaming a symbol automatically updates imports across the monorepo.
400
- - **Consistency:** Enforces consistent naming across all files importing the module.
401
-
402
- ```typescript
403
- // ✅ GOOD
404
- export class UserController { ... }
405
-
406
- // ❌ BAD
407
- export default class UserController { ... }
408
- ```
409
-
410
- ## Function Signatures
411
-
412
- ### The Options Object Pattern
413
- Prefer using a single object parameter (`opts`) over multiple positional arguments, especially for constructors and public methods with more than 2 arguments.
414
-
415
- **Why?**
416
- - **Extensibility:** You can add new properties without breaking existing calls.
417
- - **Readability:** Named keys act as documentation at the call site.
418
-
419
- ```typescript
420
- // ✅ GOOD
421
- class UserService {
422
- createUser(opts: { name: string; email: string; role?: string }) { ... }
423
- }
424
- // Usage: service.createUser({ name: 'John', email: 'john@example.com' });
425
-
426
- // ❌ BAD
427
- class UserService {
428
- createUser(name: string, email: string, role?: string) { ... }
429
- }
430
- // Usage: service.createUser('John', 'john@example.com');
431
- ```
432
-
433
- ## Function Naming Conventions
434
-
435
- Use consistent prefixes based on function purpose:
436
-
437
- | Prefix | Purpose | Examples |
438
- |--------|---------|----------|
439
- | `generate*` | Create column definitions / schemas | `generateIdColumnDefs()`, `generateTzColumnDefs()` |
440
- | `build*` | Construct complex objects | `buildPrimitiveCondition()`, `buildJsonOrderBy()` |
441
- | `to*` | Convert/transform data | `toCamel()`, `toBoolean()`, `toStringDecimal()` |
442
- | `is*` | Boolean validation/check | `isWeekday()`, `isInt()`, `isFloat()`, `isPromiseLike()` |
443
- | `extract*` | Pull out specific parts | `extractTimestamp()`, `extractWorkerId()`, `extractSequence()` |
444
- | `enrich*` | Enhance with additional data | `enrichUserAudit()`, `enrichWithMetadata()` |
445
- | `get*` | Retrieve/fetch data | `getSchema()`, `getConnector()`, `getError()` |
446
- | `resolve*` | Determine/compute value | `resolveValue()`, `resolvePath()` |
447
-
448
- **Examples:**
449
-
450
- ```typescript
451
- // Generators - create schema definitions
452
- const idCols = generateIdColumnDefs({ id: { dataType: 'string' } });
453
- const tzCols = generateTzColumnDefs();
454
-
455
- // Builders - construct complex query objects
456
- const condition = buildPrimitiveCondition(column, operator, value);
457
- const orderBy = buildJsonOrderBy(schema, path, direction);
458
-
459
- // Converters - transform data types
460
- const camelCase = toCamel('snake_case');
461
- const bool = toBoolean('true');
462
- const decimal = toStringDecimal(123.456, 2);
463
-
464
- // Validators - boolean checks
465
- if (isWeekday(date)) { /* ... */ }
466
- if (isInt(value)) { /* ... */ }
467
- if (isPromiseLike(result)) { /* ... */ }
468
-
469
- // Extractors - pull specific data
470
- const timestamp = extractTimestamp(snowflakeId);
471
- const workerId = extractWorkerId(snowflakeId);
472
- ```
473
-
474
- ## Route Definition Patterns
475
-
476
- Ignis supports three methods for defining routes. Choose based on your needs:
477
-
478
- ### Method 1: Config-Driven Routes
479
-
480
- Define route configurations as constants with UPPER_CASE names:
481
-
482
- ```typescript
483
- // common/rest-paths.ts
484
- export class UserRestPaths {
485
- static readonly ROOT = '/';
486
- static readonly BY_ID = '/:id';
487
- static readonly PROFILE = '/profile';
488
- }
489
-
490
- // common/route-configs.ts
491
- export const RouteConfigs = {
492
- GET_USERS: {
493
- method: HTTP.Methods.GET,
494
- path: UserRestPaths.ROOT,
495
- responses: jsonResponse({
496
- [HTTP.ResultCodes.RS_2.Ok]: UserListSchema,
497
- }),
498
- },
499
- GET_USER_BY_ID: {
500
- method: HTTP.Methods.GET,
501
- path: UserRestPaths.BY_ID,
502
- request: {
503
- params: z.object({ id: z.string() }),
504
- },
505
- responses: jsonResponse({
506
- [HTTP.ResultCodes.RS_2.Ok]: UserSchema,
507
- [HTTP.ResultCodes.RS_4.NotFound]: ErrorSchema,
508
- }),
509
- },
510
- } as const;
511
- ```
512
-
513
- ### Method 2: Using `@api` Decorator
514
-
515
- ```typescript
516
- @controller({ path: '/users' })
517
- export class UserController extends BaseController {
518
-
519
- @api({ configs: RouteConfigs.GET_USERS })
520
- list(context: TRouteContext<typeof RouteConfigs.GET_USERS>) {
521
- return context.json({ users: [] }, HTTP.ResultCodes.RS_2.Ok);
522
- }
523
-
524
- @api({ configs: RouteConfigs.GET_USER_BY_ID })
525
- getById(context: TRouteContext<typeof RouteConfigs.GET_USER_BY_ID>) {
526
- const { id } = context.req.valid('param');
527
- return context.json({ id, name: 'User' }, HTTP.ResultCodes.RS_2.Ok);
528
- }
529
- }
530
- ```
531
-
532
- ### Method 3: Using `bindRoute` (Programmatic)
533
-
534
- ```typescript
535
- @controller({ path: '/health' })
536
- export class HealthCheckController extends BaseController {
537
- constructor() {
538
- super({ scope: HealthCheckController.name });
539
-
540
- this.bindRoute({ configs: RouteConfigs.GET_HEALTH }).to({
541
- handler: context => context.json({ status: 'ok' }),
542
- });
543
- }
544
- }
545
- ```
546
-
547
- ### Method 4: Using `defineRoute` (Inline)
548
-
549
- ```typescript
550
- @controller({ path: '/health' })
551
- export class HealthCheckController extends BaseController {
552
- constructor() {
553
- super({ scope: HealthCheckController.name });
554
-
555
- this.defineRoute({
556
- configs: RouteConfigs.POST_PING,
557
- handler: context => {
558
- const { message } = context.req.valid('json');
559
- return context.json({ echo: message }, HTTP.ResultCodes.RS_2.Ok);
560
- },
561
- });
562
- }
563
- }
564
- ```
565
-
566
- ### OpenAPI Schema Integration
567
-
568
- Use Zod with `.openapi()` for automatic documentation:
569
-
570
- ```typescript
571
- const CreateUserSchema = z.object({
572
- email: z.string().email(),
573
- name: z.string().min(1).max(100),
574
- }).openapi({
575
- description: 'Create user request body',
576
- example: { email: 'user@example.com', name: 'John Doe' },
577
- });
578
-
579
- const UserSchema = z.object({
580
- id: z.string().uuid(),
581
- email: z.string().email(),
582
- name: z.string(),
583
- createdAt: z.string().datetime(),
584
- }).openapi({
585
- description: 'User response',
586
- });
587
- ```
588
-
589
- ## Constants Pattern
590
-
591
- **Prefer static classes over enums** for better tree-shaking and extensibility.
592
-
593
- ### Basic Constants
594
-
595
- ```typescript
596
- export class Authentication {
597
- static readonly STRATEGY_BASIC = 'basic';
598
- static readonly STRATEGY_JWT = 'jwt';
599
- static readonly TYPE_BEARER = 'Bearer';
600
- }
601
-
602
- export class HealthCheckRestPaths {
603
- static readonly ROOT = '/';
604
- static readonly PING = '/ping';
605
- static readonly METRICS = '/metrics';
606
- }
607
- ```
608
-
609
- ### Typed Constants with Validation
610
-
611
- For constants that need type extraction and runtime validation, use this pattern:
612
-
613
- ```typescript
614
- import { TConstValue } from '@venizia/ignis-helpers';
615
-
616
- export class DocumentUITypes {
617
- // 1. Define static readonly values
618
- static readonly SWAGGER = 'swagger';
619
- static readonly SCALAR = 'scalar';
620
-
621
- // 2. Create a Set for O(1) validation lookup
622
- static readonly SCHEME_SET = new Set([this.SWAGGER, this.SCALAR]);
623
-
624
- // 3. Validation helper method
625
- static isValid(value: string): boolean {
626
- return this.SCHEME_SET.has(value);
627
- }
628
- }
629
-
630
- // 4. Extract union type from class values
631
- export type TDocumentUIType = TConstValue<typeof DocumentUITypes>;
632
- // Result: 'swagger' | 'scalar'
633
- ```
634
-
635
- **Full Example with Usage:**
636
-
637
- ```typescript
638
- import { TConstValue } from '@venizia/ignis-helpers';
639
-
640
- export class UserStatuses {
641
- static readonly ACTIVE = 'active';
642
- static readonly INACTIVE = 'inactive';
643
- static readonly PENDING = 'pending';
644
- static readonly BANNED = 'banned';
645
-
646
- static readonly SCHEME_SET = new Set([
647
- this.ACTIVE,
648
- this.INACTIVE,
649
- this.PENDING,
650
- this.BANNED,
651
- ]);
652
-
653
- static isValid(value: string): boolean {
654
- return this.SCHEME_SET.has(value);
655
- }
656
-
657
- // Optional: get all values as array
658
- static values(): string[] {
659
- return [...this.SCHEME_SET];
660
- }
661
- }
662
-
663
- // Type-safe union type
664
- export type TUserStatus = TConstValue<typeof UserStatuses>;
665
- // Result: 'active' | 'inactive' | 'pending' | 'banned'
666
-
667
- // Usage in interfaces
668
- interface IUser {
669
- id: string;
670
- status: TUserStatus; // Type-safe!
671
- }
672
-
673
- // Usage with validation
674
- function updateUserStatus(userId: string, status: string) {
675
- if (!UserStatuses.isValid(status)) {
676
- throw getError({
677
- statusCode: HTTP.ResultCodes.RS_4.BadRequest,
678
- message: `Invalid status: ${status}. Valid: ${UserStatuses.values().join(', ')}`,
679
- });
680
- }
681
- // status is validated at runtime
682
- }
683
- ```
684
-
685
- ### Enum vs Static Class Comparison
686
-
687
- | Aspect | Static Class | TypeScript Enum |
688
- |--------|--------------|-----------------|
689
- | Tree-shaking | Full support | Partial (IIFE blocks it) |
690
- | Bundle size | Minimal | Larger (IIFE wrapper) |
691
- | Runtime validation | O(1) with `Set` | O(n) with `Object.values()` |
692
- | Type extraction | `TConstValue<typeof X>` → values | `keyof typeof X` → keys (not values!) |
693
- | Add methods | Yes | Not possible |
694
- | Compiled output | Clean class | IIFE wrapper |
695
-
696
- **Compiled JavaScript:**
697
-
698
- ```typescript
699
- // Enum compiles to IIFE (not tree-shakable)
700
- var UserStatus;
701
- (function (UserStatus) {
702
- UserStatus["ACTIVE"] = "active";
703
- })(UserStatus || (UserStatus = {}));
704
-
705
- // Static class compiles cleanly
706
- class UserStatuses { }
707
- UserStatuses.ACTIVE = 'active';
708
- ```
709
-
710
- **Type Extraction Difference:**
711
-
712
- ```typescript
713
- // Enum - extracts KEYS
714
- type T = keyof typeof UserStatus; // 'ACTIVE' | 'INACTIVE'
715
-
716
- // Static Class - extracts VALUES
717
- type T = TConstValue<typeof UserStatuses>; // 'active' | 'inactive'
718
- ```
719
-
720
- **When to use `const enum`:** Only for numeric flags with no iteration needed (values are inlined, zero runtime). But doesn't work with `--isolatedModules`.
721
-
722
- **Verdict:** Use Static Class for 90% of cases - better tree-shaking, easy validation, type-safe values, extensible with methods.
723
-
724
- ## Configuration Patterns
725
-
726
- ### Default Options
727
-
728
- Every configurable class should define `DEFAULT_OPTIONS`:
729
-
730
- ```typescript
731
- const DEFAULT_OPTIONS: IHealthCheckOptions = {
732
- restOptions: { path: '/health' },
733
- };
734
-
735
- const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
736
- identifier: 'SOCKET_IO_SERVER',
737
- path: '/io',
738
- cors: {
739
- origin: '*',
740
- methods: ['GET', 'POST'],
741
- },
742
- };
743
- ```
744
-
745
- ### Option Merging
746
-
747
- ```typescript
748
- // In component constructor or binding
749
- const extraOptions = this.application.get<Partial<IServerOptions>>({
750
- key: BindingKeys.SERVER_OPTIONS,
751
- isOptional: true,
752
- }) ?? {};
753
-
754
- this.options = Object.assign({}, DEFAULT_OPTIONS, extraOptions);
755
- ```
756
-
757
- ### Constructor Validation
758
-
759
- Validate required options in the constructor:
760
-
761
- ```typescript
762
- constructor(options: IJWTTokenServiceOptions) {
763
- super({ scope: JWTTokenService.name });
764
-
765
- if (!options.jwtSecret) {
766
- throw getError({
767
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
768
- message: '[JWTTokenService] Invalid jwtSecret',
769
- });
770
- }
771
-
772
- if (!options.applicationSecret) {
773
- throw getError({
774
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
775
- message: '[JWTTokenService] Invalid applicationSecret',
776
- });
777
- }
778
-
779
- this.options = options;
780
- }
781
- ```
782
-
783
- ## Environment Variables Management
784
-
785
- 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.
786
-
787
- **Define Keys (`src/common/environments.ts`):**
788
- ```typescript
789
- export class EnvironmentKeys {
790
- static readonly APP_ENV_STRIPE_KEY = 'APP_ENV_STRIPE_KEY';
791
- static readonly APP_ENV_MAX_RETRIES = 'APP_ENV_MAX_RETRIES';
792
- }
793
- ```
794
-
795
- **Usage:**
796
- ```typescript
797
- import { applicationEnvironment } from '@venizia/ignis';
798
- import { EnvironmentKeys } from '@/common/environments';
799
-
800
- // Correct usage
801
- const stripeKey = applicationEnvironment.get<string>(EnvironmentKeys.APP_ENV_STRIPE_KEY);
802
- const retries = applicationEnvironment.get<number>(EnvironmentKeys.APP_ENV_MAX_RETRIES);
803
- ```
804
-
805
- ## Logging Patterns
806
-
807
- ### Method Context Prefix
808
-
809
- Always include class and method context in log messages:
810
-
811
- ```typescript
812
- // Format: [ClassName][methodName] Message with %s placeholders
813
- this.logger.info('[binding] Asset storage bound | Key: %s | Type: %s', key, storageType);
814
- this.logger.debug('[authenticate] Token validated | User: %s', userId);
815
- this.logger.warn('[register] Skipping duplicate registration | Type: %s', opts.type);
816
- this.logger.error('[generate] Token generation failed | Error: %s', error.message);
817
- ```
818
-
819
- ### Structured Data
820
-
821
- Use format specifiers for structured logging:
822
-
823
- ```typescript
824
- // %s - string, %d - number, %j - JSON object
825
- this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.email);
826
- this.logger.debug('[config] Server options: %j', this.serverOptions);
827
- ```
828
-
829
- ## Standardized Error Handling
830
-
831
- Use the `getError` helper and `HTTP` constants to throw consistent, formatted exceptions that the framework's error handler can process correctly.
832
-
833
- ### Basic Error
834
-
835
- ```typescript
836
- import { getError, HTTP } from '@venizia/ignis';
837
-
838
- if (!record) {
839
- throw getError({
840
- statusCode: HTTP.ResultCodes.RS_4.NotFound,
841
- message: 'Record not found',
842
- details: { id: requestedId },
843
- });
844
- }
845
- ```
846
-
847
- ### Error with Context
848
-
849
- Include class/method context in error messages:
850
-
851
- ```typescript
852
- // Format: [ClassName][methodName] Descriptive message
853
- throw getError({
854
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
855
- message: '[JWTTokenService][generate] Failed to generate token',
856
- });
857
-
858
- throw getError({
859
- statusCode: HTTP.ResultCodes.RS_4.Unauthorized,
860
- message: '[AuthMiddleware][authenticate] Missing authorization header',
861
- });
862
- ```
863
-
864
- ### Validation Errors
865
-
866
- ```typescript
867
- constructor(options: IServiceOptions) {
868
- if (!options.apiKey) {
869
- throw getError({
870
- statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
871
- message: '[PaymentService] Missing required apiKey configuration',
872
- });
873
- }
874
- }
875
- ```
876
-
877
- ### HTTP Status Code Categories
878
-
879
- | Category | Constant | Use Case |
880
- |----------|----------|----------|
881
- | Success | `HTTP.ResultCodes.RS_2.Ok` | Successful response |
882
- | Created | `HTTP.ResultCodes.RS_2.Created` | Resource created |
883
- | Bad Request | `HTTP.ResultCodes.RS_4.BadRequest` | Invalid input |
884
- | Unauthorized | `HTTP.ResultCodes.RS_4.Unauthorized` | Missing/invalid auth |
885
- | Forbidden | `HTTP.ResultCodes.RS_4.Forbidden` | Insufficient permissions |
886
- | Not Found | `HTTP.ResultCodes.RS_4.NotFound` | Resource not found |
887
- | Internal Error | `HTTP.ResultCodes.RS_5.InternalServerError` | Server errors |
888
-
889
- ## Control Flow Patterns
890
-
891
- ### Mandatory Braces
892
-
893
- **Always use braces for `if`, `for`, `while`, and `do-while` statements**, even for single-line bodies. Never use inline statements.
894
-
895
- ```typescript
896
- // ✅ GOOD - Always use braces
897
- if (condition) {
898
- doSomething();
899
- }
900
-
901
- for (const item of items) {
902
- process(item);
903
- }
904
-
905
- while (running) {
906
- tick();
907
- }
908
-
909
- do {
910
- attempt();
911
- } while (retrying);
912
-
913
- // ❌ BAD - Never inline without braces
914
- if (condition) doSomething();
915
- for (const item of items) process(item);
916
- while (running) tick();
917
- ```
918
-
919
- **Why braces are mandatory:**
920
- - Prevents bugs when adding statements later
921
- - Clearer code structure at a glance
922
- - Consistent formatting across codebase
923
-
924
- ### Switch Statement Requirements
925
-
926
- **All switch statements must:**
927
- 1. Use braces `{}` for each case block
928
- 2. Include a `default` case (even if it throws)
929
-
930
- ```typescript
931
- // ✅ GOOD - Braces and default case
932
- switch (status) {
933
- case 'active': {
934
- activateUser();
935
- break;
936
- }
937
- case 'inactive': {
938
- deactivateUser();
939
- break;
940
- }
941
- case 'pending': {
942
- notifyAdmin();
943
- break;
944
- }
945
- default: {
946
- throw getError({
947
- statusCode: HTTP.ResultCodes.RS_4.BadRequest,
948
- message: `Unknown status: ${status}`,
949
- });
950
- }
951
- }
952
-
953
- // ❌ BAD - Missing braces and default case
954
- switch (status) {
955
- case 'active':
956
- activateUser();
957
- break;
958
- case 'inactive':
959
- deactivateUser();
960
- break;
961
- // Missing default case!
962
- }
963
- ```
964
-
965
- **Why these rules:**
966
- - Braces prevent variable scoping issues between cases
967
- - Default case ensures all values are handled
968
- - Throwing in default catches unexpected values early
969
-
970
- ## Scope Naming
971
-
972
- Every class extending a base class should set its scope using `ClassName.name`:
973
-
974
- ```typescript
975
- export class JWTTokenService extends BaseService {
976
- constructor() {
977
- super({ scope: JWTTokenService.name });
978
- }
979
- }
980
-
981
- export class UserController extends BaseController {
982
- constructor() {
983
- super({ scope: UserController.name });
984
- }
985
- }
986
- ```
987
-
988
- ## Code Organization
989
-
990
- ### Section Separator Comments
991
-
992
- Use visual separators for major code sections in long files:
993
-
994
- ```typescript
995
- // ---------------------------------------------------------------------------
996
- // Type Definitions
997
- // ---------------------------------------------------------------------------
998
-
999
- type TMyType = { /* ... */ };
1000
-
1001
- // ---------------------------------------------------------------------------
1002
- // Constants
1003
- // ---------------------------------------------------------------------------
1004
-
1005
- const DEFAULT_OPTIONS = { /* ... */ };
1006
-
1007
- // ---------------------------------------------------------------------------
1008
- // Main Implementation
1009
- // ---------------------------------------------------------------------------
1010
-
1011
- export class MyClass {
1012
- // ...
1013
- }
1014
- ```
1015
-
1016
- **Guidelines:**
1017
- - Use for files > 200 lines with distinct sections
1018
- - Use 75-character wide separator lines
1019
- - Descriptive section names (2-4 words)
1020
-
1021
- ### Import Organization Order
1022
-
1023
- Organize imports in this order:
1024
-
1025
- ```typescript
1026
- // 1. Node built-ins (with 'node:' prefix)
1027
- import fs from 'node:fs';
1028
- import path from 'node:path';
1029
-
1030
- // 2. Third-party packages (alphabetical)
1031
- import { z } from '@hono/zod-openapi';
1032
- import dayjs from 'dayjs';
1033
-
1034
- // 3. Internal absolute imports (by domain/package)
1035
- import { getError } from '@venizia/ignis-helpers';
1036
- import { BaseEntity } from '@/base/models';
1037
- import { UserService } from '@/services';
1038
-
1039
- // 4. Relative imports (same feature) - LAST
1040
- import { AbstractRepository } from './base';
1041
- import { QueryBuilder } from '../query';
1042
- ```
1043
-
1044
- **Rules:**
1045
- - Blank line between each group
1046
- - Alphabetical within each group
1047
- - `node:` prefix for Node.js built-ins
1048
- - Relative imports only for same feature/module
1049
-
1050
- ## Performance Logging Pattern
1051
-
1052
- Use `performance.now()` for timing critical operations:
1053
-
1054
- ```typescript
1055
- const t = performance.now();
1056
-
1057
- // ... operation to measure ...
1058
-
1059
- this.logger.info('[methodName] DONE | Took: %s (ms)', performance.now() - t);
1060
- ```
1061
-
1062
- **With the helper utility:**
1063
-
1064
- ```typescript
1065
- import { executeWithPerformanceMeasure } from '@venizia/ignis';
1066
-
1067
- await executeWithPerformanceMeasure({
1068
- logger: this.logger,
1069
- scope: 'DataSync',
1070
- description: 'Sync user records',
1071
- task: async () => {
1072
- await syncAllUsers();
1073
- },
1074
- });
1075
- // Logs: [DataSync] Sync user records | Took: 1234.56 (ms)
1076
- ```
1077
-
1078
- ## Advanced Patterns
1079
-
1080
- ### Mixin Pattern
1081
-
1082
- Create reusable class extensions without deep inheritance:
1083
-
1084
- ```typescript
1085
- import { TMixinTarget } from '@venizia/ignis';
1086
-
1087
- export const LoggableMixin = <BaseClass extends TMixinTarget<Base>>(
1088
- baseClass: BaseClass,
1089
- ) => {
1090
- return class extends baseClass {
1091
- protected logger = LoggerFactory.getLogger(this.constructor.name);
1092
-
1093
- log(message: string): void {
1094
- this.logger.info(message);
1095
- }
1096
- };
1097
- };
1098
-
1099
- // Usage
1100
- class MyService extends LoggableMixin(BaseService) {
1101
- doWork(): void {
1102
- this.log('Work started'); // Method from mixin
1103
- }
1104
- }
1105
- ```
1106
-
1107
- ### Factory Pattern with Dynamic Class
1108
-
1109
- Generate classes dynamically with configuration:
1110
-
1111
- ```typescript
1112
- class ControllerFactory {
1113
- static defineCrudController<Schema extends TTableSchemaWithId>(
1114
- opts: ICrudControllerOptions<Schema>,
1115
- ) {
1116
- return class extends BaseController {
1117
- constructor(repository: AbstractRepository<Schema>) {
1118
- super({ scope: opts.controller.name });
1119
- this.repository = repository;
1120
- this.setupRoutes();
1121
- }
1122
-
1123
- private setupRoutes(): void {
1124
- // Dynamically bind CRUD routes
1125
- }
1126
- };
1127
- }
1128
- }
1129
-
1130
- // Usage
1131
- const UserCrudController = ControllerFactory.defineCrudController({
1132
- controller: { name: 'UserController', basePath: '/users' },
1133
- repository: { name: UserRepository.name },
1134
- entity: () => User,
1135
- });
1136
-
1137
- @controller({ path: '/users' })
1138
- export class UserController extends UserCrudController {
1139
- // Additional custom routes
1140
- }
1141
- ```
1142
-
1143
- ### Value Resolver Pattern
1144
-
1145
- Support multiple input types that resolve to a single value:
1146
-
1147
- ```typescript
1148
- export type TValueOrResolver<T> = T | TResolver<T> | TConstructor<T>;
1149
-
1150
- export const resolveValue = <T>(valueOrResolver: TValueOrResolver<T>): T => {
1151
- if (typeof valueOrResolver !== 'function') {
1152
- return valueOrResolver; // Direct value
1153
- }
1154
- if (isClassConstructor(valueOrResolver)) {
1155
- return valueOrResolver as T; // Class constructor (return as-is)
1156
- }
1157
- return (valueOrResolver as TResolver<T>)(); // Function resolver
1158
- };
1159
-
1160
- // Usage
1161
- interface IOptions {
1162
- entity: TValueOrResolver<typeof User>;
1163
- }
1164
-
1165
- // All valid:
1166
- const opts1: IOptions = { entity: User }; // Direct class
1167
- const opts2: IOptions = { entity: () => User }; // Resolver function
1168
- ```
1169
-
1170
- ## Summary Table
1171
-
1172
- | Aspect | Standard |
1173
- |--------|----------|
1174
- | Interface prefix | `I` (e.g., `IUserService`) |
1175
- | Type alias prefix | `T` (e.g., `TUserRequest`) |
1176
- | Class naming | PascalCase with suffix (e.g., `UserController`) |
1177
- | File naming | kebab-case (e.g., `user.controller.ts`) |
1178
- | Private fields | Underscore prefix (`_dataSource`) |
1179
- | Binding keys | `@app/[component]/[feature]` |
1180
- | Constants | Static readonly class (not enums) |
1181
- | Barrel exports | `index.ts` at every folder level |
1182
- | Error format | `[ClassName][method] Message` |
1183
- | Logging format | `[method] Message \| Key: %s` |
1184
- | Default options | `DEFAULT_OPTIONS` constant |
1185
- | Type safety | No `any` or `unknown` allowed |
1186
- | Scope naming | `ClassName.name` |
1187
- | Arguments | Options object (`opts`) |
1188
- | Exports | Named exports only |
1189
- | Return types | Explicitly defined |
1190
- | Control flow | Always use braces (`{}`) |
1191
- | Switch statements | Braces + default case required |
1192
- | Imports | Node → Third-party → Internal → Relative |
1193
- | Function naming | `generate*`, `build*`, `to*`, `is*`, `extract*` |