diff --git a/Packages.props b/Packages.props
index 323f603f..182b72ce 100644
--- a/Packages.props
+++ b/Packages.props
@@ -17,6 +17,7 @@
+
diff --git a/README.md b/README.md
index 39fd14c8..0e7b4e3f 100644
--- a/README.md
+++ b/README.md
@@ -230,7 +230,7 @@ printfn "Custom data: %A\n" result.CustomData
// Errors:
-// Custom data: map [("documentId", 1221427401)]
+// Custom data: map [("documentId", "")]
```
For more information about how to use the client provider, see the [examples folder](samples/client-provider).
diff --git a/docs/execution-pipeline.md b/docs/execution-pipeline.md
index 8b3cab7b..26a948cf 100644
--- a/docs/execution-pipeline.md
+++ b/docs/execution-pipeline.md
@@ -52,8 +52,8 @@ The execution phase can be performed using one of the two strategies:
The result of a GraphQL query execution is a `GQLResponse` object with the following fields:
-- `documentId`: which is the hash code of the query's AST document - it can be used to implement execution plan caching (persistent queries).
+- `documentId`: deterministic SHA-256 hash (lowercase hex string) of the canonical query document – it can be used to implement execution plan caching (persistent queries).
- `data`: optional, a formatted GraphQL response matching the requested query (`KeyValuePair seq`). Absent in case of an error that does not allow continuing processing and returning any GraphQL results.
- `errors`: optional, contains a list of errors (`GQLProblemDetails`) that occurred during query execution.
-This result can then be serialized and returned to the client.
\ No newline at end of file
+This result can then be serialized and returned to the client.
diff --git a/samples/star-wars-fabulous-client/StarWars/Common.fs b/samples/star-wars-fabulous-client/StarWars/Common.fs
index 1de5ae4e..44dfe52a 100644
--- a/samples/star-wars-fabulous-client/StarWars/Common.fs
+++ b/samples/star-wars-fabulous-client/StarWars/Common.fs
@@ -9,6 +9,6 @@ module Commands =
let IntrospectionPath = "../../../tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json"
type GraphQLApi = GraphQLProvider
- let GetCharactersData = GraphQLApi.Operation<"queries/FetchCharacters.graphql">()
+ let GetCharactersData = GraphQLApi.Operation<"queries/FetchCharacters.graphql"> ()
type Character = GraphQLApi.Operations.FetchCharacters.Types.CharactersFields.Character
diff --git a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs
index 504207bb..984a8335 100644
--- a/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs
+++ b/src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs
@@ -799,7 +799,7 @@ module internal Provider =
match validationResult with
| ValidationError msgs -> failwith (formatValidationExceptionMessage msgs)
| Success -> ()
- let key = { DocumentId = queryAst.GetHashCode(); SchemaId = schema.GetHashCode() }
+ let key = { DocumentId = DocumentId.fromCanonicalQuery (queryAst.ToQueryString()); SchemaId = schema.GetHashCode() }
let refMaker = lazy Validation.Ast.validateDocument schema queryAst
if clientQueryValidation then
refMaker.Force
diff --git a/src/FSharp.Data.GraphQL.Server/Executor.fs b/src/FSharp.Data.GraphQL.Server/Executor.fs
index 85089c99..0167aa3d 100644
--- a/src/FSharp.Data.GraphQL.Server/Executor.fs
+++ b/src/FSharp.Data.GraphQL.Server/Executor.fs
@@ -1,5 +1,6 @@
namespace FSharp.Data.GraphQL
+open System
open System.Collections.Concurrent
open System.Collections.Immutable
open System.Runtime.InteropServices
@@ -9,6 +10,7 @@ open FsToolkit.ErrorHandling
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Execution
open FSharp.Data.GraphQL.Ast
+open FSharp.Data.GraphQL.Ast.Extensions
open FSharp.Data.GraphQL.Validation
open FSharp.Data.GraphQL.Parser
open FSharp.Data.GraphQL.Planning
@@ -99,6 +101,9 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
| Success -> ()
| ValidationError errors -> raise (GQLMessageException (System.String.Join("\n", errors)))
+ // Compute schema ID once after middleware has run and cache it for the lifetime of this Executor instance
+ let schemaId = schema.Introspected.GetHashCode()
+
let eval (executionPlan: ExecutionPlan, data: 'Root option, variables: ImmutableDictionary, getInputContext : InputExecutionContextProvider): Async =
let documentId = executionPlan.DocumentId
let prepareOutput res =
@@ -137,7 +142,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
eval (executionPlan, data, variables, getInputContext)
let createExecutionPlan (ast: Document, operationName: string option, meta : Metadata) =
- let documentId = ast.GetHashCode()
+ let documentId = DocumentId.fromCanonicalQueryUnsafe (ast.ToQueryString())
result {
match findOperation ast operationName with
| Some operation ->
@@ -159,7 +164,6 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
ErrorKind.Validation
)]
do!
- let schemaId = schema.Introspected.GetHashCode()
let key = { DocumentId = documentId; SchemaId = schemaId }
let producer = fun () -> Validation.Ast.validateDocument schema.Introspected ast
validationCache.GetOrAdd producer key
@@ -185,7 +189,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
/// Asynchronously executes a provided execution plan. In case of repetitive queries, execution plan may be preprocessed
/// and cached using `documentId` as an identifier.
/// Returned value is a readonly dictionary consisting of following top level entries:
- /// 'documentId' (unique identifier of current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
+ /// 'documentId' (unique identifier of the current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
/// 'data' (GraphQL response matching the structure provided in GraphQL query string), and
/// 'errors' (optional, contains a list of errors that occurred while executing a GraphQL operation).
///
@@ -198,7 +202,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
///
/// Asynchronously executes parsed GraphQL query AST. Returned value is a readonly dictionary consisting of following top level entries:
- /// 'documentId' (unique identifier of current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
+ /// 'documentId' (unique identifier of the current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
/// 'data' (GraphQL response matching the structure provided in GraphQL query string), and
/// 'errors' (optional, contains a list of errors that occurred while executing a GraphQL operation).
///
@@ -216,7 +220,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
///
/// Asynchronously executes unparsed GraphQL query AST. Returned value is a readonly dictionary consisting of following top level entries:
- /// 'documentId' (unique identifier of current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
+ /// 'documentId' (unique identifier of the current document's AST, it can be used as a key/identifier of ExecutionPlan as well),
/// 'data' (GraphQL response matching the structure provided in GraphQL query string), and
/// 'errors' (optional, contains a list of errors that occurred while executing a GraphQL operation).
///
diff --git a/src/FSharp.Data.GraphQL.Server/IO.fs b/src/FSharp.Data.GraphQL.Server/IO.fs
index 463d8c3a..238d5ad0 100644
--- a/src/FSharp.Data.GraphQL.Server/IO.fs
+++ b/src/FSharp.Data.GraphQL.Server/IO.fs
@@ -10,7 +10,7 @@ open FSharp.Data.GraphQL.Types
type Output = IDictionary
type GQLResponse =
- { DocumentId: int
+ { DocumentId: string
Data : Output Skippable
Errors : GQLProblemDetails list Skippable }
static member Direct(documentId, data, errors) =
@@ -27,7 +27,7 @@ type GQLResponse =
Errors = Include errors }
type GQLExecutionResult =
- { DocumentId: int
+ { DocumentId: string
Content : GQLResponseContent
Metadata : Metadata }
static member Direct(documentId, data, errors, meta) =
@@ -59,7 +59,7 @@ type GQLExecutionResult =
static member Error(documentId, msg, meta) =
GQLExecutionResult.RequestError(documentId, [ GQLProblemDetails.Create msg ], meta)
- static member ErrorFromException(documentId : int, ex : Exception, meta : Metadata) =
+ static member ErrorFromException(documentId : string, ex : Exception, meta : Metadata) =
GQLExecutionResult.RequestError(documentId, [ GQLProblemDetails.Create (ex.Message, ex) ], meta)
static member Invalid(documentId, errors, meta) =
diff --git a/src/FSharp.Data.GraphQL.Shared/AstExtensions.fs b/src/FSharp.Data.GraphQL.Shared/AstExtensions.fs
index dde73940..34ea5ce7 100644
--- a/src/FSharp.Data.GraphQL.Shared/AstExtensions.fs
+++ b/src/FSharp.Data.GraphQL.Shared/AstExtensions.fs
@@ -36,13 +36,16 @@ and internal AstSelectionInfo = {
} with
member x.AliasOrName = x.Alias |> ValueOption.defaultValue x.Name
- static member Create (typeCondition : string voption, path : FieldPath, name : string, alias : string voption, [] fields : AstSelectionInfo list) = {
- TypeCondition = typeCondition
- Name = name
- Alias = alias
- Path = path
- Fields = if obj.ReferenceEquals (fields, null) then [] else fields
- }
+ static member Create
+ (typeCondition : string voption, path : FieldPath, name : string, alias : string voption, [] fields : AstSelectionInfo list)
+ =
+ {
+ TypeCondition = typeCondition
+ Name = name
+ Alias = alias
+ Path = path
+ Fields = if obj.ReferenceEquals (fields, null) then [] else fields
+ }
member x.SetFields (fields : AstSelectionInfo list) = x.Fields <- fields
and AstFieldInfo =
@@ -102,9 +105,30 @@ type Document with
/// Generates a GraphQL query string from this document.
///
/// Specify custom printing voptions for the query string.
- member x.ToQueryString ([] options : QueryStringPrintingOptions) =
+ member x.ToQueryString ([] options : QueryStringPrintingOptions) =
let sb = PaddedStringBuilder ()
- let withQuotes (s : string) = "\"" + s + "\""
+ let escapeGraphQLString (s : string) =
+ let escaped = StringBuilder (s.Length + s.Length / 4 + 2)
+ escaped.Append ('"') |> ignore
+ for c in s do
+ let appendStr =
+ match c with
+ | '"' -> "\\\""
+ | '\\' -> "\\\\"
+ | '\b' -> "\\b"
+ | '\f' -> "\\f"
+ | '\n' -> "\\n"
+ | '\r' -> "\\r"
+ | '\t' -> "\\t"
+ | '\u2028' -> "\\u2028"
+ | '\u2029' -> "\\u2029"
+ | c when c < '\u0020' ->
+ let hex = (int c).ToString ("x4", CultureInfo.InvariantCulture)
+ "\\u" + hex
+ | c -> string c
+ escaped.Append (appendStr) |> ignore
+ escaped.Append('"').ToString ()
+ let withQuotes = escapeGraphQLString
let rec printValue x =
let printObjectValue (name, value) =
sb.Append (name)
diff --git a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj
index a116e8ea..e0e6fcf3 100644
--- a/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj
+++ b/src/FSharp.Data.GraphQL.Shared/FSharp.Data.GraphQL.Shared.fsproj
@@ -49,6 +49,7 @@
+
diff --git a/src/FSharp.Data.GraphQL.Shared/Helpers/DocumentId.fs b/src/FSharp.Data.GraphQL.Shared/Helpers/DocumentId.fs
new file mode 100644
index 00000000..e80876b7
--- /dev/null
+++ b/src/FSharp.Data.GraphQL.Shared/Helpers/DocumentId.fs
@@ -0,0 +1,44 @@
+module FSharp.Data.GraphQL.DocumentId
+
+open System.Globalization
+open System.Runtime.CompilerServices
+open System.Security.Cryptography
+open System.Text
+
+let private formatByteAsLowerHex (value : byte) = value.ToString ("x2", CultureInfo.InvariantCulture)
+
+let internal fromCanonicalQueryUnsafe (canonicalQuery : string) =
+ let queryBytes = Encoding.UTF8.GetBytes canonicalQuery
+ use sha256 = SHA256.Create ()
+ let hash = sha256.ComputeHash queryBytes
+ hash |> Seq.map formatByteAsLowerHex |> String.concat ""
+
+
+///
+/// Computes a deterministic document identifier from a canonical GraphQL query string.
+///
+/// The canonical GraphQL query string (must already be properly escaped according to GraphQL specification).
+/// A lowercase hexadecimal SHA-256 hash string that uniquely identifies the document content.
+[]
+let fromCanonicalQuery (canonicalQuery : string) =
+ let normalizedCanonicalQuery =
+ let crIndex = canonicalQuery.IndexOf '\r'
+ if crIndex < 0 then
+ canonicalQuery
+ else
+ let sb = StringBuilder (canonicalQuery.Length)
+ sb.Append (canonicalQuery, 0, crIndex) |> ignore
+ let mutable i = crIndex
+ while i < canonicalQuery.Length do
+ let c = canonicalQuery[i]
+ if c = '\r' then
+ sb.Append '\n' |> ignore
+ if i + 1 < canonicalQuery.Length && canonicalQuery[i + 1] = '\n' then
+ i <- i + 2
+ else
+ i <- i + 1
+ else
+ sb.Append c |> ignore
+ i <- i + 1
+ sb.ToString ()
+ fromCanonicalQueryUnsafe normalizedCanonicalQuery
diff --git a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs
index be8e79b5..8c2f54b9 100644
--- a/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs
+++ b/src/FSharp.Data.GraphQL.Shared/TypeSystem.fs
@@ -671,7 +671,8 @@ and PlanningContext = {
RootDef : ObjectDef
Document : Document
Operation : OperationDefinition
- DocumentId : int
+ /// Unique identifier of the current document's AST.
+ DocumentId : string
Metadata : Metadata
}
@@ -887,8 +888,8 @@ and SchemaCompileContext = {
/// A planning of an execution phase.
/// It is used by the execution process to execute an operation.
and ExecutionPlan = {
- /// Unique identifier of the current execution plan.
- DocumentId : int
+ /// Unique identifier of the current document's AST.
+ DocumentId : string
/// AST definition of current operation.
Operation : OperationDefinition
/// Definition of the root type (either query or mutation) used by the
diff --git a/src/FSharp.Data.GraphQL.Shared/ValidationResultCache.fs b/src/FSharp.Data.GraphQL.Shared/ValidationResultCache.fs
index e1690260..1d2f12dd 100644
--- a/src/FSharp.Data.GraphQL.Shared/ValidationResultCache.fs
+++ b/src/FSharp.Data.GraphQL.Shared/ValidationResultCache.fs
@@ -4,7 +4,7 @@ open FSharp.Data.GraphQL
open System
type ValidationResultKey =
- { DocumentId : int
+ { DocumentId : string
SchemaId : int }
type ValidationResultProducer =
@@ -13,12 +13,10 @@ type ValidationResultProducer =
type IValidationResultCache =
abstract GetOrAdd : ValidationResultProducer -> ValidationResultKey -> ValidationResult
-
/// An in-memory cache for the results of schema/document validations, with a lifetime of 30 seconds.
type MemoryValidationResultCache () =
let expirationPolicy = CacheExpirationPolicy.SlidingExpiration(TimeSpan.FromSeconds 30.0)
- let internalCache = MemoryCache>(expirationPolicy)
+ let internalCache = MemoryCache>(expirationPolicy)
interface IValidationResultCache with
member _.GetOrAdd producer key =
- let internalKey = key.GetHashCode()
- internalCache.GetOrAddResult internalKey producer
+ internalCache.GetOrAddResult key producer
diff --git a/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json
index ad448c42..4afcbf26 100644
--- a/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json
+++ b/tests/FSharp.Data.GraphQL.IntegrationTests/integration-introspection.json
@@ -1,5 +1,5 @@
{
- "documentId": 986164407,
+ "documentId": "547d9dc982284840b3e020dfcbf43ae96cef7595afa007145ed954794363d148",
"data": {
"__schema": {
"queryType": {
@@ -1926,4 +1926,4 @@
]
}
}
-}
\ 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 a961111f..b990b61c 100644
--- a/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json
+++ b/tests/FSharp.Data.GraphQL.IntegrationTests/introspection.json
@@ -1,5 +1,5 @@
{
- "documentId": 195530235,
+ "documentId": "547d9dc982284840b3e020dfcbf43ae96cef7595afa007145ed954794363d148",
"data": {
"__schema": {
"queryType": {
@@ -1862,4 +1862,4 @@
]
}
}
-}
\ No newline at end of file
+}
diff --git a/tests/FSharp.Data.GraphQL.Tests/AstExtensionsTests.fs b/tests/FSharp.Data.GraphQL.Tests/AstExtensionsTests.fs
index 0b4e8e37..77411caa 100644
--- a/tests/FSharp.Data.GraphQL.Tests/AstExtensionsTests.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/AstExtensionsTests.fs
@@ -4,6 +4,7 @@
module FSharp.Data.GraphQL.Tests.AstExtensionsTests
open Xunit
+open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Parser
open FSharp.Data.GraphQL.Ast.Extensions
@@ -356,3 +357,141 @@ let ``Should generate information map correctly`` () =
]
actual |> equals expected
+
+[]
+let ``ToQueryString escapes double quotes in string values`` () =
+ let query = """query q { hero(name: "test\"quote") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped quote
+ Assert.Contains ("\\\"", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes backslashes in string values`` () =
+ let query = """query q { hero(path: "C:\\Users\\test") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains escaped backslashes
+ Assert.Contains ("\\\\", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes newlines in string values`` () =
+ let query = """query q { hero(text: "line1\nline2") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped newline within the string value
+ Assert.Contains ("\\n", printed)
+ // Verify the string value itself doesn't contain an actual newline (it should be escaped)
+ // The printed output will have formatting newlines, but the string value should have \n
+ Assert.Contains ("\"line1\\nline2\"", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes tabs in string values`` () =
+ let query = """query q { hero(text: "col1\tcol2") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped tab within the string value
+ Assert.Contains ("\\t", printed)
+ Assert.Contains ("\"col1\\tcol2\"", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes carriage returns in string values`` () =
+ let query = """query q { hero(text: "line1\rline2") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped carriage return
+ Assert.Contains ("\\r", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes backspace in string values`` () =
+ let query = """query q { hero(text: "test\bback") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped backspace
+ Assert.Contains ("\\b", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes form feed in string values`` () =
+ let query = """query q { hero(text: "page1\fpage2") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the escaped form feed
+ Assert.Contains ("\\f", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes control characters as unicode in string values`` () =
+ // Test with a control character (e.g., ASCII 0x01)
+ let query = "query q { hero(text: \"test\u0001control\") }"
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify the printed query contains the unicode escape (lowercase hex)
+ Assert.Contains ("\\u0001", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes unicode line separator in string values`` () =
+ let query = """query q { hero(text: "\u2028") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ Assert.Contains ("\\u2028", printed)
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes unicode paragraph separator in string values`` () =
+ let query = """query q { hero(text: "\u2029") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ Assert.Contains ("\\u2029", printed)
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString escapes multiple special characters correctly`` () =
+ let query = """query q { hero(text: "quote:\"newline:\nslash:\\tab:\t") }"""
+ let document = parse query
+ let printed = document.ToQueryString ()
+ // Verify all escapes are present
+ Assert.Contains ("\\\"", printed)
+ Assert.Contains ("\\n", printed)
+ Assert.Contains ("\\\\", printed)
+ Assert.Contains ("\\t", printed)
+ // Verify it can be parsed back
+ let reparsed = parse printed
+ equals (document.ToQueryString ()) (reparsed.ToQueryString ())
+
+[]
+let ``ToQueryString produces deterministic output for escaped strings`` () =
+ // This test verifies that the same query with escaped strings produces
+ // the same canonical output, which is critical for documentId stability
+ let query = """query Test { field(arg: "test\"quote\nline\ttab\\back") }"""
+ let document = parse query
+ let printed1 = document.ToQueryString ()
+ let printed2 = document.ToQueryString ()
+ equals printed1 printed2
+ // Verify the documentId is deterministic
+ let documentId = DocumentId.fromCanonicalQuery printed1
+ equals 64 documentId.Length // SHA-256 hex string is always 64 chars
diff --git a/tests/FSharp.Data.GraphQL.Tests/DocumentIdTests.fs b/tests/FSharp.Data.GraphQL.Tests/DocumentIdTests.fs
new file mode 100644
index 00000000..b8a86610
--- /dev/null
+++ b/tests/FSharp.Data.GraphQL.Tests/DocumentIdTests.fs
@@ -0,0 +1,91 @@
+// The MIT License (MIT)
+// Copyright (c) 2016 Bazinga Technologies Inc
+
+module FSharp.Data.GraphQL.Tests.DocumentIdTests
+
+open Xunit
+open FSharp.Data.GraphQL
+
+[]
+let ``DocumentId.fromCanonicalQueryUnsafe produces deterministic hash`` () =
+ let query = "query Example { a b }"
+ let hash1 = DocumentId.fromCanonicalQueryUnsafe query
+ let hash2 = DocumentId.fromCanonicalQueryUnsafe query
+ equals hash1 hash2
+ equals 64 hash1.Length // SHA-256 hex string is 64 chars
+
+[]
+let ``DocumentId.fromCanonicalQueryUnsafe produces different hashes for different queries`` () =
+ let query1 = "query Example1 { a }"
+ let query2 = "query Example2 { b }"
+ let hash1 = DocumentId.fromCanonicalQueryUnsafe query1
+ let hash2 = DocumentId.fromCanonicalQueryUnsafe query2
+ notEquals hash1 hash2
+
+[]
+let ``DocumentId.fromCanonicalQueryUnsafe handles empty string`` () =
+ let query = ""
+ let hash = DocumentId.fromCanonicalQueryUnsafe query
+ equals 64 hash.Length
+ // SHA-256 of empty string
+ equals "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" hash
+
+[]
+let ``DocumentId.fromCanonicalQuery produces deterministic hash`` () =
+ let query = "query Example { a b }"
+ let hash1 = DocumentId.fromCanonicalQuery query
+ let hash2 = DocumentId.fromCanonicalQuery query
+ equals hash1 hash2
+ equals 64 hash1.Length // SHA-256 hex string is 64 chars
+
+[]
+let ``DocumentId.fromCanonicalQuery produces different hashes for different queries`` () =
+ let query1 = "query Example1 { a }"
+ let query2 = "query Example2 { b }"
+ let hash1 = DocumentId.fromCanonicalQuery query1
+ let hash2 = DocumentId.fromCanonicalQuery query2
+ notEquals hash1 hash2
+
+[]
+let ``DocumentId.fromCanonicalQuery handles empty string`` () =
+ let query = ""
+ let hash = DocumentId.fromCanonicalQuery query
+ equals 64 hash.Length
+ // SHA-256 of empty string
+ equals "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" hash
+
+[]
+let ``DocumentId.fromCanonicalQuery produces lowercase hex`` () =
+ let query = "query Test { field }"
+ let hash = DocumentId.fromCanonicalQuery query
+ equals hash (hash.ToLowerInvariant ())
+ Assert.True (
+ hash
+ |> Seq.forall (fun c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
+ )
+
+[]
+let ``DocumentId.fromCanonicalQuery handles special characters in strings`` () =
+ // Test with escaped characters that should be in the canonical form
+ let query1 = """query Test { field(arg: "test\"quote") }"""
+ let query2 = """query Test { field(arg: "test\nline") }"""
+ let query3 = """query Test { field(arg: "test\ttab") }"""
+ let hash1 = DocumentId.fromCanonicalQuery query1
+ let hash2 = DocumentId.fromCanonicalQuery query2
+ let hash3 = DocumentId.fromCanonicalQuery query3
+ // All should produce valid hashes
+ equals 64 hash1.Length
+ equals 64 hash2.Length
+ equals 64 hash3.Length
+ // All should be different
+ notEquals hash1 hash2
+ notEquals hash2 hash3
+ notEquals hash1 hash3
+
+[]
+let ``DocumentId.fromCanonicalQuery is consistent with known SHA-256 values`` () =
+ // Test a simple known case
+ let query = "test"
+ let hash = DocumentId.fromCanonicalQuery query
+ // SHA-256 of "test"
+ equals "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" hash
diff --git a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs
index 07bc5827..aae97f40 100644
--- a/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/ExecutionTests.fs
@@ -5,6 +5,7 @@ module FSharp.Data.GraphQL.Tests.ExecutionTests
open Xunit
open System
+open System.Threading.Tasks
open System.Text.Json
open System.Text.Json.Serialization
open System.Collections.Immutable
@@ -19,23 +20,23 @@ open FSharp.Data.GraphQL.Parser
open FSharp.Data.GraphQL.Execution
type TestSubject = {
- a: string
- b: string
- c: string
- d: string
- e: string
- f: string
- deep: DeepTestSubject
- pic: int voption -> string
- promise: Async
+ a : string
+ b : string
+ c : string
+ d : string
+ e : string
+ f : string
+ deep : DeepTestSubject
+ pic : int voption -> string
+ promise : Async
}
and DeepTestSubject = {
- a: string
- b: string
- c: string option
- d: string voption
- l: string option list
+ a : string
+ b : string
+ c : string option
+ d : string voption
+ l : string option list
}
and DUArg =
@@ -47,29 +48,32 @@ and EnumArg =
| Enum2 = 2
[]
-let ``Execution handles basic tasks: executes arbitrary code`` () =
- let rec data =
- {
- a = "Apple"
- b = "Banana"
- c = "Cookie"
- d = "Donut"
- e = "Egg"
- f = "Fish"
- pic = (fun size -> "Pic of size: " + (if size.IsSome then size.Value else 50).ToString())
- promise = async { return data }
- deep = deep
- }
- and deep =
- {
- a = "Already Been Done"
- b = "Boring"
- c = Some "Contrived"
- d = ValueSome "Donut"
- l = [Some "Contrived"; None; Some "Confusing"]
- }
-
- let ast = parse """query Example($size: Int) {
+let ``Execution handles basic tasks: executes arbitrary code`` () : Task =
+ let rec data = {
+ a = "Apple"
+ b = "Banana"
+ c = "Cookie"
+ d = "Donut"
+ e = "Egg"
+ f = "Fish"
+ pic =
+ (fun size ->
+ "Pic of size: "
+ + (if size.IsSome then size.Value else 50).ToString ())
+ promise = async { return data }
+ deep = deep
+ }
+ and deep = {
+ a = "Already Been Done"
+ b = "Boring"
+ c = Some "Contrived"
+ d = ValueSome "Donut"
+ l = [ Some "Contrived"; None; Some "Confusing" ]
+ }
+
+ let ast =
+ parse
+ """query Example($size: Int) {
a,
b,
x: c
@@ -105,54 +109,72 @@ let ``Execution handles basic tasks: executes arbitrary code`` () =
"f", upcast "Fish"
"pic", upcast "Pic of size: 100"
"promise", upcast NameValueLookup.ofList [ "a", upcast "Apple" ]
- "deep", upcast NameValueLookup.ofList [
- "a", "Already Been Done" :> obj
- "b", upcast "Boring"
- "c", upcast "Contrived"
- "d", upcast "Donut"
- "l", upcast ["Contrived" :> obj; null; upcast "Confusing"]
- ]
+ "deep",
+ upcast
+ NameValueLookup.ofList [
+ "a", "Already Been Done" :> obj
+ "b", upcast "Boring"
+ "c", upcast "Contrived"
+ "d", upcast "Donut"
+ "l", upcast [ "Contrived" :> obj; null; upcast "Confusing" ]
+ ]
]
let DeepDataType =
- Define.Object(
- "DeepDataType", [
- Define.Field("a", StringType, (fun _ dt -> dt.a))
- Define.Field("b", StringType, (fun _ dt -> dt.b))
- Define.Field("c", Nullable StringType, (fun _ dt -> dt.c))
- Define.Field("d", StructNullable StringType, (fun _ dt -> dt.d))
- Define.Field("l", (ListOf (Nullable StringType)), (fun _ dt -> dt.l))
- ])
+ Define.Object (
+ "DeepDataType",
+ [
+ Define.Field ("a", StringType, (fun _ dt -> dt.a))
+ Define.Field ("b", StringType, (fun _ dt -> dt.b))
+ Define.Field ("c", Nullable StringType, (fun _ dt -> dt.c))
+ Define.Field ("d", StructNullable StringType, (fun _ dt -> dt.d))
+ Define.Field ("l", (ListOf (Nullable StringType)), (fun _ dt -> dt.l))
+ ]
+ )
let rec DataType =
- DefineRec.Object(
- "DataType",
- fieldsFn = fun () ->
- [
- Define.Field("a", StringType, resolve = fun _ dt -> dt.a)
- Define.Field("b", StringType, resolve = fun _ dt -> dt.b)
- Define.Field("c", StringType, resolve = fun _ dt -> dt.c)
- Define.Field("d", StringType, resolve = fun _ dt -> dt.d)
- Define.Field("e", StringType, fun _ dt -> dt.e)
- Define.Field("f", StringType, fun _ dt -> dt.f)
- Define.Field("pic", StringType, "Picture resizer", [ Define.Input("size", Nullable IntType) ], fun ctx dt -> dt.pic(ctx.TryArg("size")))
- Define.AsyncField("promise", DataType, fun _ dt -> dt.promise)
- Define.Field("deep", DeepDataType, fun _ dt -> dt.deep)
- ])
-
- let schema = Schema(DataType)
- let schemaProcessor = Executor(schema)
- let params' = JsonDocument.Parse("""{"size":100}""").RootElement.Deserialize>(serializerOptions)
- let result = sync <| schemaProcessor.AsyncExecute(ast, getMockInputContext, data, variables = params', operationName = "Example")
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast expected)
-
-type TestThing = { mutable Thing: string }
+ DefineRec.Object (
+ "DataType",
+ fieldsFn =
+ fun () -> [
+ Define.Field ("a", StringType, resolve = (fun _ dt -> dt.a))
+ Define.Field ("b", StringType, resolve = (fun _ dt -> dt.b))
+ Define.Field ("c", StringType, resolve = (fun _ dt -> dt.c))
+ Define.Field ("d", StringType, resolve = (fun _ dt -> dt.d))
+ Define.Field ("e", StringType, fun _ dt -> dt.e)
+ Define.Field ("f", StringType, fun _ dt -> dt.f)
+ Define.Field (
+ "pic",
+ StringType,
+ "Picture resizer",
+ [ Define.Input ("size", Nullable IntType) ],
+ fun ctx dt -> dt.pic (ctx.TryArg ("size"))
+ )
+ Define.AsyncField ("promise", DataType, fun _ dt -> dt.promise)
+ Define.Field ("deep", DeepDataType, fun _ dt -> dt.deep)
+ ]
+ )
+
+ let schema = Schema (DataType)
+ let schemaProcessor = Executor (schema)
+ let params' =
+ JsonDocument.Parse("""{"size":100}""").RootElement.Deserialize> (serializerOptions)
+
+ task {
+ let! result = schemaProcessor.AsyncExecute (ast, getMockInputContext, data, variables = params', operationName = "Example")
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data |> equals (upcast expected)
+ }
+
+type TestThing = { mutable Thing : string }
[]
-let ``Execution handles basic tasks: merges parallel fragments`` () =
- let ast = parse """{ a, ...FragOne, ...FragTwo }
+let ``Execution handles basic tasks: merges parallel fragments`` () : Task =
+ let ast =
+ parse
+ """{ a, ...FragOne, ...FragTwo }
fragment FragOne on Type {
b
@@ -165,441 +187,532 @@ let ``Execution handles basic tasks: merges parallel fragments`` () =
}"""
let rec Type =
- DefineRec.Object(
- name = "Type",
- fieldsFn = fun () ->
- [
- Define.Field("a", StringType, fun _ _ -> "Apple")
- Define.Field("b", StringType, fun _ _ -> "Banana")
- Define.Field("c", StringType, fun _ _ -> "Cherry")
- Define.Field("deep", Type, fun _ v -> v)
- ])
-
- let schema = Schema(Type)
- let schemaProcessor = Executor(schema)
+ DefineRec.Object (
+ name = "Type",
+ fieldsFn =
+ fun () -> [
+ Define.Field ("a", StringType, fun _ _ -> "Apple")
+ Define.Field ("b", StringType, fun _ _ -> "Banana")
+ Define.Field ("c", StringType, fun _ _ -> "Cherry")
+ Define.Field ("deep", Type, fun _ v -> v)
+ ]
+ )
+
+ let schema = Schema (Type)
+ let schemaProcessor = Executor (schema)
let expected =
NameValueLookup.ofList [
"a", upcast "Apple"
"b", upcast "Banana"
- "deep", upcast NameValueLookup.ofList [
- "b", upcast "Banana"
- "deeper", upcast NameValueLookup.ofList [
- "b", "Banana" :> obj
+ "deep",
+ upcast
+ NameValueLookup.ofList [
+ "b", upcast "Banana"
+ "deeper", upcast NameValueLookup.ofList [ "b", "Banana" :> obj; "c", upcast "Cherry" ]
"c", upcast "Cherry"
]
- "c", upcast "Cherry"
- ]
"c", upcast "Cherry"
]
- let result = sync <| schemaProcessor.AsyncExecute(ast, getMockInputContext, obj())
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast expected)
+ task {
+ let! result = schemaProcessor.AsyncExecute (ast, getMockInputContext, obj ())
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data |> equals (upcast expected)
+ }
[]
-let ``Execution handles basic tasks: threads root value context correctly`` () =
+let ``Execution handles basic tasks: threads root value context correctly`` () : Task =
let query = "query Example { a }"
let data = { Thing = "" }
- let Thing = Define.Object("Type", [ Define.Field("a", StringType, fun _ value -> value.Thing <- "thing"; value.Thing) ])
- let result = sync <| Executor(Schema(Thing)).AsyncExecute(parse query, getMockInputContext, data)
- ensureDirect result <| fun _ errors -> empty errors
- equals "thing" data.Thing
+ let Thing =
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "a",
+ StringType,
+ fun _ value ->
+ value.Thing <- "thing"
+ value.Thing
+ )
+ ]
+ )
+ task {
+ let! result = Executor(Schema (Thing)).AsyncExecute (parse query, getMockInputContext, data)
+ ensureDirect result <| fun _ errors -> empty errors
+ equals "thing" data.Thing
+ }
-type TestTarget =
- { mutable Num: int voption
- mutable Str: string voption }
+type TestTarget = { mutable Num : int voption; mutable Str : string voption }
[]
-let ``Execution handles basic tasks: correctly threads arguments`` () =
- let query = """query Example {
+let ``Execution handles basic tasks: correctly threads arguments`` () : Task =
+ let query =
+ """query Example {
b(numArg: 123, stringArg: "foo")
}"""
let data = { Num = ValueNone; Str = ValueNone }
let Type =
- Define.Object("Type",
- [ Define.Field("b", StructNullable StringType, "", [ Define.Input("numArg", IntType); Define.Input("stringArg", StringType) ],
- fun ctx value ->
- value.Num <- ctx.TryArg("numArg")
- value.Str <- ctx.TryArg("stringArg")
- value.Str) ])
-
- let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, getMockInputContext,data)
- ensureDirect result <| fun _ errors -> empty errors
- equals (ValueSome 123) data.Num
- equals (ValueSome "foo") data.Str
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "b",
+ StructNullable StringType,
+ "",
+ [ Define.Input ("numArg", IntType); Define.Input ("stringArg", StringType) ],
+ fun ctx value ->
+ value.Num <- ctx.TryArg ("numArg")
+ value.Str <- ctx.TryArg ("stringArg")
+ value.Str
+ )
+ ]
+ )
+ task {
+ let! result = Executor(Schema (Type)).AsyncExecute (parse query, getMockInputContext, data)
+ ensureDirect result <| fun _ errors -> empty errors
+ equals (ValueSome 123) data.Num
+ equals (ValueSome "foo") data.Str
+ }
[]
-let ``Execution handles basic tasks: correctly handles null arguments`` () =
- let query = """query Example {
+let ``Execution handles basic tasks: correctly handles null arguments`` () : Task =
+ let query =
+ """query Example {
b(numArg: null, stringArg: null)
}"""
let data = { Num = ValueNone; Str = ValueNone }
let Type =
- Define.Object("Type",
- [ Define.Field("b", StructNullable StringType, "", [ Define.Input("numArg", Nullable IntType); Define.Input("stringArg", Nullable StringType) ],
- fun ctx value ->
- value.Num <- ctx.TryArg("numArg")
- value.Str <- ctx.TryArg("stringArg")
- value.Str) ])
-
- let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, getMockInputContext, data)
- ensureDirect result <| fun _ errors -> empty errors
- equals ValueNone data.Num
- equals ValueNone data.Str
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "b",
+ StructNullable StringType,
+ "",
+ [ Define.Input ("numArg", Nullable IntType); Define.Input ("stringArg", Nullable StringType) ],
+ fun ctx value ->
+ value.Num <- ctx.TryArg ("numArg")
+ value.Str <- ctx.TryArg ("stringArg")
+ value.Str
+ )
+ ]
+ )
+ task {
+ let! result = Executor(Schema (Type)).AsyncExecute (parse query, getMockInputContext, data)
+ ensureDirect result <| fun _ errors -> empty errors
+ equals ValueNone data.Num
+ equals ValueNone data.Str
+ }
-type InlineTest = { A: string }
+type InlineTest = { A : string }
[]
-let ``Execution handles basic tasks: correctly handles discriminated union arguments`` () =
- let query = """query Example {
+let ``Execution handles basic tasks: correctly handles discriminated union arguments`` () : Task =
+ let query =
+ """query Example {
b(enumArg: Case1)
}"""
let EnumType =
- Define.Enum(
+ Define.Enum (
name = "EnumArg",
- options =
- [ Define.EnumValue("Case1", DUArg.Case1, "Case 1")
- Define.EnumValue("Case2", DUArg.Case2, "Case 2") ])
+ options = [
+ Define.EnumValue ("Case1", DUArg.Case1, "Case 1")
+ Define.EnumValue ("Case2", DUArg.Case2, "Case 2")
+ ]
+ )
let data = { Num = ValueNone; Str = ValueNone }
let Type =
- Define.Object("Type",
- [ Define.Field("b", StructNullable StringType, "", [ Define.Input("enumArg", EnumType) ],
- fun ctx value ->
- let arg = ctx.TryArg("enumArg")
- match arg with
- | ValueSome (Case1) ->
- value.Str <- ValueSome "foo"
- value.Num <- ValueSome 123
- value.Str
- | _ -> ValueNone) ])
- let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, getMockInputContext, data)
- ensureDirect result <| fun _ errors -> empty errors
- equals (ValueSome 123) data.Num
- equals (ValueSome "foo") data.Str
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "b",
+ StructNullable StringType,
+ "",
+ [ Define.Input ("enumArg", EnumType) ],
+ fun ctx value ->
+ let arg = ctx.TryArg ("enumArg")
+ match arg with
+ | ValueSome (Case1) ->
+ value.Str <- ValueSome "foo"
+ value.Num <- ValueSome 123
+ value.Str
+ | _ -> ValueNone
+ )
+ ]
+ )
+ task {
+ let! result = Executor(Schema (Type)).AsyncExecute (parse query, getMockInputContext, data)
+ ensureDirect result <| fun _ errors -> empty errors
+ equals (ValueSome 123) data.Num
+ equals (ValueSome "foo") data.Str
+ }
[]
-let ``Execution handles basic tasks: correctly handles Enum arguments`` () =
- let query = """query Example {
+let ``Execution handles basic tasks: correctly handles Enum arguments`` () : Task =
+ let query =
+ """query Example {
b(enumArg: Enum1)
}"""
let EnumType =
- Define.Enum(
+ Define.Enum (
name = "EnumArg",
- options =
- [ Define.EnumValue("Enum1", EnumArg.Enum1, "Enum 1")
- Define.EnumValue("Enum2", EnumArg.Enum2, "Enum 2") ])
+ options = [
+ Define.EnumValue ("Enum1", EnumArg.Enum1, "Enum 1")
+ Define.EnumValue ("Enum2", EnumArg.Enum2, "Enum 2")
+ ]
+ )
let data = { Num = ValueNone; Str = ValueNone }
let Type =
- Define.Object("Type",
- [ Define.Field("b", StructNullable StringType, "", [ Define.Input("enumArg", EnumType) ],
- fun ctx value ->
- let arg = ctx.TryArg("enumArg")
- match arg with
- | ValueSome _ ->
- value.Str <- ValueSome "foo"
- value.Num <- ValueSome 123
- value.Str
- | _ -> ValueNone) ])
- let result = sync <| Executor(Schema(Type)).AsyncExecute(parse query, getMockInputContext, data)
- ensureDirect result <| fun _ errors -> empty errors
- equals (ValueSome 123) data.Num
- equals (ValueSome "foo") data.Str
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "b",
+ StructNullable StringType,
+ "",
+ [ Define.Input ("enumArg", EnumType) ],
+ fun ctx value ->
+ let arg = ctx.TryArg ("enumArg")
+ match arg with
+ | ValueSome _ ->
+ value.Str <- ValueSome "foo"
+ value.Num <- ValueSome 123
+ value.Str
+ | _ -> ValueNone
+ )
+ ]
+ )
+ task {
+ let! result = Executor(Schema (Type)).AsyncExecute (parse query, getMockInputContext, data)
+ ensureDirect result <| fun _ errors -> empty errors
+ equals (ValueSome 123) data.Num
+ equals (ValueSome "foo") data.Str
+ }
[]
-let ``Execution handles basic tasks: uses the inline operation if no operation name is provided`` () =
+let ``Execution handles basic tasks: uses the inline operation if no operation name is provided`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ x -> x.A)
- ]))
- let result = sync <| Executor(schema).AsyncExecute(parse "{ a }", getMockInputContext, { A = "b" })
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast NameValueLookup.ofList ["a", "b" :> obj])
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A) ]))
+ task {
+ let! result = Executor(schema).AsyncExecute (parse "{ a }", getMockInputContext, { A = "b" })
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data
+ |> equals (upcast NameValueLookup.ofList [ "a", "b" :> obj ])
+ }
[]
-let ``Execution handles basic tasks: uses the only operation if no operation name is provided`` () =
+let ``Execution handles basic tasks: uses the only operation if no operation name is provided`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ x -> x.A)
- ]))
- let result = sync <| Executor(schema).AsyncExecute(parse "query Example { a }", getMockInputContext, { A = "b" })
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast NameValueLookup.ofList ["a", "b" :> obj])
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A) ]))
+ task {
+ let! result = Executor(schema).AsyncExecute (parse "query Example { a }", getMockInputContext, { A = "b" })
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data
+ |> equals (upcast NameValueLookup.ofList [ "a", "b" :> obj ])
+ }
[]
-let ``Execution handles basic tasks: uses the named operation if operation name is provided`` () =
+let ``Execution handles basic tasks: uses the named operation if operation name is provided`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ x -> x.A)
- ]))
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A) ]))
let query = "query Example { first: a } query OtherExample { second: a }"
- let result = sync <| Executor(schema).AsyncExecute(parse query, getMockInputContext, { A = "b" }, operationName = "OtherExample")
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast NameValueLookup.ofList ["second", "b" :> obj])
+ task {
+ let! result = Executor(schema).AsyncExecute (parse query, getMockInputContext, { A = "b" }, operationName = "OtherExample")
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data
+ |> equals (upcast NameValueLookup.ofList [ "second", "b" :> obj ])
+ }
[]
-let ``Execution handles basic tasks: list of scalars`` () =
+let ``Execution handles basic tasks: list of scalars`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("strings", ListOf StringType, fun _ _ -> ["foo"; "bar"; "baz"])
- ]))
- let result = sync <| Executor(schema).AsyncExecute("query Example { strings }", getMockInputContext)
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast NameValueLookup.ofList ["strings", box [ box "foo"; upcast "bar"; upcast "baz" ]])
+ Schema (Define.Object ("Type", [ Define.Field ("strings", ListOf StringType, fun _ _ -> [ "foo"; "bar"; "baz" ]) ]))
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { strings }", getMockInputContext)
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data
+ |> equals (upcast NameValueLookup.ofList [ "strings", box [ box "foo"; upcast "bar"; upcast "baz" ] ])
+ }
type TwiceTest = { A : string; B : int }
[]
-let ``Execution when querying the same field twice will return it`` () =
+let ``Execution when querying the same field twice will return it`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ x -> x.A)
- Define.Field("b", IntType, fun _ x -> x.B)
- ]))
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A); Define.Field ("b", IntType, fun _ x -> x.B) ]))
let query = "query Example { a, b, a }"
- let result = sync <| Executor(schema).AsyncExecute(query, getMockInputContext, { A = "aa"; B = 2 });
- let expected =
- NameValueLookup.ofList [
- "a", upcast "aa"
- "b", upcast 2]
- ensureDirect result <| fun data errors ->
- empty errors
- data |> equals (upcast expected)
+ let expected = NameValueLookup.ofList [ "a", upcast "aa"; "b", upcast 2 ]
+ task {
+ let! result = Executor(schema).AsyncExecute (query, getMockInputContext, { A = "aa"; B = 2 })
+ ensureDirect result
+ <| fun data errors ->
+ empty errors
+ data |> equals (upcast expected)
+ }
[]
-let ``Execution when querying returns unique document id with response`` () =
+let ``Execution documentId handles escaped string values correctly`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ x -> x.A)
- Define.Field("b", IntType, fun _ x -> x.B)
- ]))
- let result1 = sync <| Executor(schema).AsyncExecute("query Example { a, b, a }", getMockInputContext, { A = "aa"; B = 2 })
- let result2 = sync <| Executor(schema).AsyncExecute("query Example { a, b, a }", getMockInputContext, { A = "aa"; B = 2 })
- result1.DocumentId |> notEquals Unchecked.defaultof
- result1.DocumentId |> equals result2.DocumentId
- match result1,result2 with
- | Direct(data1, errors1), Direct(data2, errors2) ->
- equals data1 data2
- equals errors1 errors2
- | response -> fail $"Expected a 'Direct' GQLResponse but got\n{response}"
+ Schema (
+ Define.Object (
+ "Type",
+ [
+ Define.Field (
+ "a",
+ StringType,
+ "",
+ [ Define.Input ("arg", StringType) ],
+ fun ctx x ->
+ match ctx.TryArg ("arg") with
+ | ValueSome arg -> arg
+ | ValueNone -> x.A
+ )
+ Define.Field ("b", IntType, fun _ x -> x.B)
+ ]
+ )
+ )
+ // Query with string containing special characters that need escaping
+ let query = """query Example { a(arg: "test\"quote\nline\ttab\\backslash") }"""
+ task {
+ let! result = Executor(schema).AsyncExecute (query, getMockInputContext, { A = "test"; B = 1 })
+ // DocumentId should be deterministic and not empty
+ result.DocumentId |> notEquals Unchecked.defaultof
+ result.DocumentId.Length |> equals 64 // SHA-256 hex string is always 64 chars
+ }
+
+[]
+let ``Execution documentId is different for different queries`` () : Task =
+ let schema =
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A); Define.Field ("b", IntType, fun _ x -> x.B) ]))
+ let query1 = "query Example1 { a }"
+ let query2 = "query Example2 { b }"
+ task {
+ let executor = Executor (schema)
+ let! result1 = executor.AsyncExecute (query1, getMockInputContext, { A = "aa"; B = 2 })
+ let! result2 = executor.AsyncExecute (query2, getMockInputContext, { A = "aa"; B = 2 })
+ result1.DocumentId |> notEquals result2.DocumentId
+ }
+
+[]
+let ``Execution documentId is same for semantically identical queries`` () : Task =
+ let schema =
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ x -> x.A); Define.Field ("b", IntType, fun _ x -> x.B) ]))
+ // Same query with different whitespace/formatting
+ let query1 = "query Example { a b }"
+ let query2 = "query Example{a b}"
+ let query3 = "query Example { a, b }"
+ task {
+ let executor = Executor (schema)
+ let! result1 = executor.AsyncExecute (query1, getMockInputContext, { A = "aa"; B = 2 })
+ let! result2 = executor.AsyncExecute (query2, getMockInputContext, { A = "aa"; B = 2 })
+ let! result3 = executor.AsyncExecute (query3, getMockInputContext, { A = "aa"; B = 2 })
+ // All should produce the same documentId since they parse to the same AST
+ result1.DocumentId |> equals result2.DocumentId
+ result1.DocumentId |> equals result3.DocumentId
+ }
type InnerNullableTest = { Kaboom : string }
-type NullableTest = {
- Inner : InnerNullableTest
- InnerPartialSuccess : InnerNullableTest
-}
+type NullableTest = { Inner : InnerNullableTest; InnerPartialSuccess : InnerNullableTest }
[]
-let ``Execution handles errors: properly propagates errors`` () =
+let ``Execution handles errors: properly propagates errors`` () : Task =
let InnerObjType =
- Define.Object(
- "Inner", [
- Define.Field("kaboom", StringType, fun _ x -> x.Kaboom)
- ])
+ Define.Object ("Inner", [ Define.Field ("kaboom", StringType, fun _ x -> x.Kaboom) ])
let InnerPartialSuccessObjType =
// executeResolvers/resolveWith, case 5
let resolvePartialSuccess (ctx : ResolveFieldContext) (_ : InnerNullableTest) =
- ctx.AddError { new IGQLError with member _.Message = "Some non-critical error" }
+ ctx.AddError
+ { new IGQLError with
+ member _.Message = "Some non-critical error"
+ }
"Yes, Rico, Kaboom"
- Define.Object(
- "InnerPartialSuccess", [
- Define.Field("kaboom", StringType, resolvePartialSuccess)
- ])
+ Define.Object ("InnerPartialSuccess", [ Define.Field ("kaboom", StringType, resolvePartialSuccess) ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("inner", Nullable InnerObjType, fun _ x -> Some x.Inner)
- Define.Field("partialSuccess", Nullable InnerPartialSuccessObjType, fun _ x -> Some x.InnerPartialSuccess)
- ]))
+ Schema (
+ Define.Object (
+ "Type",
+ [
+ Define.Field ("inner", Nullable InnerObjType, fun _ x -> Some x.Inner)
+ Define.Field ("partialSuccess", Nullable InnerPartialSuccessObjType, fun _ x -> Some x.InnerPartialSuccess)
+ ]
+ )
+ )
let expectedData =
- NameValueLookup.ofList [
- "inner", null
- "partialSuccess", NameValueLookup.ofList [
- "kaboom", "Yes, Rico, Kaboom"
- ]
- ]
+ NameValueLookup.ofList [ "inner", null; "partialSuccess", NameValueLookup.ofList [ "kaboom", "Yes, Rico, Kaboom" ] ]
let expectedErrors = [
GQLProblemDetails.CreateWithKind ("Non-Null field kaboom resolved as a null!", Execution, [ box "inner"; "kaboom" ])
GQLProblemDetails.CreateWithKind ("Some non-critical error", Execution, [ box "partialSuccess"; "kaboom" ])
]
- let result =
- let variables = { Inner = { Kaboom = null }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } }
- sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } partialSuccess { kaboom } }", getMockInputContext, variables)
- ensureDirect result <| fun data errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- data |> equals (upcast expectedData)
- errors |> equals expectedErrors
+ let variables = {
+ Inner = { Kaboom = null }
+ InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" }
+ }
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { inner { kaboom } partialSuccess { kaboom } }", getMockInputContext, variables)
+ ensureDirect result
+ <| fun data errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ data |> equals (upcast expectedData)
+ errors |> equals expectedErrors
+ }
[]
-let ``Execution handles errors: exceptions`` () =
+let ``Execution handles errors: exceptions`` () : Task =
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("a", StringType, fun _ _ -> failwith "Resolver Error!")
- ]))
+ Schema (Define.Object ("Type", [ Define.Field ("a", StringType, fun _ _ -> failwith "Resolver Error!") ]))
let expectedError = GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "a" ])
- let result = sync <| Executor(schema).AsyncExecute("query Test { a }", getMockInputContext, ())
- ensureRequestError result <| fun [ error ] -> error |> equals expectedError
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Test { a }", getMockInputContext, ())
+ ensureRequestError result
+ <| fun [ error ] -> error |> equals expectedError
+ }
[]
-let ``Execution handles errors: nullable list fields`` () =
+let ``Execution handles errors: nullable list fields`` () : Task =
let InnerObject =
- Define.Object(
- "Inner", [
- Define.Field("error", StringType, fun _ _ -> failwith "Resolver Error!")
- ])
+ Define.Object ("Inner", [ Define.Field ("error", StringType, fun _ _ -> failwith "Resolver Error!") ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("list", ListOf (Nullable InnerObject), fun _ _ -> [Some 1; Some 2; None])
- ]))
- let expectedData =
- NameValueLookup.ofList [
- "list", upcast [null; null; null]
- ]
- let expectedErrors =
- [
- GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "list"; 0; "error" ])
- GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "list"; 1; "error" ])
- ]
- let result = sync <| Executor(schema).AsyncExecute("query Test { list { error } }", getMockInputContext, ())
- ensureDirect result <| fun data errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- data |> equals (upcast expectedData)
- errors |> equals expectedErrors
+ Schema (Define.Object ("Type", [ Define.Field ("list", ListOf (Nullable InnerObject), fun _ _ -> [ Some 1; Some 2; None ]) ]))
+ let expectedData = NameValueLookup.ofList [ "list", upcast [ null; null; null ] ]
+ let expectedErrors = [
+ GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "list"; 0; "error" ])
+ GQLProblemDetails.CreateWithKind ("Resolver Error!", Execution, [ box "list"; 1; "error" ])
+ ]
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Test { list { error } }", getMockInputContext, ())
+ ensureDirect result
+ <| fun data errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ data |> equals (upcast expectedData)
+ errors |> equals expectedErrors
+ }
[]
-let ``Execution handles errors: additional error added when exception is raised in a nullable field resolver`` () =
+let ``Execution handles errors: additional error added when exception is raised in a nullable field resolver`` () : Task =
let InnerNullableExceptionObjType =
// executeResolvers/resolveWith, case 1
let resolveWithException (ctx : ResolveFieldContext) (_ : InnerNullableTest) : string option =
- ctx.AddError { new IGQLError with member _.Message = "Non-critical error" }
+ ctx.AddError
+ { new IGQLError with
+ member _.Message = "Non-critical error"
+ }
raise (Exception "Unexpected error")
- Define.Object(
- "InnerNullableException", [
- Define.Field("kaboom", Nullable StringType, resolve = resolveWithException)
- ])
+ Define.Object ("InnerNullableException", [ Define.Field ("kaboom", Nullable StringType, resolve = resolveWithException) ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("inner", Nullable InnerNullableExceptionObjType, fun _ x -> Some x.Inner)
- ]))
- let expectedData =
- NameValueLookup.ofList [
- "inner", NameValueLookup.ofList [
- "kaboom", null
- ]
- ]
- let expectedErrors =
- [
- GQLProblemDetails.CreateWithKind ("Unexpected error", Execution, [ box "inner"; "kaboom" ])
- GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
- ]
- let result =
- let variables = { Inner = { Kaboom = null }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } }
- sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables)
- ensureDirect result <| fun data errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- data |> equals (upcast expectedData)
- errors |> equals expectedErrors
+ Schema (Define.Object ("Type", [ Define.Field ("inner", Nullable InnerNullableExceptionObjType, fun _ x -> Some x.Inner) ]))
+ let expectedData = NameValueLookup.ofList [ "inner", NameValueLookup.ofList [ "kaboom", null ] ]
+ let expectedErrors = [
+ GQLProblemDetails.CreateWithKind ("Unexpected error", Execution, [ box "inner"; "kaboom" ])
+ GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
+ ]
+ let variables = {
+ Inner = { Kaboom = null }
+ InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" }
+ }
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { inner { kaboom } }", getMockInputContext, variables)
+ ensureDirect result
+ <| fun data errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ data |> equals (upcast expectedData)
+ errors |> equals expectedErrors
+ }
[]
-let ``Execution handles errors: additional error added when None returned from a nullable field resolver`` () =
+let ``Execution handles errors: additional error added when None returned from a nullable field resolver`` () : Task =
let InnerNullableNoneObjType =
// executeResolvers/resolveWith, case 2
let resolveWithNone (ctx : ResolveFieldContext) (_ : InnerNullableTest) : string option =
- ctx.AddError { new IGQLError with member _.Message = "Non-critical error" }
+ ctx.AddError
+ { new IGQLError with
+ member _.Message = "Non-critical error"
+ }
None
- Define.Object(
- "InnerNullableException", [
- Define.Field("kaboom", Nullable StringType, resolve = resolveWithNone)
- ])
+ Define.Object ("InnerNullableException", [ Define.Field ("kaboom", Nullable StringType, resolve = resolveWithNone) ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("inner", Nullable InnerNullableNoneObjType, fun _ x -> Some x.Inner)
- ]))
- let expectedData =
- NameValueLookup.ofList [
- "inner", NameValueLookup.ofList [
- "kaboom", null
- ]
- ]
- let expectedErrors =
- [
- GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
- ]
- let result =
- let variables = { Inner = { Kaboom = null }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } }
- sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables)
- ensureDirect result <| fun data errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- data |> equals (upcast expectedData)
- errors |> equals expectedErrors
+ Schema (Define.Object ("Type", [ Define.Field ("inner", Nullable InnerNullableNoneObjType, fun _ x -> Some x.Inner) ]))
+ let expectedData = NameValueLookup.ofList [ "inner", NameValueLookup.ofList [ "kaboom", null ] ]
+ let expectedErrors = [ GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ]) ]
+ let variables = {
+ Inner = { Kaboom = null }
+ InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" }
+ }
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { inner { kaboom } }", getMockInputContext, variables)
+ ensureDirect result
+ <| fun data errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ data |> equals (upcast expectedData)
+ errors |> equals expectedErrors
+ }
[]
-let ``Execution handles errors: additional error added when exception is rised in a non-nullable field resolver`` () =
+let ``Execution handles errors: additional error added when exception is rised in a non-nullable field resolver`` () : Task =
let InnerNonNullableExceptionObjType =
// executeResolvers/resolveWith, case 3
let resolveWithException (ctx : ResolveFieldContext) (_ : InnerNullableTest) : string =
- ctx.AddError { new IGQLError with member _.Message = "Non-critical error" }
+ ctx.AddError
+ { new IGQLError with
+ member _.Message = "Non-critical error"
+ }
raise (Exception "Fatal error")
- Define.Object(
- "InnerNonNullableException", [
- Define.Field("kaboom", StringType, resolve = resolveWithException)
- ])
+ Define.Object ("InnerNonNullableException", [ Define.Field ("kaboom", StringType, resolve = resolveWithException) ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("inner", InnerNonNullableExceptionObjType, fun _ x -> x.Inner)
- ]))
- let expectedErrors =
- [
- GQLProblemDetails.CreateWithKind ("Fatal error", Execution, [ box "inner"; "kaboom" ])
- GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
- ]
- let result =
- let variables = { Inner = { Kaboom = "Yes, Rico, Kaboom" }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } }
- sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables)
- ensureRequestError result <| fun errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- errors |> equals expectedErrors
+ Schema (Define.Object ("Type", [ Define.Field ("inner", InnerNonNullableExceptionObjType, fun _ x -> x.Inner) ]))
+ let expectedErrors = [
+ GQLProblemDetails.CreateWithKind ("Fatal error", Execution, [ box "inner"; "kaboom" ])
+ GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
+ ]
+ let variables = {
+ Inner = { Kaboom = "Yes, Rico, Kaboom" }
+ InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" }
+ }
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { inner { kaboom } }", getMockInputContext, variables)
+ ensureRequestError result
+ <| fun errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ errors |> equals expectedErrors
+ }
[]
-let ``Execution handles errors: additional error added and when null returned from a non-nullable field resolver`` () =
+let ``Execution handles errors: additional error added and when null returned from a non-nullable field resolver`` () : Task =
let InnerNonNullableNullObjType =
// executeResolvers/resolveWith, case 4
let resolveWithNull (ctx : ResolveFieldContext) (_ : InnerNullableTest) : string =
- ctx.AddError { new IGQLError with member _.Message = "Non-critical error" }
+ ctx.AddError
+ { new IGQLError with
+ member _.Message = "Non-critical error"
+ }
null
- Define.Object(
- "InnerNonNullableNull", [
- Define.Field("kaboom", StringType, resolveWithNull)
- ])
+ Define.Object ("InnerNonNullableNull", [ Define.Field ("kaboom", StringType, resolveWithNull) ])
let schema =
- Schema(Define.Object(
- "Type", [
- Define.Field("inner", InnerNonNullableNullObjType, fun _ x -> x.Inner)
- ]))
- let expectedErrors =
- [
- GQLProblemDetails.CreateWithKind ("Non-Null field kaboom resolved as a null!", Execution, [ box "inner"; "kaboom" ])
- GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
- ]
- let result =
- let variables = { Inner = { Kaboom = "Yes, Rico, Kaboom" }; InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" } }
- sync <| Executor(schema).AsyncExecute("query Example { inner { kaboom } }", getMockInputContext, variables)
- ensureRequestError result <| fun errors ->
- result.DocumentId |> notEquals Unchecked.defaultof
- errors |> equals expectedErrors
+ Schema (Define.Object ("Type", [ Define.Field ("inner", InnerNonNullableNullObjType, fun _ x -> x.Inner) ]))
+ let expectedErrors = [
+ GQLProblemDetails.CreateWithKind ("Non-Null field kaboom resolved as a null!", Execution, [ box "inner"; "kaboom" ])
+ GQLProblemDetails.CreateWithKind ("Non-critical error", Execution, [ box "inner"; "kaboom" ])
+ ]
+ let variables = {
+ Inner = { Kaboom = "Yes, Rico, Kaboom" }
+ InnerPartialSuccess = { Kaboom = "Yes, Rico, Kaboom" }
+ }
+ task {
+ let! result = Executor(schema).AsyncExecute ("query Example { inner { kaboom } }", getMockInputContext, variables)
+ ensureRequestError result
+ <| fun errors ->
+ result.DocumentId |> notEquals Unchecked.defaultof
+ errors |> equals expectedErrors
+ }
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 1bc22455..57ab028a 100644
--- a/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj
+++ b/tests/FSharp.Data.GraphQL.Tests/FSharp.Data.GraphQL.Tests.fsproj
@@ -16,6 +16,7 @@
+
@@ -29,6 +30,8 @@
+
+
diff --git a/tests/FSharp.Data.GraphQL.Tests/Literals.fs b/tests/FSharp.Data.GraphQL.Tests/Literals.fs
index 57d11d47..64585ca4 100644
--- a/tests/FSharp.Data.GraphQL.Tests/Literals.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/Literals.fs
@@ -1,1592 +1,6 @@
module FSharp.Data.GraphQL.Tests.Literals
-let [] IntrospectionSchemaJson = """{
- "documentId": 869718943,
- "data": {
- "__schema": {
- "queryType": {
- "name": "Query"
- },
- "mutationType": {
- "name": "Mutation"
- },
- "subscriptionType": {
- "name": "Subscription"
- },
- "types": [
- {
- "kind": "SCALAR",
- "name": "Int",
- "description": "The `Int` 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 `String` scalar type represents textual data, represented as UTF-8 character sequences. The String 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 `Boolean` scalar type represents `true` or `false`.",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "SCALAR",
- "name": "Float",
- "description": "The `Float` 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 `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID 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 `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "SCALAR",
- "name": "Date",
- "description": "The `Date` scalar type represents a Date value with Time component. The Date 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": "URI",
- "description": "The `URI` scalar type represents a string resource identifier compatible with URI standard. The URI 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"
- }
- }
- }
- },
- "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"
- }
- }
- }
- },
- "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’s 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"
- }
- }
- }
- },
- "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"
- }
- }
- }
- },
- "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 `__TypeKind` 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"
- }
- }
- }
- },
- "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. `fields` and `interfaces` are valid fields.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "INTERFACE",
- "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "UNION",
- "description": "Indicates this type is a union. `possibleTypes` is a valid field.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "ENUM",
- "description": "Indicates this type is an enum. `enumValues` is a valid field.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "INPUT_OBJECT",
- "description": "Indicates this type is an input object. `inputFields` is a valid field.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "LIST",
- "description": "Indicates this type is a list. `ofType` is a valid field.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "NON_NULL",
- "description": "Indicates this type is a non-null. `ofType` 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
- }
- ],
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Query",
- "description": null,
- "fields": [
- {
- "name": "droid",
- "description": "Gets droid",
- "args": [
- {
- "name": "id",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "OBJECT",
- "name": "Droid",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "hero",
- "description": "Gets human hero",
- "args": [
- {
- "name": "id",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "OBJECT",
- "name": "Human",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "planet",
- "description": "Gets planet",
- "args": [
- {
- "name": "id",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "OBJECT",
- "name": "Planet",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "things",
- "description": "Gets things",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "INTERFACE",
- "name": "Thing"
- }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Droid",
- "description": "A mechanical creature in the Star Wars universe.",
- "fields": [
- {
- "name": "appearsIn",
- "description": "Which movies they appear in.",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "ENUM",
- "name": "Episode"
- }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "friends",
- "description": "The friends of the Droid, or an empty list if they have none.",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "UNION",
- "name": "Character",
- "ofType": null
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "id",
- "description": "The id of the droid.",
- "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 Droid.",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "primaryFunction",
- "description": "The primary function of the droid.",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "ENUM",
- "name": "Episode",
- "description": "One of the films in the Star Wars Trilogy",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- {
- "name": "NewHope",
- "description": "Released in 1977.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "Empire",
- "description": "Released in 1980.",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "Jedi",
- "description": "Released in 1983.",
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "possibleTypes": null
- },
- {
- "kind": "UNION",
- "name": "Character",
- "description": "A character in the Star Wars Trilogy",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": [
- {
- "kind": "OBJECT",
- "name": "Human",
- "ofType": null
- },
- {
- "kind": "OBJECT",
- "name": "Droid",
- "ofType": null
- }
- ]
- },
- {
- "kind": "OBJECT",
- "name": "Human",
- "description": "A humanoid creature in the Star Wars universe.",
- "fields": [
- {
- "name": "appearsIn",
- "description": "Which movies they appear in.",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "ENUM",
- "name": "Episode"
- }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "friends",
- "description": "The friends of the human, or an empty list if they have none.",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "UNION",
- "name": "Character",
- "ofType": null
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "homePlanet",
- "description": "The home planet of the human, or null if unknown.",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "id",
- "description": "The id of the human.",
- "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 human.",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Planet",
- "description": "A planet in the Star Wars universe.",
- "fields": [
- {
- "name": "id",
- "description": "The id of the planet",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "isMoon",
- "description": "Is that a moon?",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "Boolean",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "name",
- "description": "The name of the planet.",
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "INTERFACE",
- "name": "Thing",
- "description": "Gets the shape of the thing.",
- "fields": [
- {
- "name": "format",
- "description": "The format of the shape",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "id",
- "description": "The ID of the shape",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": [
- {
- "kind": "OBJECT",
- "name": "Box",
- "ofType": null
- },
- {
- "kind": "OBJECT",
- "name": "Ball",
- "ofType": null
- }
- ]
- },
- {
- "kind": "OBJECT",
- "name": "Subscription",
- "description": null,
- "fields": [
- {
- "name": "watchMoon",
- "description": "Watches to see if a planet is a moon",
- "args": [
- {
- "name": "id",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "OBJECT",
- "name": "Planet",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Mutation",
- "description": null,
- "fields": [
- {
- "name": "setMoon",
- "description": "Sets a moon status",
- "args": [
- {
- "name": "id",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
- "name": "isMoon",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Boolean",
- "ofType": null
- }
- },
- "defaultValue": null
- }
- ],
- "type": {
- "kind": "OBJECT",
- "name": "Planet",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Ball",
- "description": "A ball.",
- "fields": [
- {
- "name": "format",
- "description": "The format of the ball",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "id",
- "description": "The ID of the ball",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [
- {
- "kind": "INTERFACE",
- "name": "Thing",
- "ofType": null
- }
- ],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "Box",
- "description": "A box.",
- "fields": [
- {
- "name": "format",
- "description": "The format of the box",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "id",
- "description": "The ID of the box",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [
- {
- "kind": "INTERFACE",
- "name": "Thing",
- "ofType": null
- }
- ],
- "enumValues": null,
- "possibleTypes": null
- }
- ],
- "directives": [
- {
- "name": "include",
- "description": "Directs the executor to include this field or fragment only when the `if` 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 `if` 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
+open FSharp.Data.LiteralProviders
+
+let [] IntrospectionSchemaJson =
+ TextFile<"../FSharp.Data.GraphQL.IntegrationTests/introspection.json", EnsureExists = true>.Text
diff --git a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs
index ad7cf74d..46d6ebe5 100644
--- a/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs
+++ b/tests/FSharp.Data.GraphQL.Tests/PlanningTests.fs
@@ -14,75 +14,80 @@ open FSharp.Data.GraphQL.Parser
open FSharp.Data.GraphQL.Planning
open FSharp.Data.GraphQL.Execution
-type Person =
- { firstName : string
- lastName : string
- age : int }
+type Person = { firstName : string; lastName : string; age : int }
-type Animal =
- { name : string
- species : string }
+type Animal = { name : string; species : string }
type Named =
| Animal of Animal
| Person of Person
-let people =
- [ { firstName = "John"
- lastName = "Doe"
- age = 21 } ]
+let people = [ { firstName = "John"; lastName = "Doe"; age = 21 } ]
-let animals =
- [ { name = "Max"
- species = "Dog" } ]
+let animals = [ { name = "Max"; species = "Dog" } ]
let rec Person =
- DefineRec.Object(
+ DefineRec.Object (
name = "Person",
- fieldsFn = (fun () ->
- [ Define.Field("firstName", StringType, fun _ person -> person.firstName)
- Define.Field("lastName", StringType, fun _ person -> person.lastName)
- Define.Field("age", IntType, fun _ person -> person.age)
- Define.Field("name", StringType, fun _ person -> person.firstName + " " + person.lastName)
- Define.Field("friends", ListOf Person, fun _ _ -> []) ]), interfaces = [ INamed ])
+ fieldsFn =
+ (fun () -> [
+ Define.Field ("firstName", StringType, fun _ person -> person.firstName)
+ Define.Field ("lastName", StringType, fun _ person -> person.lastName)
+ Define.Field ("age", IntType, fun _ person -> person.age)
+ Define.Field ("name", StringType, fun _ person -> person.firstName + " " + person.lastName)
+ Define.Field ("friends", ListOf Person, fun _ _ -> [])
+ ]),
+ interfaces = [ INamed ]
+ )
and Animal =
- Define.Object(name = "Animal",
- fields = [ Define.Field("name", StringType, fun _ animal -> animal.name)
- Define.Field("species", StringType, fun _ animal -> animal.species) ], interfaces = [ INamed ])
+ Define.Object (
+ name = "Animal",
+ fields = [
+ Define.Field ("name", StringType, fun _ animal -> animal.name)
+ Define.Field ("species", StringType, fun _ animal -> animal.species)
+ ],
+ interfaces = [ INamed ]
+ )
-and INamed = Define.Interface("INamed", [ Define.Field("name", StringType) ])
+and INamed = Define.Interface ("INamed", [ Define.Field ("name", StringType) ])
and UNamed =
- Define.Union(
- "UNamed", [ Person; Animal ],
+ Define.Union (
+ "UNamed",
+ [ Person; Animal ],
function
| Animal a -> box a
- | Person p -> upcast p)
+ | Person p -> upcast p
+ )
[]
-let ``Planning must retain correct types for leafs``() =
- let schema = Schema(Person)
- let schemaProcessor = Executor(schema)
- let query = """{
+let ``Planning must retain correct types for leafs`` () =
+ let schema = Schema (Person)
+ let schemaProcessor = Executor (schema)
+ let query =
+ """{
firstName
lastName
age
}"""
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
plan.RootDef |> equals (upcast Person)
equals 3 plan.Fields.Length
plan.Fields
|> List.map (fun info -> (info.Identifier, info.ParentDef, info.ReturnDef))
- |> equals [ ("firstName", upcast Person, upcast StringType)
- ("lastName", upcast Person, upcast StringType)
- ("age", upcast Person, upcast IntType) ]
+ |> equals [
+ ("firstName", upcast Person, upcast StringType)
+ ("lastName", upcast Person, upcast StringType)
+ ("age", upcast Person, upcast IntType)
+ ]
[]
-let ``Planning must work with fragments``() =
- let schema = Schema(Person)
- let schemaProcessor = Executor(schema)
- let query = """query Example {
+let ``Planning must work with fragments`` () =
+ let schema = Schema (Person)
+ let schemaProcessor = Executor (schema)
+ let query =
+ """query Example {
...named
age
}
@@ -90,20 +95,23 @@ let ``Planning must work with fragments``() =
firstName
lastName
}"""
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
plan.RootDef |> equals (upcast Person)
equals 3 plan.Fields.Length
plan.Fields
|> List.map (fun info -> (info.Identifier, info.ParentDef, info.ReturnDef))
- |> equals [ ("firstName", upcast Person, upcast StringType)
- ("lastName", upcast Person, upcast StringType)
- ("age", upcast Person, upcast IntType) ]
+ |> equals [
+ ("firstName", upcast Person, upcast StringType)
+ ("lastName", upcast Person, upcast StringType)
+ ("age", upcast Person, upcast IntType)
+ ]
[]
-let ``Planning must work with parallel fragments``() =
- let schema = Schema(Person)
- let schemaProcessor = Executor(schema)
- let query = """query Example {
+let ``Planning must work with parallel fragments`` () =
+ let schema = Schema (Person)
+ let schemaProcessor = Executor (schema)
+ let query =
+ """query Example {
...fnamed
...lnamed
age
@@ -115,21 +123,24 @@ let ``Planning must work with parallel fragments``() =
lastName
}
"""
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
plan.RootDef |> equals (upcast Person)
equals 3 plan.Fields.Length
plan.Fields
|> List.map (fun info -> (info.Identifier, info.ParentDef, info.ReturnDef))
- |> equals [ ("firstName", upcast Person, upcast StringType)
- ("lastName", upcast Person, upcast StringType)
- ("age", upcast Person, upcast IntType) ]
+ |> equals [
+ ("firstName", upcast Person, upcast StringType)
+ ("lastName", upcast Person, upcast StringType)
+ ("age", upcast Person, upcast IntType)
+ ]
[]
-let ``Planning must retain correct types for lists``() =
- let Query = Define.Object("Query", [ Define.Field("people", ListOf Person, fun _ () -> people) ])
- let schema = Schema(Query)
- let schemaProcessor = Executor(schema)
- let query = """{
+let ``Planning must retain correct types for lists`` () =
+ let Query = Define.Object ("Query", [ Define.Field ("people", ListOf Person, fun _ () -> people) ])
+ let schema = Schema (Query)
+ let schemaProcessor = Executor (schema)
+ let query =
+ """{
people {
firstName
lastName
@@ -140,31 +151,35 @@ let ``Planning must retain correct types for lists``() =
}
}"""
let PersonList : OutputDef = ListOf Person
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
equals 1 plan.Fields.Length
let listInfo = plan.Fields.Head
listInfo.Identifier |> equals "people"
listInfo.ReturnDef |> equals (upcast PersonList)
- let (ResolveCollection(info)) = listInfo.Kind
+ let (ResolveCollection (info)) = listInfo.Kind
info.ParentDef |> equals (upcast PersonList)
info.ReturnDef |> equals (upcast Person)
- let (SelectFields(innerFields)) = info.Kind
+ let (SelectFields (innerFields)) = info.Kind
equals 3 innerFields.Length
innerFields
|> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef))
- |> equals [ ("firstName", upcast Person, upcast StringType)
- ("lastName", upcast Person, upcast StringType)
- ("friends", upcast Person, upcast PersonList) ]
- let (ResolveCollection(friendInfo)) = (innerFields |> List.find (fun i -> i.Identifier = "friends")).Kind
+ |> equals [
+ ("firstName", upcast Person, upcast StringType)
+ ("lastName", upcast Person, upcast StringType)
+ ("friends", upcast Person, upcast PersonList)
+ ]
+ let (ResolveCollection (friendInfo)) =
+ (innerFields |> List.find (fun i -> i.Identifier = "friends")).Kind
friendInfo.ParentDef |> equals (upcast PersonList)
friendInfo.ReturnDef |> equals (upcast Person)
[]
-let ``Planning must work with interfaces``() =
- let Query = Define.Object("Query", [ Define.Field("names", ListOf INamed, fun _ () -> []) ])
- let schema = Schema(query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal ] })
- let schemaProcessor = Executor(schema)
- let query = """query Example {
+let ``Planning must work with interfaces`` () =
+ let Query = Define.Object ("Query", [ Define.Field ("names", ListOf INamed, fun _ () -> []) ])
+ let schema = Schema (query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal ] })
+ let schemaProcessor = Executor (schema)
+ let query =
+ """query Example {
names {
name
... on Animal {
@@ -176,31 +191,34 @@ let ``Planning must work with interfaces``() =
fragment ageFragment on Person {
age
}"""
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
equals 1 plan.Fields.Length
let INamedList : OutputDef = ListOf INamed
let listInfo = plan.Fields.Head
listInfo.Identifier |> equals "names"
listInfo.ReturnDef |> equals (upcast INamedList)
- let (ResolveCollection(info)) = listInfo.Kind
+ let (ResolveCollection (info)) = listInfo.Kind
info.ParentDef |> equals (upcast INamedList)
info.ReturnDef |> equals (upcast INamed)
- let (ResolveAbstraction(innerFields)) = info.Kind
+ let (ResolveAbstraction (innerFields)) = info.Kind
innerFields
- |> Map.map (fun typeName fields -> fields |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
- |> equals (Map.ofList [ "Person",
- [ ("name", upcast INamed, upcast StringType)
- ("age", upcast INamed, upcast IntType) ]
- "Animal",
- [ ("name", upcast INamed, upcast StringType)
- ("species", upcast INamed, upcast StringType) ] ])
+ |> Map.map (fun typeName fields ->
+ fields
+ |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
+ |> equals (
+ Map.ofList [
+ "Person", [ ("name", upcast INamed, upcast StringType); ("age", upcast INamed, upcast IntType) ]
+ "Animal", [ ("name", upcast INamed, upcast StringType); ("species", upcast INamed, upcast StringType) ]
+ ]
+ )
[]
-let ``Planning must work with unions``() =
- let Query = Define.Object("Query", [ Define.Field("names", ListOf UNamed, fun _ () -> []) ])
- let schema = Schema(Query)
- let schemaProcessor = Executor(schema)
- let query = """query Example {
+let ``Planning must work with unions`` () =
+ let Query = Define.Object ("Query", [ Define.Field ("names", ListOf UNamed, fun _ () -> []) ])
+ let schema = Schema (Query)
+ let schemaProcessor = Executor (schema)
+ let query =
+ """query Example {
names {
... on Animal {
name
@@ -212,27 +230,29 @@ let ``Planning must work with unions``() =
}
}
}"""
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
equals 1 plan.Fields.Length
let listInfo = plan.Fields.Head
let UNamedList : OutputDef = ListOf UNamed
listInfo.Identifier |> equals "names"
listInfo.ReturnDef |> equals (upcast UNamedList)
- let (ResolveCollection(info)) = listInfo.Kind
+ let (ResolveCollection (info)) = listInfo.Kind
info.ParentDef |> equals (upcast UNamedList)
info.ReturnDef |> equals (upcast UNamed)
- let (ResolveAbstraction(innerFields)) = info.Kind
+ let (ResolveAbstraction (innerFields)) = info.Kind
innerFields
- |> Map.map (fun typeName fields -> fields |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
- |> equals (Map.ofList [ "Animal",
- [ ("name", upcast UNamed, upcast StringType)
- ("species", upcast UNamed, upcast StringType) ]
- "Person",
- [ ("name", upcast UNamed, upcast StringType)
- ("age", upcast UNamed, upcast IntType) ] ])
+ |> Map.map (fun typeName fields ->
+ fields
+ |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
+ |> equals (
+ Map.ofList [
+ "Animal", [ ("name", upcast UNamed, upcast StringType); ("species", upcast UNamed, upcast StringType) ]
+ "Person", [ ("name", upcast UNamed, upcast StringType); ("age", upcast UNamed, upcast IntType) ]
+ ]
+ )
[]
-let ``Planning must handle inline fragment with non-matching type condition in unions``() =
+let ``Planning must handle inline fragment with non-matching type condition in unions`` () =
// ═══════════════════════════════════════════════════════════════════════════
// REGRESSION TEST for Planning_ResolveDeferred_Bug
// ═══════════════════════════════════════════════════════════════════════════
@@ -272,20 +292,24 @@ let ``Planning must handle inline fragment with non-matching type condition in u
// Create a third type that is NOT part of UNamed union
let Robot =
- Define.Object(
+ Define.Object (
name = "Robot",
- fields =
- [ Define.Field("modelNumber", StringType, fun _ (robot: string) -> robot)
- Define.Field("name", StringType, fun _ _ -> "Robot") ])
+ fields = [
+ Define.Field ("modelNumber", StringType, fun _ (robot : string) -> robot)
+ Define.Field ("name", StringType, fun _ _ -> "Robot")
+ ]
+ )
- let Query = Define.Object("Query", [ Define.Field("names", ListOf UNamed, fun _ () -> []) ])
- let schema = Schema(query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; Robot ] })
- let schemaProcessor = Executor(schema)
+ let Query = Define.Object ("Query", [ Define.Field ("names", ListOf UNamed, fun _ () -> []) ])
+ let schema =
+ Schema (query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; Robot ] })
+ let schemaProcessor = Executor (schema)
// GraphQL Query:
// UNamed union = Person | Animal (Robot is NOT in this union)
// The "... on Robot" fragment below will never match any objects
- let query = """query Example {
+ let query =
+ """query Example {
names {
... on Animal {
name
@@ -304,7 +328,7 @@ let ``Planning must handle inline fragment with non-matching type condition in u
// TEST ASSERTION:
// This must succeed per GraphQL spec – non-matching fragments are valid
// Bug would cause: "Expected an Abstraction!" runtime error during planning
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
// Verify the execution plan structure
equals 1 plan.Fields.Length
@@ -312,27 +336,29 @@ let ``Planning must handle inline fragment with non-matching type condition in u
let UNamedList : OutputDef = ListOf UNamed
listInfo.Identifier |> equals "names"
listInfo.ReturnDef |> equals (upcast UNamedList)
- let (ResolveCollection(info)) = listInfo.Kind
+ let (ResolveCollection (info)) = listInfo.Kind
info.ParentDef |> equals (upcast UNamedList)
info.ReturnDef |> equals (upcast UNamed)
// Must successfully extract abstraction info
// Bug would fail here with wrong execution info kind
- let (ResolveAbstraction(innerFields)) = info.Kind
+ let (ResolveAbstraction (innerFields)) = info.Kind
// Result: Only Animal and Person fields (Robot is filtered out)
// This is correct GraphQL behavior – non-matching fragments produce no fields
innerFields
- |> Map.map (fun typeName fields -> fields |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
- |> equals (Map.ofList [ "Animal",
- [ ("name", upcast UNamed, upcast StringType)
- ("species", upcast UNamed, upcast StringType) ]
- "Person",
- [ ("name", upcast UNamed, upcast StringType)
- ("age", upcast UNamed, upcast IntType) ] ])
+ |> Map.map (fun typeName fields ->
+ fields
+ |> List.map (fun i -> (i.Identifier, i.ParentDef, i.ReturnDef)))
+ |> equals (
+ Map.ofList [
+ "Animal", [ ("name", upcast UNamed, upcast StringType); ("species", upcast UNamed, upcast StringType) ]
+ "Person", [ ("name", upcast UNamed, upcast StringType); ("age", upcast UNamed, upcast IntType) ]
+ ]
+ )
[]
-let ``Planning must handle nested inline fragments with non-matching type conditions``() =
+let ``Planning must handle nested inline fragments with non-matching type conditions`` () =
// REGRESSION TEST for Planning_ResolveDeferred_Bug (nested scenario)
//
// GraphQL SCENARIO:
@@ -352,28 +378,27 @@ let ``Planning must handle nested inline fragments with non-matching type condit
// Define Robot type (not part of UNamed union)
let RobotType =
- Define.Object(
+ Define.Object (
name = "Robot",
- fields =
- [ Define.Field("modelNumber", StringType, fun _ (robot: string) -> robot)
- Define.Field("name", StringType, fun _ _ -> "Robot") ])
+ fields = [
+ Define.Field ("modelNumber", StringType, fun _ (robot : string) -> robot)
+ Define.Field ("name", StringType, fun _ _ -> "Robot")
+ ]
+ )
// Container type with nested union list – creates deeper nesting
let ContainerType =
- Define.Object(
- name = "Container",
- fields = [ Define.Field("nested", ListOf UNamed, fun _ () -> []) ])
+ Define.Object (name = "Container", fields = [ Define.Field ("nested", ListOf UNamed, fun _ () -> []) ])
- let Query =
- Define.Object(
- "Query",
- [ Define.Field("container", ContainerType, fun _ () -> ()) ])
+ let Query = Define.Object ("Query", [ Define.Field ("container", ContainerType, fun _ () -> ()) ])
- let schema = Schema(query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
- let schemaProcessor = Executor(schema)
+ let schema =
+ Schema (query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
+ let schemaProcessor = Executor (schema)
// Nested query with non-matching fragment
- let query = """query Example {
+ let query =
+ """query Example {
container {
nested {
... on Animal {
@@ -392,14 +417,14 @@ let ``Planning must handle nested inline fragments with non-matching type condit
}"""
// Must succeed – nested non-matching fragments are valid per GraphQL spec
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
// Verify the plan structure is correct
equals 1 plan.Fields.Length
plan.Fields.Head.Identifier |> equals "container"
[]
-let ``Planning must return ResolveAbstraction even when all fragments are non-matching``() =
+let ``Planning must return ResolveAbstraction even when all fragments are non-matching`` () =
// REGRESSION TEST for Planning_ResolveDeferred_Bug (extreme case)
//
// GraphQL SCENARIO – EDGE CASE:
@@ -431,18 +456,18 @@ let ``Planning must return ResolveAbstraction even when all fragments are non-ma
// Robot is NOT in UNamed union
let RobotType =
- Define.Object(
- name = "Robot",
- fields = [ Define.Field("modelNumber", StringType, fun _ (robot: string) -> robot) ])
+ Define.Object (name = "Robot", fields = [ Define.Field ("modelNumber", StringType, fun _ (robot : string) -> robot) ])
- let Query = Define.Object("Query", [ Define.Field("names", ListOf UNamed, fun _ () -> []) ])
- let schema = Schema(query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
- let schemaProcessor = Executor(schema)
+ let Query = Define.Object ("Query", [ Define.Field ("names", ListOf UNamed, fun _ () -> []) ])
+ let schema =
+ Schema (query = Query, config = { SchemaConfig.Default with Types = [ Person; Animal; RobotType ] })
+ let schemaProcessor = Executor (schema)
// GraphQL Query – ONLY non-matching fragment!
// UNamed union = Person | Animal (NOT Robot)
// This query will match zero objects at runtime
- let query = """query Example {
+ let query =
+ """query Example {
names {
... on Robot {
modelNumber
@@ -453,15 +478,15 @@ let ``Planning must return ResolveAbstraction even when all fragments are non-ma
// TEST ASSERTION:
// Must succeed per GraphQL spec – empty result is valid, not an error
// Bug would cause: Runtime crash "Expected an Abstraction!" during planning
- let plan = schemaProcessor.CreateExecutionPlanOrFail(query)
+ let plan = schemaProcessor.CreateExecutionPlanOrFail (query)
// Verify the plan was created successfully
equals 1 plan.Fields.Length
let listInfo = plan.Fields.Head
- let (ResolveCollection(info)) = listInfo.Kind
+ let (ResolveCollection (info)) = listInfo.Kind
// Must successfully extract abstraction info
- let (ResolveAbstraction(innerFields)) = info.Kind
+ let (ResolveAbstraction (innerFields)) = info.Kind
// Result: Empty map – no matching types
// This is CORRECT per GraphQL spec – valid query, just matches nothing
diff --git a/tests/FSharp.Data.GraphQL.Tests/ValidationCacheTests.fs b/tests/FSharp.Data.GraphQL.Tests/ValidationCacheTests.fs
new file mode 100644
index 00000000..c6d93202
--- /dev/null
+++ b/tests/FSharp.Data.GraphQL.Tests/ValidationCacheTests.fs
@@ -0,0 +1,145 @@
+// The MIT License (MIT)
+// Copyright (c) 2016 Bazinga Technologies Inc
+
+module FSharp.Data.GraphQL.Tests.ValidationCacheTests
+
+open System.Threading
+open System.Threading.Tasks
+open Xunit
+open FSharp.Data.GraphQL
+open FSharp.Data.GraphQL.Validation
+
+[]
+let ``MemoryValidationResultCache caches results for same key`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+ let mutable callCount = 0
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ Success
+
+ let key = { DocumentId = "doc1"; SchemaId = 1 }
+
+ // First call should invoke producer
+ let result1 = cache.GetOrAdd producer key
+ equals 1 callCount
+ equals Success result1
+
+ // Second call with same key should NOT invoke producer (cached)
+ let result2 = cache.GetOrAdd producer key
+ equals 1 callCount // Still 1, not 2
+ equals Success result2
+
+[]
+let ``MemoryValidationResultCache uses different cache entries for different DocumentIds`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+ let mutable callCount = 0
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ Success
+
+ let key1 = { DocumentId = "doc1"; SchemaId = 1 }
+ let key2 = { DocumentId = "doc2"; SchemaId = 1 }
+
+ // First call
+ let result1 = cache.GetOrAdd producer key1
+ equals 1 callCount
+
+ // Second call with different DocumentId should invoke producer again
+ let result2 = cache.GetOrAdd producer key2
+ equals 2 callCount // Should be 2 now
+
+[]
+let ``MemoryValidationResultCache uses different cache entries for different SchemaIds`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+ let mutable callCount = 0
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ Success
+
+ let key1 = { DocumentId = "doc1"; SchemaId = 1 }
+ let key2 = { DocumentId = "doc1"; SchemaId = 2 }
+
+ // First call
+ let result1 = cache.GetOrAdd producer key1
+ equals 1 callCount
+
+ // Second call with different SchemaId should invoke producer again
+ let result2 = cache.GetOrAdd producer key2
+ equals 2 callCount // Should be 2 now
+
+[]
+let ``MemoryValidationResultCache distinguishes keys with same hash code`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+
+ // Create two different keys that might have hash collisions
+ // Using very similar but different strings
+ let key1 = {
+ DocumentId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ SchemaId = 1
+ }
+ let key2 = {
+ DocumentId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
+ SchemaId = 1
+ }
+
+ let mutable callCount = 0
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ Success
+
+ // First call
+ let result1 = cache.GetOrAdd producer key1
+ equals 1 callCount
+
+ // Second call with different key should invoke producer even if hash codes collide
+ let result2 = cache.GetOrAdd producer key2
+ equals 2 callCount // Should be 2, proving we use full key not just hash
+
+[]
+let ``MemoryValidationResultCache caches error results`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+ let mutable callCount = 0
+ let error = GQLProblemDetails.Create ("Test error")
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ ValidationError [ error ]
+
+ let key = { DocumentId = "doc1"; SchemaId = 1 }
+
+ // First call should invoke producer
+ let result1 = cache.GetOrAdd producer key
+ equals 1 callCount
+ match result1 with
+ | ValidationError errors -> equals 1 (Seq.length errors)
+ | Success -> fail "Expected ValidationError"
+
+ // Second call with same key should NOT invoke producer (cached)
+ let result2 = cache.GetOrAdd producer key
+ equals 1 callCount // Still 1, not 2
+ match result2 with
+ | ValidationError errors -> equals 1 (Seq.length errors)
+ | Success -> fail "Expected ValidationError"
+
+[]
+let ``MemoryValidationResultCache handles concurrent access`` () =
+ let cache = MemoryValidationResultCache () :> IValidationResultCache
+ let mutable callCount = 0
+ let producer () =
+ Interlocked.Increment (&callCount) |> ignore
+ Thread.Sleep (10) // Simulate some work
+ Success
+
+ let key = { DocumentId = "doc1"; SchemaId = 1 }
+
+ let workerCount = 10
+ let results = Array.zeroCreate workerCount
+
+ Parallel.For (0, workerCount, fun i ->
+ results.[i] <- cache.GetOrAdd producer key
+ ) |> ignore
+
+ // All results should be Success
+ results |> Array.iter (fun r -> equals Success r)
+
+ // Producer should be called at least once, but can run up to workerCount times due to ConcurrentDictionary.GetOrAdd factory semantics.
+ Assert.True (callCount >= 1 && callCount <= workerCount, $"Expected callCount between 1 and {workerCount}, got {callCount}")