Skip to content

Commit bc7627d

Browse files
authored
Merge pull request #515 from VectorTetra/object-list-filter-voption
Migrated object list filter to `voption`
2 parents cd08ffb + 02f0986 commit bc7627d

File tree

26 files changed

+921
-495
lines changed

26 files changed

+921
-495
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ query TestQuery {
361361
}
362362
```
363363

364+
Also you can apply `not` operator like this:
365+
366+
```graphql
367+
query TestQuery {
368+
hero(id:"1000") {
369+
id
370+
name
371+
appearsIn
372+
homePlanet
373+
friends (filter : { not : { name_starts_with: "A" } }) {
374+
id
375+
name
376+
}
377+
}
378+
}
379+
```
380+
381+
And combine filters with `and` and `or` operators like this:
382+
383+
```graphql
384+
query TestQuery {
385+
hero(id:"1000") {
386+
id
387+
name
388+
appearsIn
389+
homePlanet
390+
friends (filter : { or : [{ name_starts_with: "A"}, { name_starts_with: "B" }]}) {
391+
id
392+
name
393+
}
394+
}
395+
}
396+
```
397+
364398
This filter is mapped by the middleware inside an `ObjectListFilter` definition:
365399

366400
```fsharp
@@ -381,7 +415,7 @@ type ObjectListFilter =
381415
| FilterField of FieldFilter<ObjectListFilter>
382416
```
383417

384-
And the value recovered by the filter in the query is usable in the `ResolveFieldContext` of the resolve function of the field. To easily access it, you can use the extension method `Filter`, wich returns an `ObjectListFilter option` (it does not have a value if the object doesn't implement a list with the middleware generic definition, or if the user didn't provide a filter input).
418+
And the value recovered by the filter in the query is usable in the `ResolveFieldContext` of the resolve function of the field. To easily access it, you can use the extension method `Filter`, which returns an `ObjectListFilter voption` (it does not have a value if the object doesn't implement a list with the middleware generic definition, or if the user didn't provide a filter input).
385419

386420
```fsharp
387421
Define.Field("friends", ListOf (Nullable CharacterType),

samples/relay-book-store/Domain.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module BookCursor =
2020
Title = get.Required.Field "t" Decode.string
2121
})
2222

