Skip to content

Commit ce57938

Browse files
committed
Improved query normalization
1 parent 31639fc commit ce57938

4 files changed

Lines changed: 54 additions & 7 deletions

File tree

src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@ module internal Provider =
799799
match validationResult with
800800
| ValidationError msgs -> failwith (formatValidationExceptionMessage msgs)
801801
| Success -> ()
802-
let key = { DocumentId = DocumentId.fromCanonicalQuery (queryAst.ToQueryString()); SchemaId = schema.GetHashCode() }
802+
let key = { DocumentId = DocumentId.fromCanonicalQueryUnsafe (queryAst.ToQueryString()); SchemaId = schema.GetHashCode() }
803803
let refMaker = lazy Validation.Ast.validateDocument schema queryAst
804804
if clientQueryValidation then
805805
refMaker.Force

src/FSharp.Data.GraphQL.Server/Executor.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
142142
eval (executionPlan, data, variables, getInputContext)
143143

144144
let createExecutionPlan (ast: Document, operationName: string option, meta : Metadata) =
145-
let documentId = DocumentId.fromCanonicalQuery (ast.ToQueryString())
145+
let documentId = DocumentId.fromCanonicalQueryUnsafe (ast.ToQueryString())
146146
result {
147147
match findOperation ast operationName with
148148
| Some operation ->

src/FSharp.Data.GraphQL.Shared/Helpers/DocumentId.fs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,38 @@ open System.Text
77

88
let private formatByteAsLowerHex (value : byte) = value.ToString ("x2", CultureInfo.InvariantCulture)
99

10+
let internal fromCanonicalQueryUnsafe (canonicalQuery : string) =
11+
let queryBytes = Encoding.UTF8.GetBytes canonicalQuery
12+
use sha256 = SHA256.Create ()
13+
let hash = sha256.ComputeHash queryBytes
14+
hash |> Seq.map formatByteAsLowerHex |> String.concat ""
15+
16+
1017
/// <summary>
1118
/// Computes a deterministic document identifier from a canonical GraphQL query string.
1219
/// </summary>
1320
/// <param name="canonicalQuery">The canonical GraphQL query string (must already be properly escaped according to GraphQL specification).</param>
1421
/// <returns>A lowercase hexadecimal SHA-256 hash string that uniquely identifies the document content.</returns>
1522
[<CompiledName("FromCanonicalQuery")>]
1623
let fromCanonicalQuery (canonicalQuery : string) =
17-
let normalizedCanonicalQuery = canonicalQuery.Replace("\r\n", "\n").Replace ("\r", "\n")
18-
let queryBytes = Encoding.UTF8.GetBytes normalizedCanonicalQuery
19-
use sha256 = SHA256.Create ()
20-
let hash = sha256.ComputeHash queryBytes
21-
hash |> Seq.map formatByteAsLowerHex |> String.concat ""
24+
let normalizedCanonicalQuery =
25+
let crIndex = canonicalQuery.IndexOf '\r'
26+
if crIndex < 0 then
27+
canonicalQuery
28+
else
29+
let sb = StringBuilder (canonicalQuery.Length)
30+
sb.Append (canonicalQuery, 0, crIndex) |> ignore
31+
let mutable i = crIndex
32+
while i < canonicalQuery.Length do
33+
let c = canonicalQuery[i]
34+
if c = '\r' then
35+
sb.Append '\n' |> ignore
36+
if i + 1 < canonicalQuery.Length && canonicalQuery[i + 1] = '\n' then
37+
i <- i + 2
38+
else
39+
i <- i + 1
40+
else
41+
sb.Append c |> ignore
42+
i <- i + 1
43+
sb.ToString ()
44+
fromCanonicalQueryUnsafe normalizedCanonicalQuery

tests/FSharp.Data.GraphQL.Tests/DocumentIdTests.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ module FSharp.Data.GraphQL.Tests.DocumentIdTests
66
open Xunit
77
open FSharp.Data.GraphQL
88

9+
[<Fact>]
10+
let ``DocumentId.fromCanonicalQueryUnsafe produces deterministic hash`` () =
11+
let query = "query Example { a b }"
12+
let hash1 = DocumentId.fromCanonicalQueryUnsafe query
13+
let hash2 = DocumentId.fromCanonicalQueryUnsafe query
14+
equals hash1 hash2
15+
equals 64 hash1.Length // SHA-256 hex string is 64 chars
16+
17+
[<Fact>]
18+
let ``DocumentId.fromCanonicalQueryUnsafe produces different hashes for different queries`` () =
19+
let query1 = "query Example1 { a }"
20+
let query2 = "query Example2 { b }"
21+
let hash1 = DocumentId.fromCanonicalQueryUnsafe query1
22+
let hash2 = DocumentId.fromCanonicalQueryUnsafe query2
23+
notEquals hash1 hash2
24+
25+
[<Fact>]
26+
let ``DocumentId.fromCanonicalQueryUnsafe handles empty string`` () =
27+
let query = ""
28+
let hash = DocumentId.fromCanonicalQueryUnsafe query
29+
equals 64 hash.Length
30+
// SHA-256 of empty string
31+
equals "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" hash
32+
933
[<Fact>]
1034
let ``DocumentId.fromCanonicalQuery produces deterministic hash`` () =
1135
let query = "query Example { a b }"

0 commit comments

Comments
 (0)