@venizia/ignis-docs 0.0.2 → 0.0.4-0
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/README.md +1 -1
- package/package.json +4 -2
- package/wiki/best-practices/api-usage-examples.md +591 -0
- package/wiki/best-practices/architectural-patterns.md +415 -0
- package/wiki/best-practices/architecture-decisions.md +488 -0
- package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +647 -182
- package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
- package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
- package/wiki/best-practices/data-modeling.md +376 -0
- package/wiki/best-practices/deployment-strategies.md +698 -0
- package/wiki/best-practices/index.md +27 -0
- package/wiki/best-practices/performance-optimization.md +196 -0
- package/wiki/best-practices/security-guidelines.md +218 -0
- package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
- package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
- package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
- package/wiki/changelogs/2025-12-17-refactor.md +1 -1
- package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
- package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
- 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/2025-12-29-dynamic-binding-registration.md +104 -0
- package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
- package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
- package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
- package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
- package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
- package/wiki/changelogs/index.md +8 -1
- package/wiki/changelogs/planned-schema-migrator.md +2 -10
- package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
- package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
- package/wiki/guides/core-concepts/components-guide.md +509 -0
- package/wiki/guides/core-concepts/components.md +122 -0
- package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
- package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
- package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
- package/wiki/guides/core-concepts/persistent/index.md +119 -0
- package/wiki/guides/core-concepts/persistent/models.md +241 -0
- package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
- package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
- package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
- package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
- package/wiki/guides/get-started/philosophy.md +682 -0
- package/wiki/guides/get-started/setup.md +157 -0
- package/wiki/guides/index.md +89 -0
- package/wiki/guides/reference/glossary.md +243 -0
- package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
- package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
- package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
- package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
- package/wiki/guides/tutorials/realtime-chat.md +1261 -0
- package/wiki/guides/tutorials/testing.md +723 -0
- package/wiki/index.md +176 -37
- package/wiki/references/base/application.md +27 -0
- package/wiki/references/base/bootstrapping.md +30 -26
- package/wiki/references/base/components.md +532 -31
- package/wiki/references/base/controllers.md +136 -38
- package/wiki/references/base/datasources.md +108 -5
- package/wiki/references/base/dependency-injection.md +39 -3
- package/wiki/references/base/filter-system/application-usage.md +224 -0
- package/wiki/references/base/filter-system/array-operators.md +132 -0
- package/wiki/references/base/filter-system/comparison-operators.md +109 -0
- package/wiki/references/base/filter-system/default-filter.md +428 -0
- package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
- package/wiki/references/base/filter-system/index.md +127 -0
- package/wiki/references/base/filter-system/json-filtering.md +197 -0
- package/wiki/references/base/filter-system/list-operators.md +71 -0
- package/wiki/references/base/filter-system/logical-operators.md +156 -0
- package/wiki/references/base/filter-system/null-operators.md +58 -0
- package/wiki/references/base/filter-system/pattern-matching.md +108 -0
- package/wiki/references/base/filter-system/quick-reference.md +431 -0
- package/wiki/references/base/filter-system/range-operators.md +63 -0
- package/wiki/references/base/filter-system/tips.md +190 -0
- package/wiki/references/base/filter-system/use-cases.md +452 -0
- package/wiki/references/base/index.md +90 -0
- package/wiki/references/base/middlewares.md +602 -0
- package/wiki/references/base/models.md +215 -23
- package/wiki/references/base/providers.md +732 -0
- package/wiki/references/base/repositories/advanced.md +555 -0
- package/wiki/references/base/repositories/index.md +228 -0
- package/wiki/references/base/repositories/mixins.md +331 -0
- package/wiki/references/base/repositories/relations.md +486 -0
- package/wiki/references/base/repositories.md +40 -549
- package/wiki/references/base/services.md +28 -4
- package/wiki/references/components/authentication.md +22 -2
- package/wiki/references/components/health-check.md +12 -0
- package/wiki/references/components/index.md +23 -0
- package/wiki/references/components/mail.md +687 -0
- package/wiki/references/components/request-tracker.md +16 -0
- package/wiki/references/components/socket-io.md +18 -0
- package/wiki/references/components/static-asset.md +14 -26
- package/wiki/references/components/swagger.md +17 -0
- package/wiki/references/configuration/environment-variables.md +427 -0
- package/wiki/references/configuration/index.md +73 -0
- package/wiki/references/helpers/cron.md +14 -0
- package/wiki/references/helpers/crypto.md +15 -0
- package/wiki/references/helpers/env.md +16 -0
- package/wiki/references/helpers/error.md +17 -0
- package/wiki/references/helpers/index.md +15 -0
- package/wiki/references/helpers/inversion.md +24 -4
- package/wiki/references/helpers/logger.md +19 -0
- package/wiki/references/helpers/network.md +11 -0
- package/wiki/references/helpers/queue.md +19 -0
- package/wiki/references/helpers/redis.md +21 -0
- package/wiki/references/helpers/socket-io.md +24 -5
- package/wiki/references/helpers/storage.md +18 -10
- package/wiki/references/helpers/testing.md +18 -0
- package/wiki/references/helpers/types.md +167 -0
- package/wiki/references/helpers/uid.md +167 -0
- package/wiki/references/helpers/worker-thread.md +16 -0
- package/wiki/references/index.md +177 -0
- package/wiki/references/quick-reference.md +634 -0
- package/wiki/references/src-details/boot.md +3 -3
- package/wiki/references/src-details/dev-configs.md +0 -4
- package/wiki/references/src-details/docs.md +2 -2
- package/wiki/references/src-details/index.md +86 -0
- package/wiki/references/src-details/inversion.md +1 -6
- package/wiki/references/src-details/mcp-server.md +3 -15
- package/wiki/references/utilities/index.md +86 -10
- package/wiki/references/utilities/jsx.md +577 -0
- package/wiki/references/utilities/request.md +0 -2
- package/wiki/references/utilities/statuses.md +740 -0
- package/wiki/changelogs/planned-transaction-support.md +0 -216
- package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
- package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
- package/wiki/get-started/best-practices/data-modeling.md +0 -177
- package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
- package/wiki/get-started/best-practices/performance-optimization.md +0 -88
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/components.md +0 -98
- package/wiki/get-started/core-concepts/persistent.md +0 -543
- package/wiki/get-started/index.md +0 -65
- package/wiki/get-started/philosophy.md +0 -296
- package/wiki/get-started/prerequisites.md +0 -113
|
@@ -115,7 +115,56 @@ Use the centralized TypeScript configs:
|
|
|
115
115
|
- `strict: true` - Strict type checking with selective relaxations
|
|
116
116
|
- `skipLibCheck: true` - Faster compilation
|
|
117
117
|
|
|
118
|
-
See [`@venizia/dev-configs` documentation](
|
|
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
|
+
```
|
|
119
168
|
|
|
120
169
|
## Naming Conventions
|
|
121
170
|
|
|
@@ -184,53 +233,357 @@ export class SocketIOBindingKeys {
|
|
|
184
233
|
}
|
|
185
234
|
```
|
|
186
235
|
|
|
187
|
-
##
|
|
236
|
+
## Private Field Naming Convention
|
|
188
237
|
|
|
189
|
-
|
|
238
|
+
Use underscore prefix (`_`) for private and protected class fields to distinguish them from public fields and method parameters.
|
|
190
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
|
+
}
|
|
191
255
|
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
280
|
```
|
|
202
281
|
|
|
203
|
-
|
|
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.
|
|
204
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) { ... }
|
|
205
318
|
```
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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>;
|
|
218
333
|
```
|
|
219
334
|
|
|
220
|
-
###
|
|
335
|
+
### Const Assertion for Literal Types
|
|
221
336
|
|
|
222
|
-
|
|
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
|
|
223
348
|
|
|
224
349
|
```typescript
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
350
|
+
export class DefaultCRUDRepository<
|
|
351
|
+
Schema extends TTableSchemaWithId = TTableSchemaWithId
|
|
352
|
+
> {
|
|
353
|
+
// Schema is constrained to have an 'id' column
|
|
354
|
+
}
|
|
229
355
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
});
|
|
234
587
|
```
|
|
235
588
|
|
|
236
589
|
## Constants Pattern
|
|
@@ -427,6 +780,28 @@ constructor(options: IJWTTokenServiceOptions) {
|
|
|
427
780
|
}
|
|
428
781
|
```
|
|
429
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
|
+
|
|
430
805
|
## Logging Patterns
|
|
431
806
|
|
|
432
807
|
### Method Context Prefix
|
|
@@ -451,46 +826,6 @@ this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.ema
|
|
|
451
826
|
this.logger.debug('[config] Server options: %j', this.serverOptions);
|
|
452
827
|
```
|
|
453
828
|
|
|
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
829
|
## Standardized Error Handling
|
|
495
830
|
|
|
496
831
|
Use the `getError` helper and `HTTP` constants to throw consistent, formatted exceptions that the framework's error handler can process correctly.
|
|
@@ -551,163 +886,285 @@ constructor(options: IServiceOptions) {
|
|
|
551
886
|
| Not Found | `HTTP.ResultCodes.RS_4.NotFound` | Resource not found |
|
|
552
887
|
| Internal Error | `HTTP.ResultCodes.RS_5.InternalServerError` | Server errors |
|
|
553
888
|
|
|
554
|
-
##
|
|
555
|
-
|
|
556
|
-
Ignis supports three methods for defining routes. Choose based on your needs:
|
|
889
|
+
## Control Flow Patterns
|
|
557
890
|
|
|
558
|
-
###
|
|
891
|
+
### Mandatory Braces
|
|
559
892
|
|
|
560
|
-
|
|
893
|
+
**Always use braces for `if`, `for`, `while`, and `do-while` statements**, even for single-line bodies. Never use inline statements.
|
|
561
894
|
|
|
562
895
|
```typescript
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
static readonly BY_ID = '/:id';
|
|
567
|
-
static readonly PROFILE = '/profile';
|
|
896
|
+
// ✅ GOOD - Always use braces
|
|
897
|
+
if (condition) {
|
|
898
|
+
doSomething();
|
|
568
899
|
}
|
|
569
900
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
[HTTP.ResultCodes.RS_2.Ok]: UserSchema,
|
|
587
|
-
[HTTP.ResultCodes.RS_4.NotFound]: ErrorSchema,
|
|
588
|
-
}),
|
|
589
|
-
},
|
|
590
|
-
} as const;
|
|
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();
|
|
591
917
|
```
|
|
592
918
|
|
|
593
|
-
|
|
919
|
+
**Why braces are mandatory:**
|
|
920
|
+
- Prevents bugs when adding statements later
|
|
921
|
+
- Clearer code structure at a glance
|
|
922
|
+
- Consistent formatting across codebase
|
|
594
923
|
|
|
595
|
-
|
|
596
|
-
@controller({ path: '/users' })
|
|
597
|
-
export class UserController extends BaseController {
|
|
924
|
+
### Switch Statement Requirements
|
|
598
925
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
926
|
+
**All switch statements must:**
|
|
927
|
+
1. Use braces `{}` for each case block
|
|
928
|
+
2. Include a `default` case (even if it throws)
|
|
603
929
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
+
});
|
|
608
950
|
}
|
|
609
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
|
+
}
|
|
610
963
|
```
|
|
611
964
|
|
|
612
|
-
|
|
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`:
|
|
613
973
|
|
|
614
974
|
```typescript
|
|
615
|
-
|
|
616
|
-
export class HealthCheckController extends BaseController {
|
|
975
|
+
export class JWTTokenService extends BaseService {
|
|
617
976
|
constructor() {
|
|
618
|
-
super({ scope:
|
|
977
|
+
super({ scope: JWTTokenService.name });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
619
980
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
});
|
|
981
|
+
export class UserController extends BaseController {
|
|
982
|
+
constructor() {
|
|
983
|
+
super({ scope: UserController.name });
|
|
623
984
|
}
|
|
624
985
|
}
|
|
625
986
|
```
|
|
626
987
|
|
|
627
|
-
|
|
988
|
+
## Code Organization
|
|
989
|
+
|
|
990
|
+
### Section Separator Comments
|
|
991
|
+
|
|
992
|
+
Use visual separators for major code sections in long files:
|
|
628
993
|
|
|
629
994
|
```typescript
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
super({ scope: HealthCheckController.name });
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
// Type Definitions
|
|
997
|
+
// ---------------------------------------------------------------------------
|
|
634
998
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
+
// ...
|
|
643
1013
|
}
|
|
644
1014
|
```
|
|
645
1015
|
|
|
646
|
-
|
|
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)
|
|
647
1020
|
|
|
648
|
-
|
|
1021
|
+
### Import Organization Order
|
|
1022
|
+
|
|
1023
|
+
Organize imports in this order:
|
|
649
1024
|
|
|
650
1025
|
```typescript
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
```
|
|
658
1043
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
+
},
|
|
666
1074
|
});
|
|
1075
|
+
// Logs: [DataSync] Sync user records | Took: 1234.56 (ms)
|
|
667
1076
|
```
|
|
668
1077
|
|
|
669
|
-
##
|
|
1078
|
+
## Advanced Patterns
|
|
670
1079
|
|
|
671
|
-
###
|
|
1080
|
+
### Mixin Pattern
|
|
1081
|
+
|
|
1082
|
+
Create reusable class extensions without deep inheritance:
|
|
672
1083
|
|
|
673
1084
|
```typescript
|
|
674
|
-
|
|
675
|
-
export const SignInRequestSchema = z.object({
|
|
676
|
-
email: z.string().email(),
|
|
677
|
-
password: z.string().min(8),
|
|
678
|
-
});
|
|
1085
|
+
import { TMixinTarget } from '@venizia/ignis';
|
|
679
1086
|
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
1105
|
```
|
|
683
1106
|
|
|
684
|
-
###
|
|
1107
|
+
### Factory Pattern with Dynamic Class
|
|
1108
|
+
|
|
1109
|
+
Generate classes dynamically with configuration:
|
|
685
1110
|
|
|
686
1111
|
```typescript
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
+
}
|
|
691
1129
|
|
|
692
|
-
//
|
|
693
|
-
|
|
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
|
+
}
|
|
694
1141
|
```
|
|
695
1142
|
|
|
696
|
-
###
|
|
1143
|
+
### Value Resolver Pattern
|
|
1144
|
+
|
|
1145
|
+
Support multiple input types that resolve to a single value:
|
|
697
1146
|
|
|
698
1147
|
```typescript
|
|
699
|
-
export
|
|
700
|
-
Schema extends TTableSchemaWithId = TTableSchemaWithId
|
|
701
|
-
> {
|
|
702
|
-
// Schema is constrained to have an 'id' column
|
|
703
|
-
}
|
|
1148
|
+
export type TValueOrResolver<T> = T | TResolver<T> | TConstructor<T>;
|
|
704
1149
|
|
|
705
|
-
export
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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>;
|
|
710
1163
|
}
|
|
1164
|
+
|
|
1165
|
+
// All valid:
|
|
1166
|
+
const opts1: IOptions = { entity: User }; // Direct class
|
|
1167
|
+
const opts2: IOptions = { entity: () => User }; // Resolver function
|
|
711
1168
|
```
|
|
712
1169
|
|
|
713
1170
|
## Summary Table
|
|
@@ -718,11 +1175,19 @@ export interface IAuthService<
|
|
|
718
1175
|
| Type alias prefix | `T` (e.g., `TUserRequest`) |
|
|
719
1176
|
| Class naming | PascalCase with suffix (e.g., `UserController`) |
|
|
720
1177
|
| File naming | kebab-case (e.g., `user.controller.ts`) |
|
|
1178
|
+
| Private fields | Underscore prefix (`_dataSource`) |
|
|
721
1179
|
| Binding keys | `@app/[component]/[feature]` |
|
|
722
1180
|
| Constants | Static readonly class (not enums) |
|
|
723
1181
|
| Barrel exports | `index.ts` at every folder level |
|
|
724
1182
|
| Error format | `[ClassName][method] Message` |
|
|
725
1183
|
| Logging format | `[method] Message \| Key: %s` |
|
|
726
1184
|
| Default options | `DEFAULT_OPTIONS` constant |
|
|
1185
|
+
| Type safety | No `any` or `unknown` allowed |
|
|
727
1186
|
| Scope naming | `ClassName.name` |
|
|
728
|
-
|
|
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*` |
|