Skip to content

Commit 355d611

Browse files
Copilotxperiandri
andcommitted
Make SchemaId deterministic using SHA-256 hash of introspection schema JSON
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Data.GraphQL/sessions/ce0fba11-9043-452f-b948-e03c8b644f26 Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
1 parent c729a3d commit 355d611

5 files changed

Lines changed: 50 additions & 4 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.fromCanonicalQuery (queryAst.ToQueryString()); SchemaId = SchemaId.fromIntrospectionSchema schema }
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
@@ -161,7 +161,7 @@ type Executor<'Root>(schema: ISchema<'Root>, middlewares : IExecutorMiddleware s
161161
ErrorKind.Validation
162162
)]
163163
do!
164-
let schemaId = schema.Introspected.GetHashCode()
164+
let schemaId = SchemaId.fromIntrospectionSchema schema.Introspected
165165
let key = { DocumentId = documentId; SchemaId = schemaId }
166166
let producer = fun () -> Validation.Ast.validateDocument schema.Introspected ast
167167
validationCache.GetOrAdd producer key

src/FSharp.Data.GraphQL.Shared/AstExtensions.fs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,22 @@ type Document with
104104
/// <param name="options">Specify custom printing voptions for the query string.</param>
105105
member x.ToQueryString ([<Optional; DefaultParameterValue (QueryStringPrintingOptions.None)>] options : QueryStringPrintingOptions) =
106106
let sb = PaddedStringBuilder ()
107-
let withQuotes (s : string) = "\"" + s + "\""
107+
let escapeGraphQLString (s : string) =
108+
let escaped = StringBuilder(s.Length + 2)
109+
escaped.Append('"') |> ignore
110+
for c in s do
111+
match c with
112+
| '"' -> escaped.Append("\\\"") |> ignore
113+
| '\\' -> escaped.Append("\\\\") |> ignore
114+
| '\b' -> escaped.Append("\\b") |> ignore
115+
| '\f' -> escaped.Append("\\f") |> ignore
116+
| '\n' -> escaped.Append("\\n") |> ignore
117+
| '\r' -> escaped.Append("\\r") |> ignore
118+
| '\t' -> escaped.Append("\\t") |> ignore
119+
| c when c < '\u0020' -> escaped.AppendFormat("\\u{0:x4}", int c) |> ignore
120+
| c -> escaped.Append(c) |> ignore
121+
escaped.Append('"').ToString()
122+
let withQuotes = escapeGraphQLString
108123
let rec printValue x =
109124
let printObjectValue (name, value) =
110125
sb.Append (name)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
module FSharp.Data.GraphQL.DocumentId
22

33
open System.Globalization
4+
open System.Runtime.CompilerServices
45
open System.Security.Cryptography
56
open System.Text
67

78
let private formatByteAsLowerHex (value : byte) =
89
value.ToString("x2", CultureInfo.InvariantCulture)
910

11+
/// <summary>
12+
/// Computes a deterministic document identifier from a canonical GraphQL query string.
13+
/// </summary>
14+
/// <param name="canonicalQuery">The canonical GraphQL query string (must already be properly escaped according to GraphQL specification).</param>
15+
/// <returns>A lowercase hexadecimal SHA-256 hash string that uniquely identifies the document content.</returns>
16+
[<CompiledName("FromCanonicalQuery")>]
1017
let fromCanonicalQuery (canonicalQuery : string) =
1118
let queryBytes = Encoding.UTF8.GetBytes canonicalQuery
1219
use sha256 = SHA256.Create()

src/FSharp.Data.GraphQL.Shared/ValidationResultCache.fs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
11
namespace FSharp.Data.GraphQL.Validation
22

33
open FSharp.Data.GraphQL
4+
open FSharp.Data.GraphQL.Types.Introspection
45
open System
6+
open System.Security.Cryptography
7+
open System.Text
8+
open System.Text.Json
59

610
type ValidationResultKey =
711
{ DocumentId : string
8-
SchemaId : int }
12+
SchemaId : string }
913

1014
type ValidationResultProducer =
1115
unit -> ValidationResult<GQLProblemDetails>
1216

1317
type IValidationResultCache =
1418
abstract GetOrAdd : ValidationResultProducer -> ValidationResultKey -> ValidationResult<GQLProblemDetails>
1519

20+
module SchemaId =
21+
let private formatByteAsLowerHex (value : byte) =
22+
value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture)
23+
24+
/// <summary>
25+
/// Computes a deterministic schema identifier from an introspection schema.
26+
/// </summary>
27+
/// <param name="introspectionSchema">The introspection schema to hash.</param>
28+
/// <returns>A lowercase hexadecimal SHA-256 hash string that uniquely identifies the schema structure.</returns>
29+
let fromIntrospectionSchema (introspectionSchema : IntrospectionSchema) =
30+
let options = JsonSerializerOptions()
31+
options.WriteIndented <- false
32+
options.DefaultIgnoreCondition <- System.Text.Json.Serialization.JsonIgnoreCondition.Never
33+
let json = JsonSerializer.Serialize(introspectionSchema, options)
34+
let jsonBytes = Encoding.UTF8.GetBytes json
35+
use sha256 = SHA256.Create()
36+
let hash = sha256.ComputeHash jsonBytes
37+
hash
38+
|> Seq.map formatByteAsLowerHex
39+
|> String.concat ""
1640

1741
/// An in-memory cache for the results of schema/document validations, with a lifetime of 30 seconds.
1842
type MemoryValidationResultCache () =

0 commit comments

Comments
 (0)