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