@acontplus/ng-infrastructure 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,85 +1,159 @@
1
1
  # @acontplus/ng-infrastructure
2
2
 
3
- Angular infrastructure library providing HTTP interceptors, repositories,
4
- adapters, and core services following clean architecture patterns for robust
5
- enterprise applications.
3
+ Angular infrastructure library providing HTTP interceptors with multi-language
4
+ support, repositories, adapters, and core services following clean architecture
5
+ patterns for robust enterprise applications.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- # Using npm
11
10
  npm install @acontplus/ng-infrastructure
12
-
13
- # Using pnpm
11
+ # or
14
12
  pnpm add @acontplus/ng-infrastructure
15
13
  ```
16
14
 
15
+ ## Peer Dependencies
16
+
17
+ - `@angular/common`: ^22.1.0
18
+ - `@angular/core`: ^22.1.0
19
+ - `@angular/router`: ^22.1.0
20
+ - `@acontplus/core`: ^1.2.0
21
+ - `@acontplus/ng-config`: ^1.1.0
22
+ - `@acontplus/ng-notifications`: ^1.0.0
23
+
17
24
  ## Features
18
25
 
19
- - **HTTP Interceptors**: API request/response handling, HTTP context management,
20
- and loading indicators
26
+ - **HTTP Interceptors**: API request/response handling with localized error messages,
27
+ `Accept-Language` header injection, HTTP context management, and loading indicators
28
+ - **Language Detection**: `LanguageInfo` service resolves language from JWT claims,
29
+ session storage, and browser settings
21
30
  - **Repository Pattern**: Base HTTP repository, generic repository, and
22
31
  repository factory
23
- - **HTTP Adapters**: Angular HTTP client adapter for external service
24
- integration
25
- - **Core Services**: Configuration, correlation tracking, logging, and tenant
26
- management
32
+ - **HTTP Adapters**: Angular HTTP client adapter for external service integration
33
+ - **Core Services**: Configuration, correlation tracking, language info, logging,
34
+ and tenant management
27
35
  - **Use Cases**: Base use case patterns with command and query separation (CQRS)
28
- - **Clean Architecture**: Separation of concerns with infrastructure layer
29
- abstractions
30
- - **TypeScript Support**: Full type safety with comprehensive interfaces
36
+ - **Clean Architecture**: Separation of concerns with infrastructure layer abstractions
31
37
 
32
38
  ## Quick Start
33
39
 
34
40
  ### Configure Interceptors
35
41
 
42
+ The recommended interceptor order is **httpContextInterceptor first**, then
43
+ apiInterceptor, then others:
44
+
36
45
  ```typescript
37
46
  import {
38
47
  apiInterceptor,
39
48
  httpContextInterceptor,
40
49
  spinnerInterceptor,
50
+ provideHttpContext,
41
51
  } from '@acontplus/ng-infrastructure';
42
52
 
43
- // In app.config.ts
44
53
  export const appConfig: ApplicationConfig = {
45
54
  providers: [
46
55
  provideHttpClient(
47
- withInterceptors([apiInterceptor, spinnerInterceptor, httpContextInterceptor]),
56
+ withInterceptors([
57
+ httpContextInterceptor, // 1st: URL resolution, headers, Accept-Language, 401 refresh
58
+ apiInterceptor, // 2nd: response normalization, localized toasts, Bearer tokens
59
+ spinnerInterceptor, // 3rd: loading indicator
60
+ // csrfInterceptor, // 4th: CSRF tokens (from @acontplus/ng-auth)
61
+ ]),
48
62
  ),
63
+ provideHttpContext({
64
+ enableLanguageHeader: true, // sends Accept-Language header
65
+ }),
49
66
  ],
50
67
  };
