Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/publish-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Add version to global.json
run: |
$version = "9.0.200"
$version = "9.0.304"
$globalJsonPath = "global.json"
$globalJson = Get-Content -Raw -Path $globalJsonPath | ConvertFrom-Json
if ($null -eq $globalJson.sdk.version) {
Expand All @@ -46,7 +46,7 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.200
dotnet-version: 9.0.304

- name: Add the GitHub source
run: dotnet nuget add source --username USERNAME --password ${{secrets.GITHUB_TOKEN}} --store-password-in-clear-text --name "github.com" "https://nuget.pkg.github.com/fsprojects/index.json"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

- name: Add version to global.json
run: |
$version = "9.0.200"
$version = "9.0.304"
$globalJsonPath = "global.json"
$globalJson = Get-Content -Raw -Path $globalJsonPath | ConvertFrom-Json
if ($null -eq $globalJson.sdk.version) {
Expand All @@ -48,7 +48,7 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.200
dotnet-version: 9.0.304

- name: Install local tools
run: dotnet tool restore
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest, macOS-latest]
dotnet: [9.0.200]
dotnet: [9.0.304]
runs-on: ${{ matrix.os }}

steps:
Expand Down
2 changes: 1 addition & 1 deletion build/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let ctx = Context.forceFakeContext ()
let embedAll = ctx.Arguments |> List.exists (fun arg -> arg = BuildArguments.EmbedAll)

module DotNetCli =
let setVersion (o : DotNet.Options) = { o with Version = Some "9.0.200" }
let setVersion (o : DotNet.Options) = { o with Version = Some "9.0.304" }
let setRestoreOptions (o : DotNet.RestoreOptions) = o.WithCommon setVersion

let configurationString = Environment.environVarOrDefault "CONFIGURATION" "Release"
Expand Down
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
Loading