UNPKG

@adaptabletools/adaptable

Version:

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

449 lines (448 loc) 20.2 kB
import { defer, of } from 'rxjs'; import { count, debounceTime, delay, filter, map, mergeAll, mergeMap, startWith, takeUntil, tap, withLatestFrom, } from 'rxjs/operators'; import { parseIntFn as parseInt } from '../Extensions/NumberExtensions'; import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError'; import { extractColumnParameter, extractParameter, getDataChangeLog$, getGridChangeLog$, getNumericValue, handleColumnFunction, handleWhereFunction, validateColumnType, } from './expressionFunctionUtils'; import { getTypedKeys } from '../Extensions/TypeExtensions'; const TIMEFRAME_REGEX = /^(\d+)(s|m|h)$/i; const SYSTEM_MAX_TIMEFRAME_SIZE = 86400000; export const observableExpressionFunctions = { WHERE: { handler(args, context) { return handleWhereFunction(args, context); }, isHiddenFromMenu: true, description: 'Limits the rows against which the Observable expression will be evaluated', signatures: ['<main_query> WHERE <boolean_query>'], examples: ['<main_query> WHERE <boolean_query>', '<main_query> WHERE QUERY("abc")'], hasEagerEvaluation: true, category: 'conditional', }, ROW_CHANGE: { handler(args, context) { const operandParameter = extractParameter('ROW_CHANGE', 'operand', ['COUNT', 'MAX', 'MIN', 'NONE'], args); const timeframeParameter = extractParameter('ROW_CHANGE', 'config', ['TIMEFRAME'], args); let dataChangeLog$ = getDataChangeLog$(context, operandParameter.column); switch (operandParameter.name) { case 'COUNT': { const timeframeChange$ = getTrailingRowCountChange$(dataChangeLog$, timeframeParameter.value, operandParameter.value); return getDataChangeCount$(dataChangeLog$, timeframeChange$, operandParameter.value); } case 'MIN': { validateColumnType(operandParameter.column, ['number'], 'MIN', context.adaptableApi); const timeframeChange$ = getTrailingRowValueChange$(dataChangeLog$, timeframeParameter.value); return getDataChangeMin$(dataChangeLog$, timeframeChange$); } case 'MAX': { validateColumnType(operandParameter.column, ['number'], 'MAX', context.adaptableApi); const timeframeChange$ = getTrailingRowValueChange$(dataChangeLog$, timeframeParameter.value); return getDataChangeMax$(dataChangeLog$, timeframeChange$); } case 'NONE': { return dataChangeLog$.pipe(debounceTime(timeframeParameter.value), takeUntil(dataChangeLog$.pipe(count()))); } } }, returnType: 'boolean', description: 'Observes changes with each row in a distinct scope', signatures: ['ROW_CHANGE(changeFunction: COUNT|MIN|MAX|NONE, timeframe:TIMEFRAME)'], examples: [ "ROW_CHANGE( COUNT([columnName],2),TIMEFRAME('20s'))", "ROW_CHANGE( MAX(COL('columnName')), TIMEFRAME('20s'))", ], category: 'observable', }, GRID_CHANGE: { handler(args, context) { const operandParameter = extractParameter('GRID_CHANGE', 'operand', ['COUNT', 'MAX', 'MIN', 'NONE'], args); const timeframeParameter = extractParameter('GRID_CHANGE', 'config', ['TIMEFRAME'], args); let dataChangeLog$ = getDataChangeLog$(context, operandParameter.column); switch (operandParameter.name) { case 'COUNT': { const timeframeChange$ = getTrailingGridCountChange$(dataChangeLog$, timeframeParameter.value, operandParameter.value); return getDataChangeCount$(dataChangeLog$, timeframeChange$, operandParameter.value); } case 'MIN': { validateColumnType(operandParameter.column, ['number'], 'MIN', context.adaptableApi); const timeframeChange$ = getTrailingGridValueChange$(dataChangeLog$, timeframeParameter.value); return getDataChangeMin$(dataChangeLog$, timeframeChange$); } case 'MAX': { validateColumnType(operandParameter.column, ['number'], 'MAX', context.adaptableApi); const timeframeChange$ = getTrailingGridValueChange$(dataChangeLog$, timeframeParameter.value); return getDataChangeMax$(dataChangeLog$, timeframeChange$); } case 'NONE': { return dataChangeLog$.pipe(startWith(getDataChangedInfoStub(context)), debounceTime(timeframeParameter.value), takeUntil(dataChangeLog$.pipe(count()))); } } }, returnType: 'boolean', description: 'Observes changes with the entire grid in a single scope', signatures: ['GRID_CHANGE(changeFunction: COUNT|MIN|MAX|NONE, timeframe:TIMEFRAME)'], examples: [ "GRID_CHANGE( COUNT([columnName],2),TIMEFRAME('20s'))", "GRID_CHANGE( MAX(COL('columnName')), TIMEFRAME('20s'))", ], category: 'observable', }, ROW_ADDED: { handler(args, context) { const gridChangeLog$ = getGridChangeLog$(context, 'Add'); return handleGridRowAddedOrRemoved(args, gridChangeLog$, 'ROW_ADDED', context); }, returnType: 'boolean', description: 'Observes added rows in the grid', signatures: [ 'ROW_ADDED()', 'ROW_ADDED(rowCount: number)', 'ROW_ADDED(timeframe:TIMEFRAME)', 'ROW_ADDED(rowCount: number, timeframe:TIMEFRAME)', ], examples: [ 'ROW_ADDED()', 'ROW_ADDED(2)', "ROW_ADDED(TIMEFRAME('20s'))", "ROW_ADDED(2, TIMEFRAME('20s'))", ], category: 'observable', }, ROW_REMOVED: { handler(args, context) { const gridChangeLog$ = getGridChangeLog$(context, 'Delete'); return handleGridRowAddedOrRemoved(args, gridChangeLog$, 'ROW_REMOVED', context); }, returnType: 'boolean', description: 'Observes removed rows in the grid', signatures: [ 'ROW_REMOVED()', 'ROW_REMOVED(rowCount: number)', 'ROW_REMOVED(timeframe:TIMEFRAME)', 'ROW_REMOVED(rowCount: number, timeframe:TIMEFRAME)', ], examples: [ 'ROW_REMOVED()', 'ROW_REMOVED(2)', "ROW_REMOVED(TIMEFRAME('20s'))", "ROW_REMOVED(2, TIMEFRAME('20s'))", ], category: 'observable', }, COL: { handler(args, context) { return handleColumnFunction(args, context); }, description: 'References a column by its unique identifier', signatures: ['[colName]', 'COL(name: string)'], examples: ['[col1]', "COL('col1')"], category: 'special', }, COUNT: { handler(args) { const columnParameter = extractColumnParameter('COUNT', args); const countValue = args.find((arg) => typeof arg === 'number'); if (countValue == null || countValue <= 0) { throw new ExpressionEvaluationError('COUNT', 'expects a positive number as argument'); } const result = { type: 'operand', name: 'COUNT', value: countValue, column: columnParameter.value, }; return result; }, description: 'Observes if a column value has changed a given number of times.\nValid only as an operand for an observable function.', signatures: [ 'COUNT([colName],changeCount: number)', 'COUNT(COL(name: string),changeCount: number)', ], examples: ['COUNT([colName],2)', `COUNT(COL('col1'),10)`], category: 'operand', }, NONE: { handler(args) { const columnParameter = extractColumnParameter('NONE', args); const result = { type: 'operand', name: 'NONE', column: columnParameter.value, }; return result; }, description: 'Observes if a column value has NOT changed.\nValid only as an operand for an observable function.', signatures: ['NONE([colName])', 'NONE(COL(name: string))'], examples: ['NONE([colA])', `NONE(COL('col1'))`], category: 'operand', }, MIN: { handler(args) { const columnParameter = extractColumnParameter('MIN', args); const result = { type: 'operand', name: 'MIN', column: columnParameter.value, }; return result; }, description: 'Observes if the changed column value is the lowest.\nValid only as an operand for an observable function.', signatures: ['MIN([colName])', 'MIN(COL(name: string))'], examples: ['MIN([colA])', `MIN(COL('col1'))`], category: 'operand', }, MAX: { handler(args) { const columnParameter = extractColumnParameter('MAX', args); const result = { type: 'operand', name: 'MAX', column: columnParameter.value, }; return result; }, description: 'Observes if the changed column value is the highest.\nValid only as an operand for an observable function.', signatures: ['MAX([colName])', 'MAX(COL(name: string))'], examples: ['MAX([colA])', `MAX(COL('col1'))`], category: 'operand', }, TIMEFRAME: { handler(args, context) { const durationUnitRatios = { s: 1000, m: 60000, h: 3600000, }; const input = args[0] + ''; const matchingResult = input.match(TIMEFRAME_REGEX); const value = matchingResult?.[1]; const unit = matchingResult?.[2]?.toLowerCase(); let duration; if (value && unit && durationUnitRatios[unit]) { duration = parseInt(value) * durationUnitRatios[unit]; } if (!duration) { throw new ExpressionEvaluationError('TIMEFRAME', `timeframe expression is invalid`); } duration = getMaxTimeframeSize(duration, context); const result = { type: 'config', name: 'TIMEFRAME', value: duration, }; return result; }, description: 'Limits the monitoring operators to a trailing timeframe defined in seconds, minutes or hours.\nValid only as an operand for an observable function.', signatures: [ "TIMEFRAME('%numeric_value%s')", "TIMEFRAME('%numeric_value%m')", "TIMEFRAME('%numeric_value%h')", ], examples: ["TIMEFRAME('20s')", "TIMEFRAME('5m')", "TIMEFRAME('1h')"], category: 'operand', }, }; export const observableExpressionFunctionNames = getTypedKeys(observableExpressionFunctions); const isLastElementMaxValue = (values) => { const [tailValue] = values.slice(-1); const restValues = values.slice(0, -1); return restValues.every((value) => value < tailValue); }; const isLastElementMinValue = (values) => { const [tailValue] = values.slice(-1); const restValues = values.slice(0, -1); return restValues.every((value) => value > tailValue); }; const getDataChangedInfoStub = (context) => { let rowNodeStub; if (!context.filterFn) { rowNodeStub = context.node ?? context.adaptableApi.gridApi.getFirstRowNode(); } else { context.adaptableApi.internalApi.forAllRowNodesDo((rowNode) => { if (!rowNodeStub) { if (context.filterFn(rowNode)) { rowNodeStub = rowNode; } } }); } const rowNode = rowNodeStub; if (rowNode) { const primaryKeyValue = context.adaptableApi.gridApi.getPrimaryKeyValueForRowNode(rowNode); const columnId = context.adaptableApi.columnApi.internalApi.getQueryableColumnsForUIEditor()[0]?.columnId; const oldValue = context.adaptableApi.gridApi.getCellRawValue(primaryKeyValue, columnId); const column = context.adaptableApi.columnApi.getColumnWithColumnId(columnId); const newValue = oldValue; return { changedAt: Date.now(), rowNode, primaryKeyValue, column: column, oldValue, newValue, }; } else { return { changedAt: Date.now(), }; } }; const getDataChangeCount$ = (dataChangeLog$, timeframeChange$, targetCount) => { return dataChangeLog$.pipe(withLatestFrom(timeframeChange$), filter(([_, changeCountWithinTimeframe]) => { return targetCount === changeCountWithinTimeframe; }), map(([source]) => source)); }; const getDataChangeMin$ = (dataChangeLog$, timeframeChange$) => { return dataChangeLog$.pipe(withLatestFrom(timeframeChange$), filter(([changeLog, values]) => { return values.length > 1 ? isLastElementMinValue(values) : getNumericValue(changeLog.oldValue) > getNumericValue(changeLog.newValue); }), map(([changeLog]) => { return changeLog; })); }; const getDataChangeMax$ = (dataChangeLog$, timeframeChange$) => { return dataChangeLog$.pipe(withLatestFrom(timeframeChange$), filter(([changeLog, values]) => { return values.length > 1 ? isLastElementMaxValue(values) : getNumericValue(changeLog.oldValue) < getNumericValue(changeLog.newValue); }), map(([changeLog]) => { return changeLog; })); }; const getTrailingGridCountChange$ = (source$, trailingPeriod, counterLimit) => { return defer(() => { let counter = 0; const movingTimeWindow$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => { counter++; }, (dataChangeLog) => { if (counter > 0) { counter--; } }); return movingTimeWindow$.pipe(map(() => { const gridCounter = counter; if (gridCounter === counterLimit) { counter = 0; } return gridCounter; })); }); }; const getTrailingRowCountChange$ = (source$, trailingPeriod, counterLimit) => { return defer(() => { const counterMap = new Map(); const getCellCounter = (dataChangeLog) => counterMap.get(dataChangeLog.primaryKeyValue) ?? 0; const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => { let currentCounter = getCellCounter(dataChangeLog); counterMap.set(dataChangeLog.primaryKeyValue, ++currentCounter); }, (dataChangeLog) => { let currentCounter = getCellCounter(dataChangeLog); if (currentCounter > 0) { counterMap.set(dataChangeLog.primaryKeyValue, --currentCounter); } }); return slidingTimeframe$.pipe(map((dataChangeLog) => { const cellCounter = getCellCounter(dataChangeLog); if (cellCounter === counterLimit) { counterMap.set(dataChangeLog.primaryKeyValue, 0); } return cellCounter; })); }); }; const getTrailingGridValueChange$ = (source$, trailingPeriod) => { return defer(() => { let values = []; const doubleInsertionsMap = new WeakMap(); const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => { if (!values.length) { values.push(getNumericValue(dataChangeLog.oldValue)); doubleInsertionsMap.set(dataChangeLog, true); } values.push(getNumericValue(dataChangeLog.newValue)); }, (dataChangeLog) => { values.shift(); if (doubleInsertionsMap.get(dataChangeLog)) { values.shift(); doubleInsertionsMap.delete(dataChangeLog); } }); return slidingTimeframe$.pipe(map(() => values)); }); }; const getTrailingRowValueChange$ = (source$, trailingPeriod) => { return defer(() => { const rowValuesMap = new Map(); const doubleInsertionsMap = new WeakMap(); const getRowValues = (dataChangeLog) => rowValuesMap.get(dataChangeLog.primaryKeyValue) ?? []; const slidingTimeframe$ = getSlidingTimeframe$(source$, trailingPeriod, (dataChangeLog) => { let rowValues = getRowValues(dataChangeLog); if (!rowValues.length) { rowValues.push(getNumericValue(dataChangeLog.oldValue)); doubleInsertionsMap.set(dataChangeLog, true); } rowValues.push(getNumericValue(dataChangeLog.newValue)); rowValuesMap.set(dataChangeLog.primaryKeyValue, rowValues); }, (dataChangeLog) => { let rowValues = getRowValues(dataChangeLog); rowValues.shift(); if (doubleInsertionsMap.get(dataChangeLog)) { rowValues.shift(); doubleInsertionsMap.delete(dataChangeLog); } rowValuesMap.set(dataChangeLog.primaryKeyValue, rowValues); }); return slidingTimeframe$.pipe(map((dataChangeLog) => getRowValues(dataChangeLog))); }); }; const getSlidingTimeframe$ = (source$, timeframeDuration, onTimeframeEnter, onTimeframeExit) => { return source$.pipe(mergeMap((dataChangeLog) => { const enter$ = of(dataChangeLog).pipe(tap((dataChangeLog) => onTimeframeEnter(dataChangeLog))); const exit$ = of(dataChangeLog).pipe(delay(timeframeDuration), tap((dataChangeLog) => onTimeframeExit(dataChangeLog))); return of(enter$, exit$).pipe(mergeAll()); })); }; const getMaxTimeframeSize = (expressionValue, context) => { let maxTimeframeSize = context.adaptableApi.optionsApi.getExpressionOptions().maxTimeframeSize; if (maxTimeframeSize > SYSTEM_MAX_TIMEFRAME_SIZE) { maxTimeframeSize = SYSTEM_MAX_TIMEFRAME_SIZE; } return expressionValue > maxTimeframeSize ? maxTimeframeSize : expressionValue; }; const handleGridRowAddedOrRemoved = (args, gridChangeLog$, consumingFunction, context) => { let countValue = args.find((arg) => typeof arg === 'number'); if (countValue < 0) { throw new ExpressionEvaluationError(consumingFunction, 'supports only zero or a positive number as argument'); } let timeframeParameter = extractParameter(consumingFunction, 'config', ['TIMEFRAME'], args, { isOptional: true }); if (countValue === 0 && timeframeParameter == null) { throw new ExpressionEvaluationError(consumingFunction, 'requires a TIMEFRAME parameter when observing for no changes'); } if (countValue == null && timeframeParameter == null) { return gridChangeLog$; } if (countValue == null) { countValue = 1; } if (timeframeParameter == null) { timeframeParameter = { name: 'TIMEFRAME', type: 'config', value: SYSTEM_MAX_TIMEFRAME_SIZE, }; } if (countValue === 0) { const rowDataChangeInfoStub = { rowTrigger: consumingFunction === 'ROW_ADDED' ? 'Add' : 'Delete', rowNodes: [], dataRows: [], changedAt: Date.now(), ...context.adaptableApi.internalApi.buildBaseContext(), }; return gridChangeLog$.pipe(startWith(rowDataChangeInfoStub), debounceTime(timeframeParameter.value), takeUntil(gridChangeLog$.pipe(count()))); } const timeframeChange$ = getTrailingGridCountChange$(gridChangeLog$, timeframeParameter.value, countValue); return getDataChangeCount$(gridChangeLog$, timeframeChange$, countValue); };