@venizia/ignis-docs 0.2.0 → 0.2.1-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.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -2,6 +2,28 @@
2
2
 
3
3
  Use JSDoc comments for public APIs to improve IDE support and generate documentation.
4
4
 
5
+ ## Comment Style
6
+
7
+ A comment states **only the constraint the code cannot show**. Keep it to 1-3 lines.
8
+
9
+ Never write:
10
+
11
+ - History or dates ("changed in June", "was previously async")
12
+ - Decision narration ("we tried X, then chose Y")
13
+ - A restatement of what the next line already says
14
+
15
+ ```typescript
16
+ // ✅ GOOD - states a constraint the code cannot show
17
+ // winston is an optional peer; a compiled binary must register a provider explicitly,
18
+ // because only a class reference carries one into the bundle.
19
+ LoggerFactory.use({ provider: PinoLogger });
20
+
21
+ // ❌ BAD - restates the code, narrates history
22
+ // 2026-07-18: after discussing with the team we decided to switch the logger
23
+ // provider. This line calls use() on LoggerFactory and passes PinoLogger.
24
+ LoggerFactory.use({ provider: PinoLogger });
25
+ ```
26
+
5
27
  ## When to Use JSDoc
6
28
 
7
29
  | Context | Required? | Reason |
@@ -56,10 +78,7 @@ async find(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<T
56
78
 
57
79
  ```typescript
58
80
  /**
59
- * Creates a new user account with the given data.
60
- *
61
- * Validates that the email is unique, hashes the password,
62
- * and sends a welcome email upon successful creation.
81
+ * Creates a new user account. Email must be unique; the password is hashed here, never by the caller.
63
82
  *
64
83
  * @param data - User creation data
65
84
  * @returns The created user without sensitive fields
@@ -185,14 +204,14 @@ interface IJWTStrategyOptions {
185
204
  *
186
205
  * @example
187
206
  * @repository({ model: User, dataSource: PostgresDataSource })
188
- * export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
207
+ * export class UserRepository extends DefaultRelationalRepository<typeof User.schema> {
189
208
  * // Custom methods here
190
209
  * }
191
210
  *
192
- * @see {@link BasePostgresEntity} for model definition
193
- * @see {@link BasePostgresDataSource} for database connection
211
+ * @see {@link BaseRelationalEntity} for model definition
212
+ * @see {@link BaseRelationalDataSource} for database connection
194
213
  */
195
- class DefaultCRUDRepository<EntitySchema extends TTableSchemaWithId = TTableSchemaWithId> {
214
+ class DefaultRelationalRepository<EntitySchema extends TTableSchemaWithId = TTableSchemaWithId> {
196
215
  // ...
197
216
  }
198
217
  ```
@@ -213,6 +232,7 @@ class DefaultCRUDRepository<EntitySchema extends TTableSchemaWithId = TTableSche
213
232
  - Don't copy TypeScript types into JSDoc (they're already visible)
214
233
  - Don't write multi-paragraph descriptions for simple functions
215
234
  - Don't use JSDoc for private implementation details
235
+ - Don't record history, dates, or the reasoning behind a past change
216
236
 
217
237
  ## See Also
218
238
 
