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