File

libs/ngx-pfe/pfe-service-activator/service/service-activator.service.ts

Description

This service is responsible to handle all configured service activator requests and responses.

Index

Properties
Methods

Constructor

constructor(ngxPfeModuleConfiguration: NgxPfeModuleConfiguration, http: HttpClient, pfeStateService: PfeStateService, pfeNavigationUtilService: PfeNavigationUtilService, pfeConfigurationService: PfeConfigurationService, logger: NgxLoggerService, pfeActionsService: PfeActionsService, pfeConditionsService: PfeConditionsService, pfeTriggerServiceActivatorsService: PfeTriggerServiceActivatorsService)
Parameters :
Name Type Optional
ngxPfeModuleConfiguration NgxPfeModuleConfiguration No
http HttpClient No
pfeStateService PfeStateService No
pfeNavigationUtilService PfeNavigationUtilService No
pfeConfigurationService PfeConfigurationService No
logger NgxLoggerService No
pfeActionsService PfeActionsService No
pfeConditionsService PfeConditionsService No
pfeTriggerServiceActivatorsService PfeTriggerServiceActivatorsService No

Methods

Public handleServiceActivators
handleServiceActivators(serviceActivators: NavServiceActivatorConfigsArray, executeServiceActivatorsSequential?: boolean, httpErrorHandler?: HttpErrorHandler)

Calls all the service activators of a configuration. It either returns a forkJoin observable for all issued service calls (by default) or a concatMap observable if executeServiceActivatorsSequential param is set to true, meaning to execute all the service activators sequentially.

The httpErrorHandler will be called in case of http errors. For serviceActivators that do not use the async option an exception will also be thrown. This exception should not be used for error handling, as it is not thrown for async calls. If the exception is thrown, the navigation is not done. We are aware of the ugliness of this solution and have the following TODO: Improve: Remove error callback and replace with some common async solution

To keep the promise handling of the service activators less complex, spreadWithGlobalServiceActivatorConfig has to be called before handleServiceActivators is called. (with await)

Parameters :
Name Type Optional Description
serviceActivators NavServiceActivatorConfigsArray No
executeServiceActivatorsSequential boolean Yes
httpErrorHandler HttpErrorHandler Yes

will be called in case of http errors.

Static spreadSingleServiceActivatorConfig
spreadSingleServiceActivatorConfig(serviceActivatorConfig: NavServiceActivatorConfigInternally, globalServiceActivators: GlobalServiceActivators)

Applies a global service activator config on to a service activator config. Returns new config if there was a global config or undefined if there was none.

Parameters :
Name Type Optional
serviceActivatorConfig NavServiceActivatorConfigInternally No
globalServiceActivators GlobalServiceActivators No
Static spreadWithGlobalServiceActivatorConfig
spreadWithGlobalServiceActivatorConfig(serviceActivatorConfigsArray: NavServiceActivatorConfigInternally[], globalServiceActivators: GlobalServiceActivators | undefined)
Parameters :
Name Type Optional
serviceActivatorConfigsArray NavServiceActivatorConfigInternally[] No
globalServiceActivators GlobalServiceActivators | undefined No
Returns : void
Public Async spreadWithGlobalServiceActivatorConfig
spreadWithGlobalServiceActivatorConfig(serviceActivatorConfigsArray: NavServiceActivatorConfigInternally[])

Takes in a ServiceActivatorConfigsArray and spreads it on top of a global service activator config, if there is one. The array is changed directly!

Parameters :
Name Type Optional
serviceActivatorConfigsArray NavServiceActivatorConfigInternally[] No
Returns : any
Public Async waitForAsyncServiceActivators
waitForAsyncServiceActivators(pageID: string)
Parameters :
Name Type Optional
pageID string No
Returns : Promise<HttpResponse[]>

Properties

Public serviceActivatorCallInProgress$
Type : BehaviorSubject<boolean>
Default value : new BehaviorSubject<boolean>(false)

Set to true, when a navigation or network call (service activator call) is currently running.

