@venizia/ignis-docs 0.0.1 → 0.0.3

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.
@@ -12,17 +12,223 @@ Technical reference for `BaseComponent`—the foundation for creating reusable,
12
12
  | **Lifecycle Management** | Auto-called `binding()` method during startup |
13
13
  | **Default Bindings** | Self-contained with automatic DI registration |
14
14
 
15
- ## `BaseComponent` Class
15
+ ---
16
16
 
17
- Abstract class for all components - structures resource binding and lifecycle management.
17
+ ## Component Directory Structure
18
+
19
+ A well-organized component follows a consistent directory structure that separates concerns and makes the codebase maintainable.
20
+
21
+ ### Simple Component
22
+
23
+ ```
24
+ src/components/health-check/
25
+ ├── index.ts # Barrel exports (re-exports everything)
26
+ ├── component.ts # Component class with binding logic
27
+ ├── controller.ts # Controller class(es)
28
+ └── common/
29
+ ├── index.ts # Barrel exports for common/
30
+ ├── keys.ts # Binding key constants
31
+ ├── types.ts # Interfaces and type definitions
32
+ ├── constants.ts # Static class constants (optional)
33
+ └── rest-paths.ts # Route path constants (optional)
34
+ ```
35
+
36
+ ### Complex Component (with services, models, strategies)
37
+
38
+ ```
39
+ src/components/auth/
40
+ ├── index.ts
41
+ ├── authenticate/
42
+ │ ├── index.ts
43
+ │ ├── component.ts
44
+ │ ├── common/
45
+ │ │ ├── index.ts
46
+ │ │ ├── keys.ts
47
+ │ │ ├── types.ts
48
+ │ │ └── constants.ts
49
+ │ ├── controllers/
50
+ │ │ ├── index.ts
51
+ │ │ └── auth.controller.ts
52
+ │ ├── services/
53
+ │ │ ├── index.ts
54
+ │ │ └── jwt-token.service.ts
55
+ │ └── strategies/
56
+ │ ├── index.ts
57
+ │ ├── jwt.strategy.ts
58
+ │ └── basic.strategy.ts
59
+ └── models/
60
+ ├── index.ts
61
+ ├── entities/
62
+ │ └── user-token.model.ts
63
+ └── requests/
64
+ ├── sign-in.schema.ts
65
+ └── sign-up.schema.ts
66
+ ```
67
+
68
+ ---
69
+
70
+ ## The `common/` Directory
71
+
72
+ The `common/` directory contains shared definitions that are used throughout the component. Every component should have this directory with at least `keys.ts` and `types.ts`.
73
+
74
+ ### 1. Binding Keys (`keys.ts`)
75
+
76
+ Binding keys are string constants used to register and retrieve values from the DI container. They follow the pattern `@app/[component]/[feature]`.
77
+
78
+ ```typescript
79
+ // src/components/health-check/common/keys.ts
80
+ export class HealthCheckBindingKeys {
81
+ static readonly HEALTH_CHECK_OPTIONS = '@app/health-check/options';
82
+ }
83
+ ```
84
+
85
+ **For components with multiple features:**
86
+
87
+ ```typescript
88
+ // src/components/auth/authenticate/common/keys.ts
89
+ export class AuthenticateBindingKeys {
90
+ static readonly AUTHENTICATE_OPTIONS = '@app/authenticate/options';
91
+ static readonly JWT_OPTIONS = '@app/authenticate/jwt/options';
92
+ }
93
+ ```
94
+
95
+ **Naming Convention:**
96
+ - Class name: `[Feature]BindingKeys`
97
+ - Key format: `@app/[component]/[feature]` or `@app/[component]/[sub-feature]/[name]`
98
+
99
+ ### 2. Types (`types.ts`)
100
+
101
+ Define all interfaces and type aliases that the component exposes or uses internally.
102
+
103
+ ```typescript
104
+ // src/components/health-check/common/types.ts
105
+ export interface IHealthCheckOptions {
106
+ restOptions: { path: string };
107
+ }
108
+ ```
109
+
110
+ **For complex components with service interfaces:**
111
+
112
+ ```typescript
113
+ // src/components/auth/authenticate/common/types.ts
114
+ import { Context } from 'hono';
115
+ import { AnyObject, ValueOrPromise } from '@venizia/ignis-helpers';
116
+
117
+ // Options interface for the component
118
+ export interface IAuthenticateOptions {
119
+ alwaysAllowPaths: Array<string>;
120
+ tokenOptions: IJWTTokenServiceOptions;
121
+ restOptions?: {
122
+ useAuthController?: boolean;
123
+ controllerOpts?: TDefineAuthControllerOpts;
124
+ };
125
+ }
126
+
127
+ // Service options interface
128
+ export interface IJWTTokenServiceOptions {
129
+ jwtSecret: string;
130
+ applicationSecret: string;
131
+ getTokenExpiresFn: () => ValueOrPromise<number>;
132
+ }
133
+
134
+ // Service contract interface
135
+ export interface IAuthService<
136
+ SIRQ = AnyObject,
137
+ SIRS = AnyObject,
138
+ > {
139
+ signIn(context: Context, opts: SIRQ): Promise<SIRS>;
140
+ signUp(context: Context, opts: SIRQ): Promise<SIRS>;
141
+ }
142
+
143
+ // Auth user type
144
+ export interface IAuthUser {
145
+ userId: string;
146
+ [extra: string | symbol]: any;
147
+ }
148
+ ```
149
+
150
+ **Naming Conventions:**
151
+ - Interfaces: `I` prefix (e.g., `IHealthCheckOptions`, `IAuthService`)
152
+ - Type aliases: `T` prefix (e.g., `TDefineAuthControllerOpts`)
153
+
154
+ ### 3. Constants (`constants.ts`)
155
+
156
+ Use static classes (not enums) for constants that need type extraction and validation.
157
+
158
+ ```typescript
159
+ // src/components/auth/authenticate/common/constants.ts
160
+ export class Authentication {
161
+ // Strategy identifiers
162
+ static readonly STRATEGY_BASIC = 'basic';
163
+ static readonly STRATEGY_JWT = 'jwt';
164
+
165
+ // Token types
166
+ static readonly TYPE_BASIC = 'Basic';
167
+ static readonly TYPE_BEARER = 'Bearer';
168
+
169
+ // Context keys
170
+ static readonly CURRENT_USER = 'auth.current.user';
171
+ static readonly SKIP_AUTHENTICATION = 'authentication.skip';
172
+ }
173
+ ```
174
+
175
+ **With validation (for user-configurable values):**
18
176
 
