@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.
- package/package.json +2 -2
- package/wiki/changelogs/2025-12-26-nested-relations-and-generics.md +86 -0
- package/wiki/changelogs/2025-12-26-transaction-support.md +57 -0
- package/wiki/changelogs/index.md +3 -1
- package/wiki/changelogs/planned-schema-migrator.md +561 -0
- package/wiki/get-started/best-practices/code-style-standards.md +651 -10
- package/wiki/get-started/best-practices/performance-optimization.md +11 -2
- package/wiki/get-started/core-concepts/components.md +59 -42
- package/wiki/get-started/core-concepts/persistent.md +43 -47
- package/wiki/references/base/components.md +515 -31
- package/wiki/references/base/controllers.md +85 -18
- package/wiki/references/base/datasources.md +78 -5
- package/wiki/references/base/repositories.md +92 -6
- package/wiki/references/helpers/index.md +1 -0
- package/wiki/references/helpers/types.md +151 -0
- package/wiki/changelogs/planned-transaction-support.md +0 -216
|
@@ -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
|
-
|
|
15
|
+
---
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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.
|
|
40
|
-
2.
|
|
41
|
-
3.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
49
|
-
|
|
440
|
+
// Merge with defaults
|
|
441
|
+
const options = { ...DEFAULT_OPTIONS, ...extraOptions };
|
|
50
442
|
|
|
51
|
-
//
|
|
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
|
-
|
|
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 })
|
|
535
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
|
|
536
|
+
private application: BaseApplication,
|
|
61
537
|
) {
|
|
62
538
|
super({
|
|
63
|
-
scope:
|
|
539
|
+
scope: MyComponent.name,
|
|
64
540
|
initDefault: { enable: true, container: application },
|
|
65
541
|
bindings: {
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
| `
|
|
322
|
-
| `
|
|
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
|
-
###
|
|
324
|
+
### Routes Configuration
|
|
325
325
|
|
|
326
|
-
The `
|
|
326
|
+
The `routes` option provides a unified way to configure both schema overrides and authentication for each endpoint:
|
|
327
327
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
|
331
|
-
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
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
|
|