Public serviceActivatorCallInProgressDetails$
Type : Observable<ServiceActivatorProgressDetails[]>
Default value : this._serviceActivatorCallInProgressDetails$.asObservable()
import { NgxLoggerService } from '@allianz/ngx-logger';
import { HttpClient, HttpErrorResponse, HttpEvent, HttpEventType, HttpParams, HttpRequest, HttpResponse } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import jsonpath from 'jsonpath';
import { BehaviorSubject, EMPTY, Observable, from, lastValueFrom, forkJoin as observableForkJoin } from 'rxjs';
import { catchError, concatMap, filter, tap } from 'rxjs/operators';
import { GlobalServiceActivators } from '../../models/navigation-config.model';
import {
  NGX_PFE_CONFIGURATION,
  NgxPfeModuleConfiguration,
  PFE_DEFAULT_REQUEST_BODY,
} from '../../models/ngx-pfe-module-configuration.model';
import { NgxPfeConfig } from '../../models/ngx-pfe-page-config.model';
import { PfeUserInputState } from '../../models/pfe-state/user-input-state.model';
import { UnknownObject } from '../../models/unknown-object.model';
import { PfeActionsService } from '../../pfe-actions/pfe-actions.service';
import { PfeTriggerServiceActivatorsType } from '../../pfe-actions/trigger-service-activators/trigger-service-activators.model';
import { PfeTriggerServiceActivatorsService } from '../../pfe-actions/trigger-service-activators/trigger-service-activators.service';
import { PfeConditionsService } from '../../pfe-conditions/public-api';
import { PfeUtilService } from '../../pfe-util/services/util.service';
import { PfeConfigurationService } from '../../services/pfe-config-service/config-service.service';
import { PfeNavigationUtilService } from '../../services/pfe-navigation-service/navigation-util.service';
import { PfeStateService } from '../../services/pfe-state-service/state.service';
import { clone } from '../../util/clone';
import { convertHTTPHeaders } from '../../util/convert-http-response-headers';
import {
  ActiveAsyncServiceActivatorCallForPage,
  ErrorCase,
  ErrorCaseConditionsState,
  HttpParamsOptionsFromObject,
  NavServiceActivatorConfig,
  NavServiceActivatorConfigInternally,
  NavServiceActivatorConfigsArray,
  PathParams,
  ServiceActivatorDataMapping,
  ServiceActivatorProgressDetails,
} from './service-activator.model';

/**
 * Handles a serviceActivator call error.
 */
export type HttpErrorHandler = (error: HttpErrorResponse, serviceActivatorConfig?: NavServiceActivatorConfig) => Promise<void | boolean>;

/**
 * This service is responsible to handle all configured service activator requests and responses.
 */
@Injectable()
export class PfeServiceActivatorService {
  /**
   * Set to true, when a navigation or network call (service activator call) is currently running.
   */
  public serviceActivatorCallInProgress$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  protected baseURL: string;
  // A list of active async calls per pageId:
  protected activeAsyncServiceActivatorCallsPerPage: Map<string, ActiveAsyncServiceActivatorCallForPage[]> = new Map<
    string,
    ActiveAsyncServiceActivatorCallForPage[]
  >();

  protected getValueByExpression: (obj: UnknownObject, p: string) => unknown;

  private _serviceActivatorCallInProgressDetails$: BehaviorSubject<ServiceActivatorProgressDetails[]> = new BehaviorSubject<
    ServiceActivatorProgressDetails[]
  >([]);
  // eslint-disable-next-line @typescript-eslint/member-ordering
  public serviceActivatorCallInProgressDetails$: Observable<ServiceActivatorProgressDetails[]> =
    this._serviceActivatorCallInProgressDetails$.asObservable();

  constructor(
    @Inject(NGX_PFE_CONFIGURATION) protected ngxPfeModuleConfiguration: NgxPfeModuleConfiguration,
    protected http: HttpClient,
    protected pfeStateService: PfeStateService,
    protected pfeNavigationUtilService: PfeNavigationUtilService,
    protected pfeConfigurationService: PfeConfigurationService,
    protected logger: NgxLoggerService,
    protected pfeActionsService: PfeActionsService,
    protected pfeConditionsService: PfeConditionsService,
    protected pfeTriggerServiceActivatorsService: PfeTriggerServiceActivatorsService
  ) {
    /**
     * Setting up the pfeTriggerServiceActivatorsService here is a workaround to prevent a circular dependency
     * between the PfeServiceActivatorService and the PfeActionsService.
     *
     * - The PfeServiceActivatorService wants to use the PfeActionsService to trigger actions
     * - The PfeActionsService had the registration of TRIGGER_SERVICE_ACTIVATORS action directly
     * in its constructor. It requires the PfeServiceActivatorService
     * -> We're turning in circles here...
     *
     * Ways to resolve this:
     * - Moving the action registration out of the PfeActionsService into its own module did not really work.
     * It was fine within the pfe, but lead to unexpected effects in apps that use pfe services in locations where they
     * are instantiated sooner (e.g. interceptors). The background for this is, that a registration within a module leads to an immediate
     * instantiation of all the services as soon as the module is imported.
     *
     * - Moving it to another unrelated service is difficult, as that could lead to timing issues where an action is triggered
     * before it is registered.
     *
     * - Moving the registration here is safe as the action relies on the PfeServiceActivatorService. As soon as its gets
     * instantiated, it will handover its own instance to the action and register it with the PfeActionsService.
     * That way it is guaranteed that the PfeServiceActivatorService instance is available before the action can be triggered.
     *
     * How is it ensured that both of those services are instantiated before a service activator call is triggered?
     * -> Both are imported by the PFEPageRoutingEventsHandlerService, which gets active during the initial routing setup.
     * This happens before there is any chance that a call/action is triggered. It is therefore ensured that the registration
     * of the action happens before any action is triggered. (=This also works in the firstPage config which is the earliest possible trigger)
     *
     * How about the future? -> In general it seems like a refactoring of how the action registration works, might be a cleaner way to go.
     */
    this.pfeTriggerServiceActivatorsService.serviceActivatorService = this;
    this.pfeActionsService.registerAction(
      PfeTriggerServiceActivatorsType,
      this.pfeTriggerServiceActivatorsService.executeActionTriggerServiceActivators
    );

    this.baseURL = `${this.ngxPfeModuleConfiguration.serviceActivatorEndpoint}`;
    const ignoreEmptyArrays = this.ngxPfeModuleConfiguration.ignoreEmptyArraysInServiceActivatorPayloads ?? false;
    if (ignoreEmptyArrays) {
      this.getValueByExpression = (o, p) => PfeUtilService.getValueOrList(o, p, true);
    } else {
      this.getValueByExpression = (o, p) => PfeUtilService.getValueOrList(o, p);
    }
  }
  // Made static, so it can be easily accessed by the config editor:
  public static spreadWithGlobalServiceActivatorConfig(
    serviceActivatorConfigsArray: NavServiceActivatorConfigInternally[],
    globalServiceActivators: GlobalServiceActivators | undefined
  ) {
    const saThatUsesGlobalConfigID = serviceActivatorConfigsArray?.filter((sa) => !!sa.globalConfigID).map((sa) => sa.globalConfigID);
    if (saThatUsesGlobalConfigID?.length) {
      const message = `The property "globalConfigID" is deprecated. Please use "globalConfigId". Detected Service Activator with globalConfigID "${saThatUsesGlobalConfigID.join(', ')}"`;
      // Use console.error and not logger.error because the method is static.
      console.error(message);
      throw new Error(message);
    }

    if (serviceActivatorConfigsArray && serviceActivatorConfigsArray.length > 0 && globalServiceActivators) {
      serviceActivatorConfigsArray.forEach((serviceActivatorConfig, index, array) => {
        const newConfig = PfeServiceActivatorService.spreadSingleServiceActivatorConfig(serviceActivatorConfig, globalServiceActivators);
        if (newConfig) {
          array[index] = newConfig;
        }
        // Only try it one time per config:
        array[index]._alreadySpread = true;
      });
    }
  }