19
- ### Key Features
177
+ ```typescript
178
+ // src/components/swagger/common/constants.ts
179
+ import { TConstValue } from '@venizia/ignis-helpers';
180
+
181
+ export class DocumentUITypes {
182
+ static readonly SWAGGER = 'swagger';
183
+ static readonly SCALAR = 'scalar';
184
+
185
+ // Set for O(1) validation
186
+ static readonly SCHEME_SET = new Set([this.SWAGGER, this.SCALAR]);
20
187
 
21
- | Feature | Description |
22
- | :--- | :--- |
23
- | **Encapsulation** | Bundles necessary bindings (services, controllers) for a feature |
24
- | **Lifecycle Management** | `binding()` method auto-called during startup |
25
- | **Default Bindings** | Auto-registers with application container (self-contained) |
188
+ // Validation helper
189
+ static isValid(value: string): boolean {
190
+ return this.SCHEME_SET.has(value);
191
+ }
192
+ }
193
+
194
+ // Extract union type: 'swagger' | 'scalar'
195
+ export type TDocumentUIType = TConstValue<typeof DocumentUITypes>;
196
+ ```
197
+
198
+ ### 4. REST Paths (`rest-paths.ts`)
199
+
200
+ Define route path constants for controllers.
201
+
202
+ ```typescript
203
+ // src/components/health-check/common/rest-paths.ts
204
+ export class HealthCheckRestPaths {
205
+ static readonly ROOT = '/';
206
+ static readonly PING = '/ping';
207
+ static readonly METRICS = '/metrics';
208
+ }
209
+ ```
210
+
211
+ ### 5. Barrel Exports (`index.ts`)
212
+
213
+ Every folder should have an `index.ts` that re-exports its contents:
214
+
215
+ ```typescript
216
+ // src/components/health-check/common/index.ts
217
+ export * from './keys';
218
+ export * from './rest-paths';
219
+ export * from './types';
220
+
221
+ // src/components/health-check/index.ts
222
+ export * from './common';
223
+ export * from './component';
224
+ export * from './controller';
225
+ ```
226
+
227
+ ---
228
+
229
+ ## `BaseComponent` Class
230
+
231
+ Abstract class for all components - structures resource binding and lifecycle management.
26
232
 