@@ -15,11 +15,12 @@ Avoid `export default` except for configuration files (e.g., `eslint.config.mjs`
15
15
  ```typescript
16
16
  // ✅ GOOD
17
17
  export class UserController { }
18
- export function createUser() { }
18
+ export const createUser = () => { };
19
19
  export const DEFAULT_OPTIONS = { };
20
20
 
21
21
  // ❌ BAD
22
22
  export default class UserController { }
23
+ export function createUser() { } // arrow functions only, never `function`
23
24
  ```
24
25
 
25
26
  ## The Options Object Pattern
@@ -127,7 +128,8 @@ async syncData() {
127
128
  import { executeWithPerformanceMeasure } from '@venizia/ignis-helpers';
128
129
 
129
130
  await executeWithPerformanceMeasure({
130
- logger: this.logger.for('syncData'),
131
+ logger: this.logger,
132
+ level: 'info', // default: 'debug'
131
133
  scope: 'DataSync',
132
134
  description: 'Sync user records',
133
135
  task: async () => {
@@ -139,12 +141,18 @@ await executeWithPerformanceMeasure({
139
141
 
140
142
  **Method-scoped logging pattern:**
141
143
 
144
+ Any class extending `BaseHelper` (services, controllers, repositories, helpers) already has
145
+ `this.logger`. Acquire one standalone with `ApplicationLogger.get(...)` and always annotate
146
+ `ILogger` - never a concrete provider class.
147
+
142
148
  ```typescript
149
+ import { ApplicationLogger, ILogger } from '@venizia/ignis-helpers';
150
+
143
151
  class UserService {
144
- private logger = Logger.get('UserService');
152
+ private logger: ILogger = ApplicationLogger.get('UserService');
145
153
 
146
154
  async createUser(data: TCreateUserRequest) {
147
- // Use .for() to add method context to all logs
155
+ // .for() returns a method-scoped child logger
148
156
  this.logger.for('createUser').info('Creating user: %j', data);
149
157
  // Output: [UserService-createUser] Creating user: {...}
150
158
 
@@ -153,6 +161,7 @@ class UserService {
153
161
  this.logger.for('createUser').info('User created: %s', user.id);
154
162
  return user;
155
163
  } catch (error) {
164
+ // %s, not %j - `message` and `stack` are non-enumerable, so %j drops them
156
165
  this.logger.for('createUser').error('Failed: %s', error);
157
166
  throw error;
158
167
  }
@@ -160,6 +169,8 @@ class UserService {
160
169
  }
161
170
  ```
162
171
 
172
+ Levels are exactly five, each a direct method: `debug`, `info`, `warn`, `error`, `emerg`.
173
+
163
174
  ## See Also
164
175
 
165
176
  - [Naming Conventions](./naming-conventions) - Class and file naming
@@ -17,13 +17,18 @@ Maintain consistent code style using **Prettier** (formatting) and **ESLint** (c
17
17
  | Error format | `[ClassName][method] Message` |
18
18
  | Logging format | `[method] Message \| Key: %s` |
19
19
  | Default options | `DEFAULT_OPTIONS` constant |
20
- | Type safety | No `any` or `unknown` allowed |
20
+ | Type safety | Avoid `any` / `unknown`; derive types, never duplicate them |
21
+ | Abbreviations | None - `ProductRepository`, `TDocument` |
21
22
  | Scope naming | `ClassName.name` |
22
23
  | Arguments | Options object (`opts`) |
24
+ | Functions | Arrow functions, never `function` declarations |
23
25
  | Exports | Named exports only |
24
26
  | Return types | Explicitly defined |
25
27
  | Control flow | Always use braces (`{}`) |
26
28
  | Switch statements | Braces + default case required |
29
+ | Catch blocks | Never silent - always log |
30
+ | Errors | `getError` / `ApplicationError`, never `new Error` |
31
+ | Logger | Type as `ILogger`; `this.logger` or `ApplicationLogger.get(...)` |
27
32
  | Imports | Node → Third-party → Internal → Relative |
28
33
  | Function naming | `generate*`, `build*`, `to*`, `is*`, `extract*` |
29
34
 
@@ -55,7 +60,7 @@ type TUserRequest = { };
55
60
  // Classes use PascalCase with suffix
56
61
  class UserController extends BaseRestController { }
57
62
  class UserService extends BaseService { }
58
- class UserRepository extends DefaultCRUDRepository { }
63
+ class UserRepository extends DefaultRelationalRepository { }
59
64
  ```
60
65
 
61
66
  ### File Structure
@@ -2,6 +2,24 @@
2
2
 
3
3
  Consistent naming improves code readability and maintainability.
4
4
 
5
+ ## Never Abbreviate an Identifier
6
+
7
+ Hard rule. Write the whole word - in class names, variables, and type parameters alike.
8
+
9
+ ```typescript
10
+ // ✅ GOOD
11
+ class ProductRepository { }
12
+ type TProductDocument = { };
13
+ const configuration = resolveConfiguration();
14
+ class Repository<TDocument> { }
15
+
16
+ // ❌ BAD
17
+ class ProductRepo { }
18
+ type TProductDoc = { };
19
+ const cfg = resolveCfg();
20
+ class Repository<TDoc> { }
21
+ ```
22
+
5
23
  ## Directory Structure
6
24
 
7
25
  ### Component Organization
@@ -80,6 +98,8 @@ Both styles are acceptable: `[type].ts` or `[name].[type].ts`
80
98
  - Use `[type].ts` when there's only one file of that type in the folder
81
99
  - Use `[name].[type].ts` when there are multiple files of the same type
82
100
  - Use kebab-case for multi-word names: `jwt-token.service.ts`
101
+ - **A file name never repeats its folder.** Name the file for its role inside the folder:
102
+ `logger/winston/logger.ts`, not `logger/winston/winston-logger.ts`
83
103
 
84
104
  ## Type and Interface Prefixes
85
105
 
@@ -90,13 +110,17 @@ interface IHealthCheckOptions {
90
110
  }
91
111
 
92
112
  interface IAuthService {
93
- signIn(context: Context, opts: TSignInRequest): Promise<AnyObject>;
113
+ signIn(context: TContext, opts: TSignInRequest): Promise<AnyObject>;
94
114
  }
95
115
 
96
116
  // Type aliases use 'T' prefix
97
117
  type TSignInRequest = z.infer<typeof SignInRequestSchema>;
98
118
  type TAuthStrategy = TConstValue<typeof AuthenticateStrategy>;
99
119
 
120
+ // ❌ Never a bare string-literal union for an enumerable set - it has no runtime
121
+ // values to validate against. Use a const class + TConstValue instead.
122
+ type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'emerg';
123
+
100
124
  // Generic constraints
101
125
  type TTableSchemaWithId<TC extends TableConfig = TableConfig> = PgTable<TC> & {
102
126
  id: TIdColumn;
@@ -123,7 +147,7 @@ export class SocketIOBindingKeys {
123
147
  Use underscore prefix (`_`) for private and protected class fields to distinguish them from public fields and method parameters.
124
148
 
125
149
  ```typescript
126
- class MyRepository extends DefaultCRUDRepository {
150
+ class MyRepository extends DefaultRelationalRepository {
127
151
  // Private fields with underscore prefix
128
152
  private _dataSource: AbstractDataSource;
129
153
  private _entity: AbstractEntity;
@@ -61,6 +61,9 @@ export class UserController extends BaseRestController {
61
61
  }
62
62
  ```
63
63
 
64
+ `@get`, `@post`, `@put`, `@patch`, and `@del` are the same decorator with `method` preset - the
65
+ config they take omits `method`.
66
+
64
67
  ## Method 3: Using `bindRoute` (Programmatic)
65
68
 
66
69
  Register routes in the `binding()` lifecycle method:
@@ -115,18 +118,19 @@ Use Zod with `.openapi()` for automatic documentation:
115
118
 
116
119
  ```typescript
117
120
  const CreateUserSchema = z.object({
118
- email: z.string().email(),
121
+ email: z.email(),
119
122
  name: z.string().min(1).max(100),
120
123
  }).openapi({
121
124
  description: 'Create user request body',
122
125
  example: { email: 'user@example.com', name: 'John Doe' },
123
126
  });
124
127
 
128
+ // Zod v4 top-level string formats - not the deprecated z.string().email() chain
125
129
  const UserSchema = z.object({
126
- id: z.string().uuid(),
127
- email: z.string().email(),
130
+ id: z.uuid(),
131
+ email: z.email(),
128
132
  name: z.string(),
129
- createdAt: z.string().datetime(),
133
+ createdAt: z.iso.datetime(),
130
134
  }).openapi({
131
135
  description: 'User response',
132
136
  });
@@ -165,4 +169,4 @@ export const RouteConfigs = {
165
169
 
166
170
  - [API Usage Examples](../api-usage-examples) - Full API patterns
167
171
  - [Controllers Reference](../../references/base/controllers) - Controller API
168
- - [Swagger Component](../../extensions/components/api-reference) - OpenAPI setup
172
+ - [API Reference Component](../../extensions/components/api-reference) - OpenAPI setup
@@ -2,6 +2,10 @@
2
2
 
3
3
  IGNIS provides centralized development configurations via the `@venizia/dev-configs` package.
4
4
 
5
+ > [!IMPORTANT]
6
+ > **Bun is the only package manager** - never npm, yarn, or pnpm. **Compile with `tsc` directly** -
7
+ > never `npx`, `bunx`, or `bun x`.
8
+
5
9
  ## Installation
6
10
 
7
11
  ```bash
@@ -9,7 +13,7 @@ bun add -d @venizia/dev-configs
9
13
  ```
10
14
 
11
15
  This package provides:
12
- - **ESLint rules** - Pre-configured for Node.js/TypeScript projects
16
+ - **ESLint rules** - flat config, pre-configured for Node.js/TypeScript projects
13
17
  - **Prettier settings** - Consistent formatting across all IGNIS projects
14
18
  - **TypeScript configs** - Shared base and common configurations
15
19
 
@@ -121,6 +125,15 @@ Use the centralized TypeScript configs:
121
125
  | `useDefineForClassFields` | `false` | Decorator-compatible class fields |
122
126
  | `strict` | `true` | Strict type checking |
123
127
  | `skipLibCheck` | `true` | Faster compilation |
128
+ | `noEmitOnError` | `true` | A type error emits nothing - the build fails loudly |
129
+
130
+ Every package in the monorepo pins **TypeScript `^6.0.3`**; `@venizia/dev-configs` accepts
131
+ `^5.0.0 || ^6.0.0`. Do not upgrade past 6 - `typescript-eslint` breaks.
132
+
133
+ > [!WARNING]
134
+ > Bun does not resolve `experimentalDecorators` / `emitDecoratorMetadata` through `extends`. In an
135
+ > **application** `tsconfig.json` that Bun executes, declare both flags directly in
136
+ > `compilerOptions`, or `@inject` parameter decorators are dropped silently and DI fails at boot.
124
137
 
125
138
  ## IDE Integration
126
139
 
@@ -26,6 +26,39 @@ const data: TUserResponse = await fetchData();
26
26
  const result: TProcessResult = processData();
27
27
  ```
28
28
 
29
+ ### When a Cast Is Unavoidable
30
+
31
+ Some boundaries genuinely cannot be typed. Reach for the **simplest** escape hatch - `as any` or the
32
+ `AnyType` alias - never a chained one.
33
+
34
+ ```typescript
35
+ // ✅ GOOD
36
+ const handler = raw as any;
37
+ const value = raw as AnyType;
38
+
39
+ // ❌ BAD - baroque, hides what is actually being asserted
40
+ const handler = raw as unknown as TRouteHandler;
41
+ ```
42
+
43
+ ## Derive Types, Never Duplicate Them
44
+
45
+ Prefer a compile-time type derived from the definition over a hand-maintained copy. A duplicate
46
+ drifts silently; a derived type breaks the build the moment the definition changes.
47
+
48
+ ```typescript
49
+ // ✅ GOOD - derived from the single source of truth
50
+ type TUser = typeof User.schema.$inferSelect;
51
+ type TNewUser = typeof User.schema.$inferInsert;
52
+ type TSignInRequest = z.infer<typeof SignInRequestSchema>;
53
+
54
+ // ❌ BAD - a parallel definition that will drift
55
+ type TUser = {
56
+ id: string;
57
+ email: string;
58
+ createdAt: Date;
59
+ };
60
+ ```
61
+
29
62
  ## Explicit Return Types
30
63
 
31
64
  Always define explicit return types for **public methods** and **API handlers**.
@@ -53,7 +86,7 @@ public async findUser(id: string) {
53
86
  ```typescript
54
87
  // Define schema
55
88
  export const SignInRequestSchema = z.object({
56
- email: z.string().email(),
89
+ email: z.email(),
57
90
  password: z.string().min(8),
58
91
  });
59
92
 
@@ -76,7 +109,7 @@ type RouteKey = keyof typeof RouteConfigs; // 'GET_USERS' | 'GET_USER_BY_ID'
76
109
  ### Generic Type Constraints
77
110
 
78
111
  ```typescript
79
- export class DefaultCRUDRepository<
112
+ export class DefaultRelationalRepository<
80
113
  EntitySchema extends TTableSchemaWithId = TTableSchemaWithId
81
114
  > {
82
115
  // EntitySchema is constrained to have an 'id' column
@@ -124,14 +157,14 @@ const result2 = await userRepository.create({ data: user });
124
157
 
125
158
  ```typescript
126
159
  // Type guard function
127
- function isUser(obj: unknown): obj is TUser {
160
+ const isUser = (obj: unknown): obj is TUser => {
128
161
  return (
129
162
  typeof obj === 'object' &&
130
163
  obj !== null &&
131
164
  'id' in obj &&
132
165
  'email' in obj
133
166
  );
134
- }
167
+ };
135
168
 
136
169
  // Usage
137
170
  const data = await fetchData();
@@ -148,7 +181,7 @@ type TResult<T> =
148
181
  | { success: true; data: T }
149
182
  | { success: false; error: string };
150
183
 
151
- function processResult<T>(result: TResult<T>) {
184
+ const processResult = <T>(result: TResult<T>) => {
152
185
  if (result.success) {
153
186
  // TypeScript knows result.data exists
154
187
  return result.data;
@@ -156,7 +189,7 @@ function processResult<T>(result: TResult<T>) {
156
189
 
157
190
  // TypeScript knows result.error exists
158
191
  throw getError({ message: result.error });
159
- }
192
+ };
160
193
  ```
161
194
 
162
195
  ## See Also
@@ -26,7 +26,6 @@ export class Application extends BaseApplication {
26
26
 
27
27
  // Services
28
28
  this.service(MyNewService); // <-- Don't forget this line
29
- this.registerAuth();
30
29
 
31
30
  // Controllers
32
31
  this.controller(TestController);
@@ -174,31 +173,44 @@ await userRepository.updateBy({
174
173
 
175
174
  ## 7. Schema Key Mismatch
176
175
 
177
- **Problem:** Entity name doesn't match the table name registered in the DataSource's schema.
176
+ **Problem:** The repository cannot find its table in the DataSource schema.
178
177
 
179
178
  **Error Message:**
180
179
  ```
181
- [UserRepository] Schema key mismatch | Entity name 'User' not found in connector.query | Available keys: [Configuration, Post] | Ensure the model's TABLE_NAME matches the schema registration key
180
+ [UserRepository] Schema key mismatch | Entity name 'User' not found in connector.query | Available keys: [users, Post] | Ensure the model's TABLE_NAME matches the schema registration key
182
181
  ```
183
182
 
184
- **Solution:** The schema registration key follows the precedence `@model tableName metadata > static TABLE_NAME > class name`, while the repository looks the query interface up by the entity **class name**. Do not set `TABLE_NAME` (or `tableName` metadata) to a value that differs from the class name:
183
+ **Cause:** Two keys are resolved by *different* rules:
184
+
185
+ | Key | Rule |
186
+ |-----|------|
187
+ | Schema registration key | `@model({ tableName })` > `static TABLE_NAME` > class name |
188
+ | Repository lookup key (`entity.name`) | `static TABLE_NAME` > class name |
189
+
190
+ `@model({ tableName })` moves the registration key but not the lookup key. Setting it alone is what breaks:
185
191
 
186
192
  ```typescript
187
- // ❌ BAD - Schema is registered under 'users', but looked up as 'User'
188
- @model({ type: 'entity' })
189
- export class User extends BasePostgresEntity<typeof User.schema> {
190
- static override TABLE_NAME = 'users'; // Differs from class name!
191
- static override schema = pgTable('User', { /* ... */ });
193
+ // ❌ BAD - registered under 'users', looked up as 'User'
194
+ @model({ type: 'entity', tableName: 'users' })
195
+ export class User extends BaseRelationalEntity<typeof User.schema> {
196
+ static override schema = pgTable('users', { /* ... */ });
192
197
  }
193
198
 
194
- // ✅ GOOD - No TABLE_NAME override; registration key defaults to the class name
199
+ // ✅ GOOD - set neither; both keys resolve to the class name
195
200
  @model({ type: 'entity' })
196
- export class User extends BasePostgresEntity<typeof User.schema> {
197
- static override schema = pgTable('User', { /* ... */ });
201
+ export class User extends BaseRelationalEntity<typeof User.schema> {
202
+ static override schema = pgTable('users', { /* ... */ });
203
+ }
204
+
205
+ // ✅ GOOD - or set both to the same value
206
+ @model({ type: 'entity', tableName: 'users' })
207
+ export class User extends BaseRelationalEntity<typeof User.schema> {
208
+ static override TABLE_NAME = 'users';
209
+ static override schema = pgTable('users', { /* ... */ });
198
210
  }
199
211
  ```
200
212
 
201
- **Why this matters:** The framework uses `entity.name` (class name) to look up the query interface in `connector.query`, but registers the schema under the resolved table name. If they don't match, the repository can't find its table. The first argument of `pgTable()` is the physical SQL table name and is independent of this lookup - by convention it matches the class name.
213
+ `static TABLE_NAME` on its own is safe - it moves both keys together. The first argument of `pgTable()` is the physical SQL table name and plays no part in either lookup.
202
214
 
203
215
  ## 8. Validation Error Response Structure
204
216
 
@@ -209,8 +221,12 @@ export class User extends BasePostgresEntity<typeof User.schema> {
209
221
  ```json
210
222
  {
211
223
  "statusCode": 422,
212
- "message": "Invalid email",
213
- "messageCode": "invalid_string",
224
+ "message": "Invalid email address",
225
+ "normalized": {
226
+ "text": "Invalid email address",
227
+ "code": "invalid_format",
228
+ "args": {}
229
+ },
214
230
  "requestId": "abc123",
215
231
  "details": {
216
232
  "url": "http://localhost:3000/users",
@@ -218,23 +234,30 @@ export class User extends BasePostgresEntity<typeof User.schema> {
218
234
  "cause": [
219
235
  {
220
236
  "path": "email",
221
- "message": "Invalid email",
222
- "code": "invalid_string",
223
- "expected": "email",
224
- "received": "string"
237
+ "message": "Invalid email address",
238
+ "code": "invalid_format"
225
239
  },
226
240
  {
227
241
  "path": "age",
228
- "message": "Expected number, received string",
242
+ "message": "Invalid input: expected number, received string",
229
243
  "code": "invalid_type",
230
- "expected": "number",
231
- "received": "string"
244
+ "expected": "number"
232
245
  }
233
246
  ]
234
247
  }
235
248
  }
236
249
  ```
237
250
 
251
+ - `normalized.code` comes from the first issue carrying a `params.code`, otherwise the first issue's raw Zod code. Attach a stable code clients can branch on with `.refine()`:
252
+ ```typescript
253
+ z.number().refine(isTwoDecimals, {
254
+ message: 'At most 2 decimal places',
255
+ params: { code: 'app.price.too_many_places' },
256
+ });
257
+ ```
258
+ - `args` is always `{}` here - per-field detail lives in `details.cause`.
259
+ - `details.stack` is omitted in production; `details.cause` is not.
260
+
238
261
  **Client-side handling:**
239
262
  ```typescript
240
263
  try {
@@ -253,32 +276,31 @@ try {
253
276
 
254
277
  **Problem:** Application fails to start with `Cannot access 'X' before initialization` or similar errors.
255
278
 
256
- **Cause:** Two or more modules import each other directly, creating a circular reference that JavaScript cannot resolve.
279
+ **Cause:** Two models reference each other's schema. With a cycle, one module is still mid-evaluation when the other reads its exports, so `User.schema` is `undefined`.
257
280
 
258
- **Solution:** Use lazy imports or restructure your modules:
281
+ **Solution:** `relations` accepts a resolver function. Use it - the framework calls it in `buildSchema()`, after every model has finished loading:
259
282
 
260
283
  ```typescript
261
- // ❌ BAD - Direct import causes circular dependency
262
284
  import { User } from './user.model';
263
285
 
286
+ // ❌ BAD - array literal is evaluated at module load, while the cycle is still open
264
287
  @model({ type: 'entity' })
265
- export class Order extends BasePostgresEntity<typeof Order.schema> {
266
- static override relations = (): TRelationConfig[] => [
267
- { schema: User.schema, ... }, // User imports Order, Order imports User
288
+ export class Order extends BaseRelationalEntity<typeof Order.schema> {
289
+ static override relations: TRelationConfig[] = [
290
+ { name: 'owner', type: RelationTypes.ONE, schema: User.schema, metadata: { ... } },
268
291
  ];
269
292
  }
270
293
 
271
- // ✅ GOOD - Lazy import breaks the cycle
294
+ // ✅ GOOD - resolver defers the User.schema read until after both modules finish loading
272
295
  @model({ type: 'entity' })
273
- export class Order extends BasePostgresEntity<typeof Order.schema> {
274
- static override relations = (): TRelationConfig[] => {
275
- const { User } = require('./user.model'); // Lazy require
276
- return [{ schema: User.schema, ... }];
277
- };
296
+ export class Order extends BaseRelationalEntity<typeof Order.schema> {
297
+ static override relations = (): TRelationConfig[] => [
298
+ { name: 'owner', type: RelationTypes.ONE, schema: User.schema, metadata: { ... } },
299
+ ];
278
300
  }
279
301
  ```
280
302
 
281
- **Alternative:** Restructure to have a shared module that both import from.
303
+ **Alternative:** Restructure so both models import their shared pieces from a third module.
282
304
 
283
305
  ## 10. Transaction Not Rolling Back
284
306
 
@@ -306,6 +328,8 @@ try {
306
328
  }
307
329
  ```
308
330
 
331
+ This exact shape is safe even when `commit()` is what failed: a rollback on an already-torn-down transaction is a no-op, so it never replaces your original error. A failed `COMMIT` always throws - never treat a resolved `commit()` as anything but success.
332
+
309
333
  ## 11. Fire-and-Forget Promises Losing Context
310
334
 
311
335
  **Problem:** `getCurrentUserId()` or other context-dependent functions return `null` in background tasks.
@@ -77,6 +77,7 @@ The project uses a Makefile for common development tasks:
77
77
  | `make build` | Rebuild all packages in correct order |
78
78
  | `make clean` | Clean build artifacts from all packages |
79
79
  | `make lint` | Lint all packages |
80
+ | `make lint-all` | Lint packages **and** examples - this is the bar a PR must clear |
80
81
  | `make help` | Show all available commands |
81
82
 
82
83
  **Individual package builds** (dependencies are automatically resolved):
@@ -153,8 +154,8 @@ git commit -m "chore: upgrade Hono to v4.0"
153
154
  ### Step 4: Validate
154
155
 
155
156
  ```bash
156
- # Lint and format (from root)
157
- make lint
157
+ # Lint and format (from root) - zero warnings and zero errors is the bar
158
+ make lint-all
158
159
  # Or run `bun run lint:fix` inside a package directory
159
160
 
160
161
  # Build all packages (from root)
@@ -164,6 +165,9 @@ make build
164
165
  cd packages/core && bun test
165
166
  ```
166
167
 
168
+ > [!WARNING]
169
+ > `make <package>` **cleans `dist/` before it builds**, and the build type-checks `__tests__` too. One broken test therefore aborts the build after `dist/` is already gone, leaving an **empty `dist/`** and a cascade of unrelated-looking import failures in `bun test`. If imports suddenly break everywhere, check `dist/` before chasing the imports.
170
+
167
171
  ## 3. Submit Pull Request
168
172
 
169
173
  ```bash