Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions v2/pkg/astnormalization/astnormalization.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ type OperationNormalizer struct {

removeOperationDefinitionsVisitor *removeOperationDefinitionsVisitor
inlineDeferVisitor *deferExpandIntoInternalVisitor
inlineArgumentsVisitor *inlineArgumentsVisitor

options options
definitionNormalizer *DefinitionNormalizer
Expand Down Expand Up @@ -155,6 +156,7 @@ type options struct {
ignoreSkipInclude bool
enableDefer bool
prevalidationRules []func(walker *astvisitor.Walker)
inlineArgumentsValidation *InlineArgumentsValidationOptions
}

type Option func(options *options)
Expand Down Expand Up @@ -213,6 +215,17 @@ func WithPrevalidationRules(rules ...func(walker *astvisitor.Walker)) Option {
}
}

// WithInlineArgumentsValidation enables detection of arguments whose values are
// supplied inline (as literals) instead of as variables. Findings are returned
// from NormalizeNamedOperationWithResult as a NormalizationResult. When opts.Enforce
// is set, normalization aborts on the first inline argument and surfaces the error
// via the report instead of collecting findings.
func WithInlineArgumentsValidation(opts InlineArgumentsValidationOptions) Option {
return func(options *options) {
options.inlineArgumentsValidation = &opts
}
}

func (o *OperationNormalizer) setupOperationWalkers() {
o.operationWalkers = make([]walkerStage, 0, 9)

Expand Down Expand Up @@ -240,6 +253,10 @@ func (o *OperationNormalizer) setupOperationWalkers() {
}
}

if o.options.inlineArgumentsValidation != nil {
o.inlineArgumentsVisitor = registerInlineArgumentsValidation(&directivesIncludeSkip, *o.options.inlineArgumentsValidation)
}

cleanup := astvisitor.NewWalkerWithID(8, "Cleanup")
deduplicateFields(&cleanup)
if o.options.enableDefer {
Expand Down Expand Up @@ -384,12 +401,25 @@ func (o *OperationNormalizer) NormalizeOperation(operation, definition *ast.Docu
}
}

// NormalizeNamedOperation applies all registered rules to one specific named operation in the AST
func (o *OperationNormalizer) NormalizeNamedOperation(operation, definition *ast.Document, operationName []byte, report *operationreport.Report) {
o.NormalizeNamedOperationWithResult(operation, definition, operationName, report, RunOptions{})
}

func (o *OperationNormalizer) NormalizeNamedOperationWithResult(
operation, definition *ast.Document,
operationName []byte,
report *operationreport.Report,
runOpts RunOptions,
Comment thread
SkArchon marked this conversation as resolved.
) *NormalizationResult {
if o.inlineArgumentsVisitor != nil {
o.inlineArgumentsVisitor.disabled = runOpts.SkipInlineArguments
o.inlineArgumentsVisitor.result.InlineArguments = o.inlineArgumentsVisitor.result.InlineArguments[:0]
}

if o.options.normalizeDefinition {
o.prepareDefinition(definition, report)
if report.HasErrors() {
return
return nil
}
}

Expand All @@ -403,7 +433,7 @@ func (o *OperationNormalizer) NormalizeNamedOperation(operation, definition *ast
}
o.operationWalkers[i].walker.Walk(operation, definition, report)
if report.HasErrors() {
return
return nil
}

// NOTE: debug code - do not remove
Expand All @@ -412,6 +442,11 @@ func (o *OperationNormalizer) NormalizeNamedOperation(operation, definition *ast
// fmt.Println(printed)
// fmt.Println("variables:", string(operation.Input.Variables))
}

if o.inlineArgumentsVisitor != nil {
return &o.inlineArgumentsVisitor.result
}
return nil
}

type VariablesNormalizer struct {
Expand Down
116 changes: 116 additions & 0 deletions v2/pkg/astnormalization/inline_arguments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package astnormalization

import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor"
"github.com/wundergraph/graphql-go-tools/v2/pkg/lexer/position"
"github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport"
)

// InlineArgument describes a single argument in an operation whose value was
// supplied inline (a literal) instead of as a variable.
type InlineArgument struct {
ArgumentName string
EnclosingName string
Comment thread
SkArchon marked this conversation as resolved.
Outdated
EnclosingKind ast.NodeKind
ValueKind ast.ValueKind
Position position.Position
}

func (a InlineArgument) QualifiedName() string {
switch a.EnclosingKind {
case ast.NodeKindField:
return a.EnclosingName + "." + a.ArgumentName
Comment thread
SkArchon marked this conversation as resolved.
Outdated
case ast.NodeKindDirective:
return "@" + a.EnclosingName + "." + a.ArgumentName
default:
return a.ArgumentName
}
}

type InlineArgumentsValidationOptions struct {
Enforce bool
ErrorMessage string
ErrorCode string
StatusCode int
}

// NormalizationResult carries per-run outputs of normalization beyond report errors.
type NormalizationResult struct {
InlineArguments []InlineArgument
}

// RunOptions are per-call inputs to a normalization run.
type RunOptions struct {
SkipInlineArguments bool
}

func registerInlineArgumentsValidation(walker *astvisitor.Walker, opts InlineArgumentsValidationOptions) *inlineArgumentsVisitor {
visitor := &inlineArgumentsVisitor{
Walker: walker,
opts: opts,
}
walker.RegisterEnterDocumentVisitor(visitor)
walker.RegisterEnterArgumentVisitor(visitor)
return visitor
}

type inlineArgumentsVisitor struct {
*astvisitor.Walker

operation, definition *ast.Document
opts InlineArgumentsValidationOptions

// disabled is set per run (see RunOptions.SkipInlineArguments) to exempt this
// operation from detection/enforcement.
disabled bool
// result accumulates the findings for the current run. Reset by the normalizer
// at the start of each run.
result NormalizationResult
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (v *inlineArgumentsVisitor) EnterDocument(operation, definition *ast.Document) {
v.operation = operation
v.definition = definition
}

func (v *inlineArgumentsVisitor) EnterArgument(ref int) {
if v.disabled {
return
}
valueKind := v.operation.Arguments[ref].Value.Kind
if valueKind == ast.ValueKindVariable {
return
}

if v.opts.Enforce {
// Reject on the first inline argument and stop the walk. A single generic
// error is enough to signal that the operation is non-compliant; we don't
// name the argument or point at its location.
v.StopWithExternalErr(operationreport.ExternalError{
Message: v.opts.ErrorMessage,
ExtensionCode: v.opts.ErrorCode,
StatusCode: v.opts.StatusCode,
})
return
}

finding := InlineArgument{
ArgumentName: v.operation.ArgumentNameString(ref),
ValueKind: valueKind,
Position: v.operation.Arguments[ref].Position,
}

if len(v.Ancestors) > 0 {
Comment thread
SkArchon marked this conversation as resolved.
Outdated
parent := v.Ancestors[len(v.Ancestors)-1]
finding.EnclosingKind = parent.Kind
switch parent.Kind {
case ast.NodeKindField:
finding.EnclosingName = v.operation.FieldNameString(parent.Ref)
case ast.NodeKindDirective:
finding.EnclosingName = v.operation.DirectiveNameString(parent.Ref)
}
}

v.result.InlineArguments = append(v.result.InlineArguments, finding)
}
Loading
Loading