@acontplus/ng-infrastructure 1.0.1 → 1.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/README.md CHANGED
@@ -1,6 +1,6 @@
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 interceptors, repositories, adapters, and core services for robust application architecture.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,17 +10,71 @@ npm install @acontplus/ng-infrastructure
10
10
 
11
11
  ## Features
12
12
 
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
13
+ - **HTTP Interceptors**: API handling, HTTP context management, and spinner/loading indicators
14
+ - **Repositories**: Generic and specific data access repositories (base HTTP, user repository)
15
+ - **Adapters**: External integration adapters (Angular HTTP adapter)
16
+ - **Services**: Core configuration, correlation ID management, logging, and tenant services
17
+ - **Use Cases**: Base use case patterns with commands and queries
18
+ - **Interfaces**: Token provider interfaces for authentication
19
+ - **TypeScript Support**: Full type safety with comprehensive TypeScript definitions
20
+
21
+ ## Interceptors
22
+
23
+ ### API Interceptor
24
+ Handles API requests and responses.
25
+
26
+ ### HTTP Context Interceptor
27
+ Manages HTTP context for requests.
28
+
29
+ ### Spinner Interceptor
30
+ Manages loading spinners during HTTP operations.
31
+
32
+ ## Repositories
33
+
34
+ ### Base HTTP Repository
35
+ Base class for HTTP-based data access.
36
+
37
+ ### Generic Repository
38
+ Generic repository implementation.
39
+
40
+ ### User Repository
41
+ Specific repository for user data.
42
+
43
+ ### Repository Factory
44
+ Factory for creating repository instances.
45
+
46
+ ## Services
47
+
48
+ ### Core Config Service
49
+ Manages core application configuration.
50
+
51
+ ### Correlation Service
52
+ Handles correlation IDs for request tracing.
53
+
54
+ ### Logging Service
55
+ Provides logging functionality.
56
+
57
+ ### Tenant Service
58
+ Manages multi-tenant configurations.
17
59
 
18
60
  ## Usage
19
61
 
20
62
  Import the desired modules and services in your Angular application.
21
63
 
22
64
  ```typescript
23
- import { apiInterceptor } from '@acontplus/ng-infrastructure';
65
+ import { apiInterceptor, spinnerInterceptor } from '@acontplus/ng-infrastructure';
66
+
67
+ // In app.config.ts
68
+ export const appConfig: ApplicationConfig = {
69
+ providers: [
70
+ provideHttpClient(
71
+ withInterceptors([
72
+ apiInterceptor,
73
+ spinnerInterceptor,
74
+ ]),
75
+ ),
76
+ ],
77
+ };
24
78
  ```
25
79
 
26
80
  ## 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
  }
@@ -1069,9 +1105,49 @@ function createCoreConfig(overrides = {}) {
1069
1105
  };
1070
1106
  }
1071
1107
 
1108
+ class BaseUseCase {
1109
+ }
1110
+
1111
+ // Only create commands if you have complex validation logic
1112
+ class Command extends BaseUseCase {
1113
+ // Simple validation - override only when needed
1114
+ validate(_request) {
1115
+ return []; // Return array of error messages
1116
+ }
1117
+ execute(request) {
1118
+ const errors = this.validate(request);
1119
+ if (errors.length > 0) {
1120
+ return throwError(() => ({
1121
+ status: 'error',
1122
+ code: 'VALIDATION_FAILED',
1123
+ message: 'Validation failed',
1124
+ errors: errors.map(msg => ({ code: 'VALIDATION', message: msg })),
1125
+ timestamp: new Date().toISOString(),
1126
+ }));
1127
+ }
1128
+ return this.executeInternal(request).pipe(catchError(error => {
1129
+ // Log the error for debugging
1130
+ const logger = inject(LoggingService);
1131
+ logger.error('An error occurred during command execution:', error);
1132
+ // Re-throw the error so the caller can handle it
1133
+ return throwError(() => error);
1134
+ }));
1135
+ }
1136
+ }
1137
+
1138
+ class Query extends BaseUseCase {
1139
+ execute(request) {
1140
+ return this.executeInternal(request).pipe(catchError(error => {
1141
+ const logger = inject(LoggingService);
1142
+ logger.error('An error occurred during query execution:', error);
1143
+ return throwError(() => error);
1144
+ }));
1145
+ }
1146
+ }
1147
+
1072
1148
  /**
1073
1149
  * Generated bundle index. Do not edit.
1074
1150
  */
1075
1151
 
1076
- export { ActiveRequestsTracker, AngularHttpAdapter, BaseHttpRepository, CORE_CONFIG, CoreConfigService, CorrelationService, GenericRepository, HTTP_CONTEXT_CONFIG, LoggingService, REPOSITORY_CONFIG, RepositoryFactory, SHOW_NOTIFICATIONS, SKIP_NOTIFICATION, SearchableGenericRepository, TOKEN_PROVIDER, UserRepository, apiInterceptor, createCoreConfig, createHttpContextConfig, customUrl, httpContextInterceptor, provideCoreConfig, provideHttpContext, skipContextHeaders, spinnerInterceptor, withCustomHeaders, withoutSpinner };
1152
+ export { ActiveRequestsTracker, AngularHttpAdapter, BaseHttpRepository, BaseUseCase, CORE_CONFIG, Command, CoreConfigService, CorrelationService, GenericRepository, HTTP_CONTEXT_CONFIG, LoggingService, Query, REPOSITORY_CONFIG, RepositoryFactory, SHOW_NOTIFICATIONS, SKIP_NOTIFICATION, SearchableGenericRepository, TOKEN_PROVIDER, UserRepository, apiInterceptor, createCoreConfig, createHttpContextConfig, customUrl, httpContextInterceptor, provideCoreConfig, provideHttpContext, skipContextHeaders, spinnerInterceptor, withCustomHeaders, withoutSpinner };
1077
1153
  //# sourceMappingURL=acontplus-ng-infrastructure.mjs.map