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