51
68
  ```
52
69
 
70
+ `provideHttpContext()` configures the `httpContextInterceptor`. Enable features
71
+ matching your backend contract:
72
+
73
+ ```typescript
74
+ provideHttpContext({
75
+ enableLanguageHeader: true, // add Accept-Language header (default: true)
76
+ enableCorrelationTracking: true, // add Correlation-Id header (default: true)
77
+ includeAuthToken: true, // add Authorization: Bearer header (default: true)
78
+ baseUrlInjection: true, // prepend apiBaseUrl (default: true)
79
+ refreshTokenCallback: () => authService.refreshToken(), // 401 refresh handler
80
+ logoutCallback: () => authService.logout(), // failed refresh handler
81
+ });
82
+ ```
83
+
53
84
  ## HTTP Interceptors
54
85
 
55
86
  ### API Interceptor
56
87
 
57
- Handles API request/response transformation and error handling.
88
+ Handles API request/response transformation, **localized error messages**, and
89
+ notification display. Uses `@acontplus/core` `getLocalizedErrorMessage()` and
90
+ `getLocalizedAppMessage()` to show translated notifications based on the current
91
+ language.
58
92
 
59
93
  ```typescript
60
94
  import { apiInterceptor } from '@acontplus/ng-infrastructure';
61
95
 
62
96
  // Automatically handles:
63
- // - Request/response transformation
64
- // - Error standardization
65
- // - API base URL configuration
66
- // - Response format normalization
97
+ // - Response standardization into ApiResponse envelope
98
+ // - Data unwrapping for success/warning responses
99
+ // - Localized toast notifications via LanguageInfo + AppMessageKey
100
+ // - Individual ApiError entries translated by error code
101
+ // - HTTP-level errors (0, 5xx) with localized titles and messages
102
+ // - SKIP_NOTIFICATION / SHOW_NOTIFICATIONS HttpContext tokens
103
+ ```
104
+
105
+ #### HttpContext Tokens
106
+
107
+ ```typescript
108
+ import { HttpContext } from '@angular/common/http';
109
+ import { SKIP_NOTIFICATION, SHOW_NOTIFICATIONS } from '@acontplus/ng-infrastructure';
110
+
111
+ // Suppress all toast notifications for this request
112
+ this.http.get('/api/health', { context: new HttpContext().set(SKIP_NOTIFICATION, true) });
113
+
114
+ // Force-show notifications even on GET requests or excluded URLs
115
+ this.http.get('/api/export', { context: new HttpContext().set(SHOW_NOTIFICATIONS, true) });
67
116
  ```
68
117
 
69
118
  ### HTTP Context Interceptor
70
119
 
71
- Manages HTTP context and correlation IDs for request tracing.
120
+ Adds standard headers (`Correlation-Id`, `Tenant-Id`, `Request-Id`, `Accept-Language`,
121
+ `Client-Version`, `Client-Id`), resolves request URLs against `apiBaseUrl`, and
122
+ handles 401 token refresh, 403 forbidden, and 429 rate-limiting events.
72
123
 
73
124
  ```typescript
74
125
  import { httpContextInterceptor } from '@acontplus/ng-infrastructure';
75
126
 
76
127
  // Automatically adds:
77
- // - Correlation IDs to requests
78
- // - Request context information
79
- // - Tenant information
80
- // - Request metadata
128
+ // - Accept-Language: <bcp47 tag> (from LanguageInfo)
129
+ // - Correlation-Id, Request-Id, Timestamp
130
+ // - Tenant-Id (when multi-tenancy is enabled)
131
+ // - Client-Version, Client-Id
132
+ // - Authorization: Bearer (when auth token available)
133
+ // - Content-Type: application/json (for POST/PUT/PATCH with JSON body)
81
134
  ```
82
135
 
136
+ #### `Accept-Language` header
137
+
138
+ The interceptor resolves the current language from `LanguageInfo` (priority:
139
+ JWT `locale` claim → `sessionStorage` cache → browser `navigator.languages`)
140
+ and sends the corresponding BCP47 tag. Disable with `enableLanguageHeader: false`.
141
+
142
+ #### API base URL convention
143
+
144
+ Configure `Environment.apiBaseUrl` without a trailing slash. The interceptor safely joins it
145
+ with request paths, accepting paths with or without a leading slash.
146
+
147
+ ```typescript
148
+ apiBaseUrl: 'https://api.example.com/gateway';
149
+ ```
150
+
151
+ For example, both `auth/login` and `/auth/login` resolve to
152
+ `https://api.example.com/gateway/auth/login`. Absolute and protocol-relative URLs are left
153
+ unchanged. The interceptor deliberately does not use `new URL(path, baseUrl)`: it joins an API
154
+ prefix and endpoint rather than resolving an RFC 3986 relative reference, which could discard a
155
+ base path such as `/gateway`.
156
+
83
157
  ### Spinner Interceptor
