diff --git a/.github/workflows/publish-ci.yml b/.github/workflows/publish-ci.yml index ef679a58c..75ed79ae9 100644 --- a/.github/workflows/publish-ci.yml +++ b/.github/workflows/publish-ci.yml @@ -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) { @@ -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" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4b42d09d8..41637d67d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -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) { @@ -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 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6d88fe463..276818780 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -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: diff --git a/build/Program.fs b/build/Program.fs index 77f231180..df94155ef 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -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" diff --git a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs index 59ee6d983..cfe2f4219 100644 --- a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs +++ b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs @@ -137,8 +137,8 @@ type internal ProvidedRecordTypeDefinition(className, baseType) = [] module internal Failures = - let uploadTypeIsNotScalar uploadTypeName = - failwith $"""Upload type "%s{uploadTypeName}" was found on the schema, but it is not a Scalar type. Upload types can only be used if they are defined as scalar types.""" + let uploadTypeIsNotScalarOrInputObject uploadTypeName = + failwith $"""Upload type "%s{uploadTypeName}" was found on the schema, but it is not a Scalar or InputObject type. Upload types can only be used if they are defined as scalar or input Object types.""" module internal ProvidedRecord = let ctor = typeof.GetConstructors().[0] @@ -307,21 +307,21 @@ module internal ProvidedOperation = | NamedType typeName -> match uploadInputTypeName with | Some uploadInputTypeName when typeName = uploadInputTypeName -> - variableName, TypeMapping.makeOption typeof + struct (variableName, typeName, TypeMapping.makeOption typeof) | _ -> match TypeMapping.scalar.TryFind(typeName) with - | Some t -> variableName, TypeMapping.makeOption t - | None when isScalar typeName -> variableName, typeof + | Some t -> struct (variableName,typeName, TypeMapping.makeOption t) + | None when isScalar typeName -> struct (variableName, typeName, typeof) | None -> match schemaProvidedTypes.TryFind(typeName) with - | Some t -> variableName, TypeMapping.makeOption t + | Some t -> struct (variableName, typeName, TypeMapping.makeOption t) | None -> failwith $"""Unable to find variable type "%s{typeName}" in the schema definition.""" | ListType itype -> - let name, t = mapVariable variableName itype - name, t |> TypeMapping.makeArray |> TypeMapping.makeOption + let struct (varName, typeName, t) = mapVariable variableName itype + struct (varName, typeName, t |> TypeMapping.makeArray |> TypeMapping.makeOption) | NonNullType itype -> - let name, t = mapVariable variableName itype - name, TypeMapping.unwrapOption t + let struct (varName, typeName, t) = mapVariable variableName itype + struct (varName, typeName, TypeMapping.unwrapOption t) operationDefinition.VariableDefinitions |> List.map (fun vdef -> mapVariable vdef.VariableName vdef.Type) let buildVariablesExprFromArgs (varNames : string list) (args : Expr list) = let mapVariableExpr (name : string) (value : Expr) = @@ -355,34 +355,43 @@ module internal ProvidedOperation = let methodOverloadDefinitions = let overloadsWithoutContext = let optionalVariables, requiredVariables = - variables |> List.partition (fun (_, t) -> isOption t) + variables |> List.partition (fun struct (_, _, t) -> isOption t) if explicitOptionalParameters then [requiredVariables @ optionalVariables] else List.combinations optionalVariables |> List.map (fun (optionalVariables, _) -> - let optionalVariables = optionalVariables |> List.map (fun (name, t) -> name, (TypeMapping.unwrapOption t)) + let optionalVariables = optionalVariables |> List.map (fun struct (varName, typeName, t) -> struct (varName, typeName, (TypeMapping.unwrapOption t))) requiredVariables @ optionalVariables) let overloadsWithContext = overloadsWithoutContext - |> List.map (fun var -> ("runtimeContext", typeof) :: var) + |> List.map (fun var -> struct ("runtimeContext", "GraphQLProviderRuntimeContext", typeof) :: var) match contextInfo with | Some _ -> overloadsWithoutContext @ overloadsWithContext | None -> overloadsWithContext // Multipart requests should only be used when the user specifies a upload type name AND the type // is present in the query as an input value. If not, we fallback to classic requests. let shouldUseMultipartRequest = - let rec existsUploadType (foundTypes : ProvidedTypeDefinition list) (t : Type) = + let rec existsUploadType (t : Type) = match t with - | :? ProvidedTypeDefinition as tdef when not (List.contains tdef foundTypes) -> tdef.DeclaredProperties |> Seq.exists ((fun p -> p.PropertyType) >> existsUploadType (tdef :: foundTypes)) - | Option t -> existsUploadType foundTypes t - | Array t -> existsUploadType foundTypes t + | Option t -> existsUploadType t + | Array t -> existsUploadType t | _ -> t = typeof - variables |> Seq.exists (snd >> existsUploadType []) + let rec existsUploadTypeDefinition (tdef : ProvidedTypeDefinition) = + tdef.DeclaredProperties |> Seq.exists ((fun p -> p.PropertyType) >> existsUploadType) + variables |> Seq.exists (fun struct (_, _, t) -> existsUploadType t) + || variables + |> Seq.where (fun struct (_, typeName, _) -> TypeMapping.scalar.TryGetValue typeName |> fst |> not) + |> Seq.choose (fun struct (_, typeName, _) -> schemaProvidedTypes |> Map.tryFind typeName) + |> Seq.exists existsUploadTypeDefinition let runMethodOverloads : MemberInfo list = let operationName = ValueOption.toObj operationDefinition.Name methodOverloadDefinitions |> List.map (fun overloadParameters -> - let variableNames = overloadParameters |> List.map fst |> List.filter (fun name -> name <> "runtimeContext") + let variableNames = + overloadParameters + |> Seq.map (fun struct (name, _, _) -> name) + |> Seq.filter (fun name -> name <> "runtimeContext") + |> Seq.toList let invoker (args : Expr list) = // First arg is the operation instance, second should be the context, if the overload asks for one. // We determine it by seeing if the variable names have one less item than the arguments without the instance. @@ -417,14 +426,18 @@ module internal ProvidedOperation = // If the user does not provide a context, we should dispose the default one after running the query if isDefaultContext then (context :> IDisposable).Dispose() OperationResultBase(response, responseJson, %%operationFieldsExpr, operationTypeName) @@> - let methodParameters = overloadParameters |> List.map (fun (name, t) -> ProvidedParameter(name, t, ?optionalValue = if isOption t then Some null else None)) + let methodParameters = overloadParameters |> List.map (fun struct (name, _, t) -> ProvidedParameter(name, t, ?optionalValue = if isOption t then Some null else None)) let methodDef = ProvidedMethod("Run", methodParameters, operationResultDef, invoker) methodDef.AddXmlDoc("Executes the operation on the server and fetch its results.") upcast methodDef) let asyncRunMethodOverloads : MemberInfo list = let operationName = ValueOption.toObj operationDefinition.Name methodOverloadDefinitions |> List.map (fun overloadParameters -> - let variableNames = overloadParameters |> List.map fst |> List.filter (fun name -> name <> "runtimeContext") + let variableNames = + overloadParameters + |> Seq.map (fun struct (name, _, _) -> name) + |> Seq.filter (fun name -> name <> "runtimeContext") + |> Seq.toList let invoker (args : Expr list) = // First arg is the operation instance, second should be the context, if the overload asks for one. // We determine it by seeing if the variable names have one less item than the arguments without the instance. @@ -462,7 +475,7 @@ module internal ProvidedOperation = if isDefaultContext then (context :> IDisposable).Dispose() return OperationResultBase(response, responseJson, %%operationFieldsExpr, operationTypeName) } @@> - let methodParameters = overloadParameters |> List.map (fun (name, t) -> ProvidedParameter(name, t, ?optionalValue = if isOption t then Some null else None)) + let methodParameters = overloadParameters |> List.map (fun struct (name, _, t) -> ProvidedParameter(name, t, ?optionalValue = if isOption t then Some null else None)) let methodDef = ProvidedMethod("AsyncRun", methodParameters, TypeMapping.makeAsync operationResultDef, invoker) methodDef.AddXmlDoc("Executes the operation asynchronously on the server and fetch its results.") upcast methodDef) @@ -516,14 +529,14 @@ module internal Provider = let rec getProvidedType (providedTypes : Dictionary) (schemaTypes : Map) (path : FieldStringPath) (astFields : AstFieldInfo list) (tref : IntrospectionTypeRef) : Type = match tref.Kind with | TypeKind.SCALAR when tref.Name.IsSome -> TypeMapping.mapScalarType uploadInputTypeName tref.Name.Value |> TypeMapping.makeOption - | _ when uploadInputTypeName.IsSome && tref.Name.IsSome && uploadInputTypeName.Value = tref.Name.Value -> uploadTypeIsNotScalar uploadInputTypeName.Value + | _ when uploadInputTypeName.IsSome && tref.Name.IsSome && uploadInputTypeName.Value = tref.Name.Value -> uploadTypeIsNotScalarOrInputObject uploadInputTypeName.Value | TypeKind.NON_NULL when tref.Name.IsNone && tref.OfType.IsSome -> getProvidedType providedTypes schemaTypes path astFields tref.OfType.Value |> TypeMapping.unwrapOption | TypeKind.LIST when tref.Name.IsNone && tref.OfType.IsSome -> getProvidedType providedTypes schemaTypes path astFields tref.OfType.Value |> TypeMapping.makeArray |> TypeMapping.makeOption | TypeKind.ENUM when tref.Name.IsSome -> match enumProvidedTypes.TryFind(tref.Name.Value) with | Some providedEnum -> TypeMapping.makeOption providedEnum | None -> failwith $"""Could not find a enum type based on a type reference. The reference is an "%s{tref.Name.Value}" enum, but that enum was not found in the introspection schema.""" - | (TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.UNION) when tref.Name.IsSome -> + | (TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.UNION ) when tref.Name.IsSome -> if providedTypes.ContainsKey(path, tref.Name.Value) then TypeMapping.makeOption providedTypes.[path, tref.Name.Value] else @@ -533,6 +546,7 @@ module internal Provider = | false, _ -> failwith $"""Could not find a schema type based on a type reference. The reference is to a "%s{typeName}" type, but that type was not found in the schema types.""" let getPropertyMetadata typeName (info : AstFieldInfo) : RecordPropertyMetadata = let ifield = + // TODO: Optimize this lookup using cache match getIntrospectionFields typeName |> Array.tryFind(fun f -> f.Name = info.Name) with | Some ifield -> ifield | None -> failwith $"""Could not find field "%s{info.Name}" of type "%s{tref.Name.Value}". The schema type does not have a field with the specified name.""" @@ -542,20 +556,21 @@ module internal Provider = { Name = info.Name; Alias = info.Alias; Description = ifield.Description; DeprecationReason = ifield.DeprecationReason; Type = ftype } let fragmentProperties = astFields - |> List.choose (function FragmentField f when f.TypeCondition <> tref.Name.Value -> Some f | _ -> None) - |> List.groupBy (fun field -> field.TypeCondition) - |> List.map (fun (typeCondition, fields) -> + |> Seq.vchoose (function FragmentField f when f.TypeCondition <> tref.Name.Value -> ValueSome f | _ -> ValueNone) + |> Seq.groupBy (fun field -> field.TypeCondition) + |> Seq.map (fun (typeCondition, fields) -> let conditionFields = fields |> Seq.distinctBy _.AliasOrName |> Seq.map FragmentField |> Seq.toList typeCondition, List.map (getPropertyMetadata typeCondition) conditionFields) let baseProperties = astFields - |> List.choose (fun x -> + |> Seq.vchoose (fun x -> match x with - | TypeField _ -> Some x - | FragmentField f when f.TypeCondition = tref.Name.Value -> Some x - | _ -> None) - |> List.distinctBy _.AliasOrName - |> List.map (getPropertyMetadata tref.Name.Value) + | TypeField _ -> ValueSome x + | FragmentField f when f.TypeCondition = tref.Name.Value -> ValueSome x + | _ -> ValueNone) + |> Seq.distinctBy _.AliasOrName + |> Seq.map (getPropertyMetadata tref.Name.Value) + |> Seq.toList let baseType = let metadata : ProvidedTypeMetadata = { Name = tref.Name.Value; Description = tref.Description } let tdef = ProvidedRecord.preBuildProvidedType(metadata, None) @@ -572,7 +587,7 @@ module internal Provider = providedTypes.Add((path, typeName), tdef) includeType path tdef ProvidedRecord.makeProvidedType(tdef, properties, explicitOptionalParameters) |> ignore - fragmentProperties |> List.iter createFragmentType + fragmentProperties |> Seq.iter createFragmentType TypeMapping.makeOption baseType | _ -> failwith "Could not find a schema type based on a type reference. The reference has an invalid or unsupported combination of Name, Kind and OfType fields." let operationType = getProvidedType providedTypes schemaTypes [] operationAstFields operationTypeRef @@ -606,7 +621,8 @@ module internal Provider = DeprecationReason = field.DeprecationReason Type = providedType } |> makeOption - | _ when uploadInputTypeName.IsSome && field.Type.Name.IsSome && uploadInputTypeName.Value = field.Type.Name.Value -> uploadTypeIsNotScalar uploadInputTypeName.Value + | _ when uploadInputTypeName.IsSome && field.Type.Name.IsSome && uploadInputTypeName.Value = field.Type.Name.Value && field.Type.Kind <> TypeKind.INPUT_OBJECT -> + uploadTypeIsNotScalarOrInputObject uploadInputTypeName.Value | TypeKind.NON_NULL when field.Type.Name.IsNone && field.Type.OfType.IsSome -> ofFieldType field |> resolveFieldMetadata |> unwrapOption | TypeKind.LIST when field.Type.Name.IsNone && field.Type.OfType.IsSome -> ofFieldType field |> resolveFieldMetadata |> makeArrayOption | (TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.INPUT_OBJECT | TypeKind.UNION | TypeKind.ENUM) when field.Type.Name.IsSome -> @@ -629,12 +645,17 @@ module internal Provider = DeprecationReason = None Type = providedType } |> makeOption - | _ when uploadInputTypeName.IsSome && field.Type.Name.IsSome && uploadInputTypeName.Value = field.Type.Name.Value -> uploadTypeIsNotScalar uploadInputTypeName.Value + | _ when uploadInputTypeName.IsSome && field.Type.Name.IsSome && uploadInputTypeName.Value = field.Type.Name.Value && field.Type.Kind <> TypeKind.INPUT_OBJECT -> + uploadTypeIsNotScalarOrInputObject uploadInputTypeName.Value | TypeKind.NON_NULL when field.Type.Name.IsNone && field.Type.OfType.IsSome -> ofInputFieldType field |> resolveInputFieldMetadata |> unwrapOption | TypeKind.LIST when field.Type.Name.IsNone && field.Type.OfType.IsSome -> ofInputFieldType field |> resolveInputFieldMetadata |> makeArrayOption - | (TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.INPUT_OBJECT | TypeKind.UNION | TypeKind.ENUM) when field.Type.Name.IsSome -> + | (TypeKind.OBJECT | TypeKind.INTERFACE | TypeKind.INPUT_OBJECT | TypeKind.UNION | TypeKind.ENUM) as kind when field.Type.Name.IsSome -> let itype = getSchemaType field.Type - let providedType = resolveProvidedType itype + let providedType = + if kind = TypeKind.INPUT_OBJECT && uploadInputTypeName |> Option.exists ((=) itype.Name) then + typeof + else + resolveProvidedType itype { Name = field.Name Alias = ValueNone Description = field.Description @@ -654,8 +675,8 @@ module internal Provider = let properties = itype.Fields |> Option.defaultValue [||] - |> Array.map resolveFieldMetadata - |> List.ofArray + |> Seq.map resolveFieldMetadata + |> Seq.toList upcast ProvidedRecord.makeProvidedType(tdef, properties, explicitOptionalParameters) | TypeKind.INPUT_OBJECT -> let tdef = ProvidedRecord.preBuildProvidedType(metadata, None) @@ -663,8 +684,8 @@ module internal Provider = let properties = itype.InputFields |> Option.defaultValue [||] - |> Array.map resolveInputFieldMetadata - |> List.ofArray + |> Seq.map resolveInputFieldMetadata + |> Seq.toList upcast ProvidedRecord.makeProvidedType(tdef, properties, explicitOptionalParameters) | TypeKind.INTERFACE | TypeKind.UNION -> let bdef = ProvidedInterface.makeProvidedType(metadata) diff --git a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs index 0a90b5359..6a757fc2d 100644 --- a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs +++ b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs @@ -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 @@ -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 - /// 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 + /// 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) = task { let! response = response - return response.EnsureSuccessStatusCode() + return response.EnsureSuccessStatusCode () } let private addHeaders (httpHeaders : seq) (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) (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. @@ -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 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 () diff --git a/src/FSharp.Data.GraphQL.Client/MimeTypes.fs b/src/FSharp.Data.GraphQL.Client/MimeTypes.fs index d8cc5b6a3..aafd8288e 100644 --- a/src/FSharp.Data.GraphQL.Client/MimeTypes.fs +++ b/src/FSharp.Data.GraphQL.Client/MimeTypes.fs @@ -5,8 +5,8 @@ open System.Collections.ObjectModel open System let private dictBuilder() : IReadOnlyDictionary = - let types = - [| ".323", "text/h323" + let types = [| + ".323", "text/h323" ".3g2", "video/3gpp2" ".3gp", "video/3gpp" ".3gp2", "video/3gpp2" diff --git a/src/FSharp.Data.GraphQL.Client/Serialization.fs b/src/FSharp.Data.GraphQL.Client/Serialization.fs index 9eddb8a3e..6572c1aae 100644 --- a/src/FSharp.Data.GraphQL.Client/Serialization.fs +++ b/src/FSharp.Data.GraphQL.Client/Serialization.fs @@ -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 @@ -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 as items -> items |> Seq.map (fun (KeyValue (k, v)) -> k.FirstCharLower(), toJsonValue v) diff --git a/src/FSharp.Data.GraphQL.Client/Upload.fs b/src/FSharp.Data.GraphQL.Client/Upload.fs index 56a498373..add7f6ee7 100644 --- a/src/FSharp.Data.GraphQL.Client/Upload.fs +++ b/src/FSharp.Data.GraphQL.Client/Upload.fs @@ -5,16 +5,25 @@ 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, [] name : string | null, [] contentType : string | null, [] ownsStream : bool) + = + + new (bytes : byte[], fileName, [] name, [] 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, [] name, [] contentType) = new Upload (stream, fileName, name, contentType, ownsStream = false) /// Gets the stream associated to this Upload type. member _.Stream = stream @@ -22,19 +31,28 @@ type Upload (stream : Stream, fileName : string, ?contentType : string, ?ownsStr /// 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 uniquely identify upload throughout multiple uploads + /// 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 () diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs index e15a55e2a..7ffab5692 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs @@ -1,5 +1,6 @@ -namespace FSharp.Data.GraphQL.Server.AspNetCore +namespace FSharp.Data.GraphQL.Server.AspNetCore +open System.IO open FSharp.Data.GraphQL open Microsoft.AspNetCore.Http @@ -7,13 +8,20 @@ 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.Seek (0L, SeekOrigin.Begin) |> ignore + Ok { + FileName = file.FileName + Stream = memoryStream + ContentType = file.ContentType + } | ValueNone -> Error $"File with key '{key}' not found" - diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs index 71e7409ef..61d17d903 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/StartupExtensions.fs @@ -84,6 +84,7 @@ module ServiceCollectionExtensions = } ) .AddHttpContextAccessor() + .AddScoped() .AddScoped, 'Handler>() /// diff --git a/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs b/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs index 8e1d654a0..9e102c890 100644 --- a/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs +++ b/src/FSharp.Data.GraphQL.Server.Giraffe/HttpHandlers.fs @@ -1,6 +1,7 @@ namespace FSharp.Data.GraphQL.Server.AspNetCore.Giraffe open System.Threading.Tasks +open System.Net.Mime open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection @@ -21,14 +22,22 @@ module HttpHandlers = return Some ctx } - let ofTaskIResult2 ctx (taskRes: Task>) : HttpFuncResult = - taskRes - |> TaskResult.defaultWith id - |> ofTaskIResult ctx + let ofTaskIResult2 ctx (taskRes : Task>) : HttpFuncResult = taskRes |> TaskResult.defaultWith id |> ofTaskIResult ctx let private handleGraphQL<'Root> (next : HttpFunc) (ctx : HttpContext) = - let request = ctx.RequestServices.GetRequiredService>() + let request = ctx.RequestServices.GetRequiredService> () request.HandleAsync () |> ofTaskIResult2 ctx let graphQL<'Root> : HttpHandler = choose [ POST; GET ] >=> handleGraphQL<'Root> + + let private isMultipartRequest (req : HttpRequest) = + not (System.String.IsNullOrEmpty (req.ContentType)) + && req.ContentType.Contains (MediaTypeNames.Multipart.FormData) + + let setRequestType : HttpHandler = + fun (next) (ctx) -> + if isMultipartRequest ctx.Request then + setHttpHeader "Request-Type" "Multipart" next ctx + else + setHttpHeader "Request-Type" "Classic" next ctx diff --git a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj index 6da1f3c22..d97cc68fd 100644 --- a/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj +++ b/src/FSharp.Data.GraphQL.Server/FSharp.Data.GraphQL.Server.fsproj @@ -33,6 +33,7 @@ + diff --git a/src/FSharp.Data.GraphQL.Shared/InputContext.fs b/src/FSharp.Data.GraphQL.Shared/InputContext.fs index b03d5eb23..853a507b4 100644 --- a/src/FSharp.Data.GraphQL.Shared/InputContext.fs +++ b/src/FSharp.Data.GraphQL.Shared/InputContext.fs @@ -1,6 +1,7 @@ -namespace FSharp.Data.GraphQL +namespace FSharp.Data.GraphQL type FileData = { + FileName : string Stream : System.IO.Stream ContentType : string } diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index 1b5d0cf65..ec60993c0 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -482,9 +482,9 @@ module SchemaDefinitions = CoerceInput = coerceGuidInput CoerceOutput = coerceGuidValue } - /// Defines an object list filter for use as an argument for filter list of object fields. + /// Defines a file that is uploaded with a request let FileType : InputCustomDefinition = { - Name = "FileType" + Name = "File" Description = Some "The `File` type represents a file 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." @@ -502,7 +502,12 @@ module SchemaDefinitions = match c with | StringValue strValue -> getFileData strValue | VariableName varName -> - Ok (variables[varName] :?> FileData) + let fileData : FileData | null = + match variables.TryGetValue varName with + | true, null -> null + | true, value -> value :?> FileData + | false, _ -> null + Ok fileData | _ -> IGQLError.createResultErrorList "Only a string value or a variable with a string value can be used as a file name." | Variable json -> match (json |> InputValue.OfJsonElement) with diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/CustomSchemaTypes.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/CustomSchemaTypes.fs deleted file mode 100644 index e117c8c33..000000000 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/CustomSchemaTypes.fs +++ /dev/null @@ -1,35 +0,0 @@ -namespace FSharp.Data.GraphQL.Samples.StarWarsApi - -open System.IO -open FSharp.Data.GraphQL -open FSharp.Data.GraphQL.Types - -/// Represents a file in a GraphQL file upload. -type File = - /// Gets the name of the file. - { Name : string - /// Gets the type of the content of the file. - ContentType : string - /// Gets a stream which returns the content of the file when read. - Content : Stream } - -/// Contains customized schema definitions for extensibility features. -[] -module SchemaDefinitions = - - let private coerceUploadInput (_ : InputParameterValue) : Result = - Result.Error [ - { new IGQLError with member _.Message = "Cannot coerce upload input. The type `Upload` can only be passed as a variable through a multipart request." } - ] - - let private coerceUploadValue (value : obj) = - match value with - | :? File as file -> Some file - | _ -> None - - /// GraphQL type for binary data stream representation. - let Upload : ScalarDefinition = - { Name = "Upload" - Description = Some "The `Upload` type represents an upload of binary data." - CoerceInput = coerceUploadInput - CoerceOutput = coerceUploadValue } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj index 51f15be26..8120817b6 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/FSharp.Data.GraphQL.IntegrationTests.Server.fsproj @@ -2,6 +2,7 @@ $(DotNetVersion) + FSharp.Data.GraphQL.IntegrationTests.Server @@ -14,7 +15,6 @@ - diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs index b3f314c66..970aa5961 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Schema.fs @@ -27,8 +27,6 @@ type Input = { Single : InputField option List : InputField list option } -type InputFile = { File : System.IO.Stream} - type UploadedFile = { Name : string ContentType : string @@ -39,10 +37,10 @@ type UploadedContentFile = ContentAsText : string } type UploadRequest = - { Single : File - Multiple : File list - NullableMultiple : File list option - NullableMultipleNullable : File option list option } + { Single : FileData + Multiple : FileData list + NullableMultiple : FileData list option + NullableMultipleNullable : FileData option list option } type UploadResponse = { Single : UploadedFile @@ -101,10 +99,10 @@ module Schema = name = "UploadRequest", description = "Request for uploading files in several different forms.", fields = - [ Define.Input("single", Upload, description = "A single file upload.") - Define.Input("multiple", ListOf Upload, description = "Multiple file uploads.") - Define.Input("nullableMultiple", Nullable (ListOf Upload), description = "Optional list of multiple file uploads.") - Define.Input("nullableMultipleNullable", Nullable (ListOf (Nullable Upload)), description = "Optional list of multiple optional file uploads.") ]) + [ Define.Input("single", FileType, description = "A single file upload.") + Define.Input("multiple", ListOf FileType, description = "Multiple file uploads.") + Define.Input("nullableMultiple", Nullable (ListOf FileType), description = "Optional list of multiple file uploads.") + Define.Input("nullableMultipleNullable", Nullable (ListOf (Nullable FileType)), description = "Optional list of multiple optional file uploads.") ]) let UploadResponseType = Define.Object( @@ -143,71 +141,70 @@ module Schema = Define.Input("file", FileType) ]) - // let MutationType = - // let contentAsText (stream : System.IO.Stream) = - // use reader = new System.IO.StreamReader(stream, Encoding.UTF8) - // reader.ReadToEnd() - // let mapUploadToOutput (file : File) = - // { Name = file.Name; ContentType = file.ContentType; ContentAsText = contentAsText file.Content } - // let mapUploadRequestToOutput (request : UploadRequest) = - // { Single = mapUploadToOutput request.Single - // Multiple = request.Multiple |> List.map mapUploadToOutput - // NullableMultiple = request.NullableMultiple |> Option.map (List.map mapUploadToOutput) - // NullableMultipleNullable = request.NullableMultipleNullable |> Option.map (List.map (Option.map mapUploadToOutput)) } - // Define.Object( - // name = "Mutation", - // fields = - // [ Define.Field( - // name = "singleUpload", - // typedef = UploadedFileType, - // description = "Uploads a single file to the server and get it back.", - // args = [ Define.Input("file", Upload, description = "The file to be uploaded.") ], - // resolve = fun ctx _ -> mapUploadToOutput (ctx.Arg("file"))) - // Define.Field( - // name = "nullableSingleUpload", - // typedef = StructNullable UploadedFileType, - // description = "Uploads (maybe) a single file to the server and get it back (maybe).", - // args = [ Define.Input("file", Nullable Upload, description = "The file to be uploaded.") ], - // resolve = fun ctx _ -> ctx.TryArg("file") |> ValueOption.flatten |> ValueOption.map mapUploadToOutput) - // Define.Field( - // name = "multipleUpload", - // typedef = ListOf UploadedFileType, - // description = "Uploads a list of files to the server and get them back.", - // args = [ Define.Input("files", ListOf Upload, description = "The files to upload.") ], - // resolve = fun ctx _ -> ctx.Arg("files") |> Seq.map mapUploadToOutput) - // Define.Field( - // name = "nullableMultipleUpload", - // typedef = StructNullable (ListOf UploadedFileType), - // description = "Uploads (maybe) a list of files to the server and get them back (maybe).", - // args = [ Define.Input("files", Nullable (ListOf Upload), description = "The files to upload.") ], - // resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.flatten |> ValueOption.map (Seq.map mapUploadToOutput)) - // Define.Field( - // name = "nullableMultipleNullableUpload", - // typedef = StructNullable (ListOf (Nullable UploadedFileType)), - // description = "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", - // args = [ Define.Input("files", Nullable (ListOf (Nullable Upload)), description = "The files to upload.") ], - // resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.flatten |> ValueOption.map (Seq.map (Option.map mapUploadToOutput))) - // Define.Field( - // name = "uploadRequest", - // typedef = UploadResponseType, - // description = "Upload several files in different forms.", - // args = [ Define.Input("request", UploadRequestType, description = "The request for uploading several files in different forms.") ], - // resolve = fun ctx _ -> mapUploadRequestToOutput (ctx.Arg("request"))) ]) let MutationType = + let contentAsText (stream : System.IO.Stream) = + use reader = new System.IO.StreamReader(stream, Encoding.UTF8) + reader.ReadToEnd() let getFileContent (ctx : ResolveFieldContext) argName = let stream = ctx.Arg argName use reader = new System.IO.StreamReader(stream, Encoding.UTF8, true) reader.ReadToEnd() - - Define.Object ( - name = "MutationType", + let mapUploadToOutput (file : FileData) = + { Name = file.FileName; ContentType = file.ContentType; ContentAsText = contentAsText file.Stream } + let mapUploadRequestToOutput (request : UploadRequest) = + { + Single = mapUploadToOutput request.Single + Multiple = request.Multiple |> List.map mapUploadToOutput + NullableMultiple = request.NullableMultiple |> Option.map (List.map mapUploadToOutput) + NullableMultipleNullable = request.NullableMultipleNullable |> Option.map (List.map (Option.map mapUploadToOutput)) + } + Define.Object( + name = "Mutation", fields = - [ Define.Field ("uploadFile", StringType, "", [ Define.Input ("input", FileType) ], - (fun ctx _ -> getFileContent ctx "input")); - Define.Field ("uploadFileComplex", StringType, "", [ Define.Input ("input", InputFileObject) ], - (fun ctx _ -> getFileContent ctx "input")) - ] - ) + [ + Define.Field( + name = "singleUpload", + typedef = UploadedFileType, + description = "Uploads a single file to the server and get it back.", + args = [ Define.Input("file", FileType, description = "The file to be uploaded.") ], + resolve = fun ctx _ -> mapUploadToOutput (ctx.Arg("file"))) + Define.Field( + name = "nullableSingleUpload", + typedef = StructNullable UploadedFileType, + description = "Uploads (maybe) a single file to the server and get it back (maybe).", + args = [ Define.Input("file", Nullable FileType, description = "The file to be uploaded.") ], + resolve = fun ctx _ -> ctx.TryArg("file") |> ValueOption.map mapUploadToOutput) + Define.Field( + name = "multipleUpload", + typedef = ListOf UploadedFileType, + description = "Uploads a list of files to the server and get them back.", + args = [ Define.Input("files", ListOf FileType, description = "The files to upload.") ], + resolve = fun ctx _ -> ctx.Arg("files") |> List.map mapUploadToOutput) + Define.Field( + name = "nullableMultipleUpload", + typedef = StructNullable (ListOf UploadedFileType), + description = "Uploads (maybe) a list of files to the server and get them back (maybe).", + args = [ Define.Input("files", Nullable (ListOf FileType), description = "The files to upload.") ], + resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.map (List.map mapUploadToOutput)) + Define.Field( + name = "nullableMultipleNullableUpload", + typedef = StructNullable (ListOf (Nullable UploadedFileType)), + description = "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", + args = [ Define.Input("files", Nullable (ListOf (Nullable FileType)), description = "The files to upload.") ], + resolve = fun ctx _ -> ctx.TryArg("files") |> ValueOption.map (List.map (Option.map mapUploadToOutput))) + Define.Field( + name = "uploadRequest", + typedef = UploadResponseType, + description = "Upload several files in different forms.", + args = [ Define.Input("request", UploadRequestType, description = "The request for uploading several files in different forms.") ], + resolve = fun ctx _ -> mapUploadRequestToOutput (ctx.Arg("request"))) + Define.Field ( + name = "uploadComplex", + typedef = StringType, + description = "", + args = [ Define.Input ("input", InputFileObject) ], + resolve = fun ctx _ -> getFileContent ctx "input") + ]) let schema : ISchema = upcast Schema(QueryType, MutationType) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs index a2414575b..f354714a6 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests.Server/Startup.fs @@ -44,7 +44,7 @@ type Startup private () = app .UseGiraffeErrorHandler(errorHandler) .UseGiraffe ( - (setHttpHeader "Request-Type" "Classic") + HttpHandlers.setRequestType >=> HttpHandlers.graphQL) member val Configuration : IConfiguration = null with get, set diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/Helpers.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/Helpers.fs index 846ab1b0b..babc6b7f6 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/Helpers.fs @@ -3,41 +3,44 @@ module FSharp.Data.GraphQL.IntegrationTests.Helpers open Xunit open System.Text open System.Collections.Generic +open System.Runtime.InteropServices open FSharp.Data.GraphQL let normalize (x : string) = - x.Replace("\r\n", "\n").Split([|'\n'|]) - |> Array.map (fun x -> x.Trim()) + x.Replace("\r\n", "\n").Split ('\n') + |> Array.map _.Trim() |> Array.reduce (fun x y -> x + "\n" + y) -let equals (expected : 'T) (actual : 'T) = - Assert.Equal<'T>(expected, actual) +let equals (expected : 'T) (actual : 'T) = Assert.Equal<'T> (expected, actual) -let hasItems (seq : seq<'T>) = - Assert.True(Seq.length seq > 0) +let hasItems (seq : seq<'T>) = Assert.True (Seq.length seq > 0) let map fn x = fn x -let checkRequestTypeHeader requestType (operationResult: OperationResultBase) = +let checkRequestTypeHeader requestType (operationResult : OperationResultBase) = match operationResult.Headers.TryGetValues "Request-Type" with | true, values -> values |> Seq.contains requestType |> Assert.True - | false, _ -> Assert.Fail("Request-Type header not found") - - -type File = - { Name : string - ContentType : string - Content : string } - member x.MakeUpload() = - let bytes = Encoding.UTF8.GetBytes(x.Content) - new Upload(bytes, x.Name, x.ContentType) - static member FromDictionary(dict : IDictionary) = - { Name = downcast dict.["Name"] - ContentType = downcast dict.["ContentType"] - Content = downcast dict.["ContentAsText"] } - -type FilesRequest = - { Single : File - Multiple : File [] - NullableMultiple : File [] option - NullableMultipleNullable : File option [] option } + | false, _ -> Assert.Fail ("Request-Type header not found") + + +type File = { + Name : string + ContentType : string + Content : string +} with + + member x.MakeUpload ([] uploadName) = + let bytes = Encoding.UTF8.GetBytes (x.Content) + new Upload (bytes, x.Name, uploadName, x.ContentType) + static member FromDictionary (dict : IDictionary) = { + Name = downcast dict.["Name"] + ContentType = downcast dict.["ContentType"] + Content = downcast dict.["ContentAsText"] + } + +type FilesRequest = { + Single : File + Multiple : File[] + NullableMultiple : File[] option + NullableMultipleNullable : File option[] option +} diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs index 3479825ab..b7f6b9ae1 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs @@ -8,7 +8,7 @@ open Helpers let [] ServerUrl = "http://localhost:8085" let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider // type FileProvider = GraphQLProvider let context = Provider.GetContext(ServerUrl) @@ -201,7 +201,7 @@ let ``Should be able to execute a query using context, sending an input field wi module SingleRequiredUploadOperation = let operation = - Provider.Operation<"""mutation SingleUpload($file: Upload!) { + Provider.Operation<"""mutation SingleUpload($file: File!) { singleUpload(file: $file) { name contentType @@ -218,13 +218,13 @@ module SingleRequiredUploadOperation = result.Data.Value.SingleUpload.ContentAsText |> equals file.Content result.Data.Value.SingleUpload.ContentType |> equals file.ContentType -[] +[] let ``Should be able to execute a single required upload``() = - let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } + let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } SingleRequiredUploadOperation.operation.Run(file.MakeUpload()) |> SingleRequiredUploadOperation.validateResult file -[] +[] let ``Should be able to execute a single required upload asynchronously``() : Task = task { let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } let! result = SingleRequiredUploadOperation.operation.AsyncRun(file.MakeUpload()) @@ -233,7 +233,7 @@ let ``Should be able to execute a single required upload asynchronously``() : Ta module SingleOptionalUploadOperation = let operation = - Provider.Operation<"""mutation NullableSingleUpload($file: Upload) { + Provider.Operation<"""mutation NullableSingleUpload($file: File) { nullableSingleUpload(file: $file) { name contentType @@ -253,31 +253,25 @@ module SingleOptionalUploadOperation = result.Data.Value.NullableSingleUpload.Value.ContentType |> equals file.ContentType) -// [] -// let ``Should be able to execute a upload by passing a file with new approach``() = -// let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } -// let result = SingleOptionalUploadOperation.fileOperation.Run(file.MakeUpload()) -// |> SingleOptionalUploadOperation.validateResult (Some file) - -[] +[] let ``Should be able to execute a single optional upload by passing a file``() = let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } SingleOptionalUploadOperation.operation.Run(file.MakeUpload()) |> SingleOptionalUploadOperation.validateResult (Some file) -[] +[] let ``Should be able to execute a single optional upload by passing a file, asynchronously``() : Task = task { let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } let! result = SingleOptionalUploadOperation.operation.AsyncRun(file.MakeUpload()) result |> SingleOptionalUploadOperation.validateResult (Some file) } -[] +[] let ``Should be able to execute a single optional upload by not passing a file``() = SingleOptionalUploadOperation.operation.Run() |> SingleOptionalUploadOperation.validateResult None -[] +[] let ``Should be able to execute a single optional upload by not passing a file asynchronously``() : Task = task { let! result = SingleOptionalUploadOperation.operation.AsyncRun() result |> SingleOptionalUploadOperation.validateResult None @@ -285,7 +279,7 @@ let ``Should be able to execute a single optional upload by not passing a file a module RequiredMultipleUploadOperation = let operation = - Provider.Operation<"""mutation MultipleUpload($files: [Upload!]!) { + Provider.Operation<"""mutation MultipleUpload($files: [File!]!) { multipleUpload(files: $files) { name contentType @@ -303,7 +297,7 @@ module RequiredMultipleUploadOperation = |> Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple required upload``() = let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -311,7 +305,7 @@ let ``Should be able to execute a multiple required upload``() = RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) |> RequiredMultipleUploadOperation.validateResult files -[] +[] let ``Should be able to execute a multiple required upload asynchronously``() : Task = task { let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -322,7 +316,7 @@ let ``Should be able to execute a multiple required upload asynchronously``() : module OptionalMultipleUploadOperation = let operation = - Provider.Operation<"""mutation NullableMultipleUpload($files: [Upload!]) { + Provider.Operation<"""mutation NullableMultipleUpload($files: [File!]) { nullableMultipleUpload(files: $files) { name contentType @@ -340,7 +334,7 @@ module OptionalMultipleUploadOperation = |> Option.map (Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText })) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple upload``() = let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -348,7 +342,7 @@ let ``Should be able to execute a multiple upload``() = OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) |> OptionalMultipleUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple upload asynchronously``() : Task = task { let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -357,12 +351,12 @@ let ``Should be able to execute a multiple upload asynchronously``() : Task = ta result |> OptionalMultipleUploadOperation.validateResult (Some files) } -[] +[] let ``Should be able to execute a multiple upload by sending no uploads``() = OptionalMultipleUploadOperation.operation.Run() |> OptionalMultipleUploadOperation.validateResult None -[] +[] let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { let! result = OptionalMultipleUploadOperation.operation.AsyncRun() result |> OptionalMultipleUploadOperation.validateResult None @@ -370,7 +364,7 @@ let ``Should be able to execute a multiple upload asynchronously by sending no u module OptionalMultipleOptionalUploadOperation = let operation = - Provider.Operation<"""mutation NullableMultipleNullableUpload($files: [Upload]) { + Provider.Operation<"""mutation NullableMultipleNullableUpload($files: [File]) { nullableMultipleNullableUpload(files: $files) { name contentType @@ -388,7 +382,7 @@ module OptionalMultipleOptionalUploadOperation = |> Option.map (Array.map (Option.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }))) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple optional upload``() = let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -396,7 +390,7 @@ let ``Should be able to execute a multiple optional upload``() = OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple optional upload asynchronously``() : Task = task { let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -405,18 +399,18 @@ let ``Should be able to execute a multiple optional upload asynchronously``() : result |> (OptionalMultipleOptionalUploadOperation.validateResult (Some files)) } -[] +[] let ``Should be able to execute a multiple optional upload by sending no uploads``() = OptionalMultipleOptionalUploadOperation.operation.Run() |> OptionalMultipleOptionalUploadOperation.validateResult None -[] +[] let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads``() : Task = task { let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun() result |> OptionalMultipleOptionalUploadOperation.validateResult None } -[] +[] let ``Should be able to execute a multiple optional upload by sending some uploads``() = let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -426,7 +420,7 @@ let ``Should be able to execute a multiple optional upload by sending some uploa OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads``() : Task = task { let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -474,7 +468,7 @@ module UploadRequestOperation = result.Data.Value.UploadRequest.NullableMultiple |> Option.map (Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary)) |> equals request.NullableMultiple result.Data.Value.UploadRequest.NullableMultipleNullable |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary()) >> File.FromDictionary))) |> equals request.NullableMultipleNullable -[] +[] let ``Should be able to upload files inside another input type``() : Task = task { let request = { Single = { Name = "single.txt"; ContentType = "text/plain"; Content = "Single file content" } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs index c0af96b88..92fc1cae1 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs @@ -8,7 +8,7 @@ open Helpers let [] ServerUrl = "http://localhost:8085" let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider let context = Provider.GetContext(ServerUrl) @@ -200,7 +200,7 @@ let ``Should be able to execute a query using context, sending an input field wi module SingleRequiredUploadOperation = let operation = - Provider.Operation<"""mutation SingleUpload($file: Upload!) { + Provider.Operation<"""mutation SingleUpload($file: File!) { singleUpload(file: $file) { name contentType @@ -217,13 +217,13 @@ module SingleRequiredUploadOperation = result.Data.Value.SingleUpload.ContentAsText |> equals file.Content result.Data.Value.SingleUpload.ContentType |> equals file.ContentType -[] +[] let ``Should be able to execute a single required upload``() = let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - SingleRequiredUploadOperation.operation.Run(file.MakeUpload()) + SingleRequiredUploadOperation.operation.Run(file.MakeUpload(file.Name)) |> SingleRequiredUploadOperation.validateResult file -[] +[] let ``Should be able to execute a single required upload asynchronously``() : Task = task { let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } let! result = SingleRequiredUploadOperation.operation.AsyncRun(file.MakeUpload()) @@ -232,7 +232,7 @@ let ``Should be able to execute a single required upload asynchronously``() : Ta module SingleOptionalUploadOperation = let operation = - Provider.Operation<"""mutation NullableSingleUpload($file: Upload) { + Provider.Operation<"""mutation NullableSingleUpload($file: File) { nullableSingleUpload(file: $file) { name contentType @@ -251,25 +251,25 @@ module SingleOptionalUploadOperation = result.Data.Value.NullableSingleUpload.Value.ContentAsText |> equals file.Content result.Data.Value.NullableSingleUpload.Value.ContentType |> equals file.ContentType) -[] +[] let ``Should be able to execute a single optional upload by passing a file``() = let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } SingleOptionalUploadOperation.operation.Run(file.MakeUpload() |> Some) |> SingleOptionalUploadOperation.validateResult (Some file) -[] +[] let ``Should be able to execute a single optional upload by passing a file, asynchronously``() : Task = task { let file = { Name = "file.txt"; ContentType = "text/plain"; Content = "Sample text file contents" } - let! result = SingleOptionalUploadOperation.operation.AsyncRun(file.MakeUpload() |> Some) + let! result = SingleOptionalUploadOperation.operation.AsyncRun(file.MakeUpload("test") |> Some) result |> SingleOptionalUploadOperation.validateResult (Some file) } -[] +[] let ``Should be able to execute a single optional upload by not passing a file``() = SingleOptionalUploadOperation.operation.Run() |> SingleOptionalUploadOperation.validateResult None -[] +[] let ``Should be able to execute a single optional upload by not passing a file asynchronously``() : Task = task { let! result = SingleOptionalUploadOperation.operation.AsyncRun() result |> SingleOptionalUploadOperation.validateResult None @@ -277,7 +277,7 @@ let ``Should be able to execute a single optional upload by not passing a file a module RequiredMultipleUploadOperation = let operation = - Provider.Operation<"""mutation MultipleUpload($files: [Upload!]!) { + Provider.Operation<"""mutation MultipleUpload($files: [File!]!) { multipleUpload(files: $files) { name contentType @@ -295,7 +295,7 @@ module RequiredMultipleUploadOperation = |> Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple required upload``() = let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -303,7 +303,7 @@ let ``Should be able to execute a multiple required upload``() = RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) |> RequiredMultipleUploadOperation.validateResult files -[] +[] let ``Should be able to execute a multiple required upload asynchronously``() : Task = task { let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -314,7 +314,7 @@ let ``Should be able to execute a multiple required upload asynchronously``() : module OptionalMultipleUploadOperation = let operation = - Provider.Operation<"""mutation NullableMultipleUpload($files: [Upload!]) { + Provider.Operation<"""mutation NullableMultipleUpload($files: [File!]) { nullableMultipleUpload(files: $files) { name contentType @@ -332,7 +332,7 @@ module OptionalMultipleUploadOperation = |> Option.map (Array.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText })) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple upload``() = let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } @@ -340,21 +340,21 @@ let ``Should be able to execute a multiple upload``() = OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload()) |> Some) |> OptionalMultipleUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple upload asynchronously``() : Task = task { let files = [| { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload()) |> Some) + let! result = OptionalMultipleUploadOperation.operation.AsyncRun((files |> Array.map _.MakeUpload()) |> Some) result |> OptionalMultipleUploadOperation.validateResult (Some files) } -[] +[] let ``Should be able to execute a multiple upload by sending no uploads``() = OptionalMultipleUploadOperation.operation.Run() |> OptionalMultipleUploadOperation.validateResult None -[] +[] let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { let! result = OptionalMultipleUploadOperation.operation.AsyncRun() result |> OptionalMultipleUploadOperation.validateResult None @@ -362,7 +362,7 @@ let ``Should be able to execute a multiple upload asynchronously by sending no u module OptionalMultipleOptionalUploadOperation = let operation = - Provider.Operation<"""mutation NullableMultipleNullableUpload($files: [Upload]) { + Provider.Operation<"""mutation NullableMultipleNullableUpload($files: [File]) { nullableMultipleNullableUpload(files: $files) { name contentType @@ -380,52 +380,52 @@ module OptionalMultipleOptionalUploadOperation = |> Option.map (Array.map (Option.map (fun file -> { Name = file.Name; ContentType = file.ContentType; Content = file.ContentAsText }))) receivedFiles |> equals files -[] +[] let ``Should be able to execute a multiple optional upload``() = let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload())) |> Some) + OptionalMultipleOptionalUploadOperation.operation.Run((files |> Array.map (Option.map _.MakeUpload())) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple optional upload asynchronously``() : Task = task { let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map (fun f -> f.MakeUpload())) |> Some) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun((files |> Array.map (Option.map _.MakeUpload())) |> Some) result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } -[] +[] let ``Should be able to execute a multiple optional upload by sending no uploads``() = OptionalMultipleOptionalUploadOperation.operation.Run() |> OptionalMultipleOptionalUploadOperation.validateResult None -[] +[] let ``Should be able to execute a multiple optional upload asynchronously by sending no uploads``() : Task = task { let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun() result |> OptionalMultipleOptionalUploadOperation.validateResult None } -[] +[] let ``Should be able to execute a multiple optional upload by sending some uploads``() = let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } None Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } None |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map (fun f -> f.MakeUpload())) |> Some) + OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map _.MakeUpload()) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) -[] +[] let ``Should be able to execute a multiple optional upload asynchronously by sending some uploads``() : Task = task { let files = [| Some { Name = "file1.txt"; ContentType = "text/plain"; Content = "Sample text file contents 1" } None Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } None |] - let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map (fun f -> f.MakeUpload())) |> Some) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(files |> Array.map (Option.map _.MakeUpload()) |> Some) result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } @@ -466,7 +466,7 @@ module UploadRequestOperation = result.Data.Value.UploadRequest.NullableMultiple |> Option.map (Array.map ((fun x -> x.ToDictionary()) >> File.FromDictionary)) |> equals request.NullableMultiple result.Data.Value.UploadRequest.NullableMultipleNullable |> Option.map (Array.map (Option.map ((fun x -> x.ToDictionary()) >> File.FromDictionary))) |> equals request.NullableMultipleNullable -[] +[] let ``Should be able to upload files inside another input type``() = let request = { Single = { Name = "single.txt"; ContentType = "text/plain"; Content = "Single file content" } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json index b397d2f41..19608df07 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json @@ -1,5 +1,5 @@ { - "documentId": 1466593774, + "documentId": 1481480240, "data": { "__schema": { "queryType": { diff --git a/tests/FSharp.Data.GraphQL.Tests/FileTests.fs b/tests/FSharp.Data.GraphQL.Tests/FileTests.fs index 0374d312e..22f663f4c 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FileTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/FileTests.fs @@ -1,4 +1,4 @@ -module FSharp.Data.GraphQL.Tests.FileTests +module FSharp.Data.GraphQL.Tests.FileTests open System.Collections.Immutable open System.IO @@ -63,7 +63,7 @@ let execute (query : string) = executor.AsyncExecute (query, getMockInputContext let executeWithVariables ( query : string, variables : ImmutableDictionary) = executor.AsyncExecute (ast = parse query, getInputContext = getMockInputContext, variables = variables) |> sync -let mutationWithVariable = """mutation uploadFile ($file : FileType!) { +let mutationWithVariable = """mutation uploadFile ($file : File!) { uploadFile (input : $file) } """ diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index faacc9380..1fa7a69e6 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -211,9 +211,9 @@ module MockInputContext = interface IInputExecutionContext with member context.GetFile key = if (key = context.FileKey) then - Ok { Stream = context.Stream; ContentType = mockContentType } + Ok { FileName = key; Stream = context.Stream; ContentType = mockContentType } else if (key = context.FileKey2) then - Ok { Stream = context.Stream2; ContentType = mockContentType } + Ok { FileName = key; Stream = context.Stream2; ContentType = mockContentType } else failwith $"only file {context.FileKey} and file {context.FileKey2} exist"