@venizia/ignis-docs 0.0.3 → 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 +406 -17
- 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 +2 -2
- 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 +6 -0
- package/wiki/changelogs/planned-schema-migrator.md +0 -8
- 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/{get-started → guides}/core-concepts/components.md +24 -17
- 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 +24 -7
- package/wiki/references/base/controllers.md +51 -20
- package/wiki/references/base/datasources.md +30 -0
- 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 -635
- 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 +14 -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 +16 -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/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 -97
- package/wiki/get-started/best-practices/security-guidelines.md +0 -99
- package/wiki/get-started/core-concepts/persistent.md +0 -539
- 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,7 @@ 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
119
|
|
|
120
120
|
## Directory Structure
|
|
121
121
|
|
|
@@ -233,6 +233,57 @@ export class SocketIOBindingKeys {
|
|
|
233
233
|
}
|
|
234
234
|
```
|
|
235
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
|
+
|
|
236
287
|
## Type Safety
|
|
237
288
|
|
|
238
289
|
To ensure long-term maintainability and catch errors at compile-time, Ignis enforces strict type safety.
|
|
@@ -284,13 +335,13 @@ export type TSignInRequest = z.infer<typeof SignInRequestSchema>;
|
|
|
284
335
|
### Const Assertion for Literal Types
|
|
285
336
|
|
|
286
337
|
```typescript
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
338
|
+
const RouteConfigs = {
|
|
339
|
+
GET_USERS: { method: 'GET', path: '/users' },
|
|
340
|
+
GET_USER_BY_ID: { method: 'GET', path: '/users/:id' },
|
|
290
341
|
} as const;
|
|
291
342
|
|
|
292
343
|
// Type is now narrowed to literal values
|
|
293
|
-
type RouteKey = keyof typeof
|
|
344
|
+
type RouteKey = keyof typeof RouteConfigs; // 'GET_USERS' | 'GET_USER_BY_ID'
|
|
294
345
|
```
|
|
295
346
|
|
|
296
347
|
### Generic Type Constraints
|
|
@@ -310,6 +361,35 @@ export interface IAuthService<
|
|
|
310
361
|
}
|
|
311
362
|
```
|
|
312
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
|
+
|
|
313
393
|
## Module Exports
|
|
314
394
|
|
|
315
395
|
### Prefer Named Exports
|
|
@@ -350,13 +430,54 @@ class UserService {
|
|
|
350
430
|
// Usage: service.createUser('John', 'john@example.com');
|
|
351
431
|
```
|
|
352
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
|
+
|
|
353
474
|
## Route Definition Patterns
|
|
354
475
|
|
|
355
476
|
Ignis supports three methods for defining routes. Choose based on your needs:
|
|
356
477
|
|
|
357
478
|
### Method 1: Config-Driven Routes
|
|
358
479
|
|
|
359
|
-
Define route configurations as constants:
|
|
480
|
+
Define route configurations as constants with UPPER_CASE names:
|
|
360
481
|
|
|
361
482
|
```typescript
|
|
362
483
|
// common/rest-paths.ts
|
|
@@ -367,19 +488,19 @@ export class UserRestPaths {
|
|
|
367
488
|
}
|
|
368
489
|
|
|
369
490
|
// common/route-configs.ts
|
|
370
|
-
export const
|
|
371
|
-
|
|
491
|
+
export const RouteConfigs = {
|
|
492
|
+
GET_USERS: {
|
|
372
493
|
method: HTTP.Methods.GET,
|
|
373
494
|
path: UserRestPaths.ROOT,
|
|
374
495
|
responses: jsonResponse({
|
|
375
496
|
[HTTP.ResultCodes.RS_2.Ok]: UserListSchema,
|
|
376
497
|
}),
|
|
377
498
|
},
|
|
378
|
-
|
|
499
|
+
GET_USER_BY_ID: {
|
|
379
500
|
method: HTTP.Methods.GET,
|
|
380
501
|
path: UserRestPaths.BY_ID,
|
|
381
502
|
request: {
|
|
382
|
-
params: z.object({ id: z.string()
|
|
503
|
+
params: z.object({ id: z.string() }),
|
|
383
504
|
},
|
|
384
505
|
responses: jsonResponse({
|
|
385
506
|
[HTTP.ResultCodes.RS_2.Ok]: UserSchema,
|
|
@@ -395,13 +516,13 @@ export const ROUTE_CONFIGS = {
|
|
|
395
516
|
@controller({ path: '/users' })
|
|
396
517
|
export class UserController extends BaseController {
|
|
397
518
|
|
|
398
|
-
@api({ configs:
|
|
399
|
-
list(context: TRouteContext<typeof
|
|
519
|
+
@api({ configs: RouteConfigs.GET_USERS })
|
|
520
|
+
list(context: TRouteContext<typeof RouteConfigs.GET_USERS>) {
|
|
400
521
|
return context.json({ users: [] }, HTTP.ResultCodes.RS_2.Ok);
|
|
401
522
|
}
|
|
402
523
|
|
|
403
|
-
@api({ configs:
|
|
404
|
-
getById(context: TRouteContext<typeof
|
|
524
|
+
@api({ configs: RouteConfigs.GET_USER_BY_ID })
|
|
525
|
+
getById(context: TRouteContext<typeof RouteConfigs.GET_USER_BY_ID>) {
|
|
405
526
|
const { id } = context.req.valid('param');
|
|
406
527
|
return context.json({ id, name: 'User' }, HTTP.ResultCodes.RS_2.Ok);
|
|
407
528
|
}
|
|
@@ -416,7 +537,7 @@ export class HealthCheckController extends BaseController {
|
|
|
416
537
|
constructor() {
|
|
417
538
|
super({ scope: HealthCheckController.name });
|
|
418
539
|
|
|
419
|
-
this.bindRoute({ configs:
|
|
540
|
+
this.bindRoute({ configs: RouteConfigs.GET_HEALTH }).to({
|
|
420
541
|
handler: context => context.json({ status: 'ok' }),
|
|
421
542
|
});
|
|
422
543
|
}
|
|
@@ -432,7 +553,7 @@ export class HealthCheckController extends BaseController {
|
|
|
432
553
|
super({ scope: HealthCheckController.name });
|
|
433
554
|
|
|
434
555
|
this.defineRoute({
|
|
435
|
-
configs:
|
|
556
|
+
configs: RouteConfigs.POST_PING,
|
|
436
557
|
handler: context => {
|
|
437
558
|
const { message } = context.req.valid('json');
|
|
438
559
|
return context.json({ echo: message }, HTTP.ResultCodes.RS_2.Ok);
|
|
@@ -765,6 +886,87 @@ constructor(options: IServiceOptions) {
|
|
|
765
886
|
| Not Found | `HTTP.ResultCodes.RS_4.NotFound` | Resource not found |
|
|
766
887
|
| Internal Error | `HTTP.ResultCodes.RS_5.InternalServerError` | Server errors |
|
|
767
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
|
+
|
|
768
970
|
## Scope Naming
|
|
769
971
|
|
|
770
972
|
Every class extending a base class should set its scope using `ClassName.name`:
|
|
@@ -783,6 +985,188 @@ export class UserController extends BaseController {
|
|
|
783
985
|
}
|
|
784
986
|
```
|
|
785
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
|
+
|
|
786
1170
|
## Summary Table
|
|
787
1171
|
|
|
788
1172
|
| Aspect | Standard |
|
|
@@ -791,6 +1175,7 @@ export class UserController extends BaseController {
|
|
|
791
1175
|
| Type alias prefix | `T` (e.g., `TUserRequest`) |
|
|
792
1176
|
| Class naming | PascalCase with suffix (e.g., `UserController`) |
|
|
793
1177
|
| File naming | kebab-case (e.g., `user.controller.ts`) |
|
|
1178
|
+
| Private fields | Underscore prefix (`_dataSource`) |
|
|
794
1179
|
| Binding keys | `@app/[component]/[feature]` |
|
|
795
1180
|
| Constants | Static readonly class (not enums) |
|
|
796
1181
|
| Barrel exports | `index.ts` at every folder level |
|
|
@@ -801,4 +1186,8 @@ export class UserController extends BaseController {
|
|
|
801
1186
|
| Scope naming | `ClassName.name` |
|
|
802
1187
|
| Arguments | Options object (`opts`) |
|
|
803
1188
|
| Exports | Named exports only |
|
|
804
|
-
| Return types | Explicitly defined |
|
|
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*` |
|
|
@@ -125,14 +125,119 @@ APP_ENV_POSTGRES_DATABASE=db
|
|
|
125
125
|
|
|
126
126
|
## 5. Not Using `as const` for Route Definitions
|
|
127
127
|
|
|
128
|
-
**Pitfall:** When using the decorator-based routing with a shared `
|
|
128
|
+
**Pitfall:** When using the decorator-based routing with a shared `RouteConfigs` object, you forget to add `as const` to the object definition. TypeScript will infer the types too broadly, and you will lose the benefits of type-safe contexts (`TRouteContext`).
|
|
129
129
|
|
|
130
130
|
**Solution:** Always use `as const` when exporting a shared route configuration object.
|
|
131
131
|
|
|
132
132
|
**Example (`src/controllers/test/definitions.ts`):**
|
|
133
133
|
```typescript
|
|
134
|
-
export const
|
|
135
|
-
|
|
134
|
+
export const RouteConfigs = {
|
|
135
|
+
GET_USERS: { /* ... */ },
|
|
136
|
+
GET_USER_BY_ID: { /* ... */ },
|
|
136
137
|
} as const; // <-- This is crucial!
|
|
137
138
|
```
|
|
138
|
-
This ensures that `TRouteContext<typeof
|
|
139
|
+
This ensures that `TRouteContext<typeof RouteConfigs.GET_USERS>` has the precise types for request body, params, and response.
|
|
140
|
+
|
|
141
|
+
## 6. Bulk Operations Without WHERE Clause
|
|
142
|
+
|
|
143
|
+
**Problem:** Attempting to update or delete all records without an explicit `where` condition.
|
|
144
|
+
|
|
145
|
+
**Solution:** Ignis prevents accidental bulk data destruction. You must either provide a `where` condition or explicitly set `force: true`.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// ❌ BAD - Will throw error
|
|
149
|
+
await userRepository.updateBy({
|
|
150
|
+
data: { status: 'INACTIVE' },
|
|
151
|
+
where: {}, // Empty where = targets ALL records
|
|
152
|
+
});
|
|
153
|
+
// Error: [updateBy] DENY to perform updateBy | Empty where condition
|
|
154
|
+
|
|
155
|
+
// ✅ GOOD - Explicit where condition
|
|
156
|
+
await userRepository.updateBy({
|
|
157
|
+
data: { status: 'INACTIVE' },
|
|
158
|
+
where: { lastLoginAt: { lt: new Date('2024-01-01') } },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ✅ GOOD - Intentionally affect all records with force flag
|
|
162
|
+
await userRepository.updateBy({
|
|
163
|
+
data: { status: 'INACTIVE' },
|
|
164
|
+
where: {},
|
|
165
|
+
options: { force: true }, // Explicitly allow empty where
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> [!WARNING]
|
|
170
|
+
> The `force: true` flag bypasses the safety check. Only use when you intentionally want to affect ALL records in the table.
|
|
171
|
+
|
|
172
|
+
## 7. Schema Key Mismatch
|
|
173
|
+
|
|
174
|
+
**Problem:** Entity name doesn't match the table name registered in the DataSource's schema.
|
|
175
|
+
|
|
176
|
+
**Error Message:**
|
|
177
|
+
```
|
|
178
|
+
[UserRepository] Schema key mismatch | Entity name 'User' not found in connector.query | Available keys: [Configuration, Post]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Solution:** Ensure your entity class name matches the table name in `pgTable()`:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// ❌ BAD - Class name 'User' doesn't match table name 'users'
|
|
185
|
+
@model({ type: 'entity' })
|
|
186
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
187
|
+
static override schema = pgTable('users', { /* ... */ }); // Lowercase 'users'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ✅ GOOD - Class name matches table name
|
|
191
|
+
@model({ type: 'entity' })
|
|
192
|
+
export class User extends BaseEntity<typeof User.schema> {
|
|
193
|
+
static override schema = pgTable('User', { /* ... */ }); // Matches class name
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Why this matters:** The framework uses `entity.name` (class name) to look up the query interface in `connector.query`. If they don't match, the repository can't find its table.
|
|
198
|
+
|
|
199
|
+
## 8. Validation Error Response Structure
|
|
200
|
+
|
|
201
|
+
**Problem:** Client receives validation errors but doesn't know how to parse them.
|
|
202
|
+
|
|
203
|
+
**Solution:** Understand the Zod validation error response format:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"statusCode": 422,
|
|
208
|
+
"message": "ValidationError",
|
|
209
|
+
"requestId": "abc123",
|
|
210
|
+
"details": {
|
|
211
|
+
"cause": [
|
|
212
|
+
{
|
|
213
|
+
"path": "email",
|
|
214
|
+
"message": "Invalid email",
|
|
215
|
+
"code": "invalid_string",
|
|
216
|
+
"expected": "email",
|
|
217
|
+
"received": "string"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"path": "age",
|
|
221
|
+
"message": "Expected number, received string",
|
|
222
|
+
"code": "invalid_type",
|
|
223
|
+
"expected": "number",
|
|
224
|
+
"received": "string"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Client-side handling:**
|
|
232
|
+
```typescript
|
|
233
|
+
try {
|
|
234
|
+
await api.post('/users', data);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error.response?.status === 422) {
|
|
237
|
+
const errors = error.response.data.details.cause;
|
|
238
|
+
errors.forEach(err => {
|
|
239
|
+
console.log(`Field '${err.path}': ${err.message}`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
@@ -40,6 +40,32 @@ make install
|
|
|
40
40
|
git remote add upstream https://github.com/VENIZIA-AI/ignis.git
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Package Build Order
|
|
44
|
+
|
|
45
|
+
Ignis is a monorepo with interdependent packages. Understanding the dependency chain is critical for development:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
dev-configs → inversion → helpers → boot → core
|
|
49
|
+
↓ ↓ ↓ ↓ ↓
|
|
50
|
+
@venizia/ @venizia/ @venizia/ @venizia/ @venizia/
|
|
51
|
+
dev-configs ignis- ignis- ignis- ignis
|
|
52
|
+
inversion helpers boot (core)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Dependency meanings:**
|
|
56
|
+
| Package | Depends On | Purpose |
|
|
57
|
+
|---------|------------|---------|
|
|
58
|
+
| `dev-configs` | - | Shared ESLint, TypeScript configs |
|
|
59
|
+
| `inversion` | dev-configs | IoC container, DI primitives |
|
|
60
|
+
| `helpers` | inversion | Utilities, loggers, crypto |
|
|
61
|
+
| `boot` | helpers | Application bootstrapping |
|
|
62
|
+
| `core` | boot | Full framework (controllers, repos, etc.) |
|
|
63
|
+
|
|
64
|
+
**Why this matters:**
|
|
65
|
+
- If you modify `helpers`, you must rebuild `boot` and `core`
|
|
66
|
+
- If you modify `inversion`, you must rebuild `helpers`, `boot`, and `core`
|
|
67
|
+
- The Makefile handles this automatically with dependencies
|
|
68
|
+
|
|
43
69
|
## Makefile Commands
|
|
44
70
|
|
|
45
71
|
The project uses a Makefile for common development tasks:
|
|
@@ -53,14 +79,15 @@ The project uses a Makefile for common development tasks:
|
|
|
53
79
|
| `make lint` | Lint all packages |
|
|
54
80
|
| `make help` | Show all available commands |
|
|
55
81
|
|
|
56
|
-
**Individual package builds
|
|
82
|
+
**Individual package builds** (dependencies are automatically resolved):
|
|
57
83
|
```bash
|
|
58
|
-
make core # Build @venizia/ignis (
|
|
59
|
-
make boot # Build @venizia/ignis-boot
|
|
60
|
-
make helpers # Build @venizia/ignis-helpers
|
|
61
|
-
make inversion # Build @venizia/ignis-inversion
|
|
62
|
-
make dev-configs # Build @venizia/dev-configs
|
|
63
|
-
make docs # Build documentation
|
|
84
|
+
make core # Build @venizia/ignis (builds dev-configs → inversion → helpers → boot → core)
|
|
85
|
+
make boot # Build @venizia/ignis-boot (builds dev-configs → inversion → helpers → boot)
|
|
86
|
+
make helpers # Build @venizia/ignis-helpers (builds dev-configs → inversion → helpers)
|
|
87
|
+
make inversion # Build @venizia/ignis-inversion (builds dev-configs → inversion)
|
|
88
|
+
make dev-configs # Build @venizia/dev-configs only
|
|
89
|
+
make docs # Build VitePress documentation (independent)
|
|
90
|
+
make docs-mcp # Build MCP documentation server
|
|
64
91
|
```
|
|
65
92
|
|
|
66
93
|
**Force update individual packages:**
|