@venizia/ignis-docs 0.0.8 → 0.2.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 (180) hide show
  1. package/README.md +7 -7
  2. package/content/best-practices/api-usage-examples.md +15 -12
  3. package/content/best-practices/architectural-patterns.md +70 -78
  4. package/content/best-practices/architecture-decisions.md +91 -60
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +56 -44
  6. package/content/best-practices/code-style-standards/constants-configuration.md +11 -11
  7. package/content/best-practices/code-style-standards/control-flow.md +5 -2
  8. package/content/best-practices/code-style-standards/documentation.md +13 -13
  9. package/content/best-practices/code-style-standards/function-patterns.md +9 -10
  10. package/content/best-practices/code-style-standards/index.md +1 -1
  11. package/content/best-practices/code-style-standards/naming-conventions.md +10 -8
  12. package/content/best-practices/code-style-standards/route-definitions.md +30 -12
  13. package/content/best-practices/code-style-standards/tooling.md +8 -5
  14. package/content/best-practices/code-style-standards/type-safety.md +13 -12
  15. package/content/best-practices/common-pitfalls.md +56 -37
  16. package/content/best-practices/contribution-workflow.md +13 -14
  17. package/content/best-practices/data-modeling.md +46 -22
  18. package/content/best-practices/deployment-strategies.md +28 -27
  19. package/content/best-practices/error-handling.md +48 -24
  20. package/content/best-practices/index.md +5 -5
  21. package/content/best-practices/performance-optimization.md +40 -31
  22. package/content/best-practices/security-guidelines.md +52 -23
  23. package/content/best-practices/testing-strategies.md +65 -51
  24. package/content/best-practices/troubleshooting-tips.md +24 -24
  25. package/content/extensions/components/{swagger.md → api-reference.md} +40 -31
  26. package/content/extensions/components/authentication/api.md +19 -19
  27. package/content/extensions/components/authentication/errors.md +7 -7
  28. package/content/extensions/components/authentication/index.md +10 -8
  29. package/content/extensions/components/authentication/usage.md +101 -6
  30. package/content/extensions/components/authorization/api.md +45 -25
  31. package/content/extensions/components/authorization/errors.md +6 -6
  32. package/content/extensions/components/authorization/index.md +11 -10
  33. package/content/extensions/components/authorization/usage.md +21 -21
  34. package/content/extensions/components/health-check.md +1 -1
  35. package/content/extensions/components/index.md +5 -5
  36. package/content/extensions/components/mail/errors.md +15 -15
  37. package/content/extensions/components/mail/index.md +1 -2
  38. package/content/extensions/components/mail/usage.md +1 -1
  39. package/content/extensions/components/request-tracker.md +1 -1
  40. package/content/extensions/components/socket-io/api.md +9 -9
  41. package/content/extensions/components/socket-io/errors.md +5 -5
  42. package/content/extensions/components/socket-io/index.md +8 -8
  43. package/content/extensions/components/socket-io/usage.md +1 -1
  44. package/content/extensions/components/static-asset/api.md +17 -4
  45. package/content/extensions/components/static-asset/errors.md +4 -4
  46. package/content/extensions/components/static-asset/index.md +26 -28
  47. package/content/extensions/components/static-asset/usage.md +13 -12
  48. package/content/extensions/components/template/index.md +2 -2
  49. package/content/extensions/components/template/setup-page.md +1 -1
  50. package/content/extensions/components/websocket/api.md +3 -3
  51. package/content/extensions/components/websocket/errors.md +5 -5
  52. package/content/extensions/components/websocket/index.md +5 -5
  53. package/content/extensions/components/websocket/usage.md +3 -3
  54. package/content/extensions/helpers/cron/index.md +2 -2
  55. package/content/extensions/helpers/crypto/index.md +1 -1
  56. package/content/extensions/helpers/env/index.md +27 -12
  57. package/content/extensions/helpers/error/index.md +81 -25
  58. package/content/extensions/helpers/index.md +2 -3
  59. package/content/extensions/helpers/inversion/index.md +15 -7
  60. package/content/extensions/helpers/kafka/compile-binary.md +92 -0
  61. package/content/extensions/helpers/kafka/examples.md +1 -1
  62. package/content/extensions/helpers/kafka/index.md +3 -0
  63. package/content/extensions/helpers/logger/index.md +32 -2
  64. package/content/extensions/helpers/network/index.md +6 -0
  65. package/content/extensions/helpers/queue/index.md +14 -17
  66. package/content/extensions/helpers/redis/index.md +548 -323
  67. package/content/extensions/helpers/socket-io/index.md +14 -10
  68. package/content/extensions/helpers/storage/api.md +44 -8
  69. package/content/extensions/helpers/storage/index.md +43 -7
  70. package/content/extensions/helpers/template/index.md +6 -3
  71. package/content/extensions/helpers/types/index.md +11 -8
  72. package/content/extensions/helpers/websocket/api.md +9 -9
  73. package/content/extensions/helpers/websocket/index.md +7 -7
  74. package/content/extensions/helpers/worker-thread/index.md +2 -2
  75. package/content/extensions/index.md +3 -4
  76. package/content/extensions/src-details/mcp-server.md +18 -24
  77. package/content/guides/core-concepts/application/bootstrapping.md +11 -14
  78. package/content/guides/core-concepts/application/index.md +3 -3
  79. package/content/guides/core-concepts/components.md +19 -10
  80. package/content/guides/core-concepts/dependency-injection.md +6 -3
  81. package/content/guides/core-concepts/grpc-controllers.md +6 -5
  82. package/content/guides/core-concepts/persistent/datasources.md +42 -43
  83. package/content/guides/core-concepts/persistent/index.md +16 -7
  84. package/content/guides/core-concepts/persistent/models.md +24 -20
  85. package/content/guides/core-concepts/persistent/postgres-drivers.md +201 -0
  86. package/content/guides/core-concepts/persistent/repositories.md +40 -23
  87. package/content/guides/core-concepts/persistent/search-meilisearch.md +185 -0
  88. package/content/guides/core-concepts/persistent/search-typesense.md +431 -0
  89. package/content/guides/core-concepts/persistent/transactions.md +61 -25
  90. package/content/guides/core-concepts/rest-controllers.md +12 -9
  91. package/content/guides/core-concepts/services.md +330 -60
  92. package/content/guides/get-started/5-minute-quickstart.md +15 -15
  93. package/content/guides/get-started/philosophy.md +36 -36
  94. package/content/guides/get-started/setup.md +3 -3
  95. package/content/guides/index.md +3 -3
  96. package/content/guides/migrations/redis-helpers-migration.md +177 -0
  97. package/content/guides/migrations/scoped-rbac-migration.md +17 -17
  98. package/content/guides/migrations/unified-connectors-migration.md +113 -0
  99. package/content/guides/reference/glossary.md +19 -12
  100. package/content/guides/reference/mcp-docs-server.md +22 -18
  101. package/content/guides/tutorials/building-a-crud-api.md +37 -44
  102. package/content/guides/tutorials/complete-installation.md +17 -17
  103. package/content/guides/tutorials/ecommerce-api.md +163 -124
  104. package/content/guides/tutorials/realtime-chat.md +181 -135
  105. package/content/guides/tutorials/testing.md +65 -523
  106. package/content/index.md +2 -180
  107. package/content/public/apple-touch-icon.png +0 -0
  108. package/content/public/og-image.png +0 -0
  109. package/content/public/site.webmanifest +11 -0
  110. package/content/references/base/application.md +4 -5
  111. package/content/references/base/bootstrapping.md +18 -5
  112. package/content/references/base/components.md +149 -120
  113. package/content/references/base/connectors.md +178 -0
  114. package/content/references/base/controllers.md +41 -30
  115. package/content/references/base/datasources.md +163 -92
  116. package/content/references/base/dependency-injection.md +34 -22
  117. package/content/references/base/filter-system/application-usage.md +17 -14
  118. package/content/references/base/filter-system/array-operators.md +7 -2
  119. package/content/references/base/filter-system/comparison-operators.md +3 -0
  120. package/content/references/base/filter-system/default-filter.md +89 -71
  121. package/content/references/base/filter-system/fields-order-pagination.md +22 -22
  122. package/content/references/base/filter-system/index.md +6 -3
  123. package/content/references/base/filter-system/json-filtering.md +20 -1
  124. package/content/references/base/filter-system/list-operators.md +1 -1
  125. package/content/references/base/filter-system/logical-operators.md +33 -1
  126. package/content/references/base/filter-system/null-operators.md +30 -1
  127. package/content/references/base/filter-system/quick-reference.md +23 -4
  128. package/content/references/base/filter-system/tips.md +5 -5
  129. package/content/references/base/filter-system/use-cases.md +12 -12
  130. package/content/references/base/grpc-controllers.md +13 -13
  131. package/content/references/base/index.md +24 -12
  132. package/content/references/base/middlewares.md +265 -327
  133. package/content/references/base/models.md +63 -49
  134. package/content/references/base/providers.md +136 -130
  135. package/content/references/base/repositories/advanced.md +59 -58
  136. package/content/references/base/repositories/index.md +115 -91
  137. package/content/references/base/repositories/mixins.md +55 -291
  138. package/content/references/base/repositories/relations.md +54 -64
  139. package/content/references/base/repositories/soft-deletable.md +31 -30
  140. package/content/references/base/services.md +296 -93
  141. package/content/references/configuration/environment-variables.md +49 -31
  142. package/content/references/configuration/index.md +6 -6
  143. package/content/references/index.md +17 -12
  144. package/content/references/quick-reference.md +65 -106
  145. package/content/references/utilities/crypto.md +65 -23
  146. package/content/references/utilities/index.md +3 -3
  147. package/content/references/utilities/jsx.md +6 -4
  148. package/content/references/utilities/module.md +68 -20
  149. package/content/references/utilities/parse.md +4 -14
  150. package/content/references/utilities/promise.md +9 -7
  151. package/content/references/utilities/schema.md +5 -3
  152. package/dist/mcp-server/common/guards.d.ts +8 -0
  153. package/dist/mcp-server/common/guards.d.ts.map +1 -0
  154. package/dist/mcp-server/common/guards.js +14 -0
  155. package/dist/mcp-server/common/guards.js.map +1 -0
  156. package/dist/mcp-server/common/index.d.ts +1 -0
  157. package/dist/mcp-server/common/index.d.ts.map +1 -1
  158. package/dist/mcp-server/common/index.js +1 -0
  159. package/dist/mcp-server/common/index.js.map +1 -1
  160. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  161. package/dist/mcp-server/helpers/docs.helper.js +4 -2
  162. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  163. package/dist/mcp-server/helpers/github.helper.js +1 -1
  164. package/dist/mcp-server/index.js +7 -2
  165. package/dist/mcp-server/index.js.map +1 -1
  166. package/dist/mcp-server/tools/base.tool.d.ts +6 -2
  167. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  168. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  169. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  170. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  171. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  172. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  173. package/dist/mcp-server/tools/github/search-code.tool.js +4 -1
  174. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  175. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  176. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +3 -1
  177. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  178. package/package.json +9 -9
  179. package/content/extensions/helpers/testing/index.md +0 -510
  180. package/content/references/base/middleware.md +0 -347
