Initial QSfera import
This commit is contained in:
+24
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
+250
@@ -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
@@ -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,
|
||||
}
|
||||
Generated
Vendored
+131
@@ -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 slice’s 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)
|
||||
}
|
||||
Generated
Vendored
+49
@@ -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),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+93
@@ -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)
|
||||
},
|
||||
}
|
||||
+53
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+21
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
+37
@@ -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))
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
+79
@@ -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)
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+21
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+93
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+99
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+31
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+36
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+640
@@ -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
|
||||
}
|
||||
Generated
Vendored
+83
@@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+64
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
+46
@@ -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),
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+88
@@ -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
|
||||
}
|
||||
Generated
Vendored
+35
@@ -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]++
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+29
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+29
@@ -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
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+24
@@ -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
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+26
@@ -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]++
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+307
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+30
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
Generated
Vendored
+48
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user