Skip to content

Commit 01682cf

Browse files
committed
fixup! ObjectListFilter filter values to target type coercion (#589)
1 parent ab3b961 commit 01682cf

10 files changed

Lines changed: 873 additions & 94 deletions

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

Lines changed: 263 additions & 49 deletions
Large diffs are not rendered by default.

tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@
8080
<Compile Include="ObjectListFilter\TypeCoercionTests.Common.fs" />
8181
<Compile Include="ObjectListFilter\TypeCoercionValueTests.fs" />
8282
<Compile Include="ObjectListFilter\TypeCoercionFilterTests.fs" />
83+
<Compile Include="ObjectListFilter\TypeCoercionFilterInOperatorTests.fs" />
84+
<Compile Include="ObjectListFilter\TypeCoercionFilterOptionCollectionTests.fs" />
8385
<Compile Include="DeferredTests.fs" />
8486
<Compile Include="SubscriptionTests.fs" />
8587
<Compile Include="MiddlewareTests.fs" />

tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/ObjectListFilterLinqGenerateTests.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.Linq)>]
2-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.Linq)>]
2+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
33
module FSharp.Data.GraphQL.Tests.ObjectListFilter.Linq.GenerateTests
44

55
open Xunit

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.Linq)>]
2-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.Linq)>]
2+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
33
module FSharp.Data.GraphQL.Tests.ObjectListFilter.Linq.Tests
44

55
open Xunit

tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterFieldEnumerableTests.fs

