UNPKG

@adaptabletools/adaptable

Version:

Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

545 lines (544 loc) 29.2 kB
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, dataChangedEvent) { 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, ...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 []; } } }