File

libs/pfe-connector/src/lib/utils/pfe-resources.ts

Index

Properties

Properties

propertyPath
Type string
value
Type T
import forOwn from 'lodash/forOwn';
import isPlainObject from 'lodash/isPlainObject';
import merge from 'lodash/merge';
import setObject from 'lodash/set';
import { combineLatest, merge as mergeRxJs, Observable, of } from 'rxjs';
import { debounceTime, first, map, skip, tap } from 'rxjs/operators';

import { PfeBusinessService } from '@allianz/ngx-pfe';
import { StaticResource } from '@allianz/taly-core';
import {
  BuildingBlockResourceMap,
  isStoreQuery,
  ObservableFromQueryFunction,
  StaticResourceList
} from '../types';

const VERBOSE = false;
const DEBOUNCE_STORE_UPDATES_TIME = 300;

/**
 *
 * Create curried methods to save us from passing around the business service
 * This technique reduces our dependency to the actual PFE business service
 * and will allow use to move large parts into the ITMP core as we can't have
 * any PFE dependencies in there.
 */
export const pfeObservableExpressionBuilder =
  (businessService: PfeBusinessService) => (query: string) =>
    businessService.getObservableForExpressionKey(query, true);

/**
 * wrap the lodash `set` function (imported as setObject)
 * with a better name to help reading the context.
 */
function assignValueToObjectWithPath({
  object,
  path,
  value
}: {
  object: StaticResourceList;
  path: string;
  value: unknown;
}) {
  setObject(object, path, value);
}

/**
 * We need to deep merge resource objects
 * and a spread operator will fail this task
 * as nested keys with the same name are not merged (it's shallow)
 *
 * a = { nested : { valueA: 1}}
 * b = { nested : { valueB: 2}}
 * result = { nested : { valueA: 1, valueB: 2}}
 */
function mergeResources(objectA: StaticResourceList, objectB: StaticResourceList) {
  return merge({}, objectA, objectB);
}

/**
 * Find all the leaves of an object tree and return the fully qualified [path, value] combination
 * filtered by a given predicate fn to return only matching value types.
 *
 * We use this utility to separate PFE_QUERY objects from static values for
 * a separate processing as we need to normalize the pfe queries with the requested data
 * and merge the data back with the static set of data.
 *
 */

interface PFEQuery {
  key: string;
  query: string;
}

interface LeafRecord<T> {
  propertyPath: string;
  value: T;
}

function collectObjectLeaves<T_VALUE_TYPE = unknown>(
  givenObject: object,
  predicateFn: (value: StaticResource) => boolean,
  parentKeys: string[] = []
): LeafRecord<T_VALUE_TYPE>[] {
  const resultEntries: LeafRecord<T_VALUE_TYPE>[] = [];
  const isObjectData = (probe: unknown) =>
    isPlainObject(probe) && !isStoreQuery(probe as StaticResource);

  forOwn(givenObject, (objValue, key) => {
    const fullPathChain = [...parentKeys, key];
    const fullPath = transformPathChainToPath(fullPathChain);

    const objValueIsNonEmptyArray =
      Array.isArray(objValue) && (objValue as T_VALUE_TYPE[]).length > 0;

    if (objValueIsNonEmptyArray || isObjectData(objValue)) {
      const result = collectObjectLeaves<T_VALUE_TYPE>(objValue, predicateFn, fullPathChain);
      resultEntries.push(...result);
    } else if (predicateFn(objValue)) {
      resultEntries.push({ propertyPath: fullPath, value: objValue });
    }
  });

  return resultEntries;
}

/**
 *
 * @param pathPieces list of path pieces
 * @returns the joined path, generates list-like paths for numeric path pieces
 * @example ['object', 'key'] -> 'object.key'
 * ['list', '0'] -> 'list[0]'
 */
