Skip to content

Commit 4cb6407

Browse files
committed
Added possibility to upload files using GraphQL
1 parent 00ecaf3 commit 4cb6407

50 files changed

Lines changed: 677 additions & 433 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,28 @@ open FSharp.Data.GraphQL.Shared
1717

1818
type DefaultGraphQLRequestHandler<'Root>
1919
(
20-
/// The accessor to the current HTTP context
20+
// The accessor to the current HTTP context
2121
httpContextAccessor : IHttpContextAccessor,
22-
/// The options monitor for GraphQL options
22+
// The options monitor for GraphQL options
2323
options : IOptionsMonitor<GraphQLOptions<'Root>>,
24-
/// The logger to log messages
24+
// The logger to log messages
2525
logger : ILogger<DefaultGraphQLRequestHandler<'Root>>
2626
) =
2727
inherit GraphQLRequestHandler<'Root> (httpContextAccessor, options, logger)
2828

2929
/// Provides logic to parse and execute GraphQL request
3030
and [<AbstractClass>] GraphQLRequestHandler<'Root>
3131
(
32-
/// The accessor to the current HTTP context
32+
// The accessor to the current HTTP context
3333
httpContextAccessor : IHttpContextAccessor,
34-
/// The options monitor for GraphQL options
34+
// The options monitor for GraphQL options
3535
options : IOptionsMonitor<GraphQLOptions<'Root>>,
36-
/// The logger to log messages
36+
// The logger to log messages
3737
logger : ILogger
3838
) =
3939

4040
let ctx = httpContextAccessor.HttpContext
41+
let inputContext = fun () -> (HttpContextRequestExecutionContext ctx) :> IInputExecutionContext
4142

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

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

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

229230
let! result =
230231
Async.StartImmediateAsTask (
231-
executor.AsyncExecute (content.Ast, root, ?variables = variables, ?operationName = operationName),
232+
executor.AsyncExecute (content.Ast, inputContext, root, ?variables = variables, ?operationName = operationName),
232233
cancellationToken = ctx.RequestAborted
233234
)
234235

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLWebsocketMiddleware.fs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ open System.Text.Json
1010
open System.Text.Json.Serialization
1111
open System.Threading
1212
open System.Threading.Tasks
13+
open FSharp.Data.GraphQL.Shared
1314
open Microsoft.AspNetCore.Http
1415
open Microsoft.Extensions.Hosting
1516
open Microsoft.Extensions.Logging
@@ -45,7 +46,7 @@ type GraphQLWebSocketMiddleware<'Root>
4546
| ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse }
4647
| Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload }
4748
| Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone }
48-
| Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs }
49+
| Error (id, errMessages) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMessages }
4950
return JsonSerializer.Serialize (raw, jsonSerializerOptions)
5051
}
5152

@@ -89,9 +90,9 @@ type GraphQLWebSocketMiddleware<'Root>
8990
&& ((segmentResponse = null)
9091
|| (not segmentResponse.EndOfMessage)) do
9192
try
92-
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
93+
let! r = socket.ReceiveAsync (ArraySegment<byte>(buffer), cancellationToken)
9394
segmentResponse <- r
94-
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
95+
completeMessage.AddRange (ArraySegment<byte>(buffer, 0, r.Count))
9596
with :? OperationCanceledException ->
9697
()
9798

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

162163
let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task =
163-
let subscriptions = new Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction> ()
164+
let subscriptions = Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction>()
164165
// ---------->
165166
// Helpers -->
166167
// ---------->
167168
let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None)
168169

169170
let sendMsg = sendMessageViaSocket serializerOptions socket
170171
let rcv () = socket |> rcvMsgViaSocket serializerOptions
172+
let inputContext = fun () -> (HttpContextRequestExecutionContext httpContext) :> IInputExecutionContext
171173

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

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

