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