1 module graphql.validation.querybased; 2 3 import std.array : array, back, empty, popBack; 4 import std.algorithm.searching : canFind; 5 import std.algorithm.sorting : sort; 6 import std.algorithm.iteration : each, uniq, filter, map, joiner; 7 import std.algorithm.setops : setDifference; 8 import std.algorithm.comparison : equal; 9 import std.conv : to; 10 import std.format : format; 11 import std.exception : enforce; 12 import std.experimental.logger; 13 14 import vibe.data.json; 15 16 import fixedsizearray; 17 18 import graphql.ast; 19 import graphql.builder; 20 import graphql.helper : lexAndParse; 21 import graphql.visitor; 22 import graphql.validation.exception; 23 24 import std.exception : assertThrown, assertNotThrown; 25 import std.stdio; 26 import graphql.lexer; 27 import graphql.parser; 28 import graphql.treevisitor; 29 30 @safe: 31 32 struct OperationFragVar { 33 const(OperationDefinition) op; 34 bool[string] fragmentsUsed; 35 bool[string] variablesDefined; 36 bool[string] variablesUsed; 37 38 this(const(OperationDefinition) op) { 39 this.op = op; 40 } 41 } 42 43 class QueryValidator : ConstVisitor { 44 import std.experimental.typecons : Final; 45 alias enter = ConstVisitor.enter; 46 alias exit = ConstVisitor.exit; 47 alias accept = ConstVisitor.accept; 48 49 const(Document) doc; 50 51 this(const(Document) doc) { 52 this.doc = doc; 53 } 54 55 string[][string] fragmentChildren; 56 FixedSizeArray!(string,64) curFragment; 57 string[] allFrags; 58 bool[string] reachedFragments; 59 60 // Lone Anonymous Operation 61 bool laoFound; 62 63 // Unique Operation names 64 bool[string] operationNames; 65 66 // Unique Argument names 67 bool[string] argumentNames; 68 69 // Unique Variable names 70 bool[string] variableNames; 71 72 // Variable usage 73 bool variableDefinitionDone; 74 75 string[string] variablesUsedByFragments; 76 77 OperationFragVar[] operations; 78 79 // Unique directives per Field 80 bool[string][] directiveNames; 81 82 override void exit(const(Document) doc) { 83 foreach(ref OperationFragVar op; this.operations) { 84 bool[string] allFragsReached = allReachable(op.fragmentsUsed, 85 this.fragmentChildren 86 ); 87 bool[string] varsUsedByOp = computeVariablesUsedByFragments( 88 allFragsReached, this.variablesUsedByFragments 89 ); 90 91 auto allVars = op.variablesDefined.byKey().array.sort.uniq; 92 auto varUsed = (varsUsedByOp.byKey().array ~ 93 op.variablesUsed.byKey().array) 94 .sort.uniq; 95 96 enforce!VariablesUseException(equal(allVars, varUsed), 97 format("Variables available [%(%s, %)], " 98 ~ "Variables used [%(%s, %)]\n" 99 ~ "Unused available [%(%s, %)]\n" 100 ~ "Required unavailable [%(%s, %)]" 101 , allVars, varUsed 102 , allVars.filter!(it => !canFind(varUsed, it)) 103 , varUsed.filter!(it => !canFind(allVars, it)) 104 ) 105 ); 106 } 107 } 108 109 override void enter(const(InlineFragment) f) { 110 // Unique directive names 111 bool[string] tmp; 112 this.directiveNames ~= tmp; 113 } 114 115 override void exit(const(InlineFragment) f) { 116 this.directiveNames.popBack(); 117 } 118 119 override void enter(const(Field) f) { 120 // Unique directive names 121 bool[string] tmp; 122 this.directiveNames ~= tmp; 123 } 124 125 override void exit(const(Field) f) { 126 this.directiveNames.popBack(); 127 } 128 129 override void enter(const(Definition) od) { 130 enforce!NoTypeSystemDefinitionException( 131 od.ruleSelection != DefinitionEnum.T); 132 } 133 134 override void enter(const(Directive) dir) { 135 enforce!DirectiveNotUnique(dir.name.value !in this.directiveNames.back, 136 format("Directive '%s' must be unique at %s:%s", dir.name.value, 137 dir.name.line, dir.name.column)); 138 139 this.directiveNames.back[dir.name.value] = true; 140 } 141 142 override void enter(const(OperationDefinition) od) { 143 this.operations ~= OperationFragVar(od); 144 145 this.variableDefinitionDone = false; 146 147 enforce!LoneAnonymousOperationException(!this.laoFound); 148 if(canFind([OperationDefinitionEnum.SelSet, 149 OperationDefinitionEnum.OT, 150 OperationDefinitionEnum.OT_D, 151 OperationDefinitionEnum.OT_V, 152 OperationDefinitionEnum.OT_VD] ,od.ruleSelection)) 153 { 154 this.laoFound = true; 155 } else { 156 enforce!NonUniqueOperationNameException(od.name.value !in 157 this.operationNames, format( 158 "Operation name '%s' already present in [%(%s, %)]", 159 od.name.value, this.operationNames.byKey() 160 ) 161 ); 162 this.operationNames[od.name.value] = true; 163 } 164 165 // Unique directive names 166 bool[string] tmp; 167 this.directiveNames ~= tmp; 168 } 169 170 override void exit(const(OperationDefinition) od) { 171 this.directiveNames.popBack(); 172 } 173 174 override void enter(const(FragmentDefinition) frag) { 175 enforce!FragmentNameAlreadyInUseException( 176 !canFind(this.allFrags, frag.name.value), 177 format("Fragment names '%s' already in use '[%(%s, %)]'", 178 frag.name.value, this.allFrags) 179 ); 180 181 this.allFrags ~= frag.name.value; 182 this.curFragment.insertBack(frag.name.value); 183 184 // Unique directive names 185 bool[string] tmp; 186 this.directiveNames ~= tmp; 187 } 188 189 190 override void exit(const(FragmentDefinition) frag) { 191 this.curFragment.removeBack(); 192 193 this.directiveNames.popBack(); 194 } 195 196 override void enter(const(FragmentSpread) fragSpread) { 197 if(!this.operations.empty) { 198 this.operations.back.fragmentsUsed[fragSpread.name.value] = true; 199 } 200 201 const(FragmentDefinition) frag = findFragment(this.doc, 202 fragSpread.name.value 203 ); 204 205 enforce!FragmentNotFoundException(frag !is null, 206 format("No Fragment with name '%s' could be found", 207 fragSpread.name.value) 208 ); 209 210 this.reachedFragments[frag.name.value] = true; 211 212 if(!this.curFragment.empty) { 213 string[]* children = 214 () @trusted { 215 return &(this.fragmentChildren.require( 216 this.curFragment.back, new string[](0) 217 )); 218 }(); 219 (*children) ~= frag.name.value; 220 } 221 222 // Unique directive names 223 bool[string] tmp; 224 this.directiveNames ~= tmp; 225 } 226 227 override void enter(const(Arguments) al) { 228 () @trusted { 229 this.argumentNames.clear(); 230 }(); 231 } 232 233 override void enter(const(Argument) al) { 234 enforce!ArgumentsNotUniqueException(al.name.value !in this.argumentNames, 235 format("Argument with name '%s' already present in [%(%s, %)]", 236 al.name.value, this.argumentNames.byKey() 237 ) 238 ); 239 this.argumentNames[al.name.value] = true; 240 } 241 242 override void enter(const(VariableDefinitions) vd) { 243 () @trusted { 244 this.variableNames.clear(); 245 }(); 246 } 247 248 override void exit(const(VariableDefinitions) vd) { 249 this.variableDefinitionDone = true; 250 } 251 252 override void enter(const(Variable) v) { 253 if(variableDefinitionDone) { 254 this.operations.back.variablesUsed[v.name.value] = true; 255 if(!this.curFragment.empty) { 256 this.variablesUsedByFragments[this.curFragment.back] = 257 v.name.value; 258 } 259 } else { 260 enforce!VariablesNotUniqueException( 261 v.name.value !in this.variableNames, 262 format( 263 "Variable with name '%s' already present in [%(%s, %)]", 264 v.name.value, this.variableNames.byKey() 265 ) 266 ); 267 this.variableNames[v.name.value] = true; 268 this.operations.back.variablesDefined[v.name.value] = true; 269 } 270 } 271 } 272 273 bool[string] computeVariablesUsedByFragments(bool[string] fragmentsUsed, 274 string[string] variablesUsed) 275 { 276 bool[string] ret; 277 foreach(a; fragmentsUsed 278 .byKey() 279 .filter!(a => a in variablesUsed) 280 .map!(a => variablesUsed[a]) 281 .array 282 .sort 283 .uniq) 284 { 285 ret[a] = true; 286 } 287 return ret; 288 } 289 290 bool noCylces(string[][string] frags) { 291 foreach(string key; frags.byKey()) { 292 noCylcesImpl([key], frags); 293 } 294 return true; 295 } 296 297 void noCylcesImpl(string[] toFind, string[][string] frags) { 298 auto toIter = toFind.back in frags; 299 if(toIter is null) { 300 return; 301 } 302 303 string[] follow = *toIter; 304 foreach(f; follow) { 305 enforce!FragmentCycleException(!canFind(toFind, f), format( 306 "Found a cycle in the fragments '[%(%s -> %)]'", toFind ~ f 307 )); 308 noCylcesImpl(toFind ~ f, frags); 309 } 310 } 311 312 bool[string] allReachable(bool[string] reached, string[][string] fragChildren) { 313 bool[string] ret; 314 foreach(string key; reached.byKey()) { 315 enforce(!key.empty); 316 ret[key] = true; 317 } 318 size_t oldLen; 319 do { 320 oldLen = ret.length; 321 foreach(string key; ret.byKey()) { 322 string[]* follow = key in fragChildren; 323 if(follow !is null) { 324 foreach(string f; *follow) { 325 assert(!f.empty, format("%s [%(%s, %)]", key, *follow)); 326 ret[f] = true; 327 } 328 } 329 } 330 } while(ret.length > oldLen); 331 return ret; 332 } 333 334 void allFragmentsReached(QueryValidator fv) { 335 bool[string] reached = allReachable(fv.reachedFragments, 336 fv.fragmentChildren); 337 auto af = fv.allFrags.sort; 338 auto r = reached.byKey().array.sort; 339 enforce!UnusedFragmentsException(equal(af, r), 340 format("Fragments [%(%s, %)] are unused, allFrags [%(%s, %)], " 341 ~ "reached [%(%s, %)]", 342 setDifference(af, r), af, r 343 ) 344 ); 345 } 346 347 unittest { 348 string simpleCylce = ` 349 fragment Frag0 on Foo { 350 ...Frag1 351 } 352 353 fragment Frag1 on Foo { 354 ...Frag0 355 } 356 357 query Q { 358 ...Frag0 359 }`; 360 361 auto doc = lexAndParse(simpleCylce); 362 const(FragmentDefinition) f0 = findFragment(doc, "Frag0"); 363 assert(f0 !is null); 364 const(FragmentDefinition) f1 = findFragment(doc, "Frag1"); 365 assert(f1 !is null); 366 367 QueryValidator fv = new QueryValidator(doc); 368 fv.accept(doc); 369 assertThrown!(FragmentCycleException)(noCylces(fv.fragmentChildren)); 370 assertNotThrown(allFragmentsReached(fv)); 371 } 372 373 unittest { 374 string biggerCylce = ` 375 fragment Frag0 on Foo { 376 ...Frag1 377 } 378 379 fragment Frag1 on Foo { 380 ...Frag2 381 } 382 383 fragment Frag2 on Foo { 384 ...Frag0 385 } 386 387 query Q { 388 ...Frag0 389 }`; 390 391 auto doc = lexAndParse(biggerCylce); 392 393 QueryValidator fv = new QueryValidator(doc); 394 fv.accept(doc); 395 assertThrown!(FragmentCycleException)(noCylces(fv.fragmentChildren)); 396 assertNotThrown(allFragmentsReached(fv)); 397 } 398 399 unittest { 400 string biggerCylce = ` 401 fragment Frag0 on Foo { 402 ...Frag1 403 } 404 405 fragment Frag1 on Foo { 406 ...Frag2 407 } 408 409 fragment Frag2 on Foo { 410 hello 411 } 412 413 query Q { 414 ...Frag0 415 }`; 416 417 auto doc = lexAndParse(biggerCylce); 418 419 QueryValidator fv = new QueryValidator(doc); 420 fv.accept(doc); 421 assertNotThrown(noCylces(fv.fragmentChildren)); 422 assertNotThrown(allFragmentsReached(fv)); 423 } 424 425 unittest { 426 import std.stdio; 427 string biggerCylce = ` 428 query Q { 429 ...Frag0 430 } 431 432 fragment Frag0 on Foo { 433 ...Frag1 434 } 435 436 fragment Frag1 on Foo { 437 ...Frag 438 }`; 439 440 auto doc = lexAndParse(biggerCylce); 441 442 QueryValidator fv = new QueryValidator(doc); 443 assertThrown!FragmentNotFoundException(fv.accept(doc)); 444 assertNotThrown(allFragmentsReached(fv)); 445 } 446 447 unittest { 448 string biggerCylce = ` 449 fragment Frag0 on Foo { 450 ...Frag1 451 } 452 453 fragment Frag0 on Foo { 454 ...Frag2 455 } 456 457 fragment Frag1 on Foo { 458 ...Frag2 459 } 460 461 fragment Frag2 on Foo { 462 hello 463 } 464 465 fragment Frag4 on Foo { 466 hello 467 } 468 469 query Q { 470 ...Frag0 471 }`; 472 473 auto doc = lexAndParse(biggerCylce); 474 475 QueryValidator fv = new QueryValidator(doc); 476 assertThrown!FragmentNameAlreadyInUseException(fv.accept(doc)); 477 assertThrown!UnusedFragmentsException(allFragmentsReached(fv)); 478 } 479 480 unittest { 481 string biggerCylce = ` 482 mutation Q { 483 bar 484 } 485 486 query Q { 487 foo 488 }`; 489 490 auto l = Lexer(biggerCylce); 491 auto p = Parser(l); 492 const(Document) doc = p.parseDocument(); 493 494 QueryValidator fv = new QueryValidator(doc); 495 assertThrown!NonUniqueOperationNameException(fv.accept(doc)); 496 } 497 498 unittest { 499 string biggerCylce = ` 500 { 501 bar 502 } 503 504 query Q { 505 foo 506 }`; 507 508 auto doc = lexAndParse(biggerCylce); 509 510 QueryValidator fv = new QueryValidator(doc); 511 assertThrown!LoneAnonymousOperationException(fv.accept(doc)); 512 } 513 514 unittest { 515 string biggerCylce = ` 516 { 517 bar 518 } 519 520 { 521 foo 522 }`; 523 524 auto doc = lexAndParse(biggerCylce); 525 526 QueryValidator fv = new QueryValidator(doc); 527 assertThrown!LoneAnonymousOperationException(fv.accept(doc)); 528 } 529 530 unittest { 531 string biggerCylce = ` 532 { 533 bar 534 } 535 536 enum Dog { 537 foo 538 }`; 539 540 auto doc = lexAndParse(biggerCylce); 541 542 QueryValidator fv = new QueryValidator(doc); 543 assertThrown!NoTypeSystemDefinitionException(fv.accept(doc)); 544 } 545 546 unittest { 547 string biggerCylce = ` 548 query foo($x: Int!) { 549 bar(x: $x) { 550 a 551 } 552 }`; 553 554 auto doc = lexAndParse(biggerCylce); 555 556 QueryValidator fv = new QueryValidator(doc); 557 assertNotThrown!ArgumentsNotUniqueException(fv.accept(doc)); 558 } 559 560 unittest { 561 string biggerCylce = ` 562 query foo { 563 foo(x: 10, y: 11.1) { 564 bar(x: $x, y: $y) { 565 i 566 } 567 } 568 569 bar(x: 10, x: 11.1) { 570 bar(x: $x, y: $x) { 571 i 572 } 573 } 574 }`; 575 576 auto doc = lexAndParse(biggerCylce); 577 578 QueryValidator fv = new QueryValidator(doc); 579 assertThrown!ArgumentsNotUniqueException(fv.accept(doc)); 580 } 581 582 unittest { 583 string biggerCylce = ` 584 query foo($x: Int, $y: Float) { 585 bar(x: $x, y: $y) { 586 i 587 } 588 }`; 589 590 auto doc = lexAndParse(biggerCylce); 591 592 QueryValidator fv = new QueryValidator(doc); 593 assertNotThrown!VariablesNotUniqueException(fv.accept(doc)); 594 } 595 596 unittest { 597 string biggerCylce = ` 598 query foo($x: Int, $x: Float) { 599 bar(x: $x, y: $x) { 600 i 601 } 602 }`; 603 604 auto doc = lexAndParse(biggerCylce); 605 606 QueryValidator fv = new QueryValidator(doc); 607 assertThrown!VariablesNotUniqueException(fv.accept(doc)); 608 } 609 610 unittest { 611 string biggerCylce = ` 612 query foo($x: Int, $y: Float) { 613 bar(x: $x) { 614 i 615 } 616 }`; 617 618 auto doc = lexAndParse(biggerCylce); 619 620 QueryValidator fv = new QueryValidator(doc); 621 assertThrown!VariablesUseException(fv.accept(doc)); 622 } 623 624 unittest { 625 string biggerCylce = ` 626 query foo($x: Int) { 627 ...Foo 628 } 629 630 fragment Foo on Bar { 631 bar(x: $x) { 632 i 633 } 634 } 635 `; 636 637 auto doc = lexAndParse(biggerCylce); 638 639 QueryValidator fv = new QueryValidator(doc); 640 assertNotThrown!VariablesUseException(fv.accept(doc)); 641 } 642 643 unittest { 644 string biggerCylce = ` 645 query foo($x: Int, $y: Float) { 646 ...Foo 647 } 648 649 fragment Foo on Bar { 650 bar(x: $x) { 651 i 652 ...ZZZ 653 } 654 } 655 656 fragment ZZZ on Bar { 657 bar(x: $y) { 658 i 659 } 660 } 661 `; 662 663 auto doc = lexAndParse(biggerCylce); 664 665 QueryValidator fv = new QueryValidator(doc); 666 assertNotThrown!VariablesUseException(fv.accept(doc)); 667 } 668 669 unittest { 670 string biggerCylce = ` 671 query foo($x: Int, $y: Float) @skip(if: true) @skip(if: false) { 672 ...Foo 673 } 674 675 fragment Foo on Bar { 676 bar(x: $x) { 677 i 678 ...ZZZ 679 } 680 } 681 682 fragment ZZZ on Bar { 683 bar(x: $y) { 684 i 685 } 686 } 687 `; 688 689 auto doc = lexAndParse(biggerCylce); 690 691 QueryValidator fv = new QueryValidator(doc); 692 assertThrown!DirectiveNotUnique(fv.accept(doc)); 693 }