1 module graphql.validation.schemabased; 2 3 import std.algorithm.iteration : map; 4 import std.algorithm.searching : canFind; 5 import std.array : array, back, empty, front, popBack; 6 import std.conv : to; 7 import std.meta : staticMap, NoDuplicates; 8 import std.exception : enforce, assertThrown, assertNotThrown; 9 import std.format : format; 10 import std.stdio; 11 import std.string : strip; 12 13 import vibe.data.json; 14 15 import fixedsizearray; 16 17 import graphql.ast; 18 import graphql.builder; 19 import graphql.constants; 20 import graphql.visitor; 21 import graphql.schema.types; 22 import graphql.schema.helper; 23 import graphql.validation.exception; 24 import graphql.helper : lexAndParse; 25 26 @safe: 27 28 string astTypeToString(const(Type) input) pure { 29 final switch(input.ruleSelection) { 30 case TypeEnum.TN: 31 return format!"%s!"(input.tname.value); 32 case TypeEnum.LN: 33 return format!"[%s]!"(astTypeToString(input.list.type)); 34 case TypeEnum.T: 35 return format!"%s"(input.tname.value); 36 case TypeEnum.L: 37 return format!"[%s]"(astTypeToString(input.list.type)); 38 } 39 } 40 41 enum IsSubscription { 42 no, 43 yes 44 } 45 46 struct TypePlusName { 47 Json type; 48 string name; 49 50 string toString() const { 51 return format("%s %s", this.name, this.type); 52 } 53 } 54 55 class SchemaValidator(Schema) : Visitor { 56 import std.experimental.typecons : Final; 57 import graphql.schema.typeconversions; 58 import graphql.traits; 59 import graphql.helper : stringTypeStrip; 60 61 alias enter = Visitor.enter; 62 alias exit = Visitor.exit; 63 alias accept = Visitor.accept; 64 65 const(Document) doc; 66 GQLDSchema!(Schema) schema; 67 68 // Single root field 69 IsSubscription isSubscription; 70 int ssCnt; 71 int selCnt; 72 73 // Field Selections on Objects 74 TypePlusName[] schemaStack; 75 76 // Variables of operation 77 Type[string] variables; 78 79 void addToTypeStack(string name) { 80 //writefln("\n\nFoo '%s' %s", name, this.schemaStack.map!(a => a.name)); 81 82 enforce!FieldDoesNotExist( 83 Constants.fields in this.schemaStack.back.type, 84 format("Type '%s' does not have fields", 85 this.schemaStack.back.name) 86 ); 87 88 enforce!FieldDoesNotExist( 89 this.schemaStack.back.type.type == Json.Type.object, 90 format("Field '%s' of type '%s' is not a Json.Type.object", 91 name, this.schemaStack.back.name) 92 ); 93 94 immutable toFindIn = [Constants.__typename, Constants.__schema, 95 Constants.__type]; 96 Json field = canFind(toFindIn, name) 97 ? getIntrospectionField(name) 98 : this.schemaStack.back.type.getField(name); 99 enforce!FieldDoesNotExist( 100 field.type == Json.Type.object, 101 format("Type '%s' does not have fields named '%s'", 102 this.schemaStack.back.name, name) 103 ); 104 105 106 string followType = field[Constants.typenameOrig].get!string(); 107 string old = followType; 108 followType = followType.stringTypeStrip(); 109 this.addTypeToStackImpl(name, followType, old); 110 } 111 112 void addTypeToStackImpl(string name, string followType, string old) { 113 l: switch(followType) { 114 alias AllTypes = collectTypesPlusIntrospection!(Schema); 115 alias Stripped = staticMap!(stripArrayAndNullable, AllTypes); 116 alias NoDups = NoDuplicates!(Stripped); 117 static foreach(type; NoDups) {{ 118 case typeToTypeName!(type): { 119 this.schemaStack ~= TypePlusName( 120 removeNonNullAndList(typeToJson!(type,Schema)()), 121 name 122 ); 123 //writeln(this.schemaStack.back.type.toPrettyString()); 124 break l; 125 } 126 }} 127 default: 128 throw new UnknownTypeName( 129 format("No type with name '%s' '%s' is known", 130 followType, old), __FILE__, __LINE__); 131 } 132 } 133 134 this(const(Document) doc, GQLDSchema!(Schema) schema) { 135 this.doc = doc; 136 this.schema = schema; 137 this.schemaStack ~= TypePlusName( 138 removeNonNullAndList(typeToJson!(Schema,Schema)()), 139 Schema.stringof 140 ); 141 } 142 143 override void enter(const(OperationType) ot) { 144 this.isSubscription = ot.ruleSelection == OperationTypeEnum.Sub 145 ? IsSubscription.yes : IsSubscription.no; 146 } 147 148 override void enter(const(SelectionSet) ss) { 149 //writeln(this.schemaStack); 150 ++this.ssCnt; 151 } 152 153 override void exit(const(SelectionSet) ss) { 154 --this.ssCnt; 155 } 156 157 override void enter(const(Selection) sel) { 158 ++this.selCnt; 159 const bool notSingleRootField = this.isSubscription == IsSubscription.yes 160 && this.ssCnt == 1 161 && this.selCnt > 1; 162 163 enforce!SingleRootField(!notSingleRootField); 164 } 165 166 override void enter(const(FragmentDefinition) fragDef) { 167 string typeName = fragDef.tc.value; 168 //writefln("%s %s", typeName, fragDef.name.value); 169 l: switch(typeName) { 170 alias AllTypes = collectTypesPlusIntrospection!(Schema); 171 alias Stripped = staticMap!(stripArrayAndNullable, AllTypes); 172 alias NoDups = NoDuplicates!(Stripped); 173 static foreach(type; NoDups) {{ 174 case typeToTypeName!(type): { 175 this.schemaStack ~= TypePlusName( 176 removeNonNullAndList(typeToJson!(type,Schema)()), 177 typeName 178 ); 179 //writeln(this.schemaStack.back.type.toPrettyString()); 180 break l; 181 } 182 }} 183 default: 184 throw new UnknownTypeName( 185 format("No type with name '%s' is known", 186 typeName), __FILE__, __LINE__); 187 } 188 } 189 190 override void enter(const(FragmentSpread) fragSpread) { 191 enum uo = ["OBJECT", "UNION", "INTERFACE"]; 192 enforce!FragmentNotOnCompositeType( 193 "kind" in this.schemaStack.back.type 194 && canFind(uo, this.schemaStack.back.type["kind"].get!string()), 195 format("'%s' is not an %(%s, %)", 196 this.schemaStack.back.type.toPrettyString(), uo) 197 ); 198 const(FragmentDefinition) frag = findFragment(this.doc, 199 fragSpread.name.value 200 ); 201 frag.visit(this); 202 } 203 204 override void enter(const(OperationDefinition) op) { 205 string name = op.ruleSelection == OperationDefinitionEnum.SelSet 206 || op.ot.ruleSelection == OperationTypeEnum.Query 207 ? "queryType" 208 : op.ot.ruleSelection == OperationTypeEnum.Mutation 209 ? "mutationType" 210 : op.ot.ruleSelection == OperationTypeEnum.Sub 211 ? "subscriptionType" 212 : ""; 213 enforce(!name.empty); 214 this.addToTypeStack(name); 215 } 216 217 override void accept(const(Field) f) { 218 super.accept(f); 219 enforce!LeafIsNotAScalar(f.ss !is null || 220 (this.schemaStack.back.type["kind"].get!string() == "SCALAR" 221 || this.schemaStack.back.type["kind"].get!string() == "ENUM"), 222 format("Leaf field '%s' is not a SCALAR but '%s'", 223 this.schemaStack.back.name, 224 this.schemaStack.back.type.toPrettyString()) 225 ); 226 } 227 228 override void enter(const(FieldName) fn) { 229 this.addToTypeStack(fn.name.value); 230 } 231 232 override void enter(const(InlineFragment) inF) { 233 this.addTypeToStackImpl("InlineFragment", inF.tc.value, ""); 234 } 235 236 override void exit(const(InlineFragment) inF) { 237 this.schemaStack.popBack(); 238 } 239 240 override void exit(const(Selection) op) { 241 this.schemaStack.popBack(); 242 } 243 244 override void exit(const(OperationDefinition) op) { 245 this.schemaStack.popBack(); 246 } 247 248 override void enter(const(VariableDefinition) vd) { 249 const vdName = vd.var.name.value; 250 () @trusted { 251 this.variables[vdName] = cast()vd.type; 252 }(); 253 } 254 255 override void enter(const(Argument) arg) { 256 import std.algorithm.searching : find; 257 const argName = arg.name.value; 258 const parent = this.schemaStack[$ - 2]; 259 const curName = this.schemaStack.back.name; 260 auto fields = parent.type[Constants.fields]; 261 if(fields.type != Json.Type.Array) { 262 return; 263 } 264 auto curNameFieldRange = fields.byValue 265 .find!(f => f[Constants.name].to!string() == curName); 266 if(curNameFieldRange.empty) { 267 return; 268 } 269 270 auto curNameField = curNameFieldRange.front; 271 272 Json curArgs = curNameField[Constants.args]; 273 auto argElem = curArgs.byValue.find!(a => a[Constants.name] == argName); 274 275 enforce!ArgumentDoesNotExist(!argElem.empty, format!( 276 "Argument with name '%s' does not exist for field '%s' of type " 277 ~ " '%s'")(argName, curName, parent.type[Constants.name])); 278 279 if(arg.vv.ruleSelection == ValueOrVariableEnum.Var) { 280 const varName = arg.vv.var.name.value; 281 auto varType = varName in this.variables; 282 enforce(varName !is null); 283 284 string typeStr = astTypeToString(*varType); 285 enforce!VariableInputTypeMismatch( 286 argElem.front[Constants.typenameOrig] == typeStr, 287 format!"Variable type '%s' does not match argument type '%s'" 288 (argElem.front[Constants.typenameOrig], typeStr)); 289 } 290 } 291 } 292 293 import graphql.testschema; 294 295 private void test(T)(string str) { 296 GQLDSchema!(Schema) testSchema = new GQLDSchema!(Schema)(); 297 auto doc = lexAndParse(str); 298 auto fv = new SchemaValidator!Schema(doc, testSchema); 299 300 static if(is(T == void)) { 301 assertNotThrown(fv.accept(doc)); 302 } else { 303 assertThrown!T(fv.accept(doc)); 304 } 305 } 306 307 unittest { 308 string str = ` 309 subscription sub { 310 starships { 311 id 312 name 313 } 314 }`; 315 test!void(str); 316 } 317 318 unittest { 319 string str = ` 320 subscription sub { 321 starships { 322 id 323 name 324 } 325 starships { 326 size 327 } 328 }`; 329 330 test!SingleRootField(str); 331 } 332 333 unittest { 334 string str = ` 335 subscription sub { 336 ...multipleSubscriptions 337 } 338 339 fragment multipleSubscriptions on Subscription { 340 starships { 341 id 342 name 343 } 344 starships { 345 size 346 } 347 }`; 348 349 test!SingleRootField(str); 350 } 351 352 unittest { 353 string str = ` 354 subscription sub { 355 starships { 356 id 357 name 358 } 359 __typename 360 }`; 361 362 test!SingleRootField(str); 363 } 364 365 unittest { 366 string str = ` 367 { 368 starships { 369 id 370 } 371 } 372 `; 373 374 test!void(str); 375 } 376 377 unittest { 378 string str = ` 379 { 380 starships { 381 fieldDoesNotExist 382 } 383 } 384 `; 385 386 test!FieldDoesNotExist(str); 387 } 388 389 unittest { 390 string str = ` 391 { 392 starships { 393 id { 394 name 395 } 396 } 397 } 398 `; 399 400 test!FieldDoesNotExist(str); 401 } 402 403 unittest { 404 string str = ` 405 query q { 406 search { 407 shipId 408 } 409 }`; 410 411 test!FieldDoesNotExist(str); 412 } 413 414 unittest { 415 string str = ` 416 query q { 417 search { 418 ...ShipFrag 419 } 420 } 421 422 fragment ShipFrag on Starship { 423 designation 424 } 425 `; 426 427 test!void(str); 428 } 429 430 unittest { 431 string str = ` 432 query q { 433 search { 434 ...ShipFrag 435 ...CharFrag 436 } 437 } 438 439 fragment ShipFrag on Starship { 440 designation 441 } 442 443 fragment CharFrag on Character { 444 foobar 445 } 446 `; 447 448 test!FieldDoesNotExist(str); 449 } 450 451 unittest { 452 string str = ` 453 mutation q { 454 addCrewman { 455 ...CharFrag 456 } 457 } 458 459 fragment CharFrag on Character { 460 name 461 } 462 `; 463 464 test!void(str); 465 } 466 467 unittest { 468 string str = ` 469 mutation q { 470 addCrewman { 471 ...CharFrag 472 } 473 } 474 475 fragment CharFrag on Character { 476 foobar 477 } 478 `; 479 480 test!FieldDoesNotExist(str); 481 } 482 483 unittest { 484 string str = ` 485 subscription q { 486 starships { 487 id 488 designation 489 } 490 } 491 `; 492 493 test!void(str); 494 } 495 496 unittest { 497 string str = ` 498 subscription q { 499 starships { 500 id 501 doesNotExist 502 } 503 } 504 `; 505 506 test!FieldDoesNotExist(str); 507 } 508 509 unittest { 510 string str = ` 511 query q { 512 search { 513 ...ShipFrag 514 ...CharFrag 515 } 516 } 517 518 fragment ShipFrag on Starship { 519 designation 520 } 521 522 fragment CharFrag on Character { 523 name 524 } 525 `; 526 527 test!void(str); 528 } 529 530 unittest { 531 string str = ` 532 { 533 starships { 534 __typename 535 } 536 } 537 `; 538 539 test!void(str); 540 } 541 542 unittest { 543 string str = ` 544 { 545 __schema { 546 types { 547 name 548 } 549 } 550 } 551 `; 552 553 test!void(str); 554 } 555 556 unittest { 557 string str = ` 558 { 559 __schema { 560 types { 561 enumValues { 562 name 563 } 564 } 565 } 566 } 567 `; 568 569 test!void(str); 570 } 571 572 unittest { 573 string str = ` 574 { 575 __schema { 576 types { 577 enumValues { 578 doesNotExist 579 } 580 } 581 } 582 } 583 `; 584 585 test!FieldDoesNotExist(str); 586 } 587 588 unittest { 589 string str = ` 590 query q { 591 search { 592 ...CharFrag 593 } 594 } 595 596 fragment CharFrag on NonExistingType { 597 name 598 } 599 `; 600 601 test!UnknownTypeName(str); 602 } 603 604 unittest { 605 string str = ` 606 query q { 607 search { 608 ...CharFrag 609 } 610 } 611 612 fragment CharFrag on Character { 613 name 614 } 615 `; 616 617 test!void(str); 618 } 619 620 unittest { 621 string str = ` 622 query q { 623 starships { 624 id { 625 ...CharFrag 626 } 627 } 628 } 629 630 fragment CharFrag on Character { 631 name { 632 foo 633 } 634 } 635 `; 636 637 test!FragmentNotOnCompositeType(str); 638 } 639 640 unittest { 641 string str = ` 642 query q { 643 starships { 644 crew 645 } 646 } 647 `; 648 649 test!LeafIsNotAScalar(str); 650 } 651 652 unittest { 653 string str = ` 654 query q { 655 starships { 656 crew { 657 name 658 } 659 } 660 } 661 `; 662 663 test!void(str); 664 } 665 666 unittest { 667 string str = ` 668 { 669 starships { 670 crew { 671 id 672 ship 673 } 674 } 675 } 676 `; 677 678 test!LeafIsNotAScalar(str); 679 } 680 681 unittest { 682 string str = ` 683 { 684 starships { 685 crew { 686 id 687 ships 688 } 689 } 690 }`; 691 692 test!LeafIsNotAScalar(str); 693 } 694 695 unittest { 696 string str = ` 697 { 698 starships 699 }`; 700 701 test!LeafIsNotAScalar(str); 702 } 703 704 unittest { 705 string str = ` 706 query q($size: String) { 707 starships(overSize: $size) { 708 id 709 } 710 }`; 711 712 test!VariableInputTypeMismatch(str); 713 } 714 715 unittest { 716 string str = ` 717 query q($size: Float!) { 718 starships(overSize: $size) { 719 id 720 } 721 }`; 722 723 test!void(str); 724 } 725 726 unittest { 727 string str = ` 728 query q($ships: [Int!]!) { 729 shipsselection(ids: $ships) { 730 id 731 } 732 }`; 733 734 test!void(str); 735 } 736 737 unittest { 738 string str = ` 739 { 740 starships { 741 crew { 742 ... on Humanoid { 743 dateOfBirth 744 } 745 } 746 } 747 }`; 748 749 test!void(str); 750 } 751 752 unittest { 753 string str = ` 754 { 755 starships { 756 crew { 757 ... on Humanoid { 758 doesNotExist 759 } 760 } 761 } 762 }`; 763 764 test!FieldDoesNotExist(str); 765 }