Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
105 changes: 63 additions & 42 deletions src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs

Large diffs are not rendered by default.

158 changes: 91 additions & 67 deletions src/FSharp.Data.GraphQL.Client/GraphQLClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace FSharp.Data.GraphQL

open System
open System.Collections.Generic
open System.Collections.Immutable
open System.Net.Http
open System.Text
open System.Threading
Expand All @@ -15,40 +16,42 @@ open FSharp.Data.GraphQL.Client
open ReflectionPatterns

/// A requrest object for making GraphQL calls using the GraphQL client module.
type GraphQLRequest =
{ /// Gets the URL of the GraphQL server which will be called.
ServerUrl : string
/// Gets custom HTTP Headers to pass with each call using this request.
HttpHeaders: seq<string * string>
/// Gets the name of the operation that should run on the server.
OperationName : string option
/// Gets the query string which should be executed on the GraphQL server.
Query : string
/// Gets variables to be sent with the query.
Variables : (string * obj) [] }
type GraphQLRequest = {
/// Gets the URL of the GraphQL server which will be called.
ServerUrl : string
/// Gets custom HTTP Headers to pass with each call using this request.
HttpHeaders : seq<string * string>
/// Gets the name of the operation that should run on the server.
OperationName : string option
/// Gets the query string which should be executed on the GraphQL server.
Query : string
/// Gets variables to be sent with the query.
Variables : (string * obj)[]
}

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

let private ensureSuccessCode (response : Task<HttpResponseMessage>) = task {
let! response = response
return response.EnsureSuccessStatusCode()
return response.EnsureSuccessStatusCode ()
}

let private addHeaders (httpHeaders : seq<string * string>) (requestMessage : HttpRequestMessage) =
if not (isNull httpHeaders)
then httpHeaders |> Seq.iter (fun (name, value) -> requestMessage.Headers.Add(name, value))
if not (isNull httpHeaders) then
httpHeaders
|> Seq.iter (fun (name, value) -> requestMessage.Headers.Add (name, value))

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

let private getAsync ct (invoker : HttpMessageInvoker) (serverUrl : string) = task {
use requestMessage = new HttpRequestMessage(HttpMethod.Get, serverUrl)
return! invoker.SendAsync(requestMessage, ct) |> ensureSuccessCode
use requestMessage = new HttpRequestMessage (HttpMethod.Get, serverUrl)
return! invoker.SendAsync (requestMessage, ct) |> ensureSuccessCode
}

/// Sends a request to a GraphQL server asynchronously.
Expand All @@ -63,108 +66,129 @@ module GraphQLClient =
| Some x -> JsonValue.String x
| None -> JsonValue.Null
let requestJson =
[| "operationName", operationName
"query", JsonValue.String request.Query
"variables", variables |]
[|
"operationName", operationName
"query", JsonValue.String request.Query
"variables", variables
|]
|> JsonValue.Record
let content = new StringContent(requestJson.ToString(), Encoding.UTF8, "application/json")
let content = new StringContent (requestJson.ToString (), Encoding.UTF8, "application/json")
return! postAsync ct invoker request.ServerUrl request.HttpHeaders content
}

/// Sends a request to a GraphQL server.
let sendRequest client request = (sendRequestAsync CancellationToken.None client request).GetAwaiter().GetResult()
let sendRequest client request =
(sendRequestAsync CancellationToken.None client request).GetAwaiter().GetResult ()

/// Executes an introspection schema request to a GraphQL server asynchronously.
let sendIntrospectionRequestAsync ct (connection : GraphQLClientConnection) (serverUrl : string) httpHeaders =
let sendGet() = getAsync ct connection.Invoker serverUrl
let sendGet () = getAsync ct connection.Invoker serverUrl
let rethrow (exns : exn list) =
let rec mapper (acc : string) (exns : exn list) =
let aggregateMapper (ex : AggregateException) = mapper "" (List.ofSeq ex.InnerExceptions)
match exns with
| [] -> acc.TrimEnd()
| [] -> acc.TrimEnd ()
| ex :: tail ->
match ex with
| :? AggregateException as ex -> mapper (acc + aggregateMapper ex + " ") tail
| ex -> mapper (acc + ex.Message + " ") tail
failwith $"""Failure trying to recover introspection schema from server at "%s{serverUrl}". Errors: %s{mapper "" exns}"""
task {
try return! sendGet()
try
return! sendGet ()
with getex ->
let request =
{ ServerUrl = serverUrl
HttpHeaders = httpHeaders
OperationName = None
Query = IntrospectionQuery.Definition
Variables = [||] }
try return! sendRequestAsync ct connection request
with postex -> return rethrow [getex; postex]
let request = {
ServerUrl = serverUrl
HttpHeaders = httpHeaders
OperationName = None
Query = IntrospectionQuery.Definition
Variables = [||]
}
try
return! sendRequestAsync ct connection request
with postex ->
return rethrow [ getex; postex ]
}

