Skip to content

Commit 9f43e95

Browse files
ePashaxperiandri
andauthored
Upload files using GraphQL (fsprojects#531)
* Implemented handling multi-part requests * Implemented file upload using GraphQL * Implemented tests for single and multiple files upload --------- Co-authored-by: Andrii Chebukin <[email protected]>
1 parent 00ecaf3 commit 9f43e95

File tree

58 files changed

+914
-504
lines changed

Some content is hidden

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

58 files changed

+914
-504
lines changed

src/FSharp.Data.GraphQL.Server.AspNetCore/FSharp.Data.GraphQL.Server.AspNetCore.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<ItemGroup>
1515
<Compile Include="Helpers.fs" />
16+
<Compile Include="RequestExecutionContext.fs" />
1617
<Compile Include="GraphQLOptions.fs" />
1718
<Compile Include="GraphQLSubscriptionsManagement.fs" />
1819
<Compile Include="GraphQLWebsocketMiddleware.fs" />

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

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,34 @@ open FSharp.Data.GraphQL.Server
1616
open FSharp.Data.GraphQL.Shared
1717

1818
type DefaultGraphQLRequestHandler<'Root>
19+
/// <summary>
20+
/// Handles GraphQL requests using a provided root schema.
21+
/// </summary>
22+
/// <param name="httpContextAccessor">The accessor to the current HTTP context.</param>
23+
/// <param name="options">The options monitor for GraphQL options.</param>
24+
/// <param name="logger">The logger to log messages.</param>
1925
(
20-
/// The accessor to the current HTTP context
2126
httpContextAccessor : IHttpContextAccessor,
22-
/// The options monitor for GraphQL options
2327
options : IOptionsMonitor<GraphQLOptions<'Root>>,
24-
/// The logger to log messages
2528
logger : ILogger<DefaultGraphQLRequestHandler<'Root>>
2629
) =
2730
inherit GraphQLRequestHandler<'Root> (httpContextAccessor, options, logger)
2831

29-
/// Provides logic to parse and execute GraphQL request
3032
and [<AbstractClass>] GraphQLRequestHandler<'Root>
33+
/// <summary>
34+
/// Provides logic to parse and execute GraphQL requests.
35+
/// </summary>
36+
/// <param name="httpContextAccessor">The accessor to the current HTTP context.</param>
37+
/// <param name="options">The options monitor for GraphQL options.</param>
38+
/// <param name="logger">The logger to log messages.</param>
3139
(
32-
/// The accessor to the current HTTP context
3340
httpContextAccessor : IHttpContextAccessor,
34-
/// The options monitor for GraphQL options
3541
options : IOptionsMonitor<GraphQLOptions<'Root>>,
36-
/// The logger to log messages
3742
logger : ILogger
3843
) =
3944

4045
let ctx = httpContextAccessor.HttpContext
46+
let getInputContext() = (HttpContextRequestExecutionContext ctx) :> IInputExecutionContext
4147

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

@@ -142,8 +148,8 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>
142148
let executeIntrospectionQuery (executor : Executor<_>) (ast : Ast.Document voption) : Task<IResult> = task {
143149
let! result =
144150
match ast with
145-
| ValueNone -> executor.AsyncExecute IntrospectionQuery.Definition
146-
| ValueSome ast -> executor.AsyncExecute ast
151+
| ValueNone -> executor.AsyncExecute (IntrospectionQuery.Definition, getInputContext)
152+
| ValueSome ast -> executor.AsyncExecute (ast, getInputContext)
147153

148154
let response = result |> toResponse
149155
return (TypedResults.Ok response) :> IResult
@@ -228,7 +234,7 @@ and [<AbstractClass>] GraphQLRequestHandler<'Root>
228234

229235
let! result =
230236
Async.StartImmediateAsTask (
231-
executor.AsyncExecute (content.Ast, root, ?variables = variables, ?operationName = operationName),
237+
executor.AsyncExecute (content.Ast, getInputContext, root, ?variables = variables, ?operationName = operationName),
232238
cancellationToken = ctx.RequestAborted
233239
)
234240

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type GraphQLWebSocketMiddleware<'Root>
4545
| ServerPong p -> { Id = ValueNone; Type = "pong"; Payload = p |> ValueOption.map CustomResponse }
4646
| Next (id, payload) -> { Id = ValueSome id; Type = "next"; Payload = ValueSome <| ExecutionResult payload }
4747
| Complete id -> { Id = ValueSome id; Type = "complete"; Payload = ValueNone }
48-
| Error (id, errMsgs) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMsgs }
48+
| Error (id, errMessages) -> { Id = ValueSome id; Type = "error"; Payload = ValueSome <| ErrorMessages errMessages }
4949
return JsonSerializer.Serialize (raw, jsonSerializerOptions)
5050
}
5151

