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, 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 c1 = argElem.front[Constants.typenameOrig] == typeStr;
288 				const c2 = (typeStr.endsWith("!")
289 							&& typeStr[0 .. $ - 1] == argElem.front[Constants.typenameOrig]);
290 				const c3 = (typeStr.endsWith("In")
291 							&& typeStr[0 .. $ - 2] == argElem.front[Constants.typenameOrig]);
292 				const c4 = (typeStr.endsWith("In!")
293 							&& (typeStr[0 .. $ - 3] ~ "!") == argElem.front[Constants.typenameOrig]);
294 				const c5 = (typeStr.endsWith("In]!")
295 							&& (typeStr[0 .. $ - 4] ~ "]!") == argElem.front[Constants.typenameOrig]);
296 				const c6 = (typeStr.endsWith("In!]!")
297 							&& (typeStr[0 .. $ - 5] ~ "!]!") == argElem.front[Constants.typenameOrig]);
298 				const c7 = (typeStr.endsWith("In!]")
299 							&& (typeStr[0 .. $ - 4] ~ "!]") == argElem.front[Constants.typenameOrig]);
300 				enforce!VariableInputTypeMismatch(c1 || c2 || c3 || c4 || c5
301 					|| c6 || c7
302 						, format("Variable type '%s' does not match argument type '%s'"
303 							~ " ! %s In %s In! %s c1 %s c2 %s c3 %s c4 %s c5 %s"
304 							~ " c6 %s c7 %s"
305 						, argElem.front[Constants.typenameOrig], typeStr
306 						, typeStr.endsWith("!"), typeStr.endsWith("In")
307 						, typeStr.endsWith("In!") , c1, c2, c3, c4, c5, c6, c7
308 						));
309 			}
310 		} else {
311 			enforce!ArgumentDoesNotExist(argName == "if", format(
312 					"Argument of Directive '%s' is 'if' not '%s'",
313 					this.directiveStack.back.name, argName));
314 
315 			if(arg.vv.ruleSelection == ValueOrVariableEnum.Var) {
316 				const varName = arg.vv.var.name.value;
317 				auto varType = varName in this.variables;
318 				enforce(varName !is null);
319 
320 				string typeStr = astTypeToString(*varType);
321 				enforce!VariableInputTypeMismatch(
322 						typeStr == "Boolean!",
323 						format("Variable type '%s' does not match argument type 'Boolean!'"
324 						, typeStr));
325 			}
326 		}
327 	}
328 }
329 
330 import graphql.testschema;
331 
332 private void test(T)(string str) {
333 	GQLDSchema!(Schema) testSchema = new GQLDSchema!(Schema)();
334 	auto doc = lexAndParse(str);
335 	auto fv = new SchemaValidator!Schema(doc, testSchema);
336 
337 	static if(is(T == void)) {
338 		assertNotThrown(fv.accept(doc));
339 	} else {
340 		assertThrown!T(fv.accept(doc));
341 	}
342 }
343 
344 unittest {
345 	string str = `
346 subscription sub {
347 	starships {
348 		id
349 		name
350 	}
351 }`;
352 	test!void(str);
353 }
354 
355 unittest {
356 	string str = `
357 subscription sub {
358 	starships {
359 		id
360 		name
361 	}
362 	starships {
363 		size
364 	}
365 }`;
366 
367 	test!SingleRootField(str);
368 }
369 
370 unittest {
371 	string str = `
372 subscription sub {
373 	...multipleSubscriptions
374 }
375 
376 fragment multipleSubscriptions on Subscription {
377 	starships {
378 		id
379 		name
380 	}
381 	starships {
382 	size
383 	}
384 }`;
385 
386 	test!SingleRootField(str);
387 }
388 
389 unittest {
390 	string str = `
391 subscription sub {
392 	starships {
393 		id
394 		name
395 	}
396 	__typename
397 }`;
398 
399 	test!SingleRootField(str);
400 }
401 
402 unittest {
403 	string str = `
404 {
405 	starships {
406 		id
407 	}
408 }
409 `;
410 
411 	test!void(str);
412 }
413 
414 unittest {
415 	string str = `
416 {
417 	starships {
418 		fieldDoesNotExist
419 	}
420 }
421 `;
422 
423 	test!FieldDoesNotExist(str);
424 }
425 
426 unittest {
427 	string str = `
428 {
429 	starships {
430 		id {
431 			name
432 		}
433 	}
434 }
435 `;
436 
437 	test!FieldDoesNotExist(str);
438 }
439 
440 unittest {
441 	string str = `
442 query q {
443 	search {
444 		shipId
445 	}
446 }`;
447 
448 	test!FieldDoesNotExist(str);
449 }
450 
451 unittest {
452 	string str = `
453 query q {
454 	search {
455 		...ShipFrag
456 	}
457 }
458 
459 fragment ShipFrag on Starship {
460 	designation
461 }
462 `;
463 
464 	test!void(str);
465 }
466 
467 unittest {
468 	string str = `
469 query q {
470 	search {
471 		...ShipFrag
472 		...CharFrag
473 	}
474 }
475 
476 fragment ShipFrag on Starship {
477 	designation
478 }
479 
480 fragment CharFrag on Character {
481 	foobar
482 }
483 `;
484 
485 	test!FieldDoesNotExist(str);
486 }
487 
488 unittest {
489 	string str = `
490 mutation q {
491 	addCrewman {
492 		...CharFrag
493 	}
494 }
495 
496 fragment CharFrag on Character {
497 	name
498 }
499 `;
500 
501 	test!void(str);
502 }
503 
504 unittest {
505 	string str = `
506 mutation q {
507 	addCrewman {
508 		...CharFrag
509 	}
510 }
511 
512 fragment CharFrag on Character {
513 	foobar
514 }
515 `;
516 
517 	test!FieldDoesNotExist(str);
518 }
519 
520 unittest {
521 	string str = `
522 subscription q {
523 	starships {
524 		id
525 		designation
526 	}
527 }
528 `;
529 
530 	test!void(str);
531 }
532 
533 unittest {
534 	string str = `
535 subscription q {
536 	starships {
537 		id
538 		doesNotExist
539 	}
540 }
541 `;
542 
543 	test!FieldDoesNotExist(str);
544 }
545 
546 unittest {
547 	string str = `
548 query q {
549 	search {
550 		...ShipFrag
551 		...CharFrag
552 	}
553 }
554 
555 fragment ShipFrag on Starship {
556 	designation
557 }
558 
559 fragment CharFrag on Character {
560 	name
561 }
562 `;
563 
564 	test!void(str);
565 }
566 
567 unittest {
568 	string str = `
569 {
570 	starships {
571 		__typename
572 	}
573 }
574 `;
575 
576 	test!void(str);
577 }
578 
579 unittest {
580 	string str = `
581 {
582 	__schema {
583 		types {
584 			name
585 		}
586 	}
587 }
588 `;
589 
590 	test!void(str);
591 }
592 
593 unittest {
594 	string str = `
595 {
596 	__schema {
597 		types {
598 			enumValues {
599 				name
600 			}
601 		}
602 	}
603 }
604 `;
605 
606 	test!void(str);
607 }
608 
609 unittest {
610 	string str = `
611 {
612 	__schema {
613 		types {
614 			enumValues {
615 				doesNotExist
616 			}
617 		}
618 	}
619 }
620 `;
621 
622 	test!FieldDoesNotExist(str);
623 }
624 
625 unittest {
626 	string str = `
627 query q {
628 	search {
629 		...CharFrag
630 	}
631 }
632 
633 fragment CharFrag on NonExistingType {
634 	name
635 }
636 `;
637 
638 	test!UnknownTypeName(str);
639 }
640 
641 unittest {
642 	string str = `
643 query q {
644 	search {
645 		...CharFrag
646 	}
647 }
648 
649 fragment CharFrag on Character {
650 	name
651 }
652 `;
653 
654 	test!void(str);
655 }
656 
657 unittest {
658 	string str = `
659 query q {
660 	starships {
661 		id {
662 			...CharFrag
663 		}
664 	}
665 }
666 
667 fragment CharFrag on Character {
668 	name {
669 		foo
670 	}
671 }
672 `;
673 
674 	test!FragmentNotOnCompositeType(str);
675 }
676 
677 unittest {
678 	string str = `
679 query q {
680 	starships {
681 		crew
682 	}
683 }
684 `;
685 
686 	test!LeafIsNotAScalar(str);
687 }
688 
689 unittest {
690 	string str = `
691 query q {
692 	starships {
693 		crew {
694 			name
695 		}
696 	}
697 }
698 `;
699 
700 	test!void(str);
701 }
702 
703 unittest {
704 	string str = `
705 {
706 	starships {
707 		crew {
708 			id
709 			ship
710 		}
711 	}
712 }
713 `;
714 
715 	test!LeafIsNotAScalar(str);
716 }
717 
718 unittest {
719 	string str = `
720 {
721 	starships {
722 		crew {
723 			id
724 			ships
725 		}
726 	}
727 }`;
728 
729 	test!LeafIsNotAScalar(str);
730 }
731 
732 unittest {
733 	string str = `
734 {
735 	starships
736 }`;
737 
738 	test!LeafIsNotAScalar(str);
739 }
740 
741 unittest {
742 	string str = `
743 query q($size: String) {
744 	starships(overSize: $size) {
745 		id
746 	}
747 }`;
748 
749 	test!VariableInputTypeMismatch(str);
750 }
751 
752 unittest {
753 	string str = `
754 query q($size: Float!) {
755 	starships(overSize: $size) {
756 		id
757 	}
758 }`;
759 
760 	test!void(str);
761 }
762 
763 unittest {
764 	string str = `
765 query q($ships: [Int!]!) {
766 	shipsselection(ids: $ships) {
767 		id
768 	}
769 }`;
770 
771 	test!void(str);
772 }
773 
774 unittest {
775 	string str = `
776 {
777 	starships {
778 		crew {
779 			... on Humanoid {
780 				dateOfBirth
781 			}
782 		}
783 	}
784 }`;
785 
786 	test!void(str);
787 }
788 
789 unittest {
790 	string str = `
791 {
792 	starships {
793 		crew {
794 			... on Humanoid {
795 				doesNotExist
796 			}
797 		}
798 	}
799 }`;
800 
801 	test!FieldDoesNotExist(str);
802 }
803 
804 unittest {
805 	string str = `
806 query q($cw: Boolean!) {
807 	starships {
808 		crew @include(if: $cw) {
809 			... on Humanoid {
810 				dateOfBirth
811 			}
812 		}
813 	}
814 }`;
815 
816 	test!void(str);
817 }
818 
819 unittest {
820 	string str = `
821 query q($cw: Int!) {
822 	starships {
823 		crew @include(if: $cw) {
824 			... on Humanoid {
825 				dateOfBirth
826 			}
827 		}
828 	}
829 }`;
830 
831 	test!VariableInputTypeMismatch(str);
832 }
833 
834 unittest {
835 	string str = `
836 query q($cw: Int!) {
837 	starships {
838 		crew @include(notIf: $cw) {
839 			... on Humanoid {
840 				dateOfBirth
841 			}
842 		}
843 	}
844 }`;
845 
846 	test!ArgumentDoesNotExist(str);
847 }
848 
849 unittest {
850 	string str = `
851 query {
852 	numberBetween(searchInput:
853 		{ first: 10
854 		, after: null
855 		}
856 	) {
857 		id
858 	}
859 }
860 `;
861 	test!void(str);
862 }
863 
864 unittest {
865 	string str = `
866 query foo($after: String) {
867 	numberBetween(searchInput:
868 		{ first: 10
869 		, after: $after
870 		}
871 	) {
872 		id
873 	}
874 }
875 `;
876 	test!void(str);
877 }
878 
879 unittest {
880 	string str = `
881 query q {
882 	androids {
883 		primaryFunction #inherited
884 		name #not inherited
885 	}
886 }
887 `;
888 	test!void(str);
889 }