/// Executes an introspection schema request to a GraphQL server.
let sendIntrospectionRequest client serverUrl httpHeaders =
(sendIntrospectionRequestAsync CancellationToken.None client serverUrl httpHeaders).GetAwaiter().GetResult()
(sendIntrospectionRequestAsync CancellationToken.None client serverUrl httpHeaders).GetAwaiter().GetResult ()

/// Executes a multipart request to a GraphQL server asynchronously.
let sendMultipartRequestAsync ct (connection : GraphQLClientConnection) (request : GraphQLRequest) = task {
let invoker = connection.Invoker
let boundary = "----GraphQLProviderBoundary" + (Guid.NewGuid().ToString("N"))
let content = new MultipartContent("form-data", boundary)
let boundary =
"----GraphQLProviderBoundary"
+ (Guid.NewGuid().ToString ("N"))
let content = new MultipartContent ("form-data", boundary)
let files =
let rec tryMapFileVariable (name: string, value : obj) =
let rec tryMapFileVariable (name : string, value : obj) =
match value with
| null | :? string -> None
| :? Upload as x -> Some [|name, x|]
| OptionValue x ->
x |> Option.bind (fun x -> tryMapFileVariable (name, x))
| null
| :? string -> None
| :? Upload as x -> Some [| name, x |]
| OptionValue x -> x |> Option.bind (fun x -> tryMapFileVariable (name, x))
| :? IDictionary<string, obj> as x ->
x |> Seq.collect (fun kvp -> tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower()), kvp.Value) |> Option.defaultValue [||])
|> Array.ofSeq
|> Some
x
|> Seq.collect (fun kvp ->
tryMapFileVariable (name + "." + (kvp.Key.FirstCharLower ()), kvp.Value)
|> Option.defaultValue [||])
|> Array.ofSeq
|> Some
| EnumerableValue x ->
x |> Array.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x))
|> Array.collect (Option.defaultValue [||])
|> Some
x
|> Array.mapi (fun ix x -> tryMapFileVariable ($"%s{name}.%i{ix}", x))
|> Array.collect (Option.defaultValue [||])
|> Some
| _ -> None
request.Variables |> Array.collect (tryMapFileVariable >> (Option.defaultValue [||]))
request.Variables
|> Array.collect (tryMapFileVariable >> (Option.defaultValue [||]))

let operationContent =
let variables =
match request.Variables with
| null | [||] -> JsonValue.Null
| _ -> request.Variables |> Map.ofArray |> Serialization.toJsonValue
| null
| [||] -> JsonValue.Null
| _ ->
request.Variables
|> Map.ofArray
|> Serialization.toJsonValue
let operationName =
match request.OperationName with
| Some x -> JsonValue.String x
| None -> JsonValue.Null
let json =
[| "operationName", operationName
"query", JsonValue.String request.Query
"variables", variables |]
[|
"operationName", operationName
"query", JsonValue.String request.Query
"variables", variables
|]
|> JsonValue.Record
let content = new StringContent(json.ToString(JsonSaveOptions.DisableFormatting))
content.Headers.Add("Content-Disposition", "form-data; name=\"operations\"")
let content = new StringContent (json.ToString (JsonSaveOptions.DisableFormatting))
content.Headers.Add ("Content-Disposition", "form-data; name=\"operations\"")
content
content.Add(operationContent)
content.Add (operationContent)
let mapContent =
let files =
files
|> Array.mapi (fun ix (name, _) -> ix.ToString(), JsonValue.Array [| JsonValue.String ("variables." + name) |])
|> Array.mapi (fun ix (name, _) -> ix.ToString (), JsonValue.Array [| JsonValue.String ("variables." + name) |])
|> JsonValue.Record
let content = new StringContent(files.ToString(JsonSaveOptions.DisableFormatting))
content.Headers.Add("Content-Disposition", "form-data; name=\"map\"")
let content = new StringContent (files.ToString (JsonSaveOptions.DisableFormatting))
content.Headers.Add ("Content-Disposition", "form-data; name=\"map\"")
content
content.Add(mapContent)
content.Add (mapContent)
let fileContents =
files
|> Array.mapi (fun ix (_, value) ->
let content = new StreamContent(value.Stream)
content.Headers.Add("Content-Disposition", $"form-data; name=\"%i{ix}\"; filename=\"%s{value.FileName}\"")
content.Headers.Add("Content-Type", value.ContentType)
|> Seq.mapi (fun _ (_, value) ->
let content = new StreamContent (value.Stream)
content.Headers.Add ("Content-Disposition", $"form-data; name=\"%s{value.Name}\"; filename=\"%s{value.FileName}\"")
content.Headers.Add ("Content-Type", value.ContentType)
content)
fileContents |> Array.iter content.Add
fileContents |> Seq.iter content.Add
let! result = postAsync ct invoker request.ServerUrl request.HttpHeaders content
return result
}

/// Executes a multipart request to a GraphQL server.
let sendMultipartRequest connection request =
(sendMultipartRequestAsync CancellationToken.None connection request).GetAwaiter().GetResult()
(sendMultipartRequestAsync CancellationToken.None connection request).GetAwaiter().GetResult ()
4 changes: 2 additions & 2 deletions src/FSharp.Data.GraphQL.Client/MimeTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ open System.Collections.ObjectModel
open System

