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 }