Skip to content

Commit 59bf25d

Browse files
Copilotxperiandri
andcommitted
Support case insensitive comparison with ObjectListFilter
Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
1 parent 6e37db8 commit 59bf25d

3 files changed

Lines changed: 116 additions & 1 deletion

File tree

src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ type ObjectListFilter =
2222
| Contains of FieldFilter<System.IComparable>
2323
| OfTypes of Type list
2424
| FilterField of FieldFilter<ObjectListFilter>
25+
| EqualsCI of FieldFilter<string>
26+
| StartsWithCI of FieldFilter<string>
27+
| EndsWithCI of FieldFilter<string>
28+
| ContainsCI of FieldFilter<string>
2529

2630
open System.Linq
2731
open System.Linq.Expressions
@@ -124,9 +128,21 @@ module ObjectListFilter =
124128
/// Creates a new ObjectListFilter representing a field sub comparison.
125129
let ( --> ) fname filter = FilterField { FieldName = fname; Value = filter }
126130

127-
/// Creates a new ObjectListFilter representing a NOT opreation for the existing one.
131+
/// Creates a new ObjectListFilter representing a NOT operation for the existing one.
128132
let ( !!! ) filter = Not filter
129133

134+
/// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value.
135+
let ( ===~ ) fname value = EqualsCI { FieldName = fname; Value = value }
136+
137+
/// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value.
138+
let ( =@@~ ) fname value = StartsWithCI { FieldName = fname; Value = value }
139+
140+
/// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value.
141+
let ( @@=~ ) fname value = EndsWithCI { FieldName = fname; Value = value }
142+
143+
/// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value.
144+
let ( @=@~ ) fname value = ContainsCI { FieldName = fname; Value = value }
145+
130146
let private genericWhereMethod =
131147
typeof<Queryable>.GetMethods ()
132148
|> Seq.where (fun m -> m.Name = "Where")
@@ -147,6 +163,12 @@ module ObjectListFilter =
147163
let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |])
148164
let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |])
149165
let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |])
166+
let private stringComparisonType = typeof<StringComparison>
167+
let private StringEqualsCIMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |])
168+
let private StringStartsWithCIMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |])
169+
let private StringEndsWithCIMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |])
170+
let private StringContainsCIMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |])
171+
let private OrdinalIgnoreCase = Expression.Constant (StringComparison.OrdinalIgnoreCase)
150172
let private unwrapOptionMethod =
151173
FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap)
152174

@@ -329,6 +351,18 @@ module ObjectListFilter =
329351
| FilterField f ->
330352
let paramExpr = Expression.PropertyOrField (param, f.FieldName)
331353
buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value
354+
| EqualsCI f ->
355+
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
356+
Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
357+
| StartsWithCI f ->
358+
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
359+
Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
360+
| EndsWithCI f ->
361+
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
362+
Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
363+
| ContainsCI f ->
364+
let ``member`` = Expression.PropertyOrField (param, f.FieldName)
365+
Expression.Call (normalizeStringMemberExpr ``member``, StringContainsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase)
332366

333367
type private CompareDiscriminatorExpressionVisitor<'T, 'D>
334368
(compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) =

src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,25 @@ type private ComparisonOperator =
1818
| LessThan of string
1919
| LessThanOrEqual of string
2020
| In of string
21+
| EqualsCI of string
22+
| StartsWithCI of string
23+
| EndsWithCI of string
24+
| ContainsCI of string
2125

2226
let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result<ObjectListFilter voption, IGQLError list> =
2327