@@ -89,9 +89,9 @@ type GraphQLWebSocketMiddleware<'Root>
8989
&& ((segmentResponse = null)
9090
|| (not segmentResponse.EndOfMessage)) do
9191
try
92-
let! r = socket.ReceiveAsync (new ArraySegment<byte> (buffer), cancellationToken)
92+
let! r = socket.ReceiveAsync (ArraySegment<byte>(buffer), cancellationToken)
9393
segmentResponse <- r
94-
completeMessage.AddRange (new ArraySegment<byte> (buffer, 0, r.Count))
94+
completeMessage.AddRange (ArraySegment<byte>(buffer, 0, r.Count))
9595
with :? OperationCanceledException ->
9696
()
9797

@@ -117,7 +117,7 @@ type GraphQLWebSocketMiddleware<'Root>
117117
else
118118
// TODO: Allocate string only if a debugger is attached
119119
let! serializedMessage = message |> serializeServerMessage jsonSerializerOptions
120-
let segment = new ArraySegment<byte> (System.Text.Encoding.UTF8.GetBytes (serializedMessage))
120+
let segment = ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes (serializedMessage))
121121
if not (socket.State = WebSocketState.Open) then
122122
logger.LogTrace ($"Ignoring message to be sent via socket, since its state is not '{nameof WebSocketState.Open}', but '{{state}}'", socket.State)
123123
else
@@ -160,14 +160,15 @@ type GraphQLWebSocketMiddleware<'Root>
160160
tryToGracefullyCloseSocket (WebSocketCloseStatus.NormalClosure, "Normal Closure")
161161

162162
let handleMessages (cancellationToken : CancellationToken) (httpContext : HttpContext) (socket : WebSocket) : Task =
163-
let subscriptions = new Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction> ()
163+
let subscriptions = Dictionary<SubscriptionId, SubscriptionUnsubscriber * OnUnsubscribeAction>()
164164
// ---------->
165165
// Helpers -->
166166
// ---------->
167167
let rcvMsgViaSocket = receiveMessageViaSocket (CancellationToken.None)
168168

169169
let sendMsg = sendMessageViaSocket serializerOptions socket
170170
let rcv () = socket |> rcvMsgViaSocket serializerOptions
171+
let getInputContext() = (HttpContextRequestExecutionContext httpContext) :> IInputExecutionContext
171172

172173
let sendOutput id (output : SubscriptionExecutionResult) =
173174
sendMsg (Next (id, output))
@@ -234,10 +235,10 @@ type GraphQLWebSocketMiddleware<'Root>
234235
&& socket |> isSocketOpen do
235236
let! receivedMessage = rcv ()
236237
match receivedMessage with
237-
| Result.Error failureMsgs ->
238+
| Result.Error failureMessages ->
238239
nameof InvalidMessage
239240
|> logMsgReceivedWithOptionalPayload ValueNone
240-
match failureMsgs with
241+
match failureMessages with
241242
| InvalidMessage (code, explanation) -> do! socket.CloseAsync (enum code, explanation, CancellationToken.None)
242243
| Ok ValueNone -> logger.LogTrace ("WebSocket received empty message! State = '{socketState}'", socket.State)
243244
| Ok (ValueSome msg) ->
@@ -274,11 +275,11 @@ type GraphQLWebSocketMiddleware<'Root>
274275
let variables = query.Variables |> Skippable.toOption
275276
let! planExecutionResult =
276277
let root = options.RootFactory httpContext
277-
options.SchemaExecutor.AsyncExecute (query.Query, root, ?variables = variables)
278+
options.SchemaExecutor.AsyncExecute (query.Query, getInputContext, root, ?variables = variables)
278279
do! planExecutionResult |> applyPlanExecutionResult id socket
279280
with ex ->
280281
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) ])]))
282+
do! sendMsg (Error (id, [NameValueLookup([ ("subscription", "Unexpected error during subscription" :> obj) ])]))
282283
| ClientComplete id ->
283284
"ClientComplete" |> logMsgWithIdReceived id
284285
subscriptions
@@ -287,7 +288,7 @@ type GraphQLWebSocketMiddleware<'Root>
287288
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
288289
with ex ->
289290
logger.LogError (ex, "Cannot handle a message; dropping a websocket connection")
290-
// at this point, only something really weird must have happened.
291+
// At this point, only something really weird must have happened.
291292
// In order to avoid faulty state scenarios and unimagined damages,
292293
// just close the socket without further ado.
293294
do! socket |> tryToGracefullyCloseSocketWithDefaultBehavior
@@ -344,7 +345,7 @@ type GraphQLWebSocketMiddleware<'Root>
344345
return Result.Error <| "{nameof ConnectionInit} timeout"
345346
}
346347

