github.com/99designs/gqlgen@v0.17.45/graphql/handler/extension/apq.go (about) 1 package extension 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/hex" 7 "fmt" 8 9 "github.com/mitchellh/mapstructure" 10 "github.com/vektah/gqlparser/v2/gqlerror" 11 12 "github.com/99designs/gqlgen/graphql" 13 "github.com/99designs/gqlgen/graphql/errcode" 14 ) 15 16 const ( 17 errPersistedQueryNotFound = "PersistedQueryNotFound" 18 errPersistedQueryNotFoundCode = "PERSISTED_QUERY_NOT_FOUND" 19 ) 20 21 // AutomaticPersistedQuery saves client upload by optimistically sending only the hashes of queries, if the server 22 // does not yet know what the query is for the hash it will respond telling the client to send the query along with the 23 // hash in the next request. 24 // see https://github.com/apollographql/apollo-link-persisted-queries 25 type AutomaticPersistedQuery struct { 26 Cache graphql.Cache 27 } 28 29 type ApqStats struct { 30 // The hash of the incoming query 31 Hash string 32 33 // SentQuery is true if the incoming request sent the full query 34 SentQuery bool 35 } 36 37 const apqExtension = "APQ" 38 39 var _ interface { 40 graphql.OperationParameterMutator 41 graphql.HandlerExtension 42 } = AutomaticPersistedQuery{} 43 44 func (a AutomaticPersistedQuery) ExtensionName() string { 45 return "AutomaticPersistedQuery" 46 } 47 48 func (a AutomaticPersistedQuery) Validate(schema graphql.ExecutableSchema) error { 49 if a.Cache == nil { 50 return fmt.Errorf("AutomaticPersistedQuery.Cache can not be nil") 51 } 52 return nil 53 } 54 55 func (a AutomaticPersistedQuery) MutateOperationParameters(ctx context.Context, rawParams *graphql.RawParams) *gqlerror.Error { 56 if rawParams.Extensions["persistedQuery"] == nil { 57 return nil 58 } 59 60 var extension struct { 61 Sha256 string `mapstructure:"sha256Hash"` 62 Version int64 `mapstructure:"version"` 63 } 64 65 if err := mapstructure.Decode(rawParams.Extensions["persistedQuery"], &extension); err != nil { 66 return gqlerror.Errorf("invalid APQ extension data") 67 } 68 69 if extension.Version != 1 { 70 return gqlerror.Errorf("unsupported APQ version") 71 } 72 73 fullQuery := false 74 if rawParams.Query == "" { 75 // client sent optimistic query hash without query string, get it from the cache 76 query, ok := a.Cache.Get(ctx, extension.Sha256) 77 if !ok { 78 err := gqlerror.Errorf(errPersistedQueryNotFound) 79 errcode.Set(err, errPersistedQueryNotFoundCode) 80 return err 81 } 82 rawParams.Query = query.(string) 83 } else { 84 // client sent optimistic query hash with query string, verify and store it 85 if computeQueryHash(rawParams.Query) != extension.Sha256 { 86 return gqlerror.Errorf("provided APQ hash does not match query") 87 } 88 a.Cache.Add(ctx, extension.Sha256, rawParams.Query) 89 fullQuery = true 90 } 91 92 graphql.GetOperationContext(ctx).Stats.SetExtension(apqExtension, &ApqStats{ 93 Hash: extension.Sha256, 94 SentQuery: fullQuery, 95 }) 96 97 return nil 98 } 99 100 func GetApqStats(ctx context.Context) *ApqStats { 101 rc := graphql.GetOperationContext(ctx) 102 if rc == nil { 103 return nil 104 } 105 106 s, _ := rc.Stats.GetExtension(apqExtension).(*ApqStats) 107 return s 108 } 109 110 func computeQueryHash(query string) string { 111 b := sha256.Sum256([]byte(query)) 112 return hex.EncodeToString(b[:]) 113 }