mirror of
https://github.com/stashapp/stash.git
synced 2026-01-02 13:42:31 +01:00
401 lines
12 KiB
Go
401 lines
12 KiB
Go
//go:generate go run ./inliner/inliner.go
|
|
|
|
package validator
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
. "github.com/vektah/gqlparser/ast"
|
|
"github.com/vektah/gqlparser/gqlerror"
|
|
"github.com/vektah/gqlparser/parser"
|
|
)
|
|
|
|
func LoadSchema(inputs ...*Source) (*Schema, *gqlerror.Error) {
|
|
ast, err := parser.ParseSchemas(inputs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ValidateSchemaDocument(ast)
|
|
}
|
|
|
|
func ValidateSchemaDocument(ast *SchemaDocument) (*Schema, *gqlerror.Error) {
|
|
schema := Schema{
|
|
Types: map[string]*Definition{},
|
|
Directives: map[string]*DirectiveDefinition{},
|
|
PossibleTypes: map[string][]*Definition{},
|
|
Implements: map[string][]*Definition{},
|
|
}
|
|
|
|
for i, def := range ast.Definitions {
|
|
if schema.Types[def.Name] != nil {
|
|
return nil, gqlerror.ErrorPosf(def.Position, "Cannot redeclare type %s.", def.Name)
|
|
}
|
|
schema.Types[def.Name] = ast.Definitions[i]
|
|
}
|
|
|
|
for _, ext := range ast.Extensions {
|
|
def := schema.Types[ext.Name]
|
|
if def == nil {
|
|
return nil, gqlerror.ErrorPosf(ext.Position, "Cannot extend type %s because it does not exist.", ext.Name)
|
|
}
|
|
|
|
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 ast.Definitions {
|
|
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)
|
|
}
|
|
}
|
|
|
|
for i, dir := range ast.Directives {
|
|
if schema.Directives[dir.Name] != nil {
|
|
return nil, gqlerror.ErrorPosf(dir.Position, "Cannot redeclare directive %s.", dir.Name)
|
|
}
|
|
schema.Directives[dir.Name] = ast.Directives[i]
|
|
}
|
|
|
|
if len(ast.Schema) > 1 {
|
|
return nil, gqlerror.ErrorPosf(ast.Schema[1].Position, "Cannot have multiple schema entry points, consider schema extensions instead.")
|
|
}
|
|
|
|
if len(ast.Schema) == 1 {
|
|
for _, entrypoint := range ast.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
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, ext := range ast.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
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, typ := range schema.Types {
|
|
err := validateDefinition(&schema, typ)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, dir := range schema.Directives {
|
|
err := validateDirective(&schema, dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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: NonNullNamedType("__Type", nil),
|
|
Arguments: ArgumentDefinitionList{
|
|
{Name: "name", Type: NamedType("String", nil)},
|
|
},
|
|
},
|
|
)
|
|
}
|
|
|
|
return &schema, 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
|
|
}
|
|
if err := validateDirectives(schema, field.Directives, 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 must define one or more fields.", def.Kind)
|
|
}
|
|
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 field must be one of %s.", def.Kind, kindList(Scalar, Object, Interface, Union, Enum))
|
|
}
|
|
}
|
|
}
|
|
case Enum:
|
|
if len(def.EnumValues) == 0 {
|
|
return gqlerror.ErrorPosf(def.Position, "%s must define one or more unique enum values.", def.Kind)
|
|
}
|
|
case InputObject:
|
|
if len(def.Fields) == 0 {
|
|
return gqlerror.ErrorPosf(def.Position, "%s must define one or more input fields.", def.Kind)
|
|
}
|
|
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 field must be one of %s.", def.Kind, 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, 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, currentDirective); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDirectives(schema *Schema, dirs DirectiveList, 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)
|
|
}
|
|
if schema.Directives[dir.Name] == nil {
|
|
return gqlerror.ErrorPosf(dir.Position, "Undefined directive %s.", 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://facebook.github.io/graphql/June2018/#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 nil
|
|
}
|
|
|
|
func isCovariant(schema *Schema, required *Type, 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 {
|
|
for _, k := range valid {
|
|
if kind == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func kindList(kinds ...DefinitionKind) string {
|
|
s := make([]string, len(kinds))
|
|
for i, k := range kinds {
|
|
s[i] = string(k)
|
|
}
|
|
return strings.Join(s, ", ")
|
|
}
|