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  }