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;
}