2428
let parseFieldCondition (s : string) =
2529
let s = s.ToLowerInvariant ()
2630
let prefix (suffix : string) (s : string) = s.Substring (0, s.Length - suffix.Length)
2731
match s with
32+
| s when s.EndsWith ("_ends_with_ci") && s.Length > "_ends_with_ci".Length -> EndsWithCI (prefix "_ends_with_ci" s)
33+
| s when s.EndsWith ("_ewci") && s.Length > "_ewci".Length -> EndsWithCI (prefix "_ewci" s)
34+
| s when s.EndsWith ("_starts_with_ci") && s.Length > "_starts_with_ci".Length -> StartsWithCI (prefix "_starts_with_ci" s)
35+
| s when s.EndsWith ("_swci") && s.Length > "_swci".Length -> StartsWithCI (prefix "_swci" s)
36+
| s when s.EndsWith ("_contains_ci") && s.Length > "_contains_ci".Length -> ContainsCI (prefix "_contains_ci" s)
37+
| s when s.EndsWith ("_cci") && s.Length > "_cci".Length -> ContainsCI (prefix "_cci" s)
38+
| s when s.EndsWith ("_equals_ci") && s.Length > "_equals_ci".Length -> EqualsCI (prefix "_equals_ci" s)
39+
| s when s.EndsWith ("_eqi") && s.Length > "_eqi".Length -> EqualsCI (prefix "_eqi" s)
2840
| s when s.EndsWith ("_ends_with") && s.Length > "_ends_with".Length -> EndsWith (prefix "_ends_with" s)
2941
| s when s.EndsWith ("_ew") && s.Length > "_ew".Length -> EndsWith (prefix "_ew" s)
3042
| s when s.EndsWith ("_starts_with") && s.Length > "_starts_with".Length -> StartsWith (prefix "_starts_with" s)
@@ -99,6 +111,10 @@ let rec private coerceObjectListFilterInput (variables : Variables) inputValue :
99111
| EndsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWith { FieldName = fname; Value = value }))
100112
| StartsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWith { FieldName = fname; Value = value }))
101113
| Contains fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.Contains { FieldName = fname; Value = value }))
114+
| EndsWithCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWithCI { FieldName = fname; Value = value }))
115+
| StartsWithCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWithCI { FieldName = fname; Value = value }))
116+
| ContainsCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.ContainsCI { FieldName = fname; Value = value }))
117+
| EqualsCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EqualsCI { FieldName = fname; Value = value }))
102118
| Equals fname, ObjectValue value ->
103119
match mapInput value with
104120
| Error errs -> Error errs

tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,3 +526,68 @@ let ``ObjectListFilter OfTypes works with two or more types`` () =
526526
let animal = List.last filteredData
527527
animal.ID |> equals 4
528528
animal.Name |> equals "Horse D"
529+
530+
[<Fact>]
531+
let ``ObjectListFilter works with EqualsCI operator`` () =
532+
let filter = EqualsCI { FieldName = "firstName"; Value = "jonathan" }
533+
let queryable = data.AsQueryable ()
534+
let filteredData = queryable.Apply (filter) |> Seq.toList
535+
List.length filteredData |> equals 1
536+
let result = List.head filteredData
537+
result.ID |> equals 2
538+
result.FirstName |> equals "Jonathan"
539+
result.LastName |> equals "Abrams"
540+
541+
[<Fact>]
542+
let ``ObjectListFilter works with EqualsCI operator upper case`` () =
543+
let filter = EqualsCI { FieldName = "firstName"; Value = "JONATHAN" }
544+
let queryable = data.AsQueryable ()
545+
let filteredData = queryable.Apply (filter) |> Seq.toList
546+
List.length filteredData |> equals 1
547+
let result = List.head filteredData
548+
result.ID |> equals 2
549+
result.FirstName |> equals "Jonathan"
550+
551+
[<Fact>]
552+
let ``ObjectListFilter works with StartsWithCI operator`` () =
553+
let filter = StartsWithCI { FieldName = "firstName"; Value = "j" }
554+
let queryable = data.AsQueryable ()
555+
let filteredData = queryable.Apply (filter) |> Seq.toList
556+
List.length filteredData |> equals 2
557+
let result = List.head filteredData
558+
result.ID |> equals 2
559+
result.FirstName |> equals "Jonathan"
560+
561+
[<Fact>]
562+
let ``ObjectListFilter works with EndsWithCI operator`` () =
563+
let filter = EndsWithCI { FieldName = "lastName"; Value = "AMS" }
564+
let queryable = data.AsQueryable ()
565+
let filteredData = queryable.Apply (filter) |> Seq.toList
566+
List.length filteredData |> equals 2
567+
let result = List.head filteredData
568+
result.ID |> equals 4
569+
result.LastName |> equals "Adams"
570+
let result = List.last filteredData
571+
result.ID |> equals 2
572+
result.LastName |> equals "Abrams"
573+
574+
[<Fact>]
575+
let ``ObjectListFilter works with ContainsCI operator`` () =
576+
let filter = ContainsCI { FieldName = "firstName"; Value = "EN" }
577+
let queryable = data.AsQueryable ()
578+
let filteredData = queryable.Apply (filter) |> Seq.toList
579+
List.length filteredData |> equals 2
580+
let result = List.head filteredData
581+
result.ID |> equals 4
582+
result.FirstName |> equals "Ben"
583+
let result = List.last filteredData
584+
result.ID |> equals 7
585+
result.FirstName |> equals "Jeneffer"
586+
587+
[<Fact>]
588+
let ``ObjectListFilter case-insensitive operators do not match with case-sensitive filters`` () =
589+
// Exact case-sensitive StartsWith "j" (lowercase) should match nothing in the data
590+
let filter = StartsWith { FieldName = "firstName"; Value = "j" }
591+
let queryable = data.AsQueryable ()
592+
let filteredData = queryable.Apply (filter) |> Seq.toList
593+
List.length filteredData |> equals 0

0 commit comments

Comments
 (0)