File

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

Description

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.

Index

Properties

Properties

key
Type string
query
Type string
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 ""