diff --git a/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs b/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs index fa291a56..e15a55e2 100644 --- a/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs +++ b/src/FSharp.Data.GraphQL.Server.AspNetCore/RequestExecutionContext.fs @@ -13,6 +13,7 @@ type HttpContextRequestExecutionContext (httpContext : HttpContext) = else let form = httpContext.Request.Form match (form.Files |> Seq.vtryFind (fun f -> f.Name = key)) with - | ValueSome file -> Ok (file.OpenReadStream()) + | ValueSome file -> + Ok { Stream = file.OpenReadStream (); ContentType = file.ContentType } | ValueNone -> Error $"File with key '{key}' not found" diff --git a/src/FSharp.Data.GraphQL.Shared/InputContext.fs b/src/FSharp.Data.GraphQL.Shared/InputContext.fs index 1ca35256..b03d5eb2 100644 --- a/src/FSharp.Data.GraphQL.Shared/InputContext.fs +++ b/src/FSharp.Data.GraphQL.Shared/InputContext.fs @@ -1,7 +1,12 @@ namespace FSharp.Data.GraphQL +type FileData = { + Stream : System.IO.Stream + ContentType : string +} + type IInputExecutionContext = - abstract GetFile : string -> Result + abstract GetFile : string -> Result type InputExecutionContextProvider = unit -> IInputExecutionContext diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index 7e8e9e7c..1b5d0cf6 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -191,7 +191,7 @@ module SchemaDefinitions = | _ -> Some(x.ToString()) /// Tries to convert any value to string. - let coerceFileValue (context : IInputExecutionContext) (value : obj) : Result = + let coerceFileValue (context : IInputExecutionContext) (value : obj) : Result = match coerceStringValue value with | Some fileName -> context.GetFile fileName | None -> Error "Only string value can be used as file name" @@ -482,31 +482,31 @@ module SchemaDefinitions = CoerceInput = coerceGuidInput CoerceOutput = coerceGuidValue } - /// Defines an object list filter for use as an argument for filter list of object fields. - let FileType : InputCustomDefinition = { + /// Defines an object list filter for use as an argument for filter list of object fields. + let FileType : InputCustomDefinition = { Name = "FileType" 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." CoerceInput = (fun inputContext input variables -> - let getFileStream fileKey = + let getFileData fileKey = let inputExecutionContext = inputContext() - let streamResult = inputExecutionContext.GetFile fileKey - match streamResult with - | Ok stream -> Ok stream + let fileData = inputExecutionContext.GetFile fileKey + match fileData with + | Ok data -> Ok data | Error errorMessage -> IGQLError.createResultErrorList errorMessage match input with | InlineConstant c -> match c with - | StringValue strValue -> getFileStream strValue + | StringValue strValue -> getFileData strValue | VariableName varName -> - Ok (variables[varName] :?> System.IO.Stream) + Ok (variables[varName] :?> 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 - | StringValue str -> getFileStream str + | StringValue str -> getFileData str | _ -> IGQLError.createResultErrorList "Only a variable with a string value can be used as a file name.") } diff --git a/tests/FSharp.Data.GraphQL.Tests/FileTests.fs b/tests/FSharp.Data.GraphQL.Tests/FileTests.fs index 42dfc93d..0374d312 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FileTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/FileTests.fs @@ -19,10 +19,15 @@ let QueryType = ) type Input = { - File : Stream - File2 : Stream option + File : FileData + File2 : FileData option } +let private getFullInfo fileData = + use reader = new StreamReader(fileData.Stream, Encoding.UTF8, true) + let fileContent = reader.ReadToEnd() + fileContent + fileData.ContentType + let InputObject = Define.InputObject( name = "Input", fields = @@ -37,21 +42,18 @@ let MutationType = fields = [ Define.Field ("uploadFile", StringType, "", [ Define.Input ("input", FileType) ], (fun ctx () -> - let stream = ctx.Arg "input" - use reader = new StreamReader(stream, Encoding.UTF8, true) - reader.ReadToEnd() + let fileData = ctx.Arg "input" + getFullInfo fileData )); Define.Field ("uploadFileComplex", StringType, "", [ Define.Input ("input", InputObject) ], (fun ctx () -> let input = ctx.Arg "input" - let reader = new StreamReader(input.File, Encoding.UTF8, true) - let fileContent = reader.ReadToEnd() - let file2Content = match input.File2 with + let fileString = getFullInfo input.File + let file2String = match input.File2 with | Some file2 -> - let reader2 = new StreamReader(file2, Encoding.UTF8, true) - reader2.ReadToEnd() + getFullInfo file2 | None -> "" - fileContent + file2Content + fileString + file2String )) ] ) @@ -80,7 +82,7 @@ let mutationComplexObjectWithTwoFiles = """mutation uploadFile () { [] let ``File type: Must upload file as input scalar using inline string as a file name`` () = - let expected = NameValueLookup.ofList [ "uploadFile", MockInputContext.mockFileText ] + let expected = NameValueLookup.ofList [ "uploadFile", MockInputContext.mockFileTextAndContentType ] let result = execute mutationWithConstant ensureDirect result <| fun data errors -> empty errors @@ -89,7 +91,7 @@ let ``File type: Must upload file as input scalar using inline string as a file [] let ``File type: Must upload a file as input scalar using a variable`` () = - let expected = NameValueLookup.ofList [ "uploadFile", MockInputContext.mockFileText ] + let expected = NameValueLookup.ofList [ "uploadFile", MockInputContext.mockFileTextAndContentType ] let jsonVariable = "\"fileKey\"" |> JsonDocument.Parse |> _.RootElement let variables = ImmutableDictionary.Empty.Add ("file", jsonVariable) let result = executeWithVariables (mutationWithVariable, variables) @@ -100,7 +102,7 @@ let ``File type: Must upload a file as input scalar using a variable`` () = [] let ``File type: Must upload a file as input object field using inline string a file name`` () = - let expected = NameValueLookup.ofList [ "uploadFileComplex", MockInputContext.mockFileText ] + let expected = NameValueLookup.ofList [ "uploadFileComplex", MockInputContext.mockFileTextAndContentType ] let result = execute mutationComplexObject ensureDirect result <| fun data errors -> empty errors @@ -109,7 +111,7 @@ let ``File type: Must upload a file as input object field using inline string a [] let ``File type: Must upload two files as input object field using inline string a file name`` () = - let expectedContent = MockInputContext.mockFileText + MockInputContext.mockFileText2 + let expectedContent = MockInputContext.mockFileTextAndContentType + MockInputContext.mockFileText2AndContentType let expected = NameValueLookup.ofList [ "uploadFileComplex", expectedContent ] diff --git a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs index 12bad642..faacc938 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Helpers.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Helpers.fs @@ -185,11 +185,15 @@ type ExecutorExtensions = module MockInputContext = + let mockContentType = System.Net.Mime.MediaTypeNames.Text.Plain let mockFileKey = "fileKey" let mockFileKey2 = "fileKey2" let mockFileText = "fileText" let mockFileText2 = "fileText2" + let mockFileTextAndContentType = mockFileText + mockContentType + let mockFileText2AndContentType = mockFileText2 + mockContentType + type MockInputExecutionContext () = member _.FileKey = mockFileKey @@ -207,9 +211,9 @@ module MockInputContext = interface IInputExecutionContext with member context.GetFile key = if (key = context.FileKey) then - Ok context.Stream + Ok { Stream = context.Stream; ContentType = mockContentType } else if (key = context.FileKey2) then - Ok context.Stream2 + Ok { Stream = context.Stream2; ContentType = mockContentType } else failwith $"only file {context.FileKey} and file {context.FileKey2} exist"