@acontplus/ng-infrastructure 1.0.2 → 1.0.4
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 +75 -6
- package/fesm2022/acontplus-ng-infrastructure.mjs +130 -92
- package/fesm2022/acontplus-ng-infrastructure.mjs.map +1 -1
- package/index.d.ts +18 -14
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @acontplus/ng-infrastructure
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Angular infrastructure library for AcontPlus applications, providing HTTP
|
|
4
|
+
interceptors, repositories, adapters, and core services for robust application
|
|
5
|
+
architecture.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -10,17 +12,84 @@ npm install @acontplus/ng-infrastructure
|
|
|
10
12
|
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
|
-
- HTTP Interceptors
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
15
|
+
- **HTTP Interceptors**: API handling, HTTP context management, and
|
|
16
|
+
spinner/loading indicators
|
|
17
|
+
- **Repositories**: Generic and specific data access repositories (base HTTP,
|
|
18
|
+
user repository)
|
|
19
|
+
- **Adapters**: External integration adapters (Angular HTTP adapter)
|
|
20
|
+
- **Services**: Core configuration, correlation ID management, logging, and
|
|
21
|
+
tenant services
|
|
22
|
+
- **Use Cases**: Base use case patterns with commands and queries
|
|
23
|
+
- **Interfaces**: Token provider interfaces for authentication
|
|
24
|
+
- **TypeScript Support**: Full type safety with comprehensive TypeScript
|
|
25
|
+
definitions
|
|
26
|
+
|
|
27
|
+
## Interceptors
|
|
28
|
+
|
|
29
|
+
### API Interceptor
|
|
30
|
+
|
|
31
|
+
Handles API requests and responses.
|
|
32
|
+
|
|
33
|
+
### HTTP Context Interceptor
|
|
34
|
+
|
|
35
|
+
Manages HTTP context for requests.
|
|
36
|
+
|
|
37
|
+
### Spinner Interceptor
|
|
38
|
+
|
|
39
|
+
Manages loading spinners during HTTP operations.
|
|
40
|
+
|
|
41
|
+
## Repositories
|
|
42
|
+
|
|
43
|
+
### Base HTTP Repository
|
|
44
|
+
|
|
45
|
+
Base class for HTTP-based data access.
|
|
46
|
+
|
|
47
|
+
### Generic Repository
|
|
48
|
+
|
|
49
|
+
Generic repository implementation.
|
|
50
|
+
|
|
51
|
+
### User Repository
|
|
52
|
+
|
|
53
|
+
Specific repository for user data.
|
|
54
|
+
|
|
55
|
+
### Repository Factory
|
|
56
|
+
|
|
57
|
+
Factory for creating repository instances.
|
|
58
|
+
|
|
59
|
+
## Services
|
|
60
|
+
|
|
61
|
+
### Core Config Service
|
|
62
|
+
|
|
63
|
+
Manages core application configuration.
|
|
64
|
+
|
|
65
|
+
### Correlation Service
|
|
66
|
+
|
|
67
|
+
Handles correlation IDs for request tracing.
|
|
68
|
+
|
|
69
|
+
### Logging Service
|
|
70
|
+
|
|
71
|
+
Provides logging functionality.
|
|
72
|
+
|
|
73
|
+
### Tenant Service
|
|
74
|
+
|
|
75
|
+
Manages multi-tenant configurations.
|
|
17
76
|
|
|
18
77
|
## Usage
|
|
19
78
|
|
|
20
79
|
Import the desired modules and services in your Angular application.
|
|
21
80
|
|
|
22
81
|
```typescript
|
|
23
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
apiInterceptor,
|
|
84
|
+
spinnerInterceptor,
|
|
85
|
+
} from '@acontplus/ng-infrastructure';
|
|
86
|
+
|
|
87
|
+
// In app.config.ts
|
|
88
|
+
export const appConfig: ApplicationConfig = {
|
|
89
|
+
providers: [
|
|
90
|
+
provideHttpClient(withInterceptors([apiInterceptor, spinnerInterceptor])),
|
|
91
|
+
],
|
|
92
|
+
};
|
|
24
93
|
```
|
|
25
94
|
|
|
26
95
|
## Running unit tests
|
|
@@ -24,7 +24,7 @@ const apiInterceptor = (req, next) => {
|
|
|
24
24
|
const token = tokenProvider?.getToken();
|
|
25
25
|
if (token) {
|
|
26
26
|
modifiedReq = req.clone({
|
|
27
|
-
setHeaders: { Authorization: `Bearer ${token}` }
|
|
27
|
+
setHeaders: { Authorization: `Bearer ${token}` },
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
return next(modifiedReq).pipe(
|
|
@@ -120,7 +120,9 @@ function handleToastNotifications(response, notificationService, req) {
|
|
|
120
120
|
if (skipNotification)
|
|
121
121
|
return;
|
|
122
122
|
// Dynamic handling: Use show() for runtime type selection
|
|
123
|
-
if (response.message &&
|
|
123
|
+
if (response.message &&
|
|
124
|
+
showNotifications &&
|
|
125
|
+
['success', 'warning', 'error'].includes(response.status)) {
|
|
124
126
|
notificationService.show({
|
|
125
127
|
type: response.status,
|
|
126
128
|
message: response.message,
|
|
@@ -242,8 +244,88 @@ function getCriticalErrorMessage(error) {
|
|
|
242
244
|
return error.error?.message || error.message || 'An unexpected error occurred';
|
|
243
245
|
}
|
|
244
246
|
|
|
247
|
+
class LoggingService {
|
|
248
|
+
environment = inject(ENVIRONMENT);
|
|
249
|
+
log(level, message, context) {
|
|
250
|
+
if (this.environment.isProduction) {
|
|
251
|
+
// Production logging (e.g., to external service)
|
|
252
|
+
this.logToExternalService(level, message, context);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Development logging - only log in development mode
|
|
256
|
+
if (!this.environment.isProduction) {
|
|
257
|
+
console[level](`[${level.toUpperCase()}] ${message}`, context);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
info(message, context) {
|
|
262
|
+
this.log('info', message, context);
|
|
263
|
+
}
|
|
264
|
+
warn(message, context) {
|
|
265
|
+
this.log('warn', message, context);
|
|
266
|
+
}
|
|
267
|
+
error(message, context) {
|
|
268
|
+
this.log('error', message, context);
|
|
269
|
+
}
|
|
270
|
+
// HTTP Request Logging
|
|
271
|
+
logHttpRequest(log) {
|
|
272
|
+
this.info(`HTTP Request - ${log.method} ${log.url}`, {
|
|
273
|
+
requestId: log.requestId,
|
|
274
|
+
correlationId: log.correlationId,
|
|
275
|
+
tenantId: log.tenantId,
|
|
276
|
+
headers: log.headers,
|
|
277
|
+
isCustomUrl: log.isCustomUrl,
|
|
278
|
+
timestamp: log.timestamp,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// HTTP Error Logging
|
|
282
|
+
logHttpError(error) {
|
|
283
|
+
this.error(`HTTP Error - ${error.method} ${error.url}`, {
|
|
284
|
+
status: error.status,
|
|
285
|
+
statusText: error.statusText,
|
|
286
|
+
requestId: error.requestId,
|
|
287
|
+
correlationId: error.correlationId,
|
|
288
|
+
tenantId: error.tenantId,
|
|
289
|
+
errorDetails: error.errorDetails,
|
|
290
|
+
environment: error.environment,
|
|
291
|
+
timestamp: error.timestamp,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Network Error Logging
|
|
295
|
+
logNetworkError(correlationId) {
|
|
296
|
+
this.error('Network connection failed', {
|
|
297
|
+
type: 'network-error',
|
|
298
|
+
correlationId,
|
|
299
|
+
userAgent: navigator.userAgent,
|
|
300
|
+
online: navigator.onLine,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Rate Limit Error Logging
|
|
304
|
+
logRateLimitError(correlationId, url) {
|
|
305
|
+
this.warn('Rate limit exceeded', {
|
|
306
|
+
type: 'rate-limit-error',
|
|
307
|
+
correlationId,
|
|
308
|
+
url,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
logToExternalService(_level, _message, _context) {
|
|
312
|
+
// Implement external logging service integration
|
|
313
|
+
// e.g., Sentry, LogRocket, etc.
|
|
314
|
+
// This is a placeholder for production logging implementation
|
|
315
|
+
}
|
|
316
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
317
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, providedIn: 'root' });
|
|
318
|
+
}
|
|
319
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, decorators: [{
|
|
320
|
+
type: Injectable,
|
|
321
|
+
args: [{
|
|
322
|
+
providedIn: 'root',
|
|
323
|
+
}]
|
|
324
|
+
}] });
|
|
325
|
+
|
|
245
326
|
class TenantService {
|
|
246
327
|
tenantId = null;
|
|
328
|
+
logger = inject(LoggingService);
|
|
247
329
|
getTenantId() {
|
|
248
330
|
if (!this.tenantId) {
|
|
249
331
|
// Get from localStorage, sessionStorage, or JWT token
|
|
@@ -266,7 +348,7 @@ class TenantService {
|
|
|
266
348
|
this.setTenantId(tenantId);
|
|
267
349
|
}
|
|
268
350
|
handleForbidden() {
|
|
269
|
-
|
|
351
|
+
this.logger.error('Access forbidden for tenant:', this.tenantId);
|
|
270
352
|
// Redirect to tenant selection or show error message
|
|
271
353
|
// this.router.navigate(['/tenant-access-denied']);
|
|
272
354
|
}
|
|
@@ -298,7 +380,6 @@ class CorrelationService {
|
|
|
298
380
|
getOrCreateCorrelationId() {
|
|
299
381
|
if (!this.correlationId()) {
|
|
300
382
|
// Try to get from sessionStorage first (for page refreshes)
|
|
301
|
-
// @ts-ignore
|
|
302
383
|
const id = sessionStorage.getItem(this.CORRELATION_KEY) || v4();
|
|
303
384
|
this.correlationId.set(id);
|
|
304
385
|
sessionStorage.setItem(this.CORRELATION_KEY, id);
|
|
@@ -326,83 +407,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
326
407
|
}]
|
|
327
408
|
}] });
|
|
328
409
|
|
|
329
|
-
class LoggingService {
|
|
330
|
-
environment = inject(ENVIRONMENT);
|
|
331
|
-
log(level, message, context) {
|
|
332
|
-
if (this.environment.isProduction) {
|
|
333
|
-
// Production logging (e.g., to external service)
|
|
334
|
-
this.logToExternalService(level, message, context);
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
// Development logging
|
|
338
|
-
console[level](`[${level.toUpperCase()}] ${message}`, context);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
info(message, context) {
|
|
342
|
-
this.log('info', message, context);
|
|
343
|
-
}
|
|
344
|
-
warn(message, context) {
|
|
345
|
-
this.log('warn', message, context);
|
|
346
|
-
}
|
|
347
|
-
error(message, context) {
|
|
348
|
-
this.log('error', message, context);
|
|
349
|
-
}
|
|
350
|
-
// HTTP Request Logging
|
|
351
|
-
logHttpRequest(log) {
|
|
352
|
-
this.info(`HTTP Request - ${log.method} ${log.url}`, {
|
|
353
|
-
requestId: log.requestId,
|
|
354
|
-
correlationId: log.correlationId,
|
|
355
|
-
tenantId: log.tenantId,
|
|
356
|
-
headers: log.headers,
|
|
357
|
-
isCustomUrl: log.isCustomUrl,
|
|
358
|
-
timestamp: log.timestamp,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
// HTTP Error Logging
|
|
362
|
-
logHttpError(error) {
|
|
363
|
-
this.error(`HTTP Error - ${error.method} ${error.url}`, {
|
|
364
|
-
status: error.status,
|
|
365
|
-
statusText: error.statusText,
|
|
366
|
-
requestId: error.requestId,
|
|
367
|
-
correlationId: error.correlationId,
|
|
368
|
-
tenantId: error.tenantId,
|
|
369
|
-
errorDetails: error.errorDetails,
|
|
370
|
-
environment: error.environment,
|
|
371
|
-
timestamp: error.timestamp,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
// Network Error Logging
|
|
375
|
-
logNetworkError(correlationId) {
|
|
376
|
-
this.error('Network connection failed', {
|
|
377
|
-
type: 'network-error',
|
|
378
|
-
correlationId,
|
|
379
|
-
userAgent: navigator.userAgent,
|
|
380
|
-
online: navigator.onLine,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
// Rate Limit Error Logging
|
|
384
|
-
logRateLimitError(correlationId, url) {
|
|
385
|
-
this.warn('Rate limit exceeded', {
|
|
386
|
-
type: 'rate-limit-error',
|
|
387
|
-
correlationId,
|
|
388
|
-
url,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
logToExternalService(level, message, context) {
|
|
392
|
-
// Implement external logging service integration
|
|
393
|
-
// e.g., Sentry, LogRocket, etc.
|
|
394
|
-
// This is a placeholder for production logging implementation
|
|
395
|
-
}
|
|
396
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
397
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, providedIn: 'root' });
|
|
398
|
-
}
|
|
399
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: LoggingService, decorators: [{
|
|
400
|
-
type: Injectable,
|
|
401
|
-
args: [{
|
|
402
|
-
providedIn: 'root',
|
|
403
|
-
}]
|
|
404
|
-
}] });
|
|
405
|
-
|
|
406
410
|
// HTTP Context tokens
|
|
407
411
|
const CUSTOM_URL = new HttpContextToken(() => false);
|
|
408
412
|
const SKIP_CONTEXT_HEADERS = new HttpContextToken(() => false);
|
|
@@ -546,7 +550,7 @@ const httpContextInterceptor = (req, next) => {
|
|
|
546
550
|
// Retry the original request with new token
|
|
547
551
|
const retryReq = req.clone({
|
|
548
552
|
url: finalUrl,
|
|
549
|
-
setHeaders: { ...headers,
|
|
553
|
+
setHeaders: { ...headers, Authorization: `Bearer ${newTokens.token}` },
|
|
550
554
|
});
|
|
551
555
|
return next(retryReq);
|
|
552
556
|
}), catchError$1(refreshError => {
|
|
@@ -615,9 +619,9 @@ const httpContextInterceptor = (req, next) => {
|
|
|
615
619
|
// Handle specific error scenarios
|
|
616
620
|
switch (error.status) {
|
|
617
621
|
case 401:
|
|
618
|
-
|
|
622
|
+
loggingService.error('Unauthorized access - token expired or invalid');
|
|
619
623
|
// Note: Token clearing should be handled by the auth service, not infrastructure
|
|
620
|
-
router.navigate([
|
|
624
|
+
router.navigate([environment.loginRoute]);
|
|
621
625
|
break;
|
|
622
626
|
case 403:
|
|
623
627
|
tenantService.handleForbidden();
|
|
@@ -693,7 +697,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.2", ngImpor
|
|
|
693
697
|
*/
|
|
694
698
|
const spinnerInterceptor = (req, next) => {
|
|
695
699
|
// Track active requests requiring spinner
|
|
696
|
-
console.log(requests);
|
|
697
700
|
const activeRequests = inject(ActiveRequestsTracker);
|
|
698
701
|
const overlayService = inject(OverlayService);
|
|
699
702
|
// Skip spinner if disabled for this request
|
|
@@ -864,16 +867,30 @@ class UserRepository {
|
|
|
864
867
|
decodedToken['email'] ??
|
|
865
868
|
decodedToken['sub'] ??
|
|
866
869
|
decodedToken['user_id'];
|
|
867
|
-
const
|
|
870
|
+
const displayName = decodedToken['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'] ??
|
|
871
|
+
decodedToken['displayName'] ??
|
|
872
|
+
decodedToken['display_name'] ??
|
|
868
873
|
decodedToken['name'] ??
|
|
869
|
-
decodedToken['given_name']
|
|
870
|
-
|
|
874
|
+
decodedToken['given_name'];
|
|
875
|
+
const name = decodedToken['name'] ?? displayName;
|
|
871
876
|
if (!email) {
|
|
872
877
|
return null;
|
|
873
878
|
}
|
|
874
879
|
const userData = {
|
|
875
880
|
email: email.toString(),
|
|
876
|
-
displayName:
|
|
881
|
+
displayName: displayName?.toString() ?? 'Unknown User',
|
|
882
|
+
name: name?.toString(),
|
|
883
|
+
roles: this.extractArrayField(decodedToken, ['roles', 'role']),
|
|
884
|
+
permissions: this.extractArrayField(decodedToken, ['permissions', 'perms']),
|
|
885
|
+
tenantId: decodedToken['tenantId']?.toString() ??
|
|
886
|
+
decodedToken['tenant_id']?.toString() ??
|
|
887
|
+
decodedToken['tenant']?.toString(),
|
|
888
|
+
companyId: decodedToken['companyId']?.toString() ??
|
|
889
|
+
decodedToken['company_id']?.toString() ??
|
|
890
|
+
decodedToken['organizationId']?.toString() ??
|
|
891
|
+
decodedToken['org_id']?.toString(),
|
|
892
|
+
locale: decodedToken['locale']?.toString(),
|
|
893
|
+
timezone: decodedToken['timezone']?.toString() ?? decodedToken['tz']?.toString(),
|
|
877
894
|
};
|
|
878
895
|
return userData;
|
|
879
896
|
}
|
|
@@ -881,6 +898,25 @@ class UserRepository {
|
|
|
881
898
|
return null;
|
|
882
899
|
}
|
|
883
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* Extract array field from decoded token, trying multiple possible field names
|
|
903
|
+
*/
|
|
904
|
+
extractArrayField(decodedToken, fieldNames) {
|
|
905
|
+
for (const fieldName of fieldNames) {
|
|
906
|
+
const value = decodedToken[fieldName];
|
|
907
|
+
if (Array.isArray(value)) {
|
|
908
|
+
return value.map(v => v.toString());
|
|
909
|
+
}
|
|
910
|
+
if (typeof value === 'string') {
|
|
911
|
+
// Handle comma-separated string values
|
|
912
|
+
return value
|
|
913
|
+
.split(',')
|
|
914
|
+
.map(v => v.trim())
|
|
915
|
+
.filter(v => v.length > 0);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
884
920
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UserRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
885
921
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.2", ngImport: i0, type: UserRepository, providedIn: 'root' });
|
|
886
922
|
}
|
|
@@ -1075,7 +1111,7 @@ class BaseUseCase {
|
|
|
1075
1111
|
// Only create commands if you have complex validation logic
|
|
1076
1112
|
class Command extends BaseUseCase {
|
|
1077
1113
|
// Simple validation - override only when needed
|
|
1078
|
-
validate(
|
|
1114
|
+
validate(_request) {
|
|
1079
1115
|
return []; // Return array of error messages
|
|
1080
1116
|
}
|
|
1081
1117
|
execute(request) {
|
|
@@ -1091,7 +1127,8 @@ class Command extends BaseUseCase {
|
|
|
1091
1127
|
}
|
|
1092
1128
|
return this.executeInternal(request).pipe(catchError(error => {
|
|
1093
1129
|
// Log the error for debugging
|
|
1094
|
-
|
|
1130
|
+
const logger = inject(LoggingService);
|
|
1131
|
+
logger.error('An error occurred during command execution:', error);
|
|
1095
1132
|
// Re-throw the error so the caller can handle it
|
|
1096
1133
|
return throwError(() => error);
|
|
1097
1134
|
}));
|
|
@@ -1101,7 +1138,8 @@ class Command extends BaseUseCase {
|
|
|
1101
1138
|
class Query extends BaseUseCase {
|
|
1102
1139
|
execute(request) {
|
|
1103
1140
|
return this.executeInternal(request).pipe(catchError(error => {
|
|
1104
|
-
|
|
1141
|
+
const logger = inject(LoggingService);
|
|
1142
|
+
logger.error('An error occurred during query execution:', error);
|
|
1105
1143
|
return throwError(() => error);
|
|
1106
1144
|
}));
|
|
1107
1145
|
}
|