Skip to content

Commit 5ca80cb

Browse files
VectorTetraViktor Tochonov
andauthored
Fixed file upload tests (#538)
--------- Co-authored-by: Viktor Tochonov <[email protected]>
1 parent c3ff07b commit 5ca80cb

File tree

21 files changed

+389
-341
lines changed

21 files changed

+389
-341
lines changed

src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs

Lines changed: 63 additions & 42 deletions
Large diffs are not rendered by default.

src/FSharp.Data.GraphQL.Client/GraphQLClient.fs

Lines changed: 91 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace FSharp.Data.GraphQL
55

66
open System
77
open System.Collections.Generic
8+
open System.Collections.Immutable
89
open System.Net.Http
910
open System.Text
1011
open System.Threading
@@ -15,40 +16,42 @@ open FSharp.Data.GraphQL.Client
1516
open ReflectionPatterns
1617

1718
/// A requrest object for making GraphQL calls using the GraphQL client module.
18-
type GraphQLRequest =
19-
{ /// Gets the URL of the GraphQL server which will be called.
20-
ServerUrl : string
21-
/// Gets custom HTTP Headers to pass with each call using this request.
22-
HttpHeaders: seq<string * string>
23-
/// Gets the name of the operation that should run on the server.
24-
OperationName : string option
25-
/// Gets the query string which should be executed on the GraphQL server.
26-
Query : string
27-
/// Gets variables to be sent with the query.
28-
Variables : (string * obj) [] }
19+
type GraphQLRequest = {
20+
/// Gets the URL of the GraphQL server which will be called.
21+
ServerUrl : string
22+
/// Gets custom HTTP Headers to pass with each call using this request.
23+
HttpHeaders : seq<string * string>
24+
/// Gets the name of the operation that should run on the server.
25+
OperationName : string option
26+
/// Gets the query string which should be executed on the GraphQL server.
27+
Query : string
28+
/// Gets variables to be sent with the query.
29+
Variables : (string * obj)[]
30+
}
2931

3032
/// Executes calls to GraphQL servers and return their responses.
3133
module GraphQLClient =
3234

3335
let private ensureSuccessCode (response : Task<HttpResponseMessage>) = task {
3436
let! response = response
35-
return response.EnsureSuccessStatusCode()
37+
return response.EnsureSuccessStatusCode ()
3638
}
3739

3840
let private addHeaders (httpHeaders : seq<string * string>) (requestMessage : HttpRequestMessage) =
39-
if not (isNull httpHeaders)
40-
then httpHeaders |> Seq.iter (fun (name, value) -> requestMessage.Headers.Add(name, value))
41+
if not (isNull httpHeaders) then
42+
httpHeaders
43+
|> Seq.iter (fun (name, value) -> requestMessage.Headers.Add (name, value))
4144

4245
let private postAsync ct (invoker : HttpMessageInvoker) (serverUrl : string) (httpHeaders : seq<string * string>) (content : HttpContent) = task {
43-
use requestMessage = new HttpRequestMessage(HttpMethod.Post, serverUrl)
46+
use requestMessage = new HttpRequestMessage (HttpMethod.Post, serverUrl)
4447
requestMessage.Content <- content
4548
addHeaders httpHeaders requestMessage
46-
return! invoker.SendAsync(requestMessage, ct) |> ensureSuccessCode
49+
return! invoker.SendAsync (requestMessage, ct) |> ensureSuccessCode
4750
}
4851

4952
let private getAsync ct (invoker : HttpMessageInvoker) (serverUrl : string) = task {
50-
use requestMessage = new HttpRequestMessage(HttpMethod.Get, serverUrl)
51-
return! invoker.SendAsync(requestMessage, ct) |> ensureSuccessCode
53+
use requestMessage = new HttpRequestMessage (HttpMethod.Get, serverUrl)
54+
return! invoker.SendAsync (requestMessage, ct) |> ensureSuccessCode
5255
}
5356

5457
/// Sends a request to a GraphQL server asynchronously.
@@ -63,108 +66,129 @@ module GraphQLClient =
6366
| Some x -> JsonValue.String x
6467
| None -> JsonValue.Null
6568
let requestJson =
66-
[| "operationName", operationName
67-
"query", JsonValue.String request.Query
68-
"variables", variables |]
69+
[|
70+
"operationName", operationName
71+
"query", JsonValue.String request.Query
72+
"variables", variables
73+
|]
6974
|> JsonValue.Record
70-
let content = new StringContent(requestJson.ToString(), Encoding.UTF8, "application/json")
75+
let content = new StringContent (requestJson.ToString (), Encoding.UTF8, "application/json")
7176
return! postAsync ct invoker request.ServerUrl request.HttpHeaders content
7277
}
7378