84
158
 
85
159
  Manages loading indicators during HTTP operations.
@@ -103,7 +177,8 @@ Abstract base class for HTTP-based data access.
103
177
  ```typescript
104
178
  import { BaseHttpRepository } from '@acontplus/ng-infrastructure';
105
179
  import { Injectable } from '@angular/core';
106
- import { HttpClient } from '@angular/common/http';
180
+ import { Observable } from 'rxjs';
181
+ import { RepositoryConfig } from '@acontplus/core';
107
182
 
108
183
  interface User {
109
184
  id: number;
@@ -112,17 +187,15 @@ interface User {
112
187
  }
113
188
 
114
189
  @Injectable({ providedIn: 'root' })
115
- export class UserRepository extends BaseHttpRepository<User> {
116
- constructor(http: HttpClient) {
117
- super(http, '/api/users');
190
+ export class UserRepository extends BaseHttpRepository {
191
+ protected override config: RepositoryConfig = {
192
+ baseUrl: '/api',
193
+ endpoint: 'users',
194
+ };
195
+
196
+ findAll(): Observable<User[]> {
197
+ return this.get<User[]>();
118
198
  }
119
-
120
- // Inherits common CRUD operations:
121
- // - getById(id: number)
122
- // - getAll()
123
- // - create(entity: User)
124
- // - update(id: number, entity: Partial<User>)
125
- // - delete(id: number)
126
199
  }
127
200
  ```
128
201
 