  /**
   * Applies a global service activator config on to a service activator config.
   * Returns new config if there was a global config or undefined if there was none.
   */
  public static spreadSingleServiceActivatorConfig(
    serviceActivatorConfig: NavServiceActivatorConfigInternally,
    globalServiceActivators: GlobalServiceActivators
  ): NavServiceActivatorConfigInternally | undefined {
    if (!serviceActivatorConfig._alreadySpread && serviceActivatorConfig.globalConfigId) {
      const globalServiceActivatorConfig = globalServiceActivators[serviceActivatorConfig.globalConfigId];
      if (globalServiceActivatorConfig) {
        return { ...globalServiceActivatorConfig, ...serviceActivatorConfig };
      }
    }
  }

  /**
   * Calls all the service activators of a configuration.
   * It either returns a forkJoin observable for all issued service calls (by default) or a concatMap observable if
   * executeServiceActivatorsSequential param is set to true, meaning to execute all the service activators
   * sequentially.
   *
   * The httpErrorHandler will be called in case of http errors.
   * For serviceActivators that do not use the async option an exception will also be thrown.
   * This exception should not be used for error handling, as it is not thrown for async calls.
   * If the exception is thrown, the navigation is not done.
   * We are aware of the ugliness of this solution and have the following
   * TODO: Improve: Remove error callback and replace with some common async solution
   *
   * To keep the promise handling of the service activators less complex, spreadWithGlobalServiceActivatorConfig has to be called
   * before handleServiceActivators is called. (with await)
   *
   * @param serviceActivators
   * @param executeServiceActivatorsSequential
   * @param httpErrorHandler will be called in case of http errors.
   */
  public handleServiceActivators(
    serviceActivators: NavServiceActivatorConfigsArray,
    executeServiceActivatorsSequential?: boolean,
    httpErrorHandler?: HttpErrorHandler
  ): Promise<HttpResponse<UnknownObject>[] | HttpResponse<UnknownObject> | undefined> {
    if (!serviceActivators || serviceActivators.length === 0) {
      return Promise.resolve(undefined);
    }

    /**
     * Check for updateValues
     */
    serviceActivators
      .filter((serviceActivator) => serviceActivator.updateStateValuesBefore)
      .forEach((service) => {
        this.pfeStateService.updateStateValues(service.updateStateValuesBefore);
      });

    // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#local-fat-arrow
    const errorHandler = httpErrorHandler
      ? (error: HttpErrorResponse, serviceActivatorConfig?: NavServiceActivatorConfig) => httpErrorHandler(error, serviceActivatorConfig)
      : (error: HttpErrorResponse, serviceActivatorConfig?: NavServiceActivatorConfig) =>
          this.handleServiceActivatorError(error, serviceActivatorConfig);

    const hasCleanStaleState = serviceActivators.filter((serviceActivator) => serviceActivator.cleanStaleState).length;

    /**
     * clean state exists
     */
    if (hasCleanStaleState) {
      /**
       * check and perform stalestate cleaning
       */
      this.pfeStateService.cleanStaleStateByServiceActivator();
    }

    // We check if we need to emit the callInProgress
    let emitInProgress = this.shouldEmitInProgress(serviceActivators);
    try {
      let joinedPromise: Promise<HttpResponse<UnknownObject>[] | HttpResponse<UnknownObject>>;
      if (executeServiceActivatorsSequential) {
        this.emitServiceActivatorCallInProgress(emitInProgress, true);
        joinedPromise = this.execServiceActivatorsSequential(serviceActivators, errorHandler, emitInProgress);
      } else {
        serviceActivators = this.filterServiceActivators(serviceActivators);
        emitInProgress = this.shouldEmitInProgress(serviceActivators);
        // If the call its not in the sequential, the emit of the event its done on "waitForAsyncServiceActivators"
        joinedPromise = this.execServiceActivators(serviceActivators, errorHandler, emitInProgress);
      }

      if (serviceActivators.length > 0) {
        joinedPromise.finally(() => {
          this.emitServiceActivatorCallInProgress(emitInProgress, false);
        });
      }

      return joinedPromise;
    } catch (error) {
      this.logger.errorToServer('handleServiceActivators: Error', (error as Error).message);
      throw error;
    }
  }

