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