All files / src/app/core/services/sparque-api sparque-api.service.ts

82.25% Statements 51/62
68.42% Branches 26/38
84% Functions 21/25
81.96% Lines 50/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 21720x 20x 20x 20x 20x                                     20x 20x 20x 20x 20x     20x                               20x 20x     10x 10x 10x 10x               10x     10x 10x                                       10x 2x   8x   8x       8x     8x 32x 24x     8x   8x                         8x                                     10x   10x 10x                             10x     10x     3x                 10x 2x   8x       8x         8x         10x 2x 2x     2x         2x         10x             10x     10x          
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { concatLatestFrom } from '@ngrx/effects';
import { Store, select } from '@ngrx/store';
import {
  EMPTY,
  MonoTypeOperatorFunction,
  Observable,
  catchError,
  combineLatest,
  concatMap,
  defer,
  first,
  forkJoin,
  iif,
  map,
  of,
  switchMap,
  take,
  throwError,
} from 'rxjs';
 
import { AvailableOptions } from 'ish-core/services/api/api.service';
import { TokenService } from 'ish-core/services/token/token.service';
import { getCurrentLocale, getSparqueConfig } from 'ish-core/store/core/configuration';
import { communicationTimeoutError, serverError } from 'ish-core/store/core/error';
import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service';
import { whenTruthy } from 'ish-core/utils/operators';
 
// sparque config keys that should not be appended to the query params
const SPARQUE_CONFIG_EXCLUDE_PARAMS = ['serverUrl'];
 
/**
 * Service for interacting with the Sparque API.
 *
 * This service provides methods to construct HTTP requests with appropriate headers and parameters,
 * handle errors, and execute HTTP GET requests. It leverages Angular's HttpClient for making HTTP calls
 * and NgRx Store for accessing application state.
 *
 * The service includes methods to:
 * - Construct HTTP client parameters and headers.
 * - Convert paths to HTTP parameters based on Sparque configuration and locale.
 * - Handle errors and dispatch actions to the store in case of server errors.
 * - Execute HTTP GET requests.
 */
@Injectable({ providedIn: 'root' })
export class SparqueApiService {
  private static SPARQUE_PERSONALIZATION_IDENTIFIER = 'userId';
 
  constructor(
    private httpClient: HttpClient,
    private store: Store,
    private apiTokenService: ApiTokenService,
    private tokenService: TokenService
  ) {}
 
  private constructHttpClientParams(
    path: string,
    apiVersion: string,
    options?: AvailableOptions
  ): Observable<[string, { headers: HttpHeaders; params: HttpParams }]> {
    return forkJoin([
      this.constructUrlForPath(path, apiVersion),
      defer(() =>
        this.constructHeaders(options).pipe(
          map(headers => ({
            headers,
            params: this.sparqueQueryToHttpParams(path, options?.params),
            responseType: options?.responseType,
          }))
        )
      ),
    ]);
  }
 
  /**
   * Converts a given path to HTTP parameters based on the Sparque configuration and current locale.
   * If the path starts with 'http://' or 'https://', it returns an empty set of HTTP parameters.
   * Otherwise, it retrieves the Sparque configuration and current locale from the store,
   * and appends them as HTTP parameters, excluding specific keys defined in SPARQUE_CONFIG_EXCLUDE_PARAMS.
   *
   * @param path - The path to be converted to HTTP parameters.
   * @returns An instance of HttpParams containing the Sparque configuration and locale.
   */
  private sparqueQueryToHttpParams(path: string, params: HttpParams): HttpParams {
    if (path.startsWith('http://') || path.startsWith('https://')) {
      return new HttpParams();
    }
    let sparqueParams = new HttpParams();
 
    this.store
      .pipe(
        select(getSparqueConfig),
        take(1),
        concatLatestFrom(() => this.store.pipe(select(getCurrentLocale)))
      )
      .subscribe(([config, locale]) => {
        Object.keys(config).forEach(key => {
          if (!SPARQUE_CONFIG_EXCLUDE_PARAMS.includes(key)) {
            sparqueParams = sparqueParams.append(key, <string>config[key]);
          }
        });
        sparqueParams = sparqueParams.append('Locale', locale.replace('_', '-'));
      });
    params?.keys().forEach(key => {
      if (key.includes('selectedFacets')) {
        params
          .get(key)
          ?.split(',')
          .forEach(value => {
            sparqueParams = sparqueParams.append(key, value);
          });
      } else {
        sparqueParams = sparqueParams.append(key, params.get(key));
      }
    });
 
    return sparqueParams;
  }
 
