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