github.com/dgraph-io/dgraph@v1.2.8/graphql/schema/schemagen.go (about)

     1  /*
     2   * Copyright 2019 Dgraph Labs, Inc. and Contributors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package schema
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/pkg/errors"
    24  	"github.com/vektah/gqlparser/ast"
    25  	"github.com/vektah/gqlparser/gqlerror"
    26  	"github.com/vektah/gqlparser/parser"
    27  	"github.com/vektah/gqlparser/validator"
    28  )
    29  
    30  // A Handler can produce valid GraphQL and Dgraph schemas given an input of
    31  // types and relationships
    32  type Handler interface {
    33  	DGSchema() string
    34  	GQLSchema() string
    35  }
    36  
    37  type handler struct {
    38  	input          string
    39  	originalDefs   []string
    40  	completeSchema *ast.Schema
    41  	dgraphSchema   string
    42  }
    43  
    44  // FromString builds a GraphQL Schema from input string, or returns any parsing
    45  // or validation errors.
    46  func FromString(schema string) (Schema, error) {
    47  	// validator.Prelude includes a bunch of predefined types which help with schema introspection
    48  	// queries, hence we include it as part of the schema.
    49  	doc, gqlErr := parser.ParseSchemas(validator.Prelude, &ast.Source{Input: schema})
    50  	if gqlErr != nil {
    51  		return nil, errors.Wrap(gqlErr, "while parsing GraphQL schema")
    52  	}
    53  
    54  	gqlSchema, gqlErr := validator.ValidateSchemaDocument(doc)
    55  	if gqlErr != nil {
    56  		return nil, errors.Wrap(gqlErr, "while validating GraphQL schema")
    57  	}
    58  
    59  	return AsSchema(gqlSchema), nil
    60  }
    61  
    62  func (s *handler) GQLSchema() string {
    63  	return Stringify(s.completeSchema, s.originalDefs)
    64  }
    65  
    66  func (s *handler) DGSchema() string {
    67  	return s.dgraphSchema
    68  }
    69  
    70  // NewHandler processes the input schema.  If there are no errors, it returns
    71  // a valid Handler, otherwise it returns nil and an error.
    72  func NewHandler(input string) (Handler, error) {
    73  	if input == "" {
    74  		return nil, gqlerror.Errorf("No schema specified")
    75  	}
    76  
    77  	// The input schema contains just what's required to describe the types,
    78  	// relationships and searchability - but that's not enough to define a
    79  	// valid GraphQL schema: e.g. we allow an input schema file like
    80  	//
    81  	// type T {
    82  	//   f: Int @search
    83  	// }
    84  	//
    85  	// But, that's not valid GraphQL unless there's also definitions of scalars
    86  	// (Int, String, etc) and definitions of the directives (@search, etc).
    87  	// We don't want to make the user have those in their file and then we have
    88  	// to check that they've made the right definitions, etc, etc.
    89  	//
    90  	// So we parse the original input of just types and relationships and
    91  	// run a validation to make sure it only contains things that it should.
    92  	// To that we add all the scalars and other definitions we always require.
    93  	//
    94  	// Then, we GraphQL validate to make sure their definitions plus our additions
    95  	// is GraphQL valid.  At this point we know the definitions are GraphQL valid,
    96  	// but we need to check if it makes sense to our layer.
    97  	//
    98  	// The next final validation ensures that the definitions are made
    99  	// in such a way that our GraphQL API will be able to interpret the schema
   100  	// correctly.
   101  	//
   102  	// Then we can complete the process by adding in queries and mutations etc. to
   103  	// make the final full GraphQL schema.
   104  
   105  	doc, gqlErr := parser.ParseSchemas(validator.Prelude, &ast.Source{Input: input})
   106  	if gqlErr != nil {
   107  		return nil, gqlerror.List{gqlErr}
   108  	}
   109  
   110  	gqlErrList := preGQLValidation(doc)
   111  	if gqlErrList != nil {
   112  		return nil, gqlErrList
   113  	}
   114  
   115  	defns := make([]string, 0, len(doc.Definitions))
   116  	for _, defn := range doc.Definitions {
   117  		if defn.BuiltIn {
   118  			continue
   119  		}
   120  		defns = append(defns, defn.Name)
   121  	}
   122  
   123  	expandSchema(doc)
   124  
   125  	sch, gqlErr := validator.ValidateSchemaDocument(doc)
   126  	if gqlErr != nil {
   127  		return nil, gqlerror.List{gqlErr}
   128  	}
   129  
   130  	gqlErrList = postGQLValidation(sch, defns)
   131  	if gqlErrList != nil {
   132  		return nil, gqlErrList
   133  	}
   134  
   135  	dgSchema := genDgSchema(sch, defns)
   136  	completeSchema(sch, defns)
   137  
   138  	return &handler{
   139  		input:          input,
   140  		dgraphSchema:   dgSchema,
   141  		completeSchema: sch,
   142  		originalDefs:   defns,
   143  	}, nil
   144  }
   145  
   146  func getAllSearchIndexes(val *ast.Value) []string {
   147  	res := make([]string, len(val.Children))
   148  
   149  	for i, child := range val.Children {
   150  		res[i] = supportedSearches[child.Value.Raw].dgIndex
   151  	}
   152  
   153  	return res
   154  }
   155  
   156  func typeName(def *ast.Definition) string {
   157  	name := def.Name
   158  	dir := def.Directives.ForName(dgraphDirective)
   159  	if dir == nil {
   160  		return name
   161  	}
   162  	typeArg := dir.Arguments.ForName(dgraphTypeArg)
   163  	if typeArg == nil {
   164  		return name
   165  	}
   166  	return typeArg.Value.Raw
   167  }
   168  
   169  // fieldName returns the dgraph predicate corresponding to a field.
   170  // If the field had a dgraph directive, then it returns the value of the name field otherwise
   171  // it returns typeName + "." + fieldName.
   172  func fieldName(def *ast.FieldDefinition, typName string) string {
   173  	name := typName + "." + def.Name
   174  	dir := def.Directives.ForName(dgraphDirective)
   175  	if dir == nil {
   176  		return name
   177  	}
   178  	predArg := dir.Arguments.ForName(dgraphPredArg)
   179  	if predArg == nil {
   180  		return name
   181  	}
   182  	return predArg.Value.Raw
   183  }
   184  
   185  // genDgSchema generates Dgraph schema from a valid graphql schema.
   186  func genDgSchema(gqlSch *ast.Schema, definitions []string) string {
   187  	var typeStrings []string
   188  
   189  	for _, key := range definitions {
   190  		def := gqlSch.Types[key]
   191  		switch def.Kind {
   192  		case ast.Object, ast.Interface:
   193  			typName := typeName(def)
   194  			var typeDef, preds strings.Builder
   195  			fmt.Fprintf(&typeDef, "type %s {\n", typName)
   196  			for _, f := range def.Fields {
   197  				if f.Type.Name() == "ID" {
   198  					continue
   199  				}
   200  
   201  				typName = typeName(def)
   202  				// This field could have originally been defined in an interface that this type
   203  				// implements. If we get a parent interface, then we should prefix the field name
   204  				// with it instead of def.Name.
   205  				parentInt := parentInterface(gqlSch, def, f.Name)
   206  				if parentInt != nil {
   207  					typName = typeName(parentInt)
   208  				}
   209  				fname := fieldName(f, typName)
   210  
   211  				var prefix, suffix string
   212  				if f.Type.Elem != nil {
   213  					prefix = "["
   214  					suffix = "]"
   215  				}
   216  
   217  				var typStr string
   218  				switch gqlSch.Types[f.Type.Name()].Kind {
   219  				case ast.Object:
   220  					typStr = fmt.Sprintf("%suid%s", prefix, suffix)
   221  
   222  					fmt.Fprintf(&typeDef, "  %s: %s\n", fname, typStr)
   223  					if parentInt == nil {
   224  						fmt.Fprintf(&preds, "%s: %s .\n", fname, typStr)
   225  					}
   226  				case ast.Scalar:
   227  					typStr = fmt.Sprintf(
   228  						"%s%s%s",
   229  						prefix, scalarToDgraph[f.Type.Name()], suffix,
   230  					)
   231  
   232  					indexStr := ""
   233  					upsertStr := ""
   234  					search := f.Directives.ForName(searchDirective)
   235  					id := f.Directives.ForName(idDirective)
   236  					if id != nil {
   237  						upsertStr = "@upsert "
   238  					}
   239  
   240  					if search != nil {
   241  						arg := search.Arguments.ForName(searchArgs)
   242  						if arg != nil {
   243  							indexes := getAllSearchIndexes(arg.Value)
   244  							indexes = addHashIfRequired(f, indexes)
   245  							indexStr = fmt.Sprintf(" @index(%s)", strings.Join(indexes, ", "))
   246  						} else {
   247  							indexStr = fmt.Sprintf(" @index(%s)", defaultSearches[f.Type.Name()])
   248  						}
   249  					} else if id != nil {
   250  						indexStr = fmt.Sprintf(" @index(hash)")
   251  					}
   252  
   253  					fmt.Fprintf(&typeDef, "  %s: %s\n", fname, typStr)
   254  					if parentInt == nil {
   255  						fmt.Fprintf(&preds, "%s: %s%s %s.\n", fname, typStr, indexStr, upsertStr)
   256  					}
   257  				case ast.Enum:
   258  					typStr = fmt.Sprintf("%s%s%s", prefix, "string", suffix)
   259  
   260  					indexStr := " @index(hash)"
   261  					search := f.Directives.ForName(searchDirective)
   262  					if search != nil {
   263  						arg := search.Arguments.ForName(searchArgs)
   264  						if arg != nil {
   265  							indexes := getAllSearchIndexes(arg.Value)
   266  							indexStr = fmt.Sprintf(" @index(%s)", strings.Join(indexes, ", "))
   267  						}
   268  					}
   269  					fmt.Fprintf(&typeDef, "  %s: %s\n", fname, typStr)
   270  					if parentInt == nil {
   271  						fmt.Fprintf(&preds, "%s: %s%s .\n", fname, typStr, indexStr)
   272  					}
   273  				}
   274  			}
   275  			fmt.Fprintf(&typeDef, "}\n")
   276  
   277  			typeStrings = append(
   278  				typeStrings,
   279  				fmt.Sprintf("%s%s", typeDef.String(), preds.String()),
   280  			)
   281  		}
   282  	}
   283  
   284  	return strings.Join(typeStrings, "")
   285  }