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