@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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # @acontplus/ng-infrastructure
2
2
 
3
- Acontplus Angular Infrastructure library for managing HTTP interceptors and repositories.
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 for API handling, authentication, logging, and spinner management
14
- - Repositories for data access
15
- - Adapters for external integrations
16
- - Services for core configuration and logging
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 { apiInterceptor } from '@acontplus/ng-infrastructure';
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 && showNotifications && ['success', 'warning', 'error'].includes(response.status)) {
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
- console.error('Access forbidden for tenant:', this.tenantId);
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, 'Authorization': `Bearer ${newTokens.token}` },
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
- console.error('Unauthorized access - token expired or invalid');
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(['/login']);
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 name = decodedToken['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'] ??
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
- decodedToken['display_name'];
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: name?.toString() ?? 'Unknown User',
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(request) {
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
- console.error('An error occurred during command execution:', error);
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
- console.error('An error occurred during query execution:', error);
1141
+ const logger = inject(LoggingService);
1142
+ logger.error('An error occurred during query execution:', error);
1105
1143
  return throwError(() => error);
1106
1144
  }));
1107
1145
  }