27
233
  ### Constructor Options
28
234
 
@@ -32,49 +238,327 @@ The `super()` constructor in your component can take the following options:
32
238
  | :--- | :--- | :--- |
33
239
  | `scope` | `string` | **Required.** A unique name for the component, typically `MyComponent.name`. Used for logging. |
34
240
  | `initDefault` | `{ enable: boolean; container: Container }` | If `enable` is `true`, the `bindings` defined below will be automatically registered with the provided `container` (usually the application instance) if they are not already bound. |
35
- | `bindings` | `Record<string, Binding>` | An object where keys are binding keys (e.g., `'services.MyService'`) and values are `Binding` instances. These are the default services, values, or providers that your component offers. |
241
+ | `bindings` | `Record<string, Binding>` | An object where keys are binding keys and values are `Binding` instances. These are the default services, values, or providers that your component offers. |
36
242
 
37
243
  ### Lifecycle Flow
38
244
 
39
- 1. **Application Instantiates Component**: When you call `this.component(MyComponent)` in your application, the DI container creates an instance of your component.
40
- 2. **Constructor Runs**: Your component's constructor calls `super()`, setting up its scope and defining its default `bindings`. If `initDefault` is enabled, these bindings are immediately registered with the application container.
41
- 3. **Application Calls `binding()`**: During the `registerComponents` phase of the application startup, the `binding()` method of your component is called. This is where you can perform additional setup that might depend on the default bindings being available.
245
+ 1. **Application Instantiates Component**: When you call `this.component(MyComponent)` in your application, the DI container creates an instance of your component.
246
+ 2. **Constructor Runs**: Your component's constructor calls `super()`, setting up its scope and defining its default `bindings`. If `initDefault` is enabled, these bindings are immediately registered with the application container.
247
+ 3. **Application Calls `binding()`**: During the `registerComponents` phase of the application startup, the `binding()` method of your component is called. This is where you can perform additional setup that might depend on the default bindings being available.
248
+
249
+ ---
250
+
251
+ ## Component Implementation Patterns
252
+
253
+ ### Basic Component
254
+
255
+ ```typescript
256
+ // src/components/health-check/component.ts
257
+ import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise } from '@venizia/ignis';
258
+ import { HealthCheckBindingKeys, IHealthCheckOptions } from './common';
259
+ import { HealthCheckController } from './controller';
260
+
261
+ // 1. Define default options
262
+ const DEFAULT_OPTIONS: IHealthCheckOptions = {
263
+ restOptions: { path: '/health' },
264
+ };
265
+
266
+ export class HealthCheckComponent extends BaseComponent {
267
+ constructor(
268
+ // 2. Inject the application instance
269
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE })
270
+ private application: BaseApplication,
271
+ ) {
272
+ super({
273
+ scope: HealthCheckComponent.name,
274
+ // 3. Enable automatic binding registration
275
+ initDefault: { enable: true, container: application },
276
+ // 4. Define default bindings
277
+ bindings: {
278
+ [HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS]: Binding.bind<IHealthCheckOptions>({
279
+ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
280
+ }).toValue(DEFAULT_OPTIONS),
281
+ },
282
+ });
283
+ }
284
+
285
+ // 5. Configure resources in binding()
286
+ override binding(): ValueOrPromise<void> {
287
+ // Read options (may have been overridden by user)
288
+ const healthOptions = this.application.get<IHealthCheckOptions>({
289
+ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS,
290
+ isOptional: true,
291
+ }) ?? DEFAULT_OPTIONS;
292
+
293
+ // Register controller with dynamic path
294
+ Reflect.decorate(
295
+ [controller({ path: healthOptions.restOptions.path })],
296
+ HealthCheckController,
297
+ );
298
+ this.application.controller(HealthCheckController);
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### Component with Services
304
+
305
+ ```typescript
306
+ // src/components/auth/authenticate/component.ts
307
+ import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise, getError } from '@venizia/ignis';
308
+ import { AuthenticateBindingKeys, IAuthenticateOptions, IJWTTokenServiceOptions } from './common';
309
+ import { JWTTokenService } from './services';
310
+ import { defineAuthController } from './controllers';
311
+
312
+ const DEFAULT_OPTIONS: IAuthenticateOptions = {
313
+ alwaysAllowPaths: [],
314
+ tokenOptions: {
315
+ applicationSecret: process.env.APP_ENV_APPLICATION_SECRET ?? '',
316
+ jwtSecret: process.env.APP_ENV_JWT_SECRET ?? '',
317
+ getTokenExpiresFn: () => parseInt(process.env.APP_ENV_JWT_EXPIRES_IN ?? '86400'),
318
+ },
319
+ };
320
+
321
+ export class AuthenticateComponent extends BaseComponent {
322
+ constructor(
323
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE })
324
+ private application: BaseApplication,
325
+ ) {
326
+ super({
327
+ scope: AuthenticateComponent.name,
328
+ initDefault: { enable: true, container: application },
329
+ bindings: {
330
+ [AuthenticateBindingKeys.AUTHENTICATE_OPTIONS]: Binding.bind<IAuthenticateOptions>({
331
+ key: AuthenticateBindingKeys.AUTHENTICATE_OPTIONS,
332
+ }).toValue(DEFAULT_OPTIONS),
333
+ },
334
+ });
335
+ }
336
+
337
+ // Split complex logic into private methods
338
+ private defineAuth(): void {
339
+ const options = this.application.get<IAuthenticateOptions>({
340
+ key: AuthenticateBindingKeys.AUTHENTICATE_OPTIONS,
341
+ });
342
+
343
+ // Validate required configuration
344
+ if (!options?.tokenOptions.jwtSecret) {
345
+ throw getError({
346
+ message: '[defineAuth] Missing required jwtSecret configuration',
347
+ });
348
+ }
349
+
350
+ // Bind service options
351
+ this.application
352
+ .bind<IJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS })
353
+ .toValue(options.tokenOptions);
354
+
355
+ // Register service
356
+ this.application.service(JWTTokenService);
357
+
358
+ // Conditionally register controller
359
+ if (options.restOptions?.useAuthController) {
360
+ this.application.controller(
361
+ defineAuthController(options.restOptions.controllerOpts),
362
+ );
363
+ }
364
+ }
365
+
366
+ override binding(): ValueOrPromise<void> {
367
+ this.defineAuth();
368
+ }
369
+ }
370
+ ```
371
+
372
+ ### Component with Factory Controllers
373
+
374
+ When controllers need to be dynamically configured:
375
+
376
+ ```typescript
377
+ // src/components/static-asset/component.ts
378
+ override binding(): ValueOrPromise<void> {
379
+ const componentOptions = this.application.get<TStaticAssetsComponentOptions>({
380
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
381
+ });
382
+
383
+ // Create multiple controllers from configuration
384
+ for (const [key, opt] of Object.entries(componentOptions)) {
385
+ this.application.controller(
386
+ AssetControllerFactory.defineAssetController({
387
+ controller: opt.controller,
388
+ storage: opt.storage,
389
+ helper: opt.helper,
390
+ }),
391
+ );
42
392
 
43
- ### Example Implementation
393
+ this.application.logger.info(
394
+ '[binding] Asset storage bound | Key: %s | Type: %s',
395
+ key,
396
+ opt.storage,
397
+ );
398
+ }
399
+ }
400
+ ```
401
+
402
+ ---
403
+
404
+ ## Exposing and Consuming Component Options
405
+
406
+ ### Pattern 1: Override Before Registration
407
+
408
+ The most common pattern - override options before registering the component:
409
+
410
+ ```typescript
411
+ // src/application.ts
412
+ import { HealthCheckComponent, HealthCheckBindingKeys, IHealthCheckOptions } from '@venizia/ignis';
413
+
414
+ export class Application extends BaseApplication {
415
+ preConfigure(): ValueOrPromise<void> {
416
+ // 1. Override options BEFORE registering component
417
+ this.bind<IHealthCheckOptions>({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS })
418
+ .toValue({
419
+ restOptions: { path: '/api/health' }, // Custom path
420
+ });
421
+
422
+ // 2. Register component (will use overridden options)
423
+ this.component(HealthCheckComponent);
424
+ }
425
+ }
426
+ ```
427
+
428
+ ### Pattern 2: Merge with Defaults
429
+
430
+ For partial overrides, merge with defaults in the component:
44
431
 
45
432
  ```typescript
