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