Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,28 @@ open FSharp.Data.GraphQL.Shared

type DefaultGraphQLRequestHandler<'Root>
(
Comment thread
ePasha marked this conversation as resolved.
/// The accessor to the current HTTP context
// The accessor to the current HTTP context
Comment thread
ePasha marked this conversation as resolved.
Outdated
httpContextAccessor : IHttpContextAccessor,
/// The options monitor for GraphQL options
// The options monitor for GraphQL options
Comment thread
ePasha marked this conversation as resolved.
Outdated
options : IOptionsMonitor<GraphQLOptions<'Root>>,
/// The logger to log messages
// The logger to log messages
Comment thread
ePasha marked this conversation as resolved.
Outdated
logger : ILogger<DefaultGraphQLRequestHandler<'Root>>
) =
inherit GraphQLRequestHandler<'Root> (httpContextAccessor, options, logger)

/// Provides logic to parse and execute GraphQL request
and [<AbstractClass>] GraphQLRequestHandler<'Root>
(
Comment thread
ePasha marked this conversation as resolved.
Comment thread
ePasha marked this conversation as resolved.
/// The accessor to the current HTTP context
// The accessor to the current HTTP context
httpContextAccessor : IHttpContextAccessor,
/// The options monitor for GraphQL options
// The options monitor for GraphQL options
options : IOptionsMonitor<GraphQLOptions<'Root>>,
/// The logger to log messages
// The logger to log messages
logger : ILogger
) =

let ctx = httpContextAccessor.HttpContext
let inputContext = fun () -> (HttpContextRequestExecutionContext ctx) :> IInputExecutionContext
Comment thread
ePasha marked this conversation as resolved.
Outdated

let toResponse { DocumentId = documentId; Content = content; Metadata = metadata } =

Expand Down Expand Up @@ -142,8 +143,8 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>
let executeIntrospectionQuery (executor : Executor<_>) (ast : Ast.Document voption) : Task<IResult> = task {
let! result =
match ast with
| ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition
| ValueSome ast -> executor.AsyncExecute ast
| ValueNone -> executor.AsyncExecute (IntrospectionQuery.Definition, inputContext)
| ValueSome ast -> executor.AsyncExecute (ast, inputContext)

let response = result |> toResponse
return (TypedResults.Ok response) :> IResult
Expand Down Expand Up @@ -228,7 +229,7 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>

let! result =
Async.StartImmediateAsTask (
executor.AsyncExecute (content.Ast, root, ?variables = variables, ?operationName = operationName),
executor.AsyncExecute (content.Ast, inputContext, root, ?variables = variables, ?operationName = operationName),
cancellationToken = ctx.RequestAborted
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ open System.Text.Json
open System.Text.Json.Serialization
open System.Threading
open System.Threading.Tasks
open FSharp.Data.GraphQL.Shared
Comment thread
ePasha marked this conversation as resolved.
Outdated
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
Expand Down Expand Up @@ -45,7 +46,7 @@ type GraphQLWebSocketMiddleware<'Root>
| ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse }
| Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload }
| Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone }
| Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs }
| Error (id, errMessages) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMessages }
return JsonSerializer.Serialize (raw, jsonSerializerOptions)
}

Expand Down Expand Up @@ -89,9 +90,9 @@ type GraphQLWebSocketMiddleware<'Root>
&& ((segmentResponse = null)
|| (not segmentResponse.EndOfMessage)) do
try
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
let! r = socket.ReceiveAsync (ArraySegment<byte>(buffer), cancellationToken)
segmentResponse <- r
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
completeMessage.AddRange (ArraySegment<byte>(buffer, 0, r.Count))
with :? OperationCanceledException ->
()