Lines changed: 414 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
2+
[<Xunit.Trait (Tests.TraitType.ObjectListFilterOperator, "in")>]
3+
module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.InOperatorTests
4+
5+
open Xunit
6+
open FSharp.Data.GraphQL.Server.Middleware
7+
open FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common
8+
9+
// ──────────────────────────────────────────────────────────────────────────────
10+
// In operator test cases
11+
// Covers all coercion types: CLR enum, DU-as-enum, Guid, single-case DU, and primitives
12+
// ──────────────────────────────────────────────────────────────────────────────
13+
14+
[<Fact>]
15+
let ``In operator coerces string primitives`` () =
16+
let filter = In { FieldName = "name"; Value = [ box "Alice"; box "Bob" ] }
17+
let result = applyFilter filter
18+
result |> List.length |> equals 2
19+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ]
20+
21+
[<Fact>]
22+
let ``In operator coerces int primitives`` () =
23+
let filter = In { FieldName = "id"; Value = [ box 1; box 3 ] }
24+
let result = applyFilter filter
25+
result |> List.length |> equals 2
26+
result |> List.map (fun e -> e.Id) |> List.sort |> equals [ 1; 3 ]
27+
28+
[<Fact>]
29+
let ``In operator coerces CLR enum`` () =
30+
let filter = In { FieldName = "color"; Value = [ box "Red"; box "Blue" ] }
31+
let result = applyFilter filter
32+
result |> List.length |> equals 2
33+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
34+
35+
[<Fact>]
36+
let ``In operator coerces DU-as-enum`` () =
37+
let filter = In { FieldName = "status"; Value = [ box "Active"; box "Pending" ] }
38+
let result = applyFilter filter
39+
result |> List.length |> equals 2
40+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
41+
42+
[<Fact>]
43+
let ``In operator coerces Guid`` () =
44+
let filter =
45+
In {
46+
FieldName = "guidField"
47+
Value = [
48+
box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
49+
box "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
50+
]
51+
}
52+
let result = applyFilter filter
53+
result |> List.length |> equals 2
54+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ]
55+
56+
[<Fact>]
57+
let ``In operator coerces single-case DU wrapping string`` () =
58+
let filter = In { FieldName = "wrappedName"; Value = [ box "Alice"; box "Charlie" ] }
59+
let result = applyFilter filter
60+
result |> List.length |> equals 2
61+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
62+
63+
[<Fact>]
64+
let ``In operator coerces single-case DU wrapping int`` () =
65+
let filter = In { FieldName = "wrappedScore"; Value = [ box 10; box 30 ] }
66+
let result = applyFilter filter
67+
result |> List.length |> equals 2
68+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
69+
70+
[<Fact>]
71+
let ``In operator coerces single-case DU wrapping Guid`` () =
72+
let filter =
73+
In {
74+
FieldName = "wrappedGuid"
75+
Value = [ box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; box "cccccccc-cccc-cccc-cccc-cccccccccccc" ]
76+
}
77+
let result = applyFilter filter
78+
result |> List.length |> equals 2
79+
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
2+
[<Xunit.Trait (Tests.TraitType.ObjectListFilterOperator, "in")>]
3+
module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.OptionCollectionTests
4+
5+
open System
6+
open System.Linq
7+
open System.Text.Json
8+
open Xunit
9+
open FSharp.Data.GraphQL.Shared
10+
open FSharp.Data.GraphQL.Server.Middleware
11+
12+
// ────────────────────────────────────────────────────────────────────────────────────
13+
// Test types for option collection filters
14+
// ────────────────────────────────────────────────────────────────────────────────────
15+
16+
/// Tags wrapped in an option — tests FilterField with optional collection.
17+
type OptionalTagsEntity = {
18+
Id : int
19+
Name : string
20+
/// Optional list of tags — tests the edge case where the field itself is optional
21+
OptionalTags : string list option
22+
}
23+
24+
// ────────────────────────────────────────────────────────────────────────────────────
25+
// Test data
26+
// ────────────────────────────────────────────────────────────────────────────────────
27+
28+
let filterOptions =
29+
ObjectListFilterLinqOptions<OptionalTagsEntity, obj> (Json.getSerializerOptions Seq.empty)
30+
31+
let testData = [|
32+
{ Id = 1; Name = "Alice"; OptionalTags = Some [ "admin"; "user" ] }
33+
{ Id = 2; Name = "Bob"; OptionalTags = Some [ "user" ] }
34+
{ Id = 3; Name = "Charlie"; OptionalTags = None }
35+
{ Id = 4; Name = "Diana"; OptionalTags = Some [] }
36+
|]
37+
38+
let applyFilter (filter : ObjectListFilter) =
39+
filter.ApplyTo (testData.AsQueryable (), filterOptions)
40+
|> Seq.toList
41+
42+
// ────────────────────────────────────────────────────────────────────────────────────
43+
// Tests for FilterField with optional collections
44+
// ────────────────────────────────────────────────────────────────────────────────────
45+
46+
[<Fact>]
47+
let ``FilterField on optional collection with In operator returns entities where tag matches`` () =
48+
// Regression test: FilterField on optional collection should unwrap the option
49+
// and correctly apply the nested filter to the contained collection
50+
let filter =
51+
FilterField {
52+
FieldName = "OptionalTags"
53+
Value = In { FieldName = "_"; Value = [ box "admin" ] }
54+
}
55+
let result = applyFilter filter
56+
57+
Assert.Equal (1, result.Length)
58+
Assert.Equal<seq<string>> ([| "Alice" |] :> seq<_>, result |> List.map (fun e -> e.Name) |> List.toSeq)
59+
60+
[<Fact>]
61+
let ``FilterField on optional collection with multiple values`` () =
62+
let filter =
63+
FilterField {
64+
FieldName = "OptionalTags"
65+
Value = In { FieldName = "_"; Value = [ box "admin"; box "moderator" ] }
66+
}
67+
let result = applyFilter filter
68+
69+
Assert.Equal (1, result.Length)
70+
Assert.Equal<seq<string>> ([| "Alice" |] :> seq<_>, result |> List.map (fun e -> e.Name) |> List.toSeq)
71+
72+
[<Fact>]
73+
let ``FilterField on optional collection skips None values`` () =
74+
let filter =
75+
FilterField {
76+
FieldName = "OptionalTags"
77+
Value = In { FieldName = "_"; Value = [ box "user" ] }
78+
}
79+
let result = applyFilter filter
80+
81+
Assert.Equal (2, result.Length)
82+
Assert.Equal<seq<string>> ([| "Alice"; "Bob" |] :> seq<_>, result |> Seq.map (fun e -> e.Name) |> Seq.sort)
83+
84+
[<Fact>]
85+
let ``FilterField on optional collection with empty list`` () =
86+
let filter =
87+
FilterField {
88+
FieldName = "OptionalTags"
89+
Value = In { FieldName = "_"; Value = [ box "user" ] }
90+
}
91+
let result = applyFilter filter
92+
93+
let names = result |> List.map (fun e -> e.Name)
94+
Assert.False (names.Contains "Diana")
95+
96+
[<Fact>]
97+
let ``FilterField on optional collection with Equals operator`` () =
98+
let filter =
99+
FilterField {
100+
FieldName = "OptionalTags"
101+
Value = Equals ({ FieldName = "_"; Value = "user" }, null)
102+
}
103+
let result = applyFilter filter
104+
105+
Assert.Equal (2, result.Length)
106+
Assert.Equal<seq<string>> ([| "Alice"; "Bob" |] :> seq<_>, result |> Seq.map (fun e -> e.Name) |> Seq.sort)

tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionFilterTests.fs

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
22
module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.FilterTests
33

44
open Xunit
@@ -126,45 +126,6 @@ let ``coerceFilter coerces int through voption wrapper for Equals`` () =
126126
result |> List.length |> equals 1
127127
(List.head result).Name |> equals "Charlie"
128128

129-
// ──────────────────────────────────────────────────────────────────────────────
130-
// In operator
131-
// ──────────────────────────────────────────────────────────────────────────────
132-
133-
[<Fact>]
134-
let ``coerceFilter coerces In operator values to CLR enum`` () =
135-
let filter = In { FieldName = "color"; Value = [ box "Red"; box "Blue" ] }
136-
let result = applyFilter filter
137-
result |> List.length |> equals 2
138-
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
139-
140-
[<Fact>]
141-
let ``coerceFilter coerces In operator values to DU-as-enum`` () =
142-
let filter = In { FieldName = "status"; Value = [ box "Active"; box "Pending" ] }
143-
let result = applyFilter filter
144-
result |> List.length |> equals 2
145-
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
146-
147-
[<Fact>]
148-
let ``coerceFilter coerces In operator values to Guid`` () =
149-
let filter =
150-
In {
151-
FieldName = "guidField"
152-
Value = [
153-
box "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
154-
box "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
155-
]
156-
}
157-
let result = applyFilter filter
158-
result |> List.length |> equals 2
159-
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Bob" ]
160-
161-
[<Fact>]
162-
let ``coerceFilter coerces In operator values to single-case DU wrapping int`` () =
163-
let filter = In { FieldName = "wrappedScore"; Value = [ box 10; box 30 ] }
164-
let result = applyFilter filter
165-
result |> List.length |> equals 2
166-
result |> List.map (fun e -> e.Name) |> List.sort |> equals [ "Alice"; "Charlie" ]
167-
168129
// ──────────────────────────────────────────────────────────────────────────────
169130
// string operators
170131
// ──────────────────────────────────────────────────────────────────────────────

tests/FSharp.Data.GraphQL.Tests/ObjectListFilter/TypeCoercionTests.Common.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[<Xunit.TraitAttribute (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
1+
[<Xunit.Trait (Tests.TraitType.Category, Tests.TraitName.ObjectListFilter)>]
22
module FSharp.Data.GraphQL.Tests.ObjectListFilter.TypeCoercion.Common
33

44
open System

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ module TraitType =
2828
[<Literal>]
2929
let Category = "Category"
3030

31+
[<Literal>]
32+
let ObjectListFilterOperator = "ObjectListFilter operator"
33+
3134
module TraitName =
3235

3336
[<Literal>]

0 commit comments

Comments
 (0)