function transformPathChainToPath(pathPieces: string[]): string {
  return pathPieces.reduce((acc, pathPiece) => {
    const maybeNumber = Number.parseInt(pathPiece, 10);
    if (Number.isNaN(maybeNumber)) {
      acc += acc.length > 0 ? `.${pathPiece}` : pathPiece;
    } else {
      acc += `[${pathPiece}]`;
    }
    return acc;
  }, '');
}

/**
 * Transform a list of resources into a stream of StaticResourceList.
 * The recipe is: extract the static data and add the latest values from all given pfe queries
 * The resulting object can then be passed to a Building Block's setResource method.
 */
export function createResourceUpdateStream(
  resourceMap: BuildingBlockResourceMap,
  pfeQueries: {
    pfeExpressionObservableQuery: ObservableFromQueryFunction;
  }
): Observable<StaticResourceList> {
  const staticResourceData = getStaticResources(resourceMap);
  // extract only the pfe queries
  const queryList = getPFEQueryMap(resourceMap);
  //early exit when there are no queried data configured
  if (queryList.length === 0) {
    return of({ ...staticResourceData });
  }

  const resourceUpdate$ = mergePfeQueryUpdates(queryList, pfeQueries.pfeExpressionObservableQuery);

  /**
   * Make sure that we always pass the static data (staticResourceData)
   * combined with the extracted pfe store values that can always be updated (resourceUpdate$)
   */
  const combinedResource$ = resourceUpdate$.pipe(
    map((resources) => {
      return mergeResources(staticResourceData, resources);
    })
  );

  return combinedResource$;
}

/**
 * Take a list of PFEResourceQueries and transform them into
 * a single map containing all mapped values from the store
 */
export function mergePfeQueryUpdates(
  queries: PFEQuery[],
  pfeQueryGetter: ObservableFromQueryFunction
): Observable<StaticResourceList> {
  const pfeQueryToObservable = (key: string, query: string) => {
    const query$ = pfeQueryGetter(query).pipe(
      tap((value) => {
        if (VERBOSE) {
          console.log(`📚 Query Changed ${query} -> ${key}`, value);
        }
      }),
      map((value) => [key, value])
    );

    return query$;
  };

  const queryObservables: Observable<unknown[]>[] = queries.map((entry) =>
    pfeQueryToObservable(entry.key, entry.query)
  );

  const queryObservablesMap$ = combineLatest(queryObservables).pipe(
    map((entries) => {
      return entries.reduce((object, [path, value]) => {
        assignValueToObjectWithPath({
          object,
          path: path as string,
          value
        });

        return object;
      }, {});
    })
  );

  const resourceUpdate$ = mergeRxJs(
    queryObservablesMap$.pipe(first()),
    queryObservablesMap$.pipe(skip(1), debounceTime(DEBOUNCE_STORE_UPDATES_TIME))
  ).pipe(
    tap((value) => {
      if (VERBOSE) {
        console.log(`📚 Resource Collection Changed`, value);
      }
    })
  );

  return resourceUpdate$;
}

/**
 * Given a resource list return a map of PFE Queries
 * where the key is the future key in the resource object being passed to setResources()
 * and the value is retrieved by the pfe store expression in the PFE Query.
 */

export function getPFEQueryMap(resourceMap: BuildingBlockResourceMap): PFEQuery[] {
  const pfeQueryLeaves = collectObjectLeaves<PFEQuery>(resourceMap, (value) => isStoreQuery(value));

  const normalized = pfeQueryLeaves.map((record) => {
    return {
      key: record.propertyPath,
      query: record.value.query
    };
  });

  return normalized;
}

/**
 * Extract all static values from a resource list
 */
export function getStaticResources(resourceMap: BuildingBlockResourceMap): StaticResourceList {
  const staticLeaves = collectObjectLeaves(resourceMap, (value) => !isStoreQuery(value));

  const normalized = staticLeaves.reduce((accu, record) => {
    assignValueToObjectWithPath({
      object: accu,
      path: record.propertyPath,
      value: record.value
    });

    return accu;
  }, {});

  return normalized;
}

results matching ""

    No results matching ""