182184
let sendDeferredResponseOutput id deferredResult =
183185
match deferredResult with
184-
| DeferredResult (obj, path) ->
186+
| DeferredResult (obj, _) ->
185187
let output = obj :?> Dictionary<string, obj>
186188
{ Data = ValueSome output; Errors = [] } |> sendOutput id
187-
| DeferredErrors (obj, errors, _) ->
189+
| DeferredErrors (_, errors, _) ->
188190
logger.LogWarning (
189191
"Deferred response errors: {deferredErrors}",
190192
(String.Join ('\n', errors |> Seq.map (fun x -> $"- %s{x.Message}")))
@@ -234,10 +236,10 @@ type GraphQLWebSocketMiddleware<'Root>
234236
&& socket |> isSocketOpen do
235237
let! receivedMessage = rcv ()
236238
match receivedMessage with
237-
| Result.Error failureMsgs ->
239+
| Result.Error failureMessages ->
238240
nameof InvalidMessage
239241
|> logMsgReceivedWithOptionalPayload ValueNone
240-
match failureMsgs with
242+
match failureMessages with
241243
| InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None)
242244
| Ok ValueNone -> logger.LogTrace ("WebSocket received empty message! State = '{socketState}'", socket.State)
243245
| Ok (ValueSome msg) ->
@@ -274,11 +276,11 @@ type GraphQLWebSocketMiddleware<'Root>
274276
let variables = query.Variables |> Skippable.toOption
275277
let! planExecutionResult =
276278
let root = options.RootFactory httpContext
277-
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
279+
options.SchemaExecutor.AsyncExecute (query.Query, inputContext, root, ?variables = variables)
278280
do! planExecutionResult |> applyPlanExecutionResult id socket
279281
with ex ->
280282
logger.LogError (ex, "Unexpected error during subscription with id '{id}'", id)
281-
do! sendMsg (Error (id, [new Shared.NameValueLookup ([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
283+
do! sendMsg (Error (id, [Shared.NameValueLookup([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
282284
| ClientComplete id ->
283285
"ClientComplete" |> logMsgWithIdReceived id
284286
subscriptions
@@ -287,7 +289,7 @@ type GraphQLWebSocketMiddleware<'Root>
287289
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
288290
with ex ->
289291
logger.LogError (ex, "Cannot handle a message; dropping a websocket connection")
290-
// at this point, only something really weird must have happened.
292+
// At this point, only something really weird must have happened.
291293
// In order to avoid faulty state scenarios and unimagined damages,
292294
// just close the socket without further ado.
293295
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
@@ -344,7 +346,7 @@ type GraphQLWebSocketMiddleware<'Root>
344346
return Result.Error <| "{nameof ConnectionInit} timeout"
345347
}
346348

347-
member __.InvokeAsync (ctx : HttpContext) = task {
349+
member _.InvokeAsync (ctx : HttpContext) = task {
348350
if not (ctx.Request.Path = endpointUrl) then
349351
do! next.Invoke (ctx)
350352
else if ctx.WebSockets.IsWebSocketRequest then

src/FSharp.Data.GraphQL.Server.AspNetCore/Helpers.fs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore
22

33
open System
44
open System.Text
5+
open FSharp.Data.GraphQL
6+
open FSharp.Data.GraphQL.Shared
7+
open Microsoft.AspNetCore.Http
58

69

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

55+
type HttpContextRequestExecutionContext (httpContext : HttpContext) =
56+
interface IInputExecutionContext with
57+
member this.GetFile(key) =
58+
if not httpContext.Request.HasFormContentType then
59+
Error "Request does not have form content type"
60+
else
61+
let form = httpContext.Request.Form
62+
let maybeFile =
63+
form.Files
64+
|> Seq.tryFind (fun f -> f.Name = key)
65+
66+
match maybeFile with
67+
| Some file -> Ok (file.OpenReadStream())
68+
| None -> Error $"File with key '{key}' not found"

src/FSharp.Data.GraphQL.Server.AspNetCore/HttpContext.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ open System.Collections.Immutable
66
open System.IO
77
open System.Runtime.CompilerServices
88
open System.Text.Json
9+
open FSharp.Data.GraphQL
910
open Microsoft.AspNetCore.Http
1011
open Microsoft.Extensions.DependencyInjection
1112
open Microsoft.Extensions.Options

src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace FSharp.Data.GraphQL.Server.Middleware
22

33
open System.Collections.Generic
44
open System.Collections.Immutable
5+
open FSharp.Data.GraphQL.Shared
56
open FsToolkit.ErrorHandling
67

78
open FSharp.Data.GraphQL
@@ -11,7 +12,7 @@ open FSharp.Data.GraphQL.Types
1112

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

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

88-
let reportMiddleware (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
89+
let reportMiddleware (inputContext : InputExecutionContextProvider) (ctx : ExecutionContext) (next : ExecutionContext -> AsyncVal<GQLExecutionResult>) =
8990
let rec collectArgs (path: obj list) (acc : KeyValuePair<obj list, ObjectListFilter> list) (fields : ExecutionInfo list) =
9091
let fieldArgs currentPath field =
9192
let filterResults =
9293
field.Ast.Arguments
9394
|> Seq.map (fun x ->
9495
match x.Name, x.Value with
9596
| "filter", (VariableName variableName) -> Ok (ValueSome (ctx.Variables[variableName] :?> ObjectListFilter))
96-
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
97+
| "filter", inlineConstant -> ObjectListFilterType.CoerceInput inputContext (InlineConstant inlineConstant) ctx.Variables |> Result.map ValueOption.ofObj
9798
| _ -> Ok ValueNone)
9899
|> Seq.toList
99100
match filterResults |> splitSeqErrorsList with

src/FSharp.Data.GraphQL.Server.Middleware/SchemaDefinitions.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ let ObjectListFilterType : InputCustomDefinition<ObjectListFilter> = {
167167
Some
168168
"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."
169169
CoerceInput =
170-
(fun input variables ->
170+
(fun _ input variables ->
171171
match input with
172172
| InlineConstant c ->
173173
(coerceObjectListFilterInput variables c)

0 commit comments

Comments
 (0)