diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index e46edb7e0..5be35c26c 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,15 +1,9 @@ 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 - Value : 'Val } +type FieldFilter<'Val> = { FieldName : string; Value : 'Val } /// A filter definition for an object list. type ObjectListFilter = @@ -22,10 +16,68 @@ type ObjectListFilter = | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter - | OfTypes of FieldFilter + | OfTypes of Type list | FilterField of FieldFilter | NoFilter +open System.Linq +open System.Linq.Expressions +open System.Runtime.InteropServices +open System.Reflection + +/// +/// Allows to specify discriminator comparison or discriminator getter +/// and a function that return discriminator value depending on entity type +/// +/// +/// // discriminator custom condition +/// let result () = +/// queryable.Apply( +/// filter, +/// ObjectListFilterLinqOptions ( +/// (fun entity discriminator -> entity.Discriminator.StartsWith discriminator), +/// (function +/// | t when Type.(=)(t, typeof) -> "cat+v1" +/// | t when Type.(=)(t, typeof) -> "dog+v1") +/// ) +/// ) +/// +/// +/// // discriminator equals +/// let result () = +/// queryable.Apply( +/// filter, +/// ObjectListFilterLinqOptions ( +/// (fun entity -> entity.Discriminator), +/// (function +/// | t when Type.(=)(t, typeof) -> "cat" +/// | t when Type.(=)(t, typeof) -> "dog") +/// ) +/// ) +/// +[] +type ObjectListFilterLinqOptions<'T, 'D> + ([] compareDiscriminator : Expression> | null, [] getDiscriminatorValue : (Type -> 'D) | null) = + + member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj + member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj + + static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null) + + static member GetCompareDiscriminator (getDiscriminatorValue : Expression>) = + let tParam = Expression.Parameter (typeof<'T>, "x") + let dParam = Expression.Parameter (typeof<'D>, "d") + let body = Expression.Equal (Expression.Invoke (getDiscriminatorValue, tParam), dParam) + Expression.Lambda> (body, tParam, dParam) + + new (getDiscriminator : Expression>) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null) + new (compareDiscriminator : Expression>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null) + new (getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null, getDiscriminatorValue = getDiscriminatorValue) + new (getDiscriminator : Expression>, getDiscriminatorValue : Type -> 'D) = + ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue) + /// Contains tooling for working with ObjectListFilter. module ObjectListFilter = /// Contains operators for building and comparing ObjectListFilter values. @@ -60,116 +112,162 @@ module ObjectListFilter = /// Creates a new ObjectListFilter representing a NOT opreation for the existing one. let ( !!! ) filter = Not filter -//[] -//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>, -// [] getDiscriminator : Expr<'T -> 'D> | null, -// [] 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>.MakeGenericType([|typeof<'T>; body.Type|]) -// Expression.Lambda(delegateType, body, param) - -// // Helper to create Where expression -// let whereExpr predicate = -// let whereMethod = -// typeof.GetMethods() -// |> Seq.where (fun m -> m.Name = "Where") -// |> Seq.find (fun m -> -// let parameters = m.GetParameters() -// parameters.Length = 2 -// && parameters[1].ParameterType.GetGenericTypeDefinition() = typedefof>>) -// |> 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>>|> _.Compile().Invoke() -// buildFilterExpr f2 |> Expression.Lambda>> |> _.Compile().Invoke(q1).Expression -// | Or (f1, f2) -> -// let expr1 = buildFilterExpr f1 -// let expr2 = buildFilterExpr f2 -// let unionMethod = -// typeof.GetMethods() -// |> Array.find (fun m -> m.Name = "Union") -// |> fun m -> m.MakeGenericMethod([|typeof<'T>|]) -// Expression.Call(unionMethod, [|expr1; expr2|]) -// | Not f -> -// let exceptMethod = -// typeof.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.GetMethod("StartsWith", [|typeof|]) -// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr -// | EndsWith f -> -// let methodInfo = typeof.GetMethod("EndsWith", [|typeof|]) -// Expression.Call(getPropertyExpr f.FieldName, methodInfo, Expression.Constant(f.Value)) |> whereExpr -// | Contains f -> -// let methodInfo = typeof.GetMethod("Contains", [|typeof|]) -// 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>) with -// | Some queryableType -> -// let elementType = queryableType.GetGenericArguments().[0] -// let subFilter = f.Value -// let subQuery = Expression.Convert(propExpr, queryableType) -// Expression.Call(typeof, "Any", [|elementType|], subQuery) |> whereExpr -// | None -> query.Expression - -// // Create and execute the final expression -// query.Provider.CreateQuery<'T>(buildFilterExpr filter) + let private genericWhereMethod = + typeof.GetMethods () + |> Seq.where (fun m -> m.Name = "Where") + |> Seq.find (fun m -> + let parameters = m.GetParameters () + parameters.Length = 2 + && parameters[1].ParameterType.GetGenericTypeDefinition () = typedefof>>) + + // Helper to create Where expression + let whereExpr<'T> (query : IQueryable<'T>) (param : ParameterExpression) predicate = + let whereMethod = genericWhereMethod.MakeGenericMethod ([| typeof<'T> |]) + Expression.Call (whereMethod, [| query.Expression; Expression.Lambda> (predicate, param) |]) + + let private StringStartsWithMethod = typeof.GetMethod ("StartsWith", [| typeof |]) + let private StringEndsWithMethod = typeof.GetMethod ("EndsWith", [| typeof |]) + let private StringContainsMethod = typeof.GetMethod ("Contains", [| typeof |]) + let private getEnumerableContainsMethod (memberType : Type) = + match + typeof + .GetMethods(BindingFlags.Static ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 2) + with + | null -> raise (MissingMemberException "Static 'Contains' method with 2 parameters not found on 'Enumerable' class") + | containsGenericStaticMethod -> + if + memberType.IsGenericType + && memberType.GenericTypeArguments.Length = 1 + then + containsGenericStaticMethod.MakeGenericMethod (memberType.GenericTypeArguments) + else + let ienumerable = + memberType + .GetInterfaces() + .First (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + containsGenericStaticMethod.MakeGenericMethod ([| ienumerable.GenericTypeArguments[0] |]) + + let getField (param : ParameterExpression) fieldName = Expression.PropertyOrField (param, fieldName) + + [] + type SourceExpression private (expression : Expression) = + new (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + new (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + member _.Value = expression + static member op_Implicit (source : SourceExpression) = source.Value + static member op_Implicit (parameter : ParameterExpression) = SourceExpression (parameter :> Expression) + static member op_Implicit (``member`` : MemberExpression) = SourceExpression (``member`` :> Expression) + + let rec buildFilterExpr (param : SourceExpression) buildTypeDiscriminatorCheck filter : Expression = + let build = buildFilterExpr param buildTypeDiscriminatorCheck + match filter with + | NoFilter -> Expression.Constant (true) + | 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 -> Expression.Equal (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | GreaterThan f -> Expression.GreaterThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | LessThan f -> Expression.LessThan (Expression.PropertyOrField (param, f.FieldName), Expression.Constant (f.Value)) + | StartsWith f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringStartsWithMethod, Expression.Constant (f.Value)) + | EndsWith f -> Expression.Call (Expression.PropertyOrField (param, f.FieldName), StringEndsWithMethod, Expression.Constant (f.Value)) + | Contains f -> + let ``member`` = Expression.PropertyOrField (param, f.FieldName) + let isEnumerable (memberType : Type) = + not (Type.(=) (memberType, typeof)) + && typeof.IsAssignableFrom (memberType) + && memberType + .GetInterfaces() + .Any (fun i -> i.FullName.StartsWith "System.Collections.Generic.IEnumerable`1") + match ``member``.Member with + | :? PropertyInfo as prop when prop.PropertyType |> isEnumerable -> + match + prop.PropertyType + .GetMethods(BindingFlags.Instance ||| BindingFlags.Public) + .FirstOrDefault (fun m -> m.Name = "Contains" && m.GetParameters().Length = 1) + with + | null -> + Expression.Call ( + getEnumerableContainsMethod prop.PropertyType, + Expression.PropertyOrField (param, f.FieldName), + Expression.Constant (f.Value) + ) + | instanceContainsMethod -> + Expression.Call (Expression.PropertyOrField (param, f.FieldName), instanceContainsMethod, Expression.Constant (f.Value)) + | :? FieldInfo as field when field.FieldType |> isEnumerable -> + Expression.Call ( + getEnumerableContainsMethod field.FieldType, + Expression.PropertyOrField (param, f.FieldName), + Expression.Constant (f.Value) + ) + | _ -> Expression.Call (``member``, StringContainsMethod, Expression.Constant (f.Value)) + | OfTypes types -> + types + |> Seq.map (fun t -> buildTypeDiscriminatorCheck param t) + |> Seq.reduce (fun acc expr -> Expression.Or (acc, expr)) + | FilterField f -> + let paramExpr = Expression.PropertyOrField (param, f.FieldName) + buildFilterExpr (SourceExpression paramExpr) buildTypeDiscriminatorCheck f.Value + + let apply (options : ObjectListFilterLinqOptions<'T, 'D>) (filter : ObjectListFilter) (query : IQueryable<'T>) = + match filter with + | NoFilter -> query + | _ -> + // Helper for discriminator comparison + let buildTypeDiscriminatorCheck (param : SourceExpression) (t : Type) = + match options.CompareDiscriminator, options.GetDiscriminatorValue with + | ValueNone, ValueNone -> + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Default discriminator value + Expression.Constant (t.FullName) + ) + :> Expression + | ValueSome discExpr, ValueNone -> + Expression.Invoke ( + // Provided discriminator comparison + discExpr, + param, + // Default discriminator value gathered from type + Expression.Constant (t.FullName) + ) + :> Expression + | ValueNone, ValueSome discValueFn -> + let discriminatorValue = discValueFn t + Expression.Equal ( + // Default discriminator property + Expression.PropertyOrField (param, "__typename"), + // Provided discriminator value gathered from type + Expression.Constant (discriminatorValue) + ) + :> Expression + | ValueSome discExpr, ValueSome discValueFn -> + let discriminatorValue = discValueFn t + Expression.Invoke ( + // Provided discriminator comparison + discExpr, + param, + // Provided discriminator value gathered from type + Expression.Constant (discriminatorValue) + ) + let queryExpr = + let param = Expression.Parameter (typeof<'T>, "x") + let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter + whereExpr<'T> query param body + // Create and execute the final expression + query.Provider.CreateQuery<'T> (queryExpr) + +[] +module ObjectListFilterExtensions = + + open ObjectListFilter + + type ObjectListFilter with + + member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [] options : ObjectListFilterLinqOptions<'T, 'D>) = + apply options filter query + + type IQueryable<'T> with + + member inline query.Apply (filter : ObjectListFilter, [] options : ObjectListFilterLinqOptions<'T, 'D>) = apply options filter query diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 27fae4438..fdf3554da 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -23,6 +23,20 @@ module TypeSystemExtensions = open ObjectListFilter.Operators + type ExecutionInfo with + + member this.ResolveAbstractionFilter (typeMap : TypeMap) = + match this.Kind with + | ResolveAbstraction typeFields -> + let getType name = + match typeMap.TryFind name with + | ValueSome tdef -> tdef.Type + | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) + match typeFields.Keys |> Seq.map getType |> Seq.toList with + | [] -> ValueNone + | filters -> ValueSome (OfTypes filters) + | _ -> ValueNone + type ResolveFieldContext with /// @@ -32,16 +46,9 @@ module TypeSystemExtensions = member this.Filter = match this.Args.TryGetValue "filter" with | true, (:? ObjectListFilter as f) -> - match this.ExecutionInfo.Kind with - | ResolveAbstraction typeFields -> - let getType name = - match this.Context.Schema.TypeMap.TryFind name with - | ValueSome tdef -> tdef.Type - | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) - match typeFields.Keys |> Seq.map getType |> Seq.toList with - | [] -> ValueNone - | filters -> ValueSome (f &&& (OfTypes { FieldName = "__typename"; Value = filters })) - | _ -> ValueSome f + match this.ExecutionInfo.ResolveAbstractionFilter (this.Context.Schema.TypeMap) with + | ValueSome ofTypes -> ValueSome (ofTypes &&& f) + | ValueNone -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") diff --git a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj index 8d270a060..39291f9de 100644 --- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj +++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj @@ -17,6 +17,9 @@ <_Parameter1>FSharp.Data.GraphQL.Server + + <_Parameter1>FSharp.Data.GraphQL.Server.Middleware + <_Parameter1>FSharp.Data.GraphQL.Client diff --git a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj index 10ef9d6ca..f4352578b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -63,7 +63,8 @@ - + + diff --git a/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs new file mode 100644 index 000000000..8613dcb52 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs @@ -0,0 +1,396 @@ +module FSharp.Data.GraphQL.Tests.ObjectListFilterLinqTests + +open Xunit +open System +open System.Linq +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Server.Middleware +open FSharp.Data.GraphQL.Tests.LinqTests + +[] +let ``ObjectListFilter works with Equals operator``() = + let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable + 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" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with GreaterThan operator``() = + let filter = GreaterThan { FieldName = "id"; Value = 4 } // :> IComparable + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals "Trif" + result.Contact |> equals { Email = "j.trif@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] + +[] +let ``ObjectListFilter works with LessThan operator``() = + let filter = LessThan { FieldName = "id"; Value = 4 } // :> IComparable + 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" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with StartsWith operator``() = + let filter = StartsWith { FieldName = "firstName"; Value = "J" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 2 + result.FirstName |> equals "Jonathan" + result.LastName |> equals "Abrams" + result.Contact |> equals { Email = "j.abrams@gmail.com" } + result.Friends |> equals [] + +[] +let ``ObjectListFilter works with Contains operator``() = + let filter = Contains { FieldName = "firstName"; Value = "en" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with EndsWith operator``() = + let filter = EndsWith { FieldName = "lastName"; Value = "ams" } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with AND operator``() = + let filter = + And ( + Contains { FieldName = "firstName"; Value = "en" }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 4 + result.FirstName |> equals "Ben" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with OR operator``() = + let filter = + Or ( + GreaterThan { FieldName = "id"; Value = 4 }, + Equals { FieldName = "lastName"; Value = "Adams" } + ) + 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" + result.LastName |> equals "Adams" + result.Contact |> equals { Email = "b.adams@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" }; { Email = "l.trif@gmail.com" } ] + +[] +let ``ObjectListFilter works with FilterField operator``() = + let filter = + FilterField { FieldName = "Contact"; Value = Contains { FieldName = "Email"; Value = "j.trif@gmail.com" } } + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 1 + let result = List.head filteredData + result.ID |> equals 7 + result.FirstName |> equals "Jeneffer" + result.LastName |> equals "Trif" + result.Contact |> equals { Email = "j.trif@gmail.com" } + result.Friends |> equals [ { Email = "j.abrams@gmail.com" } ] + +[] +let ``ObjectListFilter works with NOT operator``() = + let filter = + Not (Equals { FieldName = "lastName"; Value = "Adams" }) + let queryable = data.AsQueryable() + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.ID |> equals 2 + result1.FirstName |> equals "Jonathan" + result1.LastName |> equals "Abrams" + result1.Contact |> equals { Email = "j.abrams@gmail.com" } + result1.Friends |> equals [] + +type Complex = + { ID : int + Name : string + Discriminator : string } + +type Building = + { ID : int + Name : string + Discriminator : string } + +type Community = + { ID : int + Name : string + Discriminator : string + Complexes : int list + Buildings : int list } + +type Property = + | Complex of Complex + | Building of Building + | Community of Community + + +[] +let ``ObjectListFilter works with getDiscriminator for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.FullName } + Building { ID = 2; Name = "Building B"; Discriminator = typeof.FullName } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.FullName; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.FullName } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.FullName } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.FullName; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator)) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + + +[] +let ``ObjectListFilter works with getDiscriminator and getDiscriminatorValue for Complex``() = + let propertyData: Property list = + [ + Complex { ID = 1; Name = "Complex A"; Discriminator = typeof.Name} + Building { ID = 2; Name = "Building B"; Discriminator = typeof.Name } + Community { ID = 3; Name = "Community C"; Discriminator = typeof.Name; Complexes = [1]; Buildings = [2] } + Complex { ID = 4; Name = "Complex AA"; Discriminator = typeof.Name } + Building { ID = 5; Name = "Building BB"; Discriminator = typeof.Name } + Community { ID = 6; Name = "Community CC"; Discriminator = typeof.Name; Complexes = [4]; Buildings = [5] } + ] + let queryable = propertyData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + (function + | Complex c -> c.Discriminator + | Building b -> b.Discriminator + | Community c -> c.Discriminator), + (function + | t when t = typeof -> "Complex" + | t when t = typeof -> "Building" + | t when t = typeof -> "Community" + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | Complex c -> + c.ID |> equals 1 + c.Name |> equals "Complex A" + | _ -> failwith "Expected Complex" + let result2 = List.last filteredData + match result2 with + | Complex c -> + c.ID |> equals 4 + c.Name |> equals "Complex AA" + | _ -> failwith "Expected Complex" + +type Cow = + { ID : int + Name : string + Discriminator : string + __typename : string } + +type Horse = + { ID : int + Name : string + Discriminator : string + __typename : string } + +type Hamster = + { ID : int + Name : string + Discriminator : string + __typename : string } + +let animalData = + [ + { ID = 1; Discriminator="Cow"; Name = "Cow A"; __typename = typeof.Name } + { ID = 2; Discriminator="Horse"; Name = "Horse B"; __typename = typeof.Name } + { ID = 3; Discriminator="Cow"; Name = "Cow C"; __typename = typeof.Name } + { ID = 4; Discriminator="Horse"; Name = "Horse D"; __typename = typeof.Name } + { ID = 5; Discriminator="Hamster"; Name = "Hamster E"; __typename = typeof.Name } + ] + +[] +let ``ObjectListFilter works with getDiscriminatorValue for Horse``() = + let queryable = animalData.AsQueryable() + let filter = OfTypes [typeof] + let options = + ObjectListFilterLinqOptions( + getDiscriminatorValue = (function + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + match result1 with + | h -> + h.ID |> equals 2 + h.Name |> equals "Horse B" + let result2 = List.last filteredData + match result2 with + | h -> + h.ID |> equals 4 + h.Name |> equals "Horse D" + +[] +let ``ObjectListFilter works with getDiscriminatorValue startsWith for Horse and Hamster``() = + + let queryable = animalData.AsQueryable() + let filter = StartsWith { FieldName = "Discriminator"; Value = "H" } + let options = + ObjectListFilterLinqOptions ( + (fun entity (discriminator: string) -> + entity.Discriminator.StartsWith discriminator), + getDiscriminatorValue = (function + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | t when t = typeof -> t.Name + | _ -> raise (NotSupportedException "Type not supported")) + ) + let filteredData = queryable.Apply(filter, options) |> Seq.toList + List.length filteredData |> equals 3 + let result1 = List.head filteredData + match result1 with + | c -> + c.ID |> equals 2 + c.Name |> equals "Horse B" + let result2 = List.last filteredData + match result2 with + | c -> + c.ID |> equals 5 + c.Name |> equals "Hamster E" + +type ListTagsProduct = { + Name : string + Tags : string list +} + +[] +let ``ObjectListFilter works with Contains operator on list collection properties``() = + let productList = + [ + { Name = "Product A"; Tags = ["Tag1"; "Tag2"] } + { Name = "Product B"; Tags = ["Tag2"; "Tag3"] } + { Name = "Product C"; Tags = ["Tag3"; "Tag4"] } + { Name = "Product D"; Tags = ["Tag4"; "Tag5"] } + ] + let queryable = productList.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" + +type ArrayTagsProduct = { + Name : string + Tags : string array +} + +[] +let ``ObjectListFilter works with Contains operator on array collection properties``() = + + let productArray = + [ + { Name = "Product A"; Tags = [|"Tag1"; "Tag2"|] } + { Name = "Product B"; Tags = [|"Tag2"; "Tag3"|] } + { Name = "Product C"; Tags = [|"Tag3"; "Tag4"|] } + { Name = "Product D"; Tags = [|"Tag4"; "Tag5"|] } + ] + let queryable = productArray.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" + +type SetTagsProduct = { + Name : string + Tags : string Set +} + +[] +let ``ObjectListFilter works with Contains operator on set collection properties``() = + + let productArray = + [ + { Name = "Product A"; Tags = [|"Tag1"; "Tag2"|] |> Set.ofArray} + { Name = "Product B"; Tags = [|"Tag2"; "Tag3"|] |> Set.ofArray} + { Name = "Product C"; Tags = [|"Tag3"; "Tag4"|] |> Set.ofArray} + { Name = "Product D"; Tags = [|"Tag4"; "Tag5"|] |> Set.ofArray} + ] + let queryable = productArray.AsQueryable() + let filter = Contains { FieldName = "Tags"; Value = "Tag3" } + let filteredData = queryable.Apply(filter) |> Seq.toList + List.length filteredData |> equals 2 + let result1 = List.head filteredData + result1.Name |> equals "Product B" + let result2 = List.last filteredData + result2.Name |> equals "Product C" diff --git a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs similarity index 94% rename from tests/FSharp.Data.GraphQL.Tests/LinqTests.fs rename to tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs index 0b1ee1dee..02a8b9ad8 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LinqTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/SelectLinqTests.fs @@ -6,9 +6,8 @@ open Xunit open System open System.Linq open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Linq -open FSharp.Data.GraphQL.Execution +open FSharp.Data.GraphQL.Types type Contact = { Email : string } @@ -51,6 +50,7 @@ let data = Contact = { Email = "j.trif@gmail.com" } Friends = [ { Email = "j.abrams@gmail.com" } ] } ] + let internal undefined<'t> = Unchecked.defaultof<'t> let resolveRoot ctx () = @@ -71,13 +71,24 @@ let linqArgs = Define.Input("after", Nullable StringType) ] let schema = - Schema(Define.Object("RootQuery", - [ Define.Field("people", ListOf Person, "", linqArgs, - fun ctx () -> - let info = ctx.ExecutionInfo - let queryable = data.AsQueryable() - let result = queryable.Apply(info) |> Seq.toList - result) ])) + Schema ( + Define.Object ( + "RootQuery", + [ + Define.Field ( + "people", + ListOf Person, + "", + linqArgs, + fun ctx () -> + let info = ctx.ExecutionInfo + let queryable = data.AsQueryable () + let result = queryable.Apply (info) |> Seq.toList + result + ) + ] + ) + ) let schemaProcessor = Executor(schema)