@@ -536,6 +536,12 @@ export function targetGetSegmentCount(target: Target): number {
536
536
537
537
export abstract class Filter {
538
538
abstract matches ( doc : Document ) : boolean ;
539
+
540
+ abstract getFlattenedFilters ( ) : readonly FieldFilter [ ] ;
541
+
542
+ abstract getFilters ( ) : readonly Filter [ ] ;
543
+
544
+ abstract getFirstInequalityField ( ) : FieldPath | null ;
539
545
}
540
546
541
547
export const enum Operator {
@@ -551,6 +557,11 @@ export const enum Operator {
551
557
ARRAY_CONTAINS_ANY = 'array-contains-any'
552
558
}
553
559
560
+ export const enum CompositeOperator {
561
+ OR = 'or' ,
562
+ AND = 'and'
563
+ }
564
+
554
565
/**
555
566
* The direction of sorting in an order by.
556
567
*/
@@ -559,11 +570,12 @@ export const enum Direction {
559
570
DESCENDING = 'desc'
560
571
}
561
572
573
+ // TODO(orquery) move Filter classes to a new file, e.g. filter.ts
562
574
export class FieldFilter extends Filter {
563
575
protected constructor (
564
- public field : FieldPath ,
565
- public op : Operator ,
566
- public value : ProtoValue
576
+ public readonly field : FieldPath ,
577
+ public readonly op : Operator ,
578
+ public readonly value : ProtoValue
567
579
) {
568
580
super ( ) ;
569
581
}
@@ -685,21 +697,117 @@ export class FieldFilter extends Filter {
685
697
] . indexOf ( this . op ) >= 0
686
698
) ;
687
699
}
700
+
701
+ getFlattenedFilters ( ) : readonly FieldFilter [ ] {
702
+ return [ this ] ;
703
+ }
704
+
705
+ getFilters ( ) : readonly Filter [ ] {
706
+ return [ this ] ;
707
+ }
708
+
709
+ getFirstInequalityField ( ) : FieldPath | null {
710
+ if ( this . isInequality ( ) ) {
711
+ return this . field ;
712
+ }
713
+ return null ;
714
+ }
715
+ }
716
+
717
+ export class CompositeFilter extends Filter {
718
+ private memoizedFlattenedFilters : FieldFilter [ ] | null = null ;
719
+
720
+ protected constructor (
721
+ public readonly filters : readonly Filter [ ] ,
722
+ public readonly op : CompositeOperator
723
+ ) {
724
+ super ( ) ;
725
+ }
726
+
727
+ /**
728
+ * Creates a filter based on the provided arguments.
729
+ */
730
+ static create ( filters : Filter [ ] , op : CompositeOperator ) : CompositeFilter {
731
+ return new CompositeFilter ( filters , op ) ;
732
+ }
733
+
734
+ matches ( doc : Document ) : boolean {
735
+ if ( this . isConjunction ( ) ) {
736
+ // For conjunctions, all filters must match, so return false if any filter doesn't match.
737
+ return this . filters . find ( filter => ! filter . matches ( doc ) ) === undefined ;
738
+ } else {
739
+ // For disjunctions, at least one filter should match.
740
+ return this . filters . find ( filter => filter . matches ( doc ) ) !== undefined ;
741
+ }
742
+ }
743
+
744
+ getFlattenedFilters ( ) : readonly FieldFilter [ ] {
745
+ if ( this . memoizedFlattenedFilters !== null ) {
746
+ return this . memoizedFlattenedFilters ;
747
+ }
748
+
749
+ this . memoizedFlattenedFilters = this . filters . reduce ( ( result , subfilter ) => {
750
+ return result . concat ( subfilter . getFlattenedFilters ( ) ) ;
751
+ } , [ ] as FieldFilter [ ] ) ;
752
+
753
+ return this . memoizedFlattenedFilters ;
754
+ }
755
+
756
+ getFilters ( ) : readonly Filter [ ] {
757
+ return this . filters ;
758
+ }
759
+
760
+ getFirstInequalityField ( ) : FieldPath | null {
761
+ const found = this . findFirstMatchingFilter ( filter => filter . isInequality ( ) ) ;
762
+
763
+ if ( found !== null ) {
764
+ return found . field ;
765
+ }
766
+ return null ;
767
+ }
768
+
769
+ // Performs a depth-first search to find and return the first FieldFilter in the composite filter
770
+ // that satisfies the predicate. Returns `null` if none of the FieldFilters satisfy the
771
+ // predicate.
772
+ private findFirstMatchingFilter (
773
+ predicate : ( filter : FieldFilter ) => boolean
774
+ ) : FieldFilter | null {
775
+ for ( const fieldFilter of this . getFlattenedFilters ( ) ) {
776
+ if ( predicate ( fieldFilter ) ) {
777
+ return fieldFilter ;
778
+ }
779
+ }
780
+
781
+ return null ;
782
+ }
783
+
784
+ isConjunction ( ) : boolean {
785
+ return this . op === CompositeOperator . AND ;
786
+ }
688
787
}
689
788
690
789
export function canonifyFilter ( filter : Filter ) : string {
691
790
debugAssert (
692
- filter instanceof FieldFilter ,
693
- 'canonifyFilter() only supports FieldFilters'
694
- ) ;
695
- // TODO(b/29183165): Technically, this won't be unique if two values have
696
- // the same description, such as the int 3 and the string "3". So we should
697
- // add the types in here somehow, too.
698
- return (
699
- filter . field . canonicalString ( ) +
700
- filter . op . toString ( ) +
701
- canonicalId ( filter . value )
791
+ filter instanceof FieldFilter || filter instanceof CompositeFilter ,
792
+ 'canonifyFilter() only supports FieldFilters and CompositeFilters'
702
793
) ;
794
+
795
+ if ( filter instanceof FieldFilter ) {
796
+ // TODO(b/29183165): Technically, this won't be unique if two values have
797
+ // the same description, such as the int 3 and the string "3". So we should
798
+ // add the types in here somehow, too.
799
+ return (
800
+ filter . field . canonicalString ( ) +
801
+ filter . op . toString ( ) +
802
+ canonicalId ( filter . value )
803
+ ) ;
804
+ } else {
805
+ // filter instanceof CompositeFilter
806
+ const canonicalIdsString = filter . filters
807
+ . map ( filter => canonifyFilter ( filter ) )
808
+ . join ( ',' ) ;
809
+ return `${ filter . op } (${ canonicalIdsString } )` ;
810
+ }
703
811
}
704
812
705
813
export function filterEquals ( f1 : Filter , f2 : Filter ) : boolean {
0 commit comments