github.com/dgraph-io/dgraph@v1.2.8/graphql/schema/rules.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  	"sort"
    22  	"strings"
    23  
    24  	"github.com/vektah/gqlparser/ast"
    25  	"github.com/vektah/gqlparser/gqlerror"
    26  	"github.com/vektah/gqlparser/validator"
    27  )
    28  
    29  func init() {
    30  	defnValidations = append(defnValidations, dataTypeCheck, nameCheck)
    31  
    32  	typeValidations = append(typeValidations, idCountCheck, dgraphDirectiveTypeValidation)
    33  	fieldValidations = append(fieldValidations, listValidityCheck, fieldArgumentCheck,
    34  		fieldNameCheck, isValidFieldForList)
    35  
    36  	validator.AddRule("Check variable type is correct", variableTypeCheck)
    37  	validator.AddRule("Check for list type value", listTypeCheck)
    38  
    39  }
    40  
    41  func dataTypeCheck(defn *ast.Definition) *gqlerror.Error {
    42  	if defn.Kind == ast.Object || defn.Kind == ast.Enum || defn.Kind == ast.Interface {
    43  		return nil
    44  	}
    45  	return gqlerror.ErrorPosf(
    46  		defn.Position,
    47  		"You can't add %s definitions. "+
    48  			"Only type, interface and enums are allowed in initial schema.",
    49  		strings.ToLower(string(defn.Kind)),
    50  	)
    51  }
    52  
    53  func nameCheck(defn *ast.Definition) *gqlerror.Error {
    54  
    55  	if (defn.Kind == ast.Object || defn.Kind == ast.Enum) && isReservedKeyWord(defn.Name) {
    56  		var errMesg string
    57  
    58  		if defn.Name == "Query" || defn.Name == "Mutation" {
    59  			errMesg = "You don't need to define the GraphQL Query or Mutation types." +
    60  				" Those are built automatically for you."
    61  		} else {
    62  			errMesg = fmt.Sprintf(
    63  				"%s is a reserved word, so you can't declare a type with this name. "+
    64  					"Pick a different name for the type.", defn.Name,
    65  			)
    66  		}
    67  
    68  		return gqlerror.ErrorPosf(defn.Position, errMesg)
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  func collectFieldNames(idFields []*ast.FieldDefinition) (string, []gqlerror.Location) {
    75  	var fieldNames []string
    76  	var errLocations []gqlerror.Location
    77  
    78  	for _, f := range idFields {
    79  		fieldNames = append(fieldNames, f.Name)
    80  		errLocations = append(errLocations, gqlerror.Location{
    81  			Line:   f.Position.Line,
    82  			Column: f.Position.Column,
    83  		})
    84  	}
    85  
    86  	fieldNamesString := fmt.Sprintf(
    87  		"%s and %s",
    88  		strings.Join(fieldNames[:len(fieldNames)-1], ", "), fieldNames[len(fieldNames)-1],
    89  	)
    90  	return fieldNamesString, errLocations
    91  }
    92  
    93  func dgraphDirectiveTypeValidation(typ *ast.Definition) *gqlerror.Error {
    94  	dir := typ.Directives.ForName(dgraphDirective)
    95  	if dir == nil {
    96  		return nil
    97  	}
    98  
    99  	typeArg := dir.Arguments.ForName(dgraphTypeArg)
   100  	if typeArg == nil || typeArg.Value.Raw == "" {
   101  		return gqlerror.ErrorPosf(
   102  			dir.Position,
   103  			"Type %s; type argument for @dgraph directive should not be empty.", typ.Name)
   104  	}
   105  	if typeArg.Value.Kind != ast.StringValue {
   106  		return gqlerror.ErrorPosf(
   107  			dir.Position,
   108  			"Type %s; type argument for @dgraph directive should of type String.", typ.Name)
   109  	}
   110  	return nil
   111  }
   112  
   113  func idCountCheck(typ *ast.Definition) *gqlerror.Error {
   114  	var idFields []*ast.FieldDefinition
   115  	var idDirectiveFields []*ast.FieldDefinition
   116  	for _, field := range typ.Fields {
   117  		if isIDField(typ, field) {
   118  			idFields = append(idFields, field)
   119  		}
   120  		if d := field.Directives.ForName(idDirective); d != nil {
   121  			idDirectiveFields = append(idDirectiveFields, field)
   122  		}
   123  	}
   124  
   125  	if len(idFields) > 1 {
   126  		fieldNamesString, errLocations := collectFieldNames(idFields)
   127  		errMessage := fmt.Sprintf(
   128  			"Fields %s are listed as IDs for type %s, "+
   129  				"but a type can have only one ID field. "+
   130  				"Pick a single field as the ID for type %s.",
   131  			fieldNamesString, typ.Name, typ.Name,
   132  		)
   133  
   134  		return &gqlerror.Error{
   135  			Message:   errMessage,
   136  			Locations: errLocations,
   137  		}
   138  	}
   139  
   140  	if len(idDirectiveFields) > 1 {
   141  		fieldNamesString, errLocations := collectFieldNames(idDirectiveFields)
   142  		errMessage := fmt.Sprintf(
   143  			"Type %s: fields %s have the @id directive, "+
   144  				"but a type can have only one field with @id. "+
   145  				"Pick a single field with @id for type %s.",
   146  			typ.Name, fieldNamesString, typ.Name,
   147  		)
   148  
   149  		return &gqlerror.Error{
   150  			Message:   errMessage,
   151  			Locations: errLocations,
   152  		}
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func isValidFieldForList(typ *ast.Definition, field *ast.FieldDefinition) *gqlerror.Error {
   159  	if field.Type.Elem == nil && field.Type.NamedType != "" {
   160  		return nil
   161  	}
   162  
   163  	// ID and Boolean list are not allowed.
   164  	// [Boolean] is not allowed as dgraph schema doesn't support [bool] yet.
   165  	switch field.Type.Elem.Name() {
   166  	case
   167  		"ID",
   168  		"Boolean":
   169  		return gqlerror.ErrorPosf(
   170  			field.Position, "Type %s; Field %s: %s lists are invalid.",
   171  			typ.Name, field.Name, field.Type.Elem.Name())
   172  	}
   173  	return nil
   174  }
   175  
   176  func fieldArgumentCheck(typ *ast.Definition, field *ast.FieldDefinition) *gqlerror.Error {
   177  	if field.Arguments != nil {
   178  		return gqlerror.ErrorPosf(
   179  			field.Position,
   180  			"Type %s; Field %s: You can't give arguments to fields.",
   181  			typ.Name, field.Name,
   182  		)
   183  	}
   184  	return nil
   185  }
   186  
   187  func fieldNameCheck(typ *ast.Definition, field *ast.FieldDefinition) *gqlerror.Error {
   188  	//field name cannot be a reserved word
   189  	if isReservedKeyWord(field.Name) {
   190  		return gqlerror.ErrorPosf(
   191  			field.Position, "Type %s; Field %s: %s is a reserved keyword and "+
   192  				"you cannot declare a field with this name.",
   193  			typ.Name, field.Name, field.Name)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func listValidityCheck(typ *ast.Definition, field *ast.FieldDefinition) *gqlerror.Error {
   200  	if field.Type.Elem == nil && field.Type.NamedType != "" {
   201  		return nil
   202  	}
   203  
   204  	// Nested lists are not allowed.
   205  	if field.Type.Elem.Elem != nil {
   206  		return gqlerror.ErrorPosf(field.Position,
   207  			"Type %s; Field %s: Nested lists are invalid.",
   208  			typ.Name, field.Name)
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func hasInverseValidation(sch *ast.Schema, typ *ast.Definition,
   215  	field *ast.FieldDefinition, dir *ast.Directive) *gqlerror.Error {
   216  
   217  	invTypeName := field.Type.Name()
   218  	if sch.Types[invTypeName].Kind != ast.Object && sch.Types[invTypeName].Kind != ast.Interface {
   219  		return gqlerror.ErrorPosf(
   220  			field.Position,
   221  			"Type %s; Field %s: Field %[2]s is of type %s, but @hasInverse directive only applies"+
   222  				" to fields with object types.", typ.Name, field.Name, invTypeName,
   223  		)
   224  	}
   225  
   226  	invFieldArg := dir.Arguments.ForName("field")
   227  	if invFieldArg == nil {
   228  		// This check can be removed once gqlparser bug
   229  		// #107(https://github.com/vektah/gqlparser/issues/107) is fixed.
   230  		return gqlerror.ErrorPosf(
   231  			dir.Position,
   232  			"Type %s; Field %s: @hasInverse directive doesn't have field argument.",
   233  			typ.Name, field.Name,
   234  		)
   235  	}
   236  
   237  	invFieldName := invFieldArg.Value.Raw
   238  	invType := sch.Types[invTypeName]
   239  	invField := invType.Fields.ForName(invFieldName)
   240  	if invField == nil {
   241  		return gqlerror.ErrorPosf(
   242  			dir.Position,
   243  			"Type %s; Field %s: inverse field %s doesn't exist for type %s.",
   244  			typ.Name, field.Name, invFieldName, invTypeName,
   245  		)
   246  	}
   247  
   248  	if errMsg := isInverse(typ.Name, field.Name, invTypeName, invField); errMsg != "" {
   249  		return gqlerror.ErrorPosf(dir.Position, errMsg)
   250  	}
   251  
   252  	invDirective := invField.Directives.ForName(inverseDirective)
   253  	if invDirective == nil {
   254  		invField.Directives = append(invField.Directives, &ast.Directive{
   255  			Name: inverseDirective,
   256  			Arguments: []*ast.Argument{
   257  				{
   258  					Name: inverseArg,
   259  					Value: &ast.Value{
   260  						Raw:      field.Name,
   261  						Position: dir.Position,
   262  						Kind:     ast.EnumValue,
   263  					},
   264  				},
   265  			},
   266  			Position: dir.Position,
   267  		})
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func isInverse(expectedInvType, expectedInvField, typeName string,
   274  	field *ast.FieldDefinition) string {
   275  
   276  	invType := field.Type.Name()
   277  	if invType != expectedInvType {
   278  		return fmt.Sprintf(
   279  			"Type %s; Field %s: @hasInverse is required to link the fields"+
   280  				" of same type, but the field %s is of the type %s instead of"+
   281  				" %[1]s. To link these make sure the fields are of the same type.",
   282  			expectedInvType, expectedInvField, field.Name, field.Type,
   283  		)
   284  	}
   285  
   286  	invDirective := field.Directives.ForName(inverseDirective)
   287  	if invDirective == nil {
   288  		return ""
   289  	}
   290  
   291  	invFieldArg := invDirective.Arguments.ForName("field")
   292  	if invFieldArg == nil || invFieldArg.Value.Raw != expectedInvField {
   293  		return fmt.Sprintf(
   294  			"Type %s; Field %s: @hasInverse should be consistant."+
   295  				" %[1]s.%[2]s is the inverse of %[3]s.%[4]s, but"+
   296  				" %[3]s.%[4]s is the inverse of %[1]s.%[5]s.",
   297  			expectedInvType, expectedInvField, typeName, field.Name,
   298  			invFieldArg.Value.Raw,
   299  		)
   300  	}
   301  
   302  	return ""
   303  }
   304  
   305  // validateSearchArg checks that the argument for search is valid and compatible
   306  // with the type it is applied to.
   307  func validateSearchArg(searchArg string,
   308  	sch *ast.Schema,
   309  	typ *ast.Definition,
   310  	field *ast.FieldDefinition,
   311  	dir *ast.Directive) *gqlerror.Error {
   312  
   313  	isEnum := sch.Types[field.Type.Name()].Kind == ast.Enum
   314  	search, ok := supportedSearches[searchArg]
   315  	switch {
   316  	case !ok:
   317  		// This check can be removed once gqlparser bug
   318  		// #107(https://github.com/vektah/gqlparser/issues/107) is fixed.
   319  		return gqlerror.ErrorPosf(
   320  			dir.Position,
   321  			"Type %s; Field %s: the argument to @search %s isn't valid."+
   322  				"Fields of type %s %s.",
   323  			typ.Name, field.Name, searchArg, field.Type.Name(), searchMessage(sch, field))
   324  
   325  	case search.gqlType != field.Type.Name() && !isEnum:
   326  		return gqlerror.ErrorPosf(
   327  			dir.Position,
   328  			"Type %s; Field %s: has the @search directive but the argument %s "+
   329  				"doesn't apply to field type %s.  Search by %[3]s applies to fields of type %[5]s. "+
   330  				"Fields of type %[4]s %[6]s.",
   331  			typ.Name, field.Name, searchArg, field.Type.Name(),
   332  			supportedSearches[searchArg].gqlType, searchMessage(sch, field))
   333  
   334  	case isEnum && !enumDirectives[searchArg]:
   335  		return gqlerror.ErrorPosf(
   336  			dir.Position,
   337  			"Type %s; Field %s: has the @search directive but the argument %s "+
   338  				"doesn't apply to field type %s which is an Enum. Enum only supports "+
   339  				"hash, exact, regexp and trigram",
   340  			typ.Name, field.Name, searchArg, field.Type.Name())
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  func searchValidation(
   347  	sch *ast.Schema,
   348  	typ *ast.Definition,
   349  	field *ast.FieldDefinition,
   350  	dir *ast.Directive) *gqlerror.Error {
   351  
   352  	arg := dir.Arguments.ForName(searchArgs)
   353  	if arg == nil {
   354  		// If there's no arg, then it can be an enum or has to be a scalar that's
   355  		// not ID. The schema generation will add the default search
   356  		// for that type.
   357  		if sch.Types[field.Type.Name()].Kind == ast.Enum ||
   358  			(sch.Types[field.Type.Name()].Kind == ast.Scalar && !isIDField(typ, field)) {
   359  			return nil
   360  		}
   361  
   362  		return gqlerror.ErrorPosf(
   363  			dir.Position,
   364  			"Type %s; Field %s: has the @search directive but fields of type %s "+
   365  				"can't have the @search directive.",
   366  			typ.Name, field.Name, field.Type.Name())
   367  	}
   368  
   369  	// This check can be removed once gqlparser bug
   370  	// #107(https://github.com/vektah/gqlparser/issues/107) is fixed.
   371  	if arg.Value.Kind != ast.ListValue {
   372  		return gqlerror.ErrorPosf(
   373  			dir.Position,
   374  			"Type %s; Field %s: the @search directive requires a list argument, like @search(by: [hash])",
   375  			typ.Name, field.Name)
   376  	}
   377  
   378  	searchArgs := getSearchArgs(field)
   379  	searchIndexes := make(map[string]string)
   380  	for _, searchArg := range searchArgs {
   381  		if err := validateSearchArg(searchArg, sch, typ, field, dir); err != nil {
   382  			return err
   383  		}
   384  
   385  		// Checks that the filter indexes aren't repeated and they
   386  		// don't clash with each other.
   387  		searchIndex := builtInFilters[searchArg]
   388  		if val, ok := searchIndexes[searchIndex]; ok {
   389  			if field.Type.Name() == "String" || sch.Types[field.Type.Name()].Kind == ast.Enum {
   390  				return gqlerror.ErrorPosf(
   391  					dir.Position,
   392  					"Type %s; Field %s: the argument to @search '%s' is the same "+
   393  						"as the index '%s' provided before and shouldn't "+
   394  						"be used together",
   395  					typ.Name, field.Name, searchArg, val)
   396  			}
   397  
   398  			return gqlerror.ErrorPosf(
   399  				dir.Position,
   400  				"Type %s; Field %s: has the search directive on %s. %s "+
   401  					"allows only one argument for @search.",
   402  				typ.Name, field.Name, field.Type.Name(), field.Type.Name())
   403  		}
   404  
   405  		for _, index := range filtersCollisions[searchIndex] {
   406  			if val, ok := searchIndexes[index]; ok {
   407  				return gqlerror.ErrorPosf(
   408  					dir.Position,
   409  					"Type %s; Field %s: the arguments '%s' and '%s' can't "+
   410  						"be used together as arguments to @search.",
   411  					typ.Name, field.Name, searchArg, val)
   412  			}
   413  		}
   414  
   415  		searchIndexes[searchIndex] = searchArg
   416  	}
   417  
   418  	return nil
   419  }
   420  
   421  func dgraphDirectiveValidation(sch *ast.Schema, typ *ast.Definition, field *ast.FieldDefinition,
   422  	dir *ast.Directive) *gqlerror.Error {
   423  
   424  	if isID(field) {
   425  		return gqlerror.ErrorPosf(
   426  			dir.Position,
   427  			"Type %s; Field %s: has the @dgraph directive but fields of type ID "+
   428  				"can't have the @dgraph directive.", typ.Name, field.Name)
   429  	}
   430  
   431  	predArg := dir.Arguments.ForName(dgraphPredArg)
   432  	if predArg == nil || predArg.Value.Raw == "" {
   433  		return gqlerror.ErrorPosf(
   434  			dir.Position,
   435  			"Type %s; Field %s: pred argument for @dgraph directive should not be empty.",
   436  			typ.Name, field.Name,
   437  		)
   438  	}
   439  	if predArg.Value.Kind != ast.StringValue {
   440  		return gqlerror.ErrorPosf(
   441  			dir.Position,
   442  			"Type %s; Field %s: pred argument for @dgraph directive should of type String.",
   443  			typ.Name, field.Name,
   444  		)
   445  	}
   446  	return nil
   447  }
   448  
   449  func idValidation(sch *ast.Schema,
   450  	typ *ast.Definition,
   451  	field *ast.FieldDefinition,
   452  	dir *ast.Directive) *gqlerror.Error {
   453  
   454  	if field.Type.String() != "String!" {
   455  		return gqlerror.ErrorPosf(
   456  			dir.Position,
   457  			"Type %s; Field %s: with @id directive must be of type String!, not %s",
   458  			typ.Name, field.Name, field.Type.String())
   459  	}
   460  	return nil
   461  }
   462  
   463  func searchMessage(sch *ast.Schema, field *ast.FieldDefinition) string {
   464  	var possibleSearchArgs []string
   465  	for name, typ := range supportedSearches {
   466  		if typ.gqlType == field.Type.Name() {
   467  			possibleSearchArgs = append(possibleSearchArgs, name)
   468  		}
   469  	}
   470  
   471  	switch {
   472  	case len(possibleSearchArgs) == 1 || sch.Types[field.Type.Name()].Kind == ast.Enum:
   473  		return "are searchable by just @search"
   474  	case len(possibleSearchArgs) == 0:
   475  		return "can't have the @search directive"
   476  	default:
   477  		sort.Strings(possibleSearchArgs)
   478  		return fmt.Sprintf(
   479  			"can have @search by %s and %s",
   480  			strings.Join(possibleSearchArgs[:len(possibleSearchArgs)-1], ", "),
   481  			possibleSearchArgs[len(possibleSearchArgs)-1])
   482  	}
   483  }
   484  
   485  func isScalar(s string) bool {
   486  	_, ok := scalarToDgraph[s]
   487  	return ok
   488  }
   489  
   490  func isReservedKeyWord(name string) bool {
   491  	if isScalar(name) || name == "Query" || name == "Mutation" || name == "uid" {
   492  		return true
   493  	}
   494  
   495  	return false
   496  }