46
- import { BaseApplication, BaseComponent, inject, CoreBindings, ValueOrPromise, Binding } from '@venizia/ignis';
433
+ // In your component's binding() method
434
+ override binding(): ValueOrPromise<void> {
435
+ const extraOptions = this.application.get<Partial<IMyOptions>>({
436
+ key: MyBindingKeys.OPTIONS,
437
+ isOptional: true,
438
+ }) ?? {};
47
439
 
48
- // A service this component provides
49
- class MyComponentService { /* ... */ }
440
+ // Merge with defaults
441
+ const options = { ...DEFAULT_OPTIONS, ...extraOptions };
50
442
 
51
- // A controller that uses the service
52
- @controller({ path: '/my-feature' })
53
- class MyComponentController extends BaseController {
54
- constructor(@inject({ key: 'services.MyComponentService' }) service: MyComponentService) { /* ... */ }
55
- // ...
443
+ // Use merged options...
56
444
  }
445
+ ```
57
446
 
58
- export class MyCustomComponent extends BaseComponent {
447
+ ### Pattern 3: Deep Merge for Nested Options
448
+
449
+ For complex nested configurations:
450
+
451
+ ```typescript
452
+ override binding(): ValueOrPromise<void> {
453
+ const extraOptions = this.application.get<Partial<ISwaggerOptions>>({
454
+ key: SwaggerBindingKeys.SWAGGER_OPTIONS,
455
+ isOptional: true,
456
+ }) ?? {};
457
+
458
+ // Deep merge nested objects
459
+ const options: ISwaggerOptions = {
460
+ ...DEFAULT_OPTIONS,
461
+ ...extraOptions,
462
+ restOptions: {
463
+ ...DEFAULT_OPTIONS.restOptions,
464
+ ...extraOptions.restOptions,
465
+ },
466
+ explorer: {
467
+ ...DEFAULT_OPTIONS.explorer,
468
+ ...extraOptions.explorer,
469
+ },
470
+ };
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Best Practices Summary
477
+
478
+ | Aspect | Recommendation |
479
+ |--------|----------------|
480
+ | **Directory** | Use `common/` for shared keys, types, constants |
481
+ | **Keys** | Use `@app/[component]/[feature]` format |
482
+ | **Types** | `I` prefix for interfaces, `T` prefix for type aliases |
483
+ | **Constants** | Use static classes with `SCHEME_SET` for validation |
484
+ | **Defaults** | Define `DEFAULT_OPTIONS` constant at file top |
485
+ | **Exports** | Use barrel exports (`index.ts`) at every level |
486
+ | **Validation** | Validate required options in `binding()` |
487
+ | **Logging** | Log binding activity with structured messages |
488
+ | **Scope** | Always set `scope: ComponentName.name` |
489
+
490
+ ---
491
+
492
+ ## Quick Reference Template
493
+
494
+ ```typescript
495
+ // common/keys.ts
496
+ export class MyComponentBindingKeys {
497
+ static readonly OPTIONS = '@app/my-component/options';
498
+ }
499
+
500
+ // common/types.ts
501
+ export interface IMyComponentOptions {
502
+ restOptions: { path: string };
503
+ // ... other options
504
+ }
505
+
506
+ // common/constants.ts (optional)
507
+ export class MyConstants {
508
+ static readonly VALUE_A = 'a';
509
+ static readonly VALUE_B = 'b';
510
+ }
511
+
512
+ // common/rest-paths.ts (optional)
513
+ export class MyRestPaths {
514
+ static readonly ROOT = '/';
515
+ static readonly BY_ID = '/:id';
516
+ }
517
+
518
+ // common/index.ts
519
+ export * from './keys';
520
+ export * from './types';
521
+ export * from './constants';
522
+ export * from './rest-paths';
523
+
524
+ // component.ts
525
+ import { BaseApplication, BaseComponent, inject, CoreBindings, Binding, ValueOrPromise } from '@venizia/ignis';
526
+ import { MyComponentBindingKeys, IMyComponentOptions } from './common';
527
+ import { MyController } from './controller';
528
+
529
+ const DEFAULT_OPTIONS: IMyComponentOptions = {
530
+ restOptions: { path: '/my-feature' },
531
+ };
532
+
533
+ export class MyComponent extends BaseComponent {
59
534
  constructor(
60
- @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
535
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE })
536
+ private application: BaseApplication,
61
537
  ) {
62
538
  super({
63
- scope: MyCustomComponent.name,
539
+ scope: MyComponent.name,
64
540
  initDefault: { enable: true, container: application },
65
541
  bindings: {
66
- 'services.MyComponentService': Binding.bind({ key: 'services.MyComponentService' })
67
- .toClass(MyComponentService)
68
- .setScope(BindingScopes.SINGLETON),
542
+ [MyComponentBindingKeys.OPTIONS]: Binding.bind<IMyComponentOptions>({
543
+ key: MyComponentBindingKeys.OPTIONS,
544
+ }).toValue(DEFAULT_OPTIONS),
69
545
  },
70
546
  });
71
547
  }
72
548
 
73
- // This method is called after the default bindings are registered.
74
549
  override binding(): ValueOrPromise<void> {
75
- // We can now register the controller, which depends on the service.
76
- this.application.controller(MyComponentController);
550
+ const options = this.application.get<IMyComponentOptions>({
551
+ key: MyComponentBindingKeys.OPTIONS,
552
+ isOptional: true,
553
+ }) ?? DEFAULT_OPTIONS;
554
+
555
+ // Register controllers, services, etc.
556
+ this.application.controller(MyController);
77
557
  }
78
558
  }
559
+
560
+ // index.ts
561
+ export * from './common';
562
+ export * from './component';
563
+ export * from './controller';
79
564
  ```
80
- This architecture makes features modular. The `AuthenticateComponent`, for example, uses this pattern to provide the `JWTTokenService` as a default binding and then registers the `AuthController` which depends on it.
@@ -318,27 +318,94 @@ This factory method returns a `BaseController` class that is already set up with
318
318
  | `controller.readonly` | `boolean` | If `true`, only read operations (find, findOne, findById, count) are generated. Write operations are excluded. Defaults to `false`. |
319
319
  | `controller.isStrict` | `boolean` | If `true`, query parameters like `where` will be strictly validated. Defaults to `true`. |
320
320
  | `controller.defaultLimit`| `number` | The default limit for `find` operations. Defaults to `10`. |
321
- | `schema` | `object` | An optional object to override the default Zod schemas for specific CRUD endpoints. See schema options below. |
322
- | `doDeleteWithReturn` | `boolean` | If `true`, the `deleteById` and `deleteBy` endpoints will return the deleted record(s) in the response body. Defaults to `false`. |
321
+ | `authStrategies` | `Array<TAuthStrategy>` | Auth strategies applied to all routes (unless overridden per-route). |
322
+ | `routes` | `TRoutesConfig` | Per-route configuration combining schema and auth. See routes configuration below. |
323
323
 
324
- ### Schema Override Options
324
+ ### Routes Configuration
325
325
 
326
- The `schema` option allows fine-grained control over request/response validation and OpenAPI documentation:
326
+ The `routes` option provides a unified way to configure both schema overrides and authentication for each endpoint:
327
327
 
328
- | Schema Option | Description |
329
- | :--- | :--- |
330
- | `count` | Override response schema for count endpoint |
331
- | `find` | Override response schema for find endpoint |
332
- | `findOne` | Override response schema for findOne endpoint |
333
- | `findById` | Override response schema for findById endpoint |
334
- | `create` | Override response schema for create endpoint |
335
- | `createRequestBody` | Override request body schema for create endpoint |
336
- | `updateById` | Override response schema for updateById endpoint |
337
- | `updateByIdRequestBody` | Override request body schema for updateById endpoint |
338
- | `updateBy` | Override response schema for updateBy (bulk update) endpoint |
339
- | `updateByRequestBody` | Override request body schema for updateBy endpoint |
340
- | `deleteById` | Override response schema for deleteById endpoint |
341
- | `deleteBy` | Override response schema for deleteBy (bulk delete) endpoint |
328
+ ```typescript
329
+ type TRouteAuthConfig =
330
+ | { skipAuth: true }
331
+ | { skipAuth?: false; authStrategies: Array<TAuthStrategy> };
332
+
333
+ type TReadRouteConfig = TRouteAuthConfig & { schema?: z.ZodObject };
334
+ type TWriteRouteConfig = TReadRouteConfig & { requestBody?: z.ZodObject };
335
+ type TDeleteRouteConfig = TRouteAuthConfig & { schema?: z.ZodObject };
336
+ ```
337
+
338
+ | Route | Type | Description |
339
+ | :--- | :--- | :--- |
340
+ | `count` | `TReadRouteConfig` | Config for count endpoint |
341
+ | `find` | `TReadRouteConfig` | Config for find endpoint |
342
+ | `findOne` | `TReadRouteConfig` | Config for findOne endpoint |
343
+ | `findById` | `TReadRouteConfig` | Config for findById endpoint |
344
+ | `create` | `TWriteRouteConfig` | Config for create endpoint (supports `requestBody`) |
345
+ | `updateById` | `TWriteRouteConfig` | Config for updateById endpoint (supports `requestBody`) |
346
+ | `updateBy` | `TWriteRouteConfig` | Config for updateBy endpoint (supports `requestBody`) |
347
+ | `deleteById` | `TDeleteRouteConfig` | Config for deleteById endpoint |
348
+ | `deleteBy` | `TDeleteRouteConfig` | Config for deleteBy endpoint |
349
+
350
+ ### Auth Resolution Priority
351
+
352
+ When resolving authentication for a route, the following priority applies:
353
+
354
+ 1. **Endpoint `skipAuth: true`** → No auth (ignores controller `authStrategies`)
355
+ 2. **Endpoint `authStrategies`** → Override controller (empty array = no auth)
356
+ 3. **Controller `authStrategies`** → Default fallback
357
+
358
+ ### Authentication Examples
359
+
360
+ ```typescript
361
+ // 1. JWT auth on ALL routes
362
+ const UserController = ControllerFactory.defineCrudController({
363
+ entity: UserEntity,
364
+ repository: { name: 'UserRepository' },
365
+ controller: { name: 'UserController', basePath: '/users' },
366
+ authStrategies: ['jwt'],
367
+ });
368
+
369
+ // 2. JWT auth on all, but skip for public read endpoints
370
+ const ProductController = ControllerFactory.defineCrudController({
371
+ entity: ProductEntity,
372
+ repository: { name: 'ProductRepository' },
373
+ controller: { name: 'ProductController', basePath: '/products' },
374
+ authStrategies: ['jwt'],
375
+ routes: {
376
+ find: { skipAuth: true },
377
+ findById: { skipAuth: true },
378
+ count: { skipAuth: true },
379
+ },
380
+ });
381
+
382
+ // 3. No controller auth, require JWT only for write operations
383
+ const ArticleController = ControllerFactory.defineCrudController({
384
+ entity: ArticleEntity,
385
+ repository: { name: 'ArticleRepository' },
386
+ controller: { name: 'ArticleController', basePath: '/articles' },
387
+ routes: {
388
+ create: { authStrategies: ['jwt'] },
389
+ updateById: { authStrategies: ['jwt'] },
390
+ deleteById: { authStrategies: ['jwt'] },
391
+ },
392
+ });
393
+
394
+ // 4. Custom schema with auth configuration
395
+ const OrderController = ControllerFactory.defineCrudController({
396
+ entity: OrderEntity,
397
+ repository: { name: 'OrderRepository' },
398
+ controller: { name: 'OrderController', basePath: '/orders' },
399
+ authStrategies: ['jwt'],
400
+ routes: {
401
+ find: { schema: CustomOrderListSchema, skipAuth: true },
402
+ create: {
403
+ schema: CustomOrderResponseSchema,
404
+ requestBody: CustomOrderCreateSchema,
405
+ },
406
+ },
407
+ });
408
+ ```
342
409
 
343
410
  ### Example
344
411