1 module mongoschema;
2 
3 import vibe.data.bson;
4 import vibe.db.mongo.collection;
5 import vibe.db.mongo.connection;
6 import std.datetime;
7 import std.traits;
8 import core.time;
9 import std.typecons : tuple, BitFlags, isTuple, Tuple;
10 import std.datetime : SysTime;
11 
12 // Bson Attributes
13 
14 /// Will ignore the variables and not encode/decode them.
15 enum schemaIgnore;
16 /// Custom encode function. `func` is the name of the function which must be present as child.
17 struct encode
18 { /++ Function name (needs to be member function) +/ string func;
19 }
20 /// Custom decode function. `func` is the name of the function which must be present as child.
21 struct decode
22 { /++ Function name (needs to be member function) +/ string func;
23 }
24 /// Encodes the value as binary value. Must be an array with one byte wide elements.
25 struct binaryType
26 { /++ Type to encode +/ BsonBinData.Type type = BsonBinData.Type.generic;
27 }
28 /// Custom name for special characters.
29 struct schemaName
30 { /++ Custom replacement name +/ string name;
31 }
32 
33 // Mongo Attributes
34 /// Will create an index with (by default) no flags.
35 enum mongoForceIndex;
36 /// Background index construction allows read and write operations to continue while building the index.
37 enum mongoBackground;
38 /// Drops duplicates in the database. Only for Mongo versions less than 3.0
39 enum mongoDropDuplicates;
40 /// Sparse indexes are like non-sparse indexes, except that they omit references to documents that do not include the indexed field.
41 enum mongoSparse;
42 /// MongoDB allows you to specify a unique constraint on an index. These constraints prevent applications from inserting documents that have duplicate values for the inserted fields.
43 enum mongoUnique;
44 /// TTL indexes expire documents after the specified number of seconds has passed since the indexed field value; i.e. the expiration threshold is the indexed field value plus the specified number of seconds.
45 /// Field must be a SchemaDate/BsonDate. You must update the time using collMod.
46 struct mongoExpire
47 {
48 	///
49 	this(long seconds)
50 	{
51 		this.seconds = cast(ulong) seconds;
52 	}
53 	///
54 	this(ulong seconds)
55 	{
56 		this.seconds = seconds;
57 	}
58 	///
59 	this(Duration time)
60 	{
61 		seconds = cast(ulong) time.total!"msecs";
62 	}
63 	///
64 	ulong seconds;
65 }
66 
67 private template isVariable(alias T)
68 {
69 	enum isVariable = !is(T) && is(typeof(T)) && !isCallable!T
70 			&& !is(T == void) && !__traits(isStaticFunction, T) && !__traits(isOverrideFunction, T)
71 			&& !__traits(isFinalFunction, T) && !__traits(isAbstractFunction, T)
72 			&& !__traits(isVirtualFunction, T) && !__traits(isVirtualMethod,
73 					T) && !is(ReturnType!T);
74 }
75 
76 private template isVariable(T)
77 {
78 	enum isVariable = false; // Types are no variables
79 }
80 
81 private template getUDAs(alias symbol, alias attribute)
82 {
83 	import std.typetuple : Filter;
84 
85 	enum isDesiredUDA(alias S) = is(typeof(S) == attribute);
86 	alias getUDAs = Filter!(isDesiredUDA, __traits(getAttributes, symbol));
87 }
88 
89 private Bson memberToBson(T)(T member)
90 {
91 	static if (__traits(hasMember, T, "toBson") && is(ReturnType!(typeof(T.toBson)) == Bson))
92 	{
93 		// Custom defined toBson
94 		return T.toBson(member);
95 	}
96 	else static if (is(T == Json))
97 	{
98 		return Bson.fromJson(member);
99 	}
100 	else static if (is(T == BsonBinData) || is(T == BsonObjectID)
101 			|| is(T == BsonDate) || is(T == BsonTimestamp)
102 			|| is(T == BsonRegex) || is(T == typeof(null)))
103 	{
104 		return Bson(member);
105 	}
106 	else static if (is(T == enum))
107 	{ // Enum value
108 		return Bson(cast(OriginalType!T) member);
109 	}
110 	else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe))
111 	{ // std.typecons.BitFlags
112 		return Bson(cast(OriginalType!Enum) member);
113 	}
114 	else static if (isArray!(T) && !isSomeString!T || isTuple!T)
115 	{ // Arrays of anything except strings
116 		Bson[] values;
117 		foreach (val; member)
118 			values ~= memberToBson(val);
119 		return Bson(values);
120 	}
121 	else static if (isAssociativeArray!T)
122 	{ // Associative Arrays (Objects)
123 		Bson[string] values;
124 		foreach (name, val; member)
125 			values[name] = memberToBson(val);
126 		return Bson(values);
127 	}
128 	else static if (is(T == Bson))
129 	{ // Already a Bson object
130 		return member;
131 	}
132 	else static if (__traits(compiles, { Bson(member); }))
133 	{ // Check if this can be passed
134 		return Bson(member);
135 	}
136 	else static if (!isBasicType!T)
137 	{
138 		// Mixed in MongoSchema
139 		return member.toSchemaBson();
140 	}
141 	else // Generic value
142 	{
143 		pragma(msg, "Warning falling back to serializeToBson for type " ~ T.stringof);
144 		return serializeToBson(member);
145 	}
146 }
147 
148 private T bsonToMember(T)(auto ref T member, Bson value)
149 {
150 	static if (__traits(hasMember, T, "fromBson") && is(ReturnType!(typeof(T.fromBson)) == T))
151 	{
152 		// Custom defined toBson
153 		return T.fromBson(value);
154 	}
155 	else static if (is(T == Json))
156 	{
157 		return Bson.fromJson(value);
158 	}
159 	else static if (is(T == BsonBinData) || is(T == BsonObjectID)
160 			|| is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex))
161 	{
162 		return value.get!T;
163 	}
164 	else static if (is(T == enum))
165 	{ // Enum value
166 		return cast(T) value.get!(OriginalType!T);
167 	}
168 	else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe))
169 	{ // std.typecons.BitFlags
170 		return cast(T) cast(Enum) value.get!(OriginalType!Enum);
171 	}
172 	else static if (isTuple!T)
173 	{ // Tuples
174 		auto bsons = value.get!(Bson[]);
175 		T values;
176 		foreach (i, val; values)
177 		{
178 			values[i] = bsonToMember!(typeof(val))(values[i], bsons[i]);
179 		}
180 		return values;
181 	}
182 	else static if (isArray!T && !isSomeString!T)
183 	{ // Arrays of anything except strings
184 		alias Type = typeof(member[0]);
185 		T values;
186 		foreach (val; value)
187 		{
188 			values ~= bsonToMember!Type(Type.init, val);
189 		}
190 		return values;
191 	}
192 	else static if (isAssociativeArray!T)
193 	{ // Associative Arrays (Objects)
194 		T values;
195 		alias ValType = ValueType!T;
196 		foreach (name, val; value)
197 			values[name] = bsonToMember!ValType(ValType.init, val);
198 		return values;
199 	}
200 	else static if (is(T == Bson))
201 	{ // Already a Bson object
202 		return value;
203 	}
204 	else static if (__traits(compiles, { value.get!T(); }))
205 	{ // Check if this can be passed
206 		return value.get!T();
207 	}
208 	else static if (!isBasicType!T)
209 	{
210 		// Mixed in MongoSchema
211 		return value.fromSchemaBson!T();
212 	}
213 	else // Generic value
214 	{
215 		pragma(msg, "Warning falling back to deserializeBson for type " ~ T.stringof);
216 		return deserializeBson!T(value);
217 	}
218 }
219 
220 /// Generates a Bson document from a struct/class object
221 Bson toSchemaBson(T)(T obj)
222 {
223 	static if (__traits(compiles, cast(T) null) && __traits(compiles, {
224 			T foo = null;
225 		}))
226 	{
227 		if (obj is null)
228 			return Bson(null);
229 	}
230 
231 	Bson data = Bson.emptyObject;
232 
233 	static if (hasMember!(T, "_schema_object_id_"))
234 	{
235 		if (obj.bsonID.valid)
236 			data["_id"] = obj.bsonID;
237 	}
238 
239 	foreach (memberName; __traits(allMembers, T))
240 	{
241 		static if (memberName == "_schema_object_id_")
242 			continue;
243 		else static if (__traits(compiles, {
244 				static s = isVariable!(__traits(getMember, obj, memberName));
245 			}) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, {
246 				static s = __traits(getMember, T, memberName);
247 			}) // No static members
248 			 && __traits(compiles, {
249 				typeof(__traits(getMember, obj, memberName)) t = __traits(getMember,
250 				obj, memberName);
251 			}))
252 		{
253 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
254 			{
255 				string name = memberName;
256 				Bson value;
257 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
258 				{
259 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
260 					{
261 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
262 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
263 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
264 					}
265 
266 					static if (hasUDA!((__traits(getMember, obj, memberName)), encode))
267 					{
268 						static assert(getUDAs!((__traits(getMember, obj, memberName)), encode).length == 1,
269 								"Member '" ~ memberName ~ "' can only have one encoder!");
270 						mixin("value = obj." ~ getUDAs!((__traits(getMember,
271 								obj, memberName)), encode)[0].func ~ "(obj);");
272 					}
273 					else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType))
274 					{
275 						static assert(isArray!(typeof((__traits(getMember, obj,
276 								memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1,
277 								"Binary member '" ~ memberName
278 								~ "' can only be an array of 1 byte values");
279 						static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1,
280 								"Binary member '" ~ memberName ~ "' can only have one type!");
281 						BsonBinData.Type type = getUDAs!((__traits(getMember,
282 								obj, memberName)), binaryType)[0].type;
283 						value = Bson(BsonBinData(type,
284 								cast(immutable(ubyte)[])(__traits(getMember, obj, memberName))));
285 					}
286 					else
287 					{
288 						static if (__traits(compiles, {
289 								__traits(hasMember, typeof((__traits(getMember,
290 								obj, memberName))), "toBson");
291 							}) && __traits(hasMember, typeof((__traits(getMember, obj,
292 								memberName))), "toBson") && !is(ReturnType!(typeof((__traits(getMember,
293 								obj, memberName)).toBson)) == Bson))
294 							pragma(msg, "Warning: ", typeof((__traits(getMember, obj, memberName))).stringof,
295 									".toBson does not return a vibe.data.bson.Bson struct!");
296 
297 						value = memberToBson(__traits(getMember, obj, memberName));
298 					}
299 					data[name] = value;
300 				}
301 			}
302 		}
303 	}
304 
305 	return data;
306 }
307 
308 /// Generates a struct/class object from a Bson node
309 T fromSchemaBson(T)(Bson bson)
310 {
311 	static if (__traits(compiles, cast(T) null) && __traits(compiles, {
312 			T foo = null;
313 		}))
314 	{
315 		if (bson.isNull)
316 			return null;
317 	}
318 	T obj = T.init;
319 
320 	static if (hasMember!(T, "_schema_object_id_"))
321 	{
322 		if (!bson.tryIndex("_id").isNull)
323 			obj.bsonID = bson["_id"].get!BsonObjectID;
324 	}
325 
326 	foreach (memberName; __traits(allMembers, T))
327 	{
328 		static if (memberName == "_schema_object_id_")
329 			continue;
330 		else static if (__traits(compiles, {
331 				static s = isVariable!(__traits(getMember, obj, memberName));
332 			}) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, {
333 				static s = __traits(getMember, T, memberName);
334 			}) // No static members
335 			 && __traits(compiles, {
336 				typeof(__traits(getMember, obj, memberName)) t = __traits(getMember,
337 				obj, memberName);
338 			}))
339 		{
340 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
341 			{
342 				string name = memberName;
343 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
344 				{
345 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
346 					{
347 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
348 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
349 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
350 					}
351 
352 					// compile time code will still be generated but not run at runtime
353 					if (bson.tryIndex(name).isNull || bson[name].type == Bson.Type.undefined)
354 						continue;
355 
356 					static if (hasUDA!((__traits(getMember, obj, memberName)), decode))
357 					{
358 						static assert(getUDAs!((__traits(getMember, obj, memberName)), decode).length == 1,
359 								"Member '" ~ memberName ~ "' can only have one decoder!");
360 						mixin("obj." ~ memberName ~ " = obj." ~ getUDAs!((__traits(getMember,
361 								obj, memberName)), decode)[0].func ~ "(bson);");
362 					}
363 					else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType))
364 					{
365 						static assert(isArray!(typeof((__traits(getMember, obj,
366 								memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1,
367 								"Binary member '" ~ memberName
368 								~ "' can only be an array of 1 byte values");
369 						static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1,
370 								"Binary member '" ~ memberName ~ "' can only have one type!");
371 						assert(bson[name].type == Bson.Type.binData);
372 						auto data = bson[name].get!(BsonBinData).rawData;
373 						mixin("obj." ~ memberName ~ " = cast(typeof(obj." ~ memberName ~ ")) data;");
374 					}
375 					else
376 					{
377 						mixin(
378 								"obj." ~ memberName ~ " = bsonToMember(obj."
379 								~ memberName ~ ", bson[name]);");
380 					}
381 				}
382 			}
383 		}
384 	}
385 
386 	return obj;
387 }
388 
389 /// Range for iterating over a collection using a Schema.
390 struct DocumentRange(Schema)
391 {
392 	private MongoCursor!(Bson, Bson, typeof(null)) _cursor;
393 
394 	public this(MongoCursor!(Bson, Bson, typeof(null)) cursor)
395 	{
396 		_cursor = cursor;
397 	}
398 
399 	/**
400 		Returns true if there are no more documents for this cursor.
401 
402 		Throws: An exception if there is a query or communication error.
403 	*/
404 	@property bool empty()
405 	{
406 		return _cursor.empty;
407 	}
408 
409 	/**
410 		Returns the current document of the response.
411 
412 		Use empty and popFront to iterate over the list of documents using an
413 		input range interface. Note that calling this function is only allowed
414 		if empty returns false.
415 	*/
416 	@property Schema front()
417 	{
418 		return fromSchemaBson!Schema(_cursor.front);
419 	}
420 
421 	/**
422 		Controls the order in which the query returns matching documents.
423 
424 		This method must be called before starting to iterate, or an exeption
425 		will be thrown. If multiple calls to $(D sort()) are issued, only
426 		the last one will have an effect.
427 
428 		Params:
429 			order = A BSON object convertible value that defines the sort order
430 				of the result. This BSON object must be structured according to
431 				the MongoDB documentation (see below).
432 
433 		Returns: Reference to the modified original curser instance.
434 
435 		Throws:
436 			An exception if there is a query or communication error.
437 			Also throws if the method was called after beginning of iteration.
438 
439 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort)
440 	*/
441 	auto sort(T)(T order)
442 	{
443 		_cursor.sort(serializeToBson(order));
444 		return this;
445 	}
446 
447 	/**
448 		Limits the number of documents that the cursor returns.
449 
450 		This method must be called before beginnig iteration in order to have
451 		effect. If multiple calls to limit() are made, the one with the lowest
452 		limit will be chosen.
453 
454 		Params:
455 			count = The maximum number number of documents to return. A value
456 				of zero means unlimited.
457 
458 		Returns: the same cursor
459 
460 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit)
461 	*/
462 	auto limit(size_t count)
463 	{
464 		_cursor.limit(count);
465 		return this;
466 	}
467 
468 	/**
469 		Skips a given number of elements at the beginning of the cursor.
470 
471 		This method must be called before beginnig iteration in order to have
472 		effect. If multiple calls to skip() are made, the one with the maximum
473 		number will be chosen.
474 
475 		Params:
476 			count = The number of documents to skip.
477 
478 		Returns: the same cursor
479 
480 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip)
481 	*/
482 	auto skip(int count)
483 	{
484 		_cursor.skip(count);
485 		return this;
486 	}
487 
488 	/**
489 		Advances the cursor to the next document of the response.
490 
491 		Note that calling this function is only allowed if empty returns false.
492 	*/
493 	void popFront()
494 	{
495 		_cursor.popFront();
496 	}
497 
498 	/**
499 		Iterates over all remaining documents.
500 
501 		Note that iteration is one-way - elements that have already been visited
502 		will not be visited again if another iteration is done.
503 
504 		Throws: An exception if there is a query or communication error.
505 	*/
506 	int opApply(int delegate(Schema doc) del)
507 	{
508 		while (!_cursor.empty)
509 		{
510 			auto doc = _cursor.front;
511 			_cursor.popFront();
512 			if (auto ret = del(fromSchemaBson!Schema(doc)))
513 				return ret;
514 		}
515 		return 0;
516 	}
517 }
518 
519 /// Exception thrown if a document could not be found.
520 class DocumentNotFoundException : Exception
521 {
522 	///
523 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe
524 	{
525 		super(msg, file, line);
526 	}
527 }
528 
529 ///
530 struct PipelineUnwindOperation
531 {
532 	/// Field path to an array field. To specify a field path, prefix the field name with a dollar sign $.
533 	string path;
534 	/// Optional. The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $.
535 	string includeArrayIndex = null;
536 }
537 
538 ///
539 struct SchemaPipeline
540 {
541 @safe:
542 	this(MongoCollection collection)
543 	{
544 		_collection = collection;
545 	}
546 
547 	/// Passes along the documents with only the specified fields to the next stage in the pipeline. The specified fields can be existing fields from the input documents or newly computed fields.
548 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
549 	auto project(Bson specifications)
550 	{
551 		assert(!finalized);
552 		pipeline ~= Bson(["$project" : specifications]);
553 		return this;
554 	}
555 
556 	/// Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage.
557 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match
558 	auto match(Bson query)
559 	{
560 		assert(!finalized);
561 		pipeline ~= Bson(["$match" : query]);
562 		return this;
563 	}
564 
565 	/// Restricts the contents of the documents based on information stored in the documents themselves.
566 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact
567 	auto redact(Bson expression)
568 	{
569 		assert(!finalized);
570 		pipeline ~= Bson(["$redact" : expression]);
571 		return this;
572 	}
573 
574 	/// Limits the number of documents passed to the next stage in the pipeline.
575 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit
576 	auto limit(size_t count)
577 	{
578 		assert(!finalized);
579 		pipeline ~= Bson(["$limit" : Bson(count)]);
580 		return this;
581 	}
582 
583 	/// Skips over the specified number of documents that pass into the stage and passes the remaining documents to the next stage in the pipeline.
584 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip
585 	auto skip(size_t count)
586 	{
587 		assert(!finalized);
588 		pipeline ~= Bson(["$skip" : Bson(count)]);
589 		return this;
590 	}
591 
592 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
593 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
594 	auto unwind(string path)
595 	{
596 		assert(!finalized);
597 		pipeline ~= Bson(["$unwind" : Bson(path)]);
598 		return this;
599 	}
600 
601 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
602 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
603 	auto unwind(PipelineUnwindOperation op)
604 	{
605 		assert(!finalized);
606 		Bson opb = Bson(["path" : Bson(op.path)]);
607 		if (op.includeArrayIndex !is null)
608 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
609 		pipeline ~= Bson(["$unwind" : opb]);
610 		return this;
611 	}
612 
613 	/// Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.
614 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
615 	auto unwind(PipelineUnwindOperation op, bool preserveNullAndEmptyArrays)
616 	{
617 		assert(!finalized);
618 		Bson opb = Bson(["path" : Bson(op.path), "preserveNullAndEmptyArrays"
619 				: Bson(preserveNullAndEmptyArrays)]);
620 		if (op.includeArrayIndex !is null)
621 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
622 		pipeline ~= Bson(["$unwind" : opb]);
623 		return this;
624 	}
625 
626 	/// Groups documents by some specified expression and outputs to the next stage a document for each distinct grouping. The output documents contain an _id field which contains the distinct group by key. The output documents can also contain computed fields that hold the values of some accumulator expression grouped by the $group‘s _id field. $group does not order its output documents.
627 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
628 	auto group(Bson id, Bson[string] accumulators)
629 	{
630 		assert(!finalized);
631 		accumulators["_id"] = id;
632 		pipeline ~= Bson(["$group" : Bson(accumulators)]);
633 		return this;
634 	}
635 
636 	/// Groups all documents into one specified with the accumulators. Basically just runs group(null, accumulators)
637 	auto groupAll(Bson[string] accumulators)
638 	{
639 		assert(!finalized);
640 		accumulators["_id"] = Bson(null);
641 		pipeline ~= Bson(["$group" : Bson(accumulators)]);
642 		return this;
643 	}
644 
645 	/// Randomly selects the specified number of documents from its input.
646 	/// Warning: $sample may output the same document more than once in its result set.
647 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sample/#pipe._S_sample
648 	auto sample(size_t count)
649 	{
650 		assert(!finalized);
651 		pipeline ~= Bson(["$sample" : Bson(count)]);
652 		return this;
653 	}
654 
655 	/// Sorts all input documents and returns them to the pipeline in sorted order.
656 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort
657 	auto sort(Bson sorter)
658 	{
659 		assert(!finalized);
660 		pipeline ~= Bson(["$sort" : sorter]);
661 		return this;
662 	}
663 
664 	/// Outputs documents in order of nearest to farthest from a specified point.
665 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear
666 	auto geoNear(Bson options)
667 	{
668 		assert(!finalized);
669 		pipeline ~= Bson(["$geoNear" : options]);
670 		return this;
671 	}
672 
673 	/// Performs a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing. The $lookup stage does an equality match between a field from the input documents with a field from the documents of the “joined” collection.
674 	/// To each input document, the $lookup stage adds a new array field whose elements are the matching documents from the “joined” collection. The $lookup stage passes these reshaped documents to the next stage.
675 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup
676 	auto lookup(Bson options)
677 	{
678 		assert(!finalized);
679 		pipeline ~= Bson(["$lookup" : options]);
680 		return this;
681 	}
682 
683 	/// Takes the documents returned by the aggregation pipeline and writes them to a specified collection. The $out operator must be the last stage in the pipeline. The $out operator lets the aggregation framework return result sets of any size.
684 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out
685 	auto outputTo(string outputCollection)
686 	{
687 		assert(!finalized);
688 		debug finalized = true;
689 		pipeline ~= Bson(["$out" : Bson(outputCollection)]);
690 		return this;
691 	}
692 
693 	/// Returns statistics regarding the use of each index for the collection. If running with access control, the user must have privileges that include indexStats action.
694 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/#pipe._S_indexStats
695 	auto indexStats()
696 	{
697 		assert(!finalized);
698 		pipeline ~= Bson(["$indexStats" : Bson.emptyObject]);
699 		return this;
700 	}
701 
702 	Bson run()
703 	{
704 		debug finalized = true;
705 		return _collection.aggregate(pipeline);
706 	}
707 
708 private:
709 	bool finalized = false;
710 	Bson[] pipeline;
711 	MongoCollection _collection;
712 }
713 
714 /// Mixin for functions for interacting with Mongo collections.
715 mixin template MongoSchema()
716 {
717 	import std.typecons : Nullable;
718 
719 	static MongoCollection _schema_collection_;
720 	private BsonObjectID _schema_object_id_;
721 
722 	/// Returns: the _id value (if set by save or find)
723 	@property ref BsonObjectID bsonID() @safe
724 	{
725 		return _schema_object_id_;
726 	}
727 
728 	/// Inserts or updates an existing value.
729 	void save()
730 	{
731 		if (_schema_object_id_.valid)
732 		{
733 			_schema_collection_.update(Bson(["_id" : Bson(_schema_object_id_)]),
734 					this.toSchemaBson(), UpdateFlags.upsert);
735 		}
736 		else
737 		{
738 			_schema_object_id_ = BsonObjectID.generate;
739 			auto bson = this.toSchemaBson();
740 			_schema_collection_.insert(bson);
741 		}
742 	}
743 
744 	/// Removes this object from the collection. Returns false when _id of this is not set.
745 	bool remove() @safe const
746 	{
747 		if (!_schema_object_id_.valid)
748 			return false;
749 		_schema_collection_.remove(Bson(["_id" : Bson(_schema_object_id_)]),
750 				DeleteFlags.SingleRemove);
751 		return true;
752 	}
753 
754 	/// Tries to find one document in the collection.
755 	/// Throws: DocumentNotFoundException if not found
756 	static auto findOneOrThrow(T)(T query)
757 	{
758 		Bson found = _schema_collection_.findOne(query);
759 		if (found.isNull)
760 			throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof);
761 		return found;
762 	}
763 
764 	/// Finds one element with the object id `id`.
765 	/// Throws: DocumentNotFoundException if not found
766 	static typeof(this) findById(BsonObjectID id)
767 	{
768 		return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id" : Bson(id)])));
769 	}
770 
771 	/// Finds one element with the hex id `id`.
772 	/// Throws: DocumentNotFoundException if not found
773 	static typeof(this) findById(string id)
774 	{
775 		return findById(BsonObjectID.fromString(id));
776 	}
777 
778 	/// Finds one element using a query.
779 	/// Throws: DocumentNotFoundException if not found
780 	static typeof(this) findOne(T)(T query)
781 	{
782 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
783 	}
784 
785 	/// Tries to find a document by the _id field and returns a Nullable which `isNull` if it could not be found. Otherwise it will be the document wrapped in the nullable.
786 	static Nullable!(typeof(this)) tryFindById(BsonObjectID id)
787 	{
788 		Bson found = _schema_collection_.findOne(Bson(["_id" : Bson(id)]));
789 		if (found.isNull)
790 			return Nullable!(typeof(this)).init;
791 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
792 	}
793 
794 	/// ditto
795 	static Nullable!(typeof(this)) tryFindById(string id)
796 	{
797 		return tryFindById(BsonObjectID.fromString(id));
798 	}
799 
800 	/// Tries to find a document in this collection. It will return a Nullable which `isNull` if the document could not be found. Otherwise it will be the document wrapped in the nullable.
801 	static Nullable!(typeof(this)) tryFindOne(T)(T query)
802 	{
803 		Bson found = _schema_collection_.findOne(query);
804 		if (found.isNull)
805 			return Nullable!(typeof(this)).init;
806 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
807 	}
808 
809 	/// Finds one or more elements using a query.
810 	static typeof(this)[] find(T)(T query, QueryFlags flags = QueryFlags.None,
811 			int num_skip = 0, int num_docs_per_chunk = 0)
812 	{
813 		typeof(this)[] values;
814 		foreach (entry; _schema_collection_.find(query, null, flags, num_skip, num_docs_per_chunk))
815 		{
816 			values ~= fromSchemaBson!(typeof(this))(entry);
817 		}
818 		return values;
819 	}
820 
821 	/// Queries all elements from the collection.
822 	deprecated("use findAll instead") static typeof(this)[] find()
823 	{
824 		typeof(this)[] values;
825 		foreach (entry; _schema_collection_.find())
826 		{
827 			values ~= fromSchemaBson!(typeof(this))(entry);
828 		}
829 		return values;
830 	}
831 
832 	/// Finds one or more elements using a query as range.
833 	static DocumentRange!(typeof(this)) findRange(T)(T query,
834 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
835 	{
836 		return DocumentRange!(typeof(this))(_schema_collection_.find(serializeToBson(query),
837 				null, flags, num_skip, num_docs_per_chunk));
838 	}
839 
840 	/// Queries all elements from the collection as range.
841 	static DocumentRange!(typeof(this)) findAll()
842 	{
843 		return DocumentRange!(typeof(this))(_schema_collection_.find());
844 	}
845 
846 	/// Updates a document.
847 	static void update(T, U)(T query, U update, UpdateFlags options = UpdateFlags.none)
848 	{
849 		_schema_collection_.update(query, update, options);
850 	}
851 
852 	/// Updates a document or inserts it when not existent. Shorthand for `update(..., UpdateFlags.upsert)`
853 	static void upsert(T, U)(T query, U update, UpdateFlags options = UpdateFlags.upsert)
854 	{
855 		_schema_collection_.update(query, update, options);
856 	}
857 
858 	/// Deletes one or any amount of documents matching the selector based on the flags.
859 	static void remove(T)(T selector, DeleteFlags flags = DeleteFlags.none)
860 	{
861 		_schema_collection_.remove(selector, flags);
862 	}
863 
864 	/// Removes all documents from this collection.
865 	static void removeAll()
866 	{
867 		_schema_collection_.remove();
868 	}
869 
870 	/// Drops the entire collection and all indices in the database.
871 	static void dropTable()
872 	{
873 		_schema_collection_.drop();
874 	}
875 
876 	/// Returns the count of documents in this collection matching this query.
877 	static auto count(T)(T query)
878 	{
879 		return _schema_collection_.count(query);
880 	}
881 
882 	/// Returns the count of documents in this collection.
883 	static auto countAll()
884 	{
885 		import vibe.data.bson : Bson;
886 
887 		return _schema_collection_.count(Bson.emptyObject);
888 	}
889 
890 	/// Start of an aggregation call. Returns a pipeline with typesafe functions for modifying the pipeline and running it at the end.
891 	/// Examples:
892 	/// --------------------
893 	/// auto groupResults = Book.aggregate.groupAll([
894 	/// 	"totalPrice": Bson([
895 	/// 		"$sum": Bson([
896 	/// 			"$multiply": Bson([Bson("$price"), Bson("$quantity")])
897 	/// 		])
898 	/// 	]),
899 	/// 	"averageQuantity": Bson([
900 	/// 		"$avg": Bson("$quantity")
901 	/// 	]),
902 	/// 	"count": Bson(["$sum": Bson(1)])
903 	/// ]).run;
904 	/// --------------------
905 	static SchemaPipeline aggregate()
906 	{
907 		return SchemaPipeline(_schema_collection_);
908 	}
909 }
910 
911 /// Binds a MongoCollection to a Schema. Can only be done once!
912 void register(T)(MongoCollection collection) @safe
913 {
914 	T obj = T.init;
915 
916 	static if (hasMember!(T, "_schema_collection_"))
917 	{
918 		assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!");
919 		T._schema_collection_ = collection;
920 	}
921 
922 	foreach (memberName; __traits(allMembers, T))
923 	{
924 		static if (__traits(compiles, {
925 				static s = isVariable!(__traits(getMember, obj, memberName));
926 			}) && isVariable!(__traits(getMember, obj, memberName)))
927 		{
928 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
929 			{
930 				string name = memberName;
931 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
932 				{
933 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
934 					{
935 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
936 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
937 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
938 					}
939 
940 					IndexFlags flags = IndexFlags.None;
941 					ulong expires = 0LU;
942 					bool force;
943 
944 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoForceIndex))
945 					{
946 						force = true;
947 					}
948 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoBackground))
949 					{
950 						flags |= IndexFlags.Background;
951 					}
952 					static if (hasUDA!((__traits(getMember, obj, memberName)),
953 							mongoDropDuplicates))
954 					{
955 						flags |= IndexFlags.DropDuplicates;
956 					}
957 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoSparse))
958 					{
959 						flags |= IndexFlags.Sparse;
960 					}
961 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoUnique))
962 					{
963 						flags |= IndexFlags.Unique;
964 					}
965 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoExpire))
966 					{
967 						static assert(getUDAs!((__traits(getMember, obj, memberName)), mongoExpire).length == 1,
968 								"Member '" ~ memberName ~ "' can only have one expiry value!");
969 						flags |= IndexFlags.ExpireAfterSeconds;
970 						expires = getUDAs!((__traits(getMember, obj, memberName)), mongoExpire)[0]
971 							.seconds;
972 					}
973 
974 					if (flags != IndexFlags.None || force)
975 						collection.ensureIndex([tuple(name, 1)], flags, dur!"seconds"(expires));
976 				}
977 			}
978 		}
979 	}
980 }
981 
982 /// Class serializing to a bson date containing a special `now` value that gets translated to the current time when converting to bson.
983 final struct SchemaDate
984 {
985 public @safe:
986 	///
987 	this(BsonDate date)
988 	{
989 		_time = date.value;
990 	}
991 
992 	///
993 	this(long time)
994 	{
995 		_time = time;
996 	}
997 
998 	///
999 	@property auto time() const
1000 	{
1001 		return _time;
1002 	}
1003 
1004 	///
1005 	static Bson toBson(SchemaDate date)
1006 	{
1007 		if (date._time == -1)
1008 		{
1009 			return Bson(BsonDate.fromStdTime(Clock.currStdTime()));
1010 		}
1011 		else
1012 		{
1013 			return Bson(BsonDate(date._time));
1014 		}
1015 	}
1016 
1017 	///
1018 	static SchemaDate fromBson(Bson bson)
1019 	{
1020 		return SchemaDate(bson.get!BsonDate.value);
1021 	}
1022 
1023 	///
1024 	static SchemaDate fromSysTime(SysTime stime)
1025 	{
1026 		return SchemaDate(BsonDate(stime).value);
1027 	}
1028 
1029 	/// Magic value setting the date to the current time stamp when serializing.
1030 	static SchemaDate now()
1031 	{
1032 		return SchemaDate(-1);
1033 	}
1034 
1035 	/// Converts this SchemaDate to a std.datetime.SysTime object.
1036 	SysTime toSysTime() const
1037 	{
1038 		if (_time == -1)
1039 			return Clock.currTime;
1040 		return BsonDate(_time).toSysTime();
1041 	}
1042 
1043 	/// Converts this SchemaDate to a vibed BsonDate object.
1044 	BsonDate toBsonDate() const
1045 	{
1046 		return BsonDate(_time);
1047 	}
1048 
1049 	///
1050 	string toISOExtString() const
1051 	{
1052 		return toSysTime.toISOExtString;
1053 	}
1054 
1055 	static SchemaDate fromISOExtString(S)(in S s) if (isSomeString!S)
1056 	{
1057 		return SchemaDate.fromSysTime(SysTime.fromISOExtString());
1058 	}
1059 
1060 private:
1061 	long _time;
1062 }
1063 
1064 unittest
1065 {
1066 	struct C
1067 	{
1068 		int a = 4;
1069 	}
1070 
1071 	struct B
1072 	{
1073 		C cref;
1074 	}
1075 
1076 	struct A
1077 	{
1078 		B bref;
1079 	}
1080 
1081 	A a;
1082 	a.bref.cref.a = 5;
1083 	auto bson = a.toSchemaBson();
1084 	assert(bson["bref"]["cref"]["a"].get!int == 5);
1085 	A b = bson.fromSchemaBson!A();
1086 	assert(b.bref.cref.a == 5);
1087 }
1088 
1089 unittest
1090 {
1091 	import std.digest.digest;
1092 	import std.digest.sha;
1093 
1094 	enum Activity
1095 	{
1096 		High,
1097 		Medium,
1098 		Low
1099 	}
1100 
1101 	enum Permission
1102 	{
1103 		A = 1,
1104 		B = 2,
1105 		C = 4
1106 	}
1107 
1108 	struct UserSchema
1109 	{
1110 		string username = "Unnamed";
1111 		@binaryType()
1112 		string salt = "foobar";
1113 		@encode("encodePassword")
1114 		@binaryType()
1115 		string password;
1116 		@schemaName("date-created")
1117 		SchemaDate dateCreated = SchemaDate.now;
1118 		Activity activity = Activity.Medium;
1119 		BitFlags!Permission permissions;
1120 		Tuple!(string, string) name;
1121 
1122 		Bson encodePassword(UserSchema user)
1123 		{
1124 			// TODO: Replace with something more secure
1125 			return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt)));
1126 		}
1127 	}
1128 
1129 	auto user = UserSchema();
1130 	user.password = "12345";
1131 	user.username = "Bob";
1132 	user.permissions = Permission.A | Permission.C;
1133 	user.name = tuple("Bob", "Bobby");
1134 	auto bson = user.toSchemaBson();
1135 	assert(bson["username"].get!string == "Bob");
1136 	assert(bson["date-created"].get!(BsonDate).value > 0);
1137 	assert(bson["activity"].get!(int) == cast(int) Activity.Medium);
1138 	assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar");
1139 	assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt));
1140 	assert(bson["permissions"].get!(int) == 5);
1141 	assert(bson["name"].get!(Bson[]).length == 2);
1142 
1143 	auto user2 = bson.fromSchemaBson!UserSchema();
1144 	assert(user2.username == user.username);
1145 	assert(user2.password != user.password);
1146 	assert(user2.salt == user.salt);
1147 	// dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time
1148 	assert(user2.dateCreated != user.dateCreated);
1149 	assert(user2.activity == user.activity);
1150 	assert(user2.permissions == user.permissions);
1151 	assert(user2.name == user.name);
1152 }
1153 
1154 unittest
1155 {
1156 	import vibe.db.mongo.mongo;
1157 	import std.digest.sha;
1158 	import std.exception;
1159 	import std.array;
1160 
1161 	auto client = connectMongoDB("localhost");
1162 	auto database = client.getDatabase("test");
1163 	MongoCollection users = database["users"];
1164 	users.remove(); // Clears collection
1165 
1166 	struct User
1167 	{
1168 		mixin MongoSchema;
1169 
1170 		@mongoUnique string username;
1171 		@binaryType()
1172 		ubyte[] hash;
1173 		@schemaName("profile-picture")
1174 		string profilePicture;
1175 		auto registered = SchemaDate.now;
1176 	}
1177 
1178 	users.register!User;
1179 
1180 	assert(User.findAll().array.length == 0);
1181 
1182 	User user;
1183 	user.username = "Example";
1184 	user.hash = sha512Of("password123");
1185 	user.profilePicture = "example-avatar.png";
1186 
1187 	assertNotThrown(user.save());
1188 
1189 	User user2;
1190 	user2.username = "Bob";
1191 	user2.hash = sha512Of("foobar");
1192 	user2.profilePicture = "bob-avatar.png";
1193 
1194 	assertNotThrown(user2.save());
1195 
1196 	User faker;
1197 	faker.username = "Example";
1198 	faker.hash = sha512Of("PASSWORD");
1199 	faker.profilePicture = "example-avatar.png";
1200 
1201 	assertThrown(faker.save());
1202 	// Unique username
1203 
1204 	faker.username = "Example_";
1205 	assertNotThrown(faker.save());
1206 
1207 	user.username = "NewExample";
1208 	user.save();
1209 
1210 	auto actualFakeID = faker.bsonID;
1211 	faker = User.findOne(["username" : "NewExample"]);
1212 
1213 	assert(actualFakeID != faker.bsonID);
1214 
1215 	foreach (usr; User.findAll)
1216 	{
1217 		usr.profilePicture = "default.png"; // Reset all profile pictures
1218 		usr.save();
1219 	}
1220 	user = User.findOne(["username" : "NewExample"]);
1221 	user2 = User.findOne(["username" : "Bob"]);
1222 	faker = User.findOne(["username" : "Example_"]);
1223 	assert(user.profilePicture == user2.profilePicture
1224 			&& user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png");
1225 
1226 	User user3;
1227 	user3.username = "User123";
1228 	user3.hash = sha512Of("486951");
1229 	user3.profilePicture = "new.png";
1230 	User.upsert(["username" : "User123"], user3.toSchemaBson);
1231 	user3 = User.findOne(["username" : "User123"]);
1232 	assert(user3.hash == sha512Of("486951"));
1233 	assert(user3.profilePicture == "new.png");
1234 }
1235 
1236 unittest
1237 {
1238 	import vibe.db.mongo.mongo;
1239 	import mongoschema.aliases : name, ignore, unique, binary;
1240 	import std.digest.sha;
1241 	import std.digest.md;
1242 
1243 	auto client = connectMongoDB("localhost");
1244 
1245 	struct Permission
1246 	{
1247 		string name;
1248 		int priority;
1249 	}
1250 
1251 	struct User
1252 	{
1253 		mixin MongoSchema;
1254 
1255 		@unique string username;
1256 
1257 		@binary()
1258 		ubyte[] hash;
1259 		@binary()
1260 		ubyte[] salt;
1261 
1262 		@name("profile-picture")
1263 		string profilePicture = "default.png";
1264 
1265 		Permission[] permissions;
1266 
1267 	@ignore:
1268 		int sessionID;
1269 	}
1270 
1271 	auto coll = client.getCollection("test.users2");
1272 	coll.remove();
1273 	coll.register!User;
1274 
1275 	User register(string name, string password)
1276 	{
1277 		User user;
1278 		user.username = name;
1279 		user.salt = md5Of(name).dup;
1280 		user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup;
1281 		user.permissions ~= Permission("forum.access", 1);
1282 		user.save();
1283 		return user;
1284 	}
1285 
1286 	User find(string name)
1287 	{
1288 		return User.findOne(["username" : name]);
1289 	}
1290 
1291 	User a = register("foo", "bar");
1292 	User b = find("foo");
1293 	assert(a == b);
1294 }