Skip to content

Commit

Permalink
Allow schema to be omitted when using default root op names
Browse files Browse the repository at this point in the history
  • Loading branch information
dackroyd committed Oct 30, 2019
1 parent 0a9cfbe commit 6c0f0e3
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 8 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ func (_ *query) Hello() string { return "Hello, world!" }

func main() {
s := `
schema {
query: Query
}
type Query {
hello: String!
}
Expand Down
41 changes: 41 additions & 0 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (
if err := s.schema.Parse(schemaString, s.useStringDescriptions); err != nil {
return nil, err
}
if err := s.validateSchema(); err != nil {
return nil, err
}

r, err := resolvable.ApplyResolver(s.schema, resolver)
if err != nil {
Expand Down Expand Up @@ -183,6 +186,11 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
if op.Type == query.Subscription {
return &Response{Errors: []*errors.QueryError{&errors.QueryError{ Message: "graphql-ws protocol header is missing" }}}
}
if op.Type == query.Mutation {
if _, ok := s.schema.EntryPoints["mutation"]; !ok {
return &Response{Errors: []*errors.QueryError{{ Message: "no mutations are offered by the schema" }}}
}
}

// Fill in variables with the defaults from the operation
if variables == nil {
Expand Down Expand Up @@ -223,6 +231,39 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
}
}

func (s *Schema) validateSchema() error {
// https://graphql.github.io/graphql-spec/June2018/#sec-Root-Operation-Types
// > The query root operation type must be provided and must be an Object type.
if err := validateRootOp(s.schema, "query", true); err != nil {
return err
}
// > The mutation root operation type is optional; if it is not provided, the service does not support mutations.
// > If it is provided, it must be an Object type.
if err := validateRootOp(s.schema, "mutation", false); err != nil {
return err
}
// > Similarly, the subscription root operation type is also optional; if it is not provided, the service does not
// > support subscriptions. If it is provided, it must be an Object type.
if err := validateRootOp(s.schema, "subscription", false); err != nil {
return err
}
return nil
}

func validateRootOp(s *schema.Schema, name string, mandatory bool) error {
t, ok := s.EntryPoints[name]
if !ok {
if mandatory {
return fmt.Errorf("root operation %q must be defined", name)
}
return nil
}
if t.Kind() != "OBJECT" {
return fmt.Errorf("root operation %q must be an OBJECT", name)
}
return nil
}

func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
if len(document.Operations) == 0 {
return nil, fmt.Errorf("no operations in query document")
Expand Down
244 changes: 240 additions & 4 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,236 @@ func TestHelloSnakeArguments(t *testing.T) {
})
}

func TestRootOperations_invalidSchema(t *testing.T) {
type args struct {
Schema string
}
type want struct {
Error string
}
testTable := map[string]struct {
Args args
Want want
}{
"Empty schema": {
Want: want{Error: `root operation "query" must be defined`},
},
"Query declared by schema, but type not present": {
Args: args{
Schema: `
schema {
query: Query
}
`,
},
Want: want{Error: `graphql: type "Query" not found`},
},
"Query as incorrect type": {
Args: args{
Schema: `
schema {
query: String
}
`,
},
Want: want{Error: `root operation "query" must be an OBJECT`},
},
"Query with custom name, schema omitted": {
Args: args{
Schema: `
type QueryType {
hello: String!
}
`,
},
Want: want{Error: `root operation "query" must be defined`},
},
"Mutation as incorrect type": {
Args: args{
Schema: `
schema {
query: Query
mutation: String
}
type Query {
thing: String
}
`,
},
Want: want{Error: `root operation "mutation" must be an OBJECT`},
},
"Mutation declared by schema, but type not present": {
Args: args{
Schema: `
schema {
query: Query
mutation: Mutation
}
type Query {
hello: String!
}
`,
},
Want: want{Error: `graphql: type "Mutation" not found`},
},
}

for name, tt := range testTable {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()

_, err := graphql.ParseSchema(tt.Args.Schema, nil)
if err == nil || err.Error() != tt.Want.Error {
t.Logf("got: %v", err)
t.Logf("want: %s", tt.Want.Error)
t.Fail()
}
})
}
}

func TestRootOperations_validSchema(t *testing.T) {
type resolver struct {
helloSaidResolver
helloWorldResolver1
theNumberResolver
}
gqltesting.RunTests(t, []*gqltesting.Test{
{
// Query only, default name with `schema` omitted
Schema: graphql.MustParseSchema(`
type Query {
hello: String!
}
`, &resolver{}),
Query: `{ hello }`,
ExpectedResult: `{"hello": "Hello world!"}`,
},
{
// Query only, default name with `schema` present
Schema: graphql.MustParseSchema(`
schema {
query: Query
}
type Query {
hello: String!
}
`, &resolver{}),
Query: `{ hello }`,
ExpectedResult: `{"hello": "Hello world!"}`,
},
{
// Query only, custom name
Schema: graphql.MustParseSchema(`
schema {
query: QueryType
}
type QueryType {
hello: String!
}
`, &resolver{}),
Query: `{ hello }`,
ExpectedResult: `{"hello": "Hello world!"}`,
},
{
// Query+Mutation, default names with `schema` omitted
Schema: graphql.MustParseSchema(`
type Query {
hello: String!
}
type Mutation {
changeTheNumber(newNumber: Int!): ChangedNumber!
}
type ChangedNumber {
theNumber: Int!
}
`, &resolver{}),
Query: `
mutation {
changeTheNumber(newNumber: 1) {
theNumber
}
}
`,
ExpectedResult: `{"changeTheNumber": {"theNumber": 1}}`,
},
{
// Query+Mutation, custom names
Schema: graphql.MustParseSchema(`
schema {
query: QueryType
mutation: MutationType
}
type QueryType {
hello: String!
}
type MutationType {
changeTheNumber(newNumber: Int!): ChangedNumber!
}
type ChangedNumber {
theNumber: Int!
}
`, &resolver{}),
Query: `
mutation {
changeTheNumber(newNumber: 1) {
theNumber
}
}
`,
ExpectedResult: `{"changeTheNumber": {"theNumber": 1}}`,
},
{
// Mutation with custom name, schema omitted
Schema: graphql.MustParseSchema(`
type Query {
hello: String!
}
type MutationType {
changeTheNumber(newNumber: Int!): ChangedNumber!
}
type ChangedNumber {
theNumber: Int!
}
`, &resolver{}),
Query: `
mutation {
changeTheNumber(newNumber: 1) {
theNumber
}
}
`,
ExpectedErrors: []*gqlerrors.QueryError{{Message: "no mutations are offered by the schema"}},
},
{
// Explicit schema without mutation field
Schema: graphql.MustParseSchema(`
schema {
query: Query
}
type Query {
hello: String!
}
type Mutation {
changeTheNumber(newNumber: Int!): ChangedNumber!
}
type ChangedNumber {
theNumber: Int!
}
`, &resolver{}),
Query: `
mutation {
changeTheNumber(newNumber: 1) {
theNumber
}
}
`,
ExpectedErrors: []*gqlerrors.QueryError{{Message: "no mutations are offered by the schema"}},
},
})
}

func TestBasic(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Expand Down Expand Up @@ -3204,16 +3434,22 @@ func (r *subscriptionsInExecResolver) AppUpdated() <-chan string {
}

func TestSubscriptions_In_Exec(t *testing.T) {
r := &struct {
*helloResolver
*subscriptionsInExecResolver
}{
helloResolver: &helloResolver{},
subscriptionsInExecResolver: &subscriptionsInExecResolver{},
}
gqltesting.RunTest(t, &gqltesting.Test{
Schema: graphql.MustParseSchema(`
schema {
subscription: Subscription
type Query {
hello: String!
}
type Subscription {
appUpdated : String!
}
`, &subscriptionsInExecResolver{}),
`, r),
Query: `
subscription {
appUpdated
Expand Down
3 changes: 2 additions & 1 deletion internal/exec/resolvable/resolvable.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.
var out reflect.Type
if methodIndex != -1 {
out = m.Type.Out(0)
if typeName == "Subscription" && out.Kind() == reflect.Chan {
sub, ok := b.schema.EntryPoints["subscription"]
if ok && typeName == sub.TypeName() && out.Kind() == reflect.Chan {
out = m.Type.Out(0).Elem()
}
} else {
Expand Down
15 changes: 15 additions & 0 deletions internal/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,21 @@ func (s *Schema) Parse(schemaString string, useStringDescriptions bool) error {
}
}

// https://graphql.github.io/graphql-spec/June2018/#sec-Root-Operation-Types
// > While any type can be the root operation type for a GraphQL operation, the type system definition language can
// > omit the schema definition when the query, mutation, and subscription root types are named Query, Mutation,
// > and Subscription respectively.
if len(s.entryPointNames) == 0 {
if _, ok := s.Types["Query"]; ok {
s.entryPointNames["query"] = "Query"
}
if _, ok := s.Types["Mutation"]; ok {
s.entryPointNames["mutation"] = "Mutation"
}
if _, ok := s.Types["Subscription"]; ok {
s.entryPointNames["subscription"] = "Subscription"
}
}
s.EntryPoints = make(map[string]NamedType)
for key, name := range s.entryPointNames {
t, ok := s.Types[name]
Expand Down
Loading

0 comments on commit 6c0f0e3

Please sign in to comment.