package graphql import ( "fmt" "reflect" "sort" "github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/printer" ) const ( TypeKindScalar = "SCALAR" TypeKindObject = "OBJECT" TypeKindInterface = "INTERFACE" TypeKindUnion = "UNION" TypeKindEnum = "ENUM" TypeKindInputObject = "INPUT_OBJECT" TypeKindList = "LIST" TypeKindNonNull = "NON_NULL" ) // SchemaType is type definition for __Schema var SchemaType *Object // DirectiveType is type definition for __Directive var DirectiveType *Object // TypeType is type definition for __Type var TypeType *Object // FieldType is type definition for __Field var FieldType *Object // InputValueType is type definition for __InputValue var InputValueType *Object // EnumValueType is type definition for __EnumValue var EnumValueType *Object // TypeKindEnumType is type definition for __TypeKind var TypeKindEnumType *Enum // DirectiveLocationEnumType is type definition for __DirectiveLocation var DirectiveLocationEnumType *Enum // Meta-field definitions. // SchemaMetaFieldDef Meta field definition for Schema var SchemaMetaFieldDef *FieldDefinition // TypeMetaFieldDef Meta field definition for types var TypeMetaFieldDef *FieldDefinition // TypeNameMetaFieldDef Meta field definition for type names var TypeNameMetaFieldDef *FieldDefinition func init() { TypeKindEnumType = NewEnum(EnumConfig{ Name: "__TypeKind", Description: "An enum describing what kind of type a given `__Type` is", Values: EnumValueConfigMap{ "SCALAR": &EnumValueConfig{ Value: TypeKindScalar, Description: "Indicates this type is a scalar.", }, "OBJECT": &EnumValueConfig{ Value: TypeKindObject, Description: "Indicates this type is an object. " + "`fields` and `interfaces` are valid fields.", }, "INTERFACE": &EnumValueConfig{ Value: TypeKindInterface, Description: "Indicates this type is an interface. " + "`fields` and `possibleTypes` are valid fields.", }, "UNION": &EnumValueConfig{ Value: TypeKindUnion, Description: "Indicates this type is a union. " + "`possibleTypes` is a valid field.", }, "ENUM": &EnumValueConfig{ Value: TypeKindEnum, Description: "Indicates this type is an enum. " + "`enumValues` is a valid field.", }, "INPUT_OBJECT": &EnumValueConfig{ Value: TypeKindInputObject, Description: "Indicates this type is an input object. " + "`inputFields` is a valid field.", }, "LIST": &EnumValueConfig{ Value: TypeKindList, Description: "Indicates this type is a list. " + "`ofType` is a valid field.", }, "NON_NULL": &EnumValueConfig{ Value: TypeKindNonNull, Description: "Indicates this type is a non-null. " + "`ofType` is a valid field.", }, }, }) DirectiveLocationEnumType = NewEnum(EnumConfig{ Name: "__DirectiveLocation", Description: "A Directive can be adjacent to many parts of the GraphQL language, a " + "__DirectiveLocation describes one such possible adjacencies.", Values: EnumValueConfigMap{ "QUERY": &EnumValueConfig{ Value: DirectiveLocationQuery, Description: "Location adjacent to a query operation.", }, "MUTATION": &EnumValueConfig{ Value: DirectiveLocationMutation, Description: "Location adjacent to a mutation operation.", }, "SUBSCRIPTION": &EnumValueConfig{ Value: DirectiveLocationSubscription, Description: "Location adjacent to a subscription operation.", }, "FIELD": &EnumValueConfig{ Value: DirectiveLocationField, Description: "Location adjacent to a field.", }, "FRAGMENT_DEFINITION": &EnumValueConfig{ Value: DirectiveLocationFragmentDefinition, Description: "Location adjacent to a fragment definition.", }, "FRAGMENT_SPREAD": &EnumValueConfig{ Value: DirectiveLocationFragmentSpread, Description: "Location adjacent to a fragment spread.", }, "INLINE_FRAGMENT": &EnumValueConfig{ Value: DirectiveLocationInlineFragment, Description: "Location adjacent to an inline fragment.", }, "SCHEMA": &EnumValueConfig{ Value: DirectiveLocationSchema, Description: "Location adjacent to a schema definition.", }, "SCALAR": &EnumValueConfig{ Value: DirectiveLocationScalar, Description: "Location adjacent to a scalar definition.", }, "OBJECT": &EnumValueConfig{ Value: DirectiveLocationObject, Description: "Location adjacent to a object definition.", }, "FIELD_DEFINITION": &EnumValueConfig{ Value: DirectiveLocationFieldDefinition, Description: "Location adjacent to a field definition.", }, "ARGUMENT_DEFINITION": &EnumValueConfig{ Value: DirectiveLocationArgumentDefinition, Description: "Location adjacent to an argument definition.", }, "INTERFACE": &EnumValueConfig{ Value: DirectiveLocationInterface, Description: "Location adjacent to an interface definition.", }, "UNION": &EnumValueConfig{ Value: DirectiveLocationUnion, Description: "Location adjacent to a union definition.", }, "ENUM": &EnumValueConfig{ Value: DirectiveLocationEnum, Description: "Location adjacent to an enum definition.", }, "ENUM_VALUE": &EnumValueConfig{ Value: DirectiveLocationEnumValue, Description: "Location adjacent to an enum value definition.", }, "INPUT_OBJECT": &EnumValueConfig{ Value: DirectiveLocationInputObject, Description: "Location adjacent to an input object type definition.", }, "INPUT_FIELD_DEFINITION": &EnumValueConfig{ Value: DirectiveLocationInputFieldDefinition, Description: "Location adjacent to an input object field definition.", }, }, }) // Note: some fields (for e.g "fields", "interfaces") are defined later due to cyclic reference TypeType = NewObject(ObjectConfig{ Name: "__Type", Description: "The fundamental unit of any GraphQL Schema is the type. There are " + "many kinds of types in GraphQL as represented by the `__TypeKind` enum." + "\n\nDepending on the kind of a type, certain fields describe " + "information about that type. Scalar types provide no information " + "beyond a name and description, 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.", Fields: Fields{ "kind": &Field{ Type: NewNonNull(TypeKindEnumType), Resolve: func(p ResolveParams) (interface{}, error) { switch p.Source.(type) { case *Scalar: return TypeKindScalar, nil case *Object: return TypeKindObject, nil case *Interface: return TypeKindInterface, nil case *Union: return TypeKindUnion, nil case *Enum: return TypeKindEnum, nil case *InputObject: return TypeKindInputObject, nil case *List: return TypeKindList, nil case *NonNull: return TypeKindNonNull, nil } return nil, fmt.Errorf("Unknown kind of type: %v", p.Source) }, }, "name": &Field{ Type: String, }, "description": &Field{ Type: String, }, "fields": &Field{}, "interfaces": &Field{}, "possibleTypes": &Field{}, "enumValues": &Field{}, "inputFields": &Field{}, "ofType": &Field{}, }, }) InputValueType = NewObject(ObjectConfig{ Name: "__InputValue", Description: "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.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), }, "description": &Field{ Type: String, }, "type": &Field{ Type: NewNonNull(TypeType), }, "defaultValue": &Field{ Type: String, Description: "A GraphQL-formatted string representing the default value for this " + "input value.", Resolve: func(p ResolveParams) (interface{}, error) { if inputVal, ok := p.Source.(*Argument); ok { if inputVal.DefaultValue == nil { return nil, nil } if isNullish(inputVal.DefaultValue) { return nil, nil } astVal := astFromValue(inputVal.DefaultValue, inputVal) return printer.Print(astVal), nil } if inputVal, ok := p.Source.(*InputObjectField); ok { if inputVal.DefaultValue == nil { return nil, nil } astVal := astFromValue(inputVal.DefaultValue, inputVal) return printer.Print(astVal), nil } return nil, nil }, }, }, }) FieldType = NewObject(ObjectConfig{ Name: "__Field", Description: "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.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), }, "description": &Field{ Type: String, }, "args": &Field{ Type: NewNonNull(NewList(NewNonNull(InputValueType))), Resolve: func(p ResolveParams) (interface{}, error) { if field, ok := p.Source.(*FieldDefinition); ok { return field.Args, nil } return []interface{}{}, nil }, }, "type": &Field{ Type: NewNonNull(TypeType), }, "isDeprecated": &Field{ Type: NewNonNull(Boolean), Resolve: func(p ResolveParams) (interface{}, error) { if field, ok := p.Source.(*FieldDefinition); ok { return (field.DeprecationReason != ""), nil } return false, nil }, }, "deprecationReason": &Field{ Type: String, }, }, }) DirectiveType = NewObject(ObjectConfig{ Name: "__Directive", Description: "A Directive provides a way to describe alternate runtime execution and " + "type validation behavior in a GraphQL document. " + "\n\nIn 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.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), }, "description": &Field{ Type: String, }, "locations": &Field{ Type: NewNonNull(NewList( NewNonNull(DirectiveLocationEnumType), )), }, "args": &Field{ Type: NewNonNull(NewList( NewNonNull(InputValueType), )), }, // NOTE: the following three fields are deprecated and are no longer part // of the GraphQL specification. "onOperation": &Field{ DeprecationReason: "Use `locations`.", Type: NewNonNull(Boolean), Resolve: func(p ResolveParams) (interface{}, error) { if dir, ok := p.Source.(*Directive); ok { res := false for _, loc := range dir.Locations { if loc == DirectiveLocationQuery || loc == DirectiveLocationMutation || loc == DirectiveLocationSubscription { res = true break } } return res, nil } return false, nil }, }, "onFragment": &Field{ DeprecationReason: "Use `locations`.", Type: NewNonNull(Boolean), Resolve: func(p ResolveParams) (interface{}, error) { if dir, ok := p.Source.(*Directive); ok { res := false for _, loc := range dir.Locations { if loc == DirectiveLocationFragmentSpread || loc == DirectiveLocationInlineFragment || loc == DirectiveLocationFragmentDefinition { res = true break } } return res, nil } return false, nil }, }, "onField": &Field{ DeprecationReason: "Use `locations`.", Type: NewNonNull(Boolean), Resolve: func(p ResolveParams) (interface{}, error) { if dir, ok := p.Source.(*Directive); ok { res := false for _, loc := range dir.Locations { if loc == DirectiveLocationField { res = true break } } return res, nil } return false, nil }, }, }, }) SchemaType = NewObject(ObjectConfig{ Name: "__Schema", Description: `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.`, Fields: Fields{ "types": &Field{ Description: "A list of all types supported by this server.", Type: NewNonNull(NewList( NewNonNull(TypeType), )), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { results := []Type{} for _, ttype := range schema.TypeMap() { results = append(results, ttype) } return results, nil } return []Type{}, nil }, }, "queryType": &Field{ Description: "The type that query operations will be rooted at.", Type: NewNonNull(TypeType), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { return schema.QueryType(), nil } return nil, nil }, }, "mutationType": &Field{ Description: `If this server supports mutation, the type that ` + `mutation operations will be rooted at.`, Type: TypeType, Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { if schema.MutationType() != nil { return schema.MutationType(), nil } } return nil, nil }, }, "subscriptionType": &Field{ Description: `If this server supports subscription, the type that ` + `subscription operations will be rooted at.`, Type: TypeType, Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { if schema.SubscriptionType() != nil { return schema.SubscriptionType(), nil } } return nil, nil }, }, "directives": &Field{ Description: `A list of all directives supported by this server.`, Type: NewNonNull(NewList( NewNonNull(DirectiveType), )), Resolve: func(p ResolveParams) (interface{}, error) { if schema, ok := p.Source.(Schema); ok { return schema.Directives(), nil } return nil, nil }, }, }, }) EnumValueType = NewObject(ObjectConfig{ Name: "__EnumValue", Description: "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.", Fields: Fields{ "name": &Field{ Type: NewNonNull(String), }, "description": &Field{ Type: String, }, "isDeprecated": &Field{ Type: NewNonNull(Boolean), Resolve: func(p ResolveParams) (interface{}, error) { if field, ok := p.Source.(*EnumValueDefinition); ok { return (field.DeprecationReason != ""), nil } return false, nil }, }, "deprecationReason": &Field{ Type: String, }, }, }) // Again, adding field configs to __Type that have cyclic reference here // because golang don't like them too much during init/compile-time TypeType.AddFieldConfig("fields", &Field{ Type: NewList(NewNonNull(FieldType)), Args: FieldConfigArgument{ "includeDeprecated": &ArgumentConfig{ Type: Boolean, DefaultValue: false, }, }, Resolve: func(p ResolveParams) (interface{}, error) { includeDeprecated, _ := p.Args["includeDeprecated"].(bool) switch ttype := p.Source.(type) { case *Object: if ttype == nil { return nil, nil } fields := []*FieldDefinition{} var fieldNames sort.StringSlice for name, field := range ttype.Fields() { if !includeDeprecated && field.DeprecationReason != "" { continue } fieldNames = append(fieldNames, name) } sort.Sort(fieldNames) for _, name := range fieldNames { fields = append(fields, ttype.Fields()[name]) } return fields, nil case *Interface: if ttype == nil { return nil, nil } fields := []*FieldDefinition{} for _, field := range ttype.Fields() { if !includeDeprecated && field.DeprecationReason != "" { continue } fields = append(fields, field) } return fields, nil } return nil, nil }, }) TypeType.AddFieldConfig("interfaces", &Field{ Type: NewList(NewNonNull(TypeType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *Object: return ttype.Interfaces(), nil } return nil, nil }, }) TypeType.AddFieldConfig("possibleTypes", &Field{ Type: NewList(NewNonNull(TypeType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *Interface: return p.Info.Schema.PossibleTypes(ttype), nil case *Union: return p.Info.Schema.PossibleTypes(ttype), nil } return nil, nil }, }) TypeType.AddFieldConfig("enumValues", &Field{ Type: NewList(NewNonNull(EnumValueType)), Args: FieldConfigArgument{ "includeDeprecated": &ArgumentConfig{ Type: Boolean, DefaultValue: false, }, }, Resolve: func(p ResolveParams) (interface{}, error) { includeDeprecated, _ := p.Args["includeDeprecated"].(bool) switch ttype := p.Source.(type) { case *Enum: if includeDeprecated { return ttype.Values(), nil } values := []*EnumValueDefinition{} for _, value := range ttype.Values() { if value.DeprecationReason != "" { continue } values = append(values, value) } return values, nil } return nil, nil }, }) TypeType.AddFieldConfig("inputFields", &Field{ Type: NewList(NewNonNull(InputValueType)), Resolve: func(p ResolveParams) (interface{}, error) { switch ttype := p.Source.(type) { case *InputObject: fields := []*InputObjectField{} for _, field := range ttype.Fields() { fields = append(fields, field) } return fields, nil } return nil, nil }, }) TypeType.AddFieldConfig("ofType", &Field{ Type: TypeType, }) // Note that these are FieldDefinition and not FieldConfig, // so the format for args is different. SchemaMetaFieldDef = &FieldDefinition{ Name: "__schema", Type: NewNonNull(SchemaType), Description: "Access the current type schema of this server.", Args: []*Argument{}, Resolve: func(p ResolveParams) (interface{}, error) { return p.Info.Schema, nil }, } TypeMetaFieldDef = &FieldDefinition{ Name: "__type", Type: TypeType, Description: "Request the type information of a single type.", Args: []*Argument{ { PrivateName: "name", Type: NewNonNull(String), }, }, Resolve: func(p ResolveParams) (interface{}, error) { name, ok := p.Args["name"].(string) if !ok { return nil, nil } return p.Info.Schema.Type(name), nil }, } TypeNameMetaFieldDef = &FieldDefinition{ Name: "__typename", Type: NewNonNull(String), Description: "The name of the current Object type at runtime.", Args: []*Argument{}, Resolve: func(p ResolveParams) (interface{}, error) { return p.Info.ParentType.Name(), nil }, } } // Produces a GraphQL Value AST given a Golang value. // // Optionally, a GraphQL type may be provided, which will be used to // disambiguate between value primitives. // // | JSON Value | GraphQL Value | // | ------------- | -------------------- | // | Object | Input Object | // | Array | List | // | Boolean | Boolean | // | String | String / Enum Value | // | Number | Int / Float | func astFromValue(value interface{}, ttype Type) ast.Value { if ttype, ok := ttype.(*NonNull); ok { // Note: we're not checking that the result is non-null. // This function is not responsible for validating the input value. val := astFromValue(value, ttype.OfType) return val } if isNullish(value) { return nil } valueVal := reflect.ValueOf(value) if !valueVal.IsValid() { return nil } if valueVal.Type().Kind() == reflect.Ptr { valueVal = valueVal.Elem() } if !valueVal.IsValid() { return nil } // Convert Golang slice to GraphQL list. If the Type is a list, but // the value is not an array, convert the value using the list's item type. if ttype, ok := ttype.(*List); ok { if valueVal.Type().Kind() == reflect.Slice { itemType := ttype.OfType values := []ast.Value{} for i := 0; i < valueVal.Len(); i++ { item := valueVal.Index(i).Interface() itemAST := astFromValue(item, itemType) if itemAST != nil { values = append(values, itemAST) } } return ast.NewListValue(&ast.ListValue{ Values: values, }) } // Because GraphQL will accept single values as a "list of one" when // expecting a list, if there's a non-array value and an expected list type, // create an AST using the list's item type. val := astFromValue(value, ttype.OfType) return val } if valueVal.Type().Kind() == reflect.Map { // TODO: implement astFromValue from Map to Value } if value, ok := value.(bool); ok { return ast.NewBooleanValue(&ast.BooleanValue{ Value: value, }) } if value, ok := value.(int); ok { if ttype == Float { return ast.NewIntValue(&ast.IntValue{ Value: fmt.Sprintf("%v.0", value), }) } return ast.NewIntValue(&ast.IntValue{ Value: fmt.Sprintf("%v", value), }) } if value, ok := value.(float32); ok { return ast.NewFloatValue(&ast.FloatValue{ Value: fmt.Sprintf("%v", value), }) } if value, ok := value.(float64); ok { return ast.NewFloatValue(&ast.FloatValue{ Value: fmt.Sprintf("%v", value), }) } if value, ok := value.(string); ok { if _, ok := ttype.(*Enum); ok { return ast.NewEnumValue(&ast.EnumValue{ Value: fmt.Sprintf("%v", value), }) } return ast.NewStringValue(&ast.StringValue{ Value: fmt.Sprintf("%v", value), }) } // fallback, treat as string return ast.NewStringValue(&ast.StringValue{ Value: fmt.Sprintf("%v", value), }) }