@@ -1,6 +1,6 @@
1
1
  # Architecture Decisions Guide
2
2
 
3
- This guide helps you make informed architectural decisions when building applications with Ignis. Learn when to use different patterns and how to scale your application.
3
+ This guide helps you make informed architectural decisions when building applications with IGNIS. Learn when to use different patterns and how to scale your application.
4
4
 
5
5
  ## Common Decision Points
6
6
 
@@ -24,14 +24,14 @@ This guide helps you make informed architectural decisions when building applica
24
24
  export class ItemController extends BaseRestController {
25
25
  constructor(
26
26
  @inject({ key: 'repositories.ItemRepository' })
27
- private itemRepo: ItemRepository,
27
+ private itemRepository: ItemRepository,
28
28
  ) {
29
29
  super({ scope: 'ItemController', path: '/items' });
30
30
  }
31
31
 
32
- @get({ configs: { path: '/:id' } })
32
+ @get({ configs: RouteConfigs.GET_ITEM_BY_ID })
33
33
  async getItem(c: Context) {
34
- const item = await this.itemRepo.findById(c.req.param('id'));
34
+ const item = await this.itemRepository.findById({ id: c.req.param('id') });
35
35
  return c.json(item);
36
36
  }
37
37
  }
@@ -56,7 +56,7 @@ export class OrderController extends BaseRestController {
56
56
  super({ scope: 'OrderController', path: '/orders' });
57
57
  }