7479
/// Sends a request to a GraphQL server.
75-
let sendRequest client request = (sendRequestAsync CancellationToken.None client request).GetAwaiter().GetResult()
80+
let sendRequest client request =
81+
(sendRequestAsync CancellationToken.None client request).GetAwaiter().GetResult ()
7682

7783
/// Executes an introspection schema request to a GraphQL server asynchronously.
7884
let sendIntrospectionRequestAsync ct (connection : GraphQLClientConnection) (serverUrl : string) httpHeaders =
79-
let sendGet() = getAsync ct connection.Invoker serverUrl
85+
let sendGet () = getAsync ct connection.Invoker serverUrl
8086
let rethrow (exns : exn list) =
8187
let rec mapper (acc : string) (exns : exn list) =
8288
let aggregateMapper (ex : AggregateException) = mapper "" (List.ofSeq ex.InnerExceptions)
8389
match exns with
84-
| [] -> acc.TrimEnd()
90+
| [] -> acc.TrimEnd ()
8591
| ex :: tail ->
8692
match ex with
8793
| :? AggregateException as ex -> mapper (acc + aggregateMapper ex + " ") tail
8894
| ex -> mapper (acc + ex.Message + " ") tail
8995
failwith $"""Failure trying to recover introspection schema from server at "%s{serverUrl}". Errors: %s{mapper "" exns}"""
9096
task {
91-
try return! sendGet()
97+
try
98+
return! sendGet ()
9299
with getex ->
93-
let request =
94-
{ ServerUrl = serverUrl
95-
HttpHeaders = httpHeaders
96-
OperationName = None
97-
Query = IntrospectionQuery.Definition
98-
Variables = [||] }
99-
try return! sendRequestAsync ct connection request
100-
with postex -> return rethrow [getex; postex]
100+
let request = {
101+
ServerUrl = serverUrl
102+
HttpHeaders = httpHeaders
103+
OperationName = None
104+
Query = IntrospectionQuery.Definition
105+
Variables = [||]
106+
}
107+
try
108+
return! sendRequestAsync ct connection request
109+
with postex ->
110+
return rethrow [ getex; postex ]
101111
}
102112

103113
/// Executes an introspection schema request to a GraphQL server.
104114
let sendIntrospectionRequest client serverUrl httpHeaders =
105-
(sendIntrospectionRequestAsync CancellationToken.None client serverUrl httpHeaders).GetAwaiter().GetResult()
115+
(sendIntrospectionRequestAsync CancellationToken.None client serverUrl httpHeaders).GetAwaiter().GetResult ()
106116

