Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,40 @@ query TestQuery {
}
```

Also you can apply `not` operator like this:

```graphql
query TestQuery {
hero(id:"1000") {
id
name
appearsIn
homePlanet
friends (filter : { not : { name_starts_with: "A" } }) {
id
name
}
}
}
```

And combine filters with `and` and `or` operators like this:

```graphql
query TestQuery {
hero(id:"1000") {
id
name
appearsIn
homePlanet
friends (filter : { or : [{ name_starts_with: "A"}, { name_starts_with: "B" }]}) {
id
name
}
}
}
```

This filter is mapped by the middleware inside an `ObjectListFilter` definition:

```fsharp
Expand All @@ -381,7 +415,7 @@ type ObjectListFilter =
| FilterField of FieldFilter<ObjectListFilter>
```

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).
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).

```fsharp
Define.Field("friends", ListOf (Nullable CharacterType),
Expand Down
2 changes: 1 addition & 1 deletion samples/relay-book-store/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module BookCursor =
Title = get.Required.Field "t" Decode.string
})

let tryDecode (x : string) : BookCursor option = option {
let tryDecode (x : string) : BookCursor voption = voption {
let! bytes = Base64.tryDecode x
let! json = Utf8.tryDecode bytes

Expand Down
5 changes: 0 additions & 5 deletions samples/relay-book-store/Prelude.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ namespace FSharp.Data.GraphQL.Samples.RelayBookStore
[<AutoOpen>]
module internal Prelude =

let vopt =
function
| Some x -> ValueSome x
| None -> ValueNone

[<RequireQualifiedAccess>]
module Base64 =

Expand Down
26 changes: 13 additions & 13 deletions samples/relay-book-store/Schema.fs
Original file line number Diff line number Diff line change
Expand Up @@ -53,34 +53,34 @@ let booksField =

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

let last = ctx.TryArg ("last")

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

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

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

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

// The total number of edges in the data-store, not the number of edges in the page!
let totalCount = async {
Expand Down
35 changes: 21 additions & 14 deletions src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
namespace FSharp.Data.GraphQL.Server.Middleware

open System.Collections.Generic
open System.Collections.Immutable
open FsToolkit.ErrorHandling

open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Ast
open FSharp.Data.GraphQL.Types.Patterns
open FSharp.Data.GraphQL.Types

Expand All @@ -14,8 +18,8 @@ type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool)
then 0.0
else
match f.Definition.Metadata.TryFind<float>("queryWeight") with
| Some w -> w
| None -> 0.0
| ValueSome w -> w
| ValueNone -> 0.0
// let rec getFields = function
// | ResolveValue -> []
// | SelectFields fields -> fields
Expand Down Expand Up @@ -73,7 +77,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
let typesWithListFields =
ctx.TypeMap.GetTypesWithListFields<'ObjectType, 'ListType>()
if Seq.isEmpty typesWithListFields
then failwith <| sprintf "No lists with specified type '%A' where found on object of type '%A'." typeof<'ObjectType> typeof<'ListType>
then failwith $"No lists with specified type '{typeof<'ObjectType>}' where found on object of type '{typeof<'ListType>}'."
let modifiedTypes =
typesWithListFields
|> Seq.map (fun (object, fields) -> modifyFields object fields)
Expand All @@ -82,44 +86,47 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
next ctx

let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let rec collectArgs (acc : (string * ObjectListFilter) list) (fields : ExecutionInfo list) =
let fieldArgs field =
let rec collectArgs (path: obj list) (acc : KeyValuePair<obj list, ObjectListFilter> list) (fields : ExecutionInfo list) =
let fieldArgs currentPath field =
let filterResults =
field.Ast.Arguments
|> Seq.map (fun x ->
match x.Name with
| "filter" -> ObjectListFilter.CoerceInput (InlineConstant x.Value)
match x.Name, x.Value with
| "filter", (VariableName variableName) -> Ok (ctx.Variables[variableName] :?> ObjectListFilter)
| "filter", inlineConstant -> ObjectListFilter.CoerceInput (InlineConstant inlineConstant)
| _ -> Ok NoFilter)
|> Seq.toList
match filterResults |> splitSeqErrorsList with
| Error errs -> Error errs
| Ok filters ->
filters
|> removeNoFilter
|> Seq.map (fun x -> field.Ast.AliasOrName, x)
|> Seq.map (fun x -> KeyValuePair (currentPath |> List.rev, x))
|> Seq.toList
|> Ok
match fields with
| [] -> Ok acc
| x :: xs ->
let currentPath = box x.Ast.AliasOrName :: path
let accResult =
match x.Kind with
| SelectFields fields ->
collectArgs acc fields
collectArgs currentPath acc fields
| ResolveCollection field ->
fieldArgs field
fieldArgs currentPath field
| ResolveAbstraction typeFields ->
let fields = typeFields |> Map.toList |> List.collect (fun (_, v) -> v)
collectArgs acc fields
collectArgs currentPath acc fields
| _ -> Ok acc
match accResult with
| Error errs -> Error errs
| Ok acc -> collectArgs acc xs
| Ok acc -> collectArgs path acc xs
let ctxResult = result {
match reportToMetadata with
| true ->
let! args = collectArgs [] ctx.ExecutionPlan.Fields
return { ctx with Metadata = ctx.Metadata.Add("filters", args) }
let! args = collectArgs [] [] ctx.ExecutionPlan.Fields
let filters = ImmutableDictionary.CreateRange args
return { ctx with Metadata = ctx.Metadata.Add("filters", filters) }
| false -> return ctx
}
match ctxResult with
Expand Down
122 changes: 121 additions & 1 deletion src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
namespace FSharp.Data.GraphQL.Server.Middleware

open System
open System.Linq
open System.Linq.Expressions
open System.Runtime.InteropServices
open Microsoft.FSharp.Quotations

/// A filter definition for a field value.
type FieldFilter<'Val> =
{ FieldName : string
Expand All @@ -16,10 +22,10 @@ type ObjectListFilter =
| StartsWith of FieldFilter<string>
| EndsWith of FieldFilter<string>
| Contains of FieldFilter<string>
| OfTypes of FieldFilter<Type list>
| FilterField of FieldFilter<ObjectListFilter>
| NoFilter


/// Contains tooling for working with ObjectListFilter.
module ObjectListFilter =
/// Contains operators for building and comparing ObjectListFilter values.
Expand Down Expand Up @@ -53,3 +59,117 @@ module ObjectListFilter =

/// Creates a new ObjectListFilter representing a NOT opreation for the existing one.
let ( !!! ) filter = Not filter

//[<AutoOpen>]
//module ObjectListFilterExtensions =

// type ObjectListFilter with

// member filter.Apply<'T, 'D>(query : IQueryable<'T>,
// compareDiscriminator : Expr<'T -> 'D -> 'D> | null,
// getDiscriminatorValue : (Type -> 'D) | null) =
// filter.Apply(query, compareDiscriminator, getDiscriminatorValue)

// member filter.Apply<'T, 'D>(query : IQueryable<'T>,
// [<Optional>] getDiscriminator : Expr<'T -> 'D> | null,
// [<Optional>] getDiscriminatorValue : (Type -> 'D) | null) =
// // Helper to create parameter expression for the lambda
// let param = Expression.Parameter(typeof<'T>, "x")

// // Helper to get property value
// let getPropertyExpr fieldName =
// Expression.PropertyOrField(param, fieldName)

// // Helper to create lambda from body expression
// let makeLambda (body: Expression) =
// let delegateType = typedefof<Func<_,_>>.MakeGenericType([|typeof<'T>; body.Type|])
// Expression.Lambda(delegateType, body, param)

// // Helper to create Where expression
// let whereExpr predicate =
// let whereMethod =
// typeof<Queryable>.GetMethods()
// |> Seq.where (fun m -> m.Name = "Where")
// |> Seq.find (fun m ->
// let parameters = m.GetParameters()
// parameters.Length = 2
// && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof<Expression<Func<_,_>>>)
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
// Expression.Call(whereMethod, [|query.Expression; makeLambda predicate|])

// // Helper for discriminator comparison
// let buildTypeDiscriminatorCheck (t: Type) =
// match getDiscriminator, getDiscriminatorValue with
// | null, _ | _, null -> None
// | discExpr, discValueFn ->
// let compiled = QuotationEvaluator.Eval(discExpr)
// let discriminatorValue = discValueFn t
// let discExpr = getPropertyExpr "__discriminator" // Assuming discriminator field name
// let valueExpr = Expression.Constant(discriminatorValue)
// Some(Expression.Equal(discExpr, valueExpr))

// // Main filter logic
// let rec buildFilterExpr filter =
// match filter with
// | NoFilter -> query.Expression
// | And (f1, f2) ->
// let q1 = buildFilterExpr f1 |> Expression.Lambda<Func<IQueryable<'T>>>|> _.Compile().Invoke()
// buildFilterExpr f2 |> Expression.Lambda<Func<IQueryable<'T>>> |> _.Compile().Invoke(q1).Expression
// | Or (f1, f2) ->
// let expr1 = buildFilterExpr f1
// let expr2 = buildFilterExpr f2
// let unionMethod =
// typeof<Queryable>.GetMethods()
// |> Array.find (fun m -> m.Name = "Union")
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
// Expression.Call(unionMethod, [|expr1; expr2|])
// | Not f ->
// let exceptMethod =
// typeof<Queryable>.GetMethods()
// |> Array.find (fun m -> m.Name = "Except")
// |> fun m -> m.MakeGenericMethod([|typeof<'T>|])
// Expression.Call(exceptMethod, [|query.Expression; buildFilterExpr f|])
// | Equals f ->
// Expression.Equal(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
// | GreaterThan f ->
// Expression.GreaterThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
// | LessThan f ->
// Expression.LessThan(getPropertyExpr f.FieldName, Expression.Constant(f.Value)) |> whereExpr
// | StartsWith f ->
// let methodInfo = typeof<string>.GetMethod("StartsWith", [|typeof<string>|])
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
// | EndsWith f ->
// let methodInfo = typeof<string>.GetMethod("EndsWith", [|typeof<string>|])
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
// | Contains f ->
// let methodInfo = typeof<string>.GetMethod("Contains", [|typeof<string>|])
// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr
// | OfTypes types ->
// match types.Value with
// | [] -> query.Expression // No types specified, return original query
// | types ->
// let typeChecks =
// types
// |> List.choose buildTypeDiscriminatorCheck
// |> List.fold (fun acc expr ->
// match acc with
// | None -> Some expr
// | Some prevExpr -> Some(Expression.OrElse(prevExpr, expr))) None

// match typeChecks with
// | None -> query.Expression
// | Some expr -> whereExpr expr
// | FilterField f ->
// let propExpr = getPropertyExpr f.FieldName
// match propExpr.Type.GetInterfaces()
// |> Array.tryFind (fun t ->
// t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<IQueryable<_>>) with
// | Some queryableType ->
// let elementType = queryableType.GetGenericArguments().[0]
// let subFilter = f.Value
// let subQuery = Expression.Convert(propExpr, queryableType)
// Expression.Call(typeof<Queryable>, "Any", [|elementType|], subQuery) |> whereExpr
// | None -> query.Expression

// // Create and execute the final expression
// query.Provider.CreateQuery<'T>(buildFilterExpr filter)
Loading