diff --git a/src/FSharp.Data.GraphQL.Server/Execution.fs b/src/FSharp.Data.GraphQL.Server/Execution.fs index 4e5ca4dd5..3b897a5f9 100644 --- a/src/FSharp.Data.GraphQL.Server/Execution.fs +++ b/src/FSharp.Data.GraphQL.Server/Execution.fs @@ -256,7 +256,12 @@ let private resolveField (execute: ExecuteField) (ctx: ResolveFieldContext) (par type ResolverResult<'T> = Result<'T * IObservable option * GQLProblemDetails list, GQLProblemDetails list> +[] module ResolverResult = + + let data data = Ok (data, None, []) + let defered data deferred = Ok (data, Some deferred, []) + let mapValue (f : 'T -> 'U) (r : ResolverResult<'T>) : ResolverResult<'U> = Result.map(fun (data, deferred, errs) -> (f data, deferred, errs)) r @@ -280,7 +285,7 @@ let private unionImplError unionName tyName path ctx = resolverError path ctx (G let private deferredNullableError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Deferred field %s of type '%s' must be nullable" name tyName)) let private streamListError name tyName path ctx = resolverError path ctx (GQLMessageException (sprintf "Streamed field %s of type '%s' must be list" name tyName)) -let private resolved name v : AsyncVal>> = AsyncVal.wrap <| Ok(KeyValuePair(name, box v), None, []) +let private resolved name v : AsyncVal>> = KeyValuePair(name, box v) |> ResolverResult.data |> AsyncVal.wrap let deferResults path (res : ResolverResult) : IObservable = let formattedPath = path |> List.rev @@ -370,7 +375,8 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path | kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind match Map.tryFind resolvedDef.Name typeMap with | Some fields -> executeObjectFields fields name resolvedDef ctx path value - | None -> raiseErrors <| interfaceImplError iDef.Name resolvedDef.Name path ctx + | None -> KeyValuePair(name, null) |> ResolverResult.data |> AsyncVal.wrap + //| None -> raiseErrors <| interfaceImplError iDef.Name resolvedDef.Name path ctx | Union uDef -> let possibleTypesFn = ctx.Schema.GetPossibleTypes @@ -382,7 +388,8 @@ let rec private direct (returnDef : OutputDef) (ctx : ResolveFieldContext) (path | kind -> failwithf "Unexpected value of ctx.ExecutionPlan.Kind: %A" kind match Map.tryFind resolvedDef.Name typeMap with | Some fields -> executeObjectFields fields name resolvedDef ctx path (uDef.ResolveValue value) - | None -> raiseErrors <| unionImplError uDef.Name resolvedDef.Name path ctx + | None -> KeyValuePair(name, null) |> ResolverResult.data |> AsyncVal.wrap + //| None -> raiseErrors <| unionImplError uDef.Name resolvedDef.Name path ctx | _ -> failwithf "Unexpected value of returnDef: %O" returnDef @@ -393,7 +400,7 @@ and deferred (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (valu executeResolvers ctx path parent (toOption value |> AsyncVal.wrap) |> Observable.ofAsyncVal |> Observable.bind(ResolverResult.mapValue(fun d -> d.Value) >> deferResults path) - AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, null), Some deferred, []) + ResolverResult.defered (KeyValuePair (info.Identifier, null)) deferred |> AsyncVal.wrap and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) = let info = ctx.ExecutionInfo @@ -444,7 +451,7 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (c |> Array.mapi resolveItem |> Observable.ofAsyncValSeq |> buffer - AsyncVal.wrap <| Ok(KeyValuePair(info.Identifier, box [||]), Some stream, []) + ResolverResult.defered (KeyValuePair (info.Identifier, null)) stream |> AsyncVal.wrap | _ -> raise <| GQLMessageException (ErrorMessages.expectedEnumerableValue ctx.ExecutionInfo.Identifier (value.GetType())) and private live (ctx : ResolveFieldContext) (path : FieldPath) (parent : obj) (value : obj) = diff --git a/src/FSharp.Data.GraphQL.Server/IO.fs b/src/FSharp.Data.GraphQL.Server/IO.fs index f33e56566..b9fe6e73b 100644 --- a/src/FSharp.Data.GraphQL.Server/IO.fs +++ b/src/FSharp.Data.GraphQL.Server/IO.fs @@ -61,9 +61,9 @@ type GQLExecutionResult = static member Invalid(documentId, errors, meta) = GQLExecutionResult.RequestError(documentId, errors, meta) static member ErrorAsync(documentId, msg : string, meta) = - asyncVal.Return (GQLExecutionResult.Error (documentId, msg, meta)) + AsyncVal.wrap (GQLExecutionResult.Error (documentId, msg, meta)) static member ErrorAsync(documentId, error : IGQLError, meta) = - asyncVal.Return (GQLExecutionResult.Error (documentId, error, meta)) + AsyncVal.wrap (GQLExecutionResult.Error (documentId, error, meta)) // TODO: Rename to PascalCase and GQLResponseContent = diff --git a/tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs b/tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs index 6307294e1..bc99354bb 100644 --- a/tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/AbstractionTests.fs @@ -75,7 +75,12 @@ let schemaWithInterface = [ Define.Field ( "pets", ListOf PetType, - fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; upcast { Name = "Garfield"; Meows = false } ] + fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet; { Name = "Garfield"; Meows = false } ] + ) + Define.Field ( + "nullablePets", + ListOf (Nullable PetType), + fun _ _ -> [ { Name = "Odie"; Woofs = true } :> IPet |> Some; { Name = "Garfield"; Meows = false } :> IPet |> Some ] ) ] ), config = { SchemaConfig.Default with Types = [ CatType; DogType ] } @@ -111,6 +116,79 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r empty errors data |> equals (upcast expected) +[] +let ``Execute handles execution of abstract types: not specified Interface types produce error`` () = + let query = + """{ + pets { + ... on Dog { + name + woofs + } + } + }""" + + let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query) + ensureRequestError result <| fun [ petsError ] -> + petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ] + + let query = + """{ + pets { + ... on Cat { + name + meows + } + } + }""" + + let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query) + ensureRequestError result <| fun [ petsError ] -> + petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ] + +[] +let ``Execute handles execution of abstract types: not specified Interface types must be filtered out if they allow null`` () = + let query = + """{ + nullablePets { + ... on Dog { + name + woofs + } + } + }""" + + let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query) + + let expected = + NameValueLookup.ofList + [ "nullablePets", upcast [ NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ] :> obj; null ] ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + + let query = + """{ + nullablePets { + ... on Cat { + name + meows + } + } + }""" + + let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query) + + let expected = + NameValueLookup.ofList + [ "nullablePets", + upcast [ null; NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ] :> obj ] ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + [] let ``Execute handles execution of abstract types: absent field resolution produces errors for Interface`` () = let query = @@ -155,6 +233,26 @@ let ``Execute handles execution of abstract types: absent type resolution produc catError |> ensureValidationError "Field 'unknownField2' is not defined in schema type 'Cat'." [ "pets"; "unknownField2" ] dogError |> ensureValidationError "Inline fragment has type condition 'UnknownDog', but that type does not exist in the schema." [ "pets" ] + let query = + """{ + pets { + name + ... on Dog { + woofs + unknownField1 + } + ... on UnknownCat { + meows + unknownField2 + } + } + }""" + + let result = sync <| schemaWithInterface.Value.AsyncExecute (parse query) + ensureRequestError result <| fun [ catError; dogError ] -> + catError |> ensureValidationError "Field 'unknownField1' is not defined in schema type 'Dog'." [ "pets"; "unknownField1" ] + dogError |> ensureValidationError "Inline fragment has type condition 'UnknownCat', but that type does not exist in the schema." [ "pets" ] + let schemaWithUnion = lazy @@ -184,6 +282,11 @@ let schemaWithUnion = "pets", ListOf PetType, fun _ _ -> [ DogCase { Name = "Odie"; Woofs = true }; CatCase { Name = "Garfield"; Meows = false } ] + ) + Define.Field ( + "nullablePets", + ListOf (Nullable PetType), + fun _ _ -> [ DogCase { Name = "Odie"; Woofs = true } |> Some; CatCase { Name = "Garfield"; Meows = false } |> Some ] ) ] ) ) @@ -219,6 +322,79 @@ let ``Execute handles execution of abstract types: isTypeOf is used to resolve r empty errors data |> equals (upcast expected) +[] +let ``Execute handles execution of abstract types: not specified Union types produce error`` () = + let query = + """{ + pets { + ... on Dog { + name + woofs + } + } + }""" + + let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query) + ensureRequestError result <| fun [ petsError ] -> + petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ] + + let query = + """{ + pets { + ... on Cat { + name + meows + } + } + }""" + + let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query) + ensureRequestError result <| fun [ petsError ] -> + petsError |> ensureValidationError "Field 'pets' does not allow nulls and list values." [ "pets"; "0" ] + +[] +let ``Execute handles execution of abstract types: not specified Union types must be filtered out`` () = + let query = + """{ + nullablePets { + ... on Dog { + name + woofs + } + } + }""" + + let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query) + + let expected = + NameValueLookup.ofList + [ "nullablePets", upcast [ NameValueLookup.ofList [ "name", "Odie" :> obj; "woofs", upcast true ] :> obj; null ] ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + + let query = + """{ + nullablePets { + ... on Cat { + name + meows + } + } + }""" + + let result = sync <| schemaWithUnion.Value.AsyncExecute (parse query) + + let expected = + NameValueLookup.ofList + [ "nullablePets", + upcast [ null; NameValueLookup.ofList [ "name", "Garfield" :> obj; "meows", upcast false ] :> obj ] ] + + ensureDirect result <| fun data errors -> + empty errors + data |> equals (upcast expected) + [] let ``Execute handles execution of abstract types: absent field resolution produces errors for Union`` () = let query = diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/AsyncValTests.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/AsyncValTests.fs index f96f66c44..6ea2920c5 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/AsyncValTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers and Extensions/AsyncValTests.fs @@ -72,14 +72,14 @@ let ``AsyncVal computation defines zero value`` () = [] let ``AsyncVal can be returned from Async computation`` () = - let a = async { return! asyncVal.Return 1 } + let a = async { return! AsyncVal.wrap 1 } let res = a |> sync res |> equals 1 [] let ``AsyncVal can be bound inside Async computation`` () = let a = async { - let! v = asyncVal.Return 1 + let! v = AsyncVal.wrap 1 return v } let res = a |> sync res |> equals 1