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