diff --git a/.github/workflows/publish-ci.yml b/.github/workflows/publish-ci.yml index f31487b5c..e141251e7 100644 --- a/.github/workflows/publish-ci.yml +++ b/.github/workflows/publish-ci.yml @@ -8,7 +8,7 @@ on: env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true - DOTNET_SDK_VERSION: 10.0.202 + DOTNET_SDK_VERSION: 10.0.300 jobs: publish: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d4df8454f..a73e62012 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -9,7 +9,7 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true SLEEP_DURATION: 60 - DOTNET_SDK_VERSION: 10.0.202 + DOTNET_SDK_VERSION: 10.0.300 jobs: publish: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 07356a39c..7befa633e 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: [10.0.202] + dotnet: [10.0.300] runs-on: ${{ matrix.os }} steps: diff --git a/FSharp.Data.GraphQL.Integration.slnx b/FSharp.Data.GraphQL.Integration.slnx index ebd077769..b8e081034 100644 --- a/FSharp.Data.GraphQL.Integration.slnx +++ b/FSharp.Data.GraphQL.Integration.slnx @@ -4,6 +4,9 @@ + + + @@ -12,7 +15,9 @@ + + diff --git a/Packages.props b/Packages.props index 7d3b9582b..323f603f2 100644 --- a/Packages.props +++ b/Packages.props @@ -76,6 +76,7 @@ + diff --git a/build/Program.fs b/build/Program.fs index 22bbb133c..9624a0470 100644 --- a/build/Program.fs +++ b/build/Program.fs @@ -2,8 +2,6 @@ module Program open System open System.IO -open System.Net.Http -open System.Text.Json open Fake.Core open Fake.Core.TargetOperators @@ -32,7 +30,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 "10.0.202" } + let setVersion (o : DotNet.Options) = { o with Version = Some "10.0.300" } let setRestoreOptions (o : DotNet.RestoreOptions) = o.WithCommon setVersion let configurationString = Environment.environVarOrDefault "CONFIGURATION" "Release" @@ -123,26 +121,6 @@ let runTests (project : string) (args : string) = |> _.WithCommon(DotNetCli.setVersion)) project -let starWarsServerStream = StreamRef.Empty - -let [] StartStarWarsServerTarget = "StartStarWarsServer" -Target.create StartStarWarsServerTarget <| fun _ -> - Target.activateFinal "StopStarWarsServer" - - let project = - "samples" - "star-wars-api" - "star-wars-api.fsproj" - - startGraphQLServer project 8086 starWarsServerStream - -let [] StopStarWarsServerTarget = "StopStarWarsServer" -Target.createFinal StopStarWarsServerTarget <| fun _ -> - try - starWarsServerStream.Value.Write ([| 0uy |], 0, 1) - with e -> - printfn "%s" e.Message - let integrationTestServerProjectPath = "tests" "FSharp.Data.GraphQL.IntegrationTests.Server" @@ -179,58 +157,35 @@ Target.createFinal StopIntegrationServerTarget <| fun _ -> with e -> printfn "%s" e.Message -let [] UpdateIntrospectionFileTarget = "UpdateIntrospectionFile" -Target.create UpdateIntrospectionFileTarget <| fun _ -> - let client = new HttpClient () - (task { - let! result = client.GetAsync ("http://localhost:8086") - let! contentStream = result.Content.ReadAsStreamAsync () - let! jsonDocument = JsonDocument.ParseAsync contentStream - let file = - new FileStream ("tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json", FileMode.Create, FileAccess.Write, FileShare.None) - let encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping - let jsonWriterOptions = JsonWriterOptions (Indented = true, Encoder = encoder) - let writer = new Utf8JsonWriter (file, jsonWriterOptions) - jsonDocument.WriteTo writer - do! writer.FlushAsync () - do! writer.DisposeAsync () - do! file.DisposeAsync () - result.Dispose () - }) - .Wait () - client.Dispose () - -let unitTestsProjectPath = - "tests" - "FSharp.Data.GraphQL.Tests" - "FSharp.Data.GraphQL.Tests.fsproj" - let integrationTestsProjectPath = "tests" "FSharp.Data.GraphQL.IntegrationTests" "FSharp.Data.GraphQL.IntegrationTests.fsproj" -let [] BuildIntegrationTestsTarget = "BuildIntegrationTests" -Target.create BuildIntegrationTestsTarget <| fun _ -> +let [] UpdateIntrospectionFileTarget = "UpdateIntrospectionFile" +Target.create UpdateIntrospectionFileTarget <| fun _ -> integrationTestsProjectPath - |> DotNet.build (fun options -> { + |> DotNet.test (fun options -> { options with + Framework = Some DotNetMoniker Configuration = configuration + Common = { DotNetCli.setVersion options.Common with CustomParams = Some "--filter FullyQualifiedName~IntrospectionUpdateTests" } MSBuildParams = { options.MSBuildParams with DisableInternalBinLog = true + Verbosity = Some Normal } - Common = DotNetCli.setVersion options.Common }) +let unitTestsProjectPath = + "tests" + "FSharp.Data.GraphQL.Tests" + "FSharp.Data.GraphQL.Tests.fsproj" + let [] RunUnitTestsTarget = "RunUnitTests" Target.create RunUnitTestsTarget <| fun _ -> runTests unitTestsProjectPath "" -let [] RunIntegrationTestsTarget = "RunIntegrationTests" -Target.create RunIntegrationTestsTarget <| fun _ -> - runTests integrationTestsProjectPath "" //"--filter Execution=Sync" - let prepareDocGen () = Shell.rm "docs/release-notes.md" Shell.cp "RELEASE_NOTES.md" "docs/RELEASE_NOTES.md" @@ -406,12 +361,7 @@ Target.create "PackAndPush" ignore ==> RestoreTarget ==> BuildTarget ==> RunUnitTestsTarget -==> StartStarWarsServerTarget -==> BuildIntegrationTestServerTarget -==> StartIntegrationServerTarget ==> UpdateIntrospectionFileTarget -==> BuildIntegrationTestsTarget -==> RunIntegrationTestsTarget ==> "All" =?> (GenerateDocsTarget, Environment.environVar "GITHUB_ACTIONS" = "True") |> ignore diff --git a/samples/star-wars-fabulous-client/StarWars.slnx b/samples/star-wars-fabulous-client/StarWars.slnx index 51d68264a..52cff01ec 100644 --- a/samples/star-wars-fabulous-client/StarWars.slnx +++ b/samples/star-wars-fabulous-client/StarWars.slnx @@ -14,6 +14,12 @@ + + + + + + diff --git a/samples/star-wars-fabulous-client/StarWars/Common.fs b/samples/star-wars-fabulous-client/StarWars/Common.fs index 3974e6c92..1de5ae4e9 100644 --- a/samples/star-wars-fabulous-client/StarWars/Common.fs +++ b/samples/star-wars-fabulous-client/StarWars/Common.fs @@ -5,7 +5,10 @@ open FSharp.Data.GraphQL module Commands = - type GraphQLApi = GraphQLProvider<"http://localhost:8086"> + [] + let IntrospectionPath = "../../../tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json" + + type GraphQLApi = GraphQLProvider let GetCharactersData = GraphQLApi.Operation<"queries/FetchCharacters.graphql">() type Character = GraphQLApi.Operations.FetchCharacters.Types.CharactersFields.Character diff --git a/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj b/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj index d972d6d59..4769849ad 100644 --- a/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj +++ b/samples/star-wars-fabulous-client/StarWars/StarWars.fsproj @@ -4,7 +4,7 @@ false - + @@ -18,16 +18,16 @@ - - - - - - - - + + + + + + + + - \ No newline at end of file + diff --git a/src/FSharp.Data.GraphQL.Client.DesignTime/FSharp.Data.GraphQL.Client.DesignTime.fsproj b/src/FSharp.Data.GraphQL.Client.DesignTime/FSharp.Data.GraphQL.Client.DesignTime.fsproj index 6d728c7a1..8f3559c38 100644 --- a/src/FSharp.Data.GraphQL.Client.DesignTime/FSharp.Data.GraphQL.Client.DesignTime.fsproj +++ b/src/FSharp.Data.GraphQL.Client.DesignTime/FSharp.Data.GraphQL.Client.DesignTime.fsproj @@ -20,6 +20,7 @@ contentFiles;runtime + @@ -29,8 +30,6 @@ - - diff --git a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs index 504207bbc..b9bc6434a 100644 --- a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs +++ b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs @@ -8,6 +8,7 @@ open System.Collections open System.Collections.Generic open System.Net.Http open System.Reflection +open System.Text.Json open System.Text.Json.Serialization open FSharp.Core open FSharp.Data.GraphQL @@ -333,7 +334,7 @@ module internal ProvidedOperation = let serverUrl = info.ServerUrl let headerNames = info.HttpHeaders |> Seq.map fst |> Array.ofSeq let headerValues = info.HttpHeaders |> Seq.map snd |> Array.ofSeq - <@@ { ServerUrl = serverUrl; HttpHeaders = Array.zip headerNames headerValues; Connection = new GraphQLClientConnection() } @@> + <@@ { ServerUrl = serverUrl; HttpHeaders = Array.zip headerNames headerValues; Connection = new GraphQLClientConnection(); JsonSerializerOptions = Serialization.defaultSerializerOptions.Value } @@> | None -> <@@ Unchecked.defaultof @@> // We need to use the combination strategy to generate overloads for variables in the Run/AsyncRun methods. // The strategy follows the same principle with ProvidedRecord constructor overloads, @@ -403,16 +404,16 @@ module internal ProvidedOperation = HttpHeaders = context.HttpHeaders OperationName = Option.ofObj operationName Query = actualQuery - Variables = %%variables } + Variables = %%variables + JsonSerializerOptions = context.JsonSerializerOptions } let response = if shouldUseMultipartRequest then Tracer.runAndMeasureExecutionTime "Ran a multipart GraphQL query request" (fun _ -> GraphQLClient.sendMultipartRequest context.Connection request) else Tracer.runAndMeasureExecutionTime "Ran a GraphQL query request" (fun _ -> GraphQLClient.sendRequest context.Connection request) let responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult() - let responseJson = Tracer.runAndMeasureExecutionTime "Parsed a GraphQL response to a JsonValue" (fun _ -> JsonValue.Parse responseString) // 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) @@> + new OperationResultBase(response, responseString, %%operationFieldsExpr, operationTypeName) @@> 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.") @@ -449,7 +450,8 @@ module internal ProvidedOperation = HttpHeaders = context.HttpHeaders OperationName = Option.ofObj operationName Query = actualQuery - Variables = %%variables } + Variables = %%variables + JsonSerializerOptions = context.JsonSerializerOptions } async { let! ct = Async.CancellationToken let! response = @@ -457,17 +459,16 @@ module internal ProvidedOperation = then Tracer.asyncRunAndMeasureExecutionTime "Ran a multipart GraphQL query request asynchronously" (fun _ -> GraphQLClient.sendMultipartRequestAsync ct context.Connection request |> Async.AwaitTask) else Tracer.asyncRunAndMeasureExecutionTime "Ran a GraphQL query request asynchronously" (fun _ -> GraphQLClient.sendRequestAsync ct context.Connection request |> Async.AwaitTask) let! responseString = response.Content.ReadAsStringAsync() |> Async.AwaitTask - let responseJson = Tracer.runAndMeasureExecutionTime "Parsed a GraphQL response to a JsonValue" (fun _ -> JsonValue.Parse responseString) // If the user does not provide a context, we should dispose the default one after running the query if isDefaultContext then (context :> IDisposable).Dispose() - return OperationResultBase(response, responseJson, %%operationFieldsExpr, operationTypeName) + return new OperationResultBase(response, responseString, %%operationFieldsExpr, operationTypeName) } @@> 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) let parseResultDef = - let invoker (args : Expr list) = <@@ OperationResultBase(%%args.[1], JsonValue.Parse %%args.[2], %%operationFieldsExpr, operationTypeName) @@> + let invoker (args : Expr list) = <@@ new OperationResultBase(%%args.[1], %%args.[2], %%operationFieldsExpr, operationTypeName) @@> let parameters = [ ProvidedParameter("rawResponse", typeof) ProvidedParameter("responseJson", typeof) @@ -753,7 +754,8 @@ module internal Provider = | _ -> ProvidedParameter("serverUrl", typeof) let httpHeaders = ProvidedParameter("httpHeaders", typeof>, optionalValue = null) let connectionFactory = ProvidedParameter("connectionFactory", typeof GraphQLClientConnection>, optionalValue = null) - [serverUrl; httpHeaders; connectionFactory] + let jsonSerializerOptions = ProvidedParameter("jsonSerializerOptions", typeof, optionalValue = null) + [serverUrl; httpHeaders; connectionFactory; jsonSerializerOptions] let defaultHttpHeadersExpr = let names = httpHeaders |> Seq.map fst |> Array.ofSeq let values = httpHeaders |> Seq.map snd |> Array.ofSeq @@ -768,7 +770,11 @@ module internal Provider = match %%args.[2] : unit -> GraphQLClientConnection with | argHeaders when obj.Equals(argHeaders, null) -> fun () -> new GraphQLClientConnection() | argHeaders -> argHeaders - { ServerUrl = %%serverUrl; HttpHeaders = httpHeaders; Connection = connectionFactory() } @@> + let jsonOptions = + match %%args.[3] : JsonSerializerOptions with + | null -> Serialization.defaultSerializerOptions.Value + | opts -> opts + { ServerUrl = %%serverUrl; HttpHeaders = httpHeaders; Connection = connectionFactory(); JsonSerializerOptions = jsonOptions } @@> ProvidedMethod("GetContext", methodParameters, typeof, invoker, isStatic = true) let operationMethodDef = let staticParams = diff --git a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs index 4b93c3ff1..dabdd4140 100644 --- a/src/FSharp.Data.GraphQL.Client/BaseTypes.fs +++ b/src/FSharp.Data.GraphQL.Client/BaseTypes.fs @@ -267,55 +267,54 @@ module internal TypeMapping = let makeAsync (t : Type) = typedefof>.MakeGenericType (t) module internal JsonValueHelper = - let getResponseFields (responseJson : JsonValue) = - match responseJson with - | JsonValue.Record fields -> fields - | _ -> failwithf "Expected root type to be a Record type, but type is %A." responseJson - - let getResponseDataFields (responseJson : JsonValue) = - match - getResponseFields responseJson - |> Array.tryFind (fun (name, _) -> name = "data") - with - | Some (_, data) -> - match data with - | JsonValue.Record fields -> Some fields - | JsonValue.Null -> None - | _ -> failwithf "Expected data field of root type to be a Record type, but type is %A." data - | None -> None - - let getResponseErrors (responseJson : JsonValue) = - match - getResponseFields responseJson - |> Array.tryFind (fun (name, _) -> name = "errors") - with - | Some (_, errors) -> - match errors with - | JsonValue.Array [||] - | JsonValue.Null -> None - | JsonValue.Array items -> Some items - | _ -> failwithf "Expected error field of root type to be an Array type, but type is %A." errors - | None -> None - - let getResponseCustomFields (responseJson : JsonValue) = - getResponseFields responseJson - |> Array.filter (fun (name, _) -> name <> "data" && name <> "errors") - - let private removeTypeNameField (fields : (string * JsonValue)[]) = + open System.Text.Json + + let getResponseDataFields (responseJson : JsonElement) = + match responseJson.TryGetProperty "data" with + | true, data -> + match data.ValueKind with + | JsonValueKind.Object -> + data.EnumerateObject () + |> Seq.map (fun prop -> prop.Name, prop.Value) + |> Array.ofSeq + |> Some + | JsonValueKind.Null -> None + | _ -> failwithf "Expected data field of root type to be a Record type, but type is %A." data.ValueKind + | _ -> None + + let getResponseErrors (responseJson : JsonElement) = + match responseJson.TryGetProperty "errors" with + | true, errors -> + match errors.ValueKind with + | JsonValueKind.Null -> None + | JsonValueKind.Array -> + let items = errors.EnumerateArray () |> Array.ofSeq + if items.Length = 0 then None + else Some items + | _ -> failwithf "Expected error field of root type to be an Array type, but type is %A." errors.ValueKind + | _ -> None + + let getResponseCustomFields (responseJson : JsonElement) = + responseJson.EnumerateObject () + |> Seq.filter (fun prop -> prop.Name <> "data" && prop.Name <> "errors") + |> Seq.map (fun prop -> prop.Name, prop.Value) + |> Array.ofSeq + + let private removeTypeNameField (fields : (string * JsonElement) []) = fields |> Array.filter (fun (name, _) -> name <> "__typename") let firstUpper (name : string, value) = name.FirstCharUpper (), value - let getTypeName (fields : (string * JsonValue) seq) = + let getTypeName (fields : (string * JsonElement) seq) = fields |> Seq.tryFind (fun (name, _) -> name = "__typename") |> Option.map (fun (_, value) -> - match value with - | JsonValue.String x -> x - | _ -> failwithf "Expected \"__typename\" field to be a string field, but it was %A." value) + match value.ValueKind with + | JsonValueKind.String -> value.GetString () + | _ -> failwithf "Expected \"__typename\" field to be a string field, but it was %A." value.ValueKind) - let rec getFieldValue (schemaField : SchemaFieldInfo) (fieldName : string, fieldValue : JsonValue) = + let rec getFieldValue (schemaField : SchemaFieldInfo) (fieldName : string, fieldValue : JsonElement) = let getScalarType (typeRef : IntrospectionTypeRef) = let getType (typeName : string) = match Map.tryFind typeName TypeMapping.scalar with @@ -324,7 +323,16 @@ module internal JsonValueHelper = match typeRef.Name with | Some name -> getType name | None -> failwith "Expected scalar type to have a name, but it does not have one." - let rec helper (useOption : bool) (schemaField : SchemaFieldInfo) (fieldValue : JsonValue) : obj = + + let getNumericValue (typeRef : IntrospectionTypeRef) (element : JsonElement) : obj = + let t = getScalarType typeRef + if t = typeof then element.GetDouble () |> box + elif t = typeof then element.GetInt32 () |> box + elif t = typeof then element.GetDecimal () |> box + elif t = typeof then element.GetInt64 () |> box + else element.GetDouble () |> box + + let rec helper (useOption : bool) (schemaField : SchemaFieldInfo) (fieldValue : JsonElement) : obj = let makeSomeIfNeeded value = match schemaField.SchemaTypeRef.Kind with | TypeKind.NON_NULL -> value @@ -335,8 +343,9 @@ module internal JsonValueHelper = | TypeKind.NON_NULL -> null | _ when useOption -> makeNone t | _ -> null - match fieldValue with - | JsonValue.Array items -> + match fieldValue.ValueKind with + | JsonValueKind.Array -> + let itemsArr = fieldValue.EnumerateArray () |> Array.ofSeq let items = let itemType = let tref = @@ -355,7 +364,7 @@ module internal JsonValueHelper = | None -> failwith "Schema type is a list type, but no underlying type was specified." let items = let schemaField = { schemaField with SchemaTypeRef = itemType } - items |> Array.map (helper false schemaField) + itemsArr |> Array.map (helper false schemaField) match itemType.Kind with | TypeKind.NON_NULL -> match itemType.OfType with @@ -376,12 +385,16 @@ module internal JsonValueHelper = | TypeKind.SCALAR -> makeOptionArray (getScalarType itemType) items | kind -> failwithf "Unsupported type kind \"%A\"." kind makeSomeIfNeeded items - | JsonValue.Record props -> + | JsonValueKind.Object -> + let props = + fieldValue.EnumerateObject () + |> Seq.map (fun p -> p.Name, p.Value) + |> Array.ofSeq let typeName = match getTypeName props with | Some typeName -> typeName | None -> failwith "Expected type to have a \"__typename\" field, but it was not found." - let mapRecordProperty (aliasOrName : string, value : JsonValue) = + let mapRecordProperty (aliasOrName : string, value : JsonElement) = let schemaField = match schemaField.Fields @@ -400,9 +413,22 @@ module internal JsonValueHelper = |> removeTypeNameField |> Array.map (firstUpper >> mapRecordProperty) RecordBase (typeName, props) |> makeSomeIfNeeded - | JsonValue.Boolean b -> makeSomeIfNeeded b - | JsonValue.Float f -> makeSomeIfNeeded f - | JsonValue.Null -> + | JsonValueKind.True -> makeSomeIfNeeded true + | JsonValueKind.False -> makeSomeIfNeeded false + | JsonValueKind.Number -> + // Use the schema type to determine the correct numeric CLR type, + // fixing the issue where JSON integers (e.g. 0) were returned as int + // even when the schema declares the field as Float. + let innerTypeRef = + match schemaField.SchemaTypeRef.Kind with + | TypeKind.NON_NULL -> + match schemaField.SchemaTypeRef.OfType with + | Some t -> t + | None -> schemaField.SchemaTypeRef + | _ -> schemaField.SchemaTypeRef + let numVal = getNumericValue innerTypeRef fieldValue + makeSomeIfNeeded numVal + | JsonValueKind.Null -> match schemaField.SchemaTypeRef.Kind with | TypeKind.NON_NULL -> failwith "Expected a non null item from the schema definition, but a null item was found in the response." | TypeKind.OBJECT @@ -412,8 +438,8 @@ module internal JsonValueHelper = | TypeKind.SCALAR -> getScalarType schemaField.SchemaTypeRef |> makeNoneIfNeeded | TypeKind.LIST -> null | kind -> failwithf "Unsupported type kind \"%A\"." kind - | JsonValue.Integer n -> makeSomeIfNeeded n - | JsonValue.String s -> + | JsonValueKind.String -> + let s = fieldValue.GetString () match schemaField.SchemaTypeRef.Kind with | TypeKind.NON_NULL -> match schemaField.SchemaTypeRef.OfType with @@ -457,10 +483,11 @@ module internal JsonValueHelper = | _ -> failwith "A string type was received in the query response item, but the matching schema field is not a string based type or an enum type." + | kind -> failwithf "Unexpected JSON value kind \"%A\"." kind fieldName, (helper true schemaField fieldValue) - let getFieldValues (schemaTypeName : string) (schemaFields : SchemaFieldInfo[]) (dataFields : (string * JsonValue)[]) = - let mapFieldValue (aliasOrName : string, value : JsonValue) = + let getFieldValues (schemaTypeName : string) (schemaFields : SchemaFieldInfo[]) (dataFields : (string * JsonElement) []) = + let mapFieldValue (aliasOrName : string, value : JsonElement) = let schemaField = match schemaFields @@ -476,66 +503,75 @@ module internal JsonValueHelper = removeTypeNameField dataFields |> Array.map (firstUpper >> mapFieldValue) - let getErrors (errors : JsonValue[]) = - let tryFindField fieldName (fields : (string * JsonValue)[]) = - fields - |> Array.tryFind (fun (name, _) -> name = fieldName) - |> Option.map snd - - let parsePath = - function - | Some (JsonValue.Array path) -> - let pathMapper = - function - | JsonValue.String x -> box x - | JsonValue.Integer x -> box x + let getErrors (errors : JsonElement []) = + let tryGetProperty (name : string) (element : JsonElement) = + match element.TryGetProperty name with + | true, v -> Some v + | _ -> None + + let parsePath (pathElement : JsonElement option) = + match pathElement with + | Some e when e.ValueKind = JsonValueKind.Array -> + let pathMapper (item : JsonElement) = + match item.ValueKind with + | JsonValueKind.String -> item.GetString () |> box + | JsonValueKind.Number -> item.GetInt32 () |> box | _ -> failwith "Error parsing response errors. An item in the path is neither a String nor an Integer." - path |> Array.map pathMapper - | Some JsonValue.Null + e.EnumerateArray () |> Seq.map pathMapper |> Array.ofSeq | None -> [||] + | Some e when e.ValueKind = JsonValueKind.Null -> [||] | _ -> failwith "Error parsing response errors. Path field must be an Array." - let parseLocations = - function - | Some (JsonValue.Array locations) -> - let parseLocation = - function - | JsonValue.Record locationFields -> - match tryFindField "line" locationFields, tryFindField "column" locationFields with - | Some (JsonValue.Integer line), Some (JsonValue.Integer column) -> { Line = line; Column = column } + let parseLocations (locElement : JsonElement option) = + match locElement with + | Some e when e.ValueKind = JsonValueKind.Array -> + let parseLocation (loc : JsonElement) = + match loc.ValueKind with + | JsonValueKind.Object -> + match loc.TryGetProperty "line", loc.TryGetProperty "column" with + | (true, lineEl), (true, colEl) -> { Line = lineEl.GetInt32 (); Column = colEl.GetInt32 () } | _ -> failwith "Error parsing response errors. A location item must contain Integer fields named \"line\" and \"column\"." | _ -> failwith "Error parsing response errors. A location item is not a Record." - locations |> Array.map parseLocation - | Some JsonValue.Null + e.EnumerateArray () |> Seq.map parseLocation |> Array.ofSeq | None -> [||] + | Some e when e.ValueKind = JsonValueKind.Null -> [||] | _ -> failwith "Error parsing response errors. Locations field must be an Array." - let parseExtensions = - function - | Some (JsonValue.Record fields) -> Serialization.deserializeMap fields - | Some JsonValue.Null + let parseExtensions (extElement : JsonElement option) = + match extElement with + | Some e when e.ValueKind = JsonValueKind.Object -> + e.EnumerateObject () + |> Seq.map (fun prop -> prop.Name, prop.Value) + |> Array.ofSeq + |> Serialization.deserializeMap | None -> Map.empty + | Some e when e.ValueKind = JsonValueKind.Null -> Map.empty | _ -> failwith "Error parsing response errors. Extensions field must be a Record." - let errorMapper = - function - | JsonValue.Record fields -> - match tryFindField "message" fields with - | Some (JsonValue.String message) -> { - Message = message - Locations = tryFindField "locations" fields |> parseLocations - Path = tryFindField "path" fields |> parsePath - Extensions = tryFindField "extensions" fields |> parseExtensions + let errorMapper (errorElement : JsonElement) = + match errorElement.ValueKind with + | JsonValueKind.Object -> + match tryGetProperty "message" errorElement with + | Some msgEl when msgEl.ValueKind = JsonValueKind.String -> { + Message = msgEl.GetString () + Locations = tryGetProperty "locations" errorElement |> parseLocations + Path = tryGetProperty "path" errorElement |> parsePath + Extensions = tryGetProperty "extensions" errorElement |> parseExtensions } | _ -> failwith "Error parsing response errors. Unsupported errors field format." - | other -> failwithf "Error parsing response errors. Expected error to be a Record type, but it is %s." (other.ToString ()) + | _ -> failwith "Error parsing response errors. Expected error to be a Record type." Array.map errorMapper errors /// The base type for all GraphQLProvider operation result provided types. type OperationResultBase - (rawResponse : HttpResponseMessage, responseJson : JsonValue, operationFields : SchemaFieldInfo[], operationTypeName : string) = + (rawResponse : HttpResponseMessage, responseJson : string, operationFields : SchemaFieldInfo[], operationTypeName : string) = + let parsedJson = + try System.Text.Json.JsonDocument.Parse responseJson + with ex -> raise (System.InvalidOperationException ($"Failed to parse GraphQL response JSON: {ex.Message}", ex)) + let rootElement = parsedJson.RootElement + let rawData = - let data = JsonValueHelper.getResponseDataFields responseJson + let data = JsonValueHelper.getResponseDataFields rootElement match data with | Some [||] | None -> None @@ -547,13 +583,13 @@ type OperationResultBase Some (RecordBase (operationTypeName, props)) let errors = - let errors = JsonValueHelper.getResponseErrors responseJson + let errors = JsonValueHelper.getResponseErrors rootElement match errors with | None -> [||] | Some errors -> JsonValueHelper.getErrors errors let customData = - JsonValueHelper.getResponseCustomFields responseJson + JsonValueHelper.getResponseCustomFields rootElement |> Serialization.deserializeMap member private _.ResponseJson = responseJson @@ -582,7 +618,10 @@ type OperationResultBase override x.GetHashCode () = x.ResponseJson.GetHashCode () -/// The base type for al GraphQLProvider operation provided types. + interface IDisposable with + member _.Dispose () = parsedJson.Dispose () + +/// The base type for all GraphQLProvider operation provided types. type OperationBase (query : string) = /// Gets the query string of the operation. member _.Query = query @@ -597,6 +636,7 @@ module VariableMapping = | :? string -> value | :? EnumBase as v -> v.GetValue () |> box | :? RecordBase as v -> v.ToDictionary () |> box - | OptionValue v -> v |> Option.map mapVariableValue |> box + | OptionValue None -> null + | OptionValue (Some v) -> mapVariableValue v | EnumerableValue v -> v |> Array.map mapVariableValue |> box | v -> v diff --git a/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj b/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj index ee00d2dbb..fac5e51ee 100644 --- a/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj +++ b/src/FSharp.Data.GraphQL.Client/FSharp.Data.GraphQL.Client.fsproj @@ -16,13 +16,13 @@ all runtime + + - - diff --git a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs index 6a757fc2d..a1a439a3e 100644 --- a/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs +++ b/src/FSharp.Data.GraphQL.Client/GraphQLClient.fs @@ -5,9 +5,9 @@ namespace FSharp.Data.GraphQL open System open System.Collections.Generic -open System.Collections.Immutable open System.Net.Http open System.Text +open System.Text.Json open System.Threading open System.Threading.Tasks @@ -15,7 +15,7 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Client open ReflectionPatterns -/// A requrest object for making GraphQL calls using the GraphQL client module. +/// A request 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 @@ -27,6 +27,8 @@ type GraphQLRequest = { Query : string /// Gets variables to be sent with the query. Variables : (string * obj)[] + /// Gets the JSON serializer options used for serializing request variables. + JsonSerializerOptions : JsonSerializerOptions } /// Executes calls to GraphQL servers and return their responses. @@ -57,22 +59,8 @@ module GraphQLClient = /// Sends a request to a GraphQL server asynchronously. let sendRequestAsync ct (connection : GraphQLClientConnection) (request : GraphQLRequest) = task { let invoker = connection.Invoker - let variables = - match request.Variables with - | null | [||] -> JsonValue.Null - | _ -> Map.ofArray request.Variables |> Serialization.toJsonValue - let operationName = - match request.OperationName with - | Some x -> JsonValue.String x - | None -> JsonValue.Null - let requestJson = - [| - "operationName", operationName - "query", JsonValue.String request.Query - "variables", variables - |] - |> JsonValue.Record - let content = new StringContent (requestJson.ToString (), Encoding.UTF8, "application/json") + let json = Serialization.buildRequestJson request.JsonSerializerOptions request.OperationName request.Query request.Variables + let content = new StringContent (json, Encoding.UTF8, "application/json") return! postAsync ct invoker request.ServerUrl request.HttpHeaders content } @@ -103,6 +91,7 @@ module GraphQLClient = OperationName = None Query = IntrospectionQuery.Definition Variables = [||] + JsonSerializerOptions = Serialization.defaultSerializerOptions.Value } try return! sendRequestAsync ct connection request @@ -145,35 +134,14 @@ module GraphQLClient = |> Array.collect (tryMapFileVariable >> (Option.defaultValue [||])) let operationContent = - let variables = - match request.Variables with - | 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 - |] - |> JsonValue.Record - let content = new StringContent (json.ToString (JsonSaveOptions.DisableFormatting)) + let json = Serialization.buildRequestJson request.JsonSerializerOptions request.OperationName request.Query request.Variables + let content = new StringContent (json) content.Headers.Add ("Content-Disposition", "form-data; name=\"operations\"") content content.Add (operationContent) let mapContent = - let files = - files - |> Array.mapi (fun ix (name, _) -> ix.ToString (), JsonValue.Array [| JsonValue.String ("variables." + name) |]) - |> JsonValue.Record - let content = new StringContent (files.ToString (JsonSaveOptions.DisableFormatting)) + let json = Serialization.buildMapJson files + let content = new StringContent (json) content.Headers.Add ("Content-Disposition", "form-data; name=\"map\"") content content.Add (mapContent) diff --git a/src/FSharp.Data.GraphQL.Client/GraphQLProviderRuntimeContext.fs b/src/FSharp.Data.GraphQL.Client/GraphQLProviderRuntimeContext.fs index 9c254b6fa..72d2847f0 100644 --- a/src/FSharp.Data.GraphQL.Client/GraphQLProviderRuntimeContext.fs +++ b/src/FSharp.Data.GraphQL.Client/GraphQLProviderRuntimeContext.fs @@ -4,6 +4,7 @@ namespace FSharp.Data.GraphQL open System +open System.Text.Json /// Contains information about a GraphQLRuntimeContext. type GraphQLRuntimeContextInfo = @@ -17,6 +18,9 @@ type GraphQLProviderRuntimeContext = /// Gets the HTTP headers used for calls to the server that this context refers to. HttpHeaders : seq /// Gets the connection component used to make calls to the server. - Connection : GraphQLClientConnection } + Connection : GraphQLClientConnection + /// Gets the JSON serializer options used for serializing request variables and + /// deserializing scalar values. Pass a customized instance to support custom scalar types. + JsonSerializerOptions : JsonSerializerOptions } interface IDisposable with member x.Dispose() = (x.Connection :> IDisposable).Dispose() diff --git a/src/FSharp.Data.GraphQL.Client/Serialization.fs b/src/FSharp.Data.GraphQL.Client/Serialization.fs index 6572c1aae..ae57226b9 100644 --- a/src/FSharp.Data.GraphQL.Client/Serialization.fs +++ b/src/FSharp.Data.GraphQL.Client/Serialization.fs @@ -5,198 +5,309 @@ namespace FSharp.Data.GraphQL.Client open System open System.Collections.Generic -open System.Diagnostics -open System.Globalization -open System.Reflection -open Microsoft.FSharp.Reflection +open System.IO +open System.Text +open System.Text.Json open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Client.ReflectionPatterns +open FSharp.Data.GraphQL.Types +open FSharp.Data.GraphQL.Types.Introspection + +/// Manual schema parser that uses JsonElement directly, enabling lenient handling +/// of missing fields (e.g., 'kind' is absent in queryType/mutationType references). +module private SchemaParser = + + let private tryGetString (element : JsonElement) (name : string) = + match element.TryGetProperty name with + | true, el when el.ValueKind = JsonValueKind.String -> Some (el.GetString ()) + | _ -> None + + let private tryGetBool (element : JsonElement) (name : string) (defaultValue : bool) = + match element.TryGetProperty name with + | true, el -> + match el.ValueKind with + | JsonValueKind.True -> true + | JsonValueKind.False -> false + | _ -> defaultValue + | _ -> defaultValue + + let private parseTypeKind (s : string) = + match s with + | "SCALAR" -> TypeKind.SCALAR + | "OBJECT" -> TypeKind.OBJECT + | "INTERFACE" -> TypeKind.INTERFACE + | "UNION" -> TypeKind.UNION + | "ENUM" -> TypeKind.ENUM + | "INPUT_OBJECT" -> TypeKind.INPUT_OBJECT + | "LIST" -> TypeKind.LIST + | "NON_NULL" -> TypeKind.NON_NULL + | _ -> Unchecked.defaultof + + let private parseDirectiveLocation (s : string) = + match s with + | "QUERY" -> DirectiveLocation.QUERY + | "MUTATION" -> DirectiveLocation.MUTATION + | "SUBSCRIPTION" -> DirectiveLocation.SUBSCRIPTION + | "FIELD" -> DirectiveLocation.FIELD + | "FRAGMENT_DEFINITION" -> DirectiveLocation.FRAGMENT_DEFINITION + | "FRAGMENT_SPREAD" -> DirectiveLocation.FRAGMENT_SPREAD + | "INLINE_FRAGMENT" -> DirectiveLocation.INLINE_FRAGMENT + | "SCHEMA" -> DirectiveLocation.SCHEMA + | "SCALAR" -> DirectiveLocation.SCALAR + | "OBJECT" -> DirectiveLocation.OBJECT + | "FIELD_DEFINITION" -> DirectiveLocation.FIELD_DEFINITION + | "ARGUMENT_DEFINITION" -> DirectiveLocation.ARGUMENT_DEFINITION + | "INTERFACE" -> DirectiveLocation.INTERFACE + | "UNION" -> DirectiveLocation.UNION + | "ENUM" -> DirectiveLocation.ENUM + | "ENUM_VALUE" -> DirectiveLocation.ENUM_VALUE + | "INPUT_OBJECT" -> DirectiveLocation.INPUT_OBJECT + | "INPUT_FIELD_DEFINITION" -> DirectiveLocation.INPUT_FIELD_DEFINITION + | _ -> Unchecked.defaultof + + let rec private parseTypeRef (element : JsonElement) : IntrospectionTypeRef = + { + Kind = + match element.TryGetProperty "kind" with + | true, el when el.ValueKind = JsonValueKind.String -> parseTypeKind (el.GetString ()) + | _ -> Unchecked.defaultof + Name = tryGetString element "name" + Description = tryGetString element "description" + OfType = + match element.TryGetProperty "ofType" with + | true, el when el.ValueKind = JsonValueKind.Object -> Some (parseTypeRef el) + | _ -> None + } + + let private parseInputVal (element : JsonElement) : IntrospectionInputVal = + { + Name = tryGetString element "name" |> Option.defaultValue "" + Description = tryGetString element "description" + Type = + match element.TryGetProperty "type" with + | true, el -> parseTypeRef el + | _ -> { Kind = Unchecked.defaultof; Name = None; Description = None; OfType = None } + DefaultValue = tryGetString element "defaultValue" + } + + let private parseEnumVal (element : JsonElement) : IntrospectionEnumVal = + { + Name = tryGetString element "name" |> Option.defaultValue "" + Description = tryGetString element "description" + IsDeprecated = tryGetBool element "isDeprecated" false + DeprecationReason = tryGetString element "deprecationReason" + } + + let private parseField (element : JsonElement) : IntrospectionField = + { + Name = tryGetString element "name" |> Option.defaultValue "" + Description = tryGetString element "description" + Args = + match element.TryGetProperty "args" with + | true, el when el.ValueKind = JsonValueKind.Array -> + el.EnumerateArray () |> Seq.map parseInputVal |> Array.ofSeq + | _ -> [||] + Type = + match element.TryGetProperty "type" with + | true, el -> parseTypeRef el + | _ -> { Kind = Unchecked.defaultof; Name = None; Description = None; OfType = None } + IsDeprecated = tryGetBool element "isDeprecated" false + DeprecationReason = tryGetString element "deprecationReason" + } + + let private parseType (element : JsonElement) : IntrospectionType = + let tryGetArrayOfTypeRef (name : string) = + match element.TryGetProperty name with + | true, el when el.ValueKind = JsonValueKind.Array -> + Some (el.EnumerateArray () |> Seq.map parseTypeRef |> Array.ofSeq) + | _ -> None + { + Kind = + match element.TryGetProperty "kind" with + | true, el when el.ValueKind = JsonValueKind.String -> parseTypeKind (el.GetString ()) + | _ -> Unchecked.defaultof + Name = tryGetString element "name" |> Option.defaultValue "" + Description = tryGetString element "description" + Fields = + match element.TryGetProperty "fields" with + | true, el when el.ValueKind = JsonValueKind.Array -> + Some (el.EnumerateArray () |> Seq.map parseField |> Array.ofSeq) + | _ -> None + Interfaces = tryGetArrayOfTypeRef "interfaces" + PossibleTypes = tryGetArrayOfTypeRef "possibleTypes" + EnumValues = + match element.TryGetProperty "enumValues" with + | true, el when el.ValueKind = JsonValueKind.Array -> + Some (el.EnumerateArray () |> Seq.map parseEnumVal |> Array.ofSeq) + | _ -> None + InputFields = + match element.TryGetProperty "inputFields" with + | true, el when el.ValueKind = JsonValueKind.Array -> + Some (el.EnumerateArray () |> Seq.map parseInputVal |> Array.ofSeq) + | _ -> None + OfType = + match element.TryGetProperty "ofType" with + | true, el when el.ValueKind = JsonValueKind.Object -> Some (parseTypeRef el) + | _ -> None + } + + let private parseDirective (element : JsonElement) : IntrospectionDirective = + { + Name = tryGetString element "name" |> Option.defaultValue "" + Description = tryGetString element "description" + Locations = + match element.TryGetProperty "locations" with + | true, el when el.ValueKind = JsonValueKind.Array -> + el.EnumerateArray () + |> Seq.choose (fun e -> + if e.ValueKind = JsonValueKind.String then + Some (parseDirectiveLocation (e.GetString ())) + else + None) + |> Array.ofSeq + | _ -> [||] + Args = + match element.TryGetProperty "args" with + | true, el when el.ValueKind = JsonValueKind.Array -> + el.EnumerateArray () |> Seq.map parseInputVal |> Array.ofSeq + | _ -> [||] + } + + let parseSchema (element : JsonElement) : IntrospectionSchema = + { + QueryType = + match element.TryGetProperty "queryType" with + | true, el -> parseTypeRef el + | _ -> { Kind = Unchecked.defaultof; Name = None; Description = None; OfType = None } + MutationType = + match element.TryGetProperty "mutationType" with + | true, el when el.ValueKind = JsonValueKind.Object -> Some (parseTypeRef el) + | _ -> None + SubscriptionType = + match element.TryGetProperty "subscriptionType" with + | true, el when el.ValueKind = JsonValueKind.Object -> Some (parseTypeRef el) + | _ -> None + Types = + match element.TryGetProperty "types" with + | true, el when el.ValueKind = JsonValueKind.Array -> + el.EnumerateArray () |> Seq.map parseType |> Array.ofSeq + | _ -> [||] + Directives = + match element.TryGetProperty "directives" with + | true, el when el.ValueKind = JsonValueKind.Array -> + el.EnumerateArray () |> Seq.map parseDirective |> Array.ofSeq + | _ -> [||] + } -// TODO: Remove and use FSharp.SystemTextJson module Serialization = - let private makeOption t (value : obj) = - let otype = typedefof<_ option> - let cases = FSharpType.GetUnionCases(otype.MakeGenericType([|t|])) + + /// The default JSON serializer options used for request serialization when no custom options are provided. + let defaultSerializerOptions = + lazy FSharp.Data.GraphQL.Shared.Json.getSerializerOptions Seq.empty + + /// Converts special types (Uri, Upload, etc.) that System.Text.Json cannot handle natively + /// into their JSON-serializable representations. Applied recursively to variable values. + /// Also normalizes dictionary keys to camelCase to match GraphQL field naming conventions. + let rec private normalizeForSerialization (value : obj) : obj = match value with - | null -> FSharpValue.MakeUnion(cases.[0], [||]) - | _ -> FSharpValue.MakeUnion(cases.[1], [|value|]) - - let private downcastNone<'T> t = - match t with - | Option t -> downcast (makeOption t null) - | _ -> failwith $"Error parsing JSON value: %O{t} is not an option value." - - let private downcastType (t : Type) x = - match t with - | Option t -> downcast (makeOption t (Convert.ChangeType(x, t))) - | _ -> downcast (Convert.ChangeType(x, t)) - - let private isStringType = isType typeof - let private isDateTimeType = isType typeof - let private isDateTimeOffsetType = isType typeof - let private isUriType = isType typeof - let private isGuidType = isType typeof - let private isBooleanType = isType typeof - let private isEnumType = function (Option t | t) when t.IsEnum -> true | _ -> false - - let private downcastString (t : Type) (s : string) = - match t with - | t when isStringType t -> downcastType t s - | t when isUriType t -> - match Uri.TryCreate(s, UriKind.RelativeOrAbsolute) with - | (true, uri) -> downcastType t uri - | _ -> failwith $"Error parsing JSON value: %O{t} is an URI type, but parsing of value \"%s{s}\" failed." - | t when isDateTimeType t -> - match DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None) with - | (true, d) -> downcastType t d - | _ -> failwith $"Error parsing JSON value: %O{t} is a date type, but parsing of value \"%s{s}\" failed." - | t when isDateTimeOffsetType t -> - match DateTimeOffset.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None) with - | (true, d) -> downcastType t d - | _ -> failwith $"Error parsing JSON value: %O{t} is a date time offset type, but parsing of value \"%s{s}\" failed." - | t when isGuidType t -> - match Guid.TryParse(s) with - | (true, g) -> downcastType t g - | _ -> failwith $"Error parsing JSON value: %O{t} is a Guid type, but parsing of value \"%s{s}\" failed." - | t when isEnumType t -> - match t with - | (Option et | et) -> - try Enum.Parse(et, s) |> downcastType t - with _ -> failwith $"Error parsing JSON value: %O{t} is a Enum type, but parsing of value \"%s{s}\" failed." - | _ -> failwith $"Error parsing JSON value: %O{t} is not a string type." - - let private downcastBoolean (t : Type) b = - match t with - | t when isBooleanType t -> downcastType t b - | _ -> failwith $"Error parsing JSON value: %O{t} is not a boolean type." - - let rec private getArrayValue (t : Type) (converter : Type -> JsonValue -> obj) (items : JsonValue []) = - let castArray itemType (items : obj []) : obj = - let arr = Array.CreateInstance(itemType, items.Length) - items |> Array.iteri (fun ix i -> arr.SetValue(i, ix)) - upcast arr - let castList itemType (items : obj list) = - let tlist = typedefof<_ list>.MakeGenericType([|itemType|]) - let empty = - let uc = - Reflection.FSharpType.GetUnionCases(tlist) - |> Seq.filter (fun uc -> uc.Name = "Empty") - |> Seq.exactlyOne - Reflection.FSharpValue.MakeUnion(uc, [||]) - let rec helper items = - match items with - | [] -> empty - | [x] -> Activator.CreateInstance(tlist, [|x; empty|]) - | x :: xs -> Activator.CreateInstance(tlist, [|x; helper xs|]) - helper items - Tracer.runAndMeasureExecutionTime "Converted Array JsonValue to CLR array" (fun _ -> - match t with - | Option t -> getArrayValue t converter items |> makeOption t - | Array itype | Seq itype -> items |> Array.map (converter itype) |> castArray itype - | List itype -> items |> Array.map (converter itype) |> Array.toList |> castList itype - | _ -> failwith $"Error parsing JSON value: %O{t} is not an array type.") - - let private downcastNumber (t : Type) n = - match t with - | t when isNumericType t -> downcastType t n - | _ -> failwith $"Error parsing JSON value: %O{t} is not a numeric type." - - let rec private convert t parsed : obj = - Tracer.runAndMeasureExecutionTime $"Converted JsonValue to %O{t} type." (fun _ -> - match parsed with - | JsonValue.Null -> downcastNone t - | JsonValue.String s -> downcastString t s - | JsonValue.Float n -> downcastNumber t n - | JsonValue.Integer n -> downcastNumber t n - | JsonValue.Record jprops -> - let jprops = - jprops - |> Array.map (fun (n, v) -> n.ToLowerInvariant(), v) - |> Map.ofSeq - let tprops t = - FSharpType.GetRecordFields(t, true) - |> Array.map (fun p -> p.Name.ToLowerInvariant(), p.PropertyType) - let vals t = - tprops t - |> Array.map (fun (n, t) -> - match Map.tryFind n jprops with - | Some p -> n, convert t p - | None -> n, makeOption t null) - let rcrd = - let t = match t with Option t -> t | _ -> t - let vals = vals t - if isMap t - then Map.ofArray vals |> box - else FSharpValue.MakeRecord(t, Array.map snd vals, true) - downcastType t rcrd - | JsonValue.Array items -> items |> getArrayValue t convert - | JsonValue.Boolean b -> downcastBoolean t b) - - let deserializeRecord<'T> (json : string) : 'T = - let t = typeof<'T> - Tracer.runAndMeasureExecutionTime $"Deserialized JSON string to record type %O{t}." (fun _ -> - downcast (JsonValue.Parse(json) |> convert t)) - - let deserializeMap values = - let rec helper (values : (string * JsonValue) []) = - values - |> Array.map (fun (name, value) -> - match value with - | JsonValue.Record fields -> name, (fields |> helper |> Map.ofArray |> box) - | JsonValue.Null -> name, null - | JsonValue.String s -> name, box s - | JsonValue.Integer n -> name, box n - | JsonValue.Float f -> name, box f - | JsonValue.Array items -> name, (items |> Array.map (fun item -> null, item) |> helper |> Array.map snd |> box) - | JsonValue.Boolean b -> name, box b) + | null -> null + | :? string -> value // Must come before EnumerableValue: string implements IEnumerable + | :? Uri as u -> box (u.ToString ()) + | :? Upload as u -> box u.Name // File variables are written as the form-part name string + | :? IDictionary as d -> + // Apply FirstCharLower to keys: RecordBase.ToDictionary() uses PascalCase (FirstCharUpper) for property names + d |> Seq.map (fun kvp -> kvp.Key.FirstCharLower (), normalizeForSerialization kvp.Value) |> dict |> box + | EnumerableValue items -> items |> Array.map normalizeForSerialization |> box + | v -> v + + /// Converts a JsonElement to an F# object recursively. + let rec private deserializeElement (element : JsonElement) : obj = + match element.ValueKind with + | JsonValueKind.Object -> + element.EnumerateObject () + |> Seq.map (fun prop -> prop.Name, deserializeElement prop.Value) + |> Map.ofSeq + |> box + | JsonValueKind.Array -> + element.EnumerateArray () + |> Seq.map deserializeElement + |> Array.ofSeq + |> box + | JsonValueKind.String -> element.GetString () |> box + | JsonValueKind.Number -> + match element.TryGetInt32 () with + | true, n -> box n + | _ -> + match element.TryGetInt64 () with + | true, n -> box n + | _ -> element.GetDouble () |> box + | JsonValueKind.True -> box true + | JsonValueKind.False -> box false + | _ -> null + + let deserializeMap (values : (string * JsonElement) []) = Tracer.runAndMeasureExecutionTime "Deserialized JSON Record into FSharp Map" (fun _ -> - helper values |> Map.ofArray) - - let private isoDateFormat = "yyyy-MM-dd" - let private isoDateTimeFormat = "O" - - let rec toJsonValue (x : obj) : JsonValue = - if isNull x - then JsonValue.Null - else - let t = x.GetType() - Tracer.runAndMeasureExecutionTime $"Converted object type %O{t} to JsonValue" (fun _ -> - match x with - | null -> JsonValue.Null - | OptionValue None -> JsonValue.Null - | :? int as x -> JsonValue.Integer (int x) - | :? float as x -> JsonValue.Float x - | :? string as x -> JsonValue.String x - | :? Guid as x -> JsonValue.String (x.ToString()) - | :? DateTime as x when x.Date = x -> JsonValue.String (x.ToString(isoDateFormat)) - | :? DateTime as x -> JsonValue.String (x.ToString(isoDateTimeFormat)) - | :? DateTimeOffset as x -> JsonValue.String (x.ToString(isoDateTimeFormat)) - | :? bool as x -> JsonValue.Boolean x - | :? Uri as x -> JsonValue.String (x.ToString()) - | :? Upload as u -> JsonValue.String u.Name - | :? IDictionary as items -> - items - |> Seq.map (fun (KeyValue (k, v)) -> k.FirstCharLower(), toJsonValue v) - |> Seq.toArray - |> JsonValue.Record - | EnumerableValue items -> - items - |> Array.map toJsonValue - |> JsonValue.Array - | OptionValue (Some x) -> toJsonValue x - | EnumValue x -> JsonValue.String x - | _ -> - let props = t.GetProperties(BindingFlags.Public ||| BindingFlags.Instance) - let items = props |> Array.map (fun p -> (p.Name.FirstCharLower(), p.GetValue(x) |> toJsonValue)) - JsonValue.Record items) - - let serializeRecord (x : obj) = - Tracer.runAndMeasureExecutionTime $"Serialized object type %O{x.GetType()} to a JSON string" (fun _ -> - (toJsonValue x).ToString()) + values + |> Array.map (fun (name, element) -> name, deserializeElement element) + |> Map.ofArray) + + /// Builds the JSON body for a standard GraphQL request. + let buildRequestJson (options : JsonSerializerOptions) (operationName : string option) (query : string) (variables : (string * obj) []) = + Tracer.runAndMeasureExecutionTime "Built GraphQL request JSON" (fun _ -> + use stream = new MemoryStream () + use writer = new Utf8JsonWriter (stream, JsonWriterOptions (Indented = false)) + writer.WriteStartObject () + writer.WritePropertyName "operationName" + match operationName with + | Some name -> writer.WriteStringValue name + | None -> writer.WriteNullValue () + writer.WritePropertyName "query" + writer.WriteStringValue query + writer.WritePropertyName "variables" + if variables = null || variables.Length = 0 then + writer.WriteNullValue () + else + let dict = variables |> Array.map (fun (k, v) -> k, normalizeForSerialization v) |> dict + JsonSerializer.Serialize (writer, dict, options) + writer.WriteEndObject () + writer.Flush () + Encoding.UTF8.GetString (stream.ToArray ())) + + /// Builds the JSON body for the "map" part of a multipart GraphQL request. + let buildMapJson (files : (string * Upload) []) = + Tracer.runAndMeasureExecutionTime "Built GraphQL map JSON" (fun _ -> + use stream = new MemoryStream () + use writer = new Utf8JsonWriter (stream, JsonWriterOptions (Indented = false)) + writer.WriteStartObject () + files + |> Array.iteri (fun ix (name, _) -> + writer.WritePropertyName (ix.ToString ()) + writer.WriteStartArray () + writer.WriteStringValue ("variables." + name) + writer.WriteEndArray ()) + writer.WriteEndObject () + writer.Flush () + Encoding.UTF8.GetString (stream.ToArray ())) let deserializeSchema (json : string) = Tracer.runAndMeasureExecutionTime "Deserialized schema" (fun _ -> - let result = deserializeRecord> json - match result.Errors with - | None -> result.Data.__schema - | Some errors -> String.concat "\n" errors |> failwithf "%s") + use doc = JsonDocument.Parse json + let root = doc.RootElement + let errors = + match root.TryGetProperty "errors" with + | true, errorsEl when errorsEl.ValueKind = JsonValueKind.Array && errorsEl.GetArrayLength () > 0 -> + errorsEl.EnumerateArray () + |> Seq.choose (fun e -> + match e.TryGetProperty "message" with + | true, msgEl when msgEl.ValueKind = JsonValueKind.String -> Some (msgEl.GetString ()) + | _ -> None) + |> Seq.toArray + | _ -> [||] + if errors.Length > 0 then + String.concat "\n" errors |> failwithf "%s" + match root.TryGetProperty "data" with + | true, dataEl -> + match dataEl.TryGetProperty "__schema" with + | true, schemaEl -> SchemaParser.parseSchema schemaEl + | _ -> failwith "Expected \"__schema\" field in the response data." + | _ -> failwith "Expected \"data\" field in the response.") diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs index 51f8682c5..bc33ab3e8 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitions.fs @@ -392,17 +392,73 @@ module SchemaDefinitions = | false, _ -> getParseError destinationType s | InlineConstant value -> value.GetCoerceError destinationType - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take option of provided value. - let Nullable(innerDef : #TypeDef<'Val>) : NullableDef<'Val> = upcast { NullableDefinition.OfType = innerDef } - - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take voption of provided value. - let StructNullable(innerDef : #TypeDef<'Val>) : StructNullableDef<'Val> = upcast { StructNullableDefinition.OfType = innerDef } - - /// Wraps a GraphQL type definition, allowing defining field/argument - /// to take collection of provided value. - let ListOf(innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Seq> = upcast { ListOfDefinition.OfType = innerDef } + type TypeWrapperStaticDispatch = + + static member Nullable<'Val>(innerDef : InputOutputDef<'Val>) : NullableDef<'Val> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member Nullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val option> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member Nullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val option> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { NullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : InputOutputDef<'Val>) : StructNullableDef<'Val> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : InputDef<'Val>) : InputDef<'Val voption> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member StructNullable<'Val>(innerDef : OutputDef<'Val>) : OutputDef<'Val voption> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { StructNullableDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputOutputDef<'Val>) : ListOfDef<'Val, 'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : InputDef<'Val>) : InputDef<'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + static member ListOf<'Val, 'Seq when 'Seq :> 'Val seq>(innerDef : OutputDef<'Val>) : OutputDef<'Seq> = + let ofType : TypeDef<'Val> = upcast innerDef + upcast { ListOfDefinition.OfType = ofType } + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take option of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline Nullable< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member Nullable : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member Nullable : ^Def -> ^Wrapped) innerDef) + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take voption of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline StructNullable< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member StructNullable : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member StructNullable : ^Def -> ^Wrapped) innerDef) + + /// Wraps a GraphQL input or output type definition, allowing defining field/argument + /// to take collection of provided value while preserving input/output kind of wrapped type. + /// Input wrappers produce input definitions, output wrappers produce output definitions, + /// and wrappers over types implementing both kinds keep both capabilities. + /// Dispatch is selected at compile time via SRTP. + let inline ListOf< ^Def, ^Wrapped when (^Def or TypeWrapperStaticDispatch) : (static member ListOf : ^Def -> ^Wrapped) > + (innerDef : ^Def) + : ^Wrapped = + ((^Def or TypeWrapperStaticDispatch) : (static member ListOf : ^Def -> ^Wrapped) innerDef) let internal variableOrElse other (_ : InputExecutionContextProvider) value (variables : IReadOnlyDictionary) = match value with @@ -1415,13 +1471,14 @@ module SchemaDefinitions = /// If defined, this value will be used when no matching input has been provided by the requester. /// Optional input description. Usefull for generating documentation. static member SkippableInput(name : string, typedef : #InputDef<'In>, ?description : string) : InputFieldDef = + let typedef : InputDef<'In> = upcast typedef upcast { InputFieldDefinition.Name = name Description = description |> Option.map (fun s -> s + " Skip this field if you want to avoid saving it") IsSkippable = true TypeDef = - match (box typedef) with - | :? NullableDef<'In> as n -> n - | _ -> Nullable typedef + match (box typedef) with + | :? NullableDef<'In> as n -> (n :> InputDef<'In option>) + | _ -> Nullable typedef DefaultValue = None ExecuteInput = Unchecked.defaultof } @@ -1539,4 +1596,3 @@ module SchemaDefinitions = Description = description FieldsFn = fun () -> fieldsFn() |> List.toArray ResolveType = resolveType } - diff --git a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs index b11909644..1da3341c7 100644 --- a/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs +++ b/src/FSharp.Data.GraphQL.Shared/SchemaDefinitionsExtensions.fs @@ -27,8 +27,10 @@ type internal CustomFieldsObjectDefinition<'Val> (source : ObjectDef<'Val>, fiel member _.Implements = source.Implements member _.IsTypeOf = source.IsTypeOf interface TypeDef with - member this.MakeList () = upcast (ListOf this) - member this.MakeNullable () = upcast (Nullable this) + // We construct wrappers directly here because this API works with untyped TypeDef values. + // The public ListOf/Nullable helpers use SRTP dispatch and require statically known direction. + member this.MakeList () = upcast { ListOfDefinition.OfType = this } + member this.MakeNullable () = upcast { NullableDefinition.OfType = this } member _.Type = (source :> TypeDef).Type interface NamedDef with member _.Name = (source :> NamedDef).Name diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs index 11006387b..be8e79b55 100644 --- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs +++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs @@ -609,6 +609,24 @@ and OutputDef<'Val> = inherit TypeDef<'Val> end +/// Representation of type definitions that can be used as both inputs and outputs +/// (for example scalars and enums). This marker is also used by SRTP wrapper dispatch. +and InputOutputDef = + interface + inherit InputDef + inherit OutputDef + end + +/// Representation of all type definitions, that can be used as both inputs and outputs +/// and are constrained to represent the provided .NET type. +and InputOutputDef<'Val> = + interface + inherit InputOutputDef + inherit TypeDef<'Val> + inherit InputDef<'Val> + inherit OutputDef<'Val> + end + /// Representation of leaf type definitions. Leaf types represents leafs /// of the GraphQL query tree. Each query path must end with a leaf. /// By default only scalars and enums are valid leaf types. @@ -1067,8 +1085,7 @@ and ScalarDef = abstract CoerceOutput : obj -> obj option inherit TypeDef inherit NamedDef - inherit InputDef - inherit OutputDef + inherit InputOutputDef inherit LeafDef end @@ -1098,6 +1115,7 @@ and [] ScalarDefinition<'Primitive, 'Val> = { interface InputDef interface OutputDef + interface InputOutputDef interface ScalarDef with member x.Name = x.Name @@ -1107,6 +1125,7 @@ and [] ScalarDefinition<'Primitive, 'Val> = { interface InputDef<'Val> interface OutputDef<'Val> + interface InputOutputDef<'Val> interface LeafDef interface NamedDef with @@ -1182,8 +1201,7 @@ and EnumDef = /// List of available enum cases. abstract Options : EnumVal[] inherit TypeDef - inherit InputDef - inherit OutputDef + inherit InputOutputDef inherit LeafDef inherit NamedDef end @@ -1197,8 +1215,7 @@ and EnumDef<'Val> = abstract Options : EnumValue<'Val>[] inherit EnumDef inherit TypeDef<'Val> - inherit InputDef<'Val> - inherit OutputDef<'Val> + inherit InputOutputDef<'Val> end and internal EnumDefinition<'Val> = { @@ -1212,6 +1229,7 @@ and internal EnumDefinition<'Val> = { interface InputDef interface OutputDef + interface InputOutputDef interface TypeDef with member _.Type = typeof<'Val> @@ -1523,8 +1541,7 @@ and ListOfDef<'Val, 'Seq when 'Seq :> 'Val seq> = /// GraphQL type definition of the container element type. abstract OfType : TypeDef<'Val> inherit TypeDef<'Seq> - inherit InputDef<'Seq> - inherit OutputDef<'Seq> + inherit InputOutputDef<'Seq> inherit ListOfDef end @@ -1574,8 +1591,7 @@ and NullableDef<'Val> = interface /// GraphQL type definition of the nested type. abstract OfType : TypeDef<'Val> - inherit InputDef<'Val option> - inherit OutputDef<'Val option> + inherit InputOutputDef<'Val option> inherit NullableDef end @@ -1614,8 +1630,7 @@ and StructNullableDef<'Val> = interface /// GraphQL type definition of the nested type. abstract OfType : TypeDef<'Val> - inherit InputDef<'Val voption> - inherit OutputDef<'Val voption> + inherit InputOutputDef<'Val voption> inherit NullableDef end diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj index f133dee2a..5d7e2d004 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/FSharp.Data.GraphQL.IntegrationTests.fsproj @@ -7,6 +7,7 @@ + @@ -15,6 +16,7 @@ + @@ -22,7 +24,9 @@ + + @@ -30,16 +34,19 @@ + + + + + - ..\..\src\FSharp.Data.GraphQL.Client\bin\Debug\netstandard2.0\FSharp.Data.GraphQL.Client.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Release\netstandard2.0\FSharp.Data.GraphQL.Client.dll + ..\..\src\FSharp.Data.GraphQL.Client\bin\$(Configuration)\netstandard2.0\FSharp.Data.GraphQL.Client.dll ..\..\bin\FSharp.Data.GraphQL.Client\netstandard2.0\FSharp.Data.GraphQL.Client.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Debug\netstandard2.0\FSharp.Data.GraphQL.Shared.dll - ..\..\src\FSharp.Data.GraphQL.Client\bin\Release\netstandard2.0\FSharp.Data.GraphQL.Shared.dll - ..\..\bin\FSharp.Data.GraphQL.Client\netstandard2.0\FSharp.Data.GraphQL.Shared.dll + ..\..\src\FSharp.Data.GraphQL.Shared\bin\$(Configuration)\net10.0\FSharp.Data.GraphQL.Shared.dll + ..\..\bin\FSharp.Data.GraphQL.Shared\net10.0\FSharp.Data.GraphQL.Shared.dll diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs new file mode 100644 index 000000000..80a23c5ce --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/IntrospectionUpdateTests.fs @@ -0,0 +1,90 @@ +module FSharp.Data.GraphQL.IntegrationTests.IntrospectionUpdateTests + +open System +open System.IO +open System.Net.Http.Json +open System.Text.Json +open System.Threading +open Xunit + +let introspectionFilePath = + Path.Combine (__SOURCE_DIRECTORY__, "integration-introspection.json") + |> Path.GetFullPath + +let normalizeJsonDocument options (document : JsonDocument) = + use buffer = new MemoryStream () + use writer = new Utf8JsonWriter (buffer, options) + document.WriteTo writer + writer.Flush () + buffer.Seek (0L, SeekOrigin.Begin) |> ignore + JsonDocument.Parse buffer + +let parseAndNormalizeJsonAsync ct options stream = + task { + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return normalizeJsonDocument options document + } + +let areSchemasEqual (document1 : JsonDocument) (document2 : JsonDocument) = + let schema1 = document1.RootElement.GetProperty("data").GetProperty("__schema") + let schema2 = document2.RootElement.GetProperty("data").GetProperty("__schema") + schema1.GetRawText() = schema2.GetRawText() + +let readDestinationDocumentAsync ct (stream : FileStream) = + task { + try + let! document = JsonDocument.ParseAsync (stream, cancellationToken = ct) + return ValueSome document + with :? JsonException -> + return ValueNone + } + +let updateIntrospectionFileAsync ct sourceStream = + task { + use destinationStream = + new FileStream (introspectionFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read) + + let options = JsonWriterOptions(Indented = true) + let! sourceDocument = parseAndNormalizeJsonAsync ct options sourceStream + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + let! destinationDocument = readDestinationDocumentAsync ct destinationStream + + let shouldUpdate = + match destinationDocument with + | ValueNone -> true + | ValueSome document -> not (areSchemasEqual document sourceDocument) + + if shouldUpdate then + destinationStream.Seek (0L, SeekOrigin.Begin) |> ignore + destinationStream.SetLength 0 + use writer = new Utf8JsonWriter (destinationStream, options) + sourceDocument.WriteTo writer + writer.Flush () + + return shouldUpdate + } + +[] +let ``Get GraphQL introspection response returns schema`` () = + task { + use httpClient = TestHosts.createIntegrationHttpClient () + let! response = httpClient.GetFromJsonAsync("/", CancellationToken.None) + let schema = response.GetProperty("data").GetProperty("__schema") + Assert.NotEqual(Unchecked.defaultof, schema) + let hasErrors, _ = response.TryGetProperty "errors" + Assert.False hasErrors + } + +[] +let ``Update integration introspection file when schema changes`` () = + task { + use httpClient = TestHosts.createIntegrationHttpClient () + let! sourceStream = httpClient.GetStreamAsync("/") + let! wasUpdated = updateIntrospectionFileAsync CancellationToken.None sourceStream + Assert.True(File.Exists introspectionFilePath) + if wasUpdated then + let! sourceStreamSecondRun = httpClient.GetStreamAsync("/") + use sourceStreamForVerification = sourceStreamSecondRun + let! wasUpdatedSecondRun = updateIntrospectionFileAsync CancellationToken.None sourceStreamForVerification + Assert.False wasUpdatedSecondRun + } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs index 897364cb1..24944207e 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderTests.fs @@ -5,13 +5,14 @@ open System.Threading.Tasks open FSharp.Data.GraphQL open Helpers -let [] ServerUrl = "http://localhost:8085" +let [] IntrospectionPath = "integration-introspection.json" let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider // type FileProvider = GraphQLProvider -let context = Provider.GetContext(ServerUrl) +let connection = TestHosts.createIntegrationConnection () +let context = Provider.GetContext(serverUrl = TestHosts.integrationServerUrl, connectionFactory = fun () -> connection) type Input = Provider.Types.Input type InputField = Provider.Types.InputField @@ -59,7 +60,7 @@ module SimpleOperation = [] let ``Should be able to execute a query without sending input field``() = - SimpleOperation.operation.Run() + SimpleOperation.operation.Run(context) |> SimpleOperation.validateResult None [] @@ -69,7 +70,7 @@ let ``Should be able to execute a query using context, without sending input fie [] let ``Should be able to execute a query without sending input field asynchronously``() : Task = task { - let! result = SimpleOperation.operation.AsyncRun() + let! result = SimpleOperation.operation.AsyncRun(context) result |> SimpleOperation.validateResult None } @@ -82,7 +83,7 @@ let ``Should be able to execute a query using context, without sending input fie [] let ``Should be able to execute a query sending an empty input field``() = let input = Input() - SimpleOperation.operation.Run(input) + SimpleOperation.operation.Run(context, input) |> SimpleOperation.validateResult (Some input) [] @@ -94,7 +95,7 @@ let ``Should be able to execute a query using context, sending an empty input fi [] let ``Should be able to execute a query without sending an empty input field asynchronously``() : Task = task { let input = Input() - let! result = SimpleOperation.operation.AsyncRun(input) + let! result = SimpleOperation.operation.AsyncRun(context, input) result |> SimpleOperation.validateResult (Some input) } @@ -109,7 +110,7 @@ let ``Should be able to execute a query using context, sending an empty input fi let ``Should be able to execute a query sending an input field with single field``() = let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let input = Input(single) - SimpleOperation.operation.Run(input) + SimpleOperation.operation.Run(context, input) |> SimpleOperation.validateResult (Some input) [] @@ -123,7 +124,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query without sending an input field with single field asynchronously``() : Task = task { let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let input = Input(single) - let! result = SimpleOperation.operation.AsyncRun(input) + let! result = SimpleOperation.operation.AsyncRun(context, input) result |> SimpleOperation.validateResult (Some input) } @@ -139,7 +140,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query sending an input field with list field``() = let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(list) - SimpleOperation.operation.Run(input) + SimpleOperation.operation.Run(context, input) |> SimpleOperation.validateResult (Some input) [] @@ -153,7 +154,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query without sending an input field with list field asynchronously``() : Task = task { let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(list) - let! result = SimpleOperation.operation.AsyncRun(input) + let! result = SimpleOperation.operation.AsyncRun(context, input) result |> SimpleOperation.validateResult (Some input) } @@ -170,7 +171,7 @@ let ``Should be able to execute a query sending an input field with single and l let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(single, list) - SimpleOperation.operation.Run(input) + SimpleOperation.operation.Run(context, input) |> SimpleOperation.validateResult (Some input) [] @@ -186,7 +187,7 @@ let ``Should be able to execute a query without sending an input field with sing let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(single, list) - let! result = SimpleOperation.operation.AsyncRun(input) + let! result = SimpleOperation.operation.AsyncRun(context, input) result |> SimpleOperation.validateResult (Some input) } @@ -221,13 +222,13 @@ module SingleRequiredUploadOperation = [] 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(context, 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()) + let! result = SingleRequiredUploadOperation.operation.AsyncRun(context, file.MakeUpload()) result |> SingleRequiredUploadOperation.validateResult file } @@ -256,24 +257,24 @@ module SingleOptionalUploadOperation = [] 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.operation.Run(context, 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()) + let! result = SingleOptionalUploadOperation.operation.AsyncRun(context, 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.operation.Run(context) |> 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() + let! result = SingleOptionalUploadOperation.operation.AsyncRun(context) result |> SingleOptionalUploadOperation.validateResult None } @@ -302,7 +303,7 @@ let ``Should be able to execute a multiple required upload``() = 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" } |] - RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) + RequiredMultipleUploadOperation.operation.Run(context, files |> Array.map (fun f -> f.MakeUpload())) |> RequiredMultipleUploadOperation.validateResult files [] @@ -310,7 +311,7 @@ let ``Should be able to execute a multiple required upload asynchronously``() : 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 = RequiredMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload())) + let! result = RequiredMultipleUploadOperation.operation.AsyncRun(context, files |> Array.map (fun f -> f.MakeUpload())) result |> RequiredMultipleUploadOperation.validateResult files } @@ -339,7 +340,7 @@ let ``Should be able to execute a multiple upload``() = 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" } |] - OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) + OptionalMultipleUploadOperation.operation.Run(context, files |> Array.map (fun f -> f.MakeUpload())) |> OptionalMultipleUploadOperation.validateResult (Some files) [] @@ -347,18 +348,18 @@ let ``Should be able to execute a multiple upload asynchronously``() : Task = ta 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())) + let! result = OptionalMultipleUploadOperation.operation.AsyncRun(context, files |> Array.map (fun f -> f.MakeUpload())) result |> OptionalMultipleUploadOperation.validateResult (Some files) } [] let ``Should be able to execute a multiple upload by sending no uploads``() = - OptionalMultipleUploadOperation.operation.Run() + OptionalMultipleUploadOperation.operation.Run(context) |> OptionalMultipleUploadOperation.validateResult None [] let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleUploadOperation.operation.AsyncRun() + let! result = OptionalMultipleUploadOperation.operation.AsyncRun(context) result |> OptionalMultipleUploadOperation.validateResult None } @@ -387,7 +388,7 @@ 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()))) + OptionalMultipleOptionalUploadOperation.operation.Run(context, files |> Array.map (Option.map (fun f -> f.MakeUpload()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] @@ -395,18 +396,18 @@ let ``Should be able to execute a multiple optional upload asynchronously``() : 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()))) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context, files |> Array.map (Option.map (fun f -> f.MakeUpload()))) result |> (OptionalMultipleOptionalUploadOperation.validateResult (Some files)) } [] let ``Should be able to execute a multiple optional upload by sending no uploads``() = - OptionalMultipleOptionalUploadOperation.operation.Run() + OptionalMultipleOptionalUploadOperation.operation.Run(context) |> 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() + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context) result |> OptionalMultipleOptionalUploadOperation.validateResult None } @@ -417,7 +418,7 @@ let ``Should be able to execute a multiple optional upload by sending some uploa 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()))) + OptionalMultipleOptionalUploadOperation.operation.Run(context, files |> Array.map (Option.map (fun f -> f.MakeUpload()))) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] @@ -427,7 +428,7 @@ let ``Should be able to execute a multiple optional upload asynchronously by sen 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()))) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context, files |> Array.map (Option.map (fun f -> f.MakeUpload()))) result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } @@ -484,7 +485,7 @@ let ``Should be able to upload files inside another input type``() : Task = task multiple = Array.map makeUpload request.Multiple, nullableMultiple = Array.map makeUpload request.NullableMultiple.Value, nullableMultipleNullable = Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value) - let! result = UploadRequestOperation.operation.AsyncRun(input) + let! result = UploadRequestOperation.operation.AsyncRun(context, input) result |> UploadRequestOperation.validateResult request } @@ -506,7 +507,7 @@ module UploadComplexOperation = let ``Should be able to upload file using complex input object`` () = let file = { Name = "complex.txt"; ContentType = "text/plain"; Content = "Complex input object file content" } let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(input) + UploadComplexOperation.operation.Run(context, input) |> UploadComplexOperation.validateResult file [] @@ -520,7 +521,7 @@ let ``Should be able to upload file using complex input object with context`` () let ``Should be able to upload file using complex input object asynchronously`` () : Task = task { let file = { Name = "complex_async.txt"; ContentType = "text/plain"; Content = "Complex input object async file content" } let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(input) + let! result = UploadComplexOperation.operation.AsyncRun(context, input) result |> UploadComplexOperation.validateResult file } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs index 74cc97a48..5422c356a 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/LocalProviderWithOptionalParametersOnlyTests.fs @@ -5,12 +5,13 @@ open System.Threading.Tasks open FSharp.Data.GraphQL open Helpers -let [] ServerUrl = "http://localhost:8085" +let [] IntrospectionPath = "integration-introspection.json" let [] EmptyGuidAsString = "00000000-0000-0000-0000-000000000000" -type Provider = GraphQLProvider +type Provider = GraphQLProvider -let context = Provider.GetContext(ServerUrl) +let connection = TestHosts.createIntegrationConnection () +let context = Provider.GetContext(serverUrl = TestHosts.integrationServerUrl, connectionFactory = fun () -> connection) type Input = Provider.Types.Input type InputField = Provider.Types.InputField @@ -58,7 +59,7 @@ module SimpleOperation = [] let ``Should be able to execute a query without sending input field``() = - SimpleOperation.operation.Run() + SimpleOperation.operation.Run(context) |> SimpleOperation.validateResult None [] @@ -68,7 +69,7 @@ let ``Should be able to execute a query using context, without sending input fie [] let ``Should be able to execute a query without sending input field asynchronously``() = - SimpleOperation.operation.AsyncRun() + SimpleOperation.operation.AsyncRun(context) |> Async.RunSynchronously |> SimpleOperation.validateResult None @@ -81,7 +82,7 @@ let ``Should be able to execute a query using context, without sending input fie [] let ``Should be able to execute a query sending an empty input field``() = let input = Input() - SimpleOperation.operation.Run(Some input) + SimpleOperation.operation.Run(context, Some input) |> SimpleOperation.validateResult (Some input) [] @@ -93,7 +94,7 @@ let ``Should be able to execute a query using context, sending an empty input fi [] let ``Should be able to execute a query without sending an empty input field asynchronously``() : Task = task { let input = Input() - let! result = SimpleOperation.operation.AsyncRun(Some input) + let! result = SimpleOperation.operation.AsyncRun(context, Some input) result |> SimpleOperation.validateResult (Some input) } @@ -108,7 +109,7 @@ let ``Should be able to execute a query using context, sending an empty input fi let ``Should be able to execute a query sending an input field with single field``() = let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let input = Input(Some single) - SimpleOperation.operation.Run(Some input) + SimpleOperation.operation.Run(context, Some input) |> SimpleOperation.validateResult (Some input) [] @@ -122,7 +123,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query without sending an input field with single field asynchronously``() : Task = task { let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let input = Input(Some single) - let! result = SimpleOperation.operation.AsyncRun(Some input) + let! result = SimpleOperation.operation.AsyncRun(context, Some input) result |> SimpleOperation.validateResult (Some input) } @@ -138,7 +139,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query sending an input field with list field``() = let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(list = Some list) - SimpleOperation.operation.Run(Some input) + SimpleOperation.operation.Run(context, Some input) |> SimpleOperation.validateResult (Some input) [] @@ -152,7 +153,7 @@ let ``Should be able to execute a query using context, sending an input field wi let ``Should be able to execute a query without sending an input field with list field asynchronously``() : Task = task { let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(list = Some list) - let! result = SimpleOperation.operation.AsyncRun(Some input) + let! result = SimpleOperation.operation.AsyncRun(context, Some input) result |> SimpleOperation.validateResult (Some input) } @@ -169,7 +170,7 @@ let ``Should be able to execute a query sending an input field with single and l let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(Some single, Some list) - SimpleOperation.operation.Run(Some input) + SimpleOperation.operation.Run(context, Some input) |> SimpleOperation.validateResult (Some input) [] @@ -185,7 +186,7 @@ let ``Should be able to execute a query without sending an input field with sing let single = InputField("A", 2, System.Uri("http://localhost:1234"), EmptyGuidAsString) let list = [|InputField("A", 2, System.Uri("http://localhost:4321"), EmptyGuidAsString)|] let input = Input(Some single, Some list) - let! result = SimpleOperation.operation.AsyncRun(Some input) + let! result = SimpleOperation.operation.AsyncRun(context, Some input) result |> SimpleOperation.validateResult (Some input) } @@ -220,13 +221,13 @@ module SingleRequiredUploadOperation = [] 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(file.Name)) + SingleRequiredUploadOperation.operation.Run(context, 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()) + let! result = SingleRequiredUploadOperation.operation.AsyncRun(context, file.MakeUpload()) result |> SingleRequiredUploadOperation.validateResult file } @@ -254,24 +255,24 @@ module SingleOptionalUploadOperation = [] 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.operation.Run(context, 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("test") |> Some) + let! result = SingleOptionalUploadOperation.operation.AsyncRun(context, 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.operation.Run(context) |> 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() + let! result = SingleOptionalUploadOperation.operation.AsyncRun(context) result |> SingleOptionalUploadOperation.validateResult None } @@ -300,7 +301,7 @@ let ``Should be able to execute a multiple required upload``() = 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" } |] - RequiredMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload())) + RequiredMultipleUploadOperation.operation.Run(context, files |> Array.map (fun f -> f.MakeUpload())) |> RequiredMultipleUploadOperation.validateResult files [] @@ -308,7 +309,7 @@ let ``Should be able to execute a multiple required upload asynchronously``() : 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 = RequiredMultipleUploadOperation.operation.AsyncRun(files |> Array.map (fun f -> f.MakeUpload())) + let! result = RequiredMultipleUploadOperation.operation.AsyncRun(context, files |> Array.map (fun f -> f.MakeUpload())) result |> RequiredMultipleUploadOperation.validateResult files } @@ -337,7 +338,7 @@ let ``Should be able to execute a multiple upload``() = 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" } |] - OptionalMultipleUploadOperation.operation.Run(files |> Array.map (fun f -> f.MakeUpload()) |> Some) + OptionalMultipleUploadOperation.operation.Run(context, files |> Array.map (fun f -> f.MakeUpload()) |> Some) |> OptionalMultipleUploadOperation.validateResult (Some files) [] @@ -345,18 +346,18 @@ let ``Should be able to execute a multiple upload asynchronously``() : Task = ta 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 _.MakeUpload()) |> Some) + let! result = OptionalMultipleUploadOperation.operation.AsyncRun(context, (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.operation.Run(context) |> OptionalMultipleUploadOperation.validateResult None [] let ``Should be able to execute a multiple upload asynchronously by sending no uploads``() : Task = task { - let! result = OptionalMultipleUploadOperation.operation.AsyncRun() + let! result = OptionalMultipleUploadOperation.operation.AsyncRun(context) result |> OptionalMultipleUploadOperation.validateResult None } @@ -385,7 +386,7 @@ 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 _.MakeUpload())) |> Some) + OptionalMultipleOptionalUploadOperation.operation.Run(context, (files |> Array.map (Option.map _.MakeUpload())) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] @@ -393,18 +394,18 @@ let ``Should be able to execute a multiple optional upload asynchronously``() : 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 _.MakeUpload())) |> Some) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context, (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.operation.Run(context) |> 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() + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context) result |> OptionalMultipleOptionalUploadOperation.validateResult None } @@ -415,7 +416,7 @@ let ``Should be able to execute a multiple optional upload by sending some uploa None Some { Name = "file2.txt"; ContentType = "text/plain"; Content = "Sample text file contents 2" } None |] - OptionalMultipleOptionalUploadOperation.operation.Run(files |> Array.map (Option.map _.MakeUpload()) |> Some) + OptionalMultipleOptionalUploadOperation.operation.Run(context, files |> Array.map (Option.map _.MakeUpload()) |> Some) |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) [] @@ -425,7 +426,7 @@ let ``Should be able to execute a multiple optional upload asynchronously by sen 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 _.MakeUpload()) |> Some) + let! result = OptionalMultipleOptionalUploadOperation.operation.AsyncRun(context, files |> Array.map (Option.map _.MakeUpload()) |> Some) result |> OptionalMultipleOptionalUploadOperation.validateResult (Some files) } @@ -482,7 +483,7 @@ let ``Should be able to upload files inside another input type``() = multiple = Array.map makeUpload request.Multiple, nullableMultiple = Some (Array.map makeUpload request.NullableMultiple.Value), nullableMultipleNullable = Some (Array.map (Option.map makeUpload) request.NullableMultipleNullable.Value)) - UploadRequestOperation.operation.Run(input) + UploadRequestOperation.operation.Run(context, input) |> UploadRequestOperation.validateResult request module UploadComplexOperation = @@ -503,7 +504,7 @@ module UploadComplexOperation = let ``Should be able to upload file using complex input object`` () = let file = { Name = "complex.txt"; ContentType = "text/plain"; Content = "Complex input object file content" } let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - UploadComplexOperation.operation.Run(input) + UploadComplexOperation.operation.Run(context, input) |> UploadComplexOperation.validateResult file [] @@ -517,7 +518,7 @@ let ``Should be able to upload file using complex input object with context`` () let ``Should be able to upload file using complex input object asynchronously`` () : Task = task { let file = { Name = "complex_async.txt"; ContentType = "text/plain"; Content = "Complex input object async file content" } let input = UploadComplexOperation.InputFile(file = file.MakeUpload()) - let! result = UploadComplexOperation.operation.AsyncRun(input) + let! result = UploadComplexOperation.operation.AsyncRun(context, input) result |> UploadComplexOperation.validateResult file } diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs index f95b44df0..a04b48d0b 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/OperationErrorTests.fs @@ -7,9 +7,12 @@ open FSharp.Data.GraphQL open FSharp.Data.GraphQL.Client [] -let ServerUrl = "http://localhost:8085" +let IntrospectionPath = "integration-introspection.json" -type Provider = GraphQLProvider +type Provider = GraphQLProvider + +let connection = TestHosts.createIntegrationConnection () +let context = Provider.GetContext(serverUrl = TestHosts.integrationServerUrl, connectionFactory = fun () -> connection) module ErrorOperation = let operation = @@ -22,18 +25,17 @@ module ErrorOperation = [] let ``Should parse operation error fields from raw response`` () = let result = - OperationResultBase ( + new OperationResultBase ( rawResponse = new HttpResponseMessage (), responseJson = - JsonValue.Parse - """{ - "errors": [{ - "message": "unit-test error", - "path": ["alwaysError", 0], - "locations": [{ "line": 2, "column": 13 }], - "extensions": { "code": "UNIT_TEST", "retryable": false, "severity": 7 } - }] - }""", + """{ + "errors": [{ + "message": "unit-test error", + "path": ["alwaysError", 0], + "locations": [{ "line": 2, "column": 13 }], + "extensions": { "code": "UNIT_TEST", "retryable": false, "severity": 7 } + }] + }""", operationFields = [||], operationTypeName = "Query" ) @@ -75,9 +77,9 @@ let ``Should parse all combinations of optional operation error fields`` () = let responseJson = $"""{{"errors":[{{{errorObjectJson}}}]}}""" let result = - OperationResultBase ( + new OperationResultBase ( rawResponse = new HttpResponseMessage (), - responseJson = JsonValue.Parse responseJson, + responseJson = responseJson, operationFields = [||], operationTypeName = "Query" ) @@ -106,7 +108,7 @@ let ``Should parse all combinations of optional operation error fields`` () = [] let ``Should map server error extensions and locations into operation result`` () = - let result = ErrorOperation.operation.Run () + let result = ErrorOperation.operation.Run(context) result.Errors.Length |> equals 1 diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs index e673cba63..7d9ab8331 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiLocalProviderTests.fs @@ -3,17 +3,16 @@ module FSharp.Data.GraphQL.IntegrationTests.SwapiLocalProviderTests open Xunit open Helpers open FSharp.Data.GraphQL -open System.Net.Http open System.Threading.Tasks // Local provider should be able to be created from local introspection json file. type Provider = GraphQLProvider<"introspection.json"> // We are going to re-use the same HttpClient through all requests. -let connection = new GraphQLClientConnection(new HttpClient()) +let connection = TestHosts.createStarWarsConnection () // As we are not using a connection to a server to get the introspection, we need a runtime context. -let getContext() = Provider.GetContext(serverUrl = "http://localhost:8086", connectionFactory = fun () -> connection) +let getContext() = Provider.GetContext(serverUrl = TestHosts.starWarsServerUrl, connectionFactory = fun () -> connection) type Episode = Provider.Types.Episode diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs index dc73b38db..07a8c7ed0 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/SwapiRemoteProviderTests.fs @@ -5,7 +5,10 @@ open Helpers open FSharp.Data.GraphQL open System.Threading.Tasks -type Provider = GraphQLProvider<"http://localhost:8086"> +type Provider = GraphQLProvider<"introspection.json"> + +let connection = TestHosts.createStarWarsConnection () +let context = Provider.GetContext(serverUrl = TestHosts.starWarsServerUrl, connectionFactory = fun () -> connection) type Episode = Provider.Types.Episode @@ -47,13 +50,27 @@ hero (id: "1000") { result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun e -> e.Node) - friends |> equals expectedFriends + friends.Length |> equals 4 + do + let friend0 = friends[0] + friend0.IsHuman() |> equals true + friend0.AsHuman().Name |> equals (Some "Han Solo") + do + let friend1 = friends[1] + friend1.IsHuman() |> equals true + friend1.AsHuman().Name |> equals (Some "Leia Organa") + friend1.AsHuman().HomePlanet |> equals (Some "Alderaan") + do + let friend2 = friends[2] + friend2.IsDroid() |> equals true + friend2.AsDroid().Name |> equals (Some "C-3PO") + friend2.AsDroid().PrimaryFunction |> equals (Some "Protocol") + do + let friend3 = friends[3] + friend3.IsDroid() |> equals true + friend3.AsDroid().Name |> equals (Some "R2-D2") + friend3.AsDroid().PrimaryFunction |> equals (Some "Astromech") result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data let expected = normalize <| """Some @@ -80,18 +97,18 @@ hero (id: "1000") { [] let ``Should be able to start a simple query operation synchronously`` () = - SimpleOperation.operation.Run() + SimpleOperation.operation.Run(context) |> SimpleOperation.validateResult [] let ``Should be able to start a simple query operation asynchronously`` () : Task = task { - let! result = SimpleOperation.operation.AsyncRun() + let! result = SimpleOperation.operation.AsyncRun(context) result |> SimpleOperation.validateResult } [] let ``Should be able to use pattern matching methods on an union type`` () = - let result = SimpleOperation.operation.Run() + let result = SimpleOperation.operation.Run(context) result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map (fun e -> e.Node) @@ -149,12 +166,12 @@ module MutationOperation = [] let ``Should be able to run a mutation synchronously`` () = - MutationOperation.operation.Run() + MutationOperation.operation.Run(context) |> MutationOperation.validateResult [] let ``Should be able to run a mutation asynchronously`` () : Task = task { - let! result = MutationOperation.operation.AsyncRun() + let! result = MutationOperation.operation.AsyncRun(context) result |> MutationOperation.validateResult } @@ -169,13 +186,27 @@ module FileOperation = result.Data.IsSome |> equals true result.Data.Value.Hero.IsSome |> equals true result.Data.Value.Hero.Value.AppearsIn |> equals [| Episode.NewHope; Episode.Empire; Episode.Jedi |] - let expectedFriends : Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Character array = - [| Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Han Solo") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Human(name = "Leia Organa", homePlanet = "Alderaan") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "C-3PO", primaryFunction = "Protocol") - Operation.Types.HeroFields.FriendsFields.EdgesFields.NodeFields.Droid(name = "R2-D2", primaryFunction = "Astromech") |] let friends = result.Data.Value.Hero.Value.Friends.Edges |> Array.map _.Node - friends |> equals expectedFriends + friends.Length |> equals 4 + do + let friend0 = friends[0] + friend0.IsHuman() |> equals true + friend0.AsHuman().Name |> equals (Some "Han Solo") + do + let friend1 = friends[1] + friend1.IsHuman() |> equals true + friend1.AsHuman().Name |> equals (Some "Leia Organa") + friend1.AsHuman().HomePlanet |> equals (Some "Alderaan") + do + let friend2 = friends[2] + friend2.IsDroid() |> equals true + friend2.AsDroid().Name |> equals (Some "C-3PO") + friend2.AsDroid().PrimaryFunction |> equals (Some "Protocol") + do + let friend3 = friends[3] + friend3.IsDroid() |> equals true + friend3.AsDroid().Name |> equals (Some "R2-D2") + friend3.AsDroid().PrimaryFunction |> equals (Some "Astromech") result.Data.Value.Hero.Value.HomePlanet |> equals (Some "Tatooine") let actual = normalize <| sprintf "%A" result.Data let expected = normalize <| """Some @@ -202,5 +233,5 @@ module FileOperation = [] let ``Should be able to run a query from a query file`` () = - FileOperation.fileOp.Run() + FileOperation.fileOp.Run(context) |> FileOperation.validateResult diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs b/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs new file mode 100644 index 000000000..31478a08c --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/TestHosts.fs @@ -0,0 +1,43 @@ +module FSharp.Data.GraphQL.IntegrationTests.TestHosts + +open FSharp.Data.GraphQL +open Microsoft.AspNetCore.Mvc.Testing +open System.Net.Http +open System + +type IntegrationServerApplicationFactory () = + inherit WebApplicationFactory () + +type StarWarsApplicationFactory () = + inherit WebApplicationFactory () + +let private integrationFactory = lazy (new IntegrationServerApplicationFactory ()) +let private starWarsFactory = lazy (new StarWarsApplicationFactory ()) + +let createIntegrationHttpClient () : HttpClient = + integrationFactory.Value.CreateClient () + +let createStarWarsHttpClient () : HttpClient = + starWarsFactory.Value.CreateClient () + +let private getIntegrationServerUrl () = + use client = createIntegrationHttpClient () + client.BaseAddress.ToString().TrimEnd '/' + +let private getStarWarsServerUrl () = + use client = createStarWarsHttpClient () + client.BaseAddress.ToString().TrimEnd '/' + +do + AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> + if integrationFactory.IsValueCreated then + integrationFactory.Value.Dispose () + + if starWarsFactory.IsValueCreated then + starWarsFactory.Value.Dispose ()) + +let integrationServerUrl = getIntegrationServerUrl () +let starWarsServerUrl = getStarWarsServerUrl () + +let createIntegrationConnection () = new GraphQLClientConnection (createIntegrationHttpClient ()) +let createStarWarsConnection () = new GraphQLClientConnection (createStarWarsHttpClient ()) diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json new file mode 100644 index 000000000..ad448c42f --- /dev/null +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json @@ -0,0 +1,1929 @@ +{ + "documentId": 986164407, + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": { + "name": "Mutation" + }, + "subscriptionType": null, + "types": [ + { + "kind": "SCALAR", + "name": "Int", + "description": "The \u0060Int\u0060 scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The \u0060String\u0060 scalar type represents textual data, represented as UTF-8 character sequences. The \u0060String\u0060 type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The \u0060Boolean\u0060 scalar type represents \u0060true\u0060 or \u0060false\u0060.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The \u0060Float\u0060 scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The \u0060ID\u0060 scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The \u0060ID\u0060 type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as \u0060\u00224\u0022\u0060) or integer (such as \u00604\u0060) input value will be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateTimeOffset", + "description": "The \u0060DateTimeOffset\u0060 scalar type represents a Date value with Time component. The \u0060DateTimeOffset\u0060 type appears in a JSON response as a String representation compatible with ISO-8601 format.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "DateOnly", + "description": "The \u0060DateOnly\u0060 scalar type represents a Date value without Time component. The \u0060DateOnly\u0060 type appears in a JSON response as a \u0060String\u0060 representation of full-date value as specified by [IETF 3339](https://www.ietf.org/rfc/rfc3339.txt).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "TimeOnly", + "description": "The \u0060TimeOnly\u0060 scalar type represents a Time value without Date component. The \u0060TimeOnly\u0060 type appears in a JSON response as a \u0060String\u0060 representation of full-time value as specified by [IETF 3339](https://www.ietf.org/rfc/rfc3339.txt).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "URI", + "description": "The \u0060URI\u0060 scalar type represents a string resource identifier compatible with URI standard. The \u0060URI\u0060 type appears in a JSON response as a String.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. In some cases, you need to provide options to alter GraphQL\u2019s execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onField", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onFragment", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "onOperation", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the \u0060__TypeKind\u0060 enum. Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given __Type is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. \u0060fields\u0060 and \u0060interfaces\u0060 are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. \u0060fields\u0060 and \u0060possibleTypes\u0060 are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. \u0060possibleTypes\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. \u0060enumValues\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. \u0060inputFields\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. \u0060ofType\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. \u0060ofType\u0060 is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to a field argument IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to an union IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object IDL definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field IDL definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": "The query type.", + "fields": [ + { + "name": "alwaysError", + "description": "Always produces an execution error for integration tests.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "echo", + "description": "Enters an input type and get it back.", + "args": [ + { + "name": "input", + "description": "The input to be echoed as an output.", + "type": { + "kind": "INPUT_OBJECT", + "name": "Input", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Output", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Output", + "description": "The output for an input.", + "fields": [ + { + "name": "list", + "description": "A list of output fields.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "OutputField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "single", + "description": "A single output field.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "OutputField", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "OutputField", + "description": "The output for a field input.", + "fields": [ + { + "name": "deprecated", + "description": "A string value through a deprecated field.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "This field is deprecated." + }, + { + "name": "guid", + "description": "A Guid value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "guidId", + "description": "A Guid Id value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "guidIdOption", + "description": "A Guid Id value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "int", + "description": "An integer value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "intOption", + "description": "An integer option value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "string", + "description": "A string value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringId", + "description": "A String Id value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringIdOption", + "description": "A String Id value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stringOption", + "description": "A string option value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uri", + "description": "An URI value.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URI", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Guid", + "description": "The \u0060Guid\u0060 scalar type represents a Globally Unique Identifier value. It\u0027s a 128-bit long byte key, that can be serialized to string.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "Input", + "description": "Input object type.", + "fields": null, + "inputFields": [ + { + "name": "single", + "description": "A single input field.", + "type": { + "kind": "INPUT_OBJECT", + "name": "InputField", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "list", + "description": "A list of input fields.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InputField", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputField", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "string", + "description": "A string value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "int", + "description": "An integer value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "stringOption", + "description": "A string option value.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "intOption", + "description": "An integer option value.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "uri", + "description": "An URI value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "URI", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "guid", + "description": "A Guid value.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Guid", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "multipleUpload", + "description": "Uploads a list of files to the server and get them back.", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleNullableUpload", + "description": "Uploads (maybe) a list of files (maybe) to the server and get them back (maybe).", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleUpload", + "description": "Uploads (maybe) a list of files to the server and get them back (maybe).", + "args": [ + { + "name": "files", + "description": "The files to upload.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableSingleUpload", + "description": "Uploads (maybe) a single file to the server and get it back (maybe).", + "args": [ + { + "name": "file", + "description": "The file to be uploaded.", + "type": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "singleUpload", + "description": "Uploads a single file to the server and get it back.", + "args": [ + { + "name": "file", + "description": "The file to be uploaded.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uploadComplex", + "description": "", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "InputFile", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uploadRequest", + "description": "Upload several files in different forms.", + "args": [ + { + "name": "request", + "description": "The request for uploading several files in different forms.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UploadRequest", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadResponse", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UploadedFile", + "description": "Contains data of an uploaded file.", + "fields": [ + { + "name": "contentAsText", + "description": "The content of the file as text.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "contentType", + "description": "The content type of the file.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the file.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "File", + "description": "The \u0060File\u0060 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.", + "fields": null, + "inputFields": [], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "InputFile", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "file", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UploadResponse", + "description": "Contains uploaded files of an upload files request.", + "fields": [ + { + "name": "multiple", + "description": "Multiple file uploads.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultiple", + "description": "Optional list of multiple file uploads.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullableMultipleNullable", + "description": "Optional list of multiple optional file uploads.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "single", + "description": "A single file upload.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UploadedFile", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UploadRequest", + "description": "Request for uploading files in several different forms.", + "fields": null, + "inputFields": [ + { + "name": "single", + "description": "A single file upload.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "multiple", + "description": "Multiple file uploads.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + } + }, + "defaultValue": null + }, + { + "name": "nullableMultiple", + "description": "Optional list of multiple file uploads.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "nullableMultipleNullable", + "description": "Optional list of multiple optional file uploads.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "File", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the \u0060if\u0060 argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the \u0060if\u0060 argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + } + ] + }, + { + "name": "defer", + "description": "Defers the resolution of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + }, + { + "name": "stream", + "description": "Streams the resolution of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + }, + { + "name": "live", + "description": "Subscribes for live updates of this field or fragment", + "locations": [ + "FIELD", + "FRAGMENT_DEFINITION", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json index 7b3d9abcf..a961111fd 100644 --- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json +++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json @@ -1,5 +1,5 @@ { - "documentId": -128167532, + "documentId": 195530235, "data": { "__schema": { "queryType": { diff --git a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj index 763d841ec..1bc224559 100644 --- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj +++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj @@ -41,6 +41,16 @@ + + + + + + + + + + diff --git a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs index 57b330da4..ad7cf74d5 100644 --- a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs @@ -139,7 +139,7 @@ let ``Planning must retain correct types for lists``() = } } }""" - let PersonList : ListOfDef = ListOf Person + let PersonList : OutputDef = ListOf Person let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length let listInfo = plan.Fields.Head @@ -178,7 +178,7 @@ let ``Planning must work with interfaces``() = }""" let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length - let INamedList : ListOfDef = ListOf INamed + let INamedList : OutputDef = ListOf INamed let listInfo = plan.Fields.Head listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast INamedList) @@ -215,7 +215,7 @@ let ``Planning must work with unions``() = let plan = schemaProcessor.CreateExecutionPlanOrFail(query) equals 1 plan.Fields.Length let listInfo = plan.Fields.Head - let UNamedList : ListOfDef = ListOf UNamed + let UNamedList : OutputDef = ListOf UNamed listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast UNamedList) let (ResolveCollection(info)) = listInfo.Kind @@ -309,7 +309,7 @@ let ``Planning must handle inline fragment with non-matching type condition in u // Verify the execution plan structure equals 1 plan.Fields.Length let listInfo = plan.Fields.Head - let UNamedList : ListOfDef = ListOf UNamed + let UNamedList : OutputDef = ListOf UNamed listInfo.Identifier |> equals "names" listInfo.ReturnDef |> equals (upcast UNamedList) let (ResolveCollection(info)) = listInfo.Kind diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore new file mode 100644 index 000000000..a6b4595a5 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/.gitignore @@ -0,0 +1 @@ +References.fsx diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx new file mode 100644 index 000000000..c13f43223 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.InputAsOutput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let inputOnlyType = + Define.InputObject( + name = "InputOnlyType", + fields = [ Define.Input("value", IntType) ] + ) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = ListOf inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx new file mode 100644 index 000000000..00849d1db --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/ListOf.OutputAsInput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let outputOnlyType = + Define.Object( + name = "OutputOnlyType", + fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ] + ) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = ListOf outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx new file mode 100644 index 000000000..f94d9e297 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.InputAsOutput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let inputOnlyType = + Define.InputObject( + name = "InputOnlyType", + fields = [ Define.Input("value", IntType) ] + ) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = Nullable inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx new file mode 100644 index 000000000..65a876354 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Nullable.OutputAsInput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let outputOnlyType = + Define.Object( + name = "OutputOnlyType", + fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ] + ) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = Nullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx new file mode 100644 index 000000000..ffd651eaf --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.InputAsOutput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let inputOnlyType = + Define.InputObject( + name = "InputOnlyType", + fields = [ Define.Input("value", IntType) ] + ) + +// This should fail: InputDef cannot be assigned to OutputDef +let _ : OutputDef = StructNullable inputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx new file mode 100644 index 000000000..6ec0f4950 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/StructNullable.OutputAsInput.fsx @@ -0,0 +1,15 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let outputOnlyType = + Define.Object( + name = "OutputOnlyType", + fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ] + ) + +// This should fail: OutputDef cannot be assigned to InputDef +let _ : InputDef = StructNullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx new file mode 100644 index 000000000..74c56dce3 --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafety/Valid.fsx @@ -0,0 +1,26 @@ +#load "References.fsx" + +open FSharp.Data.GraphQL.Types + +type InputOnly = { Value: int } +type OutputOnly = { Value: int } + +let inputOnlyType = + Define.InputObject( + name = "InputOnlyType", + fields = [ Define.Input("value", IntType) ] + ) + +let outputOnlyType = + Define.Object( + name = "OutputOnlyType", + fields = [ Define.Field("value", IntType, fun _ x -> x.Value) ] + ) + +// These are all valid assignments and must compile successfully +let _inputList : InputDef = ListOf inputOnlyType +let _outputList : OutputDef = ListOf outputOnlyType +let _inputNullable : InputDef = Nullable inputOnlyType +let _outputNullable : OutputDef = Nullable outputOnlyType +let _inputStruct : InputDef = StructNullable inputOnlyType +let _outputStruct : OutputDef = StructNullable outputOnlyType diff --git a/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs new file mode 100644 index 000000000..cececf2dd --- /dev/null +++ b/tests/FSharp.Data.GraphQL.Tests/TypeWrappersKindSafetyTests.fs @@ -0,0 +1,144 @@ +module FSharp.Data.GraphQL.Tests.TypeWrappersKindSafetyTests + +open System +open System.Diagnostics +open System.Threading.Tasks +open FSharp.Data.GraphQL.Types +open Xunit + +type private InputOnly = { Value : int } +type private OutputOnly = { Value : int } + +let private InputOnlyType = + Define.InputObject (name = "InputOnlyType", fields = [ Define.Input ("value", IntType) ]) + +let private OutputOnlyType = + Define.Object (name = "OutputOnlyType", fields = [ Define.Field ("value", IntType, fun _ x -> x.Value) ]) + +type TypeWrappersKindSafetyFixture () = + + let scriptsDir = IO.Path.Combine (AppContext.BaseDirectory, "TypeWrappersKindSafety") + let referencesPath = IO.Path.Combine (scriptsDir, "References.fsx") + let sourceProjectDir = + IO.Path.GetFullPath (IO.Path.Combine (AppContext.BaseDirectory, "..", "..", "..")) + let sourceScriptsDir = IO.Path.Combine (sourceProjectDir, "TypeWrappersKindSafety") + let sourceReferencesPath = IO.Path.Combine (sourceScriptsDir, "References.fsx") + + let ensureFileContentAsync (path : string) (content : string) : Task = task { + if IO.File.Exists (path) then + let! existing = IO.File.ReadAllTextAsync (path) + + if not (String.Equals (existing, content, StringComparison.Ordinal)) then + do! IO.File.WriteAllTextAsync (path, content) + else + do! IO.File.WriteAllTextAsync (path, content) + } + + member _.ScriptPath (name : string) = IO.Path.Combine (scriptsDir, name) + + member _.RunFsiCheckAsync (scriptPath : string) : Task = task { + let psi = + ProcessStartInfo ( + "dotnet", + sprintf "fsi --noninteractive \"%s\"" scriptPath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = AppContext.BaseDirectory + ) + + use proc = Process.Start (psi) + do! proc.WaitForExitAsync () + return proc.ExitCode + } + + member private _.ReferencesContent = + let sharedAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Shared.dll") + let serverAssembly = IO.Path.Combine (AppContext.BaseDirectory, "FSharp.Data.GraphQL.Server.dll") + + [| sprintf "#r @\"%s\"" sharedAssembly; sprintf "#r @\"%s\"" serverAssembly |] + |> String.concat "\n" + + interface IAsyncLifetime with + + member this.InitializeAsync () : Task = + task { + IO.Directory.CreateDirectory (scriptsDir) |> ignore + IO.Directory.CreateDirectory (sourceScriptsDir) |> ignore + + let content = this.ReferencesContent + + do! ensureFileContentAsync referencesPath content + do! ensureFileContentAsync sourceReferencesPath content + } + + member _.DisposeAsync () = Task.CompletedTask + +type TypeWrappersKindSafetyTests (fixture : TypeWrappersKindSafetyFixture) = + interface IClassFixture + + [] + member _.``ListOf keeps input-output direction`` () : Task = task { + let inputList : InputDef = ListOf InputOnlyType + let outputList : OutputDef = ListOf OutputOnlyType + Assert.Equal ("[InputOnlyType!]!", inputList.ToString ()) + Assert.Equal ("[OutputOnlyType!]!", outputList.ToString ()) + } + + [] + member _.``Nullable keeps input-output direction`` () : Task = task { + let nullableInput : InputDef = Nullable InputOnlyType + let nullableOutput : OutputDef = Nullable OutputOnlyType + Assert.Equal ("InputOnlyType", nullableInput.ToString ()) + Assert.Equal ("OutputOnlyType", nullableOutput.ToString ()) + } + + [] + member _.``StructNullable keeps input-output direction`` () : Task = task { + let nullableInput : InputDef = StructNullable InputOnlyType + let nullableOutput : OutputDef = StructNullable OutputOnlyType + Assert.Equal ("InputOnlyType", nullableInput.ToString ()) + Assert.Equal ("OutputOnlyType", nullableOutput.ToString ()) + } + + [] + member _.``Valid script compiles successfully`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Valid.fsx")) + Assert.Equal (0, exitCode) + } + + [] + member _.``ListOf rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``ListOf rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("ListOf.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``Nullable rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``Nullable rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("Nullable.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``StructNullable rejects output type as input at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.OutputAsInput.fsx")) + Assert.NotEqual (0, exitCode) + } + + [] + member _.``StructNullable rejects input type as output at compile time`` () : Task = task { + let! exitCode = fixture.RunFsiCheckAsync (fixture.ScriptPath ("StructNullable.InputAsOutput.fsx")) + Assert.NotEqual (0, exitCode) + } diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs index 954ace7ec..1e7053574 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputComplexTests.fs @@ -46,7 +46,7 @@ type TestInput = optArr : string option array option voptArr : string option array voption } // string voption array voption is too hard to implement -let InputArrayOf (innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Val array> = ListOf innerDef +let InputArrayOf (innerDef : #InputDef<'Val>) : InputDef<'Val array> = ListOf (innerDef :> InputDef<'Val>) let TestInputObject = Define.InputObject ( diff --git a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs index 69fc2b825..de5730092 100644 --- a/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs +++ b/tests/FSharp.Data.GraphQL.Tests/Variables and Inputs/InputNestedTests.fs @@ -14,7 +14,7 @@ open FSharp.Data.GraphQL.Types open FSharp.Data.GraphQL.Parser open FSharp.Data.GraphQL.Shared -let InputArrayOf (innerDef : #TypeDef<'Val>) : ListOfDef<'Val, 'Val array> = ListOf innerDef +let InputArrayOf (innerDef : #InputDef<'Val>) : InputDef<'Val array> = ListOf (innerDef :> InputDef<'Val>) let TestInputObject = InputComplexTests.TestInputObject