@venizia/ignis-docs 0.0.3 → 0.0.4-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -2
  3. package/wiki/best-practices/api-usage-examples.md +591 -0
  4. package/wiki/best-practices/architectural-patterns.md +415 -0
  5. package/wiki/best-practices/architecture-decisions.md +488 -0
  6. package/wiki/{get-started/best-practices → best-practices}/code-style-standards.md +406 -17
  7. package/wiki/{get-started/best-practices → best-practices}/common-pitfalls.md +109 -4
  8. package/wiki/{get-started/best-practices → best-practices}/contribution-workflow.md +34 -7
  9. package/wiki/best-practices/data-modeling.md +376 -0
  10. package/wiki/best-practices/deployment-strategies.md +698 -0
  11. package/wiki/best-practices/index.md +27 -0
  12. package/wiki/best-practices/performance-optimization.md +196 -0
  13. package/wiki/best-practices/security-guidelines.md +218 -0
  14. package/wiki/{get-started/best-practices → best-practices}/troubleshooting-tips.md +97 -1
  15. package/wiki/changelogs/2025-12-16-initial-architecture.md +1 -1
  16. package/wiki/changelogs/2025-12-16-model-repo-datasource-refactor.md +1 -1
  17. package/wiki/changelogs/2025-12-17-refactor.md +1 -1
  18. package/wiki/changelogs/2025-12-18-performance-optimizations.md +5 -5
  19. package/wiki/changelogs/2025-12-18-repository-validation-security.md +13 -7
  20. package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +2 -2
  21. package/wiki/changelogs/2025-12-29-dynamic-binding-registration.md +104 -0
  22. package/wiki/changelogs/2025-12-29-snowflake-uid-helper.md +100 -0
  23. package/wiki/changelogs/2025-12-30-repository-enhancements.md +214 -0
  24. package/wiki/changelogs/2025-12-31-json-path-filtering-array-operators.md +214 -0
  25. package/wiki/changelogs/2025-12-31-string-id-custom-generator.md +137 -0
  26. package/wiki/changelogs/2026-01-02-default-filter-and-repository-mixins.md +418 -0
  27. package/wiki/changelogs/index.md +6 -0
  28. package/wiki/changelogs/planned-schema-migrator.md +0 -8
  29. package/wiki/{get-started/core-concepts → guides/core-concepts/application}/bootstrapping.md +18 -5
  30. package/wiki/{get-started/core-concepts/application.md → guides/core-concepts/application/index.md} +47 -104
  31. package/wiki/guides/core-concepts/components-guide.md +509 -0
  32. package/wiki/{get-started → guides}/core-concepts/components.md +24 -17
  33. package/wiki/{get-started → guides}/core-concepts/controllers.md +30 -13
  34. package/wiki/{get-started → guides}/core-concepts/dependency-injection.md +97 -0
  35. package/wiki/guides/core-concepts/persistent/datasources.md +179 -0
  36. package/wiki/guides/core-concepts/persistent/index.md +119 -0
  37. package/wiki/guides/core-concepts/persistent/models.md +241 -0
  38. package/wiki/guides/core-concepts/persistent/repositories.md +219 -0
  39. package/wiki/guides/core-concepts/persistent/transactions.md +170 -0
  40. package/wiki/{get-started → guides}/core-concepts/services.md +26 -3
  41. package/wiki/{get-started → guides/get-started}/5-minute-quickstart.md +59 -14
  42. package/wiki/guides/get-started/philosophy.md +682 -0
  43. package/wiki/guides/get-started/setup.md +157 -0
  44. package/wiki/guides/index.md +89 -0
  45. package/wiki/guides/reference/glossary.md +243 -0
  46. package/wiki/{get-started → guides/reference}/mcp-docs-server.md +0 -10
  47. package/wiki/{get-started → guides/tutorials}/building-a-crud-api.md +134 -132
  48. package/wiki/{get-started/quickstart.md → guides/tutorials/complete-installation.md} +107 -71
  49. package/wiki/guides/tutorials/ecommerce-api.md +1399 -0
  50. package/wiki/guides/tutorials/realtime-chat.md +1261 -0
  51. package/wiki/guides/tutorials/testing.md +723 -0
  52. package/wiki/index.md +176 -37
  53. package/wiki/references/base/application.md +27 -0
  54. package/wiki/references/base/bootstrapping.md +31 -26
  55. package/wiki/references/base/components.md +24 -7
  56. package/wiki/references/base/controllers.md +50 -20
  57. package/wiki/references/base/datasources.md +30 -0
  58. package/wiki/references/base/dependency-injection.md +39 -3
  59. package/wiki/references/base/filter-system/application-usage.md +224 -0
  60. package/wiki/references/base/filter-system/array-operators.md +132 -0
  61. package/wiki/references/base/filter-system/comparison-operators.md +109 -0
  62. package/wiki/references/base/filter-system/default-filter.md +428 -0
  63. package/wiki/references/base/filter-system/fields-order-pagination.md +155 -0
  64. package/wiki/references/base/filter-system/index.md +127 -0
  65. package/wiki/references/base/filter-system/json-filtering.md +197 -0
  66. package/wiki/references/base/filter-system/list-operators.md +71 -0
  67. package/wiki/references/base/filter-system/logical-operators.md +156 -0
  68. package/wiki/references/base/filter-system/null-operators.md +58 -0
  69. package/wiki/references/base/filter-system/pattern-matching.md +108 -0
  70. package/wiki/references/base/filter-system/quick-reference.md +431 -0
  71. package/wiki/references/base/filter-system/range-operators.md +63 -0
  72. package/wiki/references/base/filter-system/tips.md +190 -0
  73. package/wiki/references/base/filter-system/use-cases.md +452 -0
  74. package/wiki/references/base/index.md +90 -0
  75. package/wiki/references/base/middlewares.md +604 -0
  76. package/wiki/references/base/models.md +215 -23
  77. package/wiki/references/base/providers.md +731 -0
  78. package/wiki/references/base/repositories/advanced.md +555 -0
  79. package/wiki/references/base/repositories/index.md +228 -0
  80. package/wiki/references/base/repositories/mixins.md +331 -0
  81. package/wiki/references/base/repositories/relations.md +486 -0
  82. package/wiki/references/base/repositories.md +40 -635
  83. package/wiki/references/base/services.md +28 -4
  84. package/wiki/references/components/authentication.md +22 -2
  85. package/wiki/references/components/health-check.md +12 -0
  86. package/wiki/references/components/index.md +23 -0
  87. package/wiki/references/components/mail.md +687 -0
  88. package/wiki/references/components/request-tracker.md +16 -0
  89. package/wiki/references/components/socket-io.md +18 -0
  90. package/wiki/references/components/static-asset.md +14 -26
  91. package/wiki/references/components/swagger.md +17 -0
  92. package/wiki/references/configuration/environment-variables.md +427 -0
  93. package/wiki/references/configuration/index.md +73 -0
  94. package/wiki/references/helpers/cron.md +14 -0
  95. package/wiki/references/helpers/crypto.md +15 -0
  96. package/wiki/references/helpers/env.md +16 -0
  97. package/wiki/references/helpers/error.md +17 -0
  98. package/wiki/references/helpers/index.md +14 -0
  99. package/wiki/references/helpers/inversion.md +24 -4
  100. package/wiki/references/helpers/logger.md +19 -0
  101. package/wiki/references/helpers/network.md +11 -0
  102. package/wiki/references/helpers/queue.md +19 -0
  103. package/wiki/references/helpers/redis.md +21 -0
  104. package/wiki/references/helpers/socket-io.md +24 -5
  105. package/wiki/references/helpers/storage.md +18 -10
  106. package/wiki/references/helpers/testing.md +18 -0
  107. package/wiki/references/helpers/types.md +16 -0
  108. package/wiki/references/helpers/uid.md +167 -0
  109. package/wiki/references/helpers/worker-thread.md +16 -0
  110. package/wiki/references/index.md +177 -0
  111. package/wiki/references/quick-reference.md +634 -0
  112. package/wiki/references/src-details/boot.md +3 -3
  113. package/wiki/references/src-details/dev-configs.md +0 -4
  114. package/wiki/references/src-details/docs.md +2 -2
  115. package/wiki/references/src-details/index.md +86 -0
  116. package/wiki/references/src-details/inversion.md +1 -6
  117. package/wiki/references/src-details/mcp-server.md +3 -15
  118. package/wiki/references/utilities/index.md +86 -10
  119. package/wiki/references/utilities/jsx.md +577 -0
  120. package/wiki/references/utilities/request.md +0 -2
  121. package/wiki/references/utilities/statuses.md +740 -0
  122. package/wiki/get-started/best-practices/api-usage-examples.md +0 -266
  123. package/wiki/get-started/best-practices/architectural-patterns.md +0 -170
  124. package/wiki/get-started/best-practices/data-modeling.md +0 -177
  125. package/wiki/get-started/best-practices/deployment-strategies.md +0 -121
  126. package/wiki/get-started/best-practices/performance-optimization.md +0 -97
  127. package/wiki/get-started/best-practices/security-guidelines.md +0 -99
  128. package/wiki/get-started/core-concepts/persistent.md +0 -539
  129. package/wiki/get-started/index.md +0 -65
  130. package/wiki/get-started/philosophy.md +0 -296
  131. 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](../../references/src-details/dev-configs.md) for full details.
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 ROUTE_CONFIGS = {
288
- '/users': { method: 'GET', path: '/users' },
289
- '/users/:id': { method: 'GET', path: '/users/:id' },
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 ROUTE_CONFIGS; // '/users' | '/users/:id'
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 ROUTE_CONFIGS = {
371
- [UserRestPaths.ROOT]: {
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
- [UserRestPaths.BY_ID]: {
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().uuid() }),
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: ROUTE_CONFIGS[UserRestPaths.ROOT] })
399
- list(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.ROOT]>) {
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: ROUTE_CONFIGS[UserRestPaths.BY_ID] })
404
- getById(context: TRouteContext<typeof ROUTE_CONFIGS[typeof UserRestPaths.BY_ID]>) {
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: ROUTE_CONFIGS['/'] }).to({
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: ROUTE_CONFIGS['/ping'],
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 `ROUTE_CONFIGS` 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`).
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 ROUTE_CONFIGS = {
135
- // ... your route definitions
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 ROUTE_CONFIGS['/path']>` has the precise types for request body, params, and response.
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 (after dependencies)
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:**