  public async waitForAsyncServiceActivators(pageID: string): Promise<HttpResponse<UnknownObject>[]> {
    if (this.activeAsyncServiceActivatorCallsPerPage && this.activeAsyncServiceActivatorCallsPerPage.size > 0) {
      const activeAsyncCalls: ActiveAsyncServiceActivatorCallForPage[] | undefined =
        this.activeAsyncServiceActivatorCallsPerPage.get(pageID);
      if (activeAsyncCalls && activeAsyncCalls.length > 0) {
        const emitInProgress = this.shouldEmitInProgress(activeAsyncCalls);
        this.addAsyncInProgressServiceActivatorDetails(activeAsyncCalls, emitInProgress);
        this.emitServiceActivatorCallInProgress(emitInProgress, true);

        const allPromise = Promise.all(activeAsyncCalls.map((call) => call.callPromise));

        allPromise.finally(() => this.emitServiceActivatorCallInProgress(emitInProgress, false));

        return allPromise;
      }
    }
    return [];
  }

  /**
   * Takes in a ServiceActivatorConfigsArray and spreads it on top of a global
   * service activator config, if there is one.
   * The array is changed directly!
   */
  public async spreadWithGlobalServiceActivatorConfig(serviceActivatorConfigsArray: NavServiceActivatorConfigInternally[]) {
    const config: NgxPfeConfig = await this.pfeConfigurationService.getConfig();

    PfeServiceActivatorService.spreadWithGlobalServiceActivatorConfig(
      serviceActivatorConfigsArray,
      config.navConfiguration.serviceActivators
    );
  }

  /**
   * Handles the responses of service activator calls and writes data back to the state if a response mapping is configured.
   */
  protected handleServiceActivatorResult(
    response: HttpResponse<UnknownObject> | HttpErrorResponse,
    serviceActivatorConfig: NavServiceActivatorConfig | undefined,
    dataMapping: ServiceActivatorDataMapping[] | undefined
  ) {
    this.removeInProgressServiceActivatorDetails(serviceActivatorConfig);

    if (dataMapping && dataMapping.length > 0) {
      this.handleResponseDataMapping(dataMapping, response, serviceActivatorConfig?.mapUndefinedResponses);
    }

    // Handles the configured error mappings
    if (
      serviceActivatorConfig?.serviceActivatorResponseStatusHandlers &&
      serviceActivatorConfig.serviceActivatorResponseStatusHandlers.length > 0
    ) {
      serviceActivatorConfig.serviceActivatorResponseStatusHandlers.forEach((responseStatusHandler) => {
        if (response.status === responseStatusHandler.statusCode) {
          try {
            this.pfeStateService.storeValueByExpression(responseStatusHandler.stateKeyExpression, responseStatusHandler.stateValue);
          } catch (error) {
            this.logger.errorToServer('handleServiceActivatorResult: Error in responseErrorMapping', error);
            throw error;
          }
        }
      });
    }
  }

  /**
   * Legacy error handling that is still supported. Called by the new error handling, if there is no
   * new error handling config.
   */
  protected async handleServiceActivatorErrorLegacy(
    errorResponse: HttpErrorResponse,
    serviceActivatorConfig?: NavServiceActivatorConfigInternally
  ): Promise<void> {
    if (this.isStatusExcluded(errorResponse, serviceActivatorConfig)) {
      try {
        this.handleServiceActivatorResult(errorResponse, serviceActivatorConfig, serviceActivatorConfig?.responseDataMapping);
      } catch (error) {
        this.logger.error('There was an error in the service activator error handling', error);
        if (!serviceActivatorConfig?._disableErrorPageNavigation) {
          this.pfeNavigationUtilService.navigateToGlobalErrorPage(error as HttpErrorResponse);
        }
      }
    } else {
      if (serviceActivatorConfig?.errorResponseDataMapping) {
        this.handleServiceActivatorResult(errorResponse, serviceActivatorConfig, serviceActivatorConfig?.errorResponseDataMapping);
      }
      if (!serviceActivatorConfig?._disableErrorPageNavigation) {
        this.pfeNavigationUtilService.navigateToGlobalErrorPage(errorResponse);
      }
    }
  }

