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, 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 //writefln("%s %s %s", __LINE__, name, field.toString()); 107 string followType = field[Constants.typenameOrig].get!string(); 108 string old = followType; 109 StringTypeStrip stripped = followType.stringTypeStrip(); 110 this.addTypeToStackImpl(name, stripped.str, old); 111 } 112 113 void addTypeToStackImpl(string name, string followType, string old) { 114 l: switch(followType) { 115 alias AllTypes = collectTypesPlusIntrospection!(Schema); 116 alias Stripped = staticMap!(stripArrayAndNullable, AllTypes); 117 alias NoDups = NoDuplicates!(Stripped); 118 //pragma(msg, staticMap!(typeToTypeName, NoDups)); 119 static foreach(type; NoDups) {{ 120 case typeToTypeName!(type): { 121 Json tmp = typeToJson!(type,Schema); 122 Json stripped = removeNonNullAndList(tmp); 123 //writefln("%s %s", __LINE__, tmp.toString()); 124 //writefln("%s %s", __LINE__, stripped.toString()); 125 this.schemaStack ~= TypePlusName(stripped, name); 126 //writeln(this.schemaStack.back.type.toPrettyString()); 127 break l; 128 } 129 }} 130 default: 131 throw new UnknownTypeName( 132 format("No type with name '%s' '%s' is known", 133 followType, old), __FILE__, __LINE__); 134 } 135 } 136 137 this(const(Document) doc, GQLDSchema!(Schema) schema) { 138 this.doc = doc; 139 this.schema = schema; 140 this.schemaStack ~= TypePlusName( 141 removeNonNullAndList(typeToJson!(Schema,Schema)()), 142 Schema.stringof 143 ); 144 } 145 146 override void enter(const(OperationType) ot) { 147 this.isSubscription = ot.ruleSelection == OperationTypeEnum.Sub 148 ? IsSubscription.yes : IsSubscription.no; 149 } 150 151 override void enter(const(SelectionSet) ss) { 152 //writeln(this.schemaStack); 153 ++this.ssCnt; 154 } 155 156 override void exit(const(SelectionSet) ss) { 157 --this.ssCnt; 158 } 159 160 override void enter(const(Selection) sel) { 161 ++this.selCnt; 162 const bool notSingleRootField = this.isSubscription == IsSubscription.yes 163 && this.ssCnt == 1 164 && this.selCnt > 1; 165 166 enforce!SingleRootField(!notSingleRootField); 167 } 168 169 override void enter(const(FragmentDefinition) fragDef) { 170 string typeName = fragDef.tc.value; 171 //writefln("%s %s", typeName, fragDef.name.value); 172 l: switch(typeName) { 173 alias AllTypes = collectTypesPlusIntrospection!(Schema); 174 alias Stripped = staticMap!(stripArrayAndNullable, AllTypes); 175 alias NoDups = NoDuplicates!(Stripped); 176 static foreach(type; NoDups) {{ 177 case typeToTypeName!(type): { 178 this.schemaStack ~= TypePlusName( 179 removeNonNullAndList(typeToJson!(type,Schema)()), 180 typeName 181 ); 182 //writeln(this.schemaStack.back.type.toPrettyString()); 183 break l; 184 } 185 }} 186 default: 187 throw new UnknownTypeName( 188 format("No type with name '%s' is known", 189 typeName), __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("InlineFragment", inF.tc.value, ""); 239 } 240 241 override void exit(const(InlineFragment) inF) { 242 this.schemaStack.popBack(); 243 } 244 245 override void exit(const(Selection) op) { 246 this.schemaStack.popBack(); 247 } 248 249 override void exit(const(OperationDefinition) op) { 250 this.schemaStack.popBack(); 251 } 252 253 override void enter(const(VariableDefinition) vd) { 254 const vdName = vd.var.name.value; 255 () @trusted { 256 this.variables[vdName] = cast()vd.type; 257 }(); 258 } 259 260 override void enter(const(Argument) arg) { 261 import std.algorithm.searching : find; 262 const argName = arg.name.value; 263 const parent = this.schemaStack[$ - 2]; 264 const curName = this.schemaStack.back.name; 265 auto fields = parent.type[Constants.fields]; 266 if(fields.type != Json.Type.Array) { 267 return; 268 } 269 auto curNameFieldRange = fields.byValue 270 .find!(f => f[Constants.name].to!string() == curName); 271 if(curNameFieldRange.empty) { 272 return; 273 } 274 275 auto curNameField = curNameFieldRange.front; 276 277 Json curArgs = curNameField[Constants.args]; 278 auto argElem = curArgs.byValue.find!(a => a[Constants.name] == argName); 279 280 enforce!ArgumentDoesNotExist(!argElem.empty, format!( 281 "Argument with name '%s' does not exist for field '%s' of type " 282 ~ " '%s'")(argName, curName, parent.type[Constants.name])); 283 284 if(arg.vv.ruleSelection == ValueOrVariableEnum.Var) { 285 const varName = arg.vv.var.name.value; 286 auto varType = varName in this.variables; 287 enforce(varName !is null); 288 289 string typeStr = astTypeToString(*varType); 290 enforce!VariableInputTypeMismatch( 291 argElem.front[Constants.typenameOrig] == typeStr, 292 format!"Variable type '%s' does not match argument type '%s'" 293 (argElem.front[Constants.typenameOrig], typeStr)); 294 } 295 } 296 } 297 298 import graphql.testschema; 299 300 private void test(T)(string str) { 301 GQLDSchema!(Schema) testSchema = new GQLDSchema!(Schema)(); 302 auto doc = lexAndParse(str); 303 auto fv = new SchemaValidator!Schema(doc, testSchema); 304 305 static if(is(T == void)) { 306 assertNotThrown(fv.accept(doc)); 307 } else { 308 assertThrown!T(fv.accept(doc)); 309 } 310 } 311 312 unittest { 313 string str = ` 314 subscription sub { 315 starships { 316 id 317 name 318 } 319 }`; 320 test!void(str); 321 } 322 323 unittest { 324 string str = ` 325 subscription sub { 326 starships { 327 id 328 name 329 } 330 starships { 331 size 332 } 333 }`; 334 335 test!SingleRootField(str); 336 } 337 338 unittest { 339 string str = ` 340 subscription sub { 341 ...multipleSubscriptions 342 } 343 344 fragment multipleSubscriptions on Subscription { 345 starships { 346 id 347 name 348 } 349 starships { 350 size 351 } 352 }`; 353 354 test!SingleRootField(str); 355 } 356 357 unittest { 358 string str = ` 359 subscription sub { 360 starships { 361 id 362 name 363 } 364 __typename 365 }`; 366 367 test!SingleRootField(str); 368 } 369 370 unittest { 371 string str = ` 372 { 373 starships { 374 id 375 } 376 } 377 `; 378 379 test!void(str); 380 } 381 382 unittest { 383 string str = ` 384 { 385 starships { 386 fieldDoesNotExist 387 } 388 } 389 `; 390 391 test!FieldDoesNotExist(str); 392 } 393 394 unittest { 395 string str = ` 396 { 397 starships { 398 id { 399 name 400 } 401 } 402 } 403 `; 404 405 test!FieldDoesNotExist(str); 406 } 407 408 unittest { 409 string str = ` 410 query q { 411 search { 412 shipId 413 } 414 }`; 415 416 test!FieldDoesNotExist(str); 417 } 418 419 unittest { 420 string str = ` 421 query q { 422 search { 423 ...ShipFrag 424 } 425 } 426 427 fragment ShipFrag on Starship { 428 designation 429 } 430 `; 431 432 test!void(str); 433 } 434 435 unittest { 436 string str = ` 437 query q { 438 search { 439 ...ShipFrag 440 ...CharFrag 441 } 442 } 443 444 fragment ShipFrag on Starship { 445 designation 446 } 447 448 fragment CharFrag on Character { 449 foobar 450 } 451 `; 452 453 test!FieldDoesNotExist(str); 454 } 455 456 unittest { 457 string str = ` 458 mutation q { 459 addCrewman { 460 ...CharFrag 461 } 462 } 463 464 fragment CharFrag on Character { 465 name 466 } 467 `; 468 469 test!void(str); 470 } 471 472 unittest { 473 string str = ` 474 mutation q { 475 addCrewman { 476 ...CharFrag 477 } 478 } 479 480 fragment CharFrag on Character { 481 foobar 482 } 483 `; 484 485 test!FieldDoesNotExist(str); 486 } 487 488 unittest { 489 string str = ` 490 subscription q { 491 starships { 492 id 493 designation 494 } 495 } 496 `; 497 498 test!void(str); 499 } 500 501 unittest { 502 string str = ` 503 subscription q { 504 starships { 505 id 506 doesNotExist 507 } 508 } 509 `; 510 511 test!FieldDoesNotExist(str); 512 } 513 514 unittest { 515 string str = ` 516 query q { 517 search { 518 ...ShipFrag 519 ...CharFrag 520 } 521 } 522 523 fragment ShipFrag on Starship { 524 designation 525 } 526 527 fragment CharFrag on Character { 528 name 529 } 530 `; 531 532 test!void(str); 533 } 534 535 unittest { 536 string str = ` 537 { 538 starships { 539 __typename 540 } 541 } 542 `; 543 544 test!void(str); 545 } 546 547 unittest { 548 string str = ` 549 { 550 __schema { 551 types { 552 name 553 } 554 } 555 } 556 `; 557 558 test!void(str); 559 } 560 561 unittest { 562 string str = ` 563 { 564 __schema { 565 types { 566 enumValues { 567 name 568 } 569 } 570 } 571 } 572 `; 573 574 test!void(str); 575 } 576 577 unittest { 578 string str = ` 579 { 580 __schema { 581 types { 582 enumValues { 583 doesNotExist 584 } 585 } 586 } 587 } 588 `; 589 590 test!FieldDoesNotExist(str); 591 } 592 593 unittest { 594 string str = ` 595 query q { 596 search { 597 ...CharFrag 598 } 599 } 600 601 fragment CharFrag on NonExistingType { 602 name 603 } 604 `; 605 606 test!UnknownTypeName(str); 607 } 608 609 unittest { 610 string str = ` 611 query q { 612 search { 613 ...CharFrag 614 } 615 } 616 617 fragment CharFrag on Character { 618 name 619 } 620 `; 621 622 test!void(str); 623 } 624 625 unittest { 626 string str = ` 627 query q { 628 starships { 629 id { 630 ...CharFrag 631 } 632 } 633 } 634 635 fragment CharFrag on Character { 636 name { 637 foo 638 } 639 } 640 `; 641 642 test!FragmentNotOnCompositeType(str); 643 } 644 645 unittest { 646 string str = ` 647 query q { 648 starships { 649 crew 650 } 651 } 652 `; 653 654 test!LeafIsNotAScalar(str); 655 } 656 657 unittest { 658 string str = ` 659 query q { 660 starships { 661 crew { 662 name 663 } 664 } 665 } 666 `; 667 668 test!void(str); 669 } 670 671 unittest { 672 string str = ` 673 { 674 starships { 675 crew { 676 id 677 ship 678 } 679 } 680 } 681 `; 682 683 test!LeafIsNotAScalar(str); 684 } 685 686 unittest { 687 string str = ` 688 { 689 starships { 690 crew { 691 id 692 ships 693 } 694 } 695 }`; 696 697 test!LeafIsNotAScalar(str); 698 } 699 700 unittest { 701 string str = ` 702 { 703 starships 704 }`; 705 706 test!LeafIsNotAScalar(str); 707 } 708 709 unittest { 710 string str = ` 711 query q($size: String) { 712 starships(overSize: $size) { 713 id 714 } 715 }`; 716 717 test!VariableInputTypeMismatch(str); 718 } 719 720 unittest { 721 string str = ` 722 query q($size: Float!) { 723 starships(overSize: $size) { 724 id 725 } 726 }`; 727 728 test!void(str); 729 } 730 731 unittest { 732 string str = ` 733 query q($ships: [Int!]!) { 734 shipsselection(ids: $ships) { 735 id 736 } 737 }`; 738 739 test!void(str); 740 } 741 742 unittest { 743 string str = ` 744 { 745 starships { 746 crew { 747 ... on Humanoid { 748 dateOfBirth 749 } 750 } 751 } 752 }`; 753 754 test!void(str); 755 } 756 757 unittest { 758 string str = ` 759 { 760 starships { 761 crew { 762 ... on Humanoid { 763 doesNotExist 764 } 765 } 766 } 767 }`; 768 769 test!FieldDoesNotExist(str); 770 }