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