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 }