@@ -130,21 +203,9 @@ export class UserRepository extends BaseHttpRepository<User> {
130
203
 
131
204
  Generic repository implementation with type safety.
132
205
 
133
- ```typescript
134
- import { GenericRepository } from '@acontplus/ng-infrastructure';
135
-
136
- @Injectable({ providedIn: 'root' })
137
- export class CustomerRepository extends GenericRepository<Customer, number> {
138
- constructor(http: HttpClient) {
139
- super(http, '/api/customers');
140
- }
141
-
142
- // Custom business methods
143
- async findByEmail(email: string): Promise<Customer[]> {
144
- return this.http.get<Customer[]>(`${this.baseUrl}/search?email=${email}`).toPromise() || [];
145
- }
146
- }
147
- ```
206
+ `GenericRepository` receives its `RepositoryConfig` through `REPOSITORY_CONFIG`.
207
+ For application services, prefer `RepositoryFactory` when a reusable CRUD
208
+ repository is sufficient.
148
209
 
149
210
  ### Repository Factory
150
211
 
@@ -152,17 +213,18 @@ Factory pattern for creating repository instances.
152
213
 
153
214
  ```typescript
154
215
  import { RepositoryFactory } from '@acontplus/ng-infrastructure';
216
+ import { Injectable } from '@angular/core';
155
217
 
156
218
  @Injectable({ providedIn: 'root' })
157
219
  export class DataService {
158
220
  constructor(private repositoryFactory: RepositoryFactory) {}
159
221
 
160
222
  getUserRepository() {
161
- return this.repositoryFactory.create<User>('users');
223
+ return this.repositoryFactory.create<User>({ baseUrl: '/api', endpoint: 'users' });
162
224
  }
163
225
 
164
226
  getCustomerRepository() {
165
- return this.repositoryFactory.create<Customer>('customers');
227
+ return this.repositoryFactory.create<Customer>({ baseUrl: '/api', endpoint: 'customers' });
166
228
  }
167
229
  }
168
230
  ```
@@ -174,41 +236,87 @@ export class DataService {
174
236
  Adapter for Angular HTTP client integration.
175
237
 
176
238
  ```typescript
239
+ import { HttpClient } from '@angular/common/http';
240
+ import { inject, Injectable } from '@angular/core';
177
241
  import { AngularHttpAdapter } from '@acontplus/ng-infrastructure';
178
242
 
179
243
  @Injectable({ providedIn: 'root' })
180
244
  export class ExternalApiService {
181
- constructor(private httpAdapter: AngularHttpAdapter) {}
245
+ private readonly httpAdapter = new AngularHttpAdapter(
246
+ inject(HttpClient),
247
+ 'https://api.example.com',
248
+ );
182
249
 
183
- async fetchExternalData(url: string): Promise<any> {
184
- return this.httpAdapter.get(url);
250
+ async fetchExternalData(): Promise<unknown> {
251
+ return this.httpAdapter.get('data');
185
252
  }
186
253
 
187
- async postData(url: string, data: any): Promise<any> {
188
- return this.httpAdapter.post(url, data);
254
+ async postData(data: unknown): Promise<unknown> {
255
+ return this.httpAdapter.post('data', data);
189
256
  }
190
257
  }
191
258
  ```
192
259
 
193
260
  ## Core Services
194
261
 
262
+ ### LanguageInfo Service
263
+
264
+ Detects and manages the current application language with a priority chain:
265
+ JWT `locale` claim → `sessionStorage` cache → browser `navigator.languages`.
266
+
267
+ ```typescript
268
+ import { LanguageInfo } from '@acontplus/ng-infrastructure';
269
+ import { Language, languageToBcp47 } from '@acontplus/core';
270
+ import { Injectable } from '@angular/core';
271
+
272
+ @Injectable({ providedIn: 'root' })
273
+ export class LocaleService {
274
+ constructor(private languageInfo: LanguageInfo) {}
275
+
276
+ getCurrentLanguage(): Language {
277
+ return this.languageInfo.getCurrentLanguage();
278
+ }
279
+
280
+ getBcp47Tag(): string {
281
+ return this.languageInfo.getBcp47Tag();
282
+ }
283
+
284
+ setLanguage(language: Language): void {
285
+ this.languageInfo.setLanguage(language);
286
+ // persists to sessionStorage automatically
287
+ }
288
+ }
289
+ ```
290
+
291
+ The `httpContextInterceptor` consumes `LanguageInfo` to set the `Accept-Language`
292
+ header on every request. The `apiInterceptor` uses it for localized toast
293
+ notifications.
294
+
295
+ **Signals**:
296
+
297
+ ```typescript
298
+ const lang = this.languageInfo.language(); // Signal<Language>
299
+ const tag = this.languageInfo.bcp47Tag(); // Signal<string>
300
+ ```
301
+
195
302
  ### Core Config Service
196
303
 
197
304
  Manages application configuration and settings.
198
305
 
199
306
  ```typescript
200
307
  import { CoreConfigService } from '@acontplus/ng-infrastructure';
308
+ import { Injectable } from '@angular/core';
201
309
 
202
310
  @Injectable({ providedIn: 'root' })
203
311
  export class AppService {
204
312
  constructor(private configService: CoreConfigService) {}
205
313
 
206
314
  getApiBaseUrl(): string {
207
- return this.configService.getApiBaseUrl();
315
+ return this.configService.getConfig().apiBaseUrl;
208
316
  }
209
317
 
210
- getTimeout(): number {
211
- return this.configService.getTimeout();
318
+ getTimeout(): number | undefined {
319
+ return this.configService.get('apiTimeout');
212
320
  }
213
321
  }
214
322
  ```
@@ -219,17 +327,18 @@ Handles correlation IDs for distributed request tracing.
219
327
 
220
328
  ```typescript
221
329
  import { CorrelationInfo } from '@acontplus/ng-infrastructure';
330
+ import { Injectable } from '@angular/core';
222
331
 
223
332
  @Injectable({ providedIn: 'root' })
224
333
  export class TrackingService {
225
334
  constructor(private correlationInfo: CorrelationInfo) {}
226
335
 
227
336
  getCurrentCorrelationId(): string {
228
- return this.correlationInfo.getCorrelationId();
337
+ return this.correlationInfo.getId();
229
338
  }
230
339
 
231
- generateNewCorrelationId(): string {
232
- return this.correlationInfo.generateCorrelationId();
340
+ resetCorrelationId(): void {
341
+ this.correlationInfo.resetCorrelationId();
233
342
  }
234
343
  }
235
344
  ```
@@ -240,6 +349,7 @@ Structured logging with correlation tracking.
240
349
 
241
350
  ```typescript
242
351
  import { LoggingService } from '@acontplus/ng-infrastructure';
352
+ import { Injectable } from '@angular/core';
243
353
 
244
354
  @Injectable({ providedIn: 'root' })
245
355
  export class BusinessService {
@@ -266,8 +376,13 @@ export class BusinessService {
266
376
 
267
377
  Abstract base class for business logic encapsulation.
268
378
 
379
+ `Command` and `Query` capture `LoggingService` while Angular creates the
380
+ subclass. Register concrete subclasses with Angular DI; do not instantiate
381
+ them outside an injection context.
382
+
269
383
  ```typescript
270
384
  import { BaseUseCase } from '@acontplus/ng-infrastructure';
385
+ import { Observable } from 'rxjs';
271
386
 
272
387
  export class CreateUserUseCase extends BaseUseCase<CreateUserCommand, User> {
273
388
  constructor(
@@ -277,20 +392,17 @@ export class CreateUserUseCase extends BaseUseCase<CreateUserCommand, User> {
277
392
  super();
278
393
  }
279
394
 
280
- async execute(command: CreateUserCommand): Promise<User> {
395
+ execute(command: CreateUserCommand): Observable<User> {
281
396
  this.logger.info('Creating user', { email: command.email });
282
397
 
283
398
  // Validation
284
399
  this.validateCommand(command);
285
400
 
286
401
  // Business logic
287
- const user = await this.userRepository.create({
402
+ return this.userRepository.create({
288
403
  name: command.name,
289
404
  email: command.email,
290
405
  });
291
-
292
- this.logger.info('User created successfully', { userId: user.id });
293
- return user;
294
406
  }
295
407
 
296
408
  private validateCommand(command: CreateUserCommand): void {
@@ -306,42 +418,34 @@ export class CreateUserUseCase extends BaseUseCase<CreateUserCommand, User> {
306
418
  Separation of read and write operations.
307
419
 
308
420
  ```typescript
309
- import { Commands, Queries } from '@acontplus/ng-infrastructure';
421
+ import { Command, Query } from '@acontplus/ng-infrastructure';
422
+ import { Observable } from 'rxjs';
310
423
 
311
424
  // Command for write operations
312
- export class UpdateUserCommand extends Commands.BaseCommand {
313
- constructor(
314
- public readonly userId: number,
315
- public readonly name: string,
316
- public readonly email: string,
317
- ) {
318
- super();
319
- }
425
+ export interface UpdateUserRequest {
426
+ userId: number;
427
+ name: string;
428
+ email: string;
320
429
  }
321
430
 
322
- // Query for read operations
323
- export class GetUserQuery extends Queries.BaseQuery<User> {
324
- constructor(public readonly userId: number) {
431
+ export class UpdateUserCommand extends Command<UpdateUserRequest, User> {
432
+ constructor(private readonly userRepository: UserRepository) {
325
433
  super();
326
434
  }
327
- }
328
435
 
329
- // Usage
330
- @Injectable({ providedIn: 'root' })
331
- export class UserService {
332
- constructor(
333
- private updateUserUseCase: UpdateUserUseCase,
334
- private getUserUseCase: GetUserUseCase,
335
- ) {}
436
+ protected executeInternal(request: UpdateUserRequest): Observable<User> {
437
+ return this.userRepository.update(request.userId, request);
438
+ }
439
+ }
336
440
 
337
- async updateUser(userId: number, name: string, email: string): Promise<User> {
338
- const command = new UpdateUserCommand(userId, name, email);
339
- return this.updateUserUseCase.execute(command);
441
+ // Query for read operations
442
+ export class GetUserQuery extends Query<number, User> {
443
+ constructor(private readonly userRepository: UserRepository) {
444
+ super();
340
445
  }
341
446
 
342
- async getUser(userId: number): Promise<User> {
343
- const query = new GetUserQuery(userId);
344
- return this.getUserUseCase.execute(query);
447
+ protected executeInternal(userId: number): Observable<User> {
448
+ return this.userRepository.getById(userId);
345
449
  }
346
450
  }
347
451
  ```