diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj index c12ce2e62..81c566f15 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequest.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequest.fs new file mode 100644 index 000000000..ec09ffd2a --- /dev/null +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequest.fs @@ -0,0 +1,252 @@ +namespace FSharp.Data.GraphQL.Server.AspNetCore + +open System +open System.IO +open System.Text.Json +open System.Text.Json.Serialization +open System.Threading.Tasks +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.Logging +open Microsoft.Extensions.Options + +open FSharp.Data.GraphQL +open FsToolkit.ErrorHandling + +open FSharp.Data.GraphQL.Server + +/// Provides logic to parse and execute GraphQL request +type GraphQLRequest<'Root> ( + httpContextAccessor : IHttpContextAccessor, + options : IOptionsMonitor>, + logger : ILogger> +) = + + let ctx = httpContextAccessor.HttpContext + + let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = + + let serializeIndented value = + let jsonSerializerOptions = options.Get(GraphQLOptions.IndentedOptionsName).SerializerOptions + JsonSerializer.Serialize(value, jsonSerializerOptions) + + match content with + | Direct(data, errs) -> + 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) + + GQLResponse.Direct(documentId, data, errs) + | Deferred(data, errs, deferred) -> + logger.LogDebug( + $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", + documentId, + metadata + ) + + if logger.IsEnabled LogLevel.Debug then + deferred + |> Observable.add (function + | DeferredResult(data, path) -> + logger.LogDebug( + "Produced GraphQL deferred result for path: {path}", + path |> Seq.map string |> Seq.toArray |> Path.Join + ) + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL deferred data:\n{{data}}", + serializeIndented data + ) + | DeferredErrors(null, 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) -> + logger.LogDebug( + "Produced GraphQL deferred result with 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}}\nGraphQL deferred data:\n{{data}}", + errors, + serializeIndented data + )) + + GQLResponse.Direct(documentId, data, errs) + | Stream stream -> + logger.LogDebug( + $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", + documentId, + metadata + ) + + if logger.IsEnabled LogLevel.Debug then + stream + |> Observable.add (function + | SubscriptionResult data -> + logger.LogDebug("Produced GraphQL subscription result") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL subscription data:\n{{data}}", + serializeIndented data + ) + | SubscriptionErrors(null, errors) -> + logger.LogDebug("Produced GraphQL subscription errors") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace($"GraphQL subscription errors:\n{{errors}}", errors) + | SubscriptionErrors(data, errors) -> + logger.LogDebug("Produced GraphQL subscription result with errors") + + if logger.IsEnabled LogLevel.Trace then + logger.LogTrace( + $"GraphQL subscription errors:\n{{errors}}\nGraphQL deferred data:\n{{data}}", + errors, + serializeIndented data + )) + + GQLResponse.Stream documentId + | RequestError errs -> + logger.LogWarning( + $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", + documentId, + metadata + ) + + GQLResponse.RequestError(documentId, errs) + + /// Checks if the request contains a body + let checkIfHasBody (request: HttpRequest) = task { + if request.Body.CanSeek then + return (request.Body.Length > 0L) + else + request.EnableBuffering() + let body = request.Body + let buffer = Array.zeroCreate 1 + let! bytesRead = body.ReadAsync(buffer, 0, 1) + body.Seek(0, SeekOrigin.Begin) |> ignore + return bytesRead > 0 + } + + /// Execute default or custom introspection query + let executeIntrospectionQuery (executor: Executor<_>) (ast: Ast.Document voption) : Task = task { + let! result = + match ast with + | ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition + | ValueSome ast -> executor.AsyncExecute ast + + let response = result |> toResponse + return (TypedResults.Ok response) :> IResult + } + + /// Check if the request is an introspection query + /// by first checking on such properties as `GET` method or `empty request body` + /// and lastly by parsing document AST for introspection operation definition. + /// + /// Result of check of + let checkOperationType () = taskResult { + + let checkAnonymousFieldsOnly (ctx: HttpContext) = taskResult { + let! gqlRequest = ctx.TryBindJsonAsync(GQLRequestContent.expectedJSON) + let! ast = Parser.parseOrIResult ctx.Request.Path.Value gqlRequest.Query + let operationName = gqlRequest.OperationName |> Skippable.toOption + + let createParsedContent() = { + Query = gqlRequest.Query + Ast = ast + OperationName = gqlRequest.OperationName + Variables = gqlRequest.Variables + } + if ast.IsEmpty then + logger.LogTrace( + "Request is not GET, but 'query' field is an empty string. Must be an introspection query" + ) + return IntrospectionQuery <| ValueNone + else + match Ast.tryFindOperationByName operationName ast with + | None -> + logger.LogTrace "Document has no operation" + return IntrospectionQuery <| ValueNone + | Some op -> + if not (op.OperationType = Ast.Query) then + logger.LogTrace "Document operation is not of type Query" + return createParsedContent () |> OperationQuery + else + let hasNonMetaFields = + Ast.containsFieldsBeyond + Ast.metaTypeFields + (fun x -> + logger.LogTrace($"Operation Selection in Field with name: {{fieldName}}", x.Name)) + (fun _ -> logger.LogTrace "Operation Selection is non-Field type") + op + + if hasNonMetaFields then + return createParsedContent() |> OperationQuery + else + return IntrospectionQuery <| ValueSome ast + } + + let request = ctx.Request + + if HttpMethods.Get = request.Method then + logger.LogTrace("Request is GET. Must be an introspection query") + return IntrospectionQuery <| ValueNone + else + let! hasBody = checkIfHasBody request + + if not hasBody then + logger.LogTrace("Request is not GET, but has no body. Must be an introspection query") + return IntrospectionQuery <| ValueNone + else + return! checkAnonymousFieldsOnly ctx + } + + abstract ExecuteOperation<'Root> : Executor<'Root> -> ParsedGQLQueryRequestContent -> Task + + /// Execute the operation for given request + default _.ExecuteOperation<'Root> (executor: Executor<'Root>) content = task { + + let operationName = content.OperationName |> Skippable.filter (not << isNull) |> Skippable.toOption + let variables = content.Variables |> Skippable.filter (not << isNull) |> Skippable.toOption + + operationName + |> Option.iter (fun on -> logger.LogTrace("GraphQL operation name: '{operationName}'", on)) + + logger.LogTrace($"Executing GraphQL query:\n{{query}}", content.Query) + + variables + |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:\n{{variables}}", v)) + + let root = options.CurrentValue.RootFactory ctx + + let! result = + Async.StartAsTask( + executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName), + cancellationToken = ctx.RequestAborted + ) + + let response = result |> toResponse + return (TypedResults.Ok response) :> IResult + } + + member request.HandleAsync () : Task> = taskResult { + if ctx.RequestAborted.IsCancellationRequested then + return TypedResults.Empty + else + let executor = options.CurrentValue.SchemaExecutor + match! checkOperationType () with + | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument + | OperationQuery content -> return! request.ExecuteOperation executor content + } diff --git a/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs index b349aa96f..ebad0e0d0 100644 --- a/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs @@ -1,21 +1,12 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore.Giraffe -open System -open System.IO -open System.Text.Json -open System.Text.Json.Serialization open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Logging -open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open Giraffe -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Ast -open FSharp.Data.GraphQL.Server open FSharp.Data.GraphQL.Server.AspNetCore type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult @@ -36,236 +27,8 @@ module HttpHandlers = |> ofTaskIResult ctx let private handleGraphQL<'Root> (next : HttpFunc) (ctx : HttpContext) = - let sp = ctx.RequestServices - let logger = sp.CreateLogger moduleType - - let options = sp.GetRequiredService>> () - - let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = - - let serializeIndented value = - let jsonSerializerOptions = options.Get(GraphQLOptions.IndentedOptionsName).SerializerOptions - JsonSerializer.Serialize (value, jsonSerializerOptions) - - match content with - | Direct (data, errs) -> - 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) - - GQLResponse.Direct (documentId, data, errs) - | Deferred (data, errs, deferred) -> - logger.LogDebug ( - $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", - documentId, - metadata - ) - - if logger.IsEnabled LogLevel.Debug then - deferred - |> Observable.add (function - | DeferredResult (data, path) -> - logger.LogDebug ("Produced GraphQL deferred result for path: {path}", path |> Seq.map string |> Seq.toArray |> Path.Join) - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace ($"GraphQL deferred data:\n{{data}}", serializeIndented data) - | DeferredErrors (null, 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) -> - logger.LogDebug ( - "Produced GraphQL deferred result with 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}}\nGraphQL deferred data:\n{{data}}", - errors, - serializeIndented data - )) - - GQLResponse.Direct (documentId, data, errs) - | Stream stream -> - logger.LogDebug ( - $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", - documentId, - metadata - ) - - if logger.IsEnabled LogLevel.Debug then - stream - |> Observable.add (function - | SubscriptionResult data -> - logger.LogDebug ("Produced GraphQL subscription result") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace ($"GraphQL subscription data:\n{{data}}", serializeIndented data) - | SubscriptionErrors (null, errors) -> - logger.LogDebug ("Produced GraphQL subscription errors") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace ($"GraphQL subscription errors:\n{{errors}}", errors) - | SubscriptionErrors (data, errors) -> - logger.LogDebug ("Produced GraphQL subscription result with errors") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace ( - $"GraphQL subscription errors:\n{{errors}}\nGraphQL deferred data:\n{{data}}", - errors, - serializeIndented data - )) - - GQLResponse.Stream documentId - | RequestError errs -> - let noExceptionsFound = - errs - |> Seq.map - (fun x -> - x.Exception |> ValueOption.iter (fun ex -> - logger.LogError (ex, "Error while processing request that generated response with documentId '{documentId}'", documentId) - ) - x.Exception.IsNone - ) - |> Seq.forall id - if noExceptionsFound then - logger.LogWarning ( - ("Produced request error GraphQL response with:\n" - + "- documentId: '{documentId}'\n" - + "- error(s):\n {requestError}\n" - + "- metadata:\n {metadata}\n"), - documentId, - errs, - metadata - ) - - GQLResponse.RequestError (documentId, errs) - - /// Checks if the request contains a body - let checkIfHasBody (request : HttpRequest) = task { - if request.Body.CanSeek then - return (request.Body.Length > 0L) - else - request.EnableBuffering () - let body = request.Body - let buffer = Array.zeroCreate 1 - let! bytesRead = body.ReadAsync (buffer, 0, 1) - body.Seek (0, SeekOrigin.Begin) |> ignore - return bytesRead > 0 - } - - /// Check if the request is an introspection query - /// by first checking on such properties as `GET` method or `empty request body` - /// and lastly by parsing document AST for introspection operation definition. - /// - /// Result of check of - let checkOperationType (ctx : HttpContext) = taskResult { - - let checkAnonymousFieldsOnly (ctx : HttpContext) = taskResult { - let! gqlRequest = ctx.TryBindJsonAsync (GQLRequestContent.expectedJSON) - let! ast = Parser.parseOrIResult ctx.Request.Path.Value gqlRequest.Query - let operationName = gqlRequest.OperationName |> Skippable.toOption - - let createParsedContent () = { - Query = gqlRequest.Query - Ast = ast - OperationName = gqlRequest.OperationName - Variables = gqlRequest.Variables - } - if ast.IsEmpty then - logger.LogTrace ("Request is not GET, but 'query' field is an empty string. Must be an introspection query") - return IntrospectionQuery <| ValueNone - else - match Ast.tryFindOperationByName operationName ast with - | None -> - logger.LogTrace "Document has no operation" - return IntrospectionQuery <| ValueNone - | Some op -> - if not (op.OperationType = Ast.Query) then - logger.LogTrace "Document operation is not of type Query" - return createParsedContent () |> OperationQuery - else - let hasNonMetaFields = - Ast.containsFieldsBeyond - Ast.metaTypeFields - (fun x -> logger.LogTrace ($"Operation Selection in Field with name: {{fieldName}}", x.Name)) - (fun _ -> logger.LogTrace "Operation Selection is non-Field type") - op - - if hasNonMetaFields then - return createParsedContent () |> OperationQuery - else - return IntrospectionQuery <| ValueSome ast - } - - let request = ctx.Request - - if HttpMethods.Get = request.Method then - logger.LogTrace ("Request is GET. Must be an introspection query") - return IntrospectionQuery <| ValueNone - else - let! hasBody = checkIfHasBody request - - if not hasBody then - logger.LogTrace ("Request is not GET, but has no body. Must be an introspection query") - return IntrospectionQuery <| ValueNone - else - return! checkAnonymousFieldsOnly ctx - } - - /// Execute default or custom introspection query - let executeIntrospectionQuery (executor : Executor<_>) (ast : Ast.Document voption) = task { - let! result = - match ast with - | ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition - | ValueSome ast -> executor.AsyncExecute ast - - let response = result |> toResponse - return Results.Ok response - } - - /// Execute the operation for given request - let executeOperation (executor: Executor<_>) content = task { - let operationName = content.OperationName |> Skippable.filter (not << isNull) |> Skippable.toOption - let variables = content.Variables |> Skippable.filter (not << isNull) |> Skippable.toOption - - operationName - |> Option.iter (fun on -> logger.LogTrace ("GraphQL operation name: '{operationName}'", on)) - - logger.LogTrace ($"Executing GraphQL query:\n{{query}}", content.Query) - - variables - |> Option.iter (fun v -> logger.LogTrace ($"GraphQL variables:\n{{variables}}", v)) - - let root = options.CurrentValue.RootFactory ctx - - let! result = - Async.StartAsTask ( - executor.AsyncExecute (content.Ast, root, ?variables = variables, ?operationName = operationName), - cancellationToken = ctx.RequestAborted - ) - - let response = result |> toResponse - return Results.Ok response - } - - if ctx.RequestAborted.IsCancellationRequested then - Task.FromResult None - else - taskResult { - let executor = options.CurrentValue.SchemaExecutor - match! checkOperationType ctx with - | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument - | OperationQuery content -> return! executeOperation executor content - } - |> ofTaskIResult2 ctx + let request = ctx.RequestServices.GetRequiredService>() + request.HandleAsync () |> ofTaskIResult2 ctx let graphQL<'Root> : HttpHandler = choose [ POST; GET ] >=> handleGraphQL<'Root> diff --git a/src/FSharp.Data.GraphQL.Server.Oxpecker/HttpEndpoints.fs b/src/FSharp.Data.GraphQL.Server.Oxpecker/HttpEndpoints.fs index 04ee52b6d..5d0b98060 100644 --- a/src/FSharp.Data.GraphQL.Server.Oxpecker/HttpEndpoints.fs +++ b/src/FSharp.Data.GraphQL.Server.Oxpecker/HttpEndpoints.fs @@ -1,264 +1,25 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore.Giraffe -open System -open System.IO open System.Runtime.InteropServices -open System.Text.Json -open System.Text.Json.Serialization open System.Threading.Tasks open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Logging -open Microsoft.Extensions.Options open FsToolkit.ErrorHandling open Oxpecker -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Ast -open FSharp.Data.GraphQL.Server open FSharp.Data.GraphQL.Server.AspNetCore module HttpEndpoints = - let rec private moduleType = getModuleType <@ moduleType @> - let internal writeIResult2 (ctx : HttpContext) (taskRes: Task>) : Task = task { let! result = taskRes |> TaskResult.defaultWith id do! ctx.Write result } let private handleGraphQL<'Root> (ctx : HttpContext) : Task = - let sp = ctx.RequestServices - - let logger = sp.CreateLogger moduleType - - let options = sp.GetRequiredService>>() - - let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } = - - let serializeIndented value = - let jsonSerializerOptions = options.Get(GraphQLOptions.IndentedOptionsName).SerializerOptions - JsonSerializer.Serialize(value, jsonSerializerOptions) - - match content with - | Direct(data, errs) -> - 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) - - GQLResponse.Direct(documentId, data, errs) - | Deferred(data, errs, deferred) -> - logger.LogDebug( - $"Produced deferred GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", - documentId, - metadata - ) - - if logger.IsEnabled LogLevel.Debug then - deferred - |> Observable.add (function - | DeferredResult(data, path) -> - logger.LogDebug( - "Produced GraphQL deferred result for path: {path}", - path |> Seq.map string |> Seq.toArray |> Path.Join - ) - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace( - $"GraphQL deferred data:\n{{data}}", - serializeIndented data - ) - | DeferredErrors(null, 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) -> - logger.LogDebug( - "Produced GraphQL deferred result with 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}}\nGraphQL deferred data:\n{{data}}", - errors, - serializeIndented data - )) - - GQLResponse.Direct(documentId, data, errs) - | Stream stream -> - logger.LogDebug( - $"Produced stream GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", - documentId, - metadata - ) - - if logger.IsEnabled LogLevel.Debug then - stream - |> Observable.add (function - | SubscriptionResult data -> - logger.LogDebug("Produced GraphQL subscription result") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace( - $"GraphQL subscription data:\n{{data}}", - serializeIndented data - ) - | SubscriptionErrors(null, errors) -> - logger.LogDebug("Produced GraphQL subscription errors") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace($"GraphQL subscription errors:\n{{errors}}", errors) - | SubscriptionErrors(data, errors) -> - logger.LogDebug("Produced GraphQL subscription result with errors") - - if logger.IsEnabled LogLevel.Trace then - logger.LogTrace( - $"GraphQL subscription errors:\n{{errors}}\nGraphQL deferred data:\n{{data}}", - errors, - serializeIndented data - )) - - GQLResponse.Stream documentId - | RequestError errs -> - logger.LogWarning( - $"Produced request error GraphQL response with documentId = '{{documentId}}' and metadata:\n{{metadata}}", - documentId, - metadata - ) - - GQLResponse.RequestError(documentId, errs) - - /// Checks if the request contains a body - let checkIfHasBody (request: HttpRequest) = task { - if request.Body.CanSeek then - return (request.Body.Length > 0L) - else - request.EnableBuffering() - let body = request.Body - let buffer = Array.zeroCreate 1 - let! bytesRead = body.ReadAsync(buffer, 0, 1) - body.Seek(0, SeekOrigin.Begin) |> ignore - return bytesRead > 0 - } - - /// Check if the request is an introspection query - /// by first checking on such properties as `GET` method or `empty request body` - /// and lastly by parsing document AST for introspection operation definition. - /// - /// Result of check of - let checkOperationType (ctx: HttpContext) = taskResult { - - let checkAnonymousFieldsOnly (ctx: HttpContext) = taskResult { - let! gqlRequest = ctx.TryBindJsonAsync(GQLRequestContent.expectedJSON) - let! ast = Parser.parseOrIResult ctx.Request.Path.Value gqlRequest.Query - let operationName = gqlRequest.OperationName |> Skippable.toOption - - let createParsedContent() = { - Query = gqlRequest.Query - Ast = ast - OperationName = gqlRequest.OperationName - Variables = gqlRequest.Variables - } - if ast.IsEmpty then - logger.LogTrace( - "Request is not GET, but 'query' field is an empty string. Must be an introspection query" - ) - return IntrospectionQuery <| ValueNone - else - match Ast.tryFindOperationByName operationName ast with - | None -> - logger.LogTrace "Document has no operation" - return IntrospectionQuery <| ValueNone - | Some op -> - if not (op.OperationType = Ast.Query) then - logger.LogTrace "Document operation is not of type Query" - return createParsedContent () |> OperationQuery - else - let hasNonMetaFields = - Ast.containsFieldsBeyond - Ast.metaTypeFields - (fun x -> - logger.LogTrace($"Operation Selection in Field with name: {{fieldName}}", x.Name)) - (fun _ -> logger.LogTrace "Operation Selection is non-Field type") - op - - if hasNonMetaFields then - return createParsedContent() |> OperationQuery - else - return IntrospectionQuery <| ValueSome ast - } - - let request = ctx.Request - - if HttpMethods.Get = request.Method then - logger.LogTrace("Request is GET. Must be an introspection query") - return IntrospectionQuery <| ValueNone - else - let! hasBody = checkIfHasBody request - - if not hasBody then - logger.LogTrace("Request is not GET, but has no body. Must be an introspection query") - return IntrospectionQuery <| ValueNone - else - return! checkAnonymousFieldsOnly ctx - } - - /// Execute default or custom introspection query - let executeIntrospectionQuery (executor: Executor<_>) (ast: Ast.Document voption) = task { - let! result = - match ast with - | ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition - | ValueSome ast -> executor.AsyncExecute ast - - let response = result |> toResponse - return Results.Ok response - } - - /// Execute the operation for given request - let executeOperation (executor: Executor<_>) content = task { - let operationName = content.OperationName |> Skippable.filter (not << isNull) |> Skippable.toOption - let variables = content.Variables |> Skippable.filter (not << isNull) |> Skippable.toOption - - operationName - |> Option.iter (fun on -> logger.LogTrace("GraphQL operation name: '{operationName}'", on)) - - logger.LogTrace($"Executing GraphQL query:\n{{query}}", content.Query) - - variables - |> Option.iter (fun v -> logger.LogTrace($"GraphQL variables:\n{{variables}}", v)) - - let root = options.CurrentValue.RootFactory ctx - - let! result = - Async.StartAsTask( - executor.AsyncExecute(content.Ast, root, ?variables = variables, ?operationName = operationName), - cancellationToken = ctx.RequestAborted - ) - - let response = result |> toResponse - return Results.Ok response - } - if ctx.RequestAborted.IsCancellationRequested then - Task.CompletedTask - else - taskResult { - let executor = options.CurrentValue.SchemaExecutor - match! checkOperationType ctx with - | IntrospectionQuery optionalAstDocument -> return! executeIntrospectionQuery executor optionalAstDocument - | OperationQuery content -> return! executeOperation executor content - } - |> writeIResult2 ctx + let request = ctx.RequestServices.GetRequiredService>() + request.HandleAsync () |> writeIResult2 ctx let graphQL<'Root> (route, [] configure) : Endpoint = SimpleEndpoint(Verbs [HttpVerb.GET; HttpVerb.POST], route, handleGraphQL<'Root>, configure) diff --git a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj index bfb262467..3ad275cc7 100644 --- a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj +++ b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj @@ -24,10 +24,7 @@ - <_Parameter1>FSharp.Data.GraphQL.Server.Giraffe - - - <_Parameter1>FSharp.Data.GraphQL.Server.Oxpecker + <_Parameter1>FSharp.Data.GraphQL.Server.AspNetCore