77
88import { isObject , partition } from 'lodash' ;
99import { i18n } from '@kbn/i18n' ;
10- import { parse , TinymathLocation } from '@kbn/tinymath' ;
10+ import { parse , TinymathLocation , TinymathVariable } from '@kbn/tinymath' ;
1111import type { TinymathAST , TinymathFunction , TinymathNamedArgument } from '@kbn/tinymath' ;
1212import { esKuery , esQuery } from '../../../../../../../../src/plugins/data/public' ;
1313import {
@@ -63,6 +63,19 @@ interface ValidationErrors {
6363 message : string ;
6464 type : { } ;
6565 } ;
66+ tooManyFirstArguments : {
67+ message : string ;
68+ type : {
69+ operation : string ;
70+ type : string ;
71+ text : string ;
72+ supported ?: number ;
73+ } ;
74+ } ;
75+ wrongArgument : {
76+ message : string ;
77+ type : { operation : string ; text : string ; type : string } ;
78+ } ;
6679}
6780
6881type ErrorTypes = keyof ValidationErrors ;
@@ -276,6 +289,25 @@ function getMessageFromId<K extends ErrorTypes>({
276289 defaultMessage : 'Use only one of kql= or lucene=, not both' ,
277290 } ) ;
278291 break ;
292+ case 'tooManyFirstArguments' :
293+ message = i18n . translate ( 'xpack.lens.indexPattern.formulaOperationTooManyFirstArguments' , {
294+ defaultMessage :
295+ 'The operation {operation} in the Formula requires a {supported, plural, one {single} other {supported}} {type}, found: {text}' ,
296+ values : {
297+ operation : out . operation ,
298+ text : out . text ,
299+ type : out . type ,
300+ supported : out . supported || 1 ,
301+ } ,
302+ } ) ;
303+ break ;
304+ case 'wrongArgument' :
305+ message = i18n . translate ( 'xpack.lens.indexPattern.formulaOperationwrongArgument' , {
306+ defaultMessage :
307+ 'The operation {operation} in the Formula does not support {type} parameters, found: {text}' ,
308+ values : { operation : out . operation , text : out . text , type : out . type } ,
309+ } ) ;
310+ break ;
279311 // case 'mathRequiresFunction':
280312 // message = i18n.translate('xpack.lens.indexPattern.formulaMathRequiresFunctionLabel', {
281313 // defaultMessage; 'The function {name} requires an Elasticsearch function',
@@ -531,14 +563,16 @@ function runFullASTValidation(
531563 } else {
532564 if ( nodeOperation . input === 'field' ) {
533565 if ( shouldHaveFieldArgument ( node ) ) {
534- if ( ! isFirstArgumentValidType ( firstArg , 'variable' ) ) {
566+ if ( ! isArgumentValidType ( firstArg , 'variable' ) ) {
535567 if ( isMathNode ( firstArg ) ) {
536568 errors . push (
537569 getMessageFromId ( {
538570 messageId : 'wrongFirstArgument' ,
539571 values : {
540572 operation : node . name ,
541- type : 'field' ,
573+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaFieldValue' , {
574+ defaultMessage : 'field' ,
575+ } ) ,
542576 argument : `math operation` ,
543577 } ,
544578 locations : node . location ? [ node . location ] : [ ] ,
@@ -550,7 +584,9 @@ function runFullASTValidation(
550584 messageId : 'wrongFirstArgument' ,
551585 values : {
552586 operation : node . name ,
553- type : 'field' ,
587+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaFieldValue' , {
588+ defaultMessage : 'field' ,
589+ } ) ,
554590 argument :
555591 getValueOrName ( firstArg ) ||
556592 i18n . translate ( 'xpack.lens.indexPattern.formulaNoFieldForOperation' , {
@@ -561,6 +597,25 @@ function runFullASTValidation(
561597 } )
562598 ) ;
563599 }
600+ } else {
601+ // If the first argument is valid proceed with the other arguments validation
602+ const fieldErrors = validateFieldArguments ( node , variables , {
603+ isFieldOperation : true ,
604+ firstArg,
605+ } ) ;
606+ if ( fieldErrors . length ) {
607+ errors . push ( ...fieldErrors ) ;
608+ }
609+ }
610+ const functionErrors = validateFunctionArguments ( node , functions , 0 , {
611+ isFieldOperation : true ,
612+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaFieldValue' , {
613+ defaultMessage : 'field' ,
614+ } ) ,
615+ firstArgValidation : false ,
616+ } ) ;
617+ if ( functionErrors . length ) {
618+ errors . push ( ...functionErrors ) ;
564619 }
565620 } else {
566621 // Named arguments only
@@ -602,16 +657,20 @@ function runFullASTValidation(
602657 if ( nodeOperation . input === 'fullReference' ) {
603658 // What about fn(7 + 1)? We may want to allow that
604659 // In general this should be handled down the Esaggs route rather than here
605- if (
606- ! isFirstArgumentValidType ( firstArg , 'function' ) ||
607- ( isMathNode ( firstArg ) && validateMathNodes ( firstArg , missingVariablesSet ) . length )
608- ) {
660+ const isFirstArgumentNotValid = Boolean (
661+ ! isArgumentValidType ( firstArg , 'function' ) ||
662+ ( isMathNode ( firstArg ) && validateMathNodes ( firstArg , missingVariablesSet ) . length )
663+ ) ;
664+ // First field has a special handling
665+ if ( isFirstArgumentNotValid ) {
609666 errors . push (
610667 getMessageFromId ( {
611668 messageId : 'wrongFirstArgument' ,
612669 values : {
613670 operation : node . name ,
614- type : 'operation' ,
671+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaOperationValue' , {
672+ defaultMessage : 'operation' ,
673+ } ) ,
615674 argument :
616675 getValueOrName ( firstArg ) ||
617676 i18n . translate ( 'xpack.lens.indexPattern.formulaNoOperation' , {
@@ -622,6 +681,21 @@ function runFullASTValidation(
622681 } )
623682 ) ;
624683 }
684+ // Check for multiple function passed
685+ const requiredFunctions = nodeOperation . requiredReferences
686+ ? nodeOperation . requiredReferences . length
687+ : 1 ;
688+ const functionErrors = validateFunctionArguments ( node , functions , requiredFunctions , {
689+ isFieldOperation : false ,
690+ firstArgValidation : isFirstArgumentNotValid ,
691+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaMetricValue' , {
692+ defaultMessage : 'metric' ,
693+ } ) ,
694+ } ) ;
695+ if ( functionErrors . length ) {
696+ errors . push ( ...functionErrors ) ;
697+ }
698+
625699 if ( ! canHaveParams ( nodeOperation ) && namedArguments . length ) {
626700 errors . push (
627701 getMessageFromId ( {
@@ -633,6 +707,14 @@ function runFullASTValidation(
633707 } )
634708 ) ;
635709 } else {
710+ // check for fields passed at any position
711+ const fieldErrors = validateFieldArguments ( node , variables , {
712+ isFieldOperation : false ,
713+ firstArg,
714+ } ) ;
715+ if ( fieldErrors . length ) {
716+ errors . push ( ...fieldErrors ) ;
717+ }
636718 const argumentsErrors = validateNameArguments (
637719 node ,
638720 nodeOperation ,
@@ -736,7 +818,7 @@ export function hasFunctionFieldArgument(type: string) {
736818 return ! [ 'count' ] . includes ( type ) ;
737819}
738820
739- export function isFirstArgumentValidType ( arg : TinymathAST , type : TinymathNodeTypes [ 'type' ] ) {
821+ export function isArgumentValidType ( arg : TinymathAST | string , type : TinymathNodeTypes [ 'type' ] ) {
740822 return isObject ( arg ) && arg . type === type ;
741823}
742824
@@ -812,3 +894,109 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<str
812894 } ) ;
813895 return errors ;
814896}
897+
898+ function validateFieldArguments (
899+ node : TinymathFunction ,
900+ variables : Array < string | number | TinymathVariable > ,
901+ { isFieldOperation, firstArg } : { isFieldOperation : boolean ; firstArg : TinymathAST }
902+ ) {
903+ const fields = variables . filter (
904+ ( arg ) => isArgumentValidType ( arg , 'variable' ) && ! isMathNode ( arg )
905+ ) ;
906+ const errors = [ ] ;
907+ if ( isFieldOperation && ( fields . length > 1 || ( fields . length === 1 && fields [ 0 ] !== firstArg ) ) ) {
908+ errors . push (
909+ getMessageFromId ( {
910+ messageId : 'tooManyFirstArguments' ,
911+ values : {
912+ operation : node . name ,
913+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaFieldValue' , {
914+ defaultMessage : 'field' ,
915+ } ) ,
916+ supported : 1 ,
917+ text : ( fields as TinymathVariable [ ] ) . map ( ( { text } ) => text ) . join ( ', ' ) ,
918+ } ,
919+ locations : node . location ? [ node . location ] : [ ] ,
920+ } )
921+ ) ;
922+ }
923+ if ( ! isFieldOperation && fields . length ) {
924+ errors . push (
925+ getMessageFromId ( {
926+ messageId : 'wrongArgument' ,
927+ values : {
928+ operation : node . name ,
929+ text : ( fields as TinymathVariable [ ] ) . map ( ( { text } ) => text ) . join ( ', ' ) ,
930+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaFieldValue' , {
931+ defaultMessage : 'field' ,
932+ } ) ,
933+ } ,
934+ locations : node . location ? [ node . location ] : [ ] ,
935+ } )
936+ ) ;
937+ }
938+ return errors ;
939+ }
940+
941+ function validateFunctionArguments (
942+ node : TinymathFunction ,
943+ functions : TinymathFunction [ ] ,
944+ requiredFunctions : number = 0 ,
945+ {
946+ isFieldOperation,
947+ firstArgValidation,
948+ type,
949+ } : { isFieldOperation : boolean ; firstArgValidation : boolean ; type : string }
950+ ) {
951+ const errors = [ ] ;
952+ // For math operation let the native operation run its own validation
953+ const [ esOperations , mathOperations ] = partition ( functions , ( arg ) => ! isMathNode ( arg ) ) ;
954+ if ( esOperations . length > requiredFunctions ) {
955+ if ( isFieldOperation ) {
956+ errors . push (
957+ getMessageFromId ( {
958+ messageId : 'wrongArgument' ,
959+ values : {
960+ operation : node . name ,
961+ text : ( esOperations as TinymathFunction [ ] ) . map ( ( { text } ) => text ) . join ( ', ' ) ,
962+ type : i18n . translate ( 'xpack.lens.indexPattern.formulaMetricValue' , {
963+ defaultMessage : 'metric' ,
964+ } ) ,
965+ } ,
966+ locations : node . location ? [ node . location ] : [ ] ,
967+ } )
968+ ) ;
969+ } else {
970+ errors . push (
971+ getMessageFromId ( {
972+ messageId : 'tooManyFirstArguments' ,
973+ values : {
974+ operation : node . name ,
975+ type,
976+ supported : requiredFunctions ,
977+ text : ( esOperations as TinymathFunction [ ] ) . map ( ( { text } ) => text ) . join ( ', ' ) ,
978+ } ,
979+ locations : node . location ? [ node . location ] : [ ] ,
980+ } )
981+ ) ;
982+ }
983+ }
984+ // full reference operation have another way to handle math operations
985+ if (
986+ isFieldOperation &&
987+ ( ( ! firstArgValidation && mathOperations . length ) || mathOperations . length > 1 )
988+ ) {
989+ errors . push (
990+ getMessageFromId ( {
991+ messageId : 'wrongArgument' ,
992+ values : {
993+ operation : node . name ,
994+ type,
995+ text : ( mathOperations as TinymathFunction [ ] ) . map ( ( { text } ) => text ) . join ( ', ' ) ,
996+ } ,
997+ locations : node . location ? [ node . location ] : [ ] ,
998+ } )
999+ ) ;
1000+ }
1001+ return errors ;
1002+ }
0 commit comments