diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 12f54d71..cb1f004a 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -22,6 +22,10 @@ type ObjectListFilter = | Contains of FieldFilter | OfTypes of Type list | FilterField of FieldFilter + | EqualsCI of FieldFilter + | StartsWithCI of FieldFilter + | EndsWithCI of FieldFilter + | ContainsCI of FieldFilter open System.Linq open System.Linq.Expressions @@ -124,9 +128,21 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a field sub comparison. let ( --> ) fname filter = FilterField { FieldName = fname; Value = filter } - /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. + /// Creates a new ObjectListFilter representing a NOT operation for the existing one. let ( !!! ) filter = Not filter + /// Creates a new ObjectListFilter representing a case-insensitive EQUALS operation on a string value. + let ( ===~ ) fname value = EqualsCI { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value. + let ( =@@~ ) fname value = StartsWithCI { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value. + let ( @@=~ ) fname value = EndsWithCI { FieldName = fname; Value = value } + + /// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value. + let ( @=@~ ) fname value = ContainsCI { FieldName = fname; Value = value } + let private genericWhereMethod = typeof.GetMethods () |> Seq.where (fun m -> m.Name = "Where") @@ -147,6 +163,12 @@ module ObjectListFilter = let private StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType |]) let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType |]) let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType |]) + let private stringComparisonType = typeof + let private StringEqualsCIMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |]) + let private StringStartsWithCIMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |]) + let private StringEndsWithCIMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |]) + let private StringContainsCIMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |]) + let private OrdinalIgnoreCase = Expression.Constant (StringComparison.OrdinalIgnoreCase) let private unwrapOptionMethod = FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) @@ -329,6 +351,18 @@ module ObjectListFilter = | FilterField f -> let paramExpr = Expression.PropertyOrField (param, f.FieldName) buildFilterExpr isEnumerableQuery (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + | EqualsCI f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase) + | StartsWithCI f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase) + | EndsWithCI f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase) + | ContainsCI f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + Expression.Call (normalizeStringMemberExpr ``member``, StringContainsCIMethod, Expression.Constant f.Value, OrdinalIgnoreCase) type private CompareDiscriminatorExpressionVisitor<'T, 'D> (compareDiscriminator : CompareDiscriminatorExpression<'T, 'D>, param : SourceExpression, value : obj) = diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index e8239cad..d1d9401c 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -18,6 +18,10 @@ type private ComparisonOperator = | LessThan of string | LessThanOrEqual of string | In of string + | EqualsCI of string + | StartsWithCI of string + | EndsWithCI of string + | ContainsCI of string let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result = @@ -25,6 +29,14 @@ let rec private coerceObjectListFilterInput (variables : Variables) inputValue : let s = s.ToLowerInvariant () let prefix (suffix : string) (s : string) = s.Substring (0, s.Length - suffix.Length) match s with + | s when s.EndsWith ("_ends_with_ci") && s.Length > "_ends_with_ci".Length -> EndsWithCI (prefix "_ends_with_ci" s) + | s when s.EndsWith ("_ewci") && s.Length > "_ewci".Length -> EndsWithCI (prefix "_ewci" s) + | s when s.EndsWith ("_starts_with_ci") && s.Length > "_starts_with_ci".Length -> StartsWithCI (prefix "_starts_with_ci" s) + | s when s.EndsWith ("_swci") && s.Length > "_swci".Length -> StartsWithCI (prefix "_swci" s) + | s when s.EndsWith ("_contains_ci") && s.Length > "_contains_ci".Length -> ContainsCI (prefix "_contains_ci" s) + | s when s.EndsWith ("_cci") && s.Length > "_cci".Length -> ContainsCI (prefix "_cci" s) + | s when s.EndsWith ("_equals_ci") && s.Length > "_equals_ci".Length -> EqualsCI (prefix "_equals_ci" s) + | s when s.EndsWith ("_eqi") && s.Length > "_eqi".Length -> EqualsCI (prefix "_eqi" s) | s when s.EndsWith ("_ends_with") && s.Length > "_ends_with".Length -> EndsWith (prefix "_ends_with" s) | s when s.EndsWith ("_ew") && s.Length > "_ew".Length -> EndsWith (prefix "_ew" s) | 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 : | EndsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWith { FieldName = fname; Value = value })) | StartsWith fname, StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWith { FieldName = fname; Value = value })) | Contains fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.Contains { FieldName = fname; Value = value })) + | EndsWithCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWithCI { FieldName = fname; Value = value })) + | StartsWithCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWithCI { FieldName = fname; Value = value })) + | ContainsCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.ContainsCI { FieldName = fname; Value = value })) + | EqualsCI fname, StringValue value -> Ok (ValueSome (ObjectListFilter.EqualsCI { FieldName = fname; Value = value })) | Equals fname, ObjectValue value -> match mapInput value with | Error errs -> Error errs diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index f1c06fa8..7f5759b1 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -526,3 +526,68 @@ let ``ObjectListFilter OfTypes works with two or more types`` () = let animal = List.last filteredData animal.ID |> equals 4 animal.Name |> equals "Horse D" + +[] +let ``ObjectListFilter works with EqualsCI operator`` () = + let filter = EqualsCI { FieldName = "firstName"; Value = "jonathan" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + +[] +let ``ObjectListFilter works with EqualsCI operator upper case`` () = + let filter = EqualsCI { FieldName = "firstName"; Value = "JONATHAN" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + +[] +let ``ObjectListFilter works with StartsWithCI operator`` () = + let filter = StartsWithCI { FieldName = "firstName"; Value = "j" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + +[] +let ``ObjectListFilter works with EndsWithCI operator`` () = + let filter = EndsWithCI { FieldName = "lastName"; Value = "AMS" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.LastName |> equals "Adams" + let result = List.last filteredData + result.ID |> equals 2 + result.LastName |> equals "Abrams" + +[] +let ``ObjectListFilter works with ContainsCI operator`` () = + let filter = ContainsCI { FieldName = "firstName"; Value = "EN" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + let result = List.last filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + +[] +let ``ObjectListFilter case-insensitive operators do not match with case-sensitive filters`` () = + // Exact case-sensitive StartsWith "j" (lowercase) should match nothing in the data + let filter = StartsWith { FieldName = "firstName"; Value = "j" } + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0