  /**
   * Called if one or multiple service activator calls failed.
   */
  protected async handleServiceActivatorError(
    errorResponse: HttpErrorResponse,
    serviceActivatorConfig?: NavServiceActivatorConfigInternally
  ): Promise<void | boolean> {
    if (!serviceActivatorConfig?.errorHandling) {
      return this.handleServiceActivatorErrorLegacy(errorResponse, serviceActivatorConfig);
    }

    const state: PfeUserInputState = this.pfeStateService.getFullState();

    const errorCase = this.determineErrorCase(serviceActivatorConfig.errorHandling?.errorCases, {
      state: state,
      response: errorResponse,
      responseHeaders: convertHTTPHeaders(errorResponse),
    });
    if (!errorCase) {
      this.logger.warn('Could not determine service activator error handling. Falling back to global error page.');
      this.pfeNavigationUtilService.navigateToGlobalErrorPage(errorResponse);
      return false;
    }

    // We do have an error case configuration, let's do the actual error handling:

    if (errorCase.errorResponseDataMapping) {
      this.handleResponseDataMapping(errorCase.errorResponseDataMapping, errorResponse, true);
    }

    if (errorCase.actions) {
      try {
        const actionsResult = await this.pfeActionsService.executeActions(errorCase.actions);
        if (!errorCase.ignoreActionsResult) {
          return actionsResult;
        }
      } catch (error) {
        // Something went wrong in the action. Simply continue to the errorPageNavigation.
        this.logger.error('Error when executing action in service activator error handling. Continuing with error handling...');
      }
    }

    if (errorCase.ignoreError) {
      return true;
    }

    if (!errorCase.disableErrorPageNavigation) {
      let errorPageOption;
      if (errorCase.errorPageNavigation) {
        errorPageOption = this.pfeNavigationUtilService.determineErrorPageOption(errorCase.errorPageNavigation, errorResponse);
      }
      if (errorPageOption) {
        this.pfeNavigationUtilService.navigateToErrorPage(errorPageOption);
      } else {
        this.pfeNavigationUtilService.navigateToGlobalErrorPage(errorResponse);
      }
    }

    return false;
  }

  private addAsyncInProgressServiceActivatorDetails(activeAsyncCalls: ActiveAsyncServiceActivatorCallForPage[], emitInProgress: boolean) {
    if (!emitInProgress) {
      return;
    }

    const activeAsyncCallConfigs = activeAsyncCalls.map(
      (call): Pick<NavServiceActivatorConfig, 'globalConfigId' | 'path' | 'displayMessage'> => ({
        globalConfigId: call.id,
        displayMessage: call.displayMessage,
        path: call.id,
      })
    );

    activeAsyncCallConfigs.forEach((config) => {
      this.addInProgressServiceActivatorDetails(config, true);
    });
  }

  private addInProgressServiceActivatorDetails(serviceActivator: NavServiceActivatorConfig, emitInProgress: boolean): void {
    if (!emitInProgress) {
      return;
    }

    // we allow users to configure a displayMessage as a string
    // but we want to always emit a displayMessage object
    const normalizedDisplayMessage =
      typeof serviceActivator.displayMessage === 'string' ? { headline: serviceActivator.displayMessage } : serviceActivator.displayMessage;

    const serviceActivatorProgressDetails = {
      ...serviceActivator,
      displayMessage: normalizedDisplayMessage,
    };

    this._serviceActivatorCallInProgressDetails$.next([
      ...this._serviceActivatorCallInProgressDetails$.value,
      clone(serviceActivatorProgressDetails),
    ]);
  }

  private removeInProgressServiceActivatorDetails(serviceActivator: NavServiceActivatorConfig | undefined): void {
    if (!serviceActivator) {
      return;
    }

    const inProgressServices = this._serviceActivatorCallInProgressDetails$.value.filter(
      (inProgressService) => !this.areServiceActivatorsEqual(serviceActivator, inProgressService)
    );
    this._serviceActivatorCallInProgressDetails$.next(inProgressServices);
  }