Expand All @@ -117,7 +118,7 @@ type GraphQLWebSocketMiddleware<'Root>
else
// TODO: Allocate string only if a debugger is attached
let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions
let segment = new ArraySegment<byte> (System.Text.Encoding.UTF8.GetBytes (serializedMessage))
let segment = ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes (serializedMessage))
if not (socket.State = WebSocketState.Open) then
logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State)
else
Expand Down Expand Up @@ -160,31 +161,32 @@ type GraphQLWebSocketMiddleware<'Root>
tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure")

let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task =
let subscriptions = new Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction> ()
let subscriptions = Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction>()
// ---------->
// Helpers -->
// ---------->
let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None)

let sendMsg = sendMessageViaSocket serializerOptions socket
let rcv () = socket |> rcvMsgViaSocket serializerOptions
let inputContext = fun () -> (HttpContextRequestExecutionContext httpContext) :> IInputExecutionContext
Comment thread
ePasha marked this conversation as resolved.
Outdated

let sendOutput id (output : SubscriptionExecutionResult) =
sendMsg (Next (id, output))

let sendSubscriptionResponseOutput id subscriptionResult =
match subscriptionResult with
| SubscriptionResult output -> { Data = ValueSome output; Errors = [] } |> sendOutput id
| SubscriptionErrors (output, errors) ->
| SubscriptionErrors (_, errors) ->
logger.LogWarning ("Subscription errors: {subscriptionErrors}", (String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}"))))
{ Data = ValueNone; Errors = errors } |> sendOutput id

let sendDeferredResponseOutput id deferredResult =
match deferredResult with
| DeferredResult (obj, path) ->
| DeferredResult (obj, _) ->
let output = obj :?> Dictionary<string, obj>
{ Data = ValueSome output; Errors = [] } |> sendOutput id
| DeferredErrors (obj, errors, _) ->
| DeferredErrors (_, errors, _) ->
Comment thread
ePasha marked this conversation as resolved.
Outdated
logger.LogWarning (
"Deferred response errors: {deferredErrors}",
(String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))
Expand Down Expand Up @@ -234,10 +236,10 @@ type GraphQLWebSocketMiddleware<'Root>
&& socket |> isSocketOpen do
let! receivedMessage = rcv ()
match receivedMessage with
| Result.Error failureMsgs ->
| Result.Error failureMessages ->
nameof InvalidMessage
|> logMsgReceivedWithOptionalPayload ValueNone
match failureMsgs with
match failureMessages with
| InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None)
| Ok ValueNone -> logger.LogTrace ("WebSocket received empty message! State = '{socketState}'", socket.State)
| Ok (ValueSome msg) ->
Expand Down Expand Up @@ -274,11 +276,11 @@ type GraphQLWebSocketMiddleware<'Root>
let variables = query.Variables |> Skippable.toOption
let! planExecutionResult =
let root = options.RootFactory httpContext
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
options.SchemaExecutor.AsyncExecute (query.Query, inputContext, root, ?variables = variables)
do! planExecutionResult |> applyPlanExecutionResult id socket
with ex ->
logger.LogError (ex, "Unexpected error during subscription with id '{id}'", id)
do! sendMsg (Error (id, [new Shared.NameValueLookup ([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
do! sendMsg (Error (id, [Shared.NameValueLookup([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
| ClientComplete id ->
"ClientComplete" |> logMsgWithIdReceived id
subscriptions
Expand All @@ -287,7 +289,7 @@ type GraphQLWebSocketMiddleware<'Root>
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
with ex ->
logger.LogError (ex, "Cannot handle a message; dropping a websocket connection")
// at this point, only something really weird must have happened.
// At this point, only something really weird must have happened.
// In order to avoid faulty state scenarios and unimagined damages,
// just close the socket without further ado.
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
Expand Down Expand Up @@ -344,7 +346,7 @@ type GraphQLWebSocketMiddleware<'Root>
return Result.Error <| "{nameof ConnectionInit} timeout"
}

member __.InvokeAsync (ctx : HttpContext) = task {
member _.InvokeAsync (ctx : HttpContext) = task {
if not (ctx.Request.Path = endpointUrl) then
do! next.Invoke (ctx)
else if ctx.WebSockets.IsWebSocketRequest then
Expand Down
17 changes: 17 additions & 0 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore

open System
open System.Text
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Shared
open Microsoft.AspNetCore.Http


[<AutoOpen>]
Expand Down Expand Up @@ -49,3 +52,17 @@ module ReflectionHelpers =
| FieldGet (_, fieldInfo) -> fieldInfo.DeclaringType
| _ -> failwith "Expression is no property."

type HttpContextRequestExecutionContext (httpContext : HttpContext) =
interface IInputExecutionContext with
member this.GetFile(key) =
if not httpContext.Request.HasFormContentType then
Error "Request does not have form content type"
else
let form = httpContext.Request.Form
let maybeFile =
form.Files
|> Seq.tryFind (fun f -> f.Name = key)

match maybeFile with
| Some file -> Ok (file.OpenReadStream())
| None -> Error $"File with key '{key}' not found"
Comment thread
ePasha marked this conversation as resolved.
Outdated
1 change: 1 addition & 0 deletions src/FSharp.Data.GraphQL.Server.AspNetCore/HttpContext.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ open System.Collections.Immutable
open System.IO
open System.Runtime.CompilerServices
open System.Text.Json
open FSharp.Data.GraphQL
Comment thread
ePasha marked this conversation as resolved.
Outdated
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware

open System.Collections.Generic
open System.Collections.Immutable
open FSharp.Data.GraphQL.Shared
Comment thread
ePasha marked this conversation as resolved.
open FsToolkit.ErrorHandling

open FSharp.Data.GraphQL
Expand All @@ -11,7 +12,7 @@ open FSharp.Data.GraphQL.Types

type internal QueryWeightMiddleware(threshold : float, reportToMetadata : bool) =

let middleware (threshold : float) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let middleware (threshold : float) (inputContext : InputExecutionContextProvider) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let measureThreshold (threshold : float) (fields : ExecutionInfo list) =
let getWeight f =
if f.ParentDef = upcast ctx.ExecutionPlan.RootDef
Expand Down Expand Up @@ -72,7 +73,7 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
let compileMiddleware (ctx : SchemaCompileContext) (next : SchemaCompileContext -> unit) =
let modifyFields (object : ObjectDef<'ObjectType>) (fields : FieldDef<'ObjectType> seq) =
let args = [ Define.Input("filter", Nullable ObjectListFilterType) ]
let fields = fields |> Seq.map (fun x -> x.WithArgs(args)) |> List.ofSeq
let fields = fields |> Seq.map _.WithArgs(args) |> List.ofSeq
object.WithFields(fields)
let typesWithListFields =
ctx.TypeMap.GetTypesWithListFields<'ObjectType, 'ListType>()
Expand All @@ -85,15 +86,15 @@ type internal ObjectListFilterMiddleware<'ObjectType, 'ListType>(reportToMetadat
ctx.TypeMap.AddTypes(modifiedTypes, overwrite = true)
next ctx

let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let reportMiddleware (inputContext : InputExecutionContextProvider) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
let rec collectArgs (path: obj list) (acc : KeyValuePair<obj list, ObjectListFilter> list) (fields : ExecutionInfo list) =
let fieldArgs currentPath field =
let filterResults =
field.Ast.Arguments
|> Seq.map (fun x ->
match x.Name, x.Value with
| "filter", (VariableName variableName) -> Ok (ValueSome (ctx.Variables[variableName] :?> ObjectListFilter))
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput inputContext (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
| _ -> Ok ValueNone)
|> Seq.toList
match filterResults |> splitSeqErrorsList with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ let ObjectListFilterType : InputCustomDefinition<ObjectListFilter> = {
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 =
(fun input variables ->
(fun _ input variables ->
match input with
| InlineConstant c ->
(coerceObjectListFilterInput variables c)
Expand Down
Loading