From 2e546e0048954fc46a92a65e3dc38828cbdf89c1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:17:30 +0400 Subject: [PATCH 01/11] Fixed XML comments on LINQ `Tracker` --- src/FSharp.Data.GraphQL.Server/Linq.fs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Data.GraphQL.Server/Linq.fs b/src/FSharp.Data.GraphQL.Server/Linq.fs index be3b2cf3d..7efca2daa 100644 --- a/src/FSharp.Data.GraphQL.Server/Linq.fs +++ b/src/FSharp.Data.GraphQL.Server/Linq.fs @@ -68,12 +68,16 @@ type Track = /// of all properties and subproperties accessed in provided /// ExecutionInfo with top level argument given as a root. type Tracker = + /// /// Leaf of the tree. Marks a direct field/property access with no sub-trees. /// Consists of record and (neglible in this case) list of arguments. + /// | Direct of Track * Arg list - /// Marks branched field/property access - property value withh possible sub-trees. + /// + /// Marks branched field/property access - property value with possible sub-trees. /// Consists of record list of arguments used to parametrize GraphQL /// field definition and set of subtrees. + /// | Compose of Track * Arg list * Set member x.Track = match x with From 2bbebb928be08b0a4bb41bbefb75bb488960ca0e Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:17:03 +0400 Subject: [PATCH 02/11] Added indexer to `TypeMap` type --- src/FSharp.Data.GraphQL.Shared/TypeSystem.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index 502ccecaf..619b8804e 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -2055,6 +2055,8 @@ and TypeMap () = let includeDefaultTypes = defaultArg includeDefaultTypes true this.ToSeq (includeDefaultTypes) |> List.ofSeq + member _.Item (name : string) = map[name] + /// /// Tries to find a NamedDef in the map by it's key (the name). /// From 1f119c602c8067b05a0fb7166182206219fa119f Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:08:18 +0400 Subject: [PATCH 03/11] Added `wantSome` and `wantNone` test helpers --- tests/FSharp.Data.GraphQL.Tests/Helpers.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index 74baef05f..e4585d48a 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -19,6 +19,10 @@ let isSeq<'a> actual = isType<'a seq> actual let isDict<'k, 'v> actual = isSeq> actual let isNameValueDict actual = isDict actual let fail (message: string) = Assert.Fail message +let wantSome opt = match opt with | Some value -> value | None -> fail "Expected Some but got None"; Unchecked.defaultof<_> +let wantNone opt = match opt with | None -> () | Some _ -> fail "Expected None but got Some" +let wantValueSome opt = match opt with | ValueSome value -> value | _ -> fail "Expected ValueSome but got ValueNone"; Unchecked.defaultof<_> +let wantValueNone opt = match opt with | ValueNone -> () | _ -> fail "Expected ValueNone but got ValueSome" let equals (expected : 'x) (actual : 'x) = if not (actual = expected) then fail <| $"expected %A{expected}\nbut got %A{actual}" let notEquals (expected : 'x) (actual : 'x) = From b169b71129a1a49897c2ca11981eaae959b45ed8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:07:30 +0400 Subject: [PATCH 04/11] Added description sample about `or` condition in the `ObjectListFilter` --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 3e2d6ab0c..fc00fd421 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,24 @@ query TestQuery { appearsIn homePlanet friends (filter : { name_starts_with: "A" }) { + friends (filter : { or : { name_starts_with: "A", name_starts_with: "B" }}) { + id + name + } + } +} +``` + +Also you can apply `not` operator 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 } From 3bf509eddafc8687a37945ed0d2a50aecf4cc142 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:18:02 +0400 Subject: [PATCH 05/11] Implemented `ObjectListFilter` parsing from variable --- .../SchemaDefinitions.fs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index b4af52bd7..b47f80b3d 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -1,12 +1,14 @@ namespace FSharp.Data.GraphQL.Server.Middleware open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Errors - /// Contains customized schema definitions for extensibility features. [] module SchemaDefinitions = @@ -117,6 +119,18 @@ module SchemaDefinitions = // | :? ObjectListFilter as x -> Ok x // | _ -> Error [{ new IGQLError with member _.Message = $"Cannot coerce ObjectListFilter output. '%s{x.GetType().FullName}' is not 'ObjectListFilter'" }] + // TODO: Move to shared and make public + let rec private jsonElementToInputValue (element : JsonElement) = + match element.ValueKind with + | JsonValueKind.Null -> NullValue + | JsonValueKind.True -> BooleanValue true + | JsonValueKind.False -> BooleanValue false + | JsonValueKind.String -> StringValue (element.GetString ()) + | JsonValueKind.Number -> FloatValue (element.GetDouble ()) + | JsonValueKind.Array -> ListValue (element.EnumerateArray () |> Seq.map jsonElementToInputValue |> List.ofSeq) + | JsonValueKind.Object -> ObjectValue (element.EnumerateObject () |> Seq.map (fun p -> p.Name, jsonElementToInputValue p.Value) |> Map.ofSeq) + | _ -> raise (NotSupportedException "Unsupported JSON element type") + /// Defines an object list filter for use as an argument for filter list of object fields. let ObjectListFilter : ScalarDefinition = { Name = "ObjectListFilter" @@ -126,5 +140,5 @@ module SchemaDefinitions = CoerceInput = (function | InlineConstant c -> coerceObjectListFilterInput c - | Variable _ -> raise <| NotSupportedException "List filter cannot be a variable") // TODO: Investigate + | Variable json -> json |> jsonElementToInputValue |> coerceObjectListFilterInput) CoerceOutput = coerceObjectListFilterValue } From 3c2a25f7494e35a836da71786a358833d245efe9 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Thu, 6 Mar 2025 22:07:57 +0400 Subject: [PATCH 06/11] Migrated `ObjectListFilter` to `voption` --- README.md | 2 +- samples/relay-book-store/Domain.fs | 2 +- samples/relay-book-store/Prelude.fs | 5 - samples/relay-book-store/Schema.fs | 26 +-- .../MiddlewareDefinitions.fs | 14 +- .../TypeSystemExtensions.fs | 20 +- .../Connections.fs | 15 +- src/FSharp.Data.GraphQL.Server/Planning.fs | 22 +-- src/FSharp.Data.GraphQL.Server/Values.fs | 22 +-- .../Helpers/ObjAndStructConversions.fs | 18 +- .../Introspection.fs | 8 +- src/FSharp.Data.GraphQL.Shared/TypeSystem.fs | 184 +++++++++--------- .../Schema.fs | 14 +- .../ExecutionTests.fs | 54 ++--- .../ExecutorMiddlewareTests.fs | 4 +- tests/FSharp.Data.GraphQL.Tests/Helpers.fs | 2 +- .../IntrospectionTests.fs | 2 +- .../MiddlewareTests.fs | 43 ++-- .../Variables and Inputs/InputEnumTests.fs | 2 +- .../Variables and Inputs/InputListTests.fs | 2 +- .../Variables and Inputs/InputNestedTests.fs | 2 +- .../InputNullableStringTests.fs | 2 +- .../SkippablesNormalizationTests.fs | 12 +- 23 files changed, 252 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index fc00fd421..81dedb622 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ type ObjectListFilter = | FilterField of FieldFilter ``` -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), diff --git a/samples/relay-book-store/Domain.fs b/samples/relay-book-store/Domain.fs index 184c5ad4c..6201148d8 100644 --- a/samples/relay-book-store/Domain.fs +++ b/samples/relay-book-store/Domain.fs @@ -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 diff --git a/samples/relay-book-store/Prelude.fs b/samples/relay-book-store/Prelude.fs index 0e1976f4b..f83283e53 100644 --- a/samples/relay-book-store/Prelude.fs +++ b/samples/relay-book-store/Prelude.fs @@ -3,11 +3,6 @@ namespace FSharp.Data.GraphQL.Samples.RelayBookStore [] module internal Prelude = - let vopt = - function - | Some x -> ValueSome x - | None -> ValueNone - [] module Base64 = diff --git a/samples/relay-book-store/Schema.fs b/samples/relay-book-store/Schema.fs index 6aad16162..f5ba3fa76 100644 --- a/samples/relay-book-store/Schema.fs +++ b/samples/relay-book-store/Schema.fs @@ -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 { diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs index 0214b67b5..5857bfe15 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs @@ -1,6 +1,9 @@ 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.Types.Patterns open FSharp.Data.GraphQL.Types @@ -14,8 +17,8 @@ type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool) then 0.0 else match f.Definition.Metadata.TryFind("queryWeight") with - | Some w -> w - | None -> 0.0 + | ValueSome w -> w + | ValueNone -> 0.0 // let rec getFields = function // | ResolveValue -> [] // | SelectFields fields -> fields @@ -82,7 +85,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat next ctx let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal) = - let rec collectArgs (acc : (string * ObjectListFilter) list) (fields : ExecutionInfo list) = + let rec collectArgs (acc : KeyValuePair list) (fields : ExecutionInfo list) = let fieldArgs field = let filterResults = field.Ast.Arguments @@ -96,7 +99,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat | Ok filters -> filters |> removeNoFilter - |> Seq.map (fun x -> field.Ast.AliasOrName, x) + |> Seq.map (fun x -> KeyValuePair (field.Ast.AliasOrName, x)) |> Seq.toList |> Ok match fields with @@ -119,7 +122,8 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat match reportToMetadata with | true -> let! args = collectArgs [] ctx.ExecutionPlan.Fields - return { ctx with Metadata = ctx.Metadata.Add("filters", args) } + let filters = ImmutableDictionary.CreateRange args + return { ctx with Metadata = ctx.Metadata.Add("filters", filters) } | false -> return ctx } match ctxResult with diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index b089a262d..7ae014993 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -1,5 +1,9 @@ namespace FSharp.Data.GraphQL.Server.Middleware +open System +open System.Collections.Immutable +open FsToolkit.ErrorHandling + open FSharp.Data.GraphQL.Types /// Contains extensions for the type system. @@ -16,6 +20,15 @@ module TypeSystemExtensions = /// A float value representing the weight that this field have on the query. member this.WithQueryWeight (weight : float) : FieldDef<'Val> = this.WithMetadata (this.Metadata.Add ("queryWeight", weight)) + type ObjectListFilters = ImmutableDictionary + + type ExecutionContext with + + /// + /// Gets the filters applied to the lists. + /// + member this.Filters = this.Metadata.TryFind "filters" + type ResolveFieldContext with /// @@ -23,6 +36,7 @@ module TypeSystemExtensions = /// Field argument is defined by the ObjectFilterMiddleware. /// member this.Filter = - match this.Args.TryFind ("filter") with - | Some (:? ObjectListFilter as f) -> Some f - | _ -> None + match this.Args.TryGetValue "filter" with + | true, (:? ObjectListFilter as f) -> ValueSome f + | false, _ -> ValueNone + | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") diff --git a/src/FSharp.Data.GraphQL.Server.Relay/Connections.fs b/src/FSharp.Data.GraphQL.Server.Relay/Connections.fs index be823a50c..2a1a30acc 100644 --- a/src/FSharp.Data.GraphQL.Server.Relay/Connections.fs +++ b/src/FSharp.Data.GraphQL.Server.Relay/Connections.fs @@ -85,22 +85,17 @@ module Cursor = [] module Definitions = - let private vopt = - function - | Some x -> ValueSome x - | None -> ValueNone - /// Active pattern used to match context arguments in order /// to construct Relay slice information. [] let (|SliceInfo|_|) (ctx : ResolveFieldContext) = match ctx.TryArg "first", ctx.TryArg "after" with - | Some (first), None -> ValueSome (Forward (first, ValueNone)) - | Some (first), (after) -> ValueSome (Forward (first, vopt after)) - | None, _ -> + | ValueSome (first), ValueNone -> ValueSome (Forward (first, ValueNone)) + | ValueSome (first), (after) -> ValueSome (Forward (first, after)) + | ValueNone, _ -> match ctx.TryArg "last", ctx.TryArg "before" with - | Some (last), None -> ValueSome (Backward (last, ValueNone)) - | Some (last), (before) -> ValueSome (Backward (last, vopt before)) + | ValueSome (last), ValueNone -> ValueSome (Backward (last, ValueNone)) + | ValueSome (last), (before) -> ValueSome (Backward (last, before)) | _, _ -> ValueNone /// Object defintion representing information about pagination in context of Relay connection diff --git a/src/FSharp.Data.GraphQL.Server/Planning.fs b/src/FSharp.Data.GraphQL.Server/Planning.fs index bd27e87e9..d1843c4bc 100644 --- a/src/FSharp.Data.GraphQL.Server/Planning.fs +++ b/src/FSharp.Data.GraphQL.Server/Planning.fs @@ -115,7 +115,7 @@ let rec private abstractionInfo (ctx : PlanningContext) (parentDef : AbstractDef | None -> Map.empty | None -> match ctx.Schema.TryFindType typeName with - | Some (Abstract abstractDef) -> + | ValueSome (Abstract abstractDef) -> abstractionInfo ctx abstractDef field ValueNone includer | _ -> let pname = parentDef :?> NamedDef @@ -153,9 +153,9 @@ let private doesFragmentTypeApply (schema: ISchema) fragment (objectType: Object | ValueNone -> true | ValueSome typeCondition -> match schema.TryFindType typeCondition with - | None -> false - | Some conditionalType when conditionalType.Name = objectType.Name -> true - | Some (Abstract conditionalType) -> schema.IsPossibleType conditionalType objectType + | ValueNone -> false + | ValueSome conditionalType when conditionalType.Name = objectType.Name -> true + | ValueSome (Abstract conditionalType) -> schema.IsPossibleType conditionalType objectType | _ -> false let private isDeferredField (field: Field) = @@ -342,16 +342,14 @@ let private planVariables (schema: ISchema) (operation: OperationDefinition) = |> List.map (fun vdef -> let vname = vdef.VariableName match Values.tryConvertAst schema vdef.Type with - | None -> + | ValueNone -> Debug.Fail "Must be prevented by validation" raise (MalformedGQLQueryException $"GraphQL query defined variable '$%s{vname}' of type '%s{vdef.Type.ToString()}' which is not known in the current schema") - | Some tdef -> - match tdef with - | :? InputDef as idef -> - { VarDef.Name = vname; TypeDef = idef; DefaultValue = vdef.DefaultValue } - | _ -> - Debug.Fail "Must be prevented by validation" - raise (MalformedGQLQueryException $"GraphQL query defined variable '$%s{vname}' of type '%s{tdef.ToString()}' which is not an input type definition")) + | ValueSome (:? InputDef as idef) -> + { VarDef.Name = vname; TypeDef = idef; DefaultValue = vdef.DefaultValue } + | ValueSome tdef -> + Debug.Fail "Must be prevented by validation" + raise (MalformedGQLQueryException $"GraphQL query defined variable '$%s{vname}' of type '%s{tdef.ToString()}' which is not an input type definition")) let internal planOperation (ctx: PlanningContext) : ExecutionPlan = // Create artificial plan info to start with diff --git a/src/FSharp.Data.GraphQL.Server/Values.fs b/src/FSharp.Data.GraphQL.Server/Values.fs index 8196ccafb..6eda2fa57 100644 --- a/src/FSharp.Data.GraphQL.Server/Values.fs +++ b/src/FSharp.Data.GraphQL.Server/Values.fs @@ -73,21 +73,19 @@ let private normalizeOptional (outputType : Type) value = /// Tries to convert type defined in AST into one of the type defs known in schema. let inline tryConvertAst schema ast = - let rec convert isNullable (schema : ISchema) (ast : InputType) : TypeDef option = + let rec convert isNullable (schema : ISchema) (ast : InputType) : TypeDef voption = match ast with | NamedType name -> - match schema.TryFindType name with - | Some namedDef -> - Some ( - if isNullable then - upcast namedDef.MakeNullable () - else - upcast namedDef - ) - | None -> None + schema.TryFindType name + |> ValueOption.map (fun namedDef -> + if isNullable then + upcast namedDef.MakeNullable () + else + upcast namedDef + ) | ListType inner -> convert true schema inner - |> Option.map (fun i -> + |> ValueOption.map (fun i -> if isNullable then upcast i.MakeList().MakeNullable () else @@ -121,7 +119,7 @@ let rec internal compileByType (fun (allParameters : _ ResizeArray) param -> match objDef.Fields - // TODO: Improve parameter name matching logic + // TODO: Improve parameter name matching logic |> Array.tryFind (fun field -> String.Equals (field.Name, param.Name, StringComparison.InvariantCultureIgnoreCase)) with | Some field -> diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs index fc28a39e0..dd78521de 100644 --- a/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs +++ b/src/FSharp.Data.GraphQL.Shared/Helpers/ObjAndStructConversions.fs @@ -1,5 +1,6 @@ namespace rec FSharp.Data.GraphQL +open System.Linq open FsToolkit.ErrorHandling module internal ValueOption = @@ -24,10 +25,25 @@ module internal Seq = |> Seq.where ValueOption.isSome |> Seq.map ValueOption.get + let vtryFind predicate seq = + seq + |> Seq.where predicate + |> Seq.map ValueSome + |> _.FirstOrDefault() + module internal List = - let vchoose mapping list = list |> Seq.vchoose mapping |> List.ofSeq + let vchoose mapping list = list |> Seq.ofList |> Seq.vchoose mapping |> List.ofSeq + + let vtryFind predicate list = list |> Seq.ofList |> Seq.vtryFind predicate module internal Array = let vchoose mapping array = array |> Seq.vchoose mapping |> Array.ofSeq + +module internal Map = + + let vtryFind key (map : Map<_, _>) = + match map.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone diff --git a/src/FSharp.Data.GraphQL.Shared/Introspection.fs b/src/FSharp.Data.GraphQL.Shared/Introspection.fs index 0221b1af1..cefebc018 100644 --- a/src/FSharp.Data.GraphQL.Shared/Introspection.fs +++ b/src/FSharp.Data.GraphQL.Shared/Introspection.fs @@ -103,7 +103,7 @@ let rec __Type = | Some name -> let found = findIntrospected ctx name match ctx.TryArg "includeDeprecated" with - | Some true -> found.Fields |> Option.map Array.toSeq + | ValueSome true -> found.Fields |> Option.map Array.toSeq | _ -> found.Fields |> Option.map (fun x -> upcast Array.filter (fun f -> not f.IsDeprecated) x) @@ -141,9 +141,9 @@ let rec __Type = | Some name -> let found = findIntrospected ctx name match ctx.TryArg "includeDeprecated" with - | None - | Some false -> found.EnumValues |> Option.map Array.toSeq - | Some true -> + | ValueNone + | ValueSome false -> found.EnumValues |> Option.map Array.toSeq + | ValueSome true -> found.EnumValues |> Option.map (fun x -> upcast (x |> Array.filter (fun f -> not f.IsDeprecated))) ) diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index 619b8804e..9c92b8509 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -8,8 +8,11 @@ open System.Collections open System.Collections.Concurrent open System.Collections.Generic open System.Collections.Immutable +open System.Runtime.InteropServices open System.Text.Json +open FsToolkit.ErrorHandling + open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Ast open FSharp.Data.GraphQL.Extensions @@ -429,7 +432,7 @@ and ISchema = /// Method which, given type name, returns Some if provided /// type has been defined in current schema. Otherwise None. - abstract TryFindType : string -> NamedDef option + abstract TryFindType : string -> NamedDef voption /// Returns array of all possible types for provided abstract /// type. For Union types, it's the array of all union options. @@ -679,31 +682,31 @@ and ExecutionInfo = { } with /// Get a nested info recognized by path provided as parameter. Path may consist of fields names or aliases. - member this.GetPath (keys : string list) : ExecutionInfo option = + member this.GetPath (keys : string list) : ExecutionInfo voption = let rec path info segments = match segments with | [] -> match info.Kind with - | ResolveCollection inner -> Some inner - | _ -> Some info + | ResolveCollection inner -> ValueSome inner + | _ -> ValueSome info | head :: tail -> match info.Kind with | ResolveDeferred inner -> path inner segments | ResolveLive inner -> path inner segments | ResolveStreamed (inner, _) -> path inner segments - | ResolveValue -> None + | ResolveValue -> ValueNone | ResolveCollection inner -> path inner segments | SelectFields fields -> fields - |> List.tryFind (fun f -> f.Identifier = head) - |> Option.bind (fun f -> path f tail) + |> List.vtryFind (fun f -> f.Identifier = head) + |> ValueOption.bind (fun f -> path f tail) | ResolveAbstraction typeMap -> typeMap |> Map.toSeq |> Seq.map snd |> Seq.collect id - |> Seq.tryFind (fun f -> f.Identifier = head) - |> Option.bind (fun f -> path f tail) + |> Seq.vtryFind (fun f -> f.Identifier = head) + |> ValueOption.bind (fun f -> path f tail) path this keys override this.ToString () = @@ -941,10 +944,10 @@ and ResolveFieldContext = { this.Context.AddError (this, { new IGQLError with member _.Message = errorMessage } ) /// Tries to find an argument by provided name. - member this.TryArg (name : string) : 't option = + member this.TryArg (name : string) : 't voption = match Map.tryFind name this.Args with - | Some o -> Some (o :?> 't) // TODO: Use Convert.ChangeType - | None -> None + | Some o -> ValueSome (o :?> 't) // TODO: Use Convert.ChangeType + | None -> ValueNone /// Function type for the compiled field executor. and ExecuteField = ResolveFieldContext -> obj -> AsyncVal @@ -1927,15 +1930,20 @@ and Metadata (data : Map) = /// Creates an empty Metadata object. static member Empty = Metadata.FromList [] + /// + /// Tries to find an value inside the metadata by it's key. + /// + /// The key to be used to search information for. + member _.Contains (key : string) = data.ContainsKey key + /// /// Tries to find an value inside the metadata by it's key. /// /// The key to be used to search information for. member _.TryFind<'Value> (key : string) = - if data.ContainsKey key then - data.Item key :?> 'Value |> Some - else - None + match data.TryGetValue key with + | true, v -> ValueSome (v :?> 'Value) + | _ -> ValueNone override _.ToString () = sprintf "%A" data @@ -1958,10 +1966,10 @@ and TypeMap () = let rec named (tdef : TypeDef) = match tdef with - | :? NamedDef as n -> Some n + | :? NamedDef as n -> ValueSome n | :? NullableDef as n -> named n.OfType | :? ListOfDef as l -> named l.OfType - | _ -> None + | _ -> ValueNone /// /// Adds (or optionally overwrites) a type to the type map. @@ -1977,7 +1985,7 @@ and TypeMap () = map.[name] <- def let asNamed x = match named x with - | Some n -> n + | ValueSome n -> n | _ -> failwith "Expected a Named type!" let rec insert (def : NamedDef) = match def with @@ -2013,19 +2021,19 @@ and TypeMap () = udef.Options |> Seq.iter insert | :? ListOfDef as ldef -> match named ldef.OfType with - | Some innerdef -> insert innerdef - | None -> () + | ValueSome innerdef -> insert innerdef + | ValueNone -> () | :? NullableDef as ndef -> match named ndef.OfType with - | Some innerdef -> insert innerdef - | None -> () + | ValueSome innerdef -> insert innerdef + | ValueNone -> () | :? InputObjectDef as iodef -> add iodef.Name def overwrite iodef.Fields |> Seq.collect (fun x -> (x.TypeDef :> TypeDef) |> Seq.singleton) |> Seq.map (fun x -> match named x with - | Some n -> n + | ValueSome n -> n | _ -> failwith "Expected a Named type!") |> Seq.filter (fun x -> not (map.ContainsKey (x.Name))) |> Seq.iter insert @@ -2051,8 +2059,7 @@ and TypeMap () = result /// Converts this type map to a list of string * NamedDef values, with the first item being the key. - member this.ToList (?includeDefaultTypes : bool) = - let includeDefaultTypes = defaultArg includeDefaultTypes true + member this.ToList ([]includeDefaultTypes : bool) = this.ToSeq (includeDefaultTypes) |> List.ofSeq member _.Item (name : string) = map[name] @@ -2065,11 +2072,11 @@ and TypeMap () = member _.TryFind (name : string, ?includeDefaultTypes : bool) = let includeDefaultTypes = defaultArg includeDefaultTypes false if not includeDefaultTypes && isDefaultType name then - None + ValueNone else match map.TryGetValue (name) with - | (true, item) -> Some item - | _ -> None + | (true, item) -> ValueSome item + | _ -> ValueNone /// /// Tries to find a NamedDef of a specific type in the map by it's key (the name). @@ -2079,11 +2086,11 @@ and TypeMap () = member this.TryFind<'Type when 'Type :> NamedDef> (name : string, ?includeDefaultTypes : bool) = let includeDefaultTypes = defaultArg includeDefaultTypes false match this.TryFind (name, includeDefaultTypes) with - | Some item -> + | ValueSome item -> match item with - | :? 'Type as item -> Some item - | _ -> None - | _ -> None + | :? 'Type as item -> ValueSome item + | _ -> ValueNone + | _ -> ValueNone /// /// Gets all NamedDef's inside the map that are, or implements the specified type. @@ -2097,10 +2104,10 @@ and TypeMap () = snd >> (fun x -> match x with - | :? 'Type as x -> Some x - | _ -> None) + | :? 'Type as x -> ValueSome x + | _ -> ValueNone) ) - |> Seq.choose id + |> Seq.vchoose id |> List.ofSeq /// @@ -2109,23 +2116,22 @@ and TypeMap () = /// /// The name of the ObjectDef that has the field that are being searched. /// The name of the FieldDef to be searched for. - member this.TryFindField (objname : string, fname : string) = - match this.TryFind (objname) with - | Some odef -> odef.Fields |> Map.tryFind fname - | None -> None + member this.TryFindField (objname : string, fname : string) = voption { + let! odef = this.TryFind (objname) + return! odef.Fields |> Map.vtryFind fname + } /// /// Tries to find a FieldDef inside an ObjectDef by its type, the object name and the field name. /// /// The name of the ObjectDef that has the field that are being searched. /// The name of the FieldDef to be searched for. - member this.TryFindField<'Type when 'Type :> OutputDef> (objname : string, fname : string) = - match this.TryFindField (objname, fname) with - | Some fdef -> - match fdef.TypeDef with - | :? 'Type -> Some fdef - | _ -> None - | _ -> None + member this.TryFindField<'Type when 'Type :> OutputDef> (objname : string, fname : string) = voption { + let! fdef = this.TryFindField (objname, fname) + match fdef.TypeDef with + | :? 'Type as x -> return x + | _ -> return! ValueNone + } /// /// Tries to find ObjectDef<'Val> types inside the map, that have fields that are lists of 'Res type. @@ -2295,28 +2301,28 @@ module Resolve = let (|BoxedSync|_|) = function - | Sync (d, c, expr) -> Some (d, c, boxifyExpr expr) - | _ -> None + | Sync (d, c, expr) -> ValueSome (d, c, boxifyExpr expr) + | _ -> ValueNone let (|BoxedAsync|_|) = function - | Async (d, c, expr) -> Some (d, c, boxifyExprAsync expr) - | _ -> None + | Async (d, c, expr) -> ValueSome (d, c, boxifyExprAsync expr) + | _ -> ValueNone let (|BoxedExpr|_|) = function - | ResolveExpr (e) -> Some (boxifyExpr e) - | _ -> None + | ResolveExpr (e) -> ValueSome (boxifyExpr e) + | _ -> ValueNone let (|BoxedFilterExpr|_|) = function - | Filter (r, i, o, expr) -> Some (r, i, o, boxifyFilterExpr expr) - | _ -> None + | Filter (r, i, o, expr) -> ValueSome (r, i, o, boxifyFilterExpr expr) + | _ -> ValueNone let (|BoxedAsyncFilterExpr|_|) = function - | AsyncFilter (r, i, o, expr) -> Some (r, i, o, boxifyAsyncFilterExpr expr) - | _ -> None + | AsyncFilter (r, i, o, expr) -> ValueSome (r, i, o, boxifyAsyncFilterExpr expr) + | _ -> ValueNone let private genMethodResolve<'Val, 'Res> (typeInfo : TypeInfo) (methodInfo : MethodInfo) = let argInfo = typeof.GetTypeInfo().GetDeclaredMethod ("Arg") @@ -2365,103 +2371,103 @@ module Patterns = /// Active pattern to match GraphQL type defintion with Scalar. let (|Scalar|_|) (tdef : TypeDef) = match tdef with - | :? ScalarDef as x -> Some x - | _ -> None + | :? ScalarDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with Object. let (|Object|_|) (tdef : TypeDef) = match tdef with - | :? ObjectDef as x -> Some x - | _ -> None + | :? ObjectDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with Interface. let (|Interface|_|) (tdef : TypeDef) = match tdef with - | :? InterfaceDef as x -> Some x - | _ -> None + | :? InterfaceDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with Union. let (|Union|_|) (tdef : TypeDef) = match tdef with - | :? UnionDef as x -> Some x - | _ -> None + | :? UnionDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with Enum. let (|Enum|_|) (tdef : TypeDef) = match tdef with - | :? EnumDef as x -> Some x - | _ -> None + | :? EnumDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with input object. let (|InputObject|_|) (tdef : TypeDef) = match tdef with - | :? InputObjectDef as x -> Some x - | _ -> None + | :? InputObjectDef as x -> ValueSome x + | _ -> ValueNone /// Active patter to match GraphQL subscription object definitions let (|SubscriptionObject|_|) (tdef : TypeDef) = match tdef with - | :? SubscriptionObjectDef as x -> Some x - | _ -> None + | :? SubscriptionObjectDef as x -> ValueSome x + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with List. let (|List|_|) (tdef : TypeDef) = match tdef with - | :? ListOfDef as x -> Some x.OfType - | _ -> None + | :? ListOfDef as x -> ValueSome x.OfType + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with nullable / optional types. let (|Nullable|_|) (tdef : TypeDef) = match tdef with - | :? NullableDef as x -> Some x.OfType - | _ -> None + | :? NullableDef as x -> ValueSome x.OfType + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with non-null types. let (|NonNull|_|) (tdef : TypeDef) = match tdef with - | :? NullableDef -> None - | other -> Some other + | :? NullableDef -> ValueNone + | other -> ValueSome other /// Active pattern to match GraphQL type defintion with valid input types. let (|Input|_|) (tdef : TypeDef) = match tdef with - | :? InputDef as i -> Some i - | _ -> None + | :? InputDef as i -> ValueSome i + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with valid output types. let (|Output|_|) (tdef : TypeDef) = match tdef with - | :? OutputDef as o -> Some o - | _ -> None + | :? OutputDef as o -> ValueSome o + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with valid leaf types. let (|Leaf|_|) (tdef : TypeDef) = match tdef with - | :? LeafDef as ldef -> Some ldef - | _ -> None + | :? LeafDef as ldef -> ValueSome ldef + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with valid composite types. let (|Composite|_|) (tdef : TypeDef) = match tdef with | :? ObjectDef | :? InterfaceDef - | :? UnionDef -> Some tdef - | _ -> None + | :? UnionDef -> ValueSome tdef + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with valid abstract types. let (|Abstract|_|) (tdef : TypeDef) = match tdef with | :? InterfaceDef - | :? UnionDef -> Some (tdef :?> AbstractDef) - | _ -> None + | :? UnionDef -> ValueSome (tdef :?> AbstractDef) + | _ -> ValueNone let rec private named (tdef : TypeDef) = match tdef with - | :? NamedDef as n -> Some n + | :? NamedDef as n -> ValueSome n | Nullable inner -> named inner | List inner -> named inner - | _ -> None + | _ -> ValueNone /// Active pattern to match GraphQL type defintion with named types. let rec (|Named|_|) (tdef : TypeDef) = named tdef diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs index 772b93fec..b94cd567d 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs @@ -125,7 +125,7 @@ module Schema = fields = [ Define.Field( name = "echo", - typedef = Nullable OutputType, + typedef = StructNullable OutputType, description = "Enters an input type and get it back.", args = [ Define.Input("input", Nullable InputType, description = "The input to be echoed as an output.") ], resolve = fun ctx _ -> ctx.TryArg("input")) ]) @@ -152,10 +152,10 @@ module Schema = resolve = fun ctx _ -> mapUploadToOutput (ctx.Arg("file"))) Define.Field( name = "nullableSingleUpload", - typedef = Nullable UploadedFileType, + typedef = StructNullable UploadedFileType, description = "Uploads (maybe) a single file to the server and get it back (maybe).", args = [ Define.Input("file", Nullable Upload, description = "The file to be uploaded.") ], - resolve = fun ctx _ -> ctx.TryArg("file") |> Option.flatten |> Option.map mapUploadToOutput) + resolve = fun ctx _ -> ctx.TryArg("file") |> ValueOption.flatten |> ValueOption.map mapUploadToOutput) Define.Field( name = "multipleUpload", typedef = ListOf UploadedFileType, @@ -164,16 +164,16 @@ module Schema = resolve = fun ctx _ -> ctx.Arg("files") |> Seq.map mapUploadToOutput) Define.Field( name = "nullableMultipleUpload", - typedef = Nullable (ListOf UploadedFileType), + typedef = StructNullable (ListOf UploadedFileType), description = "Uploads (maybe) a list of files to the server and get them back (maybe).", args = [ Define.Input("files", Nullable (ListOf Upload), description = "The files to upload.") ], - resolve = fun ctx _ -> ctx.TryArg("files") |> Option.flatten |> Option.map (Seq.map mapUploadToOutput)) + resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.flatten |> ValueOption.map (Seq.map mapUploadToOutput)) Define.Field( name = "nullableMultipleNullableUpload", - typedef = Nullable (ListOf (Nullable UploadedFileType)), + typedef = StructNullable (ListOf (Nullable UploadedFileType)), description = "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", args = [ Define.Input("files", Nullable (ListOf (Nullable Upload)), description = "The files to upload.") ], - resolve = fun ctx _ -> ctx.TryArg("files") |> Option.flatten |> Option.map (Seq.map (Option.map mapUploadToOutput))) + resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.flatten |> ValueOption.map (Seq.map (Option.map mapUploadToOutput))) Define.Field( name = "uploadRequest", typedef = UploadResponseType, diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index 244bef547..a1820de10 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -26,7 +26,7 @@ type TestSubject = { e: string f: string deep: DeepTestSubject - pic: int option -> string + pic: int voption -> string promise: Async } @@ -206,18 +206,18 @@ let ``Execution handles basic tasks: threads root value context correctly`` () = equals "thing" data.Thing type TestTarget = - { mutable Num: int option - mutable Str: string option } + { mutable Num: int voption + mutable Str: string voption } [] let ``Execution handles basic tasks: correctly threads arguments`` () = let query = """query Example { b(numArg: 123, stringArg: "foo") }""" - let data = { Num = None; Str = None } + let data = { Num = ValueNone; Str = ValueNone } let Type = Define.Object("Type", - [ Define.Field("b", Nullable StringType, "", [ Define.Input("numArg", IntType); Define.Input("stringArg", StringType) ], + [ Define.Field("b", StructNullable StringType, "", [ Define.Input("numArg", IntType); Define.Input("stringArg", StringType) ], fun ctx value -> value.Num <- ctx.TryArg("numArg") value.Str <- ctx.TryArg("stringArg") @@ -225,18 +225,18 @@ let ``Execution handles basic tasks: correctly threads arguments`` () = let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, data) ensureDirect result <| fun data errors -> empty errors - equals (Some 123) data.Num - equals (Some "foo") data.Str + equals (ValueSome 123) data.Num + equals (ValueSome "foo") data.Str [] let ``Execution handles basic tasks: correctly handles null arguments`` () = let query = """query Example { b(numArg: null, stringArg: null) }""" - let data = { Num = None; Str = None } + let data = { Num = ValueNone; Str = ValueNone } let Type = Define.Object("Type", - [ Define.Field("b", Nullable StringType, "", [ Define.Input("numArg", Nullable IntType); Define.Input("stringArg", Nullable StringType) ], + [ Define.Field("b", StructNullable StringType, "", [ Define.Input("numArg", Nullable IntType); Define.Input("stringArg", Nullable StringType) ], fun ctx value -> value.Num <- ctx.TryArg("numArg") value.Str <- ctx.TryArg("stringArg") @@ -244,8 +244,8 @@ let ``Execution handles basic tasks: correctly handles null arguments`` () = let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, data) ensureDirect result <| fun data errors -> empty errors - equals None data.Num - equals None data.Str + equals ValueNone data.Num + equals ValueNone data.Str type InlineTest = { A: string } @@ -260,22 +260,22 @@ let ``Execution handles basic tasks: correctly handles discriminated union argum options = [ Define.EnumValue("Case1", DUArg.Case1, "Case 1") Define.EnumValue("Case2", DUArg.Case2, "Case 2") ]) - let data = { Num = None; Str = None } + let data = { Num = ValueNone; Str = ValueNone } let Type = Define.Object("Type", - [ Define.Field("b", Nullable StringType, "", [ Define.Input("enumArg", EnumType) ], + [ Define.Field("b", StructNullable StringType, "", [ Define.Input("enumArg", EnumType) ], fun ctx value -> let arg = ctx.TryArg("enumArg") match arg with - | Some (Case1) -> - value.Str <- Some "foo" - value.Num <- Some 123 + | ValueSome (Case1) -> + value.Str <- ValueSome "foo" + value.Num <- ValueSome 123 value.Str - | _ -> None) ]) + | _ -> ValueNone) ]) let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, data) ensureDirect result <| fun data errors -> empty errors - equals (Some 123) data.Num - equals (Some "foo") data.Str + equals (ValueSome 123) data.Num + equals (ValueSome "foo") data.Str [] let ``Execution handles basic tasks: correctly handles Enum arguments`` () = @@ -288,22 +288,22 @@ let ``Execution handles basic tasks: correctly handles Enum arguments`` () = options = [ Define.EnumValue("Enum1", EnumArg.Enum1, "Enum 1") Define.EnumValue("Enum2", EnumArg.Enum2, "Enum 2") ]) - let data = { Num = None; Str = None } + let data = { Num = ValueNone; Str = ValueNone } let Type = Define.Object("Type", - [ Define.Field("b", Nullable StringType, "", [ Define.Input("enumArg", EnumType) ], + [ Define.Field("b", StructNullable StringType, "", [ Define.Input("enumArg", EnumType) ], fun ctx value -> let arg = ctx.TryArg("enumArg") match arg with - | Some _ -> - value.Str <- Some "foo" - value.Num <- Some 123 + | ValueSome _ -> + value.Str <- ValueSome "foo" + value.Num <- ValueSome 123 value.Str - | _ -> None) ]) + | _ -> ValueNone) ]) let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, data) ensureDirect result <| fun data errors -> empty errors - equals (Some 123) data.Num - equals (Some "foo") data.Str + equals (ValueSome 123) data.Num + equals (ValueSome "foo") data.Str [] diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs index 3cfa0f0c6..6611603b4 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs @@ -122,5 +122,5 @@ let ``Executor middleware: change fields and measure planning time`` () = data |> equals (upcast expected) | _ -> fail "Expected Direct GQLResponse" match result.Metadata.TryFind("planningTime") with - | Some time -> time |> greaterThanOrEqual 5L - | None -> fail "Expected planning time on GQLResponse metadata, but it was not found" + | ValueSome time -> time |> greaterThanOrEqual 5L + | ValueNone -> fail "Expected planning time on GQLResponse metadata, but it was not found" diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index e4585d48a..a814938be 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -82,7 +82,7 @@ open System.Text.Json open FSharp.Data.GraphQL.Types let stringifyArg name (ctx : ResolveFieldContext) () = - let arg = ctx.TryArg name |> Option.toObj + let arg = ctx.TryArg name |> ValueOption.toObj JsonSerializer.Serialize (arg, serializerOptions) let stringifyInput = stringifyArg "input" diff --git a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs index 068188da9..a3cea4422 100644 --- a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs @@ -332,7 +332,7 @@ type User = { FirstName: string; LastName: string } type UserInput = { Name: string } [] -let ``Introspection works with query and mutation sharing same generic param`` = +let ``Introspection works with query and mutation sharing same generic param`` () = let user = Define.Object("User", [ Define.AutoField("firstName", StringType) diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 7aa9893a9..1846bc07b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -1,6 +1,7 @@ module FSharp.Data.GraphQL.Tests.MiddlewareTests open System +open System.Collections.Generic open Xunit open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types @@ -136,8 +137,8 @@ let ``Simple query: Should pass when below threshold``() = empty errors data |> equals (upcast expected) | _ -> fail "Expected Direct GQLResponse" - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 1.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) [] let ``Simple query: Should not pass when above threshold``() = @@ -193,8 +194,8 @@ let ``Simple query: Should not pass when above threshold``() = let result = execute query result |> ensureRequestError <| fun errors -> errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 3.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) [] let ``Deferred queries : Should pass when below threshold``() = @@ -246,8 +247,8 @@ let ``Deferred queries : Should pass when below threshold``() = use sub = Observer.create deferred sub.WaitCompleted() sub.Received |> single |> equals expectedDeferred - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 2.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) [] let ``Streamed queries : Should pass when below threshold``() = @@ -307,8 +308,8 @@ let ``Streamed queries : Should pass when below threshold``() = |> contains expectedDeferred1 |> contains expectedDeferred2 |> ignore - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 2.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) [] let ``Deferred and Streamed queries : Should not pass when above threshold``() = @@ -366,8 +367,8 @@ let ``Deferred and Streamed queries : Should not pass when above threshold``() = |> Seq.iter (fun result -> ensureRequestError result <| fun errors -> errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 3.0)) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0)) [] let ``Inline fragment query : Should pass when below threshold``() = @@ -407,8 +408,8 @@ let ``Inline fragment query : Should pass when below threshold``() = ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 1.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) [] let ``Inline fragment query : Should not pass when above threshold``() = @@ -460,8 +461,8 @@ let ``Inline fragment query : Should not pass when above threshold``() = ensureRequestError result <| fun errors -> errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 3.0) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) [] let ``Object list filter: should return filter information in Metadata``() = @@ -470,7 +471,7 @@ let ``Object list filter: should return filter information in Metadata``() = A (id : 1) { id value - subjects (filter : { value_starts_with: "A", id : 2 }) { ...Value } + s : subjects (filter : { value_starts_with: "A", id : 2 }) { ...Value } } } @@ -489,7 +490,7 @@ let ``Object list filter: should return filter information in Metadata``() = "A", upcast NameValueLookup.ofList [ "id", upcast 1 "value", upcast "A1" - "subjects", upcast [ + "s", upcast [ NameValueLookup.ofList [ "id", upcast 2 "value", upcast "A2" @@ -501,12 +502,12 @@ let ``Object list filter: should return filter information in Metadata``() = ] ] ] - let expectedFilter = - "subjects", And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" }) + let expectedFilter : KeyValuePair = + KeyValuePair(["A"; "s"], And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" })) let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("queryWeightThreshold") |> equals (Some 2.0) - result.Metadata.TryFind("queryWeight") |> equals (Some 1.0) - result.Metadata.TryFind<(string * ObjectListFilter) list>("filters") |> equals (Some [ expectedFilter ]) + result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) + result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs index 82651cb0d..3ee801295 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputEnumTests.fs @@ -16,7 +16,7 @@ open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Shared let stringifyArg name (ctx : ResolveFieldContext) () = - let arg = ctx.TryArg name |> Option.toObj + let arg = ctx.TryArg name |> ValueOption.toObj JsonSerializer.Serialize (arg, serializerOptions) let stringifyInput = stringifyArg "input" diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs index a5ecf858d..06d393196 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputListTests.fs @@ -17,7 +17,7 @@ open FSharp.Data.GraphQL.Shared open ErrorHelpers let stringifyArg name (ctx : ResolveFieldContext) () = - let arg = ctx.TryArg name |> Option.toObj + let arg = ctx.TryArg name |> ValueOption.toObj JsonSerializer.Serialize (arg, serializerOptions) let stringifyInput = stringifyArg "input" diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs index 25cdaf064..714c02901 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs @@ -58,7 +58,7 @@ let rec TestRecursiveInputObject = ) let stringifyArg name (ctx : ResolveFieldContext) () = - let arg = ctx.TryArg name |> Option.toObj + let arg = ctx.TryArg name |> ValueOption.toObj JsonSerializer.Serialize (arg, serializerOptions) let stringifyInput = stringifyArg "input" diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs index 9f84587ff..d9781696c 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNullableStringTests.fs @@ -17,7 +17,7 @@ open FSharp.Data.GraphQL.Shared open ErrorHelpers let stringifyArg name (ctx : ResolveFieldContext) () = - let arg = ctx.TryArg name |> Option.toObj + let arg = ctx.TryArg name |> ValueOption.toObj JsonSerializer.Serialize (arg, serializerOptions) let stringifyInput = stringifyArg "input" diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/SkippablesNormalizationTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/SkippablesNormalizationTests.fs index b67c0b0ea..b15c36d84 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/SkippablesNormalizationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/SkippablesNormalizationTests.fs @@ -224,11 +224,11 @@ let schema verify = | Nothing -> () | Skip -> record.VerifyAllSkip - recordOptional |> Option.iter _.VerifyAllSkip + recordOptional |> ValueOption.iter _.VerifyAllSkip recordNested.HomeAddress.VerifyAllSkip | SkipAndIncludeNull -> record.VerifySkipAndIncludeNull - recordOptional |> Option.iter _.VerifySkipAndIncludeNull + recordOptional |> ValueOption.iter _.VerifySkipAndIncludeNull recordNested.HomeAddress.VerifySkipAndIncludeNull stringifyInput ctx name ) @@ -245,10 +245,10 @@ let schema verify = | Nothing -> () | Skip -> obj.VerifyAllSkip - objOptional |> Option.iter _.VerifyAllSkip + objOptional |> ValueOption.iter _.VerifyAllSkip | SkipAndIncludeNull -> obj.VerifySkipAndIncludeNull - objOptional |> Option.iter _.VerifySkipAndIncludeNull + objOptional |> ValueOption.iter _.VerifySkipAndIncludeNull stringifyInput ctx name ) ) // TODO: add all args stringificaiton @@ -264,10 +264,10 @@ let schema verify = | Nothing -> () | Skip -> obj.VerifyAllSkip - objOptional |> Option.iter _.VerifyAllSkip + objOptional |> ValueOption.iter _.VerifyAllSkip | SkipAndIncludeNull -> obj.VerifySkipAndIncludeNull - objOptional |> Option.iter _.VerifySkipAndIncludeNull + objOptional |> ValueOption.iter _.VerifySkipAndIncludeNull stringifyInput ctx name ) ) ] // TODO: add all args stringificaiton From e7fa923ad14bb6dbea5fe7d78eab52920e3d5e71 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:11:58 +0400 Subject: [PATCH 07/11] Changed `ObjectListFilter` reporting to middleware with full path to a GraphQL fields --- .../MiddlewareDefinitions.fs | 19 ++++++++++--------- .../TypeSystemExtensions.fs | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs index 5857bfe15..2d016fa88 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs @@ -76,7 +76,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) @@ -85,8 +85,8 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat next ctx let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal) = - let rec collectArgs (acc : KeyValuePair list) (fields : ExecutionInfo list) = - let fieldArgs field = + let rec collectArgs (path: obj list) (acc : KeyValuePair list) (fields : ExecutionInfo list) = + let fieldArgs currentPath field = let filterResults = field.Ast.Arguments |> Seq.map (fun x -> @@ -99,29 +99,30 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat | Ok filters -> filters |> removeNoFilter - |> Seq.map (fun x -> KeyValuePair (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 + let! args = collectArgs [] [] ctx.ExecutionPlan.Fields let filters = ImmutableDictionary.CreateRange args return { ctx with Metadata = ctx.Metadata.Add("filters", filters) } | false -> return ctx diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 7ae014993..f32d6d832 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -4,6 +4,7 @@ open System open System.Collections.Immutable open FsToolkit.ErrorHandling +open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types /// Contains extensions for the type system. @@ -20,14 +21,7 @@ module TypeSystemExtensions = /// A float value representing the weight that this field have on the query. member this.WithQueryWeight (weight : float) : FieldDef<'Val> = this.WithMetadata (this.Metadata.Add ("queryWeight", weight)) - type ObjectListFilters = ImmutableDictionary - - type ExecutionContext with - - /// - /// Gets the filters applied to the lists. - /// - member this.Filters = this.Metadata.TryFind "filters" + open ObjectListFilter.Operators type ResolveFieldContext with @@ -40,3 +34,12 @@ module TypeSystemExtensions = | true, (:? ObjectListFilter as f) -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") + + type ObjectListFilters = ImmutableDictionary + + type ExecutionContext with + + /// + /// Gets the filters applied to the lists. + /// + member this.Filters = this.Metadata.TryFind "filters" From 2e4699fc5ecf5a3220d7091be8885e4c4cc39193 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:16:22 +0400 Subject: [PATCH 08/11] Added `OfTypes` case to `ObjectListFilter` --- .../ObjectListFilter.fs | 4 +++- .../TypeSystemExtensions.fs | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index 143bea29a..c940a5b53 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,5 +1,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware +open System + /// A filter definition for a field value. type FieldFilter<'Val> = { FieldName : string @@ -16,10 +18,10 @@ type ObjectListFilter = | StartsWith of FieldFilter | EndsWith of FieldFilter | Contains of FieldFilter + | OfTypes of FieldFilter | FilterField of FieldFilter | NoFilter - /// Contains tooling for working with ObjectListFilter. module ObjectListFilter = /// Contains operators for building and comparing ObjectListFilter values. diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index f32d6d832..069c9cb96 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -31,7 +31,17 @@ module TypeSystemExtensions = /// member this.Filter = match this.Args.TryGetValue "filter" with - | true, (:? ObjectListFilter as f) -> ValueSome f + | 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 -> f &&& (OfTypes { FieldName = "__typename"; Value = filters }) |> ValueSome + | _ -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") From 48bb1523ef666fe78fe8523a8d3219a12b3d3fc7 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 7 Mar 2025 05:21:01 +0400 Subject: [PATCH 09/11] WIP implementing application of `ObjectListFilter` to `IQueryable` --- .../ObjectListFilter.fs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs index c940a5b53..e46edb7e0 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs @@ -1,6 +1,10 @@ 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> = @@ -55,3 +59,117 @@ 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) From af255479ccd5984243adda4ec7fa6f7cfff605fa Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 7 Mar 2025 14:50:55 +0200 Subject: [PATCH 10/11] Implemented `ObjectListFilter` tests for And, Or, Not filters and supplying filter from variables --- README.md | 22 +- .../MiddlewareDefinitions.fs | 6 +- .../MiddlewareTests.fs | 208 +++++++++++++++++- 3 files changed, 223 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 81dedb622..eafc505a1 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,6 @@ query TestQuery { appearsIn homePlanet friends (filter : { name_starts_with: "A" }) { - friends (filter : { or : { name_starts_with: "A", name_starts_with: "B" }}) { id name } @@ -362,7 +361,7 @@ query TestQuery { } ``` -Also you can apply `not` operator and combine filters with `and` and `or` operators like this: +Also you can apply `not` operator like this: ```graphql query TestQuery { @@ -371,7 +370,24 @@ query TestQuery { name appearsIn homePlanet - friends (filter : { or : { name_starts_with: "A", name_starts_with: "B" }}) { + 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 } diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs index 2d016fa88..cbaf6d60e 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs @@ -5,6 +5,7 @@ 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 @@ -90,8 +91,9 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat 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 diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index 1846bc07b..ea861e7ff 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -10,6 +10,8 @@ open FSharp.Data.GraphQL.Shared open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Execution open FSharp.Data.GraphQL.Ast +open System.Collections.Immutable +open System.Text.Json #nowarn "40" @@ -87,6 +89,8 @@ let executor = Define.ObjectListFilterMiddleware(true) ] Executor(schema, middleware) +let executeWithVariables (query : Document , variables: ImmutableDictionary) = + executor.AsyncExecute(ast = query, variables = variables) |> sync let execute (query : Document) = executor.AsyncExecute(query) |> sync @@ -94,7 +98,7 @@ let expectedErrors : GQLProblemDetails list = [ GQLProblemDetails.Create ("Query complexity exceeds maximum threshold. Please reduce query complexity and try again.") ] [] -let ``Simple query: Should pass when below threshold``() = +let ``Simple query: Must pass when below threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -141,7 +145,7 @@ let ``Simple query: Should pass when below threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) [] -let ``Simple query: Should not pass when above threshold``() = +let ``Simple query: Must not pass when above threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -198,7 +202,7 @@ let ``Simple query: Should not pass when above threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) [] -let ``Deferred queries : Should pass when below threshold``() = +let ``Deferred queries : Must pass when below threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -251,7 +255,7 @@ let ``Deferred queries : Should pass when below threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) [] -let ``Streamed queries : Should pass when below threshold``() = +let ``Streamed queries : Must pass when below threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -312,7 +316,7 @@ let ``Streamed queries : Should pass when below threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) [] -let ``Deferred and Streamed queries : Should not pass when above threshold``() = +let ``Deferred and Streamed queries : Must not pass when above threshold``() = let query = sprintf """query testQuery { A (id : 1) { @@ -371,7 +375,7 @@ let ``Deferred and Streamed queries : Should not pass when above threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0)) [] -let ``Inline fragment query : Should pass when below threshold``() = +let ``Inline fragment query : Must pass when below threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -412,7 +416,7 @@ let ``Inline fragment query : Should pass when below threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) [] -let ``Inline fragment query : Should not pass when above threshold``() = +let ``Inline fragment query : Must not pass when above threshold``() = let query = parse """query testQuery { A (id : 1) { @@ -465,7 +469,7 @@ let ``Inline fragment query : Should not pass when above threshold``() = result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) [] -let ``Object list filter: should return filter information in Metadata``() = +let ``Object list filter: must return filter information in Metadata``() = let query = parse """query testQuery { A (id : 1) { @@ -511,3 +515,191 @@ let ``Object list filter: should return filter information in Metadata``() = result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + +[] +let ``Object list filter: Must return AND filter information in Metadata``() = + let query = + parse """query testQuery { + A (id : 1) { + id + value + subjects (filter : { and : [{ value_starts_with: "3"}, {id : 6 }]}) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", upcast NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", upcast [ + NameValueLookup.ofList [ + "id", upcast 2 + "value", upcast "A2" + ] + NameValueLookup.ofList [ + "id", upcast 6 + "value", upcast "3000" + ] + ] + ] + ] + let expectedFilter : KeyValuePair = + KeyValuePair(["A"; "subjects"], And (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + let result = execute query + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + +[] +let ``Object list filter: Must return OR filter information in Metadata``() = + let query = + parse """query testQuery { + A (id : 1) { + id + value + subjects (filter : { or : [{value_starts_with: "3"}, {id : 6}] }) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", upcast NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", upcast [ + NameValueLookup.ofList [ + "id", upcast 2 + "value", upcast "A2" + ] + NameValueLookup.ofList [ + "id", upcast 6 + "value", upcast "3000" + ] + ] + ] + ] + let expectedFilter : KeyValuePair = + KeyValuePair(["A"; "subjects"], Or (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + let result = execute query + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + +[] +let ``Object list filter: Must return NOT filter information in Metadata``() = + let query = + parse """query testQuery { + A (id : 1) { + id + value + subjects (filter : { not : {value_starts_with: "3"} }) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", upcast NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", upcast [ + NameValueLookup.ofList [ + "id", upcast 2 + "value", upcast "A2" + ] + NameValueLookup.ofList [ + "id", upcast 6 + "value", upcast "3000" + ] + ] + ] + ] + let expectedFilter : KeyValuePair = + KeyValuePair(["A"; "subjects"], Not (StartsWith { FieldName = "value"; Value = "3" })) + let result = execute query + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + +[] +let ``Object list filter: Must return filter information in Metadata when supplied as variable``() = + let jsonString = """{ "not": { "value_starts_with": "3" } }""" + let jsonElement = JsonDocument.Parse(jsonString).RootElement + + let dict = ImmutableDictionary.Empty.Add("filter", jsonElement) + let query = + parse """query testQuery($filter: ObjectListFilter!) { + A (id : 1) { + id + value + subjects (filter : $filter) { ...Value } + } + } + + fragment Value on Subject { + ...on A { + id + value + } + ...on B { + id + value + } + }""" + let expected = + NameValueLookup.ofList [ + "A", upcast NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", upcast [ + NameValueLookup.ofList [ + "id", upcast 2 + "value", upcast "A2" + ] + NameValueLookup.ofList [ + "id", upcast 6 + "value", upcast "3000" + ] + ] + ] + ] + let expectedFilter : KeyValuePair = + KeyValuePair(["A"; "subjects"], Not (StartsWith { FieldName = "value"; Value = "3" })) + let result = executeWithVariables(query, dict) + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] From 02f0986981f7d576b0c5c341ebae58426027f283 Mon Sep 17 00:00:00 2001 From: Viktor Tochonov Date: Fri, 7 Mar 2025 15:12:44 +0200 Subject: [PATCH 11/11] Formatted `ObjectListFilter` related files --- .../SchemaDefinitions.fs | 267 +++++----- .../TypeSystemExtensions.fs | 2 +- .../MiddlewareTests.fs | 476 +++++++++--------- 3 files changed, 379 insertions(+), 366 deletions(-) diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs index b47f80b3d..449667413 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs @@ -1,4 +1,6 @@ -namespace FSharp.Data.GraphQL.Server.Middleware +/// Contains customized schema definitions for extensibility features. +[] +module FSharp.Data.GraphQL.Server.Middleware.SchemaDefinitions open System open System.Collections.Generic @@ -7,138 +9,155 @@ open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Ast -open FSharp.Data.GraphQL.Errors - -/// Contains customized schema definitions for extensibility features. -[] -module SchemaDefinitions = - let internal removeNoFilter = Seq.where (fun filter -> filter <> NoFilter) +let internal removeNoFilter = Seq.where (fun filter -> filter <> NoFilter) - let rec private coerceObjectListFilterInput x : Result = +let rec private coerceObjectListFilterInput x : Result = - let (|EndsWith|StartsWith|GreaterThan|LessThan|Contains|Equals|) (s : string) = - let s = s.ToLowerInvariant() - let prefix (suffix : string) (s : string) = s.Substring(0, s.Length - suffix.Length) - 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("_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("_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("_contains") && s.Length > "_contains".Length -> Contains (prefix "_contains" s) - | s -> Equals s + let (|EndsWith|StartsWith|GreaterThan|LessThan|Contains|Equals|) (s : string) = + let s = s.ToLowerInvariant () + let prefix (suffix : string) (s : string) = s.Substring (0, s.Length - suffix.Length) + 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 ("_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 ("_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 ("_contains") && s.Length > "_contains".Length -> Contains (prefix "_contains" s) + | s -> Equals s - let (|EquatableValue|Other|) v = - match v with - | IntValue v -> EquatableValue (v :> System.IComparable) - | FloatValue v -> EquatableValue (v :> System.IComparable) - | BooleanValue v -> EquatableValue (v :> System.IComparable) - | StringValue v -> EquatableValue (v :> System.IComparable) - | EnumValue v -> EquatableValue (v :> System.IComparable) - | v -> Other v + let (|EquatableValue|Other|) v = + match v with + | IntValue v -> EquatableValue (v :> System.IComparable) + | FloatValue v -> EquatableValue (v :> System.IComparable) + | BooleanValue v -> EquatableValue (v :> System.IComparable) + | StringValue v -> EquatableValue (v :> System.IComparable) + | EnumValue v -> EquatableValue (v :> System.IComparable) + | v -> Other v - let (|ComparableValue|Other|) v = - match v with - | IntValue v -> ComparableValue (v :> System.IComparable) - | FloatValue v -> ComparableValue (v :> System.IComparable) - | BooleanValue v -> ComparableValue (v :> System.IComparable) - | StringValue v -> ComparableValue (v :> System.IComparable) - | v -> Other v + let (|ComparableValue|Other|) v = + match v with + | IntValue v -> ComparableValue (v :> System.IComparable) + | FloatValue v -> ComparableValue (v :> System.IComparable) + | BooleanValue v -> ComparableValue (v :> System.IComparable) + | StringValue v -> ComparableValue (v :> System.IComparable) + | v -> Other v - let buildAnd x = - let rec build acc x = - match x with - | [] -> acc - | x :: xs -> - match acc with - | NoFilter -> build (x) xs - | acc -> build ((And (acc, x))) xs - build NoFilter x + let buildAnd x = + let rec build acc x = + match x with + | [] -> acc + | x :: xs -> + match acc with + | NoFilter -> build (x) xs + | acc -> build ((And (acc, x))) xs + build NoFilter x - let buildOr x = - let rec build acc x = - match x with - | [] -> acc - | x :: xs -> - match acc with - | NoFilter -> build (x) xs - | acc -> build ((Or (acc, x))) xs - build NoFilter x + let buildOr x = + let rec build acc x = + match x with + | [] -> acc + | x :: xs -> + match acc with + | NoFilter -> build (x) xs + | acc -> build ((Or (acc, x))) xs + build NoFilter x - let rec mapFilter (name : string, value : InputValue) = - let mapFilters fields = - let coerceResults = - fields |> Seq.map coerceObjectListFilterInput |> Seq.toList |> splitSeqErrorsList - match coerceResults with - | Error errs -> Error errs - | Ok coerced -> coerced |> removeNoFilter |> Seq.toList |> Ok - match name, value with - | Equals "and", ListValue fields -> fields |> mapFilters |> Result.map buildAnd - | Equals "or", ListValue fields -> fields |> mapFilters |> Result.map buildOr - | Equals "not", ObjectValue value -> - match mapInput value with - | Error errs -> Error errs - | Ok NoFilter -> Ok NoFilter - | Ok filter -> Ok (Not filter) - | EndsWith fname, StringValue value -> Ok (EndsWith { FieldName = fname; Value = value }) - | StartsWith fname, StringValue value -> Ok (StartsWith { FieldName = fname; Value = value }) - | Contains fname, StringValue value -> Ok (Contains { FieldName = fname; Value = value }) - | Equals fname, ObjectValue value -> - match mapInput value with - | Error errs -> Error errs - | Ok NoFilter -> Ok NoFilter - | Ok filter -> Ok (FilterField { FieldName = fname; Value = filter }) - | Equals fname, EquatableValue value -> Ok (Equals { FieldName = fname; Value = value }) - | GreaterThan fname, ComparableValue value -> Ok (GreaterThan { FieldName = fname; Value = value }) - | LessThan fname, ComparableValue value -> Ok (LessThan { FieldName = fname; Value = value }) - | _ -> Ok NoFilter - - and mapInput value = - let filterResults = - value |> Map.toSeq |> Seq.map mapFilter |> Seq.toList |> splitSeqErrorsList - match filterResults with + let rec mapFilter (name : string, value : InputValue) = + let mapFilters fields = + let coerceResults = + fields + |> Seq.map coerceObjectListFilterInput + |> Seq.toList + |> splitSeqErrorsList + match coerceResults with + | Error errs -> Error errs + | Ok coerced -> coerced |> removeNoFilter |> Seq.toList |> Ok + match name, value with + | Equals "and", ListValue fields -> fields |> mapFilters |> Result.map buildAnd + | Equals "or", ListValue fields -> fields |> mapFilters |> Result.map buildOr + | Equals "not", ObjectValue value -> + match mapInput value with | Error errs -> Error errs - | Ok filters -> - filters |> removeNoFilter |> List.ofSeq |> buildAnd |> Ok - match x with - | ObjectValue x -> mapInput x - | NullValue -> NoFilter |> Ok - // TODO: Get union case - | _ -> Error [{ new IGQLError with member _.Message = $"'ObjectListFilter' must be defined as object but got '{x.GetType ()}'" }] + | Ok NoFilter -> Ok NoFilter + | Ok filter -> Ok (Not filter) + | EndsWith fname, StringValue value -> Ok (EndsWith { FieldName = fname; Value = value }) + | StartsWith fname, StringValue value -> Ok (StartsWith { FieldName = fname; Value = value }) + | Contains fname, StringValue value -> Ok (Contains { FieldName = fname; Value = value }) + | Equals fname, ObjectValue value -> + match mapInput value with + | Error errs -> Error errs + | Ok NoFilter -> Ok NoFilter + | Ok filter -> Ok (FilterField { FieldName = fname; Value = filter }) + | Equals fname, EquatableValue value -> Ok (Equals { FieldName = fname; Value = value }) + | GreaterThan fname, ComparableValue value -> Ok (GreaterThan { FieldName = fname; Value = value }) + | LessThan fname, ComparableValue value -> Ok (LessThan { FieldName = fname; Value = value }) + | _ -> Ok NoFilter + + and mapInput value = + let filterResults = + value + |> Map.toSeq + |> Seq.map mapFilter + |> Seq.toList + |> splitSeqErrorsList + match filterResults with + | Error errs -> Error errs + | Ok filters -> filters |> removeNoFilter |> List.ofSeq |> buildAnd |> Ok + match x with + | ObjectValue x -> mapInput x + | NullValue -> NoFilter |> Ok + // TODO: Get union case + | _ -> + Error [ + { new IGQLError with + member _.Message = $"'ObjectListFilter' must be defined as object but got '{x.GetType ()}'" + } + ] - let private coerceObjectListFilterValue (x : obj) : ObjectListFilter option = - match x with - | :? ObjectListFilter as x -> Some x - | _ -> None - //let private coerceObjectListFilterValue (x : obj) = - // match x with - // | :? ObjectListFilter as x -> Ok x - // | _ -> Error [{ new IGQLError with member _.Message = $"Cannot coerce ObjectListFilter output. '%s{x.GetType().FullName}' is not 'ObjectListFilter'" }] +let private coerceObjectListFilterValue (x : obj) : ObjectListFilter option = + match x with + | :? ObjectListFilter as x -> Some x + | _ -> None +//let private coerceObjectListFilterValue (x : obj) = +// match x with +// | :? ObjectListFilter as x -> Ok x +// | _ -> Error [{ new IGQLError with member _.Message = $"Cannot coerce ObjectListFilter output. '%s{x.GetType().FullName}' is not 'ObjectListFilter'" }] - // TODO: Move to shared and make public - let rec private jsonElementToInputValue (element : JsonElement) = - match element.ValueKind with - | JsonValueKind.Null -> NullValue - | JsonValueKind.True -> BooleanValue true - | JsonValueKind.False -> BooleanValue false - | JsonValueKind.String -> StringValue (element.GetString ()) - | JsonValueKind.Number -> FloatValue (element.GetDouble ()) - | JsonValueKind.Array -> ListValue (element.EnumerateArray () |> Seq.map jsonElementToInputValue |> List.ofSeq) - | JsonValueKind.Object -> ObjectValue (element.EnumerateObject () |> Seq.map (fun p -> p.Name, jsonElementToInputValue p.Value) |> Map.ofSeq) - | _ -> raise (NotSupportedException "Unsupported JSON element type") +// TODO: Move to shared and make public +let rec private jsonElementToInputValue (element : JsonElement) = + match element.ValueKind with + | JsonValueKind.Null -> NullValue + | JsonValueKind.True -> BooleanValue true + | JsonValueKind.False -> BooleanValue false + | JsonValueKind.String -> StringValue (element.GetString ()) + | JsonValueKind.Number -> FloatValue (element.GetDouble ()) + | JsonValueKind.Array -> + ListValue ( + element.EnumerateArray () + |> Seq.map jsonElementToInputValue + |> List.ofSeq + ) + | JsonValueKind.Object -> + ObjectValue ( + element.EnumerateObject () + |> Seq.map (fun p -> p.Name, jsonElementToInputValue p.Value) + |> Map.ofSeq + ) + | _ -> raise (NotSupportedException "Unsupported JSON element type") - /// Defines an object list filter for use as an argument for filter list of object fields. - let ObjectListFilter : ScalarDefinition = - { 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." - CoerceInput = - (function - | InlineConstant c -> coerceObjectListFilterInput c - | Variable json -> json |> jsonElementToInputValue |> coerceObjectListFilterInput) - CoerceOutput = coerceObjectListFilterValue } +/// Defines an object list filter for use as an argument for filter list of object fields. +let ObjectListFilter : ScalarDefinition = { + 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." + CoerceInput = + (function + | InlineConstant c -> coerceObjectListFilterInput c + | Variable json -> json |> jsonElementToInputValue |> coerceObjectListFilterInput) + CoerceOutput = coerceObjectListFilterValue +} diff --git a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs index 069c9cb96..27fae4438 100644 --- a/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.Middleware/TypeSystemExtensions.fs @@ -40,7 +40,7 @@ module TypeSystemExtensions = | ValueNone -> raise (MalformedGQLQueryException ($"Type '{name}' not found in schema.")) match typeFields.Keys |> Seq.map getType |> Seq.toList with | [] -> ValueNone - | filters -> f &&& (OfTypes { FieldName = "__typename"; Value = filters }) |> ValueSome + | filters -> ValueSome (f &&& (OfTypes { FieldName = "__typename"; Value = filters })) | _ -> ValueSome f | false, _ -> ValueNone | true, _ -> raise (InvalidOperationException "Invalid filter argument type.") diff --git a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs index ea861e7ff..b936b984b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MiddlewareTests.fs @@ -15,32 +15,22 @@ open System.Text.Json #nowarn "40" -type Root = { - clientId : int -} +type Root = { clientId : int } and Subject = | A of A | B of B -and A = { - id : int - value : string - subjects : int list -} +and A = { id : int; value : string; subjects : int list } -and B = { - id : int - value : string - subjects : int list -} +and B = { id : int; value : string; subjects : int list } let executor = let a1 : A = { id = 1; value = "A1"; subjects = [ 2; 6 ] } let a2 : A = { id = 2; value = "A2"; subjects = [ 1; 3; 5 ] } let a3 : A = { id = 3; value = "A3"; subjects = [ 1; 2; 4 ] } let b1 = { id = 4; value = "1000"; subjects = [ 1; 5 ] } - let b2 = { id = 5; value = "2000"; subjects = [ 3; 4 ; 6] } + let b2 = { id = 5; value = "2000"; subjects = [ 3; 4; 6 ] } let b3 = { id = 6; value = "3000"; subjects = [ 1; 3; 5 ] } let al = [ a1; a2; a3 ] let bl = [ b1; b2; b3 ] @@ -48,59 +38,90 @@ let executor = let getB id = bl |> List.tryFind (fun b -> b.id = id) let subjects = (al |> List.map A) @ (bl |> List.map B) let getSubject id = - let matchesId id = function A a -> a.id = id | B b -> b.id = id + let matchesId id = + function + | A a -> a.id = id + | B b -> b.id = id subjects |> List.tryFind (matchesId id) let rec SubjectType = - Define.Union( + Define.Union ( name = "Subject", options = [ AType; BType ], - resolveValue = (fun u -> match u with A a -> box a | B b -> box b), - resolveType = (fun u -> match u with A _ -> upcast AType | B _ -> upcast BType)) + resolveValue = + (fun u -> + match u with + | A a -> box a + | B b -> box b), + resolveType = + (fun u -> + match u with + | A _ -> upcast AType + | B _ -> upcast BType) + ) and AType = - DefineRec.Object( + DefineRec.Object ( name = "A", isTypeOf = (fun o -> o :? A), - fieldsFn = fun () -> - [ Define.Field("id", IntType, resolve = fun _ a -> a.id) - Define.Field("value", StringType, resolve = fun _ a -> a.value) - Define.Field("subjects", Nullable (ListOf (Nullable SubjectType)), - resolve = fun _ (a : A) -> a.subjects |> List.map getSubject |> List.toSeq |> Some) - .WithQueryWeight(1.0) ]) + fieldsFn = + fun () -> [ + Define.Field ("id", IntType, resolve = (fun _ a -> a.id)) + Define.Field ("value", StringType, resolve = (fun _ a -> a.value)) + Define + .Field( + "subjects", + Nullable (ListOf (Nullable SubjectType)), + resolve = fun _ (a : A) -> a.subjects |> List.map getSubject |> List.toSeq |> Some + ) + .WithQueryWeight (1.0) + ] + ) and BType = - DefineRec.Object( + DefineRec.Object ( name = "B", isTypeOf = (fun o -> o :? B), - fieldsFn = fun () -> - [ Define.Field("id", IntType, resolve = fun _ b -> b.id) - Define.Field("value", StringType, resolve = fun _ b -> b.value) - Define.Field("subjects", Nullable (ListOf (Nullable SubjectType)), - resolve = fun _ (b : B) -> b.subjects |> List.map getSubject |> List.toSeq |> Some) - .WithQueryWeight(1.0) ]) + fieldsFn = + fun () -> [ + Define.Field ("id", IntType, resolve = (fun _ b -> b.id)) + Define.Field ("value", StringType, resolve = (fun _ b -> b.value)) + Define + .Field( + "subjects", + Nullable (ListOf (Nullable SubjectType)), + resolve = fun _ (b : B) -> b.subjects |> List.map getSubject |> List.toSeq |> Some + ) + .WithQueryWeight (1.0) + ] + ) let Query = - Define.Object( + Define.Object ( name = "Query", - fields = - [ Define.Field("A", Nullable AType, "A Field", [ Define.Input("id", IntType) ], resolve = fun ctx _ -> getA (ctx.Arg("id"))) - Define.Field("B", Nullable BType, "B Field", [ Define.Input("id", IntType) ], resolve = fun ctx _ -> getB (ctx.Arg("id"))) ]) - let schema = Schema(Query) - let middleware = - [ Define.QueryWeightMiddleware(2.0, true) - Define.ObjectListFilterMiddleware(true) - Define.ObjectListFilterMiddleware(true) ] - Executor(schema, middleware) - -let executeWithVariables (query : Document , variables: ImmutableDictionary) = - executor.AsyncExecute(ast = query, variables = variables) |> sync -let execute (query : Document) = - executor.AsyncExecute(query) |> sync - -let expectedErrors : GQLProblemDetails list = - [ GQLProblemDetails.Create ("Query complexity exceeds maximum threshold. Please reduce query complexity and try again.") ] + fields = [ + Define.Field ("A", Nullable AType, "A Field", [ Define.Input ("id", IntType) ], resolve = (fun ctx _ -> getA (ctx.Arg ("id")))) + Define.Field ("B", Nullable BType, "B Field", [ Define.Input ("id", IntType) ], resolve = (fun ctx _ -> getB (ctx.Arg ("id")))) + ] + ) + let schema = Schema (Query) + let middleware = [ + Define.QueryWeightMiddleware (2.0, true) + Define.ObjectListFilterMiddleware (true) + Define.ObjectListFilterMiddleware (true) + ] + Executor (schema, middleware) + +let execute (query : Document) = executor.AsyncExecute (query) |> sync + +let executeWithVariables (query : Document, variables : ImmutableDictionary) = + executor.AsyncExecute (ast = query, variables = variables) |> sync + +let expectedErrors : GQLProblemDetails list = [ + GQLProblemDetails.Create ("Query complexity exceeds maximum threshold. Please reduce query complexity and try again.") +] [] -let ``Simple query: Must pass when below threshold``() = +let ``Simple query: Must pass when below threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -120,20 +141,18 @@ let ``Simple query: Must pass when below threshold``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] let result = execute query match result with @@ -141,13 +160,14 @@ let ``Simple query: Must pass when below threshold``() = empty errors data |> equals (upcast expected) | _ -> fail "Expected Direct GQLResponse" - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 1.0) [] -let ``Simple query: Must not pass when above threshold``() = +let ``Simple query: Must not pass when above threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -196,15 +216,15 @@ let ``Simple query: Must not pass when above threshold``() = ...on B { ...AllB } }""" let result = execute query - result |> ensureRequestError <| fun errors -> - errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) + result |> ensureRequestError <| fun errors -> errors |> equals expectedErrors + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 3.0) [] -let ``Deferred queries : Must pass when below threshold``() = +let ``Deferred queries : Must pass when below threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -224,23 +244,13 @@ let ``Deferred queries : Must pass when below threshold``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast null - ] + "A", upcast NameValueLookup.ofList [ "id", upcast 1; "value", upcast "A1"; "subjects", upcast null ] ] let expectedDeferred = DeferredResult ( [| - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] |], [ "A"; "subjects" ] ) @@ -249,15 +259,16 @@ let ``Deferred queries : Must pass when below threshold``() = empty errors data |> equals (upcast expected) use sub = Observer.create deferred - sub.WaitCompleted() + sub.WaitCompleted () sub.Received |> single |> equals expectedDeferred - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 2.0) [] -let ``Streamed queries : Must pass when below threshold``() = +let ``Streamed queries : Must pass when below threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -277,48 +288,31 @@ let ``Streamed queries : Must pass when below threshold``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [] - ] + "A", upcast NameValueLookup.ofList [ "id", upcast 1; "value", upcast "A1"; "subjects", upcast [] ] ] let expectedDeferred1 = - DeferredResult ([| - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - |], - [ "A"; "subjects"; 0 ] - ) + DeferredResult ([| NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] |], [ "A"; "subjects"; 0 ]) let expectedDeferred2 = - DeferredResult ([| - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] - |], - [ "A"; "subjects"; 1 ] - ) + DeferredResult ([| NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] |], [ "A"; "subjects"; 1 ]) let result = execute query ensureDeferred result <| fun data errors deferred -> empty errors data |> equals (upcast expected) use sub = Observer.create deferred - sub.WaitCompleted(2) + sub.WaitCompleted (2) sub.Received |> Seq.cast |> contains expectedDeferred1 |> contains expectedDeferred2 |> ignore - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 2.0) [] -let ``Deferred and Streamed queries : Must not pass when above threshold``() = +let ``Deferred and Streamed queries : Must not pass when above threshold`` () = let query = - sprintf """query testQuery { + sprintf + """query testQuery { A (id : 1) { id value @@ -369,15 +363,15 @@ let ``Deferred and Streamed queries : Must not pass when above threshold``() = asts query |> Seq.map execute |> Seq.iter (fun result -> - ensureRequestError result <| fun errors -> - errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0)) + ensureRequestError result <| fun errors -> errors |> equals expectedErrors + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 3.0)) [] -let ``Inline fragment query : Must pass when below threshold``() = +let ``Inline fragment query : Must pass when below threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -394,31 +388,31 @@ let ``Inline fragment query : Must pass when below threshold``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6 ] + ] ] - ] ] let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 1.0) [] -let ``Inline fragment query : Must not pass when above threshold``() = +let ``Inline fragment query : Must not pass when above threshold`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -459,19 +453,15 @@ let ``Inline fragment query : Must not pass when above threshold``() = } }""" let result = execute query - match result with - | RequestError errors -> errors |> equals expectedErrors - | response -> fail $"Expected 'RequestError' GQLResponse but got\n{response}" - - ensureRequestError result <| fun errors -> - errors |> equals expectedErrors - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 3.0) + ensureRequestError result <| fun errors -> errors |> equals expectedErrors + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 3.0) [] -let ``Object list filter: must return filter information in Metadata``() = +let ``Object list filter: must return filter information in Metadata`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -491,35 +481,36 @@ let ``Object list filter: must return filter information in Metadata``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "s", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "s", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] - let expectedFilter : KeyValuePair = - KeyValuePair(["A"; "s"], And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" })) + let expectedFilter : KeyValuePair = + KeyValuePair ([ "A"; "s" ], And (Equals { FieldName = "id"; Value = 2L }, StartsWith { FieldName = "value"; Value = "A" })) let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("queryWeightThreshold") |> equals (ValueSome 2.0) - result.Metadata.TryFind("queryWeight") |> equals (ValueSome 1.0) - result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + result.Metadata.TryFind ("queryWeightThreshold") |> equals (ValueSome 2.0) + result.Metadata.TryFind ("queryWeight") |> equals (ValueSome 1.0) + result.Metadata.TryFind ("filters") + |> wantValueSome + |> seqEquals [ expectedFilter ] [] -let ``Object list filter: Must return AND filter information in Metadata``() = +let ``Object list filter: Must return AND filter information in Metadata`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -539,33 +530,34 @@ let ``Object list filter: Must return AND filter information in Metadata``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] - let expectedFilter : KeyValuePair = - KeyValuePair(["A"; "subjects"], And (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + let expectedFilter : KeyValuePair = + KeyValuePair ([ "A"; "subjects" ], And (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + result.Metadata.TryFind ("filters") + |> wantValueSome + |> seqEquals [ expectedFilter ] [] -let ``Object list filter: Must return OR filter information in Metadata``() = +let ``Object list filter: Must return OR filter information in Metadata`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -585,33 +577,34 @@ let ``Object list filter: Must return OR filter information in Metadata``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] - let expectedFilter : KeyValuePair = - KeyValuePair(["A"; "subjects"], Or (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) + let expectedFilter : KeyValuePair = + KeyValuePair ([ "A"; "subjects" ], Or (StartsWith { FieldName = "value"; Value = "3" }, Equals { FieldName = "id"; Value = 6L })) let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + result.Metadata.TryFind ("filters") + |> wantValueSome + |> seqEquals [ expectedFilter ] [] -let ``Object list filter: Must return NOT filter information in Metadata``() = +let ``Object list filter: Must return NOT filter information in Metadata`` () = let query = - parse """query testQuery { + parse + """query testQuery { A (id : 1) { id value @@ -631,37 +624,38 @@ let ``Object list filter: Must return NOT filter information in Metadata``() = }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] let expectedFilter : KeyValuePair = - KeyValuePair(["A"; "subjects"], Not (StartsWith { FieldName = "value"; Value = "3" })) + KeyValuePair ([ "A"; "subjects" ], Not (StartsWith { FieldName = "value"; Value = "3" })) let result = execute query ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + result.Metadata.TryFind ("filters") + |> wantValueSome + |> seqEquals [ expectedFilter ] [] -let ``Object list filter: Must return filter information in Metadata when supplied as variable``() = +let ``Object list filter: Must return filter information in Metadata when supplied as variable`` () = let jsonString = """{ "not": { "value_starts_with": "3" } }""" let jsonElement = JsonDocument.Parse(jsonString).RootElement - let dict = ImmutableDictionary.Empty.Add("filter", jsonElement) + let dict = ImmutableDictionary.Empty.Add ("filter", jsonElement) let query = - parse """query testQuery($filter: ObjectListFilter!) { + parse + """query testQuery($filter: ObjectListFilter!) { A (id : 1) { id value @@ -681,25 +675,25 @@ let ``Object list filter: Must return filter information in Metadata when suppli }""" let expected = NameValueLookup.ofList [ - "A", upcast NameValueLookup.ofList [ - "id", upcast 1 - "value", upcast "A1" - "subjects", upcast [ - NameValueLookup.ofList [ - "id", upcast 2 - "value", upcast "A2" - ] - NameValueLookup.ofList [ - "id", upcast 6 - "value", upcast "3000" - ] + "A", + upcast + NameValueLookup.ofList [ + "id", upcast 1 + "value", upcast "A1" + "subjects", + upcast + [ + NameValueLookup.ofList [ "id", upcast 2; "value", upcast "A2" ] + NameValueLookup.ofList [ "id", upcast 6; "value", upcast "3000" ] + ] ] - ] ] let expectedFilter : KeyValuePair = - KeyValuePair(["A"; "subjects"], Not (StartsWith { FieldName = "value"; Value = "3" })) - let result = executeWithVariables(query, dict) + KeyValuePair ([ "A"; "subjects" ], Not (StartsWith { FieldName = "value"; Value = "3" })) + let result = executeWithVariables (query, dict) ensureDirect result <| fun data errors -> empty errors data |> equals (upcast expected) - result.Metadata.TryFind("filters") |> wantValueSome |> seqEquals [ expectedFilter ] + result.Metadata.TryFind ("filters") + |> wantValueSome + |> seqEquals [ expectedFilter ]