  private handleResponseDataMapping(
    dataMapping: ServiceActivatorDataMapping[],
    response: HttpResponse<UnknownObject> | HttpErrorResponse,
    mapUndefinedResponses?: boolean
  ) {
    dataMapping.forEach((responseDataMapping) => {
      const fullState = this.pfeStateService.getFullState();
      // If we have conditions and are not fulfilled, we don't execute the mapping for this object
      if (responseDataMapping.conditions && !this.pfeConditionsService.evaluateConditions(responseDataMapping.conditions, fullState)) {
        return;
      }
      try {
        let value;
        let responseBody;

        if (response instanceof HttpErrorResponse) {
          responseBody = response.error;
        } else {
          responseBody = response.body;
        }

        const responseData = responseDataMapping.useHTTPResponseAsData ? response : responseBody;

        // Same issue as in the requestMapping: https://github.com/dchester/jsonpath/issues/72
        if (responseDataMapping.stateKeyExpression === '$') {
          let state = this.pfeStateService.getFullState();
          if (typeof responseData === 'object') {
            state = { ...state, ...responseData };
          } else {
            this.logger.error('The responseDataExpression "$" can only be used with objects in the responseData');
          }
          this.pfeStateService.setFullState(state);
        } else {
          if (responseData) {
            value = this.getValueByExpression(responseData, responseDataMapping.responseDataExpression);
          }
          if (mapUndefinedResponses || (value !== undefined && value !== null)) {
            this.pfeStateService.storeValueByExpression(responseDataMapping.stateKeyExpression, value);
          }
        }
      } catch (error) {
        this.logger.errorToServer('handleServiceActivatorResult: Error in responseDataMapping', error);
        throw error;
      }
    });
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private determineErrorCase(errorCases: ErrorCase<any>[], state: ErrorCaseConditionsState): ErrorCase<any> | undefined {
    if (!errorCases) {
      return undefined;
    }

    for (const errorCase of errorCases) {
      let conditionsResult: boolean;
      if (!errorCase.conditions) {
        conditionsResult = true;
      } else {
        conditionsResult = this.pfeConditionsService.evaluateConditions(errorCase.conditions, state);
      }

      if (conditionsResult) {
        return errorCase;
      }
    }
    return undefined;
  }

  private filterServiceActivators(serviceActivators: NavServiceActivatorConfigsArray): NavServiceActivatorConfigsArray {
    return serviceActivators.filter((serviceActivator) => this.evaluateServiceActivatorCondition(serviceActivator));
  }

  private evaluateServiceActivatorCondition(serviceActivator: NavServiceActivatorConfig) {
    // The serviceActivatorID attribute was superseded by the path attribute everywhere (For example in the data models).
    // To ensure backwards compatibility it is still checked here, for now...
    if (
      serviceActivator.cleanStaleState &&
      !(serviceActivator.path || serviceActivator['serviceActivatorID' as keyof NavServiceActivatorConfig])
    ) {
      return false;
    }

    if (!serviceActivator.conditions) {
      return true;
    }
    return this.pfeConditionsService.evaluateConditions(serviceActivator.conditions, this.pfeStateService.getFullState());
  }

  private execServiceActivatorsSequential(
    serviceActivators: NavServiceActivatorConfigsArray,
    errorHandler: HttpErrorHandler,
    emitInProgress: boolean
  ): Promise<HttpResponse<UnknownObject>> {
    return lastValueFrom(
      from(serviceActivators).pipe(
        concatMap((serviceActivator) => {
          if (this.evaluateServiceActivatorCondition(serviceActivator)) {
            this.addInProgressServiceActivatorDetails(serviceActivator, emitInProgress);
            return this.callServiceActivator(serviceActivator, errorHandler);
          }

          return EMPTY;
        })
      ),
      { defaultValue: EMPTY as unknown as HttpResponse<UnknownObject> }
    );
  }

  private execServiceActivators(
    serviceActivators: NavServiceActivatorConfigsArray,
    errorHandler: HttpErrorHandler,
    emitInProgress: boolean
  ): Promise<HttpResponse<UnknownObject>[]> {
    const serviceActivatorCalls: Observable<HttpResponse<UnknownObject>>[] = [];
    serviceActivators.forEach((serviceActivator) => {
      const serviceActivatorObservable = this.callServiceActivator(serviceActivator, errorHandler);

      if (!serviceActivator.async) {
        this.addInProgressServiceActivatorDetails(serviceActivator, emitInProgress);
        this.emitServiceActivatorCallInProgress(emitInProgress, true);
        serviceActivatorCalls.push(serviceActivatorObservable);
      } else {
        const promise: Promise<HttpResponse<UnknownObject>> = lastValueFrom(serviceActivatorObservable);
        if (serviceActivator.asyncResolveBeforePageIds) {
          this.addCallToAsyncPromiseList(serviceActivator, promise);
        }
      }
    });

    return lastValueFrom(observableForkJoin(serviceActivatorCalls), { defaultValue: EMPTY as unknown as HttpResponse<UnknownObject>[] });
  }

  private handleServiceActivatorRequest(serviceActivator: NavServiceActivatorConfig): HttpParamsOptionsFromObject | undefined {
    const fullState = this.pfeStateService.getFullState();
    const defaultRequestBody = this.ngxPfeModuleConfiguration.defaultRequestBody ?? PFE_DEFAULT_REQUEST_BODY.EMPTY;
    if (!serviceActivator.requestDataMapping && serviceActivator.serviceActivatorMethod === 'GET') {
      return undefined;
    }
    if (!serviceActivator.requestDataMapping || serviceActivator.requestDataMapping.length === 0) {
      return defaultRequestBody === PFE_DEFAULT_REQUEST_BODY.FULL_STATE ? fullState : undefined;
    }

    let payload = {};
    let uglyWorkaroundFirst = true;

    serviceActivator.requestDataMapping.forEach((requestDataMapping) => {
      // If we have conditions and are not fulfilled, we don't add this request param
      if (requestDataMapping.conditions && !this.pfeConditionsService.evaluateConditions(requestDataMapping.conditions, fullState)) {
        return;
      }
      try {
        const storeValue = this.getValueByExpression(fullState, requestDataMapping.stateKeyExpression);
        const payloadValue = this.getValueByExpression(payload, requestDataMapping.requestDataExpression);

        /**
         * BEGIN - Ugly workaround because JSONPath does not work when we are replacing the root object ("$")
         *         Read more about the issue here: https://github.com/dchester/jsonpath/issues/72
         */
        if (requestDataMapping.requestDataExpression === '$') {
          // Clone stateValue locally
          if (uglyWorkaroundFirst) {
            payload = JSON.parse(JSON.stringify(storeValue));
            uglyWorkaroundFirst = false;
            return;
          }

          if (!uglyWorkaroundFirst && Array.isArray(payloadValue)) {
            payload = [...payloadValue, ...(storeValue as unknown[])];
            return;
          }

          if (!uglyWorkaroundFirst && typeof payloadValue === 'object') {
            payload = { ...payloadValue, ...(storeValue as object) };
            return;
          }
        }
        /* END of Ugly workaround */

        // Remove value from from payload if stateKeyExpression is undefined
        if (storeValue === 'undefined') {
          jsonpath.value(payload, requestDataMapping.requestDataExpression, undefined);
          return;
        }

        // Extend the Array
        if (Array.isArray(payloadValue)) {
          jsonpath.value(payload, requestDataMapping.requestDataExpression, [...payloadValue, ...(storeValue as unknown[])]);
          return;
        }

        // Extend the Object
        if (payloadValue && typeof payloadValue === 'object' && storeValue && typeof storeValue === 'object') {
          jsonpath.value(payload, requestDataMapping.requestDataExpression, { ...payloadValue, ...(storeValue as object) });
          return;
        }

        // Replace string or integer into
        jsonpath.value(payload, requestDataMapping.requestDataExpression, storeValue);
      } catch (error) {
        this.logger.errorToServer('handleServiceActivatorRequest: Error in requestDataMapping ', error);
      }
    });

    return payload;
  }

  // Method that builds url replacing params in url
  private addPathParamsToUrl(url: string, pathParams: PathParams[]): string {
    const fullState = this.pfeStateService.getFullState();

    const pathParamPlaceholderRegex = new RegExp(`{.+?}`, 'g');

    const changedUrl = pathParams
      .filter((param) => !param.conditions || this.pfeConditionsService.evaluateConditions(param.conditions, fullState))
      .reduce((reducedUrl, param) => {
        const value = this.getValueByExpression(fullState, param.value) ?? '';
        reducedUrl = reducedUrl.replace(new RegExp(`{${param.id}}`, 'g'), encodeURIComponent(value as string | number | boolean));
        return reducedUrl;
      }, url)
      .replace(pathParamPlaceholderRegex, '');

    return changedUrl;
  }

  private callServiceActivator(
    serviceActivator: NavServiceActivatorConfig,
    httpErrorHandler?: HttpErrorHandler
  ): Observable<HttpResponse<UnknownObject>> {
    let url = `${this.baseURL}/${this.getServiceActivatorPath(serviceActivator)}`;

    // Check if the full URL is set in the config file.
    if (new RegExp(/^(http:|https:|\/\/)/, 'i').test(this.getServiceActivatorPath(serviceActivator))) {
      url = this.getServiceActivatorPath(serviceActivator);
    }

    if (serviceActivator.pathParams && serviceActivator.pathParams.length > 0) {
      url = this.addPathParamsToUrl(url, serviceActivator.pathParams);
    }
    // TODO Change here how to construct url using pathparams

    const method = serviceActivator.serviceActivatorMethod || 'POST';
    const payload = this.handleServiceActivatorRequest(serviceActivator);
    const params = method === 'GET' ? new HttpParams({ fromObject: payload }) : undefined;

    const req = new HttpRequest(method, url, payload, {
      params,
    });

    return this.http.request<UnknownObject>(req).pipe(
      // The resulting observable is converted to a promise later.
      // It seems like, this causes typescript to get confused with the
      // type information in strict mode here. That's why the any shows up here.
      filter(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (eventType: HttpResponse<UnknownObject> | HttpEvent<UnknownObject> | any) =>
          (eventType as HttpResponse<UnknownObject>).type === HttpEventType.Response
      ),
      tap((result: HttpResponse<UnknownObject>) => {
        this.handleServiceActivatorResult(result, serviceActivator, serviceActivator.responseDataMapping);
      }),
      catchError<HttpResponse<UnknownObject>, Observable<HttpResponse<UnknownObject>>>(
        (error: HttpErrorResponse) =>
          new Observable((subscriber) => {
            (async () => {
              try {
                // Wait for the httpErrorHandler to be done.
                const ignoreError = httpErrorHandler ? await httpErrorHandler(error, serviceActivator) : false;
                // If the error should be ignored, everything will continue. If not, everything is stopped

                // The legacy error handling returns a dark void, the new one a shiny boolean:
                if (ignoreError !== undefined && typeof ignoreError === 'boolean') {
                  if (ignoreError) {
                    // This will prevent an error to be thrown above the service activator handling.
                    // That means the navigation will run as if there was no error.
                    // It also means, that the error handling will be executed for all other service activators in the array
                    // (If there is another error).
                    subscriber.next(undefined);
                  } else {
                    // This will abort the navigation when the service activator fails
                    // The error handling will not be called multiple times for multiple service activators (That arrive later)
                    subscriber.error(error);
                  }
                } else {
                  // Legacy error handling:
                  if (this.isStatusExcluded(error, serviceActivator) && !serviceActivator.preventNavigationOnError) {
                    // This will prevent an error to be thrown above the service activator handling.
                    // That means the navigation will run as if there was no error.
                    // It also means, that the error handling will be executed for all other service activators in the array
                    // (If there is another error).
                    subscriber.next(undefined);
                  } else {
                    // This will abort the navigation when the service activator fails
                    //  The error handling will not be called multiple times for multiple service activators (That arrive later)
                    subscriber.error(error);
                  }
                }

                subscriber.complete();
              } catch (errorHandlerError) {
                // The httpErrorHandler failed. This should never happen, but who knows, better abort
                subscriber.error(errorHandlerError);
              }
            })();
          })
      )
    );
  }

  private addCallToAsyncPromiseList(serviceActivator: NavServiceActivatorConfig, callPromise: Promise<HttpResponse<UnknownObject>>) {
    (serviceActivator.asyncResolveBeforePageIds || []).forEach((pageID) => {
      const newCall: ActiveAsyncServiceActivatorCallForPage = {
        id: (serviceActivator.globalConfigId || serviceActivator.path) as string,
        callPromise,
        dontEmitInProgress: Boolean(serviceActivator.dontEmitInProgress),
        displayMessage: serviceActivator.displayMessage,
      };

      if (this.activeAsyncServiceActivatorCallsPerPage.has(pageID)) {
        // There's a has() directly above, get your &$§@$%§$ together typescript...
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const pageCallsWithoutPreviousServiceActivatorCall = this.activeAsyncServiceActivatorCallsPerPage
          .get(pageID)!
          .filter((pageCall) => ![serviceActivator.globalConfigId, serviceActivator.path].includes(pageCall.id));

        this.activeAsyncServiceActivatorCallsPerPage.set(pageID, [...pageCallsWithoutPreviousServiceActivatorCall, newCall]);
      } else {
        this.activeAsyncServiceActivatorCallsPerPage.set(pageID, [newCall]);
      }
    });
  }

  /**
   * The serviceActivatorID attribute was superseded by the path attribute everywhere (For example in the data models).
   *  To ensure backwards compatibility it is still checked here, for now...
   */
  private getServiceActivatorPath(config: NavServiceActivatorConfig) {
    let path = config.path;

    if (!path) {
      path = config['serviceActivatorID' as keyof NavServiceActivatorConfig] as string | undefined;
      if (path) {
        this.logger.warnToServer(
          'Found a serviceActivatorID in the configuration. Please update your configuration to use the path attribute instead.'
        );
      }
    }

    if (!path) {
      this.logger.error('Was not able to find path in serviceActivator config', config);
      throw new Error('Was not able to determine serviceActivator path');
    }

    return path;
  }

  /**
   * Check if at least one element in the array is configured to emit a progress.
   * This means that the "dontEmitInProgress" flag has to be false or undefined for at least one element.
   * If that is the case, the overall result to emit the progress will be true.
   * If all elements do not allow to emit the in progress state, the overall result will be false.
   *
   * The default for an empty config array is false.
   *
   * @param dontEmitInProgressConfig An array of objects that have the dontEmitInProgress flag
   */
  private shouldEmitInProgress(
    dontEmitInProgressConfig: {
      dontEmitInProgress?: boolean | undefined;
    }[]
  ): boolean {
    if (dontEmitInProgressConfig && dontEmitInProgressConfig.length > 0) {
      return dontEmitInProgressConfig.some((config) => !config.dontEmitInProgress);
    } else {
      return false;
    }
  }

  /**
   * Check if should emit the service activator call in progress
   * @param emit If should emit the value
   * @param value If the event its "true" or "false"
   */
  private emitServiceActivatorCallInProgress(emit: boolean, value: boolean) {
    if (emit) {
      if (!value) {
        this._serviceActivatorCallInProgressDetails$.next([]);
      }

      this.serviceActivatorCallInProgress$.next(value);
    }
  }

  /**
   * Check if the status of the error its present on the "serviceActivatorErrorHandlingExcludeStatusCodes"
   * @param error An HttpError
   * @param serviceActivatorConfig The configuration of the service activator
   */
  private isStatusExcluded(error: HttpErrorResponse, serviceActivatorConfig: NavServiceActivatorConfig | undefined): boolean {
    return Boolean(serviceActivatorConfig?.serviceActivatorErrorHandlingExcludeStatusCodes?.includes(error.status));
  }

  private areServiceActivatorsEqual(serviceActivator1: NavServiceActivatorConfig, serviceActivator2: NavServiceActivatorConfig): boolean {
    return serviceActivator1.globalConfigId === serviceActivator2.globalConfigId && serviceActivator1.path === serviceActivator2.path;
  }
}

results matching ""

    No results matching ""