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