diff --git a/README.md b/README.md index 39fd14c8..3d5b62d7 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,8 @@ query TestQuery { } ``` +For string filters, lowercase suffixes are case-insensitive (`name_starts_with`, `name_sw`, `name_contains`, `name_equals`, `name_eq`), while capitalized suffixes are case-sensitive (`name_Starts_With`, `name_SW`, `name_Contains`, `name_Equals`, `name_EQ`). `contains`/`Contains` do not have shorthand aliases. + Also you can apply `not` operator like this: ```graphql @@ -406,12 +408,16 @@ type ObjectListFilter = | And of ObjectListFilter * ObjectListFilter | Or of ObjectListFilter * ObjectListFilter | Not of ObjectListFilter - | Equals of FieldFilter + | Equals of Filter : FieldFilter * Comparer : System.Collections.IComparer | GreaterThan of FieldFilter + | GreaterThanOrEqual of FieldFilter | LessThan of FieldFilter - | StartsWith of FieldFilter - | EndsWith of FieldFilter - | Contains of FieldFilter + | LessThanOrEqual of FieldFilter + | In of FieldFilter + | StartsWith of Filter : FieldFilter * Comparer : System.StringComparer + | EndsWith of Filter : FieldFilter * Comparer : System.StringComparer + | Contains of Filter : FieldFilter * Comparer : System.Collections.IComparer + | OfTypes of System.Type list | FilterField of FieldFilter ``` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 948a801c..059e705c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -288,4 +288,5 @@ * **Breaking Change** Migrated to .NET 10 * **Breaking Change** Made Relay `Edge` a read-only struct +* Added case-insensitive string comparison support to `ObjectListFilter`, including comparer-aware filter cases and GraphQL filter suffix handling * Improved Relay XML documentation comments diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj index 09357bf2..9eddcf46 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FSharp.Data.GraphQL.Server.Middleware.fsproj @@ -21,6 +21,7 @@ + diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs b/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs new file mode 100644 index 00000000..0f6bc905 --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.Middleware/FilterSuffixConstants.fs @@ -0,0 +1,67 @@ +/// +/// String filter suffixes: +/// lowercase (e.g. _ends_with, _ew) → case-insensitive (OrdinalIgnoreCase) +/// Capitalized (e.g. _Ends_With, _EW) → case-sensitive (Ordinal) +/// +/// The submodule contains lowercase suffixes that map to case-insensitive string comparisons. +/// The submodule contains capitalized/uppercase suffixes that map to case-sensitive string comparisons. +/// Numeric and comparison operator suffixes are defined at the module level and are case-insensitive by convention. +/// +/// +[] +module FSharp.Data.GraphQL.Server.Middleware.FilterSuffixConstants + +// Numeric/comparison operators +[] +let GreaterThanOrEqualSuffix = "_greater_than_or_equal" +[] +let GTESuffix = "_gte" +[] +let GreaterThanSuffix = "_greater_than" +[] +let GTSuffix = "_gt" +[] +let LessThanOrEqualSuffix = "_less_than_or_equal" +[] +let LTESuffix = "_lte" +[] +let LessThanSuffix = "_less_than" +[] +let LTSuffix = "_lt" +[] +let InSuffix = "_in" + +/// Case-insensitive string operators (lowercase suffixes → OrdinalIgnoreCase) +module CI = + // String operators (case-insensitive) + [] + let EndsWithSuffix = "_ends_with" + [] + let EWSuffix = "_ew" + [] + let StartsWithSuffix = "_starts_with" + [] + let SWSuffix = "_sw" + [] + let ContainsSuffix = "_contains" + [] + let EqualsSuffix = "_equals" + [] + let EQSuffix = "_eq" + +/// Case-sensitive string operators +module CS = + [] + let EndsWithSuffix = "_Ends_With" + [] + let EWSuffix = "_EW" + [] + let StartsWithSuffix = "_Starts_With" + [] + let SWSuffix = "_SW" + [] + let ContainsSuffix = "_Contains" + [] + let EqualsSuffix = "_Equals" + [] + let EQSuffix = "_EQ" diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 12f54d71..bb4f80e4 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,25 +1,36 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System +open System.Collections open FSharp.Data.GraphQL /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string; Value : 'Val } +/// /// A filter definition for an object list. +/// +/// +/// String-based filters can carry a comparer. When the comparer is not provided by the default +/// string operators, `StartsWith`, `EndsWith`, and string `Contains` preserve the existing +/// case-sensitive `StringComparison.CurrentCulture` behavior. +/// `StringComparer.CurrentCultureIgnoreCase` enables case-insensitive matching. +/// When filters are provided through GraphQL input, lowercase string suffixes are interpreted +/// as case-insensitive and capitalized suffixes are interpreted as case-sensitive. +/// type ObjectListFilter = | And of ObjectListFilter * ObjectListFilter | Or of ObjectListFilter * ObjectListFilter | Not of ObjectListFilter - | Equals of FieldFilter - | GreaterThan of FieldFilter - | GreaterThanOrEqual of FieldFilter - | LessThan of FieldFilter - | LessThanOrEqual of FieldFilter + | Equals of Filter : FieldFilter * Comparer : IComparer + | GreaterThan of FieldFilter + | GreaterThanOrEqual of FieldFilter + | LessThan of FieldFilter + | LessThanOrEqual of FieldFilter | In of FieldFilter - | StartsWith of FieldFilter - | EndsWith of FieldFilter - | Contains of FieldFilter + | StartsWith of Filter : FieldFilter * Comparer : StringComparer + | EndsWith of Filter : FieldFilter * Comparer : StringComparer + | Contains of Filter : FieldFilter * Comparer : IComparer | OfTypes of Type list | FilterField of FieldFilter @@ -95,7 +106,7 @@ module ObjectListFilter = let ( ||| ) x y = Or (x, y) /// Creates a new ObjectListFilter representing an EQUALS operation between two comparable values. - let ( === ) fname value = Equals { FieldName = fname; Value = value } + let ( === ) fname value = Equals ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a GREATER THAN operation of a comparable value. let ( >>> ) fname value = GreaterThan { FieldName = fname; Value = value } @@ -110,13 +121,13 @@ module ObjectListFilter = let ( <== ) fname value = LessThanOrEqual { FieldName = fname; Value = value } /// Creates a new ObjectListFilter representing a STARTS WITH operation of a string value. - let ( =@@ ) fname value = StartsWith { FieldName = fname; Value = value } + let ( =@@ ) fname value = StartsWith ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing an ENDS WITH operation of a string value. - let ( @@= ) fname value = EndsWith { FieldName = fname; Value = value } + let ( @@= ) fname value = EndsWith ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a CONTAINS operation. - let ( @=@ ) fname value = Contains { FieldName = fname; Value = value } + let ( @=@ ) fname value = Contains ({ FieldName = fname; Value = value }, null) /// Creates a new ObjectListFilter representing a IN operation. let ( =~= ) fname value = In { FieldName = fname; Value = value } @@ -124,9 +135,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 : string) = Equals ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive STARTS WITH operation on a string value. + let ( =@@~ ) fname (value : string) = StartsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive ENDS WITH operation on a string value. + let ( @@=~ ) fname (value : string) = EndsWith ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + + /// Creates a new ObjectListFilter representing a case-insensitive CONTAINS operation on a string value. + let ( @=@~ ) fname (value : string) = Contains ({ FieldName = fname; Value = value }, StringComparer.CurrentCultureIgnoreCase) + let private genericWhereMethod = typeof.GetMethods () |> Seq.where (fun m -> m.Name = "Where") @@ -144,9 +167,11 @@ module ObjectListFilter = let private stringType = typeof let private genericIEnumerableType = typedefof> - 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 StringStartsWithMethod = stringType.GetMethod ("StartsWith", [| stringType; stringComparisonType |]) + let private StringEndsWithMethod = stringType.GetMethod ("EndsWith", [| stringType; stringComparisonType |]) + let private StringContainsMethod = stringType.GetMethod ("Contains", [| stringType; stringComparisonType |]) + let private StringEqualsMethod = stringType.GetMethod ("Equals", [| stringType; stringComparisonType |]) let private unwrapOptionMethod = FSharp.Data.GraphQL.Helpers.moduleType.GetMethod (nameof Helpers.unwrap) @@ -205,6 +230,21 @@ module ObjectListFilter = |> Seq.where (fun m -> m.Name = "Equals") |> Seq.head + /// Maps an IComparer to a StringComparison value. + /// Returns ValueNone only when the comparer is null or is not a recognized StringComparer. + let private comparerToStringComparison (comparer : IComparer) = + match comparer with + | null -> ValueNone + | :? StringComparer as sc -> + if obj.ReferenceEquals (sc, StringComparer.OrdinalIgnoreCase) then ValueSome StringComparison.OrdinalIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.InvariantCultureIgnoreCase) then ValueSome StringComparison.InvariantCultureIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.CurrentCultureIgnoreCase) then ValueSome StringComparison.CurrentCultureIgnoreCase + elif obj.ReferenceEquals (sc, StringComparer.Ordinal) then ValueSome StringComparison.Ordinal + elif obj.ReferenceEquals (sc, StringComparer.InvariantCulture) then ValueSome StringComparison.InvariantCulture + elif obj.ReferenceEquals (sc, StringComparer.CurrentCulture) then ValueSome StringComparison.CurrentCulture + else ValueNone + | _ -> ValueNone + let rec buildFilterExpr isEnumerableQuery (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = let build = buildFilterExpr isEnumerableQuery param buildTypeDiscriminatorCheck @@ -224,31 +264,41 @@ module ObjectListFilter = | _ -> Expression.Convert (``member``, stringType) match filter with - | Not (Equals f) -> + | Not (Equals (f, comparer)) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) + match comparerToStringComparison comparer with + | ValueSome comparison -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Not (Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison)) :> Expression + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.NotEqual (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.NotEqual (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Not (Expression.Call (``const``, equalsMethod, ``member``)) | Not f -> f |> build |> Expression.Not :> Expression | And (f1, f2) -> Expression.AndAlso (build f1, build f2) | Or (f1, f2) -> Expression.OrElse (build f1, build f2) - | Equals f -> + | Equals (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - let hasEqualityOperator = hasEqualityOperator ``member``.Type - match f.Value with - | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) - | NoCast - | NonEnumerableCast _ -> - Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) - | Enumerable -> - let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) - Expression.Call (``const``, equalsMethod, ``member``) + match comparerToStringComparison comparer with + | ValueSome comparison -> + let value = Helpers.unwrap (box f.Value) :?> string + Expression.Call (normalizeStringMemberExpr ``member``, StringEqualsMethod, Expression.Constant (value, typeof), Expression.Constant comparison) :> Expression + | ValueNone -> + let hasEqualityOperator = hasEqualityOperator ``member``.Type + match f.Value with + | NoCast when hasEqualityOperator -> Expression.Equal (``member``, Expression.Constant f.Value) + | NoCast + | NonEnumerableCast _ -> + Expression.Equal (Expression.Convert (``member``, objectType), Expression.Convert ((Expression.Constant f.Value), objectType)) + | Enumerable -> + let ``const`` = Expression.Constant (Values.normalizeOptional ``member``.Type f.Value) + Expression.Call (``const``, equalsMethod, ``member``) | GreaterThan f -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) match f.Value with @@ -273,14 +323,16 @@ module ObjectListFilter = | NoCast -> Expression.LessThanOrEqual (``member``, Expression.Constant f.Value) | Enumerable -> Expression.LessThanOrEqual (``member``, Expression.Constant (Values.normalizeOptional ``member``.Type f.Value)) | NonEnumerableCast ``type`` -> Expression.LessThanOrEqual ((unsafeConvertTo ``type`` ``member``), Expression.Constant f.Value) - | StartsWith f -> + | StartsWith (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value) - | EndsWith f -> + let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringStartsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) + | EndsWith (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) - Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value) + let comparison = comparerToStringComparison (comparer :> IComparer) |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringEndsWithMethod, Expression.Constant f.Value, Expression.Constant comparison) - | Contains f -> + | Contains (f, comparer) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) let isEnumerable (memberType : Type) = not (Type.(=) (memberType, stringType)) @@ -316,7 +368,8 @@ module ObjectListFilter = | :? FieldInfo as field when field.FieldType |> isEnumerable -> callContains field.FieldType | _ -> let unwrappedValue = Helpers.unwrap f.Value - Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant unwrappedValue) + let comparison = comparerToStringComparison comparer |> ValueOption.defaultValue StringComparison.CurrentCulture + Expression.Call (normalizeStringMemberExpr ``member``, StringContainsMethod, Expression.Constant (unwrappedValue :?> string, typeof), Expression.Constant comparison) | In f when not (f.Value.IsEmpty) -> let ``member`` = Expression.PropertyOrField (param, f.FieldName) let enumerableContains = getEnumerableContainsMethod objectType diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index e8239cad..813c3b7b 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -9,9 +9,10 @@ open FSharp.Data.GraphQL.Ast open FsToolkit.ErrorHandling type private ComparisonOperator = - | EndsWith of string - | StartsWith of string - | Contains of string + | EndsWith of FieldName : string * Comparer : StringComparer + | StartsWith of FieldName : string * Comparer : StringComparer + | Contains of FieldName : string * Comparer : StringComparer + | StringEquals of FieldName : string * Comparer : StringComparer | Equals of string | GreaterThan of string | GreaterThanOrEqual of string @@ -19,26 +20,40 @@ type private ComparisonOperator = | LessThanOrEqual of string | In of string + let rec private coerceObjectListFilterInput (variables : Variables) inputValue : Result = let parseFieldCondition (s : string) = - let s = s.ToLowerInvariant () let prefix (suffix : string) (s : string) = s.Substring (0, s.Length - suffix.Length) + // Phase 1: case-sensitive string ops – match original string against capitalized/uppercase suffixes + match s with + | s when s.EndsWith FilterSuffixConstants.CS.EndsWithSuffix && s.Length > FilterSuffixConstants.CS.EndsWithSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CS.EndsWithSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EWSuffix && s.Length > FilterSuffixConstants.CS.EWSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CS.EWSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.StartsWithSuffix && s.Length > FilterSuffixConstants.CS.StartsWithSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CS.StartsWithSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.SWSuffix && s.Length > FilterSuffixConstants.CS.SWSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CS.SWSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.ContainsSuffix && s.Length > FilterSuffixConstants.CS.ContainsSuffix.Length -> Contains (prefix FilterSuffixConstants.CS.ContainsSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EqualsSuffix && s.Length > FilterSuffixConstants.CS.EqualsSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CS.EqualsSuffix s, StringComparer.CurrentCulture) + | s when s.EndsWith FilterSuffixConstants.CS.EQSuffix && s.Length > FilterSuffixConstants.CS.EQSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CS.EQSuffix s, StringComparer.CurrentCulture) + | _ -> + // Phase 2: case-insensitive string ops and numeric ops – lower-case before matching + let s = s.ToLowerInvariant () match s with - | 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) - | s when s.EndsWith ("_sw") && s.Length > "_sw".Length -> StartsWith (prefix "_sw" s) - | s when s.EndsWith ("_contains") && s.Length > "_contains".Length -> Contains (prefix "_contains" s) - | s when s.EndsWith ("_greater_than") && s.Length > "_greater_than".Length -> GreaterThan (prefix "_greater_than" s) - | s when s.EndsWith ("_gt") && s.Length > "_gt".Length -> GreaterThan (prefix "_gt" s) - | s when s.EndsWith ("_greater_than_or_equal") && s.Length > "_greater_than_or_equal".Length -> GreaterThanOrEqual (prefix "_greater_than_or_equal" s) - | s when s.EndsWith ("_gte") && s.Length > "_gte".Length -> GreaterThanOrEqual (prefix "_gte" s) - | s when s.EndsWith ("_less_than") && s.Length > "_less_than".Length -> LessThan (prefix "_less_than" s) - | s when s.EndsWith ("_lt") && s.Length > "_lt".Length -> LessThan (prefix "_lt" s) - | s when s.EndsWith ("_less_than_or_equal") && s.Length > "_less_than_or_equal".Length -> LessThanOrEqual (prefix "_less_than_or_equal" s) - | s when s.EndsWith ("_lte") && s.Length > "_lte".Length -> LessThanOrEqual (prefix "_lte" s) - | s when s.EndsWith ("_in") && s.Length > "_in".Length -> In (prefix "_in" s) + | s when s.EndsWith FilterSuffixConstants.CI.EndsWithSuffix && s.Length > FilterSuffixConstants.CI.EndsWithSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CI.EndsWithSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EWSuffix && s.Length > FilterSuffixConstants.CI.EWSuffix.Length -> EndsWith (prefix FilterSuffixConstants.CI.EWSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.StartsWithSuffix && s.Length > FilterSuffixConstants.CI.StartsWithSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CI.StartsWithSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.SWSuffix && s.Length > FilterSuffixConstants.CI.SWSuffix.Length -> StartsWith (prefix FilterSuffixConstants.CI.SWSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.ContainsSuffix && s.Length > FilterSuffixConstants.CI.ContainsSuffix.Length -> Contains (prefix FilterSuffixConstants.CI.ContainsSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EqualsSuffix && s.Length > FilterSuffixConstants.CI.EqualsSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CI.EqualsSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.CI.EQSuffix && s.Length > FilterSuffixConstants.CI.EQSuffix.Length -> StringEquals (prefix FilterSuffixConstants.CI.EQSuffix s, StringComparer.CurrentCultureIgnoreCase) + | s when s.EndsWith FilterSuffixConstants.GreaterThanOrEqualSuffix && s.Length > FilterSuffixConstants.GreaterThanOrEqualSuffix.Length -> GreaterThanOrEqual (prefix FilterSuffixConstants.GreaterThanOrEqualSuffix s) + | s when s.EndsWith FilterSuffixConstants.GTESuffix && s.Length > FilterSuffixConstants.GTESuffix.Length -> GreaterThanOrEqual (prefix FilterSuffixConstants.GTESuffix s) + | s when s.EndsWith FilterSuffixConstants.GreaterThanSuffix && s.Length > FilterSuffixConstants.GreaterThanSuffix.Length -> GreaterThan (prefix FilterSuffixConstants.GreaterThanSuffix s) + | s when s.EndsWith FilterSuffixConstants.GTSuffix && s.Length > FilterSuffixConstants.GTSuffix.Length -> GreaterThan (prefix FilterSuffixConstants.GTSuffix s) + | s when s.EndsWith FilterSuffixConstants.LessThanOrEqualSuffix && s.Length > FilterSuffixConstants.LessThanOrEqualSuffix.Length -> LessThanOrEqual (prefix FilterSuffixConstants.LessThanOrEqualSuffix s) + | s when s.EndsWith FilterSuffixConstants.LTESuffix && s.Length > FilterSuffixConstants.LTESuffix.Length -> LessThanOrEqual (prefix FilterSuffixConstants.LTESuffix s) + | s when s.EndsWith FilterSuffixConstants.LessThanSuffix && s.Length > FilterSuffixConstants.LessThanSuffix.Length -> LessThan (prefix FilterSuffixConstants.LessThanSuffix s) + | s when s.EndsWith FilterSuffixConstants.LTSuffix && s.Length > FilterSuffixConstants.LTSuffix.Length -> LessThan (prefix FilterSuffixConstants.LTSuffix s) + | s when s.EndsWith FilterSuffixConstants.InSuffix && s.Length > FilterSuffixConstants.InSuffix.Length -> In (prefix FilterSuffixConstants.InSuffix s) | s -> Equals s let (|EquatableValue|NonEquatableValue|) v = @@ -96,15 +111,16 @@ let rec private coerceObjectListFilterInput (variables : Variables) inputValue : | Error errs -> Error errs | Ok ValueNone -> Ok ValueNone | Ok (ValueSome filter) -> Ok (ValueSome (Not filter)) - | 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 })) + | EndsWith (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.EndsWith ({ FieldName = fname; Value = value }, comparer))) + | StartsWith (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.StartsWith ({ FieldName = fname; Value = value }, comparer))) + | Contains (fname, comparer), ComparableValue value -> Ok (ValueSome (ObjectListFilter.Contains ({ FieldName = fname; Value = value }, comparer))) + | StringEquals (fname, comparer), StringValue value -> Ok (ValueSome (ObjectListFilter.Equals ({ FieldName = fname; Value = value }, comparer))) | Equals fname, ObjectValue value -> match mapInput value with | Error errs -> Error errs | Ok ValueNone -> Ok ValueNone | Ok (ValueSome filter) -> Ok (ValueSome (FilterField { FieldName = fname; Value = filter })) - | Equals fname, EquatableValue value -> Ok (ValueSome (ObjectListFilter.Equals { FieldName = fname; Value = value })) + | Equals fname, EquatableValue value -> Ok (ValueSome (ObjectListFilter.Equals ({ FieldName = fname; Value = value }, null))) | GreaterThan fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.GreaterThan { FieldName = fname; Value = value })) | GreaterThanOrEqual fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.GreaterThanOrEqual { FieldName = fname; Value = value })) | LessThan fname, ComparableValue value -> Ok (ValueSome (ObjectListFilter.LessThan { FieldName = fname; Value = value })) @@ -165,7 +181,14 @@ let ObjectListFilterType : InputCustomDefinition = { Name = "ObjectListFilter" Description = Some - "The `Filter` scalar type represents a filter on one or more fields of an object in an object list. The filter is represented by a JSON object where the fields are the complemented by specific suffixes to represent a query." + (String.concat + " " + [ + "The ObjectListFilter value represents field filters for object lists." + "Lowercase string suffixes such as `_starts_with`/`_sw`, `_ends_with`/`_ew`, `_contains` (no shorthand), and `_equals`/`_eq` are case-insensitive when applied to string fields." + "Capitalized string suffixes such as `_Starts_With`/`_SW`, `_Ends_With`/`_EW`, `_Contains` (no shorthand), and `_Equals`/`_EQ` are case-sensitive when applied to string fields." + "Comparison suffixes such as `_gt`, `_gte`, `_lt`, `_lte`, and `_in` are also supported." + ]) CoerceInput = (fun _ input variables -> match input with diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 8859057e..0e177cae 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -599,7 +599,7 @@ let ``Object list filter: must return filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "s" ]) (And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" })) + kvp ([ "A"; "s" ]) (And (Equals ({ FieldName = "id"; Value = 2L }, null), StartsWith ({ FieldName = "value"; Value = "A" }, StringComparer.CurrentCultureIgnoreCase))) let result = execute query ensureDirect result <| fun data errors -> @@ -647,7 +647,7 @@ let ``Object list filter: Must return AND filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (And (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + kvp ([ "A"; "subjects" ]) (And (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase), Equals ({ FieldName = "id"; Value = 6L }, null))) let result = execute query ensureDirect result <| fun data errors -> @@ -693,7 +693,7 @@ let ``Object list filter: Must return OR filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Or (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + kvp ([ "A"; "subjects" ]) (Or (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase), Equals ({ FieldName = "id"; Value = 6L }, null))) let result = execute query ensureDirect result <| fun data errors -> @@ -785,7 +785,7 @@ let ``Object list filter: Must return Contains filter information in Metadata`` ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Contains { FieldName = "value"; Value = "3" }) + kvp ([ "A"; "subjects" ]) (Contains ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let result = execute query ensureDirect result <| fun data errors -> @@ -831,7 +831,7 @@ let ``Object list filter: Must return NOT filter information in Metadata`` () = ] ] let expectedFilter : KeyValuePair = - kvp ([ "A"; "subjects" ]) (Not (StartsWith { FieldName = "value"; Value = "3" })) + kvp ([ "A"; "subjects" ]) (Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase))) let result = execute query ensureDirect result <| fun data errors -> @@ -879,7 +879,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notStartsFilter = """{ "not": { "value_starts_with": "3" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notStartsFilter) - let filter = Not (StartsWith { FieldName = "value"; Value = "3" }) + let filter = Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -891,7 +891,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEndsFilter = """{ "not": { "value_ends_with": "2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEndsFilter) - let filter = Not (EndsWith { FieldName = "value"; Value = "2" }) + let filter = Not (EndsWith ({ FieldName = "value"; Value = "2" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -903,7 +903,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notStartsFilter = """{ "not": { "value_sw": "3" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notStartsFilter) - let filter = Not (StartsWith { FieldName = "value"; Value = "3" }) + let filter = Not (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -915,7 +915,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEndsFilter = """{ "not": { "value_ew": "2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEndsFilter) - let filter = Not (EndsWith { FieldName = "value"; Value = "2" }) + let filter = Not (EndsWith ({ FieldName = "value"; Value = "2" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1023,7 +1023,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notContainsFilter = """{ "not": { "value_contains": "A" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notContainsFilter) - let filter = Not (Contains { FieldName = "value"; Value = "A" }) + let filter = Not (Contains ({ FieldName = "value"; Value = "A" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1035,7 +1035,7 @@ let ``Object list filter: Must return filter information in Metadata when suppli do let notEqualsFilter = """{ "not": { "value": "A2" } }""" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", notEqualsFilter) - let filter = Not (Equals { FieldName = "value"; Value = "A2" }) + let filter = Not (Equals ({ FieldName = "value"; Value = "A2" }, null)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) @@ -1084,7 +1084,7 @@ let ``Object list filter: Must parse filter that references variables`` () = do let filterValue = "3" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("filter", filterValue) - let filter = (StartsWith { FieldName = "value"; Value = "3" }) + let filter = (StartsWith ({ FieldName = "value"; Value = "3" }, StringComparer.CurrentCultureIgnoreCase)) let expectedFilter : KeyValuePair = kvp ([ "A"; "subjects" ]) (filter) let result = executeAndVerifyFilter (query, variables, filter) diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs index d249e788..c8a39200 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqGenerateTests.fs @@ -103,7 +103,7 @@ let filterOptions = ObjectListFilterLinqOptions.None [] let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "validStringStruct"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "validStringStruct"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] = "Jonathan")""" @@ -111,7 +111,7 @@ let ``ObjectListFilter works with Equals operator for ValidStringStruct`` () = [] let ``ObjectListFilter works with not Equals operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "validStringStruct"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "validStringStruct"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["validStringStruct"] != "Jonathan")""" @@ -119,7 +119,7 @@ let ``ObjectListFilter works with not Equals operator for ValidStringStruct`` () [] let ``ObjectListFilter works with Equals operator for ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "valueOptionString"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = "Jonathan")""" @@ -127,7 +127,7 @@ let ``ObjectListFilter works with Equals operator for ValueOptionString`` () = [] let ``ObjectListFilter works with not Equals operator for ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "valueOptionString"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "valueOptionString"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != "Jonathan")""" @@ -135,7 +135,7 @@ let ``ObjectListFilter works with not Equals operator for ValueOptionString`` () [] let ``ObjectListFilter works with Equals operator for null ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = null } + let filter = Equals ({ FieldName = "valueOptionString"; Value = null }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" @@ -143,7 +143,7 @@ let ``ObjectListFilter works with Equals operator for null ValueOptionString`` ( [] let ``ObjectListFilter works with Equals operator for ValueNone ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) } + let filter = Equals ({ FieldName = "valueOptionString"; Value = (ValueNone : voption) }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] = null)""" @@ -151,7 +151,7 @@ let ``ObjectListFilter works with Equals operator for ValueNone ValueOptionStrin [] let ``ObjectListFilter works with not Equals operator for ValueNone ValueOptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "valueOptionString"; Value = (ValueNone : voption) }) + let filter = Not (Equals ({ FieldName = "valueOptionString"; Value = (ValueNone : voption) }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["valueOptionString"] != null)""" @@ -159,7 +159,7 @@ let ``ObjectListFilter works with not Equals operator for ValueNone ValueOptionS [] let ``ObjectListFilter works with Equals operator for OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "optionString"; Value = "Jonathan" } + let filter = Equals ({ FieldName = "optionString"; Value = "Jonathan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] = "Jonathan")""" @@ -167,7 +167,7 @@ let ``ObjectListFilter works with Equals operator for OptionString`` () = [] let ``ObjectListFilter works with not Equals operator for OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "optionString"; Value = "Jonathan" }) + let filter = Not (Equals ({ FieldName = "optionString"; Value = "Jonathan" }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText """SELECT VALUE root FROM root WHERE (root["optionString"] != "Jonathan")""" @@ -175,7 +175,7 @@ let ``ObjectListFilter works with not Equals operator for OptionString`` () = [] let ``ObjectListFilter works with Equals operator for null OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Equals { FieldName = "optionString"; Value = null } + let filter = Equals ({ FieldName = "optionString"; Value = null }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" @@ -183,7 +183,7 @@ let ``ObjectListFilter works with Equals operator for null OptionString`` () = [] let ``ObjectListFilter works with not Equals operator for null OptionString`` () = let queryable = container.GetItemLinqQueryable () - let filter = Not (Equals { FieldName = "optionString"; Value = null }) + let filter = Not (Equals ({ FieldName = "optionString"; Value = null }, null)) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE (root["optionString"] = null)""" @@ -191,31 +191,55 @@ let ``ObjectListFilter works with not Equals operator for null OptionString`` () [] let ``ObjectListFilter works with StartsWith operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = StartsWith { FieldName = "validStringStruct"; Value = "J" } + let filter = StartsWith ({ FieldName = "validStringStruct"; Value = "J" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE STARTSWITH(root["validStringStruct"], "J")""" +[] +let ``ObjectListFilter works with StartsWith case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = StartsWith ({ FieldName = "validStringStruct"; Value = "J" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE STARTSWITH(root["validStringStruct"], "J", true)""" + [] let ``ObjectListFilter works with EndsWith operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = EndsWith { FieldName = "validStringStruct"; Value = "n" } + let filter = EndsWith ({ FieldName = "validStringStruct"; Value = "n" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ENDSWITH(root["validStringStruct"], "n")""" +[] +let ``ObjectListFilter works with EndsWith case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = EndsWith ({ FieldName = "validStringStruct"; Value = "n" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ENDSWITH(root["validStringStruct"], "n", true)""" + [] let ``ObjectListFilter works with Contains operator for ValidStringStruct`` () = let queryable = container.GetItemLinqQueryable () - let filter = Contains { FieldName = "validStringStruct"; Value = "athan" } + let filter = Contains ({ FieldName = "validStringStruct"; Value = "athan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan")""" +[] +let ``ObjectListFilter works with Contains case insensitive operator for ValidStringStruct`` () = + let queryable = container.GetItemLinqQueryable () + let filter = Contains ({ FieldName = "validStringStruct"; Value = "athan" }, StringComparer.OrdinalIgnoreCase) + let filterQuery = queryable.Apply (filter, filterOptions) + let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery + equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE CONTAINS(root["validStringStruct"], "athan", true)""" + [] let ``ObjectListFilter works with Contains operator for ValidStringStruct list`` () = let queryable = container.GetItemLinqQueryable () - let filter = Contains { FieldName = "validStringStructList"; Value = "athan" } + let filter = Contains ({ FieldName = "validStringStructList"; Value = "athan" }, null) let filterQuery = queryable.Apply (filter, filterOptions) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery equals queryDefinition.QueryText, """SELECT VALUE root FROM root WHERE ARRAY_CONTAINS(root["validStringStructList"], "athan")""" @@ -238,7 +262,7 @@ let ``ObjectListFilter works with In operator for empty ValidStringStruct list`` [] let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = - let filter = Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" } + let filter = Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null) let queryable = container.GetItemLinqQueryable () let filterQuery = queryable.Apply (filter) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery @@ -246,7 +270,7 @@ let ``ObjectListFilter works with Equals operator for ValidStringObject`` () = [] let ``ObjectListFilter works with not Equals operator for ValidStringObject`` () = - let filter = Not (Equals { FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }) + let filter = Not (Equals ({ FieldName = "validStringObject"; Value = ValidStringObject "Jonathan" }, null)) let queryable = container.GetItemLinqQueryable () let filterQuery = queryable.Apply (filter) let queryDefinition = CosmosLinqExtensions.ToQueryDefinition filterQuery diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs index f1c06fa8..bdd14b70 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -9,7 +9,7 @@ open FSharp.Data.GraphQL.Tests.LinqTests [] let ``ObjectListFilter works with Equals operator`` () = - let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable + let filter = Equals ({ FieldName = "firstName"; Value = "Jonathan" }, null) // :> IComparable let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 1 @@ -90,7 +90,7 @@ let ``ObjectListFilter works with LessThanOrEqual operator`` () = [] let ``ObjectListFilter works with StartsWith operator`` () = - let filter = StartsWith { FieldName = "firstName"; Value = "J" } + let filter = StartsWith ({ FieldName = "firstName"; Value = "J" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -103,7 +103,7 @@ let ``ObjectListFilter works with StartsWith operator`` () = [] let ``ObjectListFilter works with Contains operator`` () = - let filter = Contains { FieldName = "firstName"; Value = "en" } + let filter = Contains ({ FieldName = "firstName"; Value = "en" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -116,7 +116,7 @@ let ``ObjectListFilter works with Contains operator`` () = [] let ``ObjectListFilter works with EndsWith operator`` () = - let filter = EndsWith { FieldName = "lastName"; Value = "ams" } + let filter = EndsWith ({ FieldName = "lastName"; Value = "ams" }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -130,7 +130,7 @@ let ``ObjectListFilter works with EndsWith operator`` () = [] let ``ObjectListFilter works with AND operator`` () = let filter = - And (Contains { FieldName = "firstName"; Value = "en" }, Equals { FieldName = "lastName"; Value = "Adams" }) + And (Contains ({ FieldName = "firstName"; Value = "en" }, null), Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 1 @@ -144,7 +144,7 @@ let ``ObjectListFilter works with AND operator`` () = [] let ``ObjectListFilter works with OR operator`` () = let filter = - Or (GreaterThan { FieldName = "id"; Value = 4 }, Equals { FieldName = "lastName"; Value = "Adams" }) + Or (GreaterThan { FieldName = "id"; Value = 4 }, Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -191,7 +191,7 @@ let ``ObjectListFilter works with IN operator for int type field`` () = [] let ``ObjectListFilter works with Contains operator for array type field`` () = - let filter = Contains { FieldName = "friends"; Value = { Email = "j.abrams@gmail.com" } } + let filter = Contains ({ FieldName = "friends"; Value = { Email = "j.abrams@gmail.com" } }, null) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -215,7 +215,7 @@ let ``ObjectListFilter works with FilterField operator`` () = let filter = FilterField { FieldName = "Contact" - Value = Contains { FieldName = "Email"; Value = "j.trif@gmail.com" } + Value = Contains ({ FieldName = "Email"; Value = "j.trif@gmail.com" }, null) } let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList @@ -229,7 +229,7 @@ let ``ObjectListFilter works with FilterField operator`` () = [] let ``ObjectListFilter works with NOT operator`` () = - let filter = Not (Equals { FieldName = "lastName"; Value = "Adams" }) + let filter = Not (Equals ({ FieldName = "lastName"; Value = "Adams" }, null)) let queryable = data.AsQueryable () let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 @@ -416,7 +416,7 @@ let ``ObjectListFilter works with getDiscriminatorValue for Horse`` () = [] let ``ObjectListFilter works with getDiscriminatorValue startsWith for Horse and Hamster`` () = let queryable = animalData.AsQueryable () - let filter = StartsWith { FieldName = "Discriminator"; Value = "H" } + let filter = StartsWith ({ FieldName = "Discriminator"; Value = "H" }, null) let options = ObjectListFilterLinqOptions ( (fun entity (discriminator : string) -> entity.Discriminator.StartsWith discriminator), @@ -449,7 +449,7 @@ let ``ObjectListFilter works with Contains operator on list collection propertie { Name = "Product D"; Tags = [ "Tag4"; "Tag5" ] } ] let queryable = productList.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -471,7 +471,7 @@ let ``ObjectListFilter works with Contains operator on array collection properti { Name = "Product D"; Tags = [| "Tag4"; "Tag5" |] } ] let queryable = productArray.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -493,7 +493,7 @@ let ``ObjectListFilter works with Contains operator on set collection properties { Name = "Product D"; Tags = [| "Tag4"; "Tag5" |] |> Set.ofArray } ] let queryable = productArray.AsQueryable () - let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filter = Contains ({ FieldName = "Tags"; Value = "Tag3" }, null) let filteredData = queryable.Apply (filter) |> Seq.toList List.length filteredData |> equals 2 do @@ -526,3 +526,95 @@ 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 Equals case insensitive operator`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "jonathan" }, StringComparer.OrdinalIgnoreCase) + 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 Equals case sensitive operator`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "jonathan" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with Equals case insensitive operator upper case`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "JONATHAN" }, StringComparer.OrdinalIgnoreCase) + 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 Equals case sensitive operator upper case`` () = + let filter = Equals ({ FieldName = "firstName"; Value = "JONATHAN" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with StartsWith case insensitive operator`` () = + let filter = StartsWith ({ FieldName = "firstName"; Value = "j" }, StringComparer.OrdinalIgnoreCase) + 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 StartsWith case sensitive operator`` () = + let filter = StartsWith ({ FieldName = "firstName"; Value = "j" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with EndsWith case insensitive operator`` () = + let filter = EndsWith ({ FieldName = "lastName"; Value = "AMS" }, StringComparer.OrdinalIgnoreCase) + 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 EndsWith case sensitive operator`` () = + let filter = EndsWith ({ FieldName = "lastName"; Value = "AMS" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0 + +[] +let ``ObjectListFilter works with Contains case insensitive operator`` () = + let filter = Contains ({ FieldName = "firstName"; Value = "EN" }, StringComparer.OrdinalIgnoreCase) + 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 works with Contains case sensitive operator`` () = + let filter = Contains ({ FieldName = "firstName"; Value = "EN" }, null) + let queryable = data.AsQueryable () + let filteredData = queryable.Apply (filter) |> Seq.toList + List.length filteredData |> equals 0