diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bd4f20dd4..718363c9c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -291,7 +291,7 @@ * **Breaking Change** `SubscriptionExecutionResult.Data` is now `obj Skippable`, and the record has new `Path` and `HasNext` fields for incremental delivery * **Breaking Change** `BufferedStreamOptions.Interval` and `BufferedStreamOptions.PreferredBatchSize` are now `int voption` * **Breaking Change** `ServerMessage.Error` and `ServerRawPayload.ErrorMessages` now carry `GQLProblemDetails list` instead of `NameValueLookup list`, so an `error` message's `payload` is a standard GraphQL error array as the `graphql-transport-ws` protocol requires -* **Breaking Change** A query or mutation whose non-null root field fails during execution now produces a `Direct` (execution) result with `null` data instead of a `RequestError`, which is now only ever produced for a request rejected before execution (validation, planning, variable or inline argument coercion, a middleware, or the executor itself failing); HTTP and `graphql-transport-ws` responses for such a failure now carry `data: null` as the spec requires, instead of omitting `data` entirely +* **Breaking Change** A query or mutation whose non-null root field fails during execution now produces a `Direct` (execution) result with `null` data instead of a `RequestError`, which is now only ever produced for a request rejected before execution (validation, planning, variable or inline argument coercion, a middleware, or the executor itself failing); HTTP and `graphql-transport-ws` responses for such a failure now carry `data: null` as the spec requires, instead of omitting `data` entirely. This also changes the public `GQLResponse.Data`, `GQLResponseContent.Direct.Data`, `DeferredErrors.Data`, and `SubscriptionErrors.Data` signatures to use `voption` * Added case-insensitive string comparison support to `ObjectListFilter`, including comparer-aware filter cases and GraphQL filter suffix handling * Improved Relay XML documentation comments * Changed query planning to throw `MalformedGQLQueryException` for invalid queries, `NotSupportedException` for unsupported type definition implementations and `InvalidOperationException` for internal planning errors instead of `System.Exception`, with messages naming the affected field, type and execution kind diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs index 269da17a9..471f6afc7 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs @@ -55,9 +55,9 @@ and [] GraphQLRequestHandler<'Root> logger.LogDebug ("Produced direct GraphQL response with documentId = '{documentId}' and metadata:\n{metadata}", documentId, metadata) if logger.IsEnabled LogLevel.Trace then - logger.LogTrace ("GraphQL response data:\n{data}", serializeIndented data) + logger.LogTrace ("GraphQL response data:\n{data}", serializeIndented (data |> ValueOption.toObj)) - GQLResponse.Direct (documentId, data, errs) + GQLResponse.Direct (documentId, data |> ValueOption.toObj, errs) | Deferred (data, errs, deferred) -> logger.LogDebug ("Produced deferred GraphQL response with documentId = '{documentId}' and metadata:\n{metadata}", documentId, metadata) @@ -69,12 +69,12 @@ and [] GraphQLRequestHandler<'Root> if logger.IsEnabled LogLevel.Trace then logger.LogTrace ("GraphQL deferred data:\n{data}", serializeIndented data) - | DeferredErrors (null, errors, path) -> + | DeferredErrors (ValueNone, errors, path) -> logger.LogDebug ("Produced GraphQL deferred errors for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join) if logger.IsEnabled LogLevel.Trace then logger.LogTrace ("GraphQL deferred errors:\n{errors}", errors) - | DeferredErrors (data, errors, path) -> + | DeferredErrors (ValueSome data, errors, path) -> logger.LogDebug ( "Produced GraphQL deferred result with errors for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join @@ -96,12 +96,12 @@ and [] GraphQLRequestHandler<'Root> if logger.IsEnabled LogLevel.Trace then logger.LogTrace ("GraphQL subscription data:\n{data}", serializeIndented data) - | SubscriptionErrors (null, errors) -> + | SubscriptionErrors (ValueNone, errors) -> logger.LogDebug ("Produced GraphQL subscription errors") if logger.IsEnabled LogLevel.Trace then logger.LogTrace ("GraphQL subscription errors:\n{errors}", errors) - | SubscriptionErrors (data, errors) -> + | SubscriptionErrors (ValueSome data, errors) -> logger.LogDebug ("Produced GraphQL subscription result with errors") if logger.IsEnabled LogLevel.Trace then diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs index 3f093a9ff..2677f9d4c 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs @@ -246,8 +246,8 @@ type GraphQLWebSocketMiddleware<'Root> logger.LogWarning ("Subscription errors: {subscriptionErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))) // The executor may still have resolved partial data alongside the field errors; forward it as-is match output with - | null -> SubscriptionExecutionResult.CreateErrors errors |> sendOutput id - | output -> SubscriptionExecutionResult.Create (output, errors) |> sendOutput id + | ValueNone -> SubscriptionExecutionResult.CreateErrors errors |> sendOutput id + | ValueSome output -> SubscriptionExecutionResult.Create (output, errors) |> sendOutput id // Incremental payloads are sent as soon as they are produced, with their path inside the initial result, // so a client can merge them. The completion marker becomes a final payload with hasNext set to false. @@ -260,7 +260,7 @@ type GraphQLWebSocketMiddleware<'Root> do! SubscriptionExecutionResult.CreateIncremental (itemData, [], itemPath) |> sendOutput id | ValueSome (DeferredResult (data, path)) -> do! SubscriptionExecutionResult.CreateIncremental (data, [], path) |> sendOutput id - | ValueSome (DeferredErrors (data, errors, BatchPath (fieldPath, indices))) -> + | ValueSome (DeferredErrors (ValueSome data, errors, BatchPath (fieldPath, indices))) -> logger.LogWarning ( "Deferred response errors: {deferredErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) @@ -272,7 +272,7 @@ type GraphQLWebSocketMiddleware<'Root> "Deferred response errors: {deferredErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))) ) - do! SubscriptionExecutionResult.CreateIncremental (data, errors, path) |> sendOutput id + do! SubscriptionExecutionResult.CreateIncremental (data |> ValueOption.toObj, errors, path) |> sendOutput id | ValueNone -> do! SubscriptionExecutionResult.CreateCompleted () |> sendOutput id } @@ -291,7 +291,7 @@ type GraphQLWebSocketMiddleware<'Root> // message below if not errors.IsEmpty then logger.LogWarning ("Execution errors:\n{errors}", errors) - do! SubscriptionExecutionResult.Create (data, errors) |> sendOutput id + do! SubscriptionExecutionResult.Create (data |> ValueOption.toObj, errors) |> sendOutput id // The graphql-transport-ws protocol requires Complete after the single Next of a query or mutation do! sendMsg (Complete id) | RequestError problemDetails -> diff --git a/src/FSharp.Data.GraphQL.Server/Execution.fs b/src/FSharp.Data.GraphQL.Server/Execution.fs index f6bdaa0a7..441dc5d2b 100644 --- a/src/FSharp.Data.GraphQL.Server/Execution.fs +++ b/src/FSharp.Data.GraphQL.Server/Execution.fs @@ -161,10 +161,10 @@ let deferResults path (res : ResolverResult) : IObservable DeferredResult (data, formattedPath) - | _ -> DeferredErrors (data, errs, formattedPath) + | _ -> DeferredErrors (data |> ValueOption.ofObj, errs, formattedPath) |> Observable.singleton Option.foldBack Observable.concat deferred deferredData - | Error errs -> Observable.singleton <| DeferredErrors (null, errs, formattedPath) + | Error errs -> Observable.singleton <| DeferredErrors (ValueNone, errs, formattedPath) /// Collect together an array of results using the appropriate execution strategy. let collectFields (strategy : ExecutionStrategy) (rs : AsyncVal>> []) : AsyncVal []>> = asyncVal { @@ -327,7 +327,7 @@ and private streamed (options : BufferedStreamOptions) (innerDef : OutputDef) (i ||> List.foldBack (fun event struct (items, failures) -> match event with | StreamedItem (index, result) -> struct (index, result) :: items, failures - | StreamFailure error -> items, DeferredErrors (null, resolverError path ctx error, normalizeErrorPath path) :: failures) + | StreamFailure error -> items, DeferredErrors (ValueNone, resolverError path ctx error, normalizeErrorPath path) :: failures) match failures with | [] -> collectItems items | failures -> collectItems items |> Observable.concat (Observable.ofSeq failures) @@ -587,9 +587,9 @@ let private executeSubscription (resultSet: (string * ExecutionInfo) []) (inputC let onValue v = asyncVal { match! executeResolvers inputContext fieldCtx fieldPath value (toOption v |> AsyncVal.wrap) with | Ok (data, None, []) -> return SubscriptionResult (NameValueLookup.ofList [nameOrAlias, data.Value]) - | Ok (data, None, errs) -> return SubscriptionErrors (NameValueLookup.ofList [nameOrAlias, data.Value], errs) + | Ok (data, None, errs) -> return SubscriptionErrors (ValueSome (NameValueLookup.ofList [nameOrAlias, data.Value]), errs) | Ok (_, Some _, _) -> return failwith "Deferred/Streamed/Live are not supported for subscriptions!" - | Error errs -> return SubscriptionErrors (null, errs) + | Error errs -> return SubscriptionErrors (ValueNone, errs) } return ctx.Schema.SubscriptionProvider.Add fieldCtx value subDef diff --git a/src/FSharp.Data.GraphQL.Server/Executor.fs b/src/FSharp.Data.GraphQL.Server/Executor.fs index feee46a77..1ec625b47 100644 --- a/src/FSharp.Data.GraphQL.Server/Executor.fs +++ b/src/FSharp.Data.GraphQL.Server/Executor.fs @@ -104,7 +104,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s let prepareOutput res = match res with | RequestError errs -> GQLExecutionResult.Error (documentId, errs, res.Metadata) - | Direct (data, errors) -> GQLExecutionResult.Direct (documentId, data, errors, res.Metadata) + | Direct (data, errors) -> GQLExecutionResult.Direct (documentId, data |> ValueOption.toObj, errors, res.Metadata) | Deferred (data, errors, deferred) -> GQLExecutionResult.Deferred (documentId, data, errors, deferred, res.Metadata) | Stream (stream) -> GQLExecutionResult.Stream (documentId, stream, res.Metadata) async { diff --git a/src/FSharp.Data.GraphQL.Server/IO.fs b/src/FSharp.Data.GraphQL.Server/IO.fs index e4dc813fc..685a62a9e 100644 --- a/src/FSharp.Data.GraphQL.Server/IO.fs +++ b/src/FSharp.Data.GraphQL.Server/IO.fs @@ -9,83 +9,72 @@ open FSharp.Data.GraphQL.Types type Output = IDictionary -type GQLResponse = - { DocumentId: int - Data : Output Skippable - Errors : GQLProblemDetails list Skippable } - static member Direct(documentId, data, errors) = - { DocumentId = documentId - Data = Include data - Errors = Skippable.ofList errors } - static member Stream(documentId) = - { DocumentId = documentId - Data = Include null - Errors = Skip } - static member RequestError(documentId, errors) = - { DocumentId = documentId - Data = Skip - Errors = Include errors } +type GQLResponse = { + DocumentId : int + Data : Skippable + Errors : Skippable +} with -type GQLExecutionResult = - { DocumentId: int - Content : GQLResponseContent - Metadata : Metadata } - static member Direct(documentId, data, errors, meta) = - { DocumentId = documentId - Content = Direct (data, errors) - Metadata = meta } - static member Deferred(documentId, data, errors, deferred, meta) = - { DocumentId = documentId - Content = Deferred (data, errors, deferred) - Metadata = meta } - static member Stream(documentId, data, meta) = - { DocumentId = documentId - Content = Stream data - Metadata = meta } - static member RequestError(documentId, errors, meta) = - { DocumentId = documentId - Content = RequestError errors - Metadata = meta } - static member Empty(documentId, meta) = - GQLExecutionResult.Direct(documentId, Map.empty, [], meta) - static member Error(documentId, errors, meta) = - GQLExecutionResult.RequestError(documentId, errors, meta) - static member Error(documentId, error, meta) = - GQLExecutionResult.RequestError(documentId, [ error ], meta) - static member Error(documentId, error, meta) = - GQLExecutionResult.RequestError(documentId, [ GQLProblemDetails.OfError error ], meta) - static member Error(documentId, errors, meta) = - GQLExecutionResult.RequestError(documentId, errors |> List.map GQLProblemDetails.OfError, meta) - static member Error(documentId, msg, meta) = - GQLExecutionResult.RequestError(documentId, [ GQLProblemDetails.Create msg ], meta) + static member Direct (documentId, data : Output | null, errors) = { + DocumentId = documentId + Data = Include (Option.ofObj data |> ValueOption.ofOption) + Errors = Skippable.ofList errors + } + static member Stream (documentId) = { DocumentId = documentId; Data = Include ValueNone; Errors = Skip } + static member RequestError (documentId, errors) = { DocumentId = documentId; Data = Skip; Errors = Include errors } - static member ErrorFromException(documentId : int, ex : Exception, meta : Metadata) = - GQLExecutionResult.RequestError(documentId, [ GQLProblemDetails.Create (ex.Message, ex) ], meta) +type GQLExecutionResult = { + DocumentId : int + Content : GQLResponseContent + Metadata : Metadata +} with - static member Invalid(documentId, errors, meta) = - GQLExecutionResult.RequestError(documentId, errors, meta) - static member ErrorAsync(documentId, msg : string, meta) = - AsyncVal.wrap (GQLExecutionResult.Error (documentId, msg, meta)) - static member ErrorAsync(documentId, error : IGQLError, meta) = - AsyncVal.wrap (GQLExecutionResult.Error (documentId, error, meta)) + static member Direct (documentId, data : Output | null, errors, meta) = { + DocumentId = documentId + Content = Direct (Option.ofObj data |> ValueOption.ofOption, errors) + Metadata = meta + } + static member Deferred (documentId, data, errors, deferred, meta) = { + DocumentId = documentId + Content = Deferred (data, errors, deferred) + Metadata = meta + } + static member Stream (documentId, data, meta) = { DocumentId = documentId; Content = Stream data; Metadata = meta } + static member RequestError (documentId, errors, meta) = { DocumentId = documentId; Content = RequestError errors; Metadata = meta } + static member Empty (documentId, meta) = GQLExecutionResult.Direct (documentId, Map.empty, [], meta) + static member Error (documentId, errors, meta) = GQLExecutionResult.RequestError (documentId, errors, meta) + static member Error (documentId, error, meta) = GQLExecutionResult.RequestError (documentId, [ error ], meta) + static member Error (documentId, error, meta) = + GQLExecutionResult.RequestError (documentId, [ GQLProblemDetails.OfError error ], meta) + static member Error (documentId, errors, meta) = + GQLExecutionResult.RequestError (documentId, errors |> List.map GQLProblemDetails.OfError, meta) + static member Error (documentId, msg, meta) = + GQLExecutionResult.RequestError (documentId, [ GQLProblemDetails.Create msg ], meta) + + static member ErrorFromException (documentId : int, ex : Exception, meta : Metadata) = + GQLExecutionResult.RequestError (documentId, [ GQLProblemDetails.Create (ex.Message, ex) ], meta) + + static member Invalid (documentId, errors, meta) = GQLExecutionResult.RequestError (documentId, errors, meta) + static member ErrorAsync (documentId, msg : string, meta) = AsyncVal.wrap (GQLExecutionResult.Error (documentId, msg, meta)) + static member ErrorAsync (documentId, error : IGQLError, meta) = AsyncVal.wrap (GQLExecutionResult.Error (documentId, error, meta)) // TODO: Rename to PascalCase and GQLResponseContent = /// The request was rejected before execution started: validation, planning, variable or inline argument /// coercion, a middleware, or the executor itself failing. There is no data, unlike a Direct result whose /// data happens to be null. - | RequestError of Errors: GQLProblemDetails list + | RequestError of Errors : GQLProblemDetails list /// An execution result. Data is null when a non-null root field failed during execution and the error /// propagated to the root, exactly as it would for a non-null nested field, rather than being rejected as a /// RequestError. - | Direct of Data : Output * Errors: GQLProblemDetails list + | Direct of Data : Output voption * Errors : GQLProblemDetails list | Deferred of Data : Output * Errors : GQLProblemDetails list * Defer : IObservable | Stream of Stream : IObservable and GQLDeferredResponseContent = | DeferredResult of Data : obj * Path : FieldPath - | DeferredErrors of Data : obj * Errors: GQLProblemDetails list * Path : FieldPath + | DeferredErrors of Data : obj voption * Errors : GQLProblemDetails list * Path : FieldPath and GQLSubscriptionResponseContent = | SubscriptionResult of Data : Output - | SubscriptionErrors of Data : Output * Errors: GQLProblemDetails list + | SubscriptionErrors of Data : Output voption * Errors : GQLProblemDetails list diff --git a/tests/FSharp.Data.GraphQL.Tests/DeferredTests.fs b/tests/FSharp.Data.GraphQL.Tests/DeferredTests.fs index 22e27901b..1b9136d4a 100644 --- a/tests/FSharp.Data.GraphQL.Tests/DeferredTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/DeferredTests.fs @@ -275,7 +275,7 @@ let ``Resolver error`` () = ] let expectedDeferred = DeferredErrors ( - null, + ValueNone, [ GQLProblemDetails.CreateWithKind ("Resolver error!", Execution, [ box "testData"; "resolverError"; "value" ]) ], [ "testData"; "resolverError" ] ) @@ -304,13 +304,13 @@ let ``Resolver list error`` () = ] let expectedDeferred1 = DeferredErrors ( - null, + ValueNone, [ GQLProblemDetails.CreateWithKind ("Resolver error!", Execution, [ box "testData"; "resolverListError"; 0; "value" ]) ], [ box "testData"; "resolverListError"; 0 ] ) let expectedDeferred2 = DeferredErrors ( - null, + ValueNone, [ GQLProblemDetails.CreateWithKind ("Resolver error!", Execution, [ box "testData"; "resolverListError"; 1; "value" ]) ], [ box "testData"; "resolverListError"; 1 ] ) @@ -343,7 +343,7 @@ let ``Nullable error`` () = ] let expectedDeferred = DeferredErrors ( - null, + ValueNone, [ GQLProblemDetails.CreateWithKind ("Non-Null field value resolved as a null!", Execution, [ box "testData"; "nullableError"; "value" ]) ], [ "testData"; "nullableError" ] ) diff --git a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs index 2b8a7407b..9231f0780 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ErrorHelpers.fs @@ -18,7 +18,14 @@ let ensureDeferred (result : GQLExecutionResult) (onDeferred : Output -> GQLProb let ensureDirect (result : GQLExecutionResult) (onDirect : Output -> GQLProblemDetails list -> unit) : unit = match result.Content with - | Direct(data, errors) -> onDirect data errors + | Direct (ValueSome data, errors) -> onDirect data errors + | Direct (ValueNone, _) -> fail "Expected a 'Direct' GQLResponse with data but got null data" + | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" + +let ensureDirectNullData (result : GQLExecutionResult) (onDirect : GQLProblemDetails list -> unit) : unit = + match result.Content with + | Direct (ValueNone, errors) -> onDirect errors + | Direct (ValueSome _, _) -> fail "Expected a 'Direct' GQLResponse with null data but got data" | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" let ensureRequestError (result : GQLExecutionResult) (onRequestError : GQLProblemDetails list -> unit) : unit = diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs index a4f43acab..40c61ed2b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs @@ -452,8 +452,7 @@ let ``Execution handles errors: exceptions`` () = ])) let expectedError = GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "a" ]) let result = sync <| Executor(schema).AsyncExecute("query Test { a }", getMockInputContext, ()) - ensureDirect result <| fun data [ error ] -> - Assert.Null data + ensureDirectNullData result <| fun [ error ] -> error |> equals expectedError type CoercionGuardInput = { Country : string } @@ -603,8 +602,7 @@ let ``Execution handles errors: additional error added when exception is rised i let result = let variables = { Inner = { Kaboom = "Yes, Rico, Kaboom" }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } } sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables) - ensureDirect result <| fun data errors -> - Assert.Null data + ensureDirectNullData result <| fun errors -> result.DocumentId |> notEquals Unchecked.defaultof errors |> equals expectedErrors @@ -632,7 +630,6 @@ let ``Execution handles errors: additional error added and when null returned fr let result = let variables = { Inner = { Kaboom = "Yes, Rico, Kaboom" }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } } sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables) - ensureDirect result <| fun data errors -> - Assert.Null data + ensureDirectNullData result <| fun errors -> result.DocumentId |> notEquals Unchecked.defaultof errors |> equals expectedErrors diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs index 0039c3cbc..77aece82b 100644 --- a/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/ExecutorMiddlewareTests.fs @@ -117,9 +117,10 @@ let ``Executor middleware: change fields and measure planning time`` () = "b", upcast "Banana" "d", upcast false ] ] match result with - | Direct (data, errors) -> + | Direct (ValueSome data, errors) -> empty errors data |> equals (upcast expected) + | Direct (ValueNone, _) -> fail "Expected Direct GQLResponse with data" | _ -> fail "Expected Direct GQLResponse" match result.Metadata.TryFind("planningTime") with | ValueSome time -> time |> greaterThanOrEqual 5L diff --git a/tests/FSharp.Data.GraphQL.Tests/LazyEnumerationExceptionTests.fs b/tests/FSharp.Data.GraphQL.Tests/LazyEnumerationExceptionTests.fs index e0b77836e..f2579113c 100644 --- a/tests/FSharp.Data.GraphQL.Tests/LazyEnumerationExceptionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/LazyEnumerationExceptionTests.fs @@ -117,8 +117,7 @@ let ``Execution must propagate error when non-nullable list field throws during ])) let expectedError = GQLProblemDetails.CreateWithKind ("Boom during enumeration", Execution, [ box "tags" ]) let result = sync <| Executor(schema).AsyncExecute(parse "{ tags }", getMockInputContext, ()) - ensureDirect result <| fun data [ error ] -> - Assert.Null data + ensureDirectNullData result <| fun [ error ] -> error |> equals expectedError [] diff --git a/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs b/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs index 1f1f16a23..2187291cd 100644 --- a/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/MutationTests.fs @@ -74,9 +74,10 @@ let ``Execute handles mutation execution ordering: evaluates mutations serially` "fifth", upcast NameValueLookup.ofList [ "theNumber", 5 :> obj] ] match mutationResult with - | Direct(data, errors) -> + | Direct(ValueSome data, errors) -> empty errors data |> equals (upcast expected) + | Direct(ValueNone, _) -> fail "Expected a 'Direct' GQLResponse with data but got null data" | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" [] @@ -115,9 +116,10 @@ let ``Execute handles mutation execution ordering: evaluates mutations correctly ] match mutationResult with - | Direct(data, errors) -> + | Direct(ValueSome data, errors) -> data |> equals (upcast expected) List.length errors |> equals 2 + | Direct(ValueNone, _) -> fail "Expected a 'Direct' GQLResponse with data but got null data" | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}" //[] diff --git a/tests/FSharp.Data.GraphQL.Tests/Relay/ConnectionTests.fs b/tests/FSharp.Data.GraphQL.Tests/Relay/ConnectionTests.fs index 769e57513..a0d642a92 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Relay/ConnectionTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Relay/ConnectionTests.fs @@ -155,9 +155,10 @@ let ``Connection definition includes connection and edge fields for simple cases ] ] match result with - | Direct (data, errors) -> + | Direct (ValueSome data, errors) -> empty errors data |> equals (upcast expected) + | Direct (ValueNone, _) -> fail "Expected a Direct GQLResponse with data" | _ -> fail "Expected a Direct GQLResponse" [] @@ -210,9 +211,10 @@ let ``Connection definition includes connection and edge fields for complex case ] ] match result with - | Direct (data, errors) -> + | Direct (ValueSome data, errors) -> empty errors data |> equals (upcast expected) + | Direct (ValueNone, _) -> fail "Expected a Direct GQLResponse with data" | _ -> fail "Expected a Direct GQLResponse" diff --git a/tests/FSharp.Data.GraphQL.Tests/TaskSeqFieldTests.fs b/tests/FSharp.Data.GraphQL.Tests/TaskSeqFieldTests.fs index e0e282e30..ef1115f29 100644 --- a/tests/FSharp.Data.GraphQL.Tests/TaskSeqFieldTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/TaskSeqFieldTests.fs @@ -398,9 +398,8 @@ let ``Non-nullable TaskSeq field that fails during enumeration propagates the er let executor = executorFor [ Define.TaskSeqField ("numbers", ListOf IntType, fun _ _ -> failingNumbers ()) ] let result = executeQuery executor "{ numbers }" - ensureDirect result - <| fun data errors -> - Assert.Null data + ensureDirectNullData result + <| fun errors -> errors |> single |> equals (fieldError "Boom during enumeration" "numbers") @@ -423,7 +422,7 @@ let ``Streamed TaskSeq field that fails during enumeration delivers produced ite |> seqEquals [ streamedBatch "failing" [ 0, 1 ] streamedBatch "failing" [ 1, 2 ] - DeferredErrors (null, [ fieldError "Boom during enumeration" "failing" ], [ box "failing" ]) + DeferredErrors (ValueNone, [ fieldError "Boom during enumeration" "failing" ], [ box "failing" ]) ] [] @@ -443,7 +442,9 @@ let ``Streamed TaskSeq field that fails acquiring the enumerator still delivers empty errors data |> equals (upcast expectedData) waitForCompletion deferred - |> seqEquals [ DeferredErrors (null, [ fieldError "Boom acquiring the enumerator" "failing" ], [ box "failing" ]) ] + |> seqEquals [ + DeferredErrors (ValueNone, [ fieldError "Boom acquiring the enumerator" "failing" ], [ box "failing" ]) + ] [] let ``Streamed TaskSeq field emits a slower earlier item before the enumeration failure that follows it`` () = @@ -460,7 +461,7 @@ let ``Streamed TaskSeq field emits a slower earlier item before the enumeration waitForCompletion deferred |> seqEquals [ DeferredResult ([| box (NameValueLookup.ofList [ "id", upcast 1; "value", upcast "slow" ]) |], [ box "items"; box 0 ]) - DeferredErrors (null, [ fieldError "Boom during enumeration" "items" ], [ box "items" ]) + DeferredErrors (ValueNone, [ fieldError "Boom during enumeration" "items" ], [ box "items" ]) ] [] @@ -481,8 +482,10 @@ let ``Streamed TaskSeq field delivers an item's own resolver error and keeps str waitForCompletion deferred |> seqEquals [ DeferredErrors ( - null, - [ GQLProblemDetails.CreateWithKind ("Boom resolving the item", Execution, [ box "items"; box 0; box "value" ]) ], + ValueNone, + [ + GQLProblemDetails.CreateWithKind ("Boom resolving the item", Execution, [ box "items"; box 0; box "value" ]) + ], [ box "items"; box 0 ] ) DeferredResult ([| box (NameValueLookup.ofList [ "id", upcast 2; "value", upcast "two" ]) |], [ box "items"; box 1 ]) @@ -507,8 +510,10 @@ let ``A batch containing a failed item alongside a succeeding one is delivered a waitForCompletion deferred |> seqEquals [ DeferredErrors ( - [| null; box (NameValueLookup.ofList [ "id", upcast 2; "value", upcast "two" ]) |], - [ GQLProblemDetails.CreateWithKind ("Boom resolving item 0", Execution, [ box "items"; box 0; box "value" ]) ], + ValueSome [| null; box (NameValueLookup.ofList [ "id", upcast 2; "value", upcast "two" ]) |], + [ + GQLProblemDetails.CreateWithKind ("Boom resolving item 0", Execution, [ box "items"; box 0; box "value" ]) + ], [ box "items"; box [ box 0; box 1 ] ] ) ] @@ -578,7 +583,5 @@ let ``TaskSeq field resolved as null reports a non-null field error`` () = Define.TaskSeqField ("numbers", ListOf IntType, fun _ _ -> Unchecked.defaultof>) ] let result = executeQuery executor "{ numbers }" - ensureDirect result - <| fun data errors -> - Assert.Null data - hasError "Non-Null field numbers resolved as a null!" errors + ensureDirectNullData result + <| fun errors -> hasError "Non-Null field numbers resolved as a null!" errors