@venizia/ignis-docs 0.0.4-1 → 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.
- package/package.json +1 -1
- package/wiki/best-practices/api-usage-examples.md +1 -0
- package/wiki/best-practices/code-style-standards/advanced-patterns.md +259 -0
- package/wiki/best-practices/code-style-standards/constants-configuration.md +225 -0
- package/wiki/best-practices/code-style-standards/control-flow.md +245 -0
- package/wiki/best-practices/code-style-standards/documentation.md +221 -0
- package/wiki/best-practices/code-style-standards/function-patterns.md +142 -0
- package/wiki/best-practices/code-style-standards/index.md +110 -0
- package/wiki/best-practices/code-style-standards/naming-conventions.md +174 -0
- package/wiki/best-practices/code-style-standards/route-definitions.md +150 -0
- package/wiki/best-practices/code-style-standards/tooling.md +155 -0
- package/wiki/best-practices/code-style-standards/type-safety.md +165 -0
- package/wiki/best-practices/common-pitfalls.md +164 -3
- package/wiki/best-practices/contribution-workflow.md +1 -1
- package/wiki/best-practices/data-modeling.md +102 -2
- package/wiki/best-practices/error-handling.md +468 -0
- package/wiki/best-practices/index.md +204 -21
- package/wiki/best-practices/performance-optimization.md +180 -0
- package/wiki/best-practices/security-guidelines.md +249 -0
- package/wiki/best-practices/testing-strategies.md +620 -0
- package/wiki/changelogs/2026-01-05-range-queries-content-range.md +184 -0
- package/wiki/changelogs/2026-01-06-basic-authentication.md +103 -0
- package/wiki/changelogs/2026-01-07-controller-route-customization.md +209 -0
- package/wiki/changelogs/index.md +3 -0
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/persistent/models.md +10 -0
- package/wiki/guides/tutorials/complete-installation.md +1 -1
- package/wiki/guides/tutorials/testing.md +1 -1
- package/wiki/references/base/components.md +47 -29
- package/wiki/references/base/controllers.md +215 -18
- package/wiki/references/base/filter-system/fields-order-pagination.md +84 -0
- package/wiki/references/base/middlewares.md +33 -1
- package/wiki/references/base/models.md +40 -2
- package/wiki/references/base/repositories/index.md +2 -0
- package/wiki/references/components/authentication.md +261 -247
- package/wiki/references/helpers/index.md +1 -1
- package/wiki/references/src-details/core.md +1 -1
- 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*` |
|