-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraphql_validation_rules.go
More file actions
221 lines (189 loc) · 5.65 KB
/
Copy pathgraphql_validation_rules.go
File metadata and controls
221 lines (189 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package graph
import (
"fmt"
"net/http"
"strings"
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast"
)
// ValidationRule represents a single validation rule that can be applied to GraphQL queries
type ValidationRule interface {
// Name returns a unique identifier for this rule
Name() string
// Validate executes the rule against the parsed query
// Returns nil if valid, error if validation fails
Validate(ctx *ValidationContext) error
// Enabled checks if this rule should be executed
Enabled() bool
// Enable enables the rule
Enable()
// Disable disables the rule
Disable()
}
// BaseRule provides common functionality for all validation rules
// All custom rules should embed this struct
type BaseRule struct {
name string
enabled bool
}
// NewBaseRule creates a new base rule with the given name
func NewBaseRule(name string) BaseRule {
return BaseRule{
name: name,
enabled: true,
}
}
func (r *BaseRule) Name() string { return r.name }
func (r *BaseRule) Enabled() bool { return r.enabled }
func (r *BaseRule) Enable() { r.enabled = true }
func (r *BaseRule) Disable() { r.enabled = false }
// SetEnabled sets the enabled state
func (r *BaseRule) SetEnabled(enabled bool) {
r.enabled = enabled
}
// NewValidationError creates a validation error for this rule
func (r *BaseRule) NewError(message string) *ValidationError {
return &ValidationError{
Rule: r.name,
Message: message,
}
}
// NewErrorf creates a validation error with formatted message
func (r *BaseRule) NewErrorf(format string, args ...interface{}) *ValidationError {
return &ValidationError{
Rule: r.name,
Message: fmt.Sprintf(format, args...),
}
}
// ValidationContext provides all necessary information for validation
type ValidationContext struct {
// GraphQL query components
Query string
Document *ast.Document
Schema *graphql.Schema
Variables map[string]interface{}
// Request context
Request *http.Request
// User details from UserDetailsFn (can be nil if not authenticated)
// Validation rules can type-assert this to whatever structure they need
UserDetails interface{}
}
// ValidationError provides detailed error information
type ValidationError struct {
Rule string
Message string
Location *ast.Location
Path []string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("[%s] %s", e.Rule, e.Message)
}
// MultiValidationError combines multiple validation errors
type MultiValidationError struct {
Errors []error
}
func NewMultiValidationError(errors []error) *MultiValidationError {
return &MultiValidationError{Errors: errors}
}
func (e *MultiValidationError) Error() string {
if len(e.Errors) == 1 {
return e.Errors[0].Error()
}
var msgs []string
for _, err := range e.Errors {
msgs = append(msgs, err.Error())
}
return fmt.Sprintf("validation failed:\n - %s", strings.Join(msgs, "\n - "))
}
// ValidationOptions configures validation behavior
type ValidationOptions struct {
// StopOnFirstError stops validation after first error
StopOnFirstError bool
// SkipInDebug skips validation when DEBUG=true
SkipInDebug bool
// QueryCache, when non-nil, caches parsed ASTs keyed by query string so
// repeated requests with the same query skip the parser.
QueryCache *QueryASTCache
}
// ASTVisitor allows traversing the AST with hooks
type ASTVisitor struct {
EnterField func(field *ast.Field, ctx *ValidationContext) error
LeaveField func(field *ast.Field, ctx *ValidationContext) error
EnterOperation func(op *ast.OperationDefinition, ctx *ValidationContext) error
LeaveOperation func(op *ast.OperationDefinition, ctx *ValidationContext) error
EnterFragment func(frag *ast.FragmentDefinition, ctx *ValidationContext) error
LeaveFragment func(frag *ast.FragmentDefinition, ctx *ValidationContext) error
}
// traverseAST walks the AST and calls visitor hooks
func traverseAST(node ast.Node, visitor *ASTVisitor, ctx *ValidationContext) error {
switch n := node.(type) {
case *ast.Document:
for _, def := range n.Definitions {
if err := traverseAST(def, visitor, ctx); err != nil {
return err
}
}
case *ast.OperationDefinition:
if visitor.EnterOperation != nil {
if err := visitor.EnterOperation(n, ctx); err != nil {
return err
}
}
if n.SelectionSet != nil {
if err := traverseSelectionSet(n.SelectionSet, visitor, ctx); err != nil {
return err
}
}
if visitor.LeaveOperation != nil {
if err := visitor.LeaveOperation(n, ctx); err != nil {
return err
}
}
case *ast.FragmentDefinition:
if visitor.EnterFragment != nil {
if err := visitor.EnterFragment(n, ctx); err != nil {
return err
}
}
if n.SelectionSet != nil {
if err := traverseSelectionSet(n.SelectionSet, visitor, ctx); err != nil {
return err
}
}
if visitor.LeaveFragment != nil {
if err := visitor.LeaveFragment(n, ctx); err != nil {
return err
}
}
}
return nil
}
func traverseSelectionSet(selectionSet *ast.SelectionSet, visitor *ASTVisitor, ctx *ValidationContext) error {
for _, selection := range selectionSet.Selections {
switch sel := selection.(type) {
case *ast.Field:
if visitor.EnterField != nil {
if err := visitor.EnterField(sel, ctx); err != nil {
return err
}
}
if sel.SelectionSet != nil {
if err := traverseSelectionSet(sel.SelectionSet, visitor, ctx); err != nil {
return err
}
}
if visitor.LeaveField != nil {
if err := visitor.LeaveField(sel, ctx); err != nil {
return err
}
}
case *ast.InlineFragment:
if sel.SelectionSet != nil {
if err := traverseSelectionSet(sel.SelectionSet, visitor, ctx); err != nil {
return err
}
}
}
}
return nil
}