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  }