PromucFlow_constructor/app/client/src/utils/DynamicBindingUtils.ts

388 lines
11 KiB
TypeScript
Raw Normal View History

2019-11-06 06:35:15 +00:00
import _ from "lodash";
2019-11-25 05:07:27 +00:00
import { WidgetProps } from "widgets/BaseWidget";
2020-01-02 13:36:35 +00:00
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
2019-11-19 12:44:58 +00:00
import ValidationFactory from "./ValidationFactory";
2020-02-18 10:41:52 +00:00
import JSExecutionManagerSingleton, {
JSExecutorResult,
} from "jsExecution/JSExecutionManagerSingleton";
2020-01-08 10:50:49 +00:00
import unescapeJS from "unescape-js";
2020-01-17 09:28:26 +00:00
import toposort from "toposort";
2020-02-18 10:41:52 +00:00
import {
DataTree,
DataTreeAction,
DataTreeEntity,
2020-02-18 10:41:52 +00:00
DataTreeWidget,
ENTITY_TYPE,
} from "entities/DataTree/dataTreeFactory";
2019-12-06 13:16:08 +00:00
2020-01-27 13:53:33 +00:00
export const removeBindingsFromObject = (obj: object) => {
const string = JSON.stringify(obj);
const withBindings = string.replace(DATA_BIND_REGEX, "{{ }}");
return JSON.parse(withBindings);
};
2019-11-14 09:28:51 +00:00
export const isDynamicValue = (value: string): boolean =>
2019-12-02 07:37:33 +00:00
DATA_BIND_REGEX.test(value);
2019-11-28 03:56:44 +00:00
//{{}}{{}}}
2019-12-02 09:51:18 +00:00
export function parseDynamicString(dynamicString: string): string[] {
2019-11-28 03:56:44 +00:00
let parsedDynamicValues = [];
const indexOfDoubleParanStart = dynamicString.indexOf("{{");
if (indexOfDoubleParanStart === -1) {
return [dynamicString];
}
//{{}}{{}}}
const firstString = dynamicString.substring(0, indexOfDoubleParanStart);
firstString && parsedDynamicValues.push(firstString);
let rest = dynamicString.substring(
indexOfDoubleParanStart,
dynamicString.length,
);
//{{}}{{}}}
let sum = 0;
for (let i = 0; i <= rest.length - 1; i++) {
const char = rest[i];
const prevChar = rest[i - 1];
if (char === "{") {
sum++;
} else if (char === "}") {
sum--;
if (prevChar === "}" && sum === 0) {
parsedDynamicValues.push(rest.substring(0, i + 1));
rest = rest.substring(i + 1, rest.length);
if (rest) {
parsedDynamicValues = parsedDynamicValues.concat(
parseDynamicString(rest),
);
break;
}
}
}
}
if (sum !== 0 && dynamicString !== "") {
return [dynamicString];
}
return parsedDynamicValues;
}
2019-11-14 09:28:51 +00:00
2020-01-17 09:28:26 +00:00
const getAllPaths = (
tree: Record<string, any>,
prefix = "",
): Record<string, true> => {
return Object.keys(tree).reduce((res: Record<string, true>, el): Record<
string,
true
> => {
if (Array.isArray(tree[el])) {
const key = `${prefix}${el}`;
return { ...res, [key]: true };
} else if (typeof tree[el] === "object" && tree[el] !== null) {
const key = `${prefix}${el}`;
return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) };
} else {
const key = `${prefix}${el}`;
return { ...res, [key]: true };
}
}, {});
};
2019-11-14 09:28:51 +00:00
export const getDynamicBindings = (
dynamicString: string,
): { bindings: string[]; paths: string[] } => {
2020-03-06 09:45:21 +00:00
// Protect against bad string parse
if (!dynamicString || !_.isString(dynamicString)) {
return { bindings: [], paths: [] };
}
2020-01-17 09:28:26 +00:00
const sanitisedString = dynamicString.trim();
2019-11-14 09:28:51 +00:00
// Get the {{binding}} bound values
2020-01-17 09:28:26 +00:00
const bindings = parseDynamicString(sanitisedString);
2019-11-14 09:28:51 +00:00
// Get the "binding" path values
2019-11-28 03:56:44 +00:00
const paths = bindings.map(binding => {
const length = binding.length;
2019-12-02 07:37:33 +00:00
const matches = binding.match(DATA_BIND_REGEX);
2019-11-28 03:56:44 +00:00
if (matches) {
return binding.substring(2, length - 2);
}
2019-11-14 09:28:51 +00:00
return "";
});
return { bindings, paths };
};
2019-11-06 06:35:15 +00:00
// Paths are expected to have "{name}.{path}" signature
2020-02-18 10:41:52 +00:00
// Also returns any action triggers found after evaluating value
export const evaluateDynamicBoundValue = (
2020-02-18 10:41:52 +00:00
data: DataTree,
2019-11-06 06:35:15 +00:00
path: string,
2020-02-18 10:41:52 +00:00
callbackData?: any,
): JSExecutorResult => {
2020-01-08 10:50:49 +00:00
const unescapedInput = unescapeJS(path);
2020-02-18 10:41:52 +00:00
return JSExecutionManagerSingleton.evaluateSync(
unescapedInput,
data,
callbackData,
);
2019-11-14 09:28:51 +00:00
};
// For creating a final value where bindings could be in a template format
export const createDynamicValueString = (
binding: string,
subBindings: string[],
subValues: string[],
): string => {
// Replace the string with the data tree values
let finalValue = binding;
subBindings.forEach((b, i) => {
let value = subValues[i];
if (Array.isArray(value) || _.isObject(value)) {
value = JSON.stringify(value);
}
finalValue = finalValue.replace(b, value);
});
return finalValue;
};
export const getDynamicValue = (
dynamicBinding: string,
2020-02-18 10:41:52 +00:00
data: DataTree,
callBackData?: any,
includeTriggers = false,
): JSExecutorResult => {
2019-11-14 09:28:51 +00:00
// Get the {{binding}} bound values
const { bindings, paths } = getDynamicBindings(dynamicBinding);
if (bindings.length) {
// Get the Data Tree value of those "binding "paths
2019-11-28 03:56:44 +00:00
const values = paths.map((p, i) => {
2019-12-30 07:39:53 +00:00
if (p) {
2020-02-18 10:41:52 +00:00
const result = evaluateDynamicBoundValue(data, p, callBackData);
if (includeTriggers) {
return result;
} else {
return { result: result.result };
}
2019-12-30 07:39:53 +00:00
} else {
2020-02-18 10:41:52 +00:00
return { result: bindings[i], triggers: [] };
2019-12-30 07:39:53 +00:00
}
2019-11-28 03:56:44 +00:00
});
2019-11-14 09:28:51 +00:00
// if it is just one binding, no need to create template string
if (bindings.length === 1) return values[0];
// else return a string template with bindings
2020-02-18 10:41:52 +00:00
const templateString = createDynamicValueString(
dynamicBinding,
bindings,
values.map(v => v.result),
);
return {
result: templateString,
};
2019-11-14 09:28:51 +00:00
}
2020-02-18 10:41:52 +00:00
return { result: undefined, triggers: [] };
2019-11-06 06:35:15 +00:00
};
export const getValidatedTree = (tree: any) => {
2020-01-17 09:28:26 +00:00
return Object.keys(tree).reduce((tree, entityKey: string) => {
const entity = tree[entityKey];
if (entity && entity.type) {
const parsedEntity = { ...entity };
Object.keys(entity).forEach((property: string) => {
const value = entity[property];
// Pass it through parse
const {
parsed,
isValid,
message,
} = ValidationFactory.validateWidgetProperty(
2020-01-17 09:28:26 +00:00
entity.type,
property,
value,
);
parsedEntity[property] = parsed;
if (!isValid) {
_.set(parsedEntity, `invalidProps.${property}`, true);
_.set(parsedEntity, `validationMessages.${property}`, message);
}
2020-01-17 09:28:26 +00:00
});
return { ...tree, [entityKey]: parsedEntity };
}
return tree;
}, tree);
};
export const getEvaluatedDataTree = (dataTree: DataTree): DataTree => {
2020-01-17 09:28:26 +00:00
const dynamicDependencyMap = createDependencyTree(dataTree);
const evaluatedTree = dependencySortedEvaluateDataTree(
dataTree,
dynamicDependencyMap,
);
2020-01-30 13:23:04 +00:00
const treeWithLoading = setTreeLoading(evaluatedTree, dynamicDependencyMap);
return getValidatedTree(treeWithLoading);
2020-01-17 09:28:26 +00:00
};
type DynamicDependencyMap = Record<string, Array<string>>;
export const createDependencyTree = (
2020-02-18 10:41:52 +00:00
dataTree: DataTree,
2020-01-17 09:28:26 +00:00
): Array<[string, string]> => {
const dependencyMap: DynamicDependencyMap = {};
const allKeys = getAllPaths(dataTree);
Object.keys(dataTree).forEach(entityKey => {
const entity = dataTree[entityKey] as WidgetProps;
if (entity && entity.dynamicBindings) {
Object.keys(entity.dynamicBindings).forEach(prop => {
const { paths } = getDynamicBindings(entity[prop]);
dependencyMap[`${entityKey}.${prop}`] = paths.filter(p => !!p);
});
}
});
Object.keys(dependencyMap).forEach(key => {
dependencyMap[key] = _.flatten(
dependencyMap[key].map(path => calculateSubDependencies(path, allKeys)),
);
});
const dependencyTree: Array<[string, string]> = [];
Object.keys(dependencyMap).forEach((key: string) => {
2020-02-03 11:49:20 +00:00
if (dependencyMap[key].length) {
dependencyMap[key].forEach(dep => dependencyTree.push([key, dep]));
} else {
// Set no dependency
dependencyTree.push([key, ""]);
}
2020-01-17 09:28:26 +00:00
});
return dependencyTree;
};
const calculateSubDependencies = (
path: string,
all: Record<string, true>,
): Array<string> => {
const subDeps: Array<string> = [];
const identifiers = path.match(/[a-zA-Z_$][a-zA-Z_$0-9.]*/g) || [path];
identifiers.forEach((identifier: string) => {
if (identifier in all) {
subDeps.push(identifier);
} else {
const subIdentifiers =
identifier.match(/[a-zA-Z_$][a-zA-Z_$0-9]*/g) || [];
let current = "";
for (let i = 0; i < subIdentifiers.length; i++) {
const key = `${current}${current ? "." : ""}${subIdentifiers[i]}`;
if (key in all) {
current = key;
} else {
break;
}
}
if (current) subDeps.push(current);
}
});
return subDeps;
};
2020-01-30 13:23:04 +00:00
export const setTreeLoading = (
2020-02-18 10:41:52 +00:00
dataTree: DataTree,
2020-01-30 13:23:04 +00:00
dependencyMap: Array<[string, string]>,
) => {
Object.keys(dataTree)
2020-02-18 10:41:52 +00:00
.filter(e => {
const entity = dataTree[e] as DataTreeAction;
return entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && entity.isLoading;
})
2020-01-30 13:23:04 +00:00
.reduce(
(allEntities: string[], curr) =>
allEntities.concat(getEntityDependencies(dependencyMap, curr)),
[],
)
2020-02-18 10:41:52 +00:00
.forEach(w => {
const entity = dataTree[w] as DataTreeWidget;
2020-02-18 10:41:52 +00:00
entity.isLoading = true;
});
return dataTree;
2020-01-30 13:23:04 +00:00
};
export const getEntityDependencies = (
dependencyMap: Array<[string, string]>,
entity: string,
): Array<string> => {
const entityDeps: Record<string, string[]> = dependencyMap
.map(d => [d[1].split(".")[0], d[0].split(".")[0]])
.filter(d => d[0] !== d[1])
.reduce((deps: Record<string, string[]>, dep) => {
const key: string = dep[0];
const value: string = dep[1];
return {
...deps,
[key]: deps[key] ? deps[key].concat(value) : [value],
};
}, {});
if (entity in entityDeps) {
const recFind = (
keys: Array<string>,
deps: Record<string, string[]>,
): Array<string> => {
let allDeps: string[] = [];
keys.forEach(e => {
allDeps = allDeps.concat([e]);
if (e in deps) {
allDeps = allDeps.concat([...recFind(deps[e], deps)]);
}
});
return allDeps;
};
return recFind(entityDeps[entity], entityDeps);
}
return [];
};
2020-01-17 09:28:26 +00:00
export function dependencySortedEvaluateDataTree(
2020-02-18 10:41:52 +00:00
dataTree: DataTree,
2020-01-17 09:28:26 +00:00
dependencyTree: Array<[string, string]>,
2020-02-18 10:41:52 +00:00
): DataTree {
2020-01-27 13:53:33 +00:00
const tree = _.cloneDeep(dataTree);
2020-01-17 09:28:26 +00:00
try {
2020-02-03 11:49:20 +00:00
// sort dependencies and remove empty dependencies
const sortedDependencies = toposort(dependencyTree)
.reverse()
.filter(d => !!d);
2020-01-17 09:28:26 +00:00
// evaluate and replace values
2020-02-18 10:41:52 +00:00
return sortedDependencies.reduce((currentTree: DataTree, path: string) => {
const entityName = path.split(".")[0];
const entity: DataTreeEntity = currentTree[entityName];
let result = _.get(currentTree as any, path);
if (isDynamicValue(result)) {
2020-03-03 06:51:59 +00:00
try {
const dynamicResult = getDynamicValue(result, currentTree);
result = dynamicResult.result;
} catch (e) {
console.error(e);
result = undefined;
}
2020-02-18 10:41:52 +00:00
}
if (
"ENTITY_TYPE" in entity &&
entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET
) {
const propertyPath = path.split(".")[1];
const {
parsed,
isValid,
message,
} = ValidationFactory.validateWidgetProperty(
entity.type,
propertyPath,
2020-02-18 10:41:52 +00:00
result,
2020-01-17 09:28:26 +00:00
);
2020-02-18 10:41:52 +00:00
result = parsed;
if (!isValid) {
_.set(entity, `invalidProps.${propertyPath}`, true);
_.set(entity, `validationMessages.${propertyPath}`, message);
}
2020-02-18 10:41:52 +00:00
}
return _.set(currentTree, path, result);
}, tree);
2020-01-17 09:28:26 +00:00
} catch (e) {
console.error(e);
return tree;
}
}