107117
/// Executes a multipart request to a GraphQL server asynchronously.
108118
let sendMultipartRequestAsync ct (connection : GraphQLClientConnection) (request : GraphQLRequest) = task {
109119
let invoker = connection.Invoker
110-
let boundary = "----GraphQLProviderBoundary" + (Guid.NewGuid().ToString("N"))
111-
let content = new MultipartContent("form-data", boundary)
120+
let boundary =
121+
"----GraphQLProviderBoundary"
122+
+ (Guid.NewGuid().ToString ("N"))
123+
let content = new MultipartContent ("form-data", boundary)
112124
let files =
113-
let rec tryMapFileVariable (name: string, value : obj) =
125+
let rec tryMapFileVariable (name : string, value : obj) =
114126
match value with
115-
| null | :? string -> None
116-
| :? Upload as x -> Some [|name, x|]
117-
| OptionValue x ->
118-
x |> Option.bind (fun x -> tryMapFileVariable (name, x))
127+
| null
128+
| :? string -> None
129+
| :? Upload as x -> Some [| name, x |]
130+
| OptionValue x -> x |> Option.bind (fun x -> tryMapFileVariable (name, x))
119131
| :? IDictionary<string, obj> as x ->
120-
x |> Seq.collect (fun kvp -> tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower()), kvp.Value) |> Option.defaultValue [||])
121-
|> Array.ofSeq
122-
|> Some
132+
x
133+
|> Seq.collect (fun kvp ->
134+
tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower ()), kvp.Value)
135+
|> Option.defaultValue [||])
136+
|> Array.ofSeq
137+
|> Some
123138
| EnumerableValue x ->
124-
x |> Array.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x))
125-
|> Array.collect (Option.defaultValue [||])
126-
|> Some
139+
x
140+
|> Array.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x))
141+
|> Array.collect (Option.defaultValue [||])
142+
|> Some
127143
| _ -> None
128-
request.Variables |> Array.collect (tryMapFileVariable >> (Option.defaultValue [||]))
144+
request.Variables
145+
|> Array.collect (tryMapFileVariable >> (Option.defaultValue [||]))
146+
129147
let operationContent =
130148
let variables =
131149
match request.Variables with
132-
| null | [||] -> JsonValue.Null
133-
| _ -> request.Variables |> Map.ofArray |> Serialization.toJsonValue
150+
| null
151+
| [||] -> JsonValue.Null
152+
| _ ->
153+
request.Variables
154+
|> Map.ofArray
155+
|> Serialization.toJsonValue
134156
let operationName =
135157
match request.OperationName with
136158
| Some x -> JsonValue.String x
137159
| None -> JsonValue.Null
138160
let json =
139-
[| "operationName", operationName
140-
"query", JsonValue.String request.Query
141-
"variables", variables |]
161+
[|
162+
"operationName", operationName
163+
"query", JsonValue.String request.Query
164+
"variables", variables
165+
|]
142166
|> JsonValue.Record
143-
let content = new StringContent(json.ToString(JsonSaveOptions.DisableFormatting))
144-
content.Headers.Add("Content-Disposition", "form-data; name=\"operations\"")
167+
let content = new StringContent (json.ToString (JsonSaveOptions.DisableFormatting))
168+
content.Headers.Add ("Content-Disposition", "form-data; name=\"operations\"")
145169
content
146-
content.Add(operationContent)
170+
content.Add (operationContent)
147171
let mapContent =
148172
let files =
149173
files
150-
|> Array.mapi (fun ix (name, _) -> ix.ToString(), JsonValue.Array [| JsonValue.String ("variables." + name) |])
174+
|> Array.mapi (fun ix (name, _) -> ix.ToString (), JsonValue.Array [| JsonValue.String ("variables." + name) |])
151175
|> JsonValue.Record
152-
let content = new StringContent(files.ToString(JsonSaveOptions.DisableFormatting))
153-
content.Headers.Add("Content-Disposition", "form-data; name=\"map\"")
176+
let content = new StringContent (files.ToString (JsonSaveOptions.DisableFormatting))
177+
content.Headers.Add ("Content-Disposition", "form-data; name=\"map\"")
154178
content
155-
content.Add(mapContent)
179+
content.Add (mapContent)
156180
let fileContents =
157181
files
158-
|> Array.mapi (fun ix (_, value) ->
159-
let content = new StreamContent(value.Stream)
160-
content.Headers.Add("Content-Disposition", $"form-data; name=\"%i{ix}\"; filename=\"%s{value.FileName}\"")
161-
content.Headers.Add("Content-Type", value.ContentType)
182+
|> Seq.mapi (fun _ (_, value) ->
183+
let content = new StreamContent (value.Stream)
184+
content.Headers.Add ("Content-Disposition", $"form-data; name=\"%s{value.Name}\"; filename=\"%s{value.FileName}\"")
185+
content.Headers.Add ("Content-Type", value.ContentType)
162186
content)
163-
fileContents |> Array.iter content.Add
187+
fileContents |> Seq.iter content.Add
164188
let! result = postAsync ct invoker request.ServerUrl request.HttpHeaders content
165189
return result
166190
}
167191

168192
/// Executes a multipart request to a GraphQL server.
169193
let sendMultipartRequest connection request =
170-
(sendMultipartRequestAsync CancellationToken.None connection request).GetAwaiter().GetResult()
194+
(sendMultipartRequestAsync CancellationToken.None connection request).GetAwaiter().GetResult ()

src/FSharp.Data.GraphQL.Client/MimeTypes.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ open System.Collections.ObjectModel
55
open System
66