let private dictBuilder() : IReadOnlyDictionary<string, string> =
let types =
[| ".323", "text/h323"
let types = [|
".323", "text/h323"
".3g2", "video/3gpp2"
".3gp", "video/3gpp"
".3gp2", "video/3gpp2"
Expand Down
7 changes: 4 additions & 3 deletions src/FSharp.Data.GraphQL.Client/Serialization.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
namespace FSharp.Data.GraphQL.Client

open System
open Microsoft.FSharp.Reflection
open System.Reflection
open System.Collections.Generic
open System.Diagnostics
open System.Globalization
open System.Reflection
open Microsoft.FSharp.Reflection
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Client.ReflectionPatterns

Expand Down Expand Up @@ -172,7 +173,7 @@ module Serialization =
| :? DateTimeOffset as x -> JsonValue.String (x.ToString(isoDateTimeFormat))
| :? bool as x -> JsonValue.Boolean x
| :? Uri as x -> JsonValue.String (x.ToString())
| :? Upload -> JsonValue.Null
| :? Upload as u -> JsonValue.String u.Name
| :? IDictionary<string, obj> as items ->
items
|> Seq.map (fun (KeyValue (k, v)) -> k.FirstCharLower(), toJsonValue v)
Expand Down
42 changes: 30 additions & 12 deletions src/FSharp.Data.GraphQL.Client/Upload.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,54 @@ namespace FSharp.Data.GraphQL

open System
open System.IO
open System.Net.Mime
open System.Runtime.InteropServices
open FSharp.Data.GraphQL.Client

/// The base type for all GraphQLProvider upload types.
/// Upload types are used in GraphQL multipart request spec, mostly for file uploading features.
type Upload (stream : Stream, fileName : string, ?contentType : string, ?ownsStream : bool) =
new(bytes : byte [], fileName, ?contentType) =
let stream = new MemoryStream(bytes)
type Upload
(stream : Stream, fileName : string, [<Optional>] name : string | null, [<Optional>] contentType : string | null, [<Optional>] ownsStream : bool)
=

new (bytes : byte[], fileName, [<Optional>] name, [<Optional>] contentType) =
let stream = new MemoryStream (bytes)
match contentType with
| Some ct -> new Upload(stream, fileName, ct, true)
| None -> new Upload(stream, fileName, ownsStream = true)
| null -> new Upload (stream, fileName, name, ownsStream = true)
| ct -> new Upload (stream, fileName, name, ct, true)

new (bytes : byte[], fileName, contentType) = new Upload (bytes = bytes, fileName = fileName, name = null, contentType = contentType)

new (stream, fileName, [<Optional>] name, [<Optional>] contentType) = new Upload (stream, fileName, name, contentType, ownsStream = false)

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

/// Gets the content type of this Upload type.
member _.ContentType =
match contentType with
| Some ct -> ct
| None ->
let ext = Path.GetExtension(fileName)
match MimeTypes.dict.Force().TryGetValue(ext) with
| null ->
let ext = Path.GetExtension (fileName)
match MimeTypes.dict.Force().TryGetValue (ext) with
| (true, mime) -> mime
| _ -> "application/octet-stream"
| _ -> MediaTypeNames.Application.Octet
| ct -> ct

/// Gets the name used to uniquily identify upload throuout multiple uploads
Comment thread
VectorTetra marked this conversation as resolved.
Outdated
/// and within a request.
member val Name =
name
|> ValueOption.ofObj
|> ValueOption.defaultWith (Guid.NewGuid >> string)

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

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

interface IDisposable with
member x.Dispose() = if x.OwnsStream then x.Stream.Dispose()
member x.Dispose () =
if x.OwnsStream then
x.Stream.Dispose ()
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
namespace FSharp.Data.GraphQL.Server.AspNetCore
namespace FSharp.Data.GraphQL.Server.AspNetCore

open System.IO
open FSharp.Data.GraphQL
open Microsoft.AspNetCore.Http

type HttpContextRequestExecutionContext (httpContext : HttpContext) =

interface IInputExecutionContext with

member this.GetFile(key) =
member this.GetFile (key) =
if not httpContext.Request.HasFormContentType then
Error "Request does not have form content type"
else
let form = httpContext.Request.Form
match (form.Files |> Seq.vtryFind (fun f -> f.Name = key)) with
| ValueSome file ->
Ok { Stream = file.OpenReadStream (); ContentType = file.ContentType }
let memoryStream = new MemoryStream ()
use fileStream = file.OpenReadStream ()
fileStream.CopyTo (memoryStream)
memoryStream.Position <- 0L
Comment thread
VectorTetra marked this conversation as resolved.
Outdated
Ok {
FileName = file.FileName
Stream = memoryStream
ContentType = file.ContentType
}
| ValueNone -> Error $"File with key '{key}' not found"

Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module ServiceCollectionExtensions =
}
)
.AddHttpContextAccessor()
.AddScoped<IInputExecutionContext, HttpContextRequestExecutionContext>()
.AddScoped<GraphQLRequestHandler<'Root>, 'Handler>()

/// <summary>
Expand Down
Loading
Loading