diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index b86fda44..ff2eaecf 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -359,6 +359,10 @@ module SchemaDefinitions = /// to take option of provided value. let Nullable(innerDef : #TypeDef<'Val>) : NullableDef<'Val> = upcast { NullableDefinition.OfType = innerDef } + /// Wraps a GraphQL type definition, allowing defining field/argument + /// to take voption of provided value. + let StructNullable(innerDef : #TypeDef<'Val>) : StructNullableDef<'Val> = upcast { StructNullableDefinition.OfType = innerDef } + /// Wraps a GraphQL type definition, allowing defining field/argument /// to take collection of provided value. let ListOf(innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Seq> = upcast { ListOfDefinition.OfType = innerDef } diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index f1a06659..4a5ac580 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -1560,6 +1560,46 @@ and internal NullableDefinition<'Val> = { | :? ListOfDef as list -> "[" + list.OfType.ToString () + "]" | other -> other.ToString () +/// GraphQL type definition for nullable/optional types. +/// By default all GraphQL types in this library are considered +/// to be NonNull. This definition applies reversed mechanics, +/// allowing them to take null as a valid value. +and StructNullableDef<'Val> = + interface + /// GraphQL type definition of the nested type. + abstract OfType : TypeDef<'Val> + inherit InputDef<'Val voption> + inherit OutputDef<'Val voption> + inherit NullableDef + end + +and internal StructNullableDefinition<'Val> = { + OfType : TypeDef<'Val> +} with + + interface InputDef + + interface TypeDef with + member _.Type = typeof<'Val voption> + member x.MakeNullable () = upcast x + member x.MakeList () = + let list : ListOfDefinition<_, _> = { OfType = x } + upcast list + + interface OutputDef + + interface NullableDef with + member x.OfType = upcast x.OfType + + interface StructNullableDef<'Val> with + member x.OfType = x.OfType + + override x.ToString () = + match x.OfType with + | :? NamedDef as named -> named.Name + | :? ListOfDef as list -> "[" + list.OfType.ToString () + "]" + | other -> other.ToString () + /// GraphQL tye definition for input objects. They are different /// from object types (which can be used only as outputs). and InputObjectDef = diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index f4e745c8..720555f3 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -33,7 +33,9 @@ type TestSubject = { and DeepTestSubject = { a: string b: string - c: string option list + c: string option + d: string voption + l: string option list } and DUArg = @@ -62,7 +64,9 @@ let ``Execution handles basic tasks: executes arbitrary code`` () = { a = "Already Been Done" b = "Boring" - c = [Some "Contrived"; None; Some "Confusing"] + c = Some "Contrived" + d = ValueSome "Donut" + l = [Some "Contrived"; None; Some "Confusing"] } let ast = parse """query Example($size: Int) { @@ -81,6 +85,8 @@ let ``Execution handles basic tasks: executes arbitrary code`` () = a b c + d + l } } @@ -102,7 +108,9 @@ let ``Execution handles basic tasks: executes arbitrary code`` () = "deep", upcast NameValueLookup.ofList [ "a", "Already Been Done" :> obj "b", upcast "Boring" - "c", upcast ["Contrived" :> obj; null; upcast "Confusing"] + "c", upcast "Contrived" + "d", upcast "Donut" + "l", upcast ["Contrived" :> obj; null; upcast "Confusing"] ] ] @@ -111,8 +119,11 @@ let ``Execution handles basic tasks: executes arbitrary code`` () = "DeepDataType", [ Define.Field("a", StringType, (fun _ dt -> dt.a)) Define.Field("b", StringType, (fun _ dt -> dt.b)) - Define.Field("c", (ListOf (Nullable StringType)), (fun _ dt -> dt.c)) + Define.Field("c", Nullable StringType, (fun _ dt -> dt.c)) + Define.Field("d", StructNullable StringType, (fun _ dt -> dt.d)) + Define.Field("l", (ListOf (Nullable StringType)), (fun _ dt -> dt.l)) ]) + let rec DataType = DefineRec.Object( "DataType", @@ -121,7 +132,7 @@ let ``Execution handles basic tasks: executes arbitrary code`` () = Define.Field("a", StringType, resolve = fun _ dt -> dt.a) Define.Field("b", StringType, resolve = fun _ dt -> dt.b) Define.Field("c", StringType, resolve = fun _ dt -> dt.c) - Define.Field("d", StringType, fun _ dt -> dt.d) + Define.Field("d", StringType, resolve = fun _ dt -> dt.d) Define.Field("e", StringType, fun _ dt -> dt.e) Define.Field("f", StringType, fun _ dt -> dt.f) Define.Field("pic", StringType, "Picture resizer", [ Define.Input("size", Nullable IntType) ], fun ctx dt -> dt.pic(ctx.TryArg("size"))) diff --git a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs index a7fd9c05..068188da 100644 --- a/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/IntrospectionTests.fs @@ -432,6 +432,46 @@ let ``Nullabe field type definitions are considered nullable`` () = empty errors data |> equals (upcast expected) +[] +let ``StructNullabe field type definitions are considered nullable`` () = + let root = Define.Object("Query", [ Define.Field("onlyField", StructNullable StringType) ]) + let schema = Schema(root) + let query = """{ __type(name: "Query") { + fields { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } }""" + let result = sync <| Executor(schema).AsyncExecute(query) + let expected = + NameValueLookup.ofList [ + "__type", upcast NameValueLookup.ofList [ + "fields", upcast [ + box <| NameValueLookup.ofList [ + "name", upcast "onlyField" + "type", upcast NameValueLookup.ofList [ + "kind", upcast "SCALAR" + "name", upcast "String" + "ofType", null]]]]] + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + [] let ``Default field args type definitions are considered non-null`` () = let root = Define.Object("Query", [ Define.Field("onlyField", StringType, "", [ Define.Input("onlyArg", IntType) ], fun _ () -> null) ]) @@ -523,6 +563,50 @@ let ``Nullable field args type definitions are considered nullable`` () = empty errors data |> equals (upcast expected) +[] +let ``StructNullable field args type definitions are considered nullable`` () = + let root = Define.Object("Query", [ Define.Field("onlyField", StringType, "", [ Define.Input("onlyArg", StructNullable IntType) ], fun _ () -> null) ]) + let schema = Schema(root) + let query = """{ __type(name: "Query") { + fields { + args { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } }""" + let result = sync <| Executor(schema).AsyncExecute(query) + let expected = + NameValueLookup.ofList [ + "__type", upcast NameValueLookup.ofList [ + "fields", upcast [ + box <| NameValueLookup.ofList [ + "args", upcast [ + box <| NameValueLookup.ofList [ + "name", upcast "onlyArg" + "type", upcast NameValueLookup.ofList [ + "kind", upcast "SCALAR" + "name", upcast "Int" + "ofType", null ]]]]]]] + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + [] let ``Introspection executes an introspection query`` () = let root = Define.Object("QueryRoot", [ Define.Field("onlyField", StringType) ])