58
58
 
59
- @post({ configs: { path: '/' } })
59
+ @post({ configs: RouteConfigs.CREATE_ORDER })
60
60
  async createOrder(c: Context) {
61
61
  const data = await c.req.json();
62
62
  // Service handles: validation, inventory check, payment, notifications
@@ -96,22 +96,28 @@ export class OrderController extends BaseRestController {
96
96
 
97
97
  ```typescript
98
98
  // Component: Self-contained, configurable, reusable
99
- @component({ scope: 'NotificationComponent' })
100
99
  export class NotificationComponent extends BaseComponent {
101
- private emailService: EmailService;
102
- private smsService: SMSService;
103
- private pushService: PushService;
104
-
105
- override configure() {
106
- // Setup services based on configuration
107
- this.emailService = new EmailService(this.config.email);
108
- if (this.config.sms?.enabled) {
109
- this.smsService = new SMSService(this.config.sms);
110
- }
100
+ constructor(
101
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
102
+ ) {
103
+ super({
104
+ scope: NotificationComponent.name,
105
+ initDefault: { enable: true, container: application },
106
+ bindings: {
107
+ // Default configuration - applications can rebind to override
108
+ 'notification.options': Binding.bind({ key: 'notification.options' }).toValue({
109
+ email: { enabled: true },
110
+ sms: { enabled: false },
111
+ }),
112
+ },
113
+ });
111
114
  }
112
115
 
113
- async notify(opts: NotifyOptions) {
114
- // Unified notification API
116
+ // Called when the application configures components (registerComponents)
117
+ override binding(): void {
118
+ const options = this.application.get({ key: 'notification.options' });
119
+ // Register notification services based on the resolved options
120
+ this.application.service(EmailNotificationService);
115
121
  }
116
122
  }
117
123
  ```
@@ -126,7 +132,7 @@ export class NotificationComponent extends BaseComponent {
126
132
  // Inline: Simple, one-off, no need for abstraction
127
133
  @controller({ path: '/health' })
128
134
  export class HealthController extends BaseRestController {
129
- @get({ configs: { path: '/' } })
135
+ @get({ configs: RouteConfigs.HEALTH_CHECK })
130
136
  healthCheck(c: Context) {
131
137
  return c.json({ status: 'ok', timestamp: new Date() });
132
138
  }
@@ -146,17 +152,19 @@ export class HealthController extends BaseRestController {
146
152
 
147
153
  ### Start with Standard CRUD
148
154
 
149
- Every repository gets these methods from `BaseRepository`:
155
+ Every repository gets these methods from `DefaultCRUDRepository`:
150
156
 
151
157
  ```typescript
152
- // Inherited methods - use these first
153
- find(filter) // List with filters
154
- findById(id) // Get by ID
155
- findOne(filter) // Get first match
156
- create(data) // Create new
157
- updateById(id, data) // Update existing
158
- deleteById(id) // Delete
159
- count(filter) // Count matches
158
+ // Inherited methods (options-object API) - use these first
159
+ find({ filter }) // List with filters
160
+ findById({ id }) // Get by ID
161
+ findOne({ filter }) // Get first match
162
+ create({ data }) // Create new
163
+ updateById({ id, data }) // Update existing
164
+ updateAll({ data, where }) // Bulk update (updateBy is an alias)
165
+ deleteById({ id }) // Delete
166
+ deleteAll({ where }) // Bulk delete (deleteBy is an alias)
167
+ count({ where }) // Count matches
160
168
  ```
161
169
 
162
170
  ### Add Custom Methods When:
@@ -167,22 +175,24 @@ count(filter) // Count matches
167
175
 
168
176
  ```typescript
169
177
  // Custom repository methods
170
- export class OrderRepository extends BaseRepository<Order> {
178
+ export class OrderRepository extends DefaultCRUDRepository<typeof Order.schema> {
171
179
  // Complex query that's used in multiple places
172
180
  async findPendingOrdersOlderThan(hours: number) {
173
181
  const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
174
182
  return this.find({
175
- where: {
176
- status: 'pending',
177
- createdAt: { lt: cutoff },
183
+ filter: {
184
+ where: {
185
+ status: 'pending',
186
+ createdAt: { lt: cutoff },
187
+ },
188
+ order: ['createdAt ASC'],
178
189
  },
179
- orderBy: { createdAt: 'asc' },
180
190
  });
181
191
  }
182
192
 
183
- // Performance-optimized query
193
+ // Performance-optimized query (raw SQL through the Drizzle connector)
184
194
  async getOrderStats(userId: string) {
185
- return this.db.execute(sql`
195
+ return this.dataSource.connector.execute(sql`
186
196
  SELECT
187
197
  COUNT(*) as total,
188
198
  SUM(total) as revenue,
@@ -194,9 +204,12 @@ export class OrderRepository extends BaseRepository<Order> {
194
204
 
195
205
  // Business logic at data layer
196
206
  async softDelete(id: string) {
197
- return this.updateById(id, {
198
- deletedAt: new Date(),
199
- status: 'deleted',
207
+ return this.updateById({
208
+ id,
209
+ data: {
210
+ deletedAt: new Date(),
211
+ status: 'deleted',
212
+ },
200
213
  });
201
214
  }
202
215
  }
@@ -210,15 +223,15 @@ export class OrderRepository extends BaseRepository<Order> {
210
223
  ```typescript
211
224
  @controller({ path: '/users' })
212
225
  export class UserController extends BaseRestController {
213
- @post({ configs: { path: '/' } })
226
+ @post({ configs: RouteConfigs.CREATE_USER })
214
227
  async createUser(c: Context) {
215
228
  try {
216
229
  const data = await c.req.json();
217
230
  const user = await this.userService.create(data);
218
231
  return c.json(user, 201);
219
232
  } catch (error) {
220
- // Format error for API response
221
- if (error.code === 'DUPLICATE_EMAIL') {
233
+ // Format error for API response -- messageCode is always lower-cased by ApplicationError
234
+ if (error instanceof ApplicationError && error.messageCode === 'app.user.duplicate_email') {
222
235
  return c.json({ error: 'Email already exists' }, 400);
223
236
  }
224
237
  throw error; // Let global handler catch unknown errors
@@ -230,23 +243,29 @@ export class UserController extends BaseRestController {
230
243
  ### Service Level: Throw Domain Errors
231
244
 
232
245
  ```typescript
233
- @injectable()
234
246
  export class UserService extends BaseService {
247
+ constructor(
248
+ @inject({ key: 'repositories.UserRepository' })
249
+ private userRepository: UserRepository,
250
+ ) {
251
+ super({ scope: UserService.name });
252
+ }
253
+
235
254
  async create(data: CreateUserInput) {
236
255
  // Validate and throw domain-specific errors
237
- const existing = await this.userRepo.findByEmail(data.email);
256
+ const existing = await this.userRepository.findByEmail(data.email);
238
257
  if (existing) {
239
258
  throw getError({
240
259
  statusCode: 400,
241
- code: 'DUPLICATE_EMAIL',
260
+ messageCode: MessageCode.build({ parts: ['app', 'user', 'duplicate_email'] }),
242
261
  message: 'User with this email already exists',
243
262
  });
244
263
  }
245
264
 
246
265
  // Log operations
247
- this.logger.info('Creating user', { email: data.email });
266
+ this.logger.info('Creating user | email: %s', data.email);
248
267
 
249
- return this.userRepo.create(data);
268
+ return this.userRepository.create({ data });
250
269
  }
251
270
  }
252
271
  ```
@@ -254,11 +273,11 @@ export class UserService extends BaseService {
254
273
  ### Repository Level: Let Errors Bubble
255
274
 
256
275
  ```typescript
257
- export class UserRepository extends BaseRepository<User> {
276
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
258
277
  // Don't catch database errors here
259
278
  // Let them bubble up to service/controller
260
279
  async findByEmail(email: string) {
261
- return this.findOne({ where: { email } });
280
+ return this.findOne({ filter: { where: { email } } });
262
281
  }
263
282
  }
264
283
  ```
@@ -356,25 +375,27 @@ OrderStatusRepository // Probably doesn't need its own repo
356
375
 
357
376
  ```typescript
358
377
  // Use repository methods for most cases
359
- const orders = await orderRepo.find({ where: { userId } });
378
+ const orders = await orderRepository.find({ filter: { where: { userId } } });
379
+
380
+ // Use raw queries (via the datasource's Drizzle connector) for:
381
+ const connector = this.dataSource.connector;
360
382
 
361
- // Use raw queries for:
362
383
  // 1. Complex aggregations
363
- const stats = await db.execute(sql`
384
+ const stats = await connector.execute(sql`
364
385
  SELECT category, COUNT(*), AVG(price)
365
386
  FROM products
366
387
  GROUP BY category
367
388
  `);
368
389
 
369
390
  // 2. Performance-critical paths
370
- const results = await db.execute(sql`
391
+ const results = await connector.execute(sql`
371
392
  SELECT * FROM products
372
393
  WHERE tsv @@ plainto_tsquery(${search})
373
394
  LIMIT 10
374
395
  `);
375
396
 
376
397
  // 3. Database-specific features
377
- const nearby = await db.execute(sql`
398
+ const nearby = await connector.execute(sql`
378
399
  SELECT * FROM stores
379
400
  WHERE ST_DWithin(location, ${point}, 5000)
380
401
  `);
@@ -386,14 +407,16 @@ const nearby = await db.execute(sql`
386
407
  ### Environment Variables
387
408
 
388
409
  ```typescript
410
+ import { applicationEnvironment } from '@venizia/ignis-helpers';
411
+
389
412
  // Use for: secrets, environment-specific values
390
413
  const config = {
391
414
  database: {
392
- host: EnvHelper.get('APP_ENV_POSTGRES_HOST'),
393
- password: EnvHelper.get('APP_ENV_POSTGRES_PASSWORD'),
415
+ host: applicationEnvironment.get<string>('APP_ENV_POSTGRES_HOST'),
416
+ password: applicationEnvironment.get<string>('APP_ENV_POSTGRES_PASSWORD'),
394
417
  },
395
418
  stripe: {
396
- secretKey: EnvHelper.get('STRIPE_SECRET_KEY'),
419
+ secretKey: applicationEnvironment.get<string>('APP_ENV_STRIPE_SECRET_KEY'),
397
420
  },
398
421
  };
399
422
  ```
@@ -417,11 +440,19 @@ const appConfig = {
417
440
 
418
441
  ```typescript
419
442
  // Use for: component-specific settings
420
- this.component(SwaggerComponent, {
421
- title: 'My API',
422
- version: '1.0.0',
423
- path: '/doc',
443
+ // Components read their options from bindings - rebind before registering
444
+ this.bind<IApiReferenceOptions>({ key: ApiReferenceBindingKeys.API_REFERENCE_OPTIONS }).toValue({
445
+ restOptions: {
446
+ base: { path: '/doc' },
447
+ doc: { path: '/openapi.json' },
448
+ ui: { path: '/explorer', type: 'swagger' },
449
+ },
450
+ explorer: {
451
+ openapi: '3.1.0',
452
+ info: { title: 'My API', version: '1.0.0', description: 'My API documentation' },
453
+ },
424
454
  });
455
+ this.component(ApiReferenceComponent);
425
456
  ```
426
457
 
427
458
 
@@ -1,19 +1,19 @@
1
1
  # Advanced Patterns
2
2
 
3
- Advanced TypeScript patterns used throughout the Ignis framework.
3
+ Advanced TypeScript patterns used throughout the IGNIS framework.
4
4
 
5
5
  ## Mixin Pattern
6
6
 
7
7
  Create reusable class extensions without deep inheritance:
8
8
 
9
9
  ```typescript
10
- import { TMixinTarget } from '@venizia/ignis-helpers';
10
+ import { LoggerFactory, TMixinTarget } from '@venizia/ignis-helpers';
11
11
 
12
- export const LoggableMixin = <BaseClass extends TMixinTarget<Base>>(
12
+ export const LoggableMixin = <BaseClass extends TMixinTarget<object>>(
13
13
  baseClass: BaseClass,
14
14
  ) => {
15
15
  return class extends baseClass {
16
- protected logger = LoggerFactory.getLogger(this.constructor.name);
16
+ protected logger = LoggerFactory.getLogger([this.constructor.name]);
17
17
 
18
18
  log(message: string): void {
19
19
  this.logger.info(message);
@@ -32,7 +32,7 @@ class MyService extends LoggableMixin(BaseService) {
32
32
  ### Multiple Mixins
33
33
 
34
34
  ```typescript
35
- class MyRepository extends LoggableMixin(CacheableMixin(BaseRepository)) {
35
+ class MyService extends LoggableMixin(CacheableMixin(BaseService)) {
36
36
  // Has both logging and caching capabilities
37
37
  }
38
38
  ```
@@ -61,46 +61,51 @@ export const TimestampMixin = <
61
61
  Generate classes dynamically with configuration:
62
62
 
63
63
  ```typescript
64
- class ControllerFactory {
65
- static defineCrudController<Schema extends TTableSchemaWithId>(
66
- opts: ICrudControllerOptions<Schema>,
67
- ) {
64
+ class ControllerFactory extends BaseHelper {
65
+ /** `TDataObject`/`TPersistObject` cannot be inferred from `entity` -
66
+ * pass them explicitly for typed CRUD handlers. */
67
+ static defineCrudController<
68
+ TDataObject extends object = object,
69
+ TPersistObject extends object = TDataObject,
70
+ >(defOpts: ICrudControllerOptions) {
71
+ const { controller, entity } = defOpts;
72
+
73
+ // `entity` accepts a class directly or a resolver function
74
+ const entityClass = isClass(entity) ? entity : entity();
75
+ const entityInstance = new entityClass();
76
+
77
+ // Derive request/response schemas + route configs from the entity instance
78
+ const routeDefinitions = buildRouteDefinitions({ entity: entityInstance });
79
+
68
80
  return class extends BaseRestController {
69
- constructor(repository: AbstractRepository<Schema>) {
70
- super({ scope: opts.controller.name });
81
+ repository: AbstractRepository<TDataObject, TPersistObject>;
82
+
83
+ constructor(repository: AbstractRepository<TDataObject, TPersistObject>) {
84
+ super({ scope: controller.name, path: controller.basePath });
71
85
  this.repository = repository;
72
- this.setupRoutes();
73
86
  }
74
87
 
75
- private setupRoutes(): void {
76
- // Dynamically bind CRUD routes
88
+ /** Registers all CRUD route handlers. */
89
+ override binding(): ValueOrPromise<void> {
77
90
  this.defineRoute({
78
- configs: { method: 'get', path: '/' },
79
- handler: (c) => this.list(c),
91
+ configs: routeDefinitions.FIND,
92
+ handler: async context => this.find({ context }),
80
93
  });
81
94
  this.defineRoute({
82
- configs: { method: 'get', path: '/:id' },
83
- handler: (c) => this.getById(c),
95
+ configs: routeDefinitions.FIND_BY_ID,
96
+ handler: async context => this.findById({ context }),
84
97
  });
85
- // ... more routes
86
- }
87
-
88
- async list(c: Context) {
89
- const data = await this.repository.find({});
90
- return c.json(data);
91
- }
92
-
93
- async getById(c: Context) {
94
- const { id } = c.req.param();
95
- const data = await this.repository.findById({ id });
96
- return c.json(data);
98
+ // ... more routes (count/findOne/create/updateById/deleteById/...)
97
99
  }
98
100
  };
99
101
  }
100
102
  }
101
103
 
102
- // Usage
103
- const UserCrudController = ControllerFactory.defineCrudController({
104
+ // Usage - type parameters are explicit, entity can be a class or a resolver
105
+ type TUser = typeof User.schema.$inferSelect;
106
+ type TNewUser = typeof User.schema.$inferInsert;
107
+
108
+ const UserCrudController = ControllerFactory.defineCrudController<TUser, TNewUser>({
104
109
  controller: { name: 'UserController', basePath: '/users' },
105
110
  repository: { name: UserRepository.name },
106
111
  entity: () => User,
@@ -117,26 +122,33 @@ export class UserController extends UserCrudController {
117
122
  Support multiple input types that resolve to a single value:
118
123
 
119
124
  ```typescript
120
- // Type definitions
121
- export type TResolver<T> = () => T;
122
- export type TConstructor<T> = new (...args: any[]) => T;
123
- export type TValueOrResolver<T> = T | TResolver<T> | TConstructor<T>;
125
+ // Type definitions (from @venizia/ignis-helpers)
126
+ export type TResolver<T> = (...args: any[]) => T;
127
+ export type TValueOrResolver<T> = T | TResolver<T>;
124
128
 
125
129
  // Resolver function
126
130
  export const resolveValue = <T>(valueOrResolver: TValueOrResolver<T>): T => {
127
131
  if (typeof valueOrResolver !== 'function') {
128
132
  return valueOrResolver; // Direct value
129
133
  }
130
- if (isClassConstructor(valueOrResolver)) {
134
+
135
+ if (isClass(valueOrResolver as Function)) {
131
136
  return valueOrResolver as T; // Class constructor (return as-is)
132
137
  }
138
+
133
139
  return (valueOrResolver as TResolver<T>)(); // Function resolver
134
140
  };
135
141
 
136
- // Helper to detect class constructors
137
- function isClassConstructor(fn: Function): boolean {
138
- return fn.toString().startsWith('class ');
139
- }
142
+ // isClass (from @venizia/ignis-inversion, re-exported by helpers) - distinguishes class
143
+ // constructors from arrow/regular functions by testing the SOURCE against /^class[\s{]/,
144
+ // since every non-arrow function has a prototype
145
+ export const isClass = <T>(target: any): target is TClass<T> => {
146
+ if (typeof target !== 'function' || target.prototype === undefined) {
147
+ return false;
148
+ }
149
+
150
+ return /^class[\s{]/.test(Function.prototype.toString.call(target));
151
+ };
140
152
  ```
141
153
 
142
154
  ### Usage
@@ -222,7 +234,7 @@ class StrategyRegistry<T> {
222
234
 
223
235
  register(name: string, strategy: T): void {
224
236
  if (this.strategies.has(name)) {
225
- throw new Error(`Strategy '${name}' already registered`);
237
+ throw getError({ message: `[register] Strategy '${name}' already registered` });
226
238
  }
227
239
  this.strategies.set(name, strategy);
228
240
  }
@@ -230,7 +242,7 @@ class StrategyRegistry<T> {
230
242
  get(name: string): T {
231
243
  const strategy = this.strategies.get(name);
232
244
  if (!strategy) {
233
- throw new Error(`Strategy '${name}' not found`);
245
+ throw getError({ message: `[get] Strategy '${name}' not found` });
234
246
  }
235
247
  return strategy;
236
248
  }
@@ -255,5 +267,5 @@ const strategy = authRegistry.get('jwt');
255
267
  ## See Also
256
268
 
257
269
  - [Type Safety](./type-safety) - Generic type patterns
258
- - [Repositories Reference](../../references/base/repositories/) - Mixin usage
270
+ - [Repositories Reference](../../references/base/repositories/) - Repository hierarchy
259
271
  - [Architectural Patterns](../architectural-patterns) - High-level patterns
@@ -18,7 +18,6 @@ export class Authentication {
18
18
  export class HealthCheckRestPaths {
19
19
  static readonly ROOT = '/';
20
20
  static readonly PING = '/ping';
21
- static readonly METRICS = '/metrics';
22
21
  }
23
22
  ```
24
23
 
@@ -162,12 +161,13 @@ const DEFAULT_SERVER_OPTIONS: Partial<IServerOptions> = {
162
161
 
163
162
  ```typescript
164
163
  // In component constructor or binding
165
- const extraOptions = this.application.get<Partial<IServerOptions>>({
166
- key: BindingKeys.SERVER_OPTIONS,
167
- isOptional: true,
168
- }) ?? {};
164
+ const extraServerOptions =
165
+ this.application.get<Partial<ServerOptions>>({
166
+ key: SocketIOBindingKeys.SERVER_OPTIONS,
167
+ isOptional: true,
168
+ }) ?? {};
169
169
 
170
- this.options = Object.assign({}, DEFAULT_OPTIONS, extraOptions);
170
+ this.serverOptions = Object.assign({}, DEFAULT_SERVER_OPTIONS, extraServerOptions);
171
171
  ```
172
172
 
173
173
  ### Constructor Validation
@@ -175,20 +175,20 @@ this.options = Object.assign({}, DEFAULT_OPTIONS, extraOptions);
175
175
  Validate required options in the constructor:
176
176
 
177
177
  ```typescript
178
- constructor(options: IJWTTokenServiceOptions) {
179
- super({ scope: JWTTokenService.name });
178
+ constructor(options: IJWSTokenServiceOptions) {
179
+ super({ scope: JWSTokenService.name });
180
180
 
181
181
  if (!options.jwtSecret) {
182
182
  throw getError({
183
183
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
184
- message: '[JWTTokenService] Invalid jwtSecret',
184
+ message: '[JWSTokenService] Invalid jwtSecret',
185
185
  });
186
186
  }
187
187
 
188
- if (!options.applicationSecret) {
188
+ if (!options.getTokenExpiresFn) {
189
189
  throw getError({
190
190
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
191
- message: '[JWTTokenService] Invalid applicationSecret',
191
+ message: '[JWSTokenService] Invalid getTokenExpiresFn',
192
192
  });
193
193
  }
194
194
 
@@ -107,6 +107,9 @@ this.logger.info('[create] User created | ID: %s | Email: %s', user.id, user.ema
107
107
  this.logger.debug('[config] Server options: %j', this.serverOptions);
108
108
  ```
109
109
 
110
+ > [!NOTE]
111
+ > `%j` on an `Error` instance drops `message` and `stack` -- they are non-enumerable, so `JSON.stringify` never sees them. Use `%s` for errors, `%j`/`%o` for plain data objects. An object passed to `%s` is inspected up to `APP_ENV_LOGGER_INSPECT_DEPTH` levels deep (default `5`) instead of Node's hard-coded `depth: 0`, so nested fields print instead of collapsing to `[Object]`. See [Logger Helper](/extensions/helpers/logger/) for details.
112
+
110
113
  ### Log Levels
111
114
 
112
115
  | Level | Use For |
@@ -142,7 +145,7 @@ Include class/method context in error messages:
142
145
  // Format: [ClassName][methodName] Descriptive message
143
146
  throw getError({
144
147
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
145
- message: '[JWTTokenService][generate] Failed to generate token',
148
+ message: '[JWSTokenService][generate] Failed to generate token',
146
149
  });
147
150
 
148
151
  throw getError({
@@ -224,7 +227,7 @@ import dayjs from 'dayjs';
224
227
 
225
228
  // 3. Internal absolute imports (by domain/package)
226
229
  import { getError } from '@venizia/ignis-helpers';
227
- import { BaseEntity } from '@/base/models';
230
+ import { AbstractEntity } from '@/base/models';
228
231
  import { UserService } from '@/services';
229
232
 
230
233
  // 4. Relative imports (same feature) - LAST
@@ -24,14 +24,14 @@ Use JSDoc comments for public APIs to improve IDE support and generate documenta
24
24
  * @returns Promise resolving to the query result
25
25
  *
26
26
  * @example
27
- * const users = await userRepo.find({
27
+ * const users = await userRepository.find({
28
28
  * filter: { where: { status: 'ACTIVE' }, limit: 10 },
29
29
  * });
30
30
  *
31
31
  * @throws {ApplicationError} When validation fails
32
32
  * @see {@link UserService} for business logic
33
33
  */
34
- async find(opts: TFindOptions): Promise<TFindResult<TUser>> {
34
+ async find(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<TUser[]> {
35
35
  // implementation
36
36
  }
37
37
  ```
@@ -87,15 +87,15 @@ async createUser(data: TCreateUserRequest): Promise<TUser> {
87
87
  * @param opts - Find options
88
88
  * @param opts.filter - Query filter
89
89
  * @param opts.filter.where - Conditions to match
90
- * @param opts.filter.limit - Maximum records to return (default: 100)
90
+ * @param opts.filter.limit - Maximum records to return
91
91
  * @param opts.filter.offset - Records to skip for pagination
92
92
  * @param opts.filter.order - Sort order (e.g., ['createdAt DESC'])
93
93
  * @param opts.filter.include - Relations to load
94
- * @returns Promise with data array and count
94
+ * @returns Promise resolving to the array of matching entities
95
95
  *
96
96
  * @example
97
97
  * // Find active users, sorted by name
98
- * const result = await userRepo.find({
98
+ * const users = await userRepository.find({
99
99
  * filter: {
100
100
  * where: { status: 'ACTIVE' },
101
101
  * order: ['name ASC'],
@@ -103,7 +103,7 @@ async createUser(data: TCreateUserRequest): Promise<TUser> {
103
103
  * },
104
104
  * });
105
105
  */
106
- async find(opts: TFindOpts<Schema>): Promise<TFindResult<TEntity>> {
106
+ async find<R = TUser>(opts: { filter: TFilter<TUser>; options?: IExtraOptions }): Promise<R[]> {
107
107
  // ...
108
108
  }
109
109
  ```
@@ -116,13 +116,13 @@ async find(opts: TFindOpts<Schema>): Promise<TFindResult<TEntity>> {
116
116
  *
117
117
  * @example
118
118
  * // Before (deprecated)
119
- * const user = await repo.getById('123');
119
+ * const user = await userRepository.getById('123');
120
120
  *
121
121
  * // After (recommended)
122
- * const { data: user } = await repo.findById({ id: '123' });
122
+ * const user = await userRepository.findById({ id: '123' });
123
123
  */
124
124
  async getById(id: string): Promise<TUser | null> {
125
- return this.findById({ id }).then(r => r.data);
125
+ return this.findById({ id });
126
126
  }
127
127
  ```
128
128
 
@@ -181,7 +181,7 @@ interface IJWTStrategyOptions {
181
181
  * Extends this class to create entity-specific repositories with
182
182
  * type-safe operations and automatic schema binding.
183
183
  *
184
- * @template Schema - The Drizzle table schema type
184
+ * @template EntitySchema - The Drizzle table schema type
185
185
  *
186
186
  * @example
187
187
  * @repository({ model: User, dataSource: PostgresDataSource })
@@ -189,10 +189,10 @@ interface IJWTStrategyOptions {
189
189
  * // Custom methods here
190
190
  * }
191
191
  *
192
- * @see {@link BaseEntity} for model definition
193
- * @see {@link BaseDataSource} for database connection
192
+ * @see {@link BasePostgresEntity} for model definition
193
+ * @see {@link BasePostgresDataSource} for database connection
194
194
  */
195
- abstract class DefaultCRUDRepository<Schema extends TTableSchemaWithId> {
195
+ class DefaultCRUDRepository<EntitySchema extends TTableSchemaWithId = TTableSchemaWithId> {
196
196
  // ...
197
197
  }
198
198
  ```