347-
member __.InvokeAsync (ctx : HttpContext) = task {
348+
member _.InvokeAsync (ctx : HttpContext) = task {
348349
if not (ctx.Request.Path = endpointUrl) then
349350
do! next.Invoke (ctx)
350351
else if ctx.WebSockets.IsWebSocketRequest then

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore
33
open System
44
open System.Text
55

6-
76
[<AutoOpen>]
87
module Helpers =
98

@@ -48,4 +47,3 @@ module ReflectionHelpers =
4847
| PropertyGet (_, propertyInfo, _) -> propertyInfo.DeclaringType
4948
| FieldGet (_, fieldInfo) -> fieldInfo.DeclaringType
5049
| _ -> failwith "Expression is no property."
51-

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type HttpContext with
2121
/// </summary>
2222
/// <typeparam name="'T">Type to deserialize to</typeparam>
2323
/// <returns>
24-
/// Retruns a <see cref="System.Threading.Tasks.Task{T}"/>Deserialized object or
24+
/// Returns a <see cref="System.Threading.Tasks.Task{T}"/>Deserialized object or
2525
/// <see cref="ProblemDetails">ProblemDetails</see> as <see cref="IResult">IResult</see>
2626
/// if a body could not be deserialized.
2727
/// </returns>
@@ -31,10 +31,23 @@ type HttpContext with
3131
let request = ctx.Request
3232

3333
try
34-
if not request.Body.CanSeek then
35-
request.EnableBuffering()
34+
let! jsonStream =
35+
task {
36+
if request.HasFormContentType then
37+
let! form = request.ReadFormAsync(ctx.RequestAborted)
38+
match form.TryGetValue("operations") with
39+
| true, values when values.Count > 0 ->
40+
let bytes = System.Text.Encoding.UTF8.GetBytes(values[0])
41+
return new MemoryStream(bytes) :> Stream
42+
| _ ->
43+
return request.Body
44+
else
45+
if not request.Body.CanSeek then
46+
request.EnableBuffering()
47+
return request.Body
48+
}
3649

37-
return! JsonSerializer.DeserializeAsync<'T>(request.Body, serializerOptions, ctx.RequestAborted)
50+
return! JsonSerializer.DeserializeAsync<'T>(jsonStream, serializerOptions, ctx.RequestAborted)
3851
with :? JsonException ->
3952
let body = request.Body
4053
body.Seek(0, SeekOrigin.Begin) |> ignore
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace FSharp.Data.GraphQL.Server.AspNetCore
2+
3+
open FSharp.Data.GraphQL
4+
open Microsoft.AspNetCore.Http
5+
6+
type HttpContextRequestExecutionContext (httpContext : HttpContext) =
7+
8+
interface IInputExecutionContext with
9+
10+
member this.GetFile(key) =
11+
if not httpContext.Request.HasFormContentType then
12+
Error "Request does not have form content type"
13+
else
14+
let form = httpContext.Request.Form
15+
match (form.Files |> Seq.vtryFind (fun f -> f.Name = key)) with
16+
| ValueSome file -> Ok (file.OpenReadStream())
17+
| ValueNone -> Error $"File with key '{key}' not found"
18+

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)