Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
package core
import (
"github.com/vektah/gqlparser/v2/gqlerror"
)
type AddErrFunc func(options ...ErrorOption)
type RuleFunc func(observers *Events, addError AddErrFunc)
type Rule struct {
Name string
RuleFunc RuleFunc
}
// NameSorter sorts Rules by name.
// usage: sort.Sort(core.NameSorter(specifiedRules))
type NameSorter []Rule
func (a NameSorter) Len() int { return len(a) }
func (a NameSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a NameSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }
type ErrorOption func(err *gqlerror.Error)
+151
View File
@@ -0,0 +1,151 @@
package core
import (
"bytes"
"fmt"
"math"
"sort"
"strings"
"github.com/agnivade/levenshtein"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func Message(msg string, args ...any) ErrorOption {
return func(err *gqlerror.Error) {
err.Message += fmt.Sprintf(msg, args...)
}
}
func At(position *ast.Position) ErrorOption {
return func(err *gqlerror.Error) {
if position == nil {
return
}
err.Locations = append(err.Locations, gqlerror.Location{
Line: position.Line,
Column: position.Column,
})
if position.Src.Name != "" {
err.SetFile(position.Src.Name)
}
}
}
func SuggestListQuoted(prefix, typed string, suggestions []string) ErrorOption {
suggested := SuggestionList(typed, suggestions)
return func(err *gqlerror.Error) {
if len(suggested) > 0 {
err.Message += " " + prefix + " " + QuotedOrList(suggested...) + "?"
}
}
}
func SuggestListUnquoted(prefix, typed string, suggestions []string) ErrorOption {
suggested := SuggestionList(typed, suggestions)
return func(err *gqlerror.Error) {
if len(suggested) > 0 {
err.Message += " " + prefix + " " + OrList(suggested...) + "?"
}
}
}
func Suggestf(suggestion string, args ...any) ErrorOption {
return func(err *gqlerror.Error) {
err.Message += " Did you mean " + fmt.Sprintf(suggestion, args...) + "?"
}
}
// Given [ A, B, C ] return '"A", "B", or "C"'.
func QuotedOrList(items ...string) string {
itemsQuoted := make([]string, len(items))
for i, item := range items {
itemsQuoted[i] = `"` + item + `"`
}
return OrList(itemsQuoted...)
}
// Given [ A, B, C ] return 'A, B, or C'.
func OrList(items ...string) string {
var buf bytes.Buffer
if len(items) > 5 {
items = items[:5]
}
if len(items) == 2 {
buf.WriteString(items[0])
buf.WriteString(" or ")
buf.WriteString(items[1])
return buf.String()
}
for i, item := range items {
if i != 0 {
if i == len(items)-1 {
buf.WriteString(", or ")
} else {
buf.WriteString(", ")
}
}
buf.WriteString(item)
}
return buf.String()
}
// Given an invalid input string and a list of valid options, returns a filtered
// list of valid options sorted based on their similarity with the input.
func SuggestionList(input string, options []string) []string {
var results []string
optionsByDistance := map[string]int{}
for _, option := range options {
distance := lexicalDistance(input, option)
threshold := calcThreshold(input)
if distance <= threshold {
results = append(results, option)
optionsByDistance[option] = distance
}
}
sort.Slice(results, func(i, j int) bool {
return optionsByDistance[results[i]] < optionsByDistance[results[j]]
})
return results
}
func calcThreshold(a string) (threshold int) {
// the logic is copied from here
// https://github.com/graphql/graphql-js/blob/47bd8c8897c72d3efc17ecb1599a95cee6bac5e8/src/jsutils/suggestionList.ts#L14
threshold = max(int(math.Floor(float64(len(a))*0.4)+1), 1)
return threshold
}
// Computes the lexical distance between strings A and B.
//
// The "distance" between two strings is given by counting the minimum number
// of edits needed to transform string A into string B. An edit can be an
// insertion, deletion, or substitution of a single character, or a swap of two
// adjacent characters.
//
// Includes a custom alteration from Damerau-Levenshtein to treat case changes
// as a single edit which helps identify mis-cased values with an edit distance
// of 1.
//
// This distance can be useful for detecting typos in input or sorting.
func lexicalDistance(a, b string) int {
if a == b {
return 0
}
a = strings.ToLower(a)
b = strings.ToLower(b)
// Any case change counts as a single edit
if a == b {
return 1
}
return levenshtein.ComputeDistance(a, b)
}
+308
View File
@@ -0,0 +1,308 @@
package core
import (
"context"
"fmt"
"github.com/vektah/gqlparser/v2/ast"
)
type Events struct {
operationVisitor []func(walker *Walker, operation *ast.OperationDefinition)
field []func(walker *Walker, field *ast.Field)
fragment []func(walker *Walker, fragment *ast.FragmentDefinition)
inlineFragment []func(walker *Walker, inlineFragment *ast.InlineFragment)
fragmentSpread []func(walker *Walker, fragmentSpread *ast.FragmentSpread)
directive []func(walker *Walker, directive *ast.Directive)
directiveList []func(walker *Walker, directives []*ast.Directive)
value []func(walker *Walker, value *ast.Value)
variable []func(walker *Walker, variable *ast.VariableDefinition)
}
func (o *Events) OnOperation(f func(walker *Walker, operation *ast.OperationDefinition)) {
o.operationVisitor = append(o.operationVisitor, f)
}
func (o *Events) OnField(f func(walker *Walker, field *ast.Field)) {
o.field = append(o.field, f)
}
func (o *Events) OnFragment(f func(walker *Walker, fragment *ast.FragmentDefinition)) {
o.fragment = append(o.fragment, f)
}
func (o *Events) OnInlineFragment(f func(walker *Walker, inlineFragment *ast.InlineFragment)) {
o.inlineFragment = append(o.inlineFragment, f)
}
func (o *Events) OnFragmentSpread(f func(walker *Walker, fragmentSpread *ast.FragmentSpread)) {
o.fragmentSpread = append(o.fragmentSpread, f)
}
func (o *Events) OnDirective(f func(walker *Walker, directive *ast.Directive)) {
o.directive = append(o.directive, f)
}
func (o *Events) OnDirectiveList(f func(walker *Walker, directives []*ast.Directive)) {
o.directiveList = append(o.directiveList, f)
}
func (o *Events) OnValue(f func(walker *Walker, value *ast.Value)) {
o.value = append(o.value, f)
}
func (o *Events) OnVariable(f func(walker *Walker, variable *ast.VariableDefinition)) {
o.variable = append(o.variable, f)
}
func Walk(schema *ast.Schema, document *ast.QueryDocument, observers *Events) {
w := Walker{
Observers: observers,
Schema: schema,
Document: document,
}
w.walk()
}
type Walker struct {
Context context.Context
Observers *Events
Schema *ast.Schema
Document *ast.QueryDocument
validatedFragmentSpreads map[string]bool
CurrentOperation *ast.OperationDefinition
}
func (w *Walker) walk() {
for _, child := range w.Document.Operations {
w.validatedFragmentSpreads = make(map[string]bool)
w.walkOperation(child)
}
for _, child := range w.Document.Fragments {
w.validatedFragmentSpreads = make(map[string]bool)
w.walkFragment(child)
}
}
func (w *Walker) walkOperation(operation *ast.OperationDefinition) {
w.CurrentOperation = operation
for _, varDef := range operation.VariableDefinitions {
varDef.Definition = w.Schema.Types[varDef.Type.Name()]
for _, v := range w.Observers.variable {
v(w, varDef)
}
if varDef.DefaultValue != nil {
varDef.DefaultValue.ExpectedType = varDef.Type
varDef.DefaultValue.Definition = w.Schema.Types[varDef.Type.Name()]
}
}
var def *ast.Definition
var loc ast.DirectiveLocation
switch operation.Operation {
case ast.Query, "":
def = w.Schema.Query
loc = ast.LocationQuery
case ast.Mutation:
def = w.Schema.Mutation
loc = ast.LocationMutation
case ast.Subscription:
def = w.Schema.Subscription
loc = ast.LocationSubscription
}
for _, varDef := range operation.VariableDefinitions {
if varDef.DefaultValue != nil {
w.walkValue(varDef.DefaultValue)
}
w.walkDirectives(varDef.Definition, varDef.Directives, ast.LocationVariableDefinition)
}
w.walkDirectives(def, operation.Directives, loc)
w.walkSelectionSet(def, operation.SelectionSet)
for _, v := range w.Observers.operationVisitor {
v(w, operation)
}
w.CurrentOperation = nil
}
func (w *Walker) walkFragment(it *ast.FragmentDefinition) {
def := w.Schema.Types[it.TypeCondition]
it.Definition = def
w.walkDirectives(def, it.Directives, ast.LocationFragmentDefinition)
w.walkSelectionSet(def, it.SelectionSet)
for _, v := range w.Observers.fragment {
v(w, it)
}
}
func (w *Walker) walkDirectives(
parentDef *ast.Definition,
directives []*ast.Directive,
location ast.DirectiveLocation,
) {
for _, dir := range directives {
def := w.Schema.Directives[dir.Name]
dir.Definition = def
dir.ParentDefinition = parentDef
dir.Location = location
for _, arg := range dir.Arguments {
var argDef *ast.ArgumentDefinition
if def != nil {
argDef = def.Arguments.ForName(arg.Name)
}
w.walkArgument(argDef, arg)
}
for _, v := range w.Observers.directive {
v(w, dir)
}
}
for _, v := range w.Observers.directiveList {
v(w, directives)
}
}
func (w *Walker) walkValue(value *ast.Value) {
if value.Kind == ast.Variable && w.CurrentOperation != nil {
value.VariableDefinition = w.CurrentOperation.VariableDefinitions.ForName(value.Raw)
if value.VariableDefinition != nil {
value.VariableDefinition.Used = true
}
}
if value.Kind == ast.ObjectValue {
for _, child := range value.Children {
if value.Definition != nil {
fieldDef := value.Definition.Fields.ForName(child.Name)
if fieldDef != nil {
child.Value.ExpectedType = fieldDef.Type
child.Value.ExpectedTypeHasDefault = fieldDef.DefaultValue != nil &&
fieldDef.DefaultValue.Kind != ast.NullValue
child.Value.Definition = w.Schema.Types[fieldDef.Type.Name()]
}
}
w.walkValue(child.Value)
}
}
if value.Kind == ast.ListValue {
for _, child := range value.Children {
if value.ExpectedType != nil && value.ExpectedType.Elem != nil {
child.Value.ExpectedType = value.ExpectedType.Elem
child.Value.Definition = value.Definition
}
w.walkValue(child.Value)
}
}
for _, v := range w.Observers.value {
v(w, value)
}
}
func (w *Walker) walkArgument(argDef *ast.ArgumentDefinition, arg *ast.Argument) {
if argDef != nil {
arg.Value.ExpectedType = argDef.Type
arg.Value.ExpectedTypeHasDefault = argDef.DefaultValue != nil &&
argDef.DefaultValue.Kind != ast.NullValue
arg.Value.Definition = w.Schema.Types[argDef.Type.Name()]
}
w.walkValue(arg.Value)
}
func (w *Walker) walkSelectionSet(parentDef *ast.Definition, it ast.SelectionSet) {
for _, child := range it {
w.walkSelection(parentDef, child)
}
}
func (w *Walker) walkSelection(parentDef *ast.Definition, it ast.Selection) {
switch it := it.(type) {
case *ast.Field:
var def *ast.FieldDefinition
if it.Name == "__typename" {
def = &ast.FieldDefinition{
Name: "__typename",
Type: ast.NamedType("String", nil),
}
} else if parentDef != nil {
def = parentDef.Fields.ForName(it.Name)
}
it.Definition = def
it.ObjectDefinition = parentDef
var nextParentDef *ast.Definition
if def != nil {
nextParentDef = w.Schema.Types[def.Type.Name()]
}
for _, arg := range it.Arguments {
var argDef *ast.ArgumentDefinition
if def != nil {
argDef = def.Arguments.ForName(arg.Name)
}
w.walkArgument(argDef, arg)
}
w.walkDirectives(nextParentDef, it.Directives, ast.LocationField)
w.walkSelectionSet(nextParentDef, it.SelectionSet)
for _, v := range w.Observers.field {
v(w, it)
}
case *ast.InlineFragment:
it.ObjectDefinition = parentDef
nextParentDef := parentDef
if it.TypeCondition != "" {
nextParentDef = w.Schema.Types[it.TypeCondition]
}
w.walkDirectives(nextParentDef, it.Directives, ast.LocationInlineFragment)
w.walkSelectionSet(nextParentDef, it.SelectionSet)
for _, v := range w.Observers.inlineFragment {
v(w, it)
}
case *ast.FragmentSpread:
def := w.Document.Fragments.ForName(it.Name)
it.Definition = def
it.ObjectDefinition = parentDef
var nextParentDef *ast.Definition
if def != nil {
nextParentDef = w.Schema.Types[def.TypeCondition]
}
w.walkDirectives(nextParentDef, it.Directives, ast.LocationFragmentSpread)
if def != nil && !w.validatedFragmentSpreads[def.Name] {
// prevent infinite recursion
w.validatedFragmentSpreads[def.Name] = true
w.walkSelectionSet(nextParentDef, def.SelectionSet)
}
for _, v := range w.Observers.fragmentSpread {
v(w, it)
}
default:
panic(fmt.Errorf("unsupported %T", it))
}
}
@@ -0,0 +1,250 @@
# This file defines all the implicitly declared types that are required by the graphql spec. It is implicitly included by calls to LoadSchema
"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
scalar Int
"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point)."
scalar Float
"The `String`scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
scalar String
"The `Boolean` scalar type represents `true` or `false`."
scalar Boolean
"""The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID."""
scalar ID
"Directs the executor to defer this fragment when the `if` argument is true or undefined."
directive @defer(
"Deferred when true or undefined."
if: Boolean = true,
"Unique name"
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to include this field or fragment only when the `if` argument is true.
"""
directive @include(
"""Included when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to skip this field or fragment when the `if` argument is true.
"""
directive @skip(
"""Skipped when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""Marks an element of a GraphQL schema as no longer supported."""
directive @deprecated(
"""
Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).
"""
reason: String = "No longer supported"
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
"""Exposes a URL that specifies the behavior of this scalar."""
directive @specifiedBy(
"""The URL that specifies the behavior of this scalar."""
url: String!
) on SCALAR
"""
Indicates exactly one field must be supplied and this field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT
"""
A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.
"""
type __Schema {
description: String
"""A list of all types supported by this server."""
types: [__Type!]!
"""The type that query operations will be rooted at."""
queryType: __Type!
"""
If this server supports mutation, the type that mutation operations will be rooted at.
"""
mutationType: __Type
"""
If this server support subscription, the type that subscription operations will be rooted at.
"""
subscriptionType: __Type
"""A list of all directives supported by this server."""
directives: [__Directive!]!
}
"""
The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.
Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.
"""
type __Type {
kind: __TypeKind!
name: String
description: String
specifiedByURL: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
inputFields(includeDeprecated: Boolean = false): [__InputValue!]
ofType: __Type
isOneOf: Boolean
}
"""An enum describing what kind of type a given `__Type` is."""
enum __TypeKind {
"""Indicates this type is a scalar."""
SCALAR
"""
Indicates this type is an object. `fields` and `interfaces` are valid fields.
"""
OBJECT
"""
Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.
"""
INTERFACE
"""Indicates this type is a union. `possibleTypes` is a valid field."""
UNION
"""Indicates this type is an enum. `enumValues` is a valid field."""
ENUM
"""
Indicates this type is an input object. `inputFields` is a valid field.
"""
INPUT_OBJECT
"""Indicates this type is a list. `ofType` is a valid field."""
LIST
"""Indicates this type is a non-null. `ofType` is a valid field."""
NON_NULL
}
"""
Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.
"""
type __Field {
name: String!
description: String
args(includeDeprecated: Boolean = false): [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
"""
Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.
"""
type __InputValue {
name: String!
description: String
type: __Type!
"""
A GraphQL-formatted string representing the default value for this input value.
"""
defaultValue: String
isDeprecated: Boolean!
deprecationReason: String
}
"""
One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.
"""
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
"""
A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.
"""
type __Directive {
name: String!
description: String
isRepeatable: Boolean!
locations: [__DirectiveLocation!]!
args(includeDeprecated: Boolean = false): [__InputValue!]!
}
"""
A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.
"""
enum __DirectiveLocation {
"""Location adjacent to a query operation."""
QUERY
"""Location adjacent to a mutation operation."""
MUTATION
"""Location adjacent to a subscription operation."""
SUBSCRIPTION
"""Location adjacent to a field."""
FIELD
"""Location adjacent to a fragment definition."""
FRAGMENT_DEFINITION
"""Location adjacent to a fragment spread."""
FRAGMENT_SPREAD
"""Location adjacent to an inline fragment."""
INLINE_FRAGMENT
"""Location adjacent to a variable definition."""
VARIABLE_DEFINITION
"""Location adjacent to a schema definition."""
SCHEMA
"""Location adjacent to a scalar definition."""
SCALAR
"""Location adjacent to an object type definition."""
OBJECT
"""Location adjacent to a field definition."""
FIELD_DEFINITION
"""Location adjacent to an argument definition."""
ARGUMENT_DEFINITION
"""Location adjacent to an interface definition."""
INTERFACE
"""Location adjacent to a union definition."""
UNION
"""Location adjacent to an enum definition."""
ENUM
"""Location adjacent to an enum value definition."""
ENUM_VALUE
"""Location adjacent to an input object type definition."""
INPUT_OBJECT
"""Location adjacent to an input object field definition."""
INPUT_FIELD_DEFINITION
}
+16
View File
@@ -0,0 +1,16 @@
package validator
import (
_ "embed"
"github.com/vektah/gqlparser/v2/ast"
)
//go:embed imported/prelude.graphql
var preludeGraphql string
var Prelude = &ast.Source{
Name: "prelude.graphql",
Input: preludeGraphql,
BuiltIn: true,
}
@@ -0,0 +1,131 @@
package rules
import (
"fmt"
"sort"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncFieldsOnCorrectType(observers *Events, addError AddErrFunc, disableSuggestion bool) {
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.ObjectDefinition == nil || field.Definition != nil {
return
}
message := fmt.Sprintf(
`Cannot query field "%s" on type "%s".`,
field.Name,
field.ObjectDefinition.Name,
)
if !disableSuggestion {
if suggestedTypeNames := getSuggestedTypeNames(
walker,
field.ObjectDefinition,
field.Name,
); suggestedTypeNames != nil {
message += " Did you mean to use an inline fragment on " + QuotedOrList(
suggestedTypeNames...) + "?"
} else if suggestedFieldNames := getSuggestedFieldNames(
field.ObjectDefinition,
field.Name,
); suggestedFieldNames != nil {
message += " Did you mean " + QuotedOrList(suggestedFieldNames...) + "?"
}
}
addError(
Message("%s", message),
At(field.Position),
)
})
}
var FieldsOnCorrectTypeRule = Rule{
Name: "FieldsOnCorrectType",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncFieldsOnCorrectType(observers, addError, false)
},
}
var FieldsOnCorrectTypeRuleWithoutSuggestions = Rule{
Name: "FieldsOnCorrectTypeWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncFieldsOnCorrectType(observers, addError, true)
},
}
// Go through all the implementations of type, as well as the interfaces
// that they implement. If any of those types include the provided field,
// suggest them, sorted by how often the type is referenced, starting
// with Interfaces.
func getSuggestedTypeNames(walker *Walker, parent *ast.Definition, name string) []string {
if !parent.IsAbstractType() {
return nil
}
possibleTypes := walker.Schema.GetPossibleTypes(parent)
suggestedObjectTypes := make([]string, 0, len(possibleTypes))
var suggestedInterfaceTypes []string
interfaceUsageCount := map[string]int{}
for _, possibleType := range possibleTypes {
field := possibleType.Fields.ForName(name)
if field == nil {
continue
}
suggestedObjectTypes = append(suggestedObjectTypes, possibleType.Name)
for _, possibleInterface := range possibleType.Interfaces {
interfaceField := walker.Schema.Types[possibleInterface]
if interfaceField != nil && interfaceField.Fields.ForName(name) != nil {
if interfaceUsageCount[possibleInterface] == 0 {
suggestedInterfaceTypes = append(suggestedInterfaceTypes, possibleInterface)
}
interfaceUsageCount[possibleInterface]++
}
}
}
suggestedTypes := concatSlice(suggestedInterfaceTypes, suggestedObjectTypes)
sort.SliceStable(suggestedTypes, func(i, j int) bool {
typeA, typeB := suggestedTypes[i], suggestedTypes[j]
diff := interfaceUsageCount[typeB] - interfaceUsageCount[typeA]
if diff != 0 {
return diff < 0
}
return typeA < typeB
})
return suggestedTypes
}
// By employing a full slice expression (slice[low:high:max]),
// where max is set to the slices length,
// we ensure that appending elements results
// in a slice backed by a distinct array.
// This method prevents the shared array issue.
func concatSlice(first, second []string) []string {
n := len(first)
return append(first[:n:n], second...)
}
// For the field name provided, determine if there are any similar field names
// that may be the result of a typo.
func getSuggestedFieldNames(parent *ast.Definition, name string) []string {
if parent.Kind != ast.Object && parent.Kind != ast.Interface {
return nil
}
possibleFieldNames := make([]string, 0, len(parent.Fields))
for _, field := range parent.Fields {
possibleFieldNames = append(possibleFieldNames, field.Name)
}
return SuggestionList(name, possibleFieldNames)
}
@@ -0,0 +1,49 @@
package rules
import (
"fmt"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var FragmentsOnCompositeTypesRule = Rule{
Name: "FragmentsOnCompositeTypes",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
fragmentType := walker.Schema.Types[inlineFragment.TypeCondition]
if fragmentType == nil || fragmentType.IsCompositeType() {
return
}
message := fmt.Sprintf(
`Fragment cannot condition on non composite type "%s".`,
inlineFragment.TypeCondition,
)
addError(
Message("%s", message),
At(inlineFragment.Position),
)
})
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
if fragment.Definition == nil || fragment.TypeCondition == "" ||
fragment.Definition.IsCompositeType() {
return
}
message := fmt.Sprintf(
`Fragment "%s" cannot condition on non composite type "%s".`,
fragment.Name,
fragment.TypeCondition,
)
addError(
Message("%s", message),
At(fragment.Position),
)
})
},
}
@@ -0,0 +1,93 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncKnownArgumentNames(observers *Events, addError AddErrFunc, disableSuggestion bool) {
// A GraphQL field is only valid if all supplied arguments are defined by that field.
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.Definition == nil || field.ObjectDefinition == nil {
return
}
for _, arg := range field.Arguments {
def := field.Definition.Arguments.ForName(arg.Name)
if def != nil {
continue
}
if disableSuggestion {
addError(
Message(
`Unknown argument "%s" on field "%s.%s".`,
arg.Name,
field.ObjectDefinition.Name,
field.Name,
),
At(field.Position),
)
} else {
var suggestions []string
for _, argDef := range field.Definition.Arguments {
suggestions = append(suggestions, argDef.Name)
}
addError(
Message(
`Unknown argument "%s" on field "%s.%s".`,
arg.Name,
field.ObjectDefinition.Name,
field.Name,
),
SuggestListQuoted("Did you mean", arg.Name, suggestions),
At(field.Position),
)
}
}
})
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
if directive.Definition == nil {
return
}
for _, arg := range directive.Arguments {
def := directive.Definition.Arguments.ForName(arg.Name)
if def != nil {
continue
}
if disableSuggestion {
addError(
Message(`Unknown argument "%s" on directive "@%s".`, arg.Name, directive.Name),
At(directive.Position),
)
} else {
var suggestions []string
for _, argDef := range directive.Definition.Arguments {
suggestions = append(suggestions, argDef.Name)
}
addError(
Message(`Unknown argument "%s" on directive "@%s".`, arg.Name, directive.Name),
SuggestListQuoted("Did you mean", arg.Name, suggestions),
At(directive.Position),
)
}
}
})
}
var KnownArgumentNamesRule = Rule{
Name: "KnownArgumentNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownArgumentNames(observers, addError, false)
},
}
var KnownArgumentNamesRuleWithoutSuggestions = Rule{
Name: "KnownArgumentNamesWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownArgumentNames(observers, addError, true)
},
}
@@ -0,0 +1,53 @@
package rules
import (
"slices"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownDirectivesRule = Rule{
Name: "KnownDirectives",
RuleFunc: func(observers *Events, addError AddErrFunc) {
type mayNotBeUsedDirective struct {
Name string
Line int
Column int
}
seen := map[mayNotBeUsedDirective]bool{}
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
if directive.Definition == nil {
addError(
Message(`Unknown directive "@%s".`, directive.Name),
At(directive.Position),
)
return
}
if slices.Contains(directive.Definition.Locations, directive.Location) {
return
}
// position must be exists if directive.Definition != nil
tmp := mayNotBeUsedDirective{
Name: directive.Name,
Line: directive.Position.Line,
Column: directive.Position.Column,
}
if !seen[tmp] {
addError(
Message(
`Directive "@%s" may not be used on %s.`,
directive.Name,
directive.Location,
),
At(directive.Position),
)
seen[tmp] = true
}
})
},
}
@@ -0,0 +1,21 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownFragmentNamesRule = Rule{
Name: "KnownFragmentNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
if fragmentSpread.Definition == nil {
addError(
Message(`Unknown fragment "%s".`, fragmentSpread.Name),
At(fragmentSpread.Position),
)
}
})
},
}
@@ -0,0 +1,37 @@
package rules
import (
"fmt"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var KnownRootTypeRule = Rule{
Name: "KnownRootType",
RuleFunc: func(observers *Events, addError AddErrFunc) {
// A query's root must be a valid type. Surprisingly, this isn't
// checked anywhere else!
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
var def *ast.Definition
switch operation.Operation {
case ast.Query, "":
def = walker.Schema.Query
case ast.Mutation:
def = walker.Schema.Mutation
case ast.Subscription:
def = walker.Schema.Subscription
default:
// This shouldn't even parse; if it did we probably need to
// update this switch block to add the new operation type.
panic(fmt.Sprintf(`got unknown operation type "%s"`, operation.Operation))
}
if def == nil {
addError(
Message(`Schema does not support operation type "%s"`, operation.Operation),
At(operation.Position))
}
})
},
}
@@ -0,0 +1,79 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncKnownTypeNames(observers *Events, addError AddErrFunc, disableSuggestion bool) {
observers.OnVariable(func(walker *Walker, variable *ast.VariableDefinition) {
typeName := variable.Type.Name()
typdef := walker.Schema.Types[typeName]
if typdef != nil {
return
}
addError(
Message(`Unknown type "%s".`, typeName),
At(variable.Position),
)
})
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
typedName := inlineFragment.TypeCondition
if typedName == "" {
return
}
def := walker.Schema.Types[typedName]
if def != nil {
return
}
addError(
Message(`Unknown type "%s".`, typedName),
At(inlineFragment.Position),
)
})
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
typeName := fragment.TypeCondition
def := walker.Schema.Types[typeName]
if def != nil {
return
}
if disableSuggestion {
addError(
Message(`Unknown type "%s".`, typeName),
At(fragment.Position),
)
} else {
var possibleTypes []string
for _, t := range walker.Schema.Types {
possibleTypes = append(possibleTypes, t.Name)
}
addError(
Message(`Unknown type "%s".`, typeName),
SuggestListQuoted("Did you mean", typeName, possibleTypes),
At(fragment.Position),
)
}
})
}
var KnownTypeNamesRule = Rule{
Name: "KnownTypeNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownTypeNames(observers, addError, false)
},
}
var KnownTypeNamesRuleWithoutSuggestions = Rule{
Name: "KnownTypeNamesWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncKnownTypeNames(observers, addError, true)
},
}
@@ -0,0 +1,21 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var LoneAnonymousOperationRule = Rule{
Name: "LoneAnonymousOperation",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
if operation.Name == "" && len(walker.Document.Operations) > 1 {
addError(
Message(`This anonymous operation must be the only defined operation.`),
At(operation.Position),
)
}
})
},
}
@@ -0,0 +1,93 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
const maxListsDepth = 3
var MaxIntrospectionDepth = Rule{
Name: "MaxIntrospectionDepth",
RuleFunc: func(observers *Events, addError AddErrFunc) {
// Counts the depth of list fields in "__Type" recursively and
// returns `true` if the limit has been reached.
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.Name == "__schema" || field.Name == "__type" {
visitedFragments := make(map[string]bool)
if checkDepthField(field, visitedFragments, 0) {
addError(
Message(`Maximum introspection depth exceeded`),
At(field.Position),
)
}
return
}
})
},
}
func checkDepthSelectionSet(
selectionSet ast.SelectionSet,
visitedFragments map[string]bool,
depth int,
) bool {
for _, child := range selectionSet {
if field, ok := child.(*ast.Field); ok {
if checkDepthField(field, visitedFragments, depth) {
return true
}
}
if fragmentSpread, ok := child.(*ast.FragmentSpread); ok {
if checkDepthFragmentSpread(fragmentSpread, visitedFragments, depth) {
return true
}
}
if inlineFragment, ok := child.(*ast.InlineFragment); ok {
if checkDepthSelectionSet(inlineFragment.SelectionSet, visitedFragments, depth) {
return true
}
}
}
return false
}
func checkDepthField(field *ast.Field, visitedFragments map[string]bool, depth int) bool {
if field.Name == "fields" ||
field.Name == "interfaces" ||
field.Name == "possibleTypes" ||
field.Name == "inputFields" {
depth++
if depth >= maxListsDepth {
return true
}
}
return checkDepthSelectionSet(field.SelectionSet, visitedFragments, depth)
}
func checkDepthFragmentSpread(
fragmentSpread *ast.FragmentSpread,
visitedFragments map[string]bool,
depth int,
) bool {
fragmentName := fragmentSpread.Name
if visited, ok := visitedFragments[fragmentName]; ok && visited {
// Fragment cycles are handled by `NoFragmentCyclesRule`.
return false
}
fragment := fragmentSpread.Definition
if fragment == nil {
// Missing fragments checks are handled by `KnownFragmentNamesRule`.
return false
}
// Rather than following an immutable programming pattern which has
// significant memory and garbage collection overhead, we've opted to
// take a mutable approach for efficiency's sake. Importantly visiting a
// fragment twice is fine, so long as you don't do one visit inside the
// other.
visitedFragments[fragmentName] = true
defer delete(visitedFragments, fragmentName)
return checkDepthSelectionSet(fragment.SelectionSet, visitedFragments, depth)
}
@@ -0,0 +1,99 @@
package rules
import (
"fmt"
"strings"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoFragmentCyclesRule = Rule{
Name: "NoFragmentCycles",
RuleFunc: func(observers *Events, addError AddErrFunc) {
visitedFrags := make(map[string]bool)
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
var spreadPath []*ast.FragmentSpread
spreadPathIndexByName := make(map[string]int)
var recursive func(fragment *ast.FragmentDefinition)
recursive = func(fragment *ast.FragmentDefinition) {
if visitedFrags[fragment.Name] {
return
}
visitedFrags[fragment.Name] = true
spreadNodes := getFragmentSpreads(fragment.SelectionSet)
if len(spreadNodes) == 0 {
return
}
spreadPathIndexByName[fragment.Name] = len(spreadPath)
for _, spreadNode := range spreadNodes {
spreadName := spreadNode.Name
cycleIndex, ok := spreadPathIndexByName[spreadName]
spreadPath = append(spreadPath, spreadNode)
if !ok {
spreadFragment := walker.Document.Fragments.ForName(spreadName)
if spreadFragment != nil {
recursive(spreadFragment)
}
} else {
cyclePath := spreadPath[cycleIndex : len(spreadPath)-1]
var fragmentNames []string
for _, fs := range cyclePath {
fragmentNames = append(fragmentNames, fmt.Sprintf(`"%s"`, fs.Name))
}
var via string
if len(fragmentNames) != 0 {
via = fmt.Sprintf(" via %s", strings.Join(fragmentNames, ", "))
}
addError(
Message(
`Cannot spread fragment "%s" within itself%s.`,
spreadName,
via,
),
At(spreadNode.Position),
)
}
spreadPath = spreadPath[:len(spreadPath)-1]
}
delete(spreadPathIndexByName, fragment.Name)
}
recursive(fragment)
})
},
}
func getFragmentSpreads(node ast.SelectionSet) []*ast.FragmentSpread {
var spreads []*ast.FragmentSpread
setsToVisit := []ast.SelectionSet{node}
for len(setsToVisit) != 0 {
set := setsToVisit[len(setsToVisit)-1]
setsToVisit = setsToVisit[:len(setsToVisit)-1]
for _, selection := range set {
switch selection := selection.(type) {
case *ast.FragmentSpread:
spreads = append(spreads, selection)
case *ast.Field:
setsToVisit = append(setsToVisit, selection.SelectionSet)
case *ast.InlineFragment:
setsToVisit = append(setsToVisit, selection.SelectionSet)
}
}
}
return spreads
}
@@ -0,0 +1,35 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUndefinedVariablesRule = Rule{
Name: "NoUndefinedVariables",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnValue(func(walker *Walker, value *ast.Value) {
if walker.CurrentOperation == nil || value.Kind != ast.Variable ||
value.VariableDefinition != nil {
return
}
if walker.CurrentOperation.Name != "" {
addError(
Message(
`Variable "%s" is not defined by operation "%s".`,
value,
walker.CurrentOperation.Name,
),
At(value.Position),
)
} else {
addError(
Message(`Variable "%s" is not defined.`, value),
At(value.Position),
)
}
})
},
}
@@ -0,0 +1,31 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUnusedFragmentsRule = Rule{
Name: "NoUnusedFragments",
RuleFunc: func(observers *Events, addError AddErrFunc) {
inFragmentDefinition := false
fragmentNameUsed := make(map[string]bool)
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
if !inFragmentDefinition {
fragmentNameUsed[fragmentSpread.Name] = true
}
})
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
inFragmentDefinition = true
if !fragmentNameUsed[fragment.Name] {
addError(
Message(`Fragment "%s" is never used.`, fragment.Name),
At(fragment.Position),
)
}
})
},
}
@@ -0,0 +1,36 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var NoUnusedVariablesRule = Rule{
Name: "NoUnusedVariables",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
for _, varDef := range operation.VariableDefinitions {
if varDef.Used {
continue
}
if operation.Name != "" {
addError(
Message(
`Variable "$%s" is never used in operation "%s".`,
varDef.Variable,
operation.Name,
),
At(varDef.Position),
)
} else {
addError(
Message(`Variable "$%s" is never used.`, varDef.Variable),
At(varDef.Position),
)
}
}
})
},
}
@@ -0,0 +1,640 @@
package rules
import (
"bytes"
"fmt"
"reflect"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var OverlappingFieldsCanBeMergedRule = Rule{
Name: "OverlappingFieldsCanBeMerged",
RuleFunc: func(observers *Events, addError AddErrFunc) {
/**
* Algorithm:
*
* Conflicts occur when two fields exist in a query which will produce the same
* response name, but represent differing values, thus creating a conflict.
* The algorithm below finds all conflicts via making a series of comparisons
* between fields. In order to compare as few fields as possible, this makes
* a series of comparisons "within" sets of fields and "between" sets of fields.
*
* Given any selection set, a collection produces both a set of fields by
* also including all inline fragments, as well as a list of fragments
* referenced by fragment spreads.
*
* A) Each selection set represented in the document first compares "within" its
* collected set of fields, finding any conflicts between every pair of
* overlapping fields.
* Note: This is the *only time* that a the fields "within" a set are compared
* to each other. After this only fields "between" sets are compared.
*
* B) Also, if any fragment is referenced in a selection set, then a
* comparison is made "between" the original set of fields and the
* referenced fragment.
*
* C) Also, if multiple fragments are referenced, then comparisons
* are made "between" each referenced fragment.
*
* D) When comparing "between" a set of fields and a referenced fragment, first
* a comparison is made between each field in the original set of fields and
* each field in the referenced set of fields.
*
* E) Also, if any fragment is referenced in the referenced selection set,
* then a comparison is made "between" the original set of fields and the
* referenced fragment (recursively referring to step D).
*
* F) When comparing "between" two fragments, first a comparison is made between
* each field in the first referenced set of fields and each field in the the
* second referenced set of fields.
*
* G) Also, any fragments referenced by the first must be compared to the
* second, and any fragments referenced by the second must be compared to the
* first (recursively referring to step F).
*
* H) When comparing two fields, if both have selection sets, then a comparison
* is made "between" both selection sets, first comparing the set of fields in
* the first selection set with the set of fields in the second.
*
* I) Also, if any fragment is referenced in either selection set, then a
* comparison is made "between" the other set of fields and the
* referenced fragment.
*
* J) Also, if two fragments are referenced in both selection sets, then a
* comparison is made "between" the two fragments.
*
*/
m := &overlappingFieldsCanBeMergedManager{
comparedFragmentPairs: pairSet{data: make(map[string]map[string]bool)},
}
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
m.walker = walker
conflicts := m.findConflictsWithinSelectionSet(operation.SelectionSet)
for _, conflict := range conflicts {
conflict.addFieldsConflictMessage(addError)
}
})
observers.OnField(func(walker *Walker, field *ast.Field) {
if walker.CurrentOperation == nil {
// When checking both Operation and Fragment, errors are duplicated when processing
// FragmentDefinition referenced from Operation
return
}
m.walker = walker
conflicts := m.findConflictsWithinSelectionSet(field.SelectionSet)
for _, conflict := range conflicts {
conflict.addFieldsConflictMessage(addError)
}
})
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
m.walker = walker
conflicts := m.findConflictsWithinSelectionSet(inlineFragment.SelectionSet)
for _, conflict := range conflicts {
conflict.addFieldsConflictMessage(addError)
}
})
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
m.walker = walker
conflicts := m.findConflictsWithinSelectionSet(fragment.SelectionSet)
for _, conflict := range conflicts {
conflict.addFieldsConflictMessage(addError)
}
})
},
}
type pairSet struct {
data map[string]map[string]bool
}
func (pairSet *pairSet) Add(
a *ast.FragmentSpread,
b *ast.FragmentSpread,
areMutuallyExclusive bool,
) {
add := func(a *ast.FragmentSpread, b *ast.FragmentSpread) {
m := pairSet.data[a.Name]
if m == nil {
m = make(map[string]bool)
pairSet.data[a.Name] = m
}
m[b.Name] = areMutuallyExclusive
}
add(a, b)
add(b, a)
}
func (pairSet *pairSet) Has(
a *ast.FragmentSpread,
b *ast.FragmentSpread,
areMutuallyExclusive bool,
) bool {
am, ok := pairSet.data[a.Name]
if !ok {
return false
}
result, ok := am[b.Name]
if !ok {
return false
}
// areMutuallyExclusive being false is a superset of being true,
// hence if we want to know if this PairSet "has" these two with no
// exclusivity, we have to ensure it was added as such.
if !areMutuallyExclusive {
return !result
}
return true
}
type sequentialFieldsMap struct {
// We can't use map[string][]*ast.Field. because map is not stable...
seq []string
data map[string][]*ast.Field
}
type fieldIterateEntry struct {
ResponseName string
Fields []*ast.Field
}
func (m *sequentialFieldsMap) Push(responseName string, field *ast.Field) {
fields, ok := m.data[responseName]
if !ok {
m.seq = append(m.seq, responseName)
}
fields = append(fields, field)
m.data[responseName] = fields
}
func (m *sequentialFieldsMap) Get(responseName string) ([]*ast.Field, bool) {
fields, ok := m.data[responseName]
return fields, ok
}
func (m *sequentialFieldsMap) Iterator() [][]*ast.Field {
fieldsList := make([][]*ast.Field, 0, len(m.seq))
for _, responseName := range m.seq {
fields := m.data[responseName]
fieldsList = append(fieldsList, fields)
}
return fieldsList
}
func (m *sequentialFieldsMap) KeyValueIterator() []*fieldIterateEntry {
fieldEntriesList := make([]*fieldIterateEntry, 0, len(m.seq))
for _, responseName := range m.seq {
fields := m.data[responseName]
fieldEntriesList = append(fieldEntriesList, &fieldIterateEntry{
ResponseName: responseName,
Fields: fields,
})
}
return fieldEntriesList
}
type conflictMessageContainer struct {
Conflicts []*ConflictMessage
}
type ConflictMessage struct {
Message string
ResponseName string
Names []string
SubMessage []*ConflictMessage
Position *ast.Position
}
func (m *ConflictMessage) String(buf *bytes.Buffer) {
if len(m.SubMessage) == 0 {
buf.WriteString(m.Message)
return
}
for idx, subMessage := range m.SubMessage {
buf.WriteString(`subfields "`)
buf.WriteString(subMessage.ResponseName)
buf.WriteString(`" conflict because `)
subMessage.String(buf)
if idx != len(m.SubMessage)-1 {
buf.WriteString(" and ")
}
}
}
func (m *ConflictMessage) addFieldsConflictMessage(addError AddErrFunc) {
var buf bytes.Buffer
m.String(&buf)
addError(
Message(
`Fields "%s" conflict because %s. Use different aliases on the fields to fetch both if this was intentional.`,
m.ResponseName,
buf.String(),
),
At(m.Position),
)
}
type overlappingFieldsCanBeMergedManager struct {
walker *Walker
// per walker
comparedFragmentPairs pairSet
// cachedFieldsAndFragmentNames interface{}
// per selectionSet
comparedFragments map[string]bool
}
func (m *overlappingFieldsCanBeMergedManager) findConflictsWithinSelectionSet(
selectionSet ast.SelectionSet,
) []*ConflictMessage {
if len(selectionSet) == 0 {
return nil
}
fieldsMap, fragmentSpreads := getFieldsAndFragmentNames(selectionSet)
var conflicts conflictMessageContainer
// (A) Find find all conflicts "within" the fieldMap of this selection set.
// Note: this is the *only place* `collectConflictsWithin` is called.
m.collectConflictsWithin(&conflicts, fieldsMap)
m.comparedFragments = make(map[string]bool)
for idx, fragmentSpreadA := range fragmentSpreads {
// (B) Then collect conflicts between these fieldMap and those represented by
// each spread fragment name found.
m.collectConflictsBetweenFieldsAndFragment(&conflicts, false, fieldsMap, fragmentSpreadA)
for _, fragmentSpreadB := range fragmentSpreads[idx+1:] {
// (C) Then compare this fragment with all other fragments found in this
// selection set to collect conflicts between fragments spread together.
// This compares each item in the list of fragment names to every other
// item in that same list (except for itself).
m.collectConflictsBetweenFragments(&conflicts, false, fragmentSpreadA, fragmentSpreadB)
}
}
return conflicts.Conflicts
}
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetweenFieldsAndFragment(
conflicts *conflictMessageContainer,
areMutuallyExclusive bool,
fieldsMap *sequentialFieldsMap,
fragmentSpread *ast.FragmentSpread,
) {
if m.comparedFragments[fragmentSpread.Name] {
return
}
m.comparedFragments[fragmentSpread.Name] = true
if fragmentSpread.Definition == nil {
return
}
fieldsMapB, fragmentSpreads := getFieldsAndFragmentNames(fragmentSpread.Definition.SelectionSet)
// Do not compare a fragment's fieldMap to itself.
if reflect.DeepEqual(fieldsMap, fieldsMapB) {
return
}
// (D) First collect any conflicts between the provided collection of fields
// and the collection of fields represented by the given fragment.
m.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsMap, fieldsMapB)
// (E) Then collect any conflicts between the provided collection of fields
// and any fragment names found in the given fragment.
baseFragmentSpread := fragmentSpread
for _, fragmentSpread := range fragmentSpreads {
if fragmentSpread.Name == baseFragmentSpread.Name {
continue
}
m.collectConflictsBetweenFieldsAndFragment(
conflicts,
areMutuallyExclusive,
fieldsMap,
fragmentSpread,
)
}
}
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetweenFragments(
conflicts *conflictMessageContainer,
areMutuallyExclusive bool,
fragmentSpreadA *ast.FragmentSpread,
fragmentSpreadB *ast.FragmentSpread,
) {
var check func(fragmentSpreadA *ast.FragmentSpread, fragmentSpreadB *ast.FragmentSpread)
check = func(fragmentSpreadA *ast.FragmentSpread, fragmentSpreadB *ast.FragmentSpread) {
if fragmentSpreadA.Name == fragmentSpreadB.Name {
return
}
if m.comparedFragmentPairs.Has(fragmentSpreadA, fragmentSpreadB, areMutuallyExclusive) {
return
}
m.comparedFragmentPairs.Add(fragmentSpreadA, fragmentSpreadB, areMutuallyExclusive)
if fragmentSpreadA.Definition == nil {
return
}
if fragmentSpreadB.Definition == nil {
return
}
fieldsMapA, fragmentSpreadsA := getFieldsAndFragmentNames(
fragmentSpreadA.Definition.SelectionSet,
)
fieldsMapB, fragmentSpreadsB := getFieldsAndFragmentNames(
fragmentSpreadB.Definition.SelectionSet,
)
// (F) First, collect all conflicts between these two collections of fields
// (not including any nested fragments).
m.collectConflictsBetween(conflicts, areMutuallyExclusive, fieldsMapA, fieldsMapB)
// (G) Then collect conflicts between the first fragment and any nested
// fragments spread in the second fragment.
for _, fragmentSpread := range fragmentSpreadsB {
check(fragmentSpreadA, fragmentSpread)
}
// (G) Then collect conflicts between the second fragment and any nested
// fragments spread in the first fragment.
for _, fragmentSpread := range fragmentSpreadsA {
check(fragmentSpread, fragmentSpreadB)
}
}
check(fragmentSpreadA, fragmentSpreadB)
}
func (m *overlappingFieldsCanBeMergedManager) findConflictsBetweenSubSelectionSets(
areMutuallyExclusive bool,
selectionSetA ast.SelectionSet,
selectionSetB ast.SelectionSet,
) *conflictMessageContainer {
var conflicts conflictMessageContainer
fieldsMapA, fragmentSpreadsA := getFieldsAndFragmentNames(selectionSetA)
fieldsMapB, fragmentSpreadsB := getFieldsAndFragmentNames(selectionSetB)
// (H) First, collect all conflicts between these two collections of field.
m.collectConflictsBetween(&conflicts, areMutuallyExclusive, fieldsMapA, fieldsMapB)
// (I) Then collect conflicts between the first collection of fields and
// those referenced by each fragment name associated with the second.
for _, fragmentSpread := range fragmentSpreadsB {
m.comparedFragments = make(map[string]bool)
m.collectConflictsBetweenFieldsAndFragment(
&conflicts,
areMutuallyExclusive,
fieldsMapA,
fragmentSpread,
)
}
// (I) Then collect conflicts between the second collection of fields and
// those referenced by each fragment name associated with the first.
for _, fragmentSpread := range fragmentSpreadsA {
m.comparedFragments = make(map[string]bool)
m.collectConflictsBetweenFieldsAndFragment(
&conflicts,
areMutuallyExclusive,
fieldsMapB,
fragmentSpread,
)
}
// (J) Also collect conflicts between any fragment names by the first and
// fragment names by the second. This compares each item in the first set of
// names to each item in the second set of names.
for _, fragmentSpreadA := range fragmentSpreadsA {
for _, fragmentSpreadB := range fragmentSpreadsB {
m.collectConflictsBetweenFragments(
&conflicts,
areMutuallyExclusive,
fragmentSpreadA,
fragmentSpreadB,
)
}
}
if len(conflicts.Conflicts) == 0 {
return nil
}
return &conflicts
}
func (m *overlappingFieldsCanBeMergedManager) collectConflictsWithin(
conflicts *conflictMessageContainer,
fieldsMap *sequentialFieldsMap,
) {
for _, fields := range fieldsMap.Iterator() {
for idx, fieldA := range fields {
for _, fieldB := range fields[idx+1:] {
conflict := m.findConflict(false, fieldA, fieldB)
if conflict != nil {
conflicts.Conflicts = append(conflicts.Conflicts, conflict)
}
}
}
}
}
func (m *overlappingFieldsCanBeMergedManager) collectConflictsBetween(
conflicts *conflictMessageContainer,
parentFieldsAreMutuallyExclusive bool,
fieldsMapA *sequentialFieldsMap,
fieldsMapB *sequentialFieldsMap,
) {
for _, fieldsEntryA := range fieldsMapA.KeyValueIterator() {
fieldsB, ok := fieldsMapB.Get(fieldsEntryA.ResponseName)
if !ok {
continue
}
for _, fieldA := range fieldsEntryA.Fields {
for _, fieldB := range fieldsB {
conflict := m.findConflict(parentFieldsAreMutuallyExclusive, fieldA, fieldB)
if conflict != nil {
conflicts.Conflicts = append(conflicts.Conflicts, conflict)
}
}
}
}
}
func (m *overlappingFieldsCanBeMergedManager) findConflict(
parentFieldsAreMutuallyExclusive bool,
fieldA *ast.Field,
fieldB *ast.Field,
) *ConflictMessage {
if fieldA.ObjectDefinition == nil || fieldB.ObjectDefinition == nil {
return nil
}
areMutuallyExclusive := parentFieldsAreMutuallyExclusive
if !areMutuallyExclusive {
tmp := fieldA.ObjectDefinition.Name != fieldB.ObjectDefinition.Name
tmp = tmp && fieldA.ObjectDefinition.Kind == ast.Object
tmp = tmp && fieldB.ObjectDefinition.Kind == ast.Object
tmp = tmp && fieldA.Definition != nil && fieldB.Definition != nil
areMutuallyExclusive = tmp
}
fieldNameA := fieldA.Name
if fieldA.Alias != "" {
fieldNameA = fieldA.Alias
}
if !areMutuallyExclusive {
// Two aliases must refer to the same field.
if fieldA.Name != fieldB.Name {
return &ConflictMessage{
ResponseName: fieldNameA,
Message: fmt.Sprintf(
`"%s" and "%s" are different fields`,
fieldA.Name,
fieldB.Name,
),
Position: fieldB.Position,
}
}
// Two field calls must have the same arguments.
if !sameArguments(fieldA.Arguments, fieldB.Arguments) {
return &ConflictMessage{
ResponseName: fieldNameA,
Message: "they have differing arguments",
Position: fieldB.Position,
}
}
}
if fieldA.Definition != nil && fieldB.Definition != nil &&
doTypesConflict(m.walker, fieldA.Definition.Type, fieldB.Definition.Type) {
return &ConflictMessage{
ResponseName: fieldNameA,
Message: fmt.Sprintf(
`they return conflicting types "%s" and "%s"`,
fieldA.Definition.Type.String(),
fieldB.Definition.Type.String(),
),
Position: fieldB.Position,
}
}
// Collect and compare sub-fields. Use the same "visited fragment names" list
// for both collections so fields in a fragment reference are never
// compared to themselves.
conflicts := m.findConflictsBetweenSubSelectionSets(
areMutuallyExclusive,
fieldA.SelectionSet,
fieldB.SelectionSet,
)
if conflicts == nil {
return nil
}
return &ConflictMessage{
ResponseName: fieldNameA,
SubMessage: conflicts.Conflicts,
Position: fieldB.Position,
}
}
func sameArguments(args1, args2 []*ast.Argument) bool {
if len(args1) != len(args2) {
return false
}
for _, arg1 := range args1 {
var matched bool
for _, arg2 := range args2 {
if arg1.Name == arg2.Name && sameValue(arg1.Value, arg2.Value) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
func sameValue(value1, value2 *ast.Value) bool {
if value1.Kind != value2.Kind {
return false
}
if value1.Raw != value2.Raw {
return false
}
return true
}
func doTypesConflict(walker *Walker, type1, type2 *ast.Type) bool {
if type1.Elem != nil {
if type2.Elem != nil {
return doTypesConflict(walker, type1.Elem, type2.Elem)
}
return true
}
if type2.Elem != nil {
return true
}
if type1.NonNull && !type2.NonNull {
return true
}
if !type1.NonNull && type2.NonNull {
return true
}
t1 := walker.Schema.Types[type1.NamedType]
t2 := walker.Schema.Types[type2.NamedType]
if (t1.Kind == ast.Scalar || t1.Kind == ast.Enum) &&
(t2.Kind == ast.Scalar || t2.Kind == ast.Enum) {
return t1.Name != t2.Name
}
return false
}
func getFieldsAndFragmentNames(
selectionSet ast.SelectionSet,
) (*sequentialFieldsMap, []*ast.FragmentSpread) {
fieldsMap := sequentialFieldsMap{
data: make(map[string][]*ast.Field),
}
var fragmentSpreads []*ast.FragmentSpread
var walk func(selectionSet ast.SelectionSet)
walk = func(selectionSet ast.SelectionSet) {
for _, selection := range selectionSet {
switch selection := selection.(type) {
case *ast.Field:
responseName := selection.Name
if selection.Alias != "" {
responseName = selection.Alias
}
fieldsMap.Push(responseName, selection)
case *ast.InlineFragment:
walk(selection.SelectionSet)
case *ast.FragmentSpread:
fragmentSpreads = append(fragmentSpreads, selection)
}
}
}
walk(selectionSet)
return &fieldsMap, fragmentSpreads
}
@@ -0,0 +1,83 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var PossibleFragmentSpreadsRule = Rule{
Name: "PossibleFragmentSpreads",
RuleFunc: func(observers *Events, addError AddErrFunc) {
validate := func(walker *Walker, parentDef *ast.Definition, fragmentName string, emitError func()) {
if parentDef == nil {
return
}
var parentDefs []*ast.Definition
switch parentDef.Kind {
case ast.Object:
parentDefs = []*ast.Definition{parentDef}
case ast.Interface, ast.Union:
parentDefs = walker.Schema.GetPossibleTypes(parentDef)
default:
return
}
fragmentDefType := walker.Schema.Types[fragmentName]
if fragmentDefType == nil {
return
}
if !fragmentDefType.IsCompositeType() {
// checked by FragmentsOnCompositeTypes
return
}
fragmentDefs := walker.Schema.GetPossibleTypes(fragmentDefType)
for _, fragmentDef := range fragmentDefs {
for _, parentDef := range parentDefs {
if parentDef.Name == fragmentDef.Name {
return
}
}
}
emitError()
}
observers.OnInlineFragment(func(walker *Walker, inlineFragment *ast.InlineFragment) {
validate(walker, inlineFragment.ObjectDefinition, inlineFragment.TypeCondition, func() {
addError(
Message(
`Fragment cannot be spread here as objects of type "%s" can never be of type "%s".`,
inlineFragment.ObjectDefinition.Name,
inlineFragment.TypeCondition,
),
At(inlineFragment.Position),
)
})
})
observers.OnFragmentSpread(func(walker *Walker, fragmentSpread *ast.FragmentSpread) {
if fragmentSpread.Definition == nil {
return
}
validate(
walker,
fragmentSpread.ObjectDefinition,
fragmentSpread.Definition.TypeCondition,
func() {
addError(
Message(
`Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".`,
fragmentSpread.Name,
fragmentSpread.ObjectDefinition.Name,
fragmentSpread.Definition.TypeCondition,
),
At(fragmentSpread.Position),
)
},
)
})
},
}
@@ -0,0 +1,64 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var ProvidedRequiredArgumentsRule = Rule{
Name: "ProvidedRequiredArguments",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.Definition == nil {
return
}
argDef:
for _, argDef := range field.Definition.Arguments {
if !argDef.Type.NonNull {
continue
}
if argDef.DefaultValue != nil {
continue
}
for _, arg := range field.Arguments {
if arg.Name == argDef.Name {
continue argDef
}
}
addError(
Message(`Field "%s" argument "%s" of type "%s" is required, but it was not provided.`, field.Name, argDef.Name, argDef.Type.String()),
At(field.Position),
)
}
})
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
if directive.Definition == nil {
return
}
argDef:
for _, argDef := range directive.Definition.Arguments {
if !argDef.Type.NonNull {
continue
}
if argDef.DefaultValue != nil {
continue
}
for _, arg := range directive.Arguments {
if arg.Name == argDef.Name {
continue argDef
}
}
addError(
Message(`Directive "@%s" argument "%s" of type "%s" is required, but it was not provided.`, directive.Definition.Name, argDef.Name, argDef.Type.String()),
At(directive.Position),
)
}
})
},
}
+126
View File
@@ -0,0 +1,126 @@
package rules
import (
"slices"
"github.com/vektah/gqlparser/v2/validator/core"
)
// Rules manages GraphQL validation rules.
type Rules struct {
rules map[string]core.RuleFunc
ruleNameKeys []string // for deterministic order
}
// NewRules creates a Rules instance with the specified rules.
func NewRules(rs ...core.Rule) *Rules {
r := &Rules{
rules: make(map[string]core.RuleFunc),
}
for _, rule := range rs {
r.AddRule(rule.Name, rule.RuleFunc)
}
return r
}
// NewDefaultRules creates a Rules instance containing the default GraphQL validation rule set.
func NewDefaultRules() *Rules {
rules := []core.Rule{
FieldsOnCorrectTypeRule,
FragmentsOnCompositeTypesRule,
KnownArgumentNamesRule,
KnownDirectivesRule,
KnownFragmentNamesRule,
KnownRootTypeRule,
KnownTypeNamesRule,
LoneAnonymousOperationRule,
MaxIntrospectionDepth,
NoFragmentCyclesRule,
NoUndefinedVariablesRule,
NoUnusedFragmentsRule,
NoUnusedVariablesRule,
OverlappingFieldsCanBeMergedRule,
PossibleFragmentSpreadsRule,
ProvidedRequiredArgumentsRule,
ScalarLeafsRule,
SingleFieldSubscriptionsRule,
UniqueArgumentNamesRule,
UniqueDirectivesPerLocationRule,
UniqueFragmentNamesRule,
UniqueInputFieldNamesRule,
UniqueOperationNamesRule,
UniqueVariableNamesRule,
ValuesOfCorrectTypeRule,
VariablesAreInputTypesRule,
VariablesInAllowedPositionRule,
}
r := NewRules(rules...)
return r
}
// AddRule adds a rule with the specified name and rule function to the rule set.
// If a rule with the same name already exists, it will not be added.
func (r *Rules) AddRule(name string, ruleFunc core.RuleFunc) {
if r.rules == nil {
r.rules = make(map[string]core.RuleFunc)
}
if _, exists := r.rules[name]; !exists {
r.rules[name] = ruleFunc
r.ruleNameKeys = append(r.ruleNameKeys, name)
}
}
// GetInner returns the internal rule map.
// If the map is not initialized, it returns an empty map.
// This returns a copy of the rules map, not the original map.
func (r *Rules) GetInner() map[string]core.RuleFunc {
if r == nil {
return nil // impossible nonsense, hopefully
}
if r.rules == nil {
return make(map[string]core.RuleFunc)
}
rules := make(map[string]core.RuleFunc)
for k, v := range r.rules {
rules[k] = v
}
return rules
}
// RemoveRule removes a rule with the specified name from the rule set.
// If no rule with the specified name exists, it does nothing.
func (r *Rules) RemoveRule(name string) {
if r == nil {
return // impossible nonsense, hopefully
}
if r.rules != nil {
delete(r.rules, name)
}
if len(r.ruleNameKeys) > 0 {
r.ruleNameKeys = slices.DeleteFunc(r.ruleNameKeys, func(s string) bool {
return s == name // delete the name rule key
})
}
}
// ReplaceRule replaces a rule with the specified name with a new rule function.
// If no rule with the specified name exists, it does nothing.
func (r *Rules) ReplaceRule(name string, ruleFunc core.RuleFunc) {
if r == nil {
return // impossible nonsense, hopefully
}
if r.rules == nil {
r.rules = make(map[string]core.RuleFunc)
}
if _, exists := r.rules[name]; exists {
r.rules[name] = ruleFunc
}
}
@@ -0,0 +1,46 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var ScalarLeafsRule = Rule{
Name: "ScalarLeafs",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnField(func(walker *Walker, field *ast.Field) {
if field.Definition == nil {
return
}
fieldType := walker.Schema.Types[field.Definition.Type.Name()]
if fieldType == nil {
return
}
if fieldType.IsLeafType() && len(field.SelectionSet) > 0 {
addError(
Message(
`Field "%s" must not have a selection since type "%s" has no subfields.`,
field.Name,
fieldType.Name,
),
At(field.Position),
)
}
if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 {
addError(
Message(
`Field "%s" of type "%s" must have a selection of subfields.`,
field.Name,
field.Definition.Type.String(),
),
Suggestf(`"%s { ... }"`, field.Name),
At(field.Position),
)
}
})
},
}
@@ -0,0 +1,88 @@
package rules
import (
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var SingleFieldSubscriptionsRule = Rule{
Name: "SingleFieldSubscriptions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
if walker.Schema.Subscription == nil || operation.Operation != ast.Subscription {
return
}
fields := retrieveTopFieldNames(operation.SelectionSet)
name := "Anonymous Subscription"
if operation.Name != "" {
name = `Subscription ` + strconv.Quote(operation.Name)
}
if len(fields) > 1 {
addError(
Message(`%s must select only one top level field.`, name),
At(fields[1].position),
)
}
for _, field := range fields {
if strings.HasPrefix(field.name, "__") {
addError(
Message(`%s must not select an introspection top level field.`, name),
At(field.position),
)
}
}
})
},
}
type topField struct {
name string
position *ast.Position
}
func retrieveTopFieldNames(selectionSet ast.SelectionSet) []*topField {
fields := []*topField{}
inFragmentRecursive := map[string]bool{}
var walk func(selectionSet ast.SelectionSet)
walk = func(selectionSet ast.SelectionSet) {
for _, selection := range selectionSet {
switch selection := selection.(type) {
case *ast.Field:
fields = append(fields, &topField{
name: selection.Name,
position: selection.GetPosition(),
})
case *ast.InlineFragment:
walk(selection.SelectionSet)
case *ast.FragmentSpread:
if selection.Definition == nil {
return
}
fragment := selection.Definition.Name
if !inFragmentRecursive[fragment] {
inFragmentRecursive[fragment] = true
walk(selection.Definition.SelectionSet)
}
}
}
}
walk(selectionSet)
seen := make(map[string]bool, len(fields))
uniquedFields := make([]*topField, 0, len(fields))
for _, field := range fields {
if !seen[field.name] {
uniquedFields = append(uniquedFields, field)
}
seen[field.name] = true
}
return uniquedFields
}
@@ -0,0 +1,35 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueArgumentNamesRule = Rule{
Name: "UniqueArgumentNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnField(func(walker *Walker, field *ast.Field) {
checkUniqueArgs(field.Arguments, addError)
})
observers.OnDirective(func(walker *Walker, directive *ast.Directive) {
checkUniqueArgs(directive.Arguments, addError)
})
},
}
func checkUniqueArgs(args ast.ArgumentList, addError AddErrFunc) {
knownArgNames := map[string]int{}
for _, arg := range args {
if knownArgNames[arg.Name] == 1 {
addError(
Message(`There can be only one argument named "%s".`, arg.Name),
At(arg.Position),
)
}
knownArgNames[arg.Name]++
}
}
@@ -0,0 +1,29 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueDirectivesPerLocationRule = Rule{
Name: "UniqueDirectivesPerLocation",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnDirectiveList(func(walker *Walker, directives []*ast.Directive) {
seen := map[string]bool{}
for _, dir := range directives {
if dir.Name != "repeatable" && seen[dir.Name] {
addError(
Message(
`The directive "@%s" can only be used once at this location.`,
dir.Name,
),
At(dir.Position),
)
}
seen[dir.Name] = true
}
})
},
}
@@ -0,0 +1,24 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueFragmentNamesRule = Rule{
Name: "UniqueFragmentNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
seenFragments := map[string]bool{}
observers.OnFragment(func(walker *Walker, fragment *ast.FragmentDefinition) {
if seenFragments[fragment.Name] {
addError(
Message(`There can be only one fragment named "%s".`, fragment.Name),
At(fragment.Position),
)
}
seenFragments[fragment.Name] = true
})
},
}
@@ -0,0 +1,29 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueInputFieldNamesRule = Rule{
Name: "UniqueInputFieldNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnValue(func(walker *Walker, value *ast.Value) {
if value.Kind != ast.ObjectValue {
return
}
seen := map[string]bool{}
for _, field := range value.Children {
if seen[field.Name] {
addError(
Message(`There can be only one input field named "%s".`, field.Name),
At(field.Position),
)
}
seen[field.Name] = true
}
})
},
}
@@ -0,0 +1,24 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueOperationNamesRule = Rule{
Name: "UniqueOperationNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
seen := map[string]bool{}
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
if seen[operation.Name] {
addError(
Message(`There can be only one operation named "%s".`, operation.Name),
At(operation.Position),
)
}
seen[operation.Name] = true
})
},
}
@@ -0,0 +1,26 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var UniqueVariableNamesRule = Rule{
Name: "UniqueVariableNames",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
seen := map[string]int{}
for _, def := range operation.VariableDefinitions {
// add the same error only once per a variable.
if seen[def.Variable] == 1 {
addError(
Message(`There can be only one variable named "$%s".`, def.Variable),
At(def.Position),
)
}
seen[def.Variable]++
}
})
},
}
@@ -0,0 +1,307 @@
package rules
import (
"errors"
"fmt"
"strconv"
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
func ruleFuncValuesOfCorrectType(observers *Events, addError AddErrFunc, disableSuggestion bool) {
observers.OnValue(func(walker *Walker, value *ast.Value) {
if value.Definition == nil || value.ExpectedType == nil {
return
}
if value.Kind == ast.NullValue && value.ExpectedType.NonNull {
addError(
Message(
`Expected value of type "%s", found %s.`,
value.ExpectedType.String(),
value.String(),
),
At(value.Position),
)
}
if value.Definition.Kind == ast.Scalar {
// Skip custom validating scalars
if !value.Definition.OneOf("Int", "Float", "String", "Boolean", "ID") {
return
}
}
var possibleEnums []string
if value.Definition.Kind == ast.Enum {
for _, val := range value.Definition.EnumValues {
possibleEnums = append(possibleEnums, val.Name)
}
}
rawVal, err := value.Value(nil)
if err != nil {
unexpectedTypeMessage(addError, value)
}
switch value.Kind {
case ast.NullValue:
return
case ast.ListValue:
if value.ExpectedType.Elem == nil {
unexpectedTypeMessage(addError, value)
return
}
case ast.IntValue:
if !value.Definition.OneOf("Int", "Float", "ID") {
unexpectedTypeMessage(addError, value)
}
case ast.FloatValue:
if !value.Definition.OneOf("Float") {
unexpectedTypeMessage(addError, value)
}
case ast.StringValue, ast.BlockValue:
if value.Definition.Kind == ast.Enum {
if disableSuggestion {
addError(
Message(
`Enum "%s" cannot represent non-enum value: %s.`,
value.ExpectedType.String(),
value.String(),
),
At(value.Position),
)
} else {
rawValStr := fmt.Sprint(rawVal)
addError(
Message(
`Enum "%s" cannot represent non-enum value: %s.`,
value.ExpectedType.String(),
value.String(),
),
SuggestListQuoted("Did you mean the enum value", rawValStr, possibleEnums),
At(value.Position),
)
}
} else if !value.Definition.OneOf("String", "ID") {
unexpectedTypeMessage(addError, value)
}
case ast.EnumValue:
if value.Definition.Kind != ast.Enum {
if disableSuggestion {
addError(
unexpectedTypeMessageOnly(value),
At(value.Position),
)
} else {
rawValStr := fmt.Sprint(rawVal)
addError(
unexpectedTypeMessageOnly(value),
SuggestListUnquoted(
"Did you mean the enum value",
rawValStr,
possibleEnums,
),
At(value.Position),
)
}
} else if value.Definition.EnumValues.ForName(value.Raw) == nil {
if disableSuggestion {
addError(
Message(
`Value "%s" does not exist in "%s" enum.`,
value.String(),
value.ExpectedType.String(),
),
At(value.Position),
)
} else {
rawValStr := fmt.Sprint(rawVal)
addError(
Message(
`Value "%s" does not exist in "%s" enum.`,
value.String(),
value.ExpectedType.String(),
),
SuggestListQuoted("Did you mean the enum value", rawValStr, possibleEnums),
At(value.Position),
)
}
}
case ast.BooleanValue:
if !value.Definition.OneOf("Boolean") {
unexpectedTypeMessage(addError, value)
}
case ast.ObjectValue:
for _, field := range value.Definition.Fields {
if field.Type.NonNull {
fieldValue := value.Children.ForName(field.Name)
if fieldValue == nil && field.DefaultValue == nil {
addError(
Message(
`Field "%s.%s" of required type "%s" was not provided.`,
value.Definition.Name,
field.Name,
field.Type.String(),
),
At(value.Position),
)
continue
}
}
}
for _, directive := range value.Definition.Directives {
if directive.Name == "oneOf" {
func() {
if len(value.Children) != 1 {
addError(
Message(
`OneOf Input Object "%s" must specify exactly one key.`,
value.Definition.Name,
),
At(value.Position),
)
return
}
fieldValue := value.Children[0].Value
isNullLiteral := fieldValue == nil || fieldValue.Kind == ast.NullValue
if isNullLiteral {
addError(
Message(
`Field "%s.%s" must be non-null.`,
value.Definition.Name,
value.Definition.Fields[0].Name,
),
At(fieldValue.Position),
)
return
}
isVariable := fieldValue.Kind == ast.Variable
if isVariable {
variableName := fieldValue.VariableDefinition.Variable
isNullableVariable := !fieldValue.VariableDefinition.Type.NonNull
if isNullableVariable {
addError(
Message(
`Variable "%s" must be non-nullable to be used for OneOf Input Object "%s".`,
variableName,
value.Definition.Name,
),
At(fieldValue.Position),
)
}
}
}()
}
}
for _, fieldValue := range value.Children {
if value.Definition.Fields.ForName(fieldValue.Name) == nil {
if disableSuggestion {
addError(
Message(
`Field "%s" is not defined by type "%s".`,
fieldValue.Name,
value.Definition.Name,
),
At(fieldValue.Position),
)
} else {
var suggestions []string
for _, fieldValue := range value.Definition.Fields {
suggestions = append(suggestions, fieldValue.Name)
}
addError(
Message(
`Field "%s" is not defined by type "%s".`,
fieldValue.Name,
value.Definition.Name,
),
SuggestListQuoted("Did you mean", fieldValue.Name, suggestions),
At(fieldValue.Position),
)
}
}
}
case ast.Variable:
return
default:
panic(fmt.Errorf("unhandled %T", value))
}
})
}
var ValuesOfCorrectTypeRule = Rule{
Name: "ValuesOfCorrectType",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncValuesOfCorrectType(observers, addError, false)
},
}
var ValuesOfCorrectTypeRuleWithoutSuggestions = Rule{
Name: "ValuesOfCorrectTypeWithoutSuggestions",
RuleFunc: func(observers *Events, addError AddErrFunc) {
ruleFuncValuesOfCorrectType(observers, addError, true)
},
}
func unexpectedTypeMessage(addError AddErrFunc, v *ast.Value) {
addError(
unexpectedTypeMessageOnly(v),
At(v.Position),
)
}
func unexpectedTypeMessageOnly(v *ast.Value) ErrorOption {
switch v.ExpectedType.String() {
case "Int", "Int!":
if _, err := strconv.ParseInt(
v.Raw,
10,
32,
); err != nil &&
errors.Is(err, strconv.ErrRange) {
return Message(`Int cannot represent non 32-bit signed integer value: %s`, v.String())
}
return Message(`Int cannot represent non-integer value: %s`, v.String())
case "String", "String!", "[String]":
return Message(`String cannot represent a non string value: %s`, v.String())
case "Boolean", "Boolean!":
return Message(`Boolean cannot represent a non boolean value: %s`, v.String())
case "Float", "Float!":
return Message(`Float cannot represent non numeric value: %s`, v.String())
case "ID", "ID!":
return Message(`ID cannot represent a non-string and non-integer value: %s`, v.String())
// case "Enum":
// return Message(`Enum "%s" cannot represent non-enum value: %s`, v.ExpectedType.String(),
// v.String())
default:
if v.Definition.Kind == ast.Enum {
return Message(
`Enum "%s" cannot represent non-enum value: %s.`,
v.ExpectedType.String(),
v.String(),
)
}
return Message(
`Expected value of type "%s", found %s.`,
v.ExpectedType.String(),
v.String(),
)
}
}
@@ -0,0 +1,30 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var VariablesAreInputTypesRule = Rule{
Name: "VariablesAreInputTypes",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnOperation(func(walker *Walker, operation *ast.OperationDefinition) {
for _, def := range operation.VariableDefinitions {
if def.Definition == nil {
continue
}
if !def.Definition.IsInputType() {
addError(
Message(
`Variable "$%s" cannot be non-input type "%s".`,
def.Variable,
def.Type.String(),
),
At(def.Position),
)
}
}
})
},
}
@@ -0,0 +1,48 @@
package rules
import (
"github.com/vektah/gqlparser/v2/ast"
//nolint:staticcheck // Validator rules each use dot imports for convenience.
. "github.com/vektah/gqlparser/v2/validator/core"
)
var VariablesInAllowedPositionRule = Rule{
Name: "VariablesInAllowedPosition",
RuleFunc: func(observers *Events, addError AddErrFunc) {
observers.OnValue(func(walker *Walker, value *ast.Value) {
if value.Kind != ast.Variable || value.ExpectedType == nil ||
value.VariableDefinition == nil ||
walker.CurrentOperation == nil {
return
}
tmp := *value.ExpectedType
// todo: move me into walk
// If there is a default non nullable types can be null
if value.VariableDefinition.DefaultValue != nil &&
value.VariableDefinition.DefaultValue.Kind != ast.NullValue {
if value.ExpectedType.NonNull {
tmp.NonNull = false
}
}
// If the expected type has a default, the given variable can be null
if value.ExpectedTypeHasDefault {
tmp.NonNull = false
}
if !value.VariableDefinition.Type.IsCompatible(&tmp) {
addError(
Message(
`Variable "%s" of type "%s" used in position expecting type "%s".`,
value,
value.VariableDefinition.Type.String(),
value.ExpectedType.String(),
),
At(value.Position),
)
}
})
},
}
+660
View File
@@ -0,0 +1,660 @@
package validator
import (
"slices"
"sort"
"strconv"
"strings"
. "github.com/vektah/gqlparser/v2/ast" //nolint:staticcheck // bad, yeah
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/parser"
)
func LoadSchema(inputs ...*Source) (*Schema, error) {
sd, err := parser.ParseSchemas(inputs...)
if err != nil {
return nil, gqlerror.WrapIfUnwrapped(err)
}
return ValidateSchemaDocument(sd)
}
func ValidateSchemaDocument(sd *SchemaDocument) (*Schema, error) {
schema := Schema{
Types: map[string]*Definition{},
Directives: map[string]*DirectiveDefinition{},
PossibleTypes: map[string][]*Definition{},
Implements: map[string][]*Definition{},
}
for i, def := range sd.Definitions {
if schema.Types[def.Name] != nil {
return nil, gqlerror.ErrorPosf(def.Position, "Cannot redeclare type %s.", def.Name)
}
schema.Types[def.Name] = sd.Definitions[i]
}
defs := append(DefinitionList{}, sd.Definitions...)
for _, ext := range sd.Extensions {
def := schema.Types[ext.Name]
if def == nil {
schema.Types[ext.Name] = &Definition{
Kind: ext.Kind,
Name: ext.Name,
Position: ext.Position,
}
def = schema.Types[ext.Name]
defs = append(defs, def)
}
if def.Kind != ext.Kind {
return nil, gqlerror.ErrorPosf(
ext.Position,
"Cannot extend type %s because the base type is a %s, not %s.",
ext.Name,
def.Kind,
ext.Kind,
)
}
def.Directives = append(def.Directives, ext.Directives...)
def.Interfaces = append(def.Interfaces, ext.Interfaces...)
def.Fields = append(def.Fields, ext.Fields...)
def.Types = append(def.Types, ext.Types...)
def.EnumValues = append(def.EnumValues, ext.EnumValues...)
}
for _, def := range defs {
switch def.Kind {
case Union:
for _, t := range def.Types {
schema.AddPossibleType(def.Name, schema.Types[t])
schema.AddImplements(t, def)
}
case InputObject, Object:
for _, intf := range def.Interfaces {
schema.AddPossibleType(intf, def)
schema.AddImplements(def.Name, schema.Types[intf])
}
schema.AddPossibleType(def.Name, def)
case Interface:
for _, intf := range def.Interfaces {
schema.AddPossibleType(intf, def)
schema.AddImplements(def.Name, schema.Types[intf])
}
}
}
for i, dir := range sd.Directives {
if schema.Directives[dir.Name] != nil {
// While the spec says SDL must not (§3.5) explicitly define builtin
// scalars, it may (§3.13) define builtin directives. Here we check for
// that, and reject doubly-defined directives otherwise.
switch dir.Name {
case "include", "skip", "deprecated", "specifiedBy", "defer", "oneOf": // the builtins
// In principle here we might want to validate that the
// directives are the same. But they might not be, if the
// server has an older spec than we do. (Plus, validating this
// is a lot of work.) So we just keep the first one we saw.
// That's an arbitrary choice, but in theory the only way it
// fails is if the server is using features newer than this
// version of gqlparser, in which case they're in trouble
// anyway.
default:
return nil, gqlerror.ErrorPosf(
dir.Position,
"Cannot redeclare directive %s.",
dir.Name,
)
}
}
schema.Directives[dir.Name] = sd.Directives[i]
}
if len(sd.Schema) > 1 {
return nil, gqlerror.ErrorPosf(
sd.Schema[1].Position,
"Cannot have multiple schema entry points, consider schema extensions instead.",
)
}
if len(sd.Schema) == 1 {
schema.Description = sd.Schema[0].Description
for _, entrypoint := range sd.Schema[0].OperationTypes {
def := schema.Types[entrypoint.Type]
if def == nil {
return nil, gqlerror.ErrorPosf(
entrypoint.Position,
"Schema root %s refers to a type %s that does not exist.",
entrypoint.Operation,
entrypoint.Type,
)
}
switch entrypoint.Operation {
case Query:
schema.Query = def
case Mutation:
schema.Mutation = def
case Subscription:
schema.Subscription = def
}
}
if err := validateDirectives(
&schema,
sd.Schema[0].Directives,
LocationSchema,
nil,
); err != nil {
return nil, err
}
schema.SchemaDirectives = append(schema.SchemaDirectives, sd.Schema[0].Directives...)
}
for _, ext := range sd.SchemaExtension {
for _, entrypoint := range ext.OperationTypes {
def := schema.Types[entrypoint.Type]
if def == nil {
return nil, gqlerror.ErrorPosf(
entrypoint.Position,
"Schema root %s refers to a type %s that does not exist.",
entrypoint.Operation,
entrypoint.Type,
)
}
switch entrypoint.Operation {
case Query:
schema.Query = def
case Mutation:
schema.Mutation = def
case Subscription:
schema.Subscription = def
}
}
if err := validateDirectives(&schema, ext.Directives, LocationSchema, nil); err != nil {
return nil, err
}
schema.SchemaDirectives = append(schema.SchemaDirectives, ext.Directives...)
}
if err := validateTypeDefinitions(&schema); err != nil {
return nil, err
}
if err := validateDirectiveDefinitions(&schema); err != nil {
return nil, err
}
// Inferred root operation type names should be performed only when a `schema` directive is
// **not** provided, when it is, `Mutation` and `Subscription` becomes valid types and are not
// assigned as a root operation on the schema.
if len(sd.Schema) == 0 {
if schema.Query == nil && schema.Types["Query"] != nil {
schema.Query = schema.Types["Query"]
}
if schema.Mutation == nil && schema.Types["Mutation"] != nil {
schema.Mutation = schema.Types["Mutation"]
}
if schema.Subscription == nil && schema.Types["Subscription"] != nil {
schema.Subscription = schema.Types["Subscription"]
}
}
if schema.Query != nil {
schema.Query.Fields = append(
schema.Query.Fields,
&FieldDefinition{
Name: "__schema",
Type: NonNullNamedType("__Schema", nil),
},
&FieldDefinition{
Name: "__type",
Type: NamedType("__Type", nil),
Arguments: ArgumentDefinitionList{
{Name: "name", Type: NonNullNamedType("String", nil)},
},
},
)
}
return &schema, nil
}
func validateTypeDefinitions(schema *Schema) *gqlerror.Error {
types := make([]string, 0, len(schema.Types))
for typ := range schema.Types {
types = append(types, typ)
}
sort.Strings(types)
for _, typ := range types {
err := validateDefinition(schema, schema.Types[typ])
if err != nil {
return err
}
}
return nil
}
func validateDirectiveDefinitions(schema *Schema) *gqlerror.Error {
directives := make([]string, 0, len(schema.Directives))
for directive := range schema.Directives {
directives = append(directives, directive)
}
sort.Strings(directives)
for _, directive := range directives {
err := validateDirective(schema, schema.Directives[directive])
if err != nil {
return err
}
}
return nil
}
func validateDirective(schema *Schema, def *DirectiveDefinition) *gqlerror.Error {
if err := validateName(def.Position, def.Name); err != nil {
// now, GraphQL spec doesn't have reserved directive name
return err
}
return validateArgs(schema, def.Arguments, def)
}
func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
for _, field := range def.Fields {
if err := validateName(field.Position, field.Name); err != nil {
// now, GraphQL spec doesn't have reserved field name
return err
}
if err := validateTypeRef(schema, field.Type); err != nil {
return err
}
if err := validateArgs(schema, field.Arguments, nil); err != nil {
return err
}
wantDirLocation := LocationFieldDefinition
if def.Kind == InputObject {
wantDirLocation = LocationInputFieldDefinition
}
if err := validateDirectives(schema, field.Directives, wantDirLocation, nil); err != nil {
return err
}
}
for _, typ := range def.Types {
typDef := schema.Types[typ]
if typDef == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(typ))
}
if !isValidKind(typDef.Kind, Object) {
return gqlerror.ErrorPosf(
def.Position,
"%s type %s must be %s.",
def.Kind,
strconv.Quote(typ),
kindList(Object),
)
}
}
for _, intf := range def.Interfaces {
if err := validateImplements(schema, def, intf); err != nil {
return err
}
}
switch def.Kind {
case Object, Interface:
if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(
def.Position,
"%s %s: must define one or more fields.",
def.Kind,
def.Name,
)
}
for _, field := range def.Fields {
if typ, ok := schema.Types[field.Type.Name()]; ok {
if !isValidKind(typ.Kind, Scalar, Object, Interface, Union, Enum) {
return gqlerror.ErrorPosf(
field.Position,
"%s %s: field must be one of %s.",
def.Kind,
def.Name,
kindList(Scalar, Object, Interface, Union, Enum),
)
}
}
}
case Enum:
if len(def.EnumValues) == 0 {
return gqlerror.ErrorPosf(
def.Position,
"%s %s: must define one or more unique enum values.",
def.Kind,
def.Name,
)
}
for _, value := range def.EnumValues {
for _, nonEnum := range [3]string{"true", "false", "null"} {
if value.Name == nonEnum {
return gqlerror.ErrorPosf(
def.Position,
"%s %s: non-enum value %s.",
def.Kind,
def.Name,
value.Name,
)
}
}
if err := validateDirectives(
schema,
value.Directives,
LocationEnumValue,
nil,
); err != nil {
return err
}
}
case InputObject:
if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(
def.Position,
"%s %s: must define one or more input fields.",
def.Kind,
def.Name,
)
}
for _, field := range def.Fields {
if typ, ok := schema.Types[field.Type.Name()]; ok {
if !isValidKind(typ.Kind, Scalar, Enum, InputObject) {
return gqlerror.ErrorPosf(
field.Position,
"%s %s: field must be one of %s.",
typ.Kind,
field.Name,
kindList(Scalar, Enum, InputObject),
)
}
}
}
}
for idx, field1 := range def.Fields {
for _, field2 := range def.Fields[idx+1:] {
if field1.Name == field2.Name {
return gqlerror.ErrorPosf(
field2.Position,
"Field %s.%s can only be defined once.",
def.Name,
field2.Name,
)
}
}
}
if !def.BuiltIn {
// GraphQL spec has reserved type names a lot!
err := validateName(def.Position, def.Name)
if err != nil {
return err
}
}
return validateDirectives(schema, def.Directives, DirectiveLocation(def.Kind), nil)
}
func validateTypeRef(schema *Schema, typ *Type) *gqlerror.Error {
if schema.Types[typ.Name()] == nil {
return gqlerror.ErrorPosf(typ.Position, "Undefined type %s.", typ.Name())
}
return nil
}
func validateArgs(
schema *Schema,
args ArgumentDefinitionList,
currentDirective *DirectiveDefinition,
) *gqlerror.Error {
for _, arg := range args {
if err := validateName(arg.Position, arg.Name); err != nil {
// now, GraphQL spec doesn't have reserved argument name
return err
}
if err := validateTypeRef(schema, arg.Type); err != nil {
return err
}
def := schema.Types[arg.Type.Name()]
if !def.IsInputType() {
return gqlerror.ErrorPosf(
arg.Position,
"cannot use %s as argument %s because %s is not a valid input type",
arg.Type.String(),
arg.Name,
def.Kind,
)
}
if err := validateDirectives(
schema,
arg.Directives,
LocationArgumentDefinition,
currentDirective,
); err != nil {
return err
}
}
return nil
}
func validateDirectives(
schema *Schema,
dirs DirectiveList,
location DirectiveLocation,
currentDirective *DirectiveDefinition,
) *gqlerror.Error {
for _, dir := range dirs {
if err := validateName(dir.Position, dir.Name); err != nil {
// now, GraphQL spec doesn't have reserved directive name
return err
}
if currentDirective != nil && dir.Name == currentDirective.Name {
return gqlerror.ErrorPosf(
dir.Position,
"Directive %s cannot refer to itself.",
currentDirective.Name,
)
}
dirDefinition := schema.Directives[dir.Name]
if dirDefinition == nil {
return gqlerror.ErrorPosf(dir.Position, "Undefined directive %s.", dir.Name)
}
validKind := slices.Contains(dirDefinition.Locations, location)
if !validKind {
return gqlerror.ErrorPosf(
dir.Position,
"Directive %s is not applicable on %s.",
dir.Name,
location,
)
}
for _, arg := range dir.Arguments {
if dirDefinition.Arguments.ForName(arg.Name) == nil {
return gqlerror.ErrorPosf(
arg.Position,
"Undefined argument %s for directive %s.",
arg.Name,
dir.Name,
)
}
}
for _, schemaArg := range dirDefinition.Arguments {
if schemaArg.Type.NonNull && schemaArg.DefaultValue == nil {
if arg := dir.Arguments.ForName(
schemaArg.Name,
); arg == nil ||
arg.Value.Kind == NullValue {
return gqlerror.ErrorPosf(
dir.Position,
"Argument %s for directive %s cannot be null.",
schemaArg.Name,
dir.Name,
)
}
}
}
dir.Definition = schema.Directives[dir.Name]
}
return nil
}
func validateImplements(schema *Schema, def *Definition, intfName string) *gqlerror.Error {
// see validation rules at the bottom of
// https://spec.graphql.org/October2021/#sec-Objects
intf := schema.Types[intfName]
if intf == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName))
}
if intf.Kind != Interface {
return gqlerror.ErrorPosf(
def.Position,
"%s is a non interface type %s.",
strconv.Quote(intfName),
intf.Kind,
)
}
for _, requiredField := range intf.Fields {
foundField := def.Fields.ForName(requiredField.Name)
if foundField == nil {
return gqlerror.ErrorPosf(def.Position,
`For %s to implement %s it must have a field called %s.`,
def.Name, intf.Name, requiredField.Name,
)
}
if !isCovariant(schema, requiredField.Type, foundField.Type) {
return gqlerror.ErrorPosf(foundField.Position,
`For %s to implement %s the field %s must have type %s.`,
def.Name, intf.Name, requiredField.Name, requiredField.Type.String(),
)
}
for _, requiredArg := range requiredField.Arguments {
foundArg := foundField.Arguments.ForName(requiredArg.Name)
if foundArg == nil {
return gqlerror.ErrorPosf(
foundField.Position,
`For %s to implement %s the field %s must have the same arguments but it is missing %s.`,
def.Name,
intf.Name,
requiredField.Name,
requiredArg.Name,
)
}
if !requiredArg.Type.IsCompatible(foundArg.Type) {
return gqlerror.ErrorPosf(
foundArg.Position,
`For %s to implement %s the field %s must have the same arguments but %s has the wrong type.`,
def.Name,
intf.Name,
requiredField.Name,
requiredArg.Name,
)
}
}
for _, foundArgs := range foundField.Arguments {
if requiredField.Arguments.ForName(foundArgs.Name) == nil && foundArgs.Type.NonNull &&
foundArgs.DefaultValue == nil {
return gqlerror.ErrorPosf(
foundArgs.Position,
`For %s to implement %s any additional arguments on %s must be optional or have a default value but %s is required.`,
def.Name,
intf.Name,
foundField.Name,
foundArgs.Name,
)
}
}
}
return validateTypeImplementsAncestors(schema, def, intfName)
}
// validateTypeImplementsAncestors
// https://github.com/graphql/graphql-js/blob/47bd8c8897c72d3efc17ecb1599a95cee6bac5e8/src/type/validate.ts#L428
func validateTypeImplementsAncestors(
schema *Schema,
def *Definition,
intfName string,
) *gqlerror.Error {
intf := schema.Types[intfName]
if intf == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName))
}
for _, transitive := range intf.Interfaces {
if !containsString(def.Interfaces, transitive) {
if transitive == def.Name {
return gqlerror.ErrorPosf(def.Position,
`Type %s cannot implement %s because it would create a circular reference.`,
def.Name, intfName,
)
}
return gqlerror.ErrorPosf(def.Position,
`Type %s must implement %s because it is implemented by %s.`,
def.Name, transitive, intfName,
)
}
}
return nil
}
func containsString(slice []string, want string) bool {
return slices.Contains(slice, want)
}
func isCovariant(schema *Schema, required, actual *Type) bool {
if required.NonNull && !actual.NonNull {
return false
}
if required.NamedType != "" {
if required.NamedType == actual.NamedType {
return true
}
for _, pt := range schema.PossibleTypes[required.NamedType] {
if pt.Name == actual.NamedType {
return true
}
}
return false
}
if required.Elem != nil && actual.Elem == nil {
return false
}
return isCovariant(schema, required.Elem, actual.Elem)
}
func validateName(pos *Position, name string) *gqlerror.Error {
if strings.HasPrefix(name, "__") {
return gqlerror.ErrorPosf(
pos,
`Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`,
name,
)
}
return nil
}
func isValidKind(kind DefinitionKind, valid ...DefinitionKind) bool {
return slices.Contains(valid, kind)
}
func kindList(kinds ...DefinitionKind) string {
s := make([]string, len(kinds))
for i, k := range kinds {
s[i] = string(k)
}
return strings.Join(s, ", ")
}
+731
View File
@@ -0,0 +1,731 @@
types:
- name: cannot be redeclared
input: |
type A {
name: String
}
type A {
name: String
}
error:
message: "Cannot redeclare type A."
locations: [{line: 4, column: 6}]
- name: cannot be duplicated field at same definition 1
input: |
type A {
name: String
name: String
}
error:
message: "Field A.name can only be defined once."
locations: [{line: 3, column: 3}]
- name: cannot be duplicated field at same definition 2
input: |
type A {
name: String
}
extend type A {
name: String
}
error:
message: "Field A.name can only be defined once."
locations: [{line: 5, column: 3}]
- name: cannot be duplicated field at same definition 3
input: |
type A {
name: String
}
extend type A {
age: Int
age: Int
}
error:
message: "Field A.age can only be defined once."
locations: [{line: 6, column: 3}]
object types:
- name: must define one or more fields
input: |
directive @D on OBJECT
# This pattern rejected by parser
# type InvalidObject1 {}
type InvalidObject2 @D
type ValidObject {
id: ID
}
extend type ValidObject @D
extend type ValidObject {
b: Int
}
error:
message: 'OBJECT InvalidObject2: must define one or more fields.'
locations: [{line: 6, column: 6}]
- name: check reserved names on type name
input: |
type __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}]
- name: check reserved names on type field
input: |
type FooBar {
__id: ID
}
error:
message: 'Name "__id" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 3}]
- name: field argument list must not be empty
input: |
type FooBar {
foo(): ID
}
error:
message: 'expected at least one definition, found )'
locations: [{line: 2, column: 7}]
- name: check reserved names on type field argument
input: |
type FooBar {
foo(__bar: ID): ID
}
error:
message: 'Name "__bar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 7}]
- name: must not allow input object as field type
input: |
input Input {
id: ID
}
type Query {
input: Input!
}
error:
message: 'OBJECT Query: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
locations: [{line: 5, column: 3}]
interfaces:
- name: must exist
input: |
type Thing implements Object {
id: ID!
}
type Query {
Things: [Thing!]!
}
error:
message: 'Undefined type "Object".'
locations: [{line: 1, column: 6}]
- name: must be an interface
input: |
type Thing implements Object {
id: ID!
}
type Query {
Things: [Thing!]!
}
type Object {
name: String
}
error:
message: '"Object" is a non interface type OBJECT.'
locations: [{line: 1, column: 6}]
- name: must define one or more fields
input: |
directive @D on INTERFACE
# This pattern rejected by parser
# interface InvalidInterface1 {}
interface InvalidInterface2 @D
interface ValidInterface {
id: ID
}
extend interface ValidInterface @D
extend interface ValidInterface {
b: Int
}
error:
message: 'INTERFACE InvalidInterface2: must define one or more fields.'
locations: [{line: 6, column: 11}]
- name: check reserved names on type name
input: |
interface __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 11}]
- name: must not allow input object as field type
input: |
input Input {
id: ID
}
type Query {
foo: Foo!
}
interface Foo {
input: Input!
}
error:
message: 'INTERFACE Foo: field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
locations: [{line: 8, column: 3}]
- name: must have all fields from interface
input: |
type Bar implements BarInterface {
someField: Int!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface it must have a field called id.'
locations: [{line: 1, column: 6}]
- name: must have same type of fields
input: |
type Bar implements BarInterface {
id: Int!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have type ID!.'
locations: [{line: 2, column: 5}]
- name: must have all required arguments
input: |
type Bar implements BarInterface {
id: ID!
}
interface BarInterface {
id(ff: Int!): ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have the same arguments but it is missing ff.'
locations: [{line: 2, column: 5}]
- name: must have same argument types
input: |
type Bar implements BarInterface {
id(ff: ID!): ID!
}
interface BarInterface {
id(ff: Int!): ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have the same arguments but ff has the wrong type.'
locations: [{line: 2, column: 8}]
- name: may defined additional nullable arguments
input: |
type Bar implements BarInterface {
id(opt: Int): ID!
}
interface BarInterface {
id: ID!
}
- name: may defined additional required arguments with defaults
input: |
type Bar implements BarInterface {
id(opt: Int! = 1): ID!
}
interface BarInterface {
id: ID!
}
- name: must not define additional required arguments without defaults
input: |
type Bar implements BarInterface {
id(opt: Int!): ID!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface any additional arguments on id must be optional or have a default value but opt is required.'
locations: [{line: 2, column: 8}]
- name: can have covariant argument types
input: |
union U = A|B
type A { name: String }
type B { name: String }
type Bar implements BarInterface {
f: A!
}
interface BarInterface {
f: U!
}
- name: may define intermediate interfaces
input: |
interface IA {
id: ID!
}
interface IIA implements IA {
id: ID!
}
type A implements IIA & IA {
id: ID!
}
- name: Type Foo must implement Baz because it is implemented by Bar
input: |
interface Baz {
baz: String
}
interface Bar implements Baz {
bar: String
baz: String
}
type Foo implements Bar {
foo: String
bar: String
baz: String
}
error:
message: 'Type Foo must implement Baz because it is implemented by Bar.'
locations: [{line: 10, column: 6}]
- name: circular reference error
input: |
interface Circular1 implements Circular2 {
id: ID!
}
interface Circular2 implements Circular1 {
id: ID!
}
error:
message: 'Type Circular1 cannot implement Circular2 because it would create a circular reference.'
locations: [{line: 1, column: 11}]
inputs:
- name: must define one or more input fields
input: |
directive @D on INPUT_OBJECT
# This pattern rejected by parser
# input InvalidInput1 {}
input InvalidInput2 @D
input ValidInput {
id: ID
}
extend input ValidInput @D
extend input ValidInput {
b: Int
}
error:
message: 'INPUT_OBJECT InvalidInput2: must define one or more input fields.'
locations: [{line: 6, column: 7}]
- name: check reserved names on type name
input: |
input __FooBar {
id: ID
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 7}]
- name: fields cannot be Objects
input: |
type Object { id: ID }
input Foo { a: Object! }
error:
message: 'OBJECT a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
locations: [{line: 2, column: 13}]
- name: fields cannot be Interfaces
input: |
interface Interface { id: ID! }
input Foo { a: Interface! }
error:
message: 'INTERFACE a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
locations: [{line: 2, column: 13}]
- name: fields cannot be Unions
input: |
type Object { id: ID }
union Union = Object
input Foo { a: Union! }
error:
message: 'UNION a: field must be one of SCALAR, ENUM, INPUT_OBJECT.'
locations: [{line: 3, column: 13}]
args:
- name: Valid arg types
input: |
input Input { id: ID }
enum Enum { A }
scalar Scalar
type Query {
f(a: Input, b: Scalar, c: Enum): Boolean!
}
- name: Objects not allowed
input: |
type Object { id: ID }
type Query { f(a: Object): Boolean! }
error:
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
locations: [{line: 2, column: 16}]
- name: Union not allowed
input: |
type Object { id: ID }
union Union = Object
type Query { f(a: Union): Boolean! }
error:
message: 'cannot use Union as argument a because UNION is not a valid input type'
locations: [{line: 3, column: 16}]
- name: Interface not allowed
input: |
interface Interface { id: ID }
type Query { f(a: Interface): Boolean! }
error:
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
locations: [{line: 2, column: 16}]
enums:
- name: must define one or more unique enum values
input: |
directive @D on ENUM
# This pattern rejected by parser
# enum InvalidEmum1 {}
enum InvalidEnum2 @D
enum ValidEnum {
FOO
}
extend enum ValidEnum @D
extend enum ValidEnum {
BAR
}
error:
message: 'ENUM InvalidEnum2: must define one or more unique enum values.'
locations: [{line: 6, column: 6}]
- name: check reserved names on type name
input: |
enum __FooBar {
A
B
}
error:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}]
unions:
- name: union types must be defined
input: |
union Foo = Bar | Baz
type Bar {
id: ID
}
error:
message: "Undefined type \"Baz\"."
locations: [{line: 1, column: 7}]
- name: union types must be objects
input: |
union Foo = Baz
interface Baz {
id: ID
}
error:
message: "UNION type \"Baz\" must be OBJECT."
locations: [{line: 1, column: 7}]
- name: unions of pure type extensions are valid
input: |
type Review {
body: String!
author: User! @provides(fields: "username")
product: Product!
}
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review]
}
extend type Product @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}
union Foo = User | Product
scalar _Any
scalar _FieldSet
directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
directive @extends on OBJECT
type extensions:
- name: can extend non existant types
input: |
extend type A {
name: String
}
- name: cannot extend incorret type existant types
input: |
scalar A
extend type A {
name: String
}
error:
message: "Cannot extend type A because the base type is a SCALAR, not OBJECT."
locations: [{line: 2, column: 13}]
directives:
- name: cannot redeclare directives
input: |
directive @A on FIELD_DEFINITION
directive @A on FIELD_DEFINITION
error:
message: "Cannot redeclare directive A."
locations: [{line: 2, column: 12}]
- name: can redeclare builtin directives
input: |
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
- name: must be declared (type)
input: |
type User @foo {
name: String
}
error:
message: "Undefined directive foo."
locations: [{line: 1, column: 12}]
- name: must be declared (field)
input: |
type User {
name: String @foo
}
error:
message: "Undefined directive foo."
locations: [{line: 2, column: 17}]
- name: must be declared (enum)
input: |
enum Unit {
METER @foo
}
error:
message: "Undefined directive foo."
locations: [{line: 2, column: 10}]
- name: cannot be self-referential
input: |
directive @A(foo: Int! @A) on FIELD_DEFINITION
error:
message: "Directive A cannot refer to itself."
locations: [{line: 1, column: 25}]
- name: check reserved names on type name
input: |
directive @__A on FIELD_DEFINITION
error:
message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 12}]
- name: Valid arg types
input: |
input Input { id: ID }
enum Enum { A }
scalar Scalar
directive @A(a: Input, b: Scalar, c: Enum) on FIELD_DEFINITION
- name: Objects not allowed
input: |
type Object { id: ID }
directive @A(a: Object) on FIELD_DEFINITION
error:
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
locations: [{line: 2, column: 14}]
- name: Union not allowed
input: |
type Object { id: ID }
union Union = Object
directive @A(a: Union) on FIELD_DEFINITION
error:
message: 'cannot use Union as argument a because UNION is not a valid input type'
locations: [{line: 3, column: 14}]
- name: Interface not allowed
input: |
interface Interface { id: ID }
directive @A(a: Interface) on FIELD_DEFINITION
error:
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
locations: [{line: 2, column: 14}]
- name: Invalid location usage not allowed
input: |
directive @test on FIELD_DEFINITION
input I1 @test { f: String }
error:
message: 'Directive test is not applicable on INPUT_OBJECT.'
locations: [{line: 2, column: 11}]
- name: Valid location usage
input: |
directive @testInputField on INPUT_FIELD_DEFINITION
directive @testField on FIELD_DEFINITION
directive @inp on INPUT_OBJECT
input I1 @inp { f: String @testInputField }
type P { name: String @testField }
interface I { id: ID @testField }
- name: Invalid directive argument not allowed
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo(foobla: 11)}
error:
message: 'Undefined argument foobla for directive foo.'
locations: [{line: 2, column: 21}]
- name: non-null argument must be provided
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo }
error:
message: 'Argument bla for directive foo cannot be null.'
locations: [{line: 2, column: 17}]
- name: non-null argument must not be null
input: |
directive @foo(bla: Int!) on FIELD_DEFINITION
type P {f: Int @foo(bla: null) }
error:
message: 'Argument bla for directive foo cannot be null.'
locations: [{line: 2, column: 17}]
entry points:
- name: multiple schema entry points
input: |
schema {
query: Query
}
schema {
query: Query
}
scalar Query
error:
message: "Cannot have multiple schema entry points, consider schema extensions instead."
locations: [{line: 4, column: 8}]
- name: Undefined schema entrypoint
input: |
schema {
query: Query
}
error:
message: "Schema root query refers to a type Query that does not exist."
locations: [{line: 2, column: 3}]
entry point extensions:
- name: Undefined schema entrypoint
input: |
schema {
query: Query
}
scalar Query
extend schema {
mutation: Mutation
}
error:
message: "Schema root mutation refers to a type Mutation that does not exist."
locations: [{line: 6, column: 3}]
type references:
- name: Field types
input: |
type User {
posts: Post
}
error:
message: "Undefined type Post."
locations: [{line: 2, column: 10}]
- name: Arg types
input: |
type User {
posts(foo: FooBar): String
}
error:
message: "Undefined type FooBar."
locations: [{line: 2, column: 14}]
- name: Directive arg types
input: |
directive @Foo(foo: FooBar) on FIELD_DEFINITION
error:
message: "Undefined type FooBar."
locations: [{line: 1, column: 21}]
- name: Invalid enum value
input: |
enum Enum { true }
error:
message: "ENUM Enum: non-enum value true."
locations: [{line: 1, column: 6}]
+163
View File
@@ -0,0 +1,163 @@
package validator
import (
"sort"
//nolint:staticcheck // bad, yeah
. "github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/validator/core"
validatorrules "github.com/vektah/gqlparser/v2/validator/rules"
)
type (
AddErrFunc = core.AddErrFunc
RuleFunc = core.RuleFunc
Rule = core.Rule
Events = core.Events
ErrorOption = core.ErrorOption
Walker = core.Walker
)
var (
Message = core.Message
QuotedOrList = core.QuotedOrList
OrList = core.OrList
)
// Walk is an alias for core.Walk.
func Walk(schema *Schema, document *QueryDocument, observers *Events) {
core.Walk(schema, document, observers)
}
var specifiedRules []Rule
func init() {
// Initialize specifiedRules with default rules
defaultRules := validatorrules.NewDefaultRules()
for name, ruleFunc := range defaultRules.GetInner() {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
// ensure initial default is in deterministic order
sort.Sort(core.NameSorter(specifiedRules))
}
}
// AddRule adds a rule to the rule set.
// ruleFunc is called once each time `Validate` is executed.
func AddRule(name string, ruleFunc RuleFunc) {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
}
// RemoveRule removes an existing rule from the rule set
// if one of the same name exists.
// The rule set is global, so it is not safe for concurrent changes.
func RemoveRule(name string) {
var result []Rule //nolint:prealloc // using initialized with len(rules) produces a race condition
for _, r := range specifiedRules {
if r.Name == name {
continue
}
result = append(result, r)
}
specifiedRules = result
}
// ReplaceRule replaces an existing rule from the rule set
// if one of the same name exists.
// If no match is found, it will add a new rule to the rule set.
// The rule set is global, so it is not safe for concurrent changes.
func ReplaceRule(name string, ruleFunc RuleFunc) {
var found bool
var result []Rule //nolint:prealloc // using initialized with len(rules) produces a race condition
for _, r := range specifiedRules {
if r.Name == name {
found = true
result = append(result, Rule{Name: name, RuleFunc: ruleFunc})
continue
}
result = append(result, r)
}
if !found {
specifiedRules = append(specifiedRules, Rule{Name: name, RuleFunc: ruleFunc})
return
}
specifiedRules = result
}
// Deprecated: use ValidateWithRules instead.
func Validate(schema *Schema, doc *QueryDocument, rules ...Rule) gqlerror.List {
if rules == nil {
rules = specifiedRules
}
var errs gqlerror.List
if schema == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as Schema is nil"))
}
if doc == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as QueryDocument is nil"))
}
if len(errs) > 0 {
return errs
}
observers := &core.Events{}
for i := range rules {
rule := rules[i]
rule.RuleFunc(observers, func(options ...ErrorOption) {
err := &gqlerror.Error{
Rule: rule.Name,
}
for _, o := range options {
o(err)
}
errs = append(errs, err)
})
}
Walk(schema, doc, observers)
return errs
}
func ValidateWithRules(
schema *Schema,
doc *QueryDocument,
rules *validatorrules.Rules,
) gqlerror.List {
if rules == nil {
rules = validatorrules.NewDefaultRules()
}
var errs gqlerror.List
if schema == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as Schema is nil"))
}
if doc == nil {
errs = append(errs, gqlerror.Errorf("cannot validate as QueryDocument is nil"))
}
if len(errs) > 0 {
return errs
}
observers := &core.Events{}
var currentRules []Rule //nolint:prealloc // would require extra local refs for len
for name, ruleFunc := range rules.GetInner() {
currentRules = append(currentRules, Rule{Name: name, RuleFunc: ruleFunc})
// ensure deterministic order evaluation
sort.Sort(core.NameSorter(currentRules))
}
for _, currentRule := range currentRules {
currentRule.RuleFunc(observers, func(options ...ErrorOption) {
err := &gqlerror.Error{
Rule: currentRule.Name,
}
for _, o := range options {
o(err)
}
errs = append(errs, err)
})
}
Walk(schema, doc, observers)
return errs
}
+285
View File
@@ -0,0 +1,285 @@
package validator
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
//nolint:staticcheck // We do not care about capitalized error strings
var ErrUnexpectedType = errors.New("Unexpected Type")
// VariableValues coerces and validates variable values.
func VariableValues(
schema *ast.Schema,
op *ast.OperationDefinition,
variables map[string]any,
) (map[string]any, error) {
coercedVars := map[string]any{}
validator := varValidator{
path: ast.Path{ast.PathName("variable")},
schema: schema,
}
for _, v := range op.VariableDefinitions {
validator.path = append(validator.path, ast.PathName(v.Variable))
if !v.Definition.IsInputType() {
return nil, gqlerror.ErrorPathf(validator.path, "must an input type")
}
val, hasValue := variables[v.Variable]
if !hasValue {
if v.DefaultValue != nil {
var err error
val, err = v.DefaultValue.Value(nil)
if err != nil {
return nil, gqlerror.WrapPath(validator.path, err)
}
hasValue = true
} else if v.Type.NonNull {
return nil, gqlerror.ErrorPathf(validator.path, "must be defined")
}
}
if hasValue {
if val == nil {
if v.Type.NonNull {
return nil, gqlerror.ErrorPathf(validator.path, "cannot be null")
}
coercedVars[v.Variable] = nil
} else {
rv := reflect.ValueOf(val)
jsonNumber, isJSONNumber := val.(json.Number)
if isJSONNumber {
switch v.Type.NamedType {
case "Int":
n, err := jsonNumber.Int64()
if err != nil {
return nil, gqlerror.ErrorPathf(
validator.path,
"cannot use value %d as %s",
n,
v.Type.NamedType,
)
}
rv = reflect.ValueOf(n)
case "Float":
f, err := jsonNumber.Float64()
if err != nil {
return nil, gqlerror.ErrorPathf(
validator.path,
"cannot use value %f as %s",
f,
v.Type.NamedType,
)
}
rv = reflect.ValueOf(f)
}
}
if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
rv = rv.Elem()
}
rval, err := validator.validateVarType(v.Type, rv)
if err != nil {
return nil, err
}
coercedVars[v.Variable] = rval.Interface()
}
}
validator.path = validator.path[0 : len(validator.path)-1]
}
return coercedVars, nil
}
type varValidator struct {
path ast.Path
schema *ast.Schema
}
func (v *varValidator) validateVarType(
typ *ast.Type,
val reflect.Value,
) (reflect.Value, *gqlerror.Error) {
currentPath := v.path
resetPath := func() {
v.path = currentPath
}
defer resetPath()
if typ.Elem != nil {
if val.Kind() != reflect.Slice {
// GraphQL spec says that non-null values should be coerced to an array when possible.
// Hence if the value is not a slice, we create a slice and add val to it.
slc := reflect.MakeSlice(reflect.SliceOf(val.Type()), 0, 0)
slc = reflect.Append(slc, val)
val = slc
}
for i := 0; i < val.Len(); i++ {
resetPath()
v.path = append(v.path, ast.PathIndex(i))
field := val.Index(i)
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
if typ.Elem.NonNull && field.IsNil() {
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
}
field = field.Elem()
}
_, err := v.validateVarType(typ.Elem, field)
if err != nil {
return val, err
}
}
return val, nil
}
def := v.schema.Types[typ.NamedType]
if def == nil {
panic(fmt.Errorf("missing def for %s", typ.NamedType))
}
if !typ.NonNull && !val.IsValid() {
// If the type is not null and we got a invalid value namely null/nil, then it's valid
return val, nil
}
switch def.Kind {
case ast.Enum:
kind := val.Type().Kind()
if kind != reflect.Int && kind != reflect.Int32 && kind != reflect.Int64 &&
kind != reflect.String {
return val, gqlerror.ErrorPathf(v.path, "enums must be ints or strings")
}
isValidEnum := false
for _, enumVal := range def.EnumValues {
if strings.EqualFold(val.String(), enumVal.Name) {
isValidEnum = true
}
}
if !isValidEnum {
return val, gqlerror.ErrorPathf(v.path, "%s is not a valid %s", val.String(), def.Name)
}
return val, nil
case ast.Scalar:
kind := val.Type().Kind()
switch typ.NamedType {
case "Int":
if kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 ||
kind == reflect.Float32 ||
kind == reflect.Float64 ||
IsValidIntString(val, kind) {
return val, nil
}
case "Float":
if kind == reflect.Float32 || kind == reflect.Float64 || kind == reflect.Int ||
kind == reflect.Int32 ||
kind == reflect.Int64 ||
IsValidFloatString(val, kind) {
return val, nil
}
case "String":
if kind == reflect.String {
return val, nil
}
case "Boolean":
if kind == reflect.Bool {
return val, nil
}
case "ID":
if kind == reflect.Int || kind == reflect.Int32 || kind == reflect.Int64 ||
kind == reflect.String {
return val, nil
}
default:
// assume custom scalars are ok
return val, nil
}
return val, gqlerror.ErrorPathf(v.path, "cannot use %s as %s", kind.String(), typ.NamedType)
case ast.InputObject:
if val.Kind() != reflect.Map {
return val, gqlerror.ErrorPathf(v.path, "must be a %s, not a %s", def.Name, val.Kind())
}
// check for unknown fields
for _, name := range val.MapKeys() {
val.MapIndex(name)
fieldDef := def.Fields.ForName(name.String())
resetPath()
v.path = append(v.path, ast.PathName(name.String()))
switch {
case name.String() == "__typename":
continue
case fieldDef == nil:
return val, gqlerror.ErrorPathf(v.path, "unknown field")
}
}
for _, fieldDef := range def.Fields {
resetPath()
v.path = append(v.path, ast.PathName(fieldDef.Name))
field := val.MapIndex(reflect.ValueOf(fieldDef.Name))
if !field.IsValid() {
if fieldDef.Type.NonNull {
if fieldDef.DefaultValue != nil {
var err error
_, err = fieldDef.DefaultValue.Value(nil)
if err == nil {
continue
}
}
return val, gqlerror.ErrorPathf(v.path, "must be defined")
}
continue
}
if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface {
if fieldDef.Type.NonNull && field.IsNil() {
return val, gqlerror.ErrorPathf(v.path, "cannot be null")
}
// allow null object field and skip it
if !fieldDef.Type.NonNull && field.IsNil() {
continue
}
field = field.Elem()
}
cval, err := v.validateVarType(fieldDef.Type, field)
if err != nil {
return val, err
}
val.SetMapIndex(reflect.ValueOf(fieldDef.Name), cval)
}
default:
panic(fmt.Errorf("unsupported type %s", def.Kind))
}
return val, nil
}
func IsValidIntString(val reflect.Value, kind reflect.Kind) bool {
if kind != reflect.String {
return false
}
_, e := strconv.ParseInt(fmt.Sprintf("%v", val.Interface()), 10, 64)
return e == nil
}
func IsValidFloatString(val reflect.Value, kind reflect.Kind) bool {
if kind != reflect.String {
return false
}
_, e := strconv.ParseFloat(fmt.Sprintf("%v", val.Interface()), 64)
return e == nil
}