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