github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/middleware.go (about)

     1  /*
     2   * Copyright 2020 The Compass Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package operation
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	gqlgen "github.com/99designs/gqlgen/graphql"
    24  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    25  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    26  	"github.com/pkg/errors"
    27  	"github.com/tidwall/sjson"
    28  	"github.com/vektah/gqlparser/v2/ast"
    29  )
    30  
    31  // LocationsParam missing godoc
    32  const LocationsParam = "locations"
    33  
    34  type middleware struct {
    35  	directorURL string
    36  }
    37  
    38  // NewMiddleware creates a new handler struct responsible for enriching the response of Async mutations with Operation URL location information
    39  func NewMiddleware(directorURL string) *middleware {
    40  	return &middleware{
    41  		directorURL: directorURL,
    42  	}
    43  }
    44  
    45  // ExtensionName should be a CamelCase string version of the extension which may be shown in stats and logging.
    46  func (m middleware) ExtensionName() string {
    47  	return "OperationsExtension"
    48  }
    49  
    50  // Validate is called when adding an extension to the server, it allows validation against the servers schema.
    51  func (m middleware) Validate(_ gqlgen.ExecutableSchema) error {
    52  	return nil
    53  }
    54  
    55  // InterceptOperation saves an empty slice of async operations into the graphql operation context.
    56  func (m *middleware) InterceptOperation(ctx context.Context, next gqlgen.OperationHandler) gqlgen.ResponseHandler {
    57  	operations := make([]*Operation, 0)
    58  	ctx = SaveToContext(ctx, &operations)
    59  
    60  	return next(ctx)
    61  }
    62  
    63  // InterceptResponse enriches Async mutation responses with Operation URL location information and also empties the data property of the graphql response for such requests.
    64  func (m *middleware) InterceptResponse(ctx context.Context, next gqlgen.ResponseHandler) *gqlgen.Response {
    65  	resp := next(ctx)
    66  
    67  	operations, ok := FromCtx(ctx)
    68  	if !ok {
    69  		return resp
    70  	}
    71  
    72  	locations := make([]string, 0)
    73  	for _, operation := range *operations {
    74  		operationURL := fmt.Sprintf("%s/%s/%s", m.directorURL, operation.ResourceType, operation.ResourceID)
    75  		locations = append(locations, operationURL)
    76  	}
    77  
    78  	if len(locations) > 0 {
    79  		reqCtx := gqlgen.GetOperationContext(ctx)
    80  		gqlgen.RegisterExtension(ctx, LocationsParam, locations)
    81  		resp.Extensions = gqlgen.GetExtensions(ctx)
    82  
    83  		jsonPropsToDelete := make([]string, 0)
    84  		for _, gqlOperation := range reqCtx.Doc.Operations {
    85  			for _, gqlSelection := range gqlOperation.SelectionSet {
    86  				gqlField, ok := gqlSelection.(*ast.Field)
    87  				if !ok {
    88  					log.C(ctx).Errorf("Unable to prepare final response: gql field has unexpected type %T instead of *ast.Field", gqlSelection)
    89  					return gqlgen.ErrorResponse(ctx, "unable to prepare final response")
    90  				}
    91  
    92  				mutationAlias := gqlField.Alias
    93  				for _, gqlArgument := range gqlField.Arguments {
    94  					if gqlArgument.Name == ModeParam && gqlArgument.Value.Raw == string(graphql.OperationModeAsync) {
    95  						jsonPropsToDelete = append(jsonPropsToDelete, mutationAlias)
    96  					}
    97  				}
    98  			}
    99  		}
   100  
   101  		newData, err := cleanupFields(resp, jsonPropsToDelete)
   102  		if err != nil {
   103  			log.C(ctx).WithError(err).Errorf("Unable to process and delete unnecessary bytes from response body: %v", err)
   104  			return gqlgen.ErrorResponse(ctx, "failed to prepare response body")
   105  		}
   106  
   107  		resp.Data = newData
   108  	}
   109  
   110  	return resp
   111  }
   112  
   113  func cleanupFields(resp *gqlgen.Response, jsonPropsToDelete []string) ([]byte, error) {
   114  	bytes, err := resp.Data.MarshalJSON()
   115  	if err != nil {
   116  		return nil, errors.Wrap(err, "while marshalling current data body")
   117  	}
   118  
   119  	for _, prop := range jsonPropsToDelete {
   120  		var err error
   121  		bytes, err = sjson.DeleteBytes(bytes, prop)
   122  		if err != nil {
   123  			return nil, errors.Wrap(err, fmt.Sprintf("while removing property %s from data body", prop))
   124  		}
   125  	}
   126  
   127  	return bytes, nil
   128  }