@@ -14,6 +14,8 @@ import {
1414 LOGICAL_OPERATORS ,
1515 ALL_OPERATORS ,
1616 parseFilterAST ,
17+ isFilterAST ,
18+ VALID_AST_OPERATORS ,
1719 type Filter ,
1820 type QueryFilter ,
1921 type FieldOperators ,
@@ -921,3 +923,80 @@ describe('parseFilterAST', () => {
921923 expect ( ( ) => FilterConditionSchema . parse ( result ) ) . not . toThrow ( ) ;
922924 } ) ;
923925} ) ;
926+
927+ // ============================================================================
928+ // isFilterAST — structural validation
929+ // ============================================================================
930+
931+ describe ( 'isFilterAST' , ( ) => {
932+ it ( 'should return false for null/undefined/empty' , ( ) => {
933+ expect ( isFilterAST ( null ) ) . toBe ( false ) ;
934+ expect ( isFilterAST ( undefined ) ) . toBe ( false ) ;
935+ expect ( isFilterAST ( [ ] ) ) . toBe ( false ) ;
936+ } ) ;
937+
938+ it ( 'should return false for non-array types' , ( ) => {
939+ expect ( isFilterAST ( 'not an array' ) ) . toBe ( false ) ;
940+ expect ( isFilterAST ( 42 ) ) . toBe ( false ) ;
941+ expect ( isFilterAST ( true ) ) . toBe ( false ) ;
942+ expect ( isFilterAST ( { status : 'active' } ) ) . toBe ( false ) ;
943+ } ) ;
944+
945+ it ( 'should detect valid comparison node' , ( ) => {
946+ expect ( isFilterAST ( [ 'status' , '=' , 'active' ] ) ) . toBe ( true ) ;
947+ expect ( isFilterAST ( [ 'age' , '>' , 18 ] ) ) . toBe ( true ) ;
948+ expect ( isFilterAST ( [ 'age' , '>=' , 18 ] ) ) . toBe ( true ) ;
949+ expect ( isFilterAST ( [ 'role' , 'in' , [ 'admin' , 'editor' ] ] ) ) . toBe ( true ) ;
950+ expect ( isFilterAST ( [ 'name' , 'contains' , 'John' ] ) ) . toBe ( true ) ;
951+ expect ( isFilterAST ( [ 'name' , 'like' , 'John' ] ) ) . toBe ( true ) ;
952+ expect ( isFilterAST ( [ 'created_at' , 'between' , [ '2024-01-01' , '2024-12-31' ] ] ) ) . toBe ( true ) ;
953+ expect ( isFilterAST ( [ 'deleted_at' , 'is_null' , null ] ) ) . toBe ( true ) ;
954+ } ) ;
955+
956+ it ( 'should detect valid logical nodes' , ( ) => {
957+ expect ( isFilterAST ( [ 'and' , [ 'status' , '=' , 'active' ] , [ 'priority' , '=' , 'high' ] ] ) ) . toBe ( true ) ;
958+ expect ( isFilterAST ( [ 'or' , [ 'role' , '=' , 'admin' ] , [ 'role' , '=' , 'editor' ] ] ) ) . toBe ( true ) ;
959+ expect ( isFilterAST ( [ 'AND' , [ 'status' , '=' , 'active' ] ] ) ) . toBe ( true ) ;
960+ expect ( isFilterAST ( [ 'OR' , [ 'a' , '=' , 1 ] , [ 'b' , '=' , 2 ] ] ) ) . toBe ( true ) ;
961+ } ) ;
962+
963+ it ( 'should detect legacy flat array format' , ( ) => {
964+ expect ( isFilterAST ( [ [ 'status' , '=' , 'active' ] , [ 'priority' , '=' , 'high' ] ] ) ) . toBe ( true ) ;
965+ } ) ;
966+
967+ it ( 'should reject invalid arrays that are not filter ASTs' , ( ) => {
968+ // Arbitrary number arrays
969+ expect ( isFilterAST ( [ 1 , 2 , 3 ] ) ) . toBe ( false ) ;
970+ // Array of strings that aren't a valid comparison
971+ expect ( isFilterAST ( [ 'hello' , 'world' ] ) ) . toBe ( false ) ;
972+ // Array where second element is not a known operator
973+ expect ( isFilterAST ( [ 'field' , 'UNKNOWN_OP' , 'value' ] ) ) . toBe ( false ) ;
974+ // Mixed array types
975+ expect ( isFilterAST ( [ true , false , null ] ) ) . toBe ( false ) ;
976+ } ) ;
977+
978+ it ( 'should reject "and"/"or" with no children' , ( ) => {
979+ expect ( isFilterAST ( [ 'and' ] ) ) . toBe ( false ) ;
980+ expect ( isFilterAST ( [ 'or' ] ) ) . toBe ( false ) ;
981+ } ) ;
982+
983+ it ( 'should reject "and"/"or" with non-array children' , ( ) => {
984+ expect ( isFilterAST ( [ 'and' , 'not-an-array' ] ) ) . toBe ( false ) ;
985+ expect ( isFilterAST ( [ 'or' , 123 ] ) ) . toBe ( false ) ;
986+ } ) ;
987+ } ) ;
988+
989+ // ============================================================================
990+ // VALID_AST_OPERATORS constant
991+ // ============================================================================
992+
993+ describe ( 'VALID_AST_OPERATORS' , ( ) => {
994+ it ( 'should contain all standard comparison operators' , ( ) => {
995+ const expected = [ '=' , '==' , '!=' , '<>' , '>' , '>=' , '<' , '<=' , 'in' , 'nin' , 'not_in' ,
996+ 'contains' , 'like' , 'startswith' , 'starts_with' , 'endswith' , 'ends_with' ,
997+ 'between' , 'is_null' , 'is_not_null' ] ;
998+ for ( const op of expected ) {
999+ expect ( VALID_AST_OPERATORS . has ( op ) ) . toBe ( true ) ;
1000+ }
1001+ } ) ;
1002+ } ) ;
0 commit comments