23-
let tryDecode (x : string) : BookCursor option = option {
23+
let tryDecode (x : string) : BookCursor voption = voption {
2424
let! bytes = Base64.tryDecode x
2525
let! json = Utf8.tryDecode bytes
2626

samples/relay-book-store/Prelude.fs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ namespace FSharp.Data.GraphQL.Samples.RelayBookStore
33
[<AutoOpen>]
44
module internal Prelude =
55

6-
let vopt =
7-
function
8-
| Some x -> ValueSome x
9-
| None -> ValueNone
10-
116
[<RequireQualifiedAccess>]
127
module Base64 =
138

samples/relay-book-store/Schema.fs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,34 @@ let booksField =
5353

5454
let after =
5555
ctx.TryArg ("after")
56-
|> Option.map (fun s ->
56+
|> ValueOption.map (fun s ->
5757
match BookCursor.tryDecode s with
58-
| Some c -> c
59-
| None -> raise (GQLMessageException ("Invalid cursor value for after")))
58+
| ValueSome c -> c
59+
| ValueNone -> raise (GQLMessageException ("Invalid cursor value for after")))
6060

6161
let last = ctx.TryArg ("last")
6262

6363
let before =
6464
ctx.TryArg ("before")
65-
|> Option.map (fun s ->
65+
|> ValueOption.map (fun s ->
6666
match BookCursor.tryDecode s with
67-
| Some c -> c
68-
| None -> raise (GQLMessageException ("Invalid cursor value for before")))
67+
| ValueSome c -> c
68+
| ValueNone -> raise (GQLMessageException ("Invalid cursor value for before")))
6969

7070
match first, after, last, before with
71-
| Some first, _, None, None ->
71+
| ValueSome first, _, ValueNone, ValueNone ->
7272
if first < 0 then
7373
raise (GQLMessageException ($"first must be at least 0"))
7474

75-
Forward (first, vopt after)
76-
| None, None, Some last, _ ->
75+
Forward (first, after)
76+
| ValueNone, ValueNone, ValueSome last, _ ->
7777
if last < 0 then
7878
raise (GQLMessageException ($"last must be at least 0"))
7979

80-
Backward (last, vopt before)
81-
| None, _, None, _ -> raise (GQLMessageException ($"Must specify first or last"))
82-
| Some _, _, _, _ -> raise (GQLMessageException ($"Must not combine first with last or before"))
83-
| _, _, Some _, _ -> raise (GQLMessageException ($"Must not combine last with first or after"))
80+
Backward (last, before)
81+
| ValueNone, _, ValueNone, _ -> raise (GQLMessageException ($"Must specify first or last"))
82+
| ValueSome _, _, _, _ -> raise (GQLMessageException ($"Must not combine first with last or before"))
83+
| _, _, ValueSome _, _ -> raise (GQLMessageException ($"Must not combine last with first or after"))
8484

8585
// The total number of edges in the data-store, not the number of edges in the page!
8686
let totalCount = async {

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

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
namespace FSharp.Data.GraphQL.Server.Middleware
22

3+
open System.Collections.Generic
4+
open System.Collections.Immutable
35
open FsToolkit.ErrorHandling
6+
47
open FSharp.Data.GraphQL
8+
open FSharp.Data.GraphQL.Ast
59
open FSharp.Data.GraphQL.Types.Patterns
610
open FSharp.Data.GraphQL.Types
711

@@ -14,8 +18,8 @@ type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool)
1418
then 0.0
1519
else
1620
match f.Definition.Metadata.TryFind<float>("queryWeight") with
17-
| Some w -> w
18-
| None -> 0.0
21+
| ValueSome w -> w
22+
| ValueNone -> 0.0
1923
// let rec getFields = function
2024
// | ResolveValue -> []
2125
// | SelectFields fields -> fields
@@ -73,7 +77,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
7377
let typesWithListFields =
7478
ctx.TypeMap.GetTypesWithListFields<'ObjectType, 'ListType>()
7579
if Seq.isEmpty typesWithListFields
76-
then failwith <| sprintf "No lists with specified type '%A' where found on object of type '%A'." typeof<'ObjectType> typeof<'ListType>
80+
then failwith $"No lists with specified type '{typeof<'ObjectType>}' where found on object of type '{typeof<'ListType>}'."
7781
let modifiedTypes =
7882
typesWithListFields
7983
|> Seq.map (fun (object, fields) -> modifyFields object fields)
@@ -82,44 +86,47 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
8286
next ctx
8387

8488
let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
85-
let rec collectArgs (acc : (string * ObjectListFilter) list) (fields : ExecutionInfo list) =
86-
let fieldArgs field =
89+
let rec collectArgs (path: obj list) (acc : KeyValuePair<obj list, ObjectListFilter> list) (fields : ExecutionInfo list) =
90+
let fieldArgs currentPath field =
8791
let filterResults =
8892
field.Ast.Arguments
8993
|> Seq.map (fun x ->
90-
match x.Name with
91-
| "filter" -> ObjectListFilter.CoerceInput (InlineConstant x.Value)
94+
match x.Name, x.Value with
95+
| "filter", (VariableName variableName) -> Ok (ctx.Variables[variableName] :?> ObjectListFilter)
96+
| "filter", inlineConstant -> ObjectListFilter.CoerceInput (InlineConstant inlineConstant)
9297
| _ -> Ok NoFilter)
9398
|> Seq.toList
9499
match filterResults |> splitSeqErrorsList with
95100
| Error errs -> Error errs
96101
| Ok filters ->
97102
filters
98103
|> removeNoFilter
99-
|> Seq.map (fun x -> field.Ast.AliasOrName, x)
104+
|> Seq.map (fun x -> KeyValuePair (currentPath |> List.rev, x))
100105
|> Seq.toList
101106
|> Ok
102107
match fields with
103108
| [] -> Ok acc
104109
| x :: xs ->
110+
let currentPath = box x.Ast.AliasOrName :: path
105111
let accResult =
106112
match x.Kind with
107113
| SelectFields fields ->
108-
collectArgs acc fields
114+
collectArgs currentPath acc fields
109115
| ResolveCollection field ->
110-
fieldArgs field
116+
fieldArgs currentPath field
111117
| ResolveAbstraction typeFields ->
112118
let fields = typeFields |> Map.toList |> List.collect (fun (_, v) -> v)
113-
collectArgs acc fields
119+
collectArgs currentPath acc fields
114120
| _ -> Ok acc
115121
match accResult with
116122
| Error errs -> Error errs
117-
| Ok acc -> collectArgs acc xs
123+
| Ok acc -> collectArgs path acc xs
118124
let ctxResult = result {
119125
match reportToMetadata with
120126
| true ->
121-
let! args = collectArgs [] ctx.ExecutionPlan.Fields
122-
return { ctx with Metadata = ctx.Metadata.Add("filters", args) }
127+
let! args = collectArgs [] [] ctx.ExecutionPlan.Fields
128+
let filters = ImmutableDictionary.CreateRange args
129+
return { ctx with Metadata = ctx.Metadata.Add("filters", filters) }
123130
| false -> return ctx
124131
}
125132
match ctxResult with

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

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
namespace FSharp.Data.GraphQL.Server.Middleware
22

3+
open System
4+
open System.Linq
5+
open System.Linq.Expressions
6+
open System.Runtime.InteropServices
7+
open Microsoft.FSharp.Quotations
8+
39
/// A filter definition for a field value.
410
type FieldFilter<'Val> =
511
{ FieldName : string
@@ -16,10 +22,10 @@ type ObjectListFilter =
1622
| StartsWith of FieldFilter<string>
1723
| EndsWith of FieldFilter<string>
1824
| Contains of FieldFilter<string>
25+
| OfTypes of FieldFilter<Type list>
1926
| FilterField of FieldFilter<ObjectListFilter>
2027
| NoFilter
2128

22-
2329
/// Contains tooling for working with ObjectListFilter.
2430
module ObjectListFilter =
2531
/// Contains operators for building and comparing ObjectListFilter values.
@@ -53,3 +59,117 @@ module ObjectListFilter =
5359

5460
/// Creates a new ObjectListFilter representing a NOT opreation for the existing one.
5561
let ( !!! ) filter = Not filter
62+
63+
//[<AutoOpen>]
64+
//module ObjectListFilterExtensions =
65+
66+
// type ObjectListFilter with
67+
68+
// member filter.Apply<'T, 'D>(query : IQueryable<'T>,
69+
// compareDiscriminator : Expr<'T -> 'D -> 'D> | null,
70+
// getDiscriminatorValue : (Type -> 'D) | null) =
71+
// filter.Apply(query, compareDiscriminator, getDiscriminatorValue)
72+
73+
// member filter.Apply<'T, 'D>(query : IQueryable<'T>,
74+
// [<Optional>] getDiscriminator : Expr<'T -> 'D> | null,
75+
// [<Optional>] getDiscriminatorValue : (Type -> 'D) | null) =
76+
// // Helper to create parameter expression for the lambda
77+
// let param = Expression.Parameter(typeof<'T>, "x")
78+
79+
// // Helper to get property value
80+
// let getPropertyExpr fieldName =
81+
// Expression.PropertyOrField(param, fieldName)
82+
83+
// // Helper to create lambda from body expression
84+
// let makeLambda (body: Expression) =
85+
// let delegateType = typedefof<Func<_,_>>.MakeGenericType([|typeof<'T>; body.Type|])
86+
// Expression.Lambda(delegateType, body, param)
87+
88+
// // Helper to create Where expression
89+
// let whereExpr predicate =
90+
// let whereMethod =
91+
// typeof<Queryable>.GetMethods()
92+
// |> Seq.where (fun m -> m.Name = "Where")
93+
// |> Seq.find (fun m ->
94+
// let parameters = m.GetParameters()
95+
// parameters.Length = 2
96+
// && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof<Expression<Func<_,_>>>)
97+
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
98+
// Expression.Call(whereMethod, [|query.Expression; makeLambda predicate|])
99+
100+
// // Helper for discriminator comparison
101+
// let buildTypeDiscriminatorCheck (t: Type) =
102+
// match getDiscriminator, getDiscriminatorValue with
103+
// | null, _ | _, null -> None
104+
// | discExpr, discValueFn ->
105+
// let compiled = QuotationEvaluator.Eval(discExpr)
106+
// let discriminatorValue = discValueFn t
107+
// let discExpr = getPropertyExpr "__discriminator" // Assuming discriminator field name
108+
// let valueExpr = Expression.Constant(discriminatorValue)
109+
// Some(Expression.Equal(discExpr, valueExpr))
110+
111+
// // Main filter logic
112+
// let rec buildFilterExpr filter =
113+
// match filter with
114+
// | NoFilter -> query.Expression
115+
// | And (f1, f2) ->
116+
// let q1 = buildFilterExpr f1 |> Expression.Lambda<Func<IQueryable<'T>>>|> _.Compile().Invoke()
117+
// buildFilterExpr f2 |> Expression.Lambda<Func<IQueryable<'T>>> |> _.Compile().Invoke(q1).Expression
118+
// | Or (f1, f2) ->
119+
// let expr1 = buildFilterExpr f1
120+
// let expr2 = buildFilterExpr f2
121+
// let unionMethod =
122+
// typeof<Queryable>.GetMethods()
123+
// |> Array.find (fun m -> m.Name = "Union")
124+
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
125+
// Expression.Call(unionMethod, [|expr1; expr2|])
126+
// | Not f ->
127+
// let exceptMethod =
128+
// typeof<Queryable>.GetMethods()
129+
// |> Array.find (fun m -> m.Name = "Except")
130+
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
131+
// Expression.Call(exceptMethod, [|query.Expression; buildFilterExpr f|])
132+
// | Equals f ->
133+
// Expression.Equal(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
134+
// | GreaterThan f ->
135+
// Expression.GreaterThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
136+
// | LessThan f ->
137+
// Expression.LessThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
138+
// | StartsWith f ->
139+
// let methodInfo = typeof<string>.GetMethod("StartsWith", [|typeof<string>|])
140+
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
141+
// | EndsWith f ->
142+
// let methodInfo = typeof<string>.GetMethod("EndsWith", [|typeof<string>|])
143+
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
144+
// | Contains f ->
145+
// let methodInfo = typeof<string>.GetMethod("Contains", [|typeof<string>|])
146+
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
147+
// | OfTypes types ->
148+
// match types.Value with
149+
// | [] -> query.Expression // No types specified, return original query
150+
// | types ->
151+
// let typeChecks =
152+
// types
153+
// |> List.choose buildTypeDiscriminatorCheck
154+
// |> List.fold (fun acc expr ->
155+
// match acc with
156+
// | None -> Some expr
157+
// | Some prevExpr -> Some(Expression.OrElse(prevExpr, expr))) None
158+
159+
// match typeChecks with
160+
// | None -> query.Expression
161+
// | Some expr -> whereExpr expr
162+
// | FilterField f ->
163+
// let propExpr = getPropertyExpr f.FieldName
164+
// match propExpr.Type.GetInterfaces()
165+
// |> Array.tryFind (fun t ->
166+
// t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<IQueryable<_>>) with
167+
// | Some queryableType ->
168+
// let elementType = queryableType.GetGenericArguments().[0]
169+
// let subFilter = f.Value
170+
// let subQuery = Expression.Convert(propExpr, queryableType)
171+
// Expression.Call(typeof<Queryable>, "Any", [|elementType|], subQuery) |> whereExpr
172+
// | None -> query.Expression
173+
174+
// // Create and execute the final expression
175+
// query.Provider.CreateQuery<'T>(buildFilterExpr filter)

0 commit comments

Comments
 (0)