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 const c5 = (typeStr.endsWith("In]!") 296 && (typeStr[0 .. $ - 4] ~ "]!") == argElem.front[Constants.typenameOrig]); 297 const c6 = (typeStr.endsWith("In!]!") 298 && (typeStr[0 .. $ - 5] ~ "!]!") == argElem.front[Constants.typenameOrig]); 299 const c7 = (typeStr.endsWith("In!]") 300 && (typeStr[0 .. $ - 4] ~ "!]") == argElem.front[Constants.typenameOrig]); 301 enforce!VariableInputTypeMismatch(c1 || c2 || c3 || c4 || c5 302 || c6 || c7 303 , format("Variable type '%s' does not match argument type '%s'" 304 ~ " ! %s In %s In! %s c1 %s c2 %s c3 %s c4 %s c5 %s" 305 ~ " c6 %s c7 %s" 306 , argElem.front[Constants.typenameOrig], typeStr 307 , typeStr.endsWith("!"), typeStr.endsWith("In") 308 , typeStr.endsWith("In!") , c1, c2, c3, c4, c5, c6, c7 309 )); 310 } 311 } else { 312 enforce!ArgumentDoesNotExist(argName == "if", format( 313 "Argument of Directive '%s' is 'if' not '%s'", 314 this.directiveStack.back.name, argName)); 315 316 if(arg.vv.ruleSelection == ValueOrVariableEnum.Var) { 317 const varName = arg.vv.var.name.value; 318 auto varType = varName in this.variables; 319 enforce(varName !is null); 320 321 string typeStr = astTypeToString(*varType); 322 enforce!VariableInputTypeMismatch( 323 typeStr == "Boolean!", 324 format("Variable type '%s' does not match argument type 'Boolean!'" 325 , typeStr)); 326 } 327 } 328 } 329 } 330 331 import graphql.testschema; 332 333 private void test(T)(string str) { 334 GQLDSchema!(Schema) testSchema = new GQLDSchema!(Schema)(); 335 auto doc = lexAndParse(str); 336 auto fv = new SchemaValidator!Schema(doc, testSchema); 337 338 static if(is(T == void)) { 339 assertNotThrown(fv.accept(doc)); 340 } else { 341 assertThrown!T(fv.accept(doc)); 342 } 343 } 344 345 unittest { 346 string str = ` 347 subscription sub { 348 starships { 349 id 350 name 351 } 352 }`; 353 test!void(str); 354 } 355 356 unittest { 357 string str = ` 358 subscription sub { 359 starships { 360 id 361 name 362 } 363 starships { 364 size 365 } 366 }`; 367 368 test!SingleRootField(str); 369 } 370 371 unittest { 372 string str = ` 373 subscription sub { 374 ...multipleSubscriptions 375 } 376 377 fragment multipleSubscriptions on Subscription { 378 starships { 379 id 380 name 381 } 382 starships { 383 size 384 } 385 }`; 386 387 test!SingleRootField(str); 388 } 389 390 unittest { 391 string str = ` 392 subscription sub { 393 starships { 394 id 395 name 396 } 397 __typename 398 }`; 399 400 test!SingleRootField(str); 401 } 402 403 unittest { 404 string str = ` 405 { 406 starships { 407 id 408 } 409 } 410 `; 411 412 test!void(str); 413 } 414 415 unittest { 416 string str = ` 417 { 418 starships { 419 fieldDoesNotExist 420 } 421 } 422 `; 423 424 test!FieldDoesNotExist(str); 425 } 426 427 unittest { 428 string str = ` 429 { 430 starships { 431 id { 432 name 433 } 434 } 435 } 436 `; 437 438 test!FieldDoesNotExist(str); 439 } 440 441 unittest { 442 string str = ` 443 query q { 444 search { 445 shipId 446 } 447 }`; 448 449 test!FieldDoesNotExist(str); 450 } 451 452 unittest { 453 string str = ` 454 query q { 455 search { 456 ...ShipFrag 457 } 458 } 459 460 fragment ShipFrag on Starship { 461 designation 462 } 463 `; 464 465 test!void(str); 466 } 467 468 unittest { 469 string str = ` 470 query q { 471 search { 472 ...ShipFrag 473 ...CharFrag 474 } 475 } 476 477 fragment ShipFrag on Starship { 478 designation 479 } 480 481 fragment CharFrag on Character { 482 foobar 483 } 484 `; 485 486 test!FieldDoesNotExist(str); 487 } 488 489 unittest { 490 string str = ` 491 mutation q { 492 addCrewman { 493 ...CharFrag 494 } 495 } 496 497 fragment CharFrag on Character { 498 name 499 } 500 `; 501 502 test!void(str); 503 } 504 505 unittest { 506 string str = ` 507 mutation q { 508 addCrewman { 509 ...CharFrag 510 } 511 } 512 513 fragment CharFrag on Character { 514 foobar 515 } 516 `; 517 518 test!FieldDoesNotExist(str); 519 } 520 521 unittest { 522 string str = ` 523 subscription q { 524 starships { 525 id 526 designation 527 } 528 } 529 `; 530 531 test!void(str); 532 } 533 534 unittest { 535 string str = ` 536 subscription q { 537 starships { 538 id 539 doesNotExist 540 } 541 } 542 `; 543 544 test!FieldDoesNotExist(str); 545 } 546 547 unittest { 548 string str = ` 549 query q { 550 search { 551 ...ShipFrag 552 ...CharFrag 553 } 554 } 555 556 fragment ShipFrag on Starship { 557 designation 558 } 559 560 fragment CharFrag on Character { 561 name 562 } 563 `; 564 565 test!void(str); 566 } 567 568 unittest { 569 string str = ` 570 { 571 starships { 572 __typename 573 } 574 } 575 `; 576 577 test!void(str); 578 } 579 580 unittest { 581 string str = ` 582 { 583 __schema { 584 types { 585 name 586 } 587 } 588 } 589 `; 590 591 test!void(str); 592 } 593 594 unittest { 595 string str = ` 596 { 597 __schema { 598 types { 599 enumValues { 600 name 601 } 602 } 603 } 604 } 605 `; 606 607 test!void(str); 608 } 609 610 unittest { 611 string str = ` 612 { 613 __schema { 614 types { 615 enumValues { 616 doesNotExist 617 } 618 } 619 } 620 } 621 `; 622 623 test!FieldDoesNotExist(str); 624 } 625 626 unittest { 627 string str = ` 628 query q { 629 search { 630 ...CharFrag 631 } 632 } 633 634 fragment CharFrag on NonExistingType { 635 name 636 } 637 `; 638 639 test!UnknownTypeName(str); 640 } 641 642 unittest { 643 string str = ` 644 query q { 645 search { 646 ...CharFrag 647 } 648 } 649 650 fragment CharFrag on Character { 651 name 652 } 653 `; 654 655 test!void(str); 656 } 657 658 unittest { 659 string str = ` 660 query q { 661 starships { 662 id { 663 ...CharFrag 664 } 665 } 666 } 667 668 fragment CharFrag on Character { 669 name { 670 foo 671 } 672 } 673 `; 674 675 test!FragmentNotOnCompositeType(str); 676 } 677 678 unittest { 679 string str = ` 680 query q { 681 starships { 682 crew 683 } 684 } 685 `; 686 687 test!LeafIsNotAScalar(str); 688 } 689 690 unittest { 691 string str = ` 692 query q { 693 starships { 694 crew { 695 name 696 } 697 } 698 } 699 `; 700 701 test!void(str); 702 } 703 704 unittest { 705 string str = ` 706 { 707 starships { 708 crew { 709 id 710 ship 711 } 712 } 713 } 714 `; 715 716 test!LeafIsNotAScalar(str); 717 } 718 719 unittest { 720 string str = ` 721 { 722 starships { 723 crew { 724 id 725 ships 726 } 727 } 728 }`; 729 730 test!LeafIsNotAScalar(str); 731 } 732 733 unittest { 734 string str = ` 735 { 736 starships 737 }`; 738 739 test!LeafIsNotAScalar(str); 740 } 741 742 unittest { 743 string str = ` 744 query q($size: String) { 745 starships(overSize: $size) { 746 id 747 } 748 }`; 749 750 test!VariableInputTypeMismatch(str); 751 } 752 753 unittest { 754 string str = ` 755 query q($size: Float!) { 756 starships(overSize: $size) { 757 id 758 } 759 }`; 760 761 test!void(str); 762 } 763 764 unittest { 765 string str = ` 766 query q($ships: [Int!]!) { 767 shipsselection(ids: $ships) { 768 id 769 } 770 }`; 771 772 test!void(str); 773 } 774 775 unittest { 776 string str = ` 777 { 778 starships { 779 crew { 780 ... on Humanoid { 781 dateOfBirth 782 } 783 } 784 } 785 }`; 786 787 test!void(str); 788 } 789 790 unittest { 791 string str = ` 792 { 793 starships { 794 crew { 795 ... on Humanoid { 796 doesNotExist 797 } 798 } 799 } 800 }`; 801 802 test!FieldDoesNotExist(str); 803 } 804 805 unittest { 806 string str = ` 807 query q($cw: Boolean!) { 808 starships { 809 crew @include(if: $cw) { 810 ... on Humanoid { 811 dateOfBirth 812 } 813 } 814 } 815 }`; 816 817 test!void(str); 818 } 819 820 unittest { 821 string str = ` 822 query q($cw: Int!) { 823 starships { 824 crew @include(if: $cw) { 825 ... on Humanoid { 826 dateOfBirth 827 } 828 } 829 } 830 }`; 831 832 test!VariableInputTypeMismatch(str); 833 } 834 835 unittest { 836 string str = ` 837 query q($cw: Int!) { 838 starships { 839 crew @include(notIf: $cw) { 840 ... on Humanoid { 841 dateOfBirth 842 } 843 } 844 } 845 }`; 846 847 test!ArgumentDoesNotExist(str); 848 } 849 850 unittest { 851 string str = ` 852 query { 853 numberBetween(searchInput: 854 { first: 10 855 , after: null 856 } 857 ) { 858 id 859 } 860 } 861 `; 862 test!void(str); 863 } 864 865 unittest { 866 string str = ` 867 query foo($after: String) { 868 numberBetween(searchInput: 869 { first: 10 870 , after: $after 871 } 872 ) { 873 id 874 } 875 } 876 `; 877 test!void(str); 878 } 879 880 unittest { 881 string str = ` 882 query q { 883 androids { 884 primaryFunction #inherited 885 name #not inherited 886 } 887 } 888 `; 889 test!void(str); 890 }