@adaptabletools/adaptable
Version:
Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
546 lines (545 loc) • 29.3 kB
JavaScript
import { isObservable } from 'rxjs';
import * as parser from '../../parser/src';
import { aggregatedBooleanExpressionFunctions, } from '../ExpressionFunctions/aggregatedBooleanExpressionFunctions';
import { addGroupByParams, aggregatedScalarExpressionFunctions, } from '../ExpressionFunctions/aggregatedScalarExpressionFunctions';
import { booleanExpressionFunctions } from '../ExpressionFunctions/booleanExpressionFunctions';
import { extractColumnParameters, extractParameter, } from '../ExpressionFunctions/expressionFunctionUtils';
import { observableExpressionFunctions } from '../ExpressionFunctions/observableExpressionFunctions';
import { scalarExpressionFunctions } from '../ExpressionFunctions/scalarExpressionFunctions';
import { getTypedKeys } from '../Extensions/TypeExtensions';
import { AggregatedScalarLiveValue } from './AggregatedScalarLiveValue';
export class QueryLanguageService {
constructor(adaptableApi) {
this.adaptableApi = adaptableApi;
this.cacheBooleanValidation = new Map();
this.cacheObservableValidation = new Map();
this.cacheAggregatedBooleanValidation = new Map();
this.cacheAggregatedScalarValidation = new Map();
this.cacheModuleSpecificExpressionFunctions = new Map();
}
evaluateBooleanExpression(expression, module, rowNode, evalContext) {
if (expression == undefined) {
// should never happen, but just in case
this.adaptableApi.logError('QueryLanguageService.evaluateBooleanExpression was called with an undefined expression');
return false;
}
const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
return parser.evaluate(expression, {
node: rowNode,
functions: booleanAndScalarFunctions,
evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
dataChangedEvent: evalContext?.dataChangedEvent,
pivotResultColumn: evalContext?.pivotResultColumn,
...this.adaptableApi.internalApi.buildBaseContext(),
});
}
evaluateScalarExpression(expression, module, rowNode) {
// currently scalar and boolean expressions are evaluated the same
return this.evaluateBooleanExpression(expression, module, rowNode);
}
evaluateAggregatedScalarExpression(expression, module, getRowNodes) {
const aggregatedScalarFunctions = this.getModuleExpressionFunctionsMap(module).aggregatedScalarFunctions;
return parser.evaluate(expression, {
node: null, // no node as this is an aggregation query
functions: aggregatedScalarFunctions,
evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
getRowNodes,
...this.adaptableApi.internalApi.buildBaseContext(),
});
}
evaluateObservableExpression(reactiveExpression, module) {
const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
const reactiveExpression$ = parser.evaluate(reactiveExpression, {
node: this.adaptableApi.gridApi.getFirstRowNode(),
functions: moduleExpressionFunctions.observableFunctions,
whereClauseFunctions: booleanAndScalarFunctions,
evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
...this.adaptableApi.internalApi.buildBaseContext(),
});
return reactiveExpression$;
}
evaluateAggregatedBooleanExpression(aggregationExpression, module) {
const moduleExpressionFunctionsMap = this.getModuleExpressionFunctionsMap(module);
const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctionsMap);
const aggregationEvaluation = parser.evaluate(aggregationExpression, {
node: this.adaptableApi.gridApi.getFirstRowNode(),
functions: moduleExpressionFunctionsMap.aggregatedBooleanFunctions,
whereClauseFunctions: booleanAndScalarFunctions,
evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
...this.adaptableApi.internalApi.buildBaseContext(),
});
return aggregationEvaluation;
}
validateBoolean(expressionInput = '', module, config = { force: false }) {
const cacheKey = this.getExpressionCacheKey(expressionInput, module);
try {
if (this.cacheBooleanValidation.has(cacheKey)) {
return this.cacheBooleanValidation.get(cacheKey);
}
const expression = expressionInput.trim();
if (expression === '') {
const result = {
isValid: false,
errorMessage: 'empty boolean expression',
};
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
const force = config?.force ?? false;
// see if validation should be performed
if (!this.adaptableApi.optionsApi.getExpressionOptions().performExpressionValidation &&
!force) {
const result = { isValid: true, errorMessage: '' };
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
const { ast } = parser.parse(expression.trim());
const rootFn = ast[ast.length - 1];
const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
if (rootFn.type === undefined || booleanAndScalarFunctions[rootFn.type] === undefined) {
const result = {
isValid: false,
errorMessage: `provided function ${rootFn?.type} cannot be handled`,
};
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
if (booleanAndScalarFunctions[rootFn.type].returnType !== 'boolean') {
const result = {
isValid: false,
errorMessage: `provided function ${rootFn?.type} is not a predicate`,
};
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
// evaluating the expression is the only way to catch circular named query references
const firstRowNode = this.adaptableApi.gridApi.getFirstRowNode();
// Mock datachangedevent for change-based functions e.g. PERCENT_CHANGE
const dataChangedEvent = {
newValue: 100,
oldValue: 150,
};
this.evaluateBooleanExpression(expression, module, firstRowNode, { dataChangedEvent });
const result = {
isValid: true,
errorMessage: '',
};
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
catch (error) {
const result = { isValid: false, errorMessage: error };
this.cacheBooleanValidation.set(cacheKey, result);
return result;
}
}
validateObservable(expressionInput = '', module) {
const cacheKey = this.getExpressionCacheKey(expressionInput, module);
if (this.cacheObservableValidation.has(cacheKey)) {
return this.cacheObservableValidation.get(cacheKey);
}
const expression = expressionInput.trim();
if (expression === '') {
const result = {
isValid: false,
errorMessage: 'empty observable expression',
};
this.cacheObservableValidation.set(cacheKey, result);
return result;
}
try {
const observableExpression = this.evaluateObservableExpression(expression, module);
if (!isObservable(observableExpression)) {
const result = {
isValid: false,
errorMessage: `provided observable expression does not evaluate to an Observable`,
};
this.cacheObservableValidation.set(cacheKey, result);
return result;
}
const result = { isValid: true, errorMessage: '' };
this.cacheObservableValidation.set(cacheKey, result);
return result;
}
catch (error) {
const result = {
isValid: false,
errorMessage: error,
};
this.cacheObservableValidation.set(cacheKey, result);
return result;
}
}
validateAggregatedBoolean(expressionInput = '', module) {
const cacheKey = this.getExpressionCacheKey(expressionInput, module);
if (this.cacheAggregatedBooleanValidation.has(cacheKey)) {
return this.cacheAggregatedBooleanValidation.get(cacheKey);
}
const expression = expressionInput.trim();
if (expression === '') {
const result = {
isValid: false,
errorMessage: 'empty AggregatedBoolean expression',
};
this.cacheAggregatedBooleanValidation.set(cacheKey, result);
return result;
}
try {
const evaluationResult = this.evaluateAggregatedBooleanExpression(expression, module);
if (evaluationResult.type !== 'aggregationBoolean') {
const result = {
isValid: false,
errorMessage: 'provided AggregatedBoolean expression does not evaluate to a supported aggregation',
};
this.cacheAggregatedBooleanValidation.set(cacheKey, result);
return result;
}
// no exception,so everything seems to be fine
const result = { isValid: true, errorMessage: '' };
this.cacheAggregatedBooleanValidation.set(cacheKey, result);
return result;
}
catch (error) {
const result = {
isValid: false,
errorMessage: error,
};
this.cacheAggregatedBooleanValidation.set(cacheKey, result);
return result;
}
}
validateAggregatedScalar(expressionInput = '', module) {
const cacheKey = this.getExpressionCacheKey(expressionInput, module);
if (this.cacheAggregatedScalarValidation.has(cacheKey)) {
return this.cacheAggregatedScalarValidation.get(cacheKey);
}
const expression = expressionInput.trim();
if (expression === '') {
const result = {
isValid: false,
errorMessage: 'empty AggregatedScalar expression',
};
this.cacheAggregatedScalarValidation.set(cacheKey, result);
return result;
}
try {
const evaluationResult = this.evaluateAggregatedScalarExpression(expression, module);
if (evaluationResult.type !== 'aggregationScalar') {
const result = {
isValid: false,
errorMessage: 'provided AggregatedScalar expression does not evaluate to a supported aggregation',
};
this.cacheAggregatedScalarValidation.set(cacheKey, result);
return result;
}
// no exception,so everything seems to be fine
const result = { isValid: true, errorMessage: '' };
this.cacheAggregatedScalarValidation.set(cacheKey, result);
return result;
}
catch (error) {
const result = {
isValid: false,
errorMessage: error,
};
this.cacheAggregatedScalarValidation.set(cacheKey, result);
return result;
}
}
computeAggregatedBooleanValue(expression, module) {
const booleanAggregationParameter = this.evaluateAggregatedBooleanExpression(expression, module);
const aggregatedScalarExpressionEvaluation = booleanAggregationParameter.scalarAggregation.value;
const aggregatedScalarLiveValue = new AggregatedScalarLiveValue({ aggregatedScalarExpressionEvaluation }, module, this.adaptableApi);
const allAggregationValues = aggregatedScalarLiveValue.getAllAggregationValues();
const numericOperand = booleanAggregationParameter.conditionValue;
const booleanConditionFn = booleanAggregationParameter.conditionFn;
return allAggregationValues.some((aggregationValue) => {
return booleanConditionFn(aggregationValue, numericOperand);
});
}
getColumnsFromExpression(input = '') {
return this.getNodesFromExpression(input, 'COL');
}
getNamedQueryNamesFromExpression(input = '') {
return this.getNodesFromExpression(input, 'QUERY');
}
getExpressionWithColumnFriendlyNames(expression = '') {
let result = expression;
const columnIds = this.getColumnsFromExpression(expression);
columnIds.forEach((columnId) => {
const columnFriendlyName = this.adaptableApi.columnApi.getFriendlyNameForColumnId(columnId);
result = result.split(columnId).join(columnFriendlyName);
});
return result;
}
// Returns the ExpressionFunctions available for the given Module as specified in the `QueryLanguageOptions.moduleExpressionFunctions`
// if there are no specific functions defined, it falls back to the default values
getModuleExpressionFunctionsMap(module) {
const expressionOptions = this.adaptableApi.optionsApi.getExpressionOptions();
if (module) {
let cachedResult = this.cacheModuleSpecificExpressionFunctions.get(module);
if (cachedResult) {
return cachedResult;
}
}
const generalBooleanExpressionFunctions = this.extractMappedExpressionFunctions(booleanExpressionFunctions, expressionOptions.systemBooleanFunctions, expressionOptions.customBooleanFunctions);
const generalScalarExpressionFunctions = this.extractMappedExpressionFunctions(scalarExpressionFunctions, expressionOptions.systemScalarFunctions, expressionOptions.customScalarFunctions);
const generalObservableExpressionFunctions = this.extractMappedExpressionFunctions(observableExpressionFunctions, expressionOptions.systemObservableFunctions);
const generalAggregatedBooleanExpressionFunctions = this.extractMappedExpressionFunctions(aggregatedBooleanExpressionFunctions, expressionOptions.systemAggregatedBooleanFunctions,
// right now the custom aggregated scalar functions are used in the aggregated boolean functions as well
// hopefully we'll simplify this in the future
this.getCustomAggregatedScalarFunctions());
const generalAggregatedScalarExpressionFunctions = this.extractMappedExpressionFunctions(aggregatedScalarExpressionFunctions, expressionOptions.systemAggregatedScalarFunctions, this.getCustomAggregatedScalarFunctions());
if (!module) {
this.adaptableApi.logWarn(`QueryLanguageService.getModuleExpressionFunctions() was called with an undefined 'module' param, this should never happen`);
return {
booleanFunctions: generalBooleanExpressionFunctions,
scalarFunctions: generalScalarExpressionFunctions,
observableFunctions: generalObservableExpressionFunctions,
aggregatedBooleanFunctions: generalAggregatedBooleanExpressionFunctions,
aggregatedScalarFunctions: generalAggregatedScalarExpressionFunctions,
};
}
let moduleExpressionFunctions;
if (typeof expressionOptions.moduleExpressionFunctions === 'function') {
const context = {
module,
availableBooleanFunctionNames: getTypedKeys(generalBooleanExpressionFunctions),
availableScalarFunctionNames: getTypedKeys(generalScalarExpressionFunctions),
availableObservableFunctionNames: getTypedKeys(generalObservableExpressionFunctions),
availableAggregatedBooleanFunctionNames: getTypedKeys(generalAggregatedBooleanExpressionFunctions),
availableAggregatedScalarFunctionNames: getTypedKeys(generalAggregatedScalarExpressionFunctions),
...this.adaptableApi.internalApi.buildBaseContext(),
};
moduleExpressionFunctions = expressionOptions.moduleExpressionFunctions(context) ?? {};
}
else {
moduleExpressionFunctions = expressionOptions.moduleExpressionFunctions?.[module] ?? {};
}
const moduleExpressionFunctionsMap = {
booleanFunctions: this.extractMappedExpressionFunctions(generalBooleanExpressionFunctions, moduleExpressionFunctions.systemBooleanFunctions, moduleExpressionFunctions.customBooleanFunctions),
scalarFunctions: this.extractMappedExpressionFunctions(generalScalarExpressionFunctions, moduleExpressionFunctions.systemScalarFunctions, moduleExpressionFunctions.customScalarFunctions),
observableFunctions: this.extractMappedExpressionFunctions(generalObservableExpressionFunctions, moduleExpressionFunctions.systemObservableFunctions),
aggregatedBooleanFunctions: this.extractMappedExpressionFunctions(generalAggregatedBooleanExpressionFunctions, moduleExpressionFunctions.systemAggregatedBooleanFunctions),
aggregatedScalarFunctions: this.extractMappedExpressionFunctions(generalAggregatedScalarExpressionFunctions, moduleExpressionFunctions.systemAggregatedScalarFunctions),
};
this.cacheModuleSpecificExpressionFunctions.set(module, moduleExpressionFunctionsMap);
return moduleExpressionFunctionsMap;
}
getCustomAggregatedScalarFunctions() {
const customAggregatedScalarFunctions = this.adaptableApi.optionsApi.getExpressionOptions()?.customAggregatedFunctions;
if (!customAggregatedScalarFunctions) {
return {};
}
if (typeof customAggregatedScalarFunctions === 'function') {
return (context) => {
const customDefinitions = customAggregatedScalarFunctions(context);
return this.mapCustomAggregatedScalarFunctionsToInternal(customDefinitions);
};
}
return this.mapCustomAggregatedScalarFunctionsToInternal(customAggregatedScalarFunctions);
}
mapCustomAggregatedScalarFunctionsToInternal(aggregatedScalarFunctions) {
const getValueForColId = (colId, rowNode) => {
return this.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, colId);
};
return Object.entries(aggregatedScalarFunctions).reduce((acc, [expressionName, customExpressionDefinition]) => {
const expression = {
handler: (args, context) => {
const [aggColParam, ...restOfColParams] = extractColumnParameters(expressionName, args);
const aggColumnId = aggColParam?.value;
const groupByParameter = extractParameter(expressionName, 'operand', ['GROUP_BY'], args, {
isOptional: true,
});
const groupByColumnIds = groupByParameter?.value;
const contextArgs = args.filter((arg) => {
if (typeof arg === 'object' && arg.name === 'COL') {
// filter out aggregation column
return arg.value !== aggColumnId;
}
if (typeof arg === 'object' && arg.name === 'GROUP_BY') {
// filter out groupBy column
return false;
}
return true;
});
const aggregationExpressionEvaluation = {
aggregationParams: {
reducers: {
[expressionName]: {
name: expressionName,
field: aggColumnId,
initialValue: customExpressionDefinition.initialValue,
reducer: (acc, value, rowNode, dataIndex) => {
const context = {
accumulator: acc,
currentValue: value,
index: dataIndex,
rowNode,
args: contextArgs.map((arg) => {
// if col, replace with value
if (typeof arg === 'object' && arg.name === 'COL') {
return getValueForColId(arg.value, rowNode);
}
return arg;
}),
aggColumnId,
groupByColumnIds,
getValueForColId: (colId) => getValueForColId(colId, rowNode),
...this.adaptableApi.internalApi.buildBaseContext(),
};
return customExpressionDefinition.reducer(context);
},
done: (accValue, dataArray) => {
if (customExpressionDefinition.processAggregatedValue) {
const context = {
...this.adaptableApi.internalApi.buildBaseContext(),
aggregatedValue: accValue,
rowNodes: dataArray,
args: contextArgs,
aggColumnId,
groupByColumnIds,
};
return customExpressionDefinition.processAggregatedValue(context);
}
return accValue;
},
},
},
},
rowValueGetter: (rowNode, aggregationValue) => {
if (customExpressionDefinition.prepareRowValue) {
const context = {
rowNode: rowNode,
aggregatedValue: aggregationValue,
...this.adaptableApi.internalApi.buildBaseContext(),
args: contextArgs.map((arg) => {
// if col, replace with value
if (typeof arg === 'object' && arg.name === 'COL') {
return getValueForColId(arg.value, rowNode);
}
return arg;
}),
aggColumnId,
groupByColumnIds,
getValueForColId: (colId) => getValueForColId(colId, rowNode),
};
return customExpressionDefinition.prepareRowValue(context);
}
return aggregationValue;
},
rowFilterFn: (rowNode) => {
if (customExpressionDefinition.filterRow) {
return customExpressionDefinition.filterRow({
rowNode,
...this.adaptableApi.internalApi.buildBaseContext(),
args: contextArgs.map((arg) => {
// if col, replace with value
if (typeof arg === 'object' && arg.name === 'COL') {
return getValueForColId(arg.value, rowNode);
}
return arg;
}),
aggColumnId,
groupByColumnIds,
getValueForColId: (colId) => getValueForColId(colId, rowNode),
});
}
return context?.filterFn?.(rowNode) ?? true;
},
getRowNodes: context.getRowNodes,
};
addGroupByParams(groupByParameter?.value, aggregationExpressionEvaluation);
const result = {
name: expressionName,
type: 'aggregationScalar',
value: aggregationExpressionEvaluation,
};
return result;
},
description: customExpressionDefinition.description,
signatures: customExpressionDefinition.signatures,
inputs: customExpressionDefinition.inputTypes,
};
acc[expressionName] = expression;
return acc;
}, {});
}
extractMappedExpressionFunctions(availableExpressionFunctions, systemFunctions, customFunctions) {
const systemFunctionNames = typeof systemFunctions === 'function'
? systemFunctions({
availableExpressionFunctionNames: getTypedKeys(availableExpressionFunctions),
...this.adaptableApi.internalApi.buildBaseContext(),
})
: systemFunctions;
let generalExpressionFunctions = {};
// add system functions
if (Array.isArray(systemFunctionNames)) {
// add only system functions specified by user
systemFunctionNames.forEach((systemFunctionName) => {
generalExpressionFunctions[systemFunctionName] =
availableExpressionFunctions[systemFunctionName];
});
}
else {
// add ALL system functions
generalExpressionFunctions = { ...availableExpressionFunctions };
}
const customFunctionDefinitions = typeof customFunctions === 'function'
? customFunctions({
availableExpressionFunctionNames: getTypedKeys(generalExpressionFunctions),
...this.adaptableApi.internalApi.buildBaseContext(),
})
: customFunctions;
if (customFunctionDefinitions) {
generalExpressionFunctions = { ...generalExpressionFunctions, ...customFunctionDefinitions };
}
return generalExpressionFunctions;
}
evaluateCustomQueryVariable(functionName, args) {
const context = {
args,
...this.adaptableApi.internalApi.buildBaseContext(),
};
const customQueryVariableDefinition = this.adaptableApi.optionsApi.getExpressionOptions()?.customQueryVariables?.[functionName];
return typeof customQueryVariableDefinition === 'function'
? customQueryVariableDefinition(context)
: customQueryVariableDefinition;
}
getBooleanAndScalarFunctions(moduleExpressionFunctionsMap) {
return {
...moduleExpressionFunctionsMap.booleanFunctions,
...moduleExpressionFunctionsMap.scalarFunctions,
};
}
getExpressionCacheKey(expression, module) {
return `${module}::${expression}`;
}
getNodesFromExpression(input, nodeType) {
try {
const resultSet = new Set();
// @ts-ignore
const walk = (node) => {
if (typeof node !== 'object') {
return false;
}
if (Array.isArray(node)) {
return node.map(walk);
}
node.args.map(walk);
if (node.type === nodeType) {
resultSet.add(String(node.args[0]));
}
};
const { ast } = parser.parse(input.trim());
walk(ast);
return Array.from(resultSet.values());
}
catch (e) {
return [];
}
}
}