  /**
   * Constructs HTTP headers for a request, optionally including an authorization token.
   *
   * @param options - Optional parameters that may include additional headers and query parameters.
   * @returns An observable that emits the constructed HttpHeaders.
   *
   * This method performs the following steps:
   * 1. Initializes default headers with 'content-type' and 'Accept' set to 'application/json'.
   * 2. Checks if the `SPARQUE_PERSONALIZATION_IDENTIFIER` is present in the query parameters.
   * 3. If the identifier is present, it attempts to retrieve an API token:
   *    - If an API token is available, it uses it.
   *    - If no API token is available, it fetches an anonymous token.
   * 4. Appends the authorization token to the headers if available.
   * 5. Merges any additional headers provided in the options with the default headers.
   */
  private constructHeaders(options?: AvailableOptions): Observable<HttpHeaders> {
    let defaultHeaders = new HttpHeaders().set('content-type', 'application/json').set('Accept', 'application/json');
 
    return iif(
      () => options?.params?.keys().includes(SparqueApiService.SPARQUE_PERSONALIZATION_IDENTIFIER),
      this.apiTokenService.apiToken$.pipe(
        first(),
        switchMap(apiToken =>
          iif(
            () => options?.params?.keys().includes(SparqueApiService.SPARQUE_PERSONALIZATION_IDENTIFIER) && !!apiToken,
            of(apiToken),
            this.tokenService.fetchToken('anonymous')
          )
        )
      ),
      of(EMPTY)
    ).pipe(
      first(),
      switchMap(apiToken => {
        Iif (apiToken && apiToken !== EMPTY) {
          defaultHeaders = defaultHeaders.append('Authorization', `bearer ${apiToken}`);
        }
        return of(
          options?.headers
            ? // append incoming headers to default ones
              options.headers.keys().reduce((acc, key) => acc.set(key, options.headers.get(key)), defaultHeaders)
            : // just use default headers
              defaultHeaders
        );
      })
    );
  }
 
  private constructUrlForPath(path: string, apiVersion: string): Observable<string> {
    if (path.startsWith('http://') || path.startsWith('https://')) {
      return of(path);
    }
    return combineLatest([
      this.store.pipe(
        select(getSparqueConfig),
        whenTruthy(),
        map(config => config.serverUrl.concat('/api/', apiVersion))
      ),
      of(`/${path}`),
    ]).pipe(
      first(),
      map(arr => arr.join(''))
    );
  }
 
  private handleErrors<T>(dispatch: boolean): MonoTypeOperatorFunction<T> {
    return catchError(error => {
      if (dispatch) {
        Iif (error.status === 0) {
          this.store.dispatch(communicationTimeoutError({ error }));
          return EMPTY;
        } else Iif (error.status >= 500 && error.status < 600) {
          this.store.dispatch(serverError({ error }));
          return EMPTY;
        }
      }
      return throwError(() => error);
    });
  }
 
  private execute<T>(options: AvailableOptions, httpCall$: Observable<T>): Observable<T> {
    return httpCall$.pipe(this.handleErrors(!options?.skipApiErrorHandling));
  }
 
  /**
   * http get request
   */
  get<T>(path: string, apiVersion: string, options?: AvailableOptions): Observable<T> {
    return this.execute(
      options,
      this.constructHttpClientParams(path, apiVersion, options).pipe(
        concatMap(([url, httpOptions]) => this.httpClient.get<T>(url, httpOptions))
      )
    );
  }
}