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