77
let private dictBuilder() : IReadOnlyDictionary<string, string> =
8-
let types =
9-
[| ".323", "text/h323"
8+
let types = [|
9+
".323", "text/h323"
1010
".3g2", "video/3gpp2"
1111
".3gp", "video/3gpp"
1212
".3gp2", "video/3gpp2"

src/FSharp.Data.GraphQL.Client/Serialization.fs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
namespace FSharp.Data.GraphQL.Client
55

66
open System
7-
open Microsoft.FSharp.Reflection
8-
open System.Reflection
97
open System.Collections.Generic
8+
open System.Diagnostics
109
open System.Globalization
10+
open System.Reflection
11+
open Microsoft.FSharp.Reflection
1112
open FSharp.Data.GraphQL
1213
open FSharp.Data.GraphQL.Client.ReflectionPatterns
1314

@@ -172,7 +173,7 @@ module Serialization =
172173
| :? DateTimeOffset as x -> JsonValue.String (x.ToString(isoDateTimeFormat))
173174
| :? bool as x -> JsonValue.Boolean x
174175
| :? Uri as x -> JsonValue.String (x.ToString())
175-
| :? Upload -> JsonValue.Null
176+
| :? Upload as u -> JsonValue.String u.Name
176177
| :? IDictionary<string, obj> as items ->
177178
items
178179
|> Seq.map (fun (KeyValue (k, v)) -> k.FirstCharLower(), toJsonValue v)

src/FSharp.Data.GraphQL.Client/Upload.fs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,54 @@ namespace FSharp.Data.GraphQL
55

66
open System
77
open System.IO
8+
open System.Net.Mime
9+
open System.Runtime.InteropServices
810
open FSharp.Data.GraphQL.Client
911

1012
/// The base type for all GraphQLProvider upload types.
1113
/// Upload types are used in GraphQL multipart request spec, mostly for file uploading features.
12-
type Upload (stream : Stream, fileName : string, ?contentType : string, ?ownsStream : bool) =
13-
new(bytes : byte [], fileName, ?contentType) =
14-
let stream = new MemoryStream(bytes)
14+
type Upload
15+
(stream : Stream, fileName : string, [<Optional>] name : string | null, [<Optional>] contentType : string | null, [<Optional>] ownsStream : bool)
16+
=
17+
18+
new (bytes : byte[], fileName, [<Optional>] name, [<Optional>] contentType) =
19+
let stream = new MemoryStream (bytes)
1520
match contentType with
16-
| Some ct -> new Upload(stream, fileName, ct, true)
17-
| None -> new Upload(stream, fileName, ownsStream = true)
21+
| null -> new Upload (stream, fileName, name, ownsStream = true)
22+
| ct -> new Upload (stream, fileName, name, ct, true)
23+
24+
new (bytes : byte[], fileName, contentType) = new Upload (bytes = bytes, fileName = fileName, name = null, contentType = contentType)
25+
26+
new (stream, fileName, [<Optional>] name, [<Optional>] contentType) = new Upload (stream, fileName, name, contentType, ownsStream = false)
1827

1928
/// Gets the stream associated to this Upload type.
2029
member _.Stream = stream
2130

2231
/// Gets the content type of this Upload type.
2332
member _.ContentType =
2433
match contentType with
25-
| Some ct -> ct
26-
| None ->
27-
let ext = Path.GetExtension(fileName)
28-
match MimeTypes.dict.Force().TryGetValue(ext) with
34+
| null ->
35+
let ext = Path.GetExtension (fileName)
36+
match MimeTypes.dict.Force().TryGetValue (ext) with
2937
| (true, mime) -> mime
30-
| _ -> "application/octet-stream"
38+
| _ -> MediaTypeNames.Application.Octet
39+
| ct -> ct
40+
41+
/// Gets the name used to uniquely identify upload throughout multiple uploads
42+
/// and within a request.
43+
member val Name =
44+
name
45+
|> ValueOption.ofObj
46+
|> ValueOption.defaultWith (Guid.NewGuid >> string)
3147

3248
/// Gets the name of the file which contained on the stream.
3349
member _.FileName = fileName
3450

3551
/// Gets a boolean value indicating if this Upload type owns the stream associated with it.
3652
/// If true, it will dispose the stream when this Upload type is disposed.
37-
member _.OwnsStream = defaultArg ownsStream false
53+
member _.OwnsStream = ownsStream
3854

3955
interface IDisposable with
40-
member x.Dispose() = if x.OwnsStream then x.Stream.Dispose()
56+
member x.Dispose () =
57+
if x.OwnsStream then
58+
x.Stream.Dispose ()

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
namespace FSharp.Data.GraphQL.Server.AspNetCore
1+
namespace FSharp.Data.GraphQL.Server.AspNetCore
22

3+
open System.IO
34
open FSharp.Data.GraphQL
45
open Microsoft.AspNetCore.Http
56

67
type HttpContextRequestExecutionContext (httpContext : HttpContext) =
78

89
interface IInputExecutionContext with
910

10-
member this.GetFile(key) =
11+
member this.GetFile (key) =
1112
if not httpContext.Request.HasFormContentType then
1213
Error "Request does not have form content type"
1314
else
1415
let form = httpContext.Request.Form
1516
match (form.Files |> Seq.vtryFind (fun f -> f.Name = key)) with
1617
| ValueSome file ->
17-
Ok { Stream = file.OpenReadStream (); ContentType = file.ContentType }
18+
let memoryStream = new MemoryStream ()
19+
use fileStream = file.OpenReadStream ()
20+
fileStream.CopyTo (memoryStream)
21+
memoryStream.Seek (0L, SeekOrigin.Begin) |> ignore
22+
Ok {
23+
FileName = file.FileName
24+
Stream = memoryStream
25+
ContentType = file.ContentType
26+
}
1827
| ValueNone -> Error $"File with key '{key}' not found"
19-

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ module ServiceCollectionExtensions =
8484
}
8585
)
8686
.AddHttpContextAccessor()
87+
.AddScoped<IInputExecutionContext, HttpContextRequestExecutionContext>()
8788
.AddScoped<GraphQLRequestHandler<'Root>, 'Handler>()
8889

8990
/// <summary>

0 commit comments

Comments
 (0)