1 /// This module provides the database utility tools which make the whole project useful.
2 module mongoschema.db;
3 
4 import core.time;
5 
6 import mongoschema;
7 
8 import std.traits;
9 import std.typecons : BitFlags, tuple, Tuple;
10 
11 /// Range for iterating over a collection using a Schema.
12 struct DocumentRange(Schema)
13 {
14 	private MongoCursor!(Bson, Bson, typeof(null)) _cursor;
15 
16 	public this(MongoCursor!(Bson, Bson, typeof(null)) cursor)
17 	{
18 		_cursor = cursor;
19 	}
20 
21 	/**
22 		Returns true if there are no more documents for this cursor.
23 
24 		Throws: An exception if there is a query or communication error.
25 	*/
26 	@property bool empty()
27 	{
28 		return _cursor.empty;
29 	}
30 
31 	/**
32 		Returns the current document of the response.
33 
34 		Use empty and popFront to iterate over the list of documents using an
35 		input range interface. Note that calling this function is only allowed
36 		if empty returns false.
37 	*/
38 	@property Schema front()
39 	{
40 		return fromSchemaBson!Schema(_cursor.front);
41 	}
42 
43 	/**
44 		Controls the order in which the query returns matching documents.
45 
46 		This method must be called before starting to iterate, or an exeption
47 		will be thrown. If multiple calls to $(D sort()) are issued, only
48 		the last one will have an effect.
49 
50 		Params:
51 			order = A BSON object convertible value that defines the sort order
52 				of the result. This BSON object must be structured according to
53 				the MongoDB documentation (see below).
54 
55 		Returns: Reference to the modified original curser instance.
56 
57 		Throws:
58 			An exception if there is a query or communication error.
59 			Also throws if the method was called after beginning of iteration.
60 
61 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort)
62 	*/
63 	auto sort(T)(T order)
64 	{
65 		_cursor.sort(serializeToBson(order));
66 		return this;
67 	}
68 
69 	/**
70 		Limits the number of documents that the cursor returns.
71 
72 		This method must be called before beginnig iteration in order to have
73 		effect. If multiple calls to limit() are made, the one with the lowest
74 		limit will be chosen.
75 
76 		Params:
77 			count = The maximum number number of documents to return. A value
78 				of zero means unlimited.
79 
80 		Returns: the same cursor
81 
82 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit)
83 	*/
84 	auto limit(size_t count)
85 	{
86 		_cursor.limit(count);
87 		return this;
88 	}
89 
90 	/**
91 		Skips a given number of elements at the beginning of the cursor.
92 
93 		This method must be called before beginnig iteration in order to have
94 		effect. If multiple calls to skip() are made, the one with the maximum
95 		number will be chosen.
96 
97 		Params:
98 			count = The number of documents to skip.
99 
100 		Returns: the same cursor
101 
102 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip)
103 	*/
104 	auto skip(int count)
105 	{
106 		_cursor.skip(count);
107 		return this;
108 	}
109 
110 	/**
111 		Advances the cursor to the next document of the response.
112 
113 		Note that calling this function is only allowed if empty returns false.
114 	*/
115 	void popFront()
116 	{
117 		_cursor.popFront();
118 	}
119 
120 	/**
121 		Iterates over all remaining documents.
122 
123 		Note that iteration is one-way - elements that have already been visited
124 		will not be visited again if another iteration is done.
125 
126 		Throws: An exception if there is a query or communication error.
127 	*/
128 	int opApply(int delegate(Schema doc) del)
129 	{
130 		while (!_cursor.empty)
131 		{
132 			auto doc = _cursor.front;
133 			_cursor.popFront();
134 			if (auto ret = del(fromSchemaBson!Schema(doc)))
135 				return ret;
136 		}
137 		return 0;
138 	}
139 }
140 
141 /// Exception thrown if a document could not be found.
142 class DocumentNotFoundException : Exception
143 {
144 	///
145 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe
146 	{
147 		super(msg, file, line);
148 	}
149 }
150 
151 ///
152 struct PipelineUnwindOperation
153 {
154 	/// Field path to an array field. To specify a field path, prefix the field name with a dollar sign $.
155 	string path;
156 	/// Optional. The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $.
157 	string includeArrayIndex = null;
158 }
159 
160 ///
161 struct SchemaPipeline
162 {
163 @safe:
164 	this(MongoCollection collection)
165 	{
166 		_collection = collection;
167 	}
168 
169 	/// 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.
170 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
171 	SchemaPipeline project(Bson specifications)
172 	{
173 		assert(!finalized);
174 		pipeline ~= Bson(["$project" : specifications]);
175 		return this;
176 	}
177 
178 	/// ditto
179 	SchemaPipeline project(T)(T specifications)
180 	{
181 		return project(serializeToBson(specifications));
182 	}
183 
184 	/// Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage.
185 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match
186 	SchemaPipeline match(Bson query)
187 	{
188 		assert(!finalized);
189 		pipeline ~= Bson(["$match" : query]);
190 		return this;
191 	}
192 
193 	/// ditto
194 	SchemaPipeline match(T)(Query!T query)
195 	{
196 		return match(query._query);
197 	}
198 
199 	/// ditto
200 	SchemaPipeline match(T)(T[string] query)
201 	{
202 		return match(serializeToBson(query));
203 	}
204 
205 	/// Restricts the contents of the documents based on information stored in the documents themselves.
206 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/redact/#pipe._S_redact
207 	SchemaPipeline redact(Bson expression)
208 	{
209 		assert(!finalized);
210 		pipeline ~= Bson(["$redact" : expression]);
211 		return this;
212 	}
213 
214 	/// ditto
215 	SchemaPipeline redact(T)(T expression)
216 	{
217 		return redact(serializeToBson(expression));
218 	}
219 
220 	/// Limits the number of documents passed to the next stage in the pipeline.
221 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit
222 	SchemaPipeline limit(size_t count)
223 	{
224 		assert(!finalized);
225 		pipeline ~= Bson(["$limit" : Bson(count)]);
226 		return this;
227 	}
228 
229 	/// Skips over the specified number of documents that pass into the stage and passes the remaining documents to the next stage in the pipeline.
230 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip
231 	SchemaPipeline skip(size_t count)
232 	{
233 		assert(!finalized);
234 		pipeline ~= Bson(["$skip" : Bson(count)]);
235 		return this;
236 	}
237 
238 	/// 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.
239 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
240 	SchemaPipeline unwind(string path)
241 	{
242 		assert(!finalized);
243 		pipeline ~= Bson(["$unwind" : Bson(path)]);
244 		return this;
245 	}
246 
247 	/// 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.
248 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
249 	SchemaPipeline unwind(PipelineUnwindOperation op)
250 	{
251 		assert(!finalized);
252 		Bson opb = Bson(["path" : Bson(op.path)]);
253 		if (op.includeArrayIndex !is null)
254 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
255 		pipeline ~= Bson(["$unwind" : opb]);
256 		return this;
257 	}
258 
259 	/// 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.
260 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
261 	SchemaPipeline unwind(PipelineUnwindOperation op, bool preserveNullAndEmptyArrays)
262 	{
263 		assert(!finalized);
264 		Bson opb = Bson(["path" : Bson(op.path), "preserveNullAndEmptyArrays"
265 				: Bson(preserveNullAndEmptyArrays)]);
266 		if (op.includeArrayIndex !is null)
267 			opb["includeArrayIndex"] = Bson(op.includeArrayIndex);
268 		pipeline ~= Bson(["$unwind" : opb]);
269 		return this;
270 	}
271 
272 	/// 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.
273 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
274 	SchemaPipeline group(Bson id, Bson accumulators)
275 	{
276 		assert(!finalized);
277 		accumulators["_id"] = id;
278 		pipeline ~= Bson(["$group" : accumulators]);
279 		return this;
280 	}
281 
282 	/// ditto
283 	SchemaPipeline group(K, T)(K id, T[string] accumulators)
284 	{
285 		return group(serializeToBson(id), serializeToBson(accumulators));
286 	}
287 
288 	/// Groups all documents into one specified with the accumulators. Basically just runs group(null, accumulators)
289 	SchemaPipeline groupAll(Bson accumulators)
290 	{
291 		assert(!finalized);
292 		accumulators["_id"] = Bson(null);
293 		pipeline ~= Bson(["$group" : accumulators]);
294 		return this;
295 	}
296 
297 	/// ditto
298 	SchemaPipeline groupAll(T)(T[string] accumulators)
299 	{
300 		return groupAll(serializeToBson(accumulators));
301 	}
302 
303 	/// Randomly selects the specified number of documents from its input.
304 	/// Warning: $sample may output the same document more than once in its result set.
305 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sample/#pipe._S_sample
306 	SchemaPipeline sample(size_t count)
307 	{
308 		assert(!finalized);
309 		pipeline ~= Bson(["$sample" : Bson(count)]);
310 		return this;
311 	}
312 
313 	/// Sorts all input documents and returns them to the pipeline in sorted order.
314 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort
315 	SchemaPipeline sort(Bson sorter)
316 	{
317 		assert(!finalized);
318 		pipeline ~= Bson(["$sort" : sorter]);
319 		return this;
320 	}
321 
322 	/// ditto
323 	SchemaPipeline sort(T)(T sorter)
324 	{
325 		return sort(serializeToBson(sorter));
326 	}
327 
328 	/// Outputs documents in order of nearest to farthest from a specified point.
329 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear
330 	SchemaPipeline geoNear(Bson options)
331 	{
332 		assert(!finalized);
333 		pipeline ~= Bson(["$geoNear" : options]);
334 		return this;
335 	}
336 
337 	/// ditto
338 	SchemaPipeline geoNear(T)(T options)
339 	{
340 		return geoNear(serializeToBson(options));
341 	}
342 
343 	/// 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.
344 	/// 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.
345 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup
346 	SchemaPipeline lookup(Bson options)
347 	{
348 		assert(!finalized);
349 		pipeline ~= Bson(["$lookup" : options]);
350 		return this;
351 	}
352 
353 	/// ditto
354 	SchemaPipeline lookup(T)(T options)
355 	{
356 		return lookup(serializeToBson(options));
357 	}
358 
359 	/// 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.
360 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out
361 	SchemaPipeline outputTo(string outputCollection)
362 	{
363 		assert(!finalized);
364 		debug finalized = true;
365 		pipeline ~= Bson(["$out" : Bson(outputCollection)]);
366 		return this;
367 	}
368 
369 	/// 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.
370 	/// MongoDB Documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/indexStats/#pipe._S_indexStats
371 	SchemaPipeline indexStats()
372 	{
373 		assert(!finalized);
374 		pipeline ~= Bson(["$indexStats" : Bson.emptyObject]);
375 		return this;
376 	}
377 
378 	Bson run()
379 	{
380 		debug finalized = true;
381 		return _collection.aggregate(pipeline);
382 	}
383 
384 	DocumentRange!T collect(T = Bson)(AggregateOptions options = AggregateOptions.init)
385 	{
386 		debug finalized = true;
387 		return _collection.aggregate!T(pipeline, options);
388 	}
389 
390 private:
391 	bool finalized = false;
392 	Bson[] pipeline;
393 	MongoCollection _collection;
394 }
395 
396 /// Mixin for functions for interacting with Mongo collections.
397 mixin template MongoSchema()
398 {
399 	import std.typecons : Nullable;
400 	import std.range : isInputRange, ElementType;
401 
402 	static MongoCollection _schema_collection_;
403 	private BsonObjectID _schema_object_id_;
404 
405 	@property static MongoCollection collection() @safe
406 	{
407 		return _schema_collection_;
408 	}
409 
410 	/// Returns: the _id value (if set by save or find)
411 	@property ref BsonObjectID bsonID() @safe
412 	{
413 		return _schema_object_id_;
414 	}
415 
416 	/// Inserts or updates an existing value.
417 	void save()
418 	{
419 		if (_schema_object_id_.valid)
420 		{
421 			collection.update(Bson(["_id" : Bson(_schema_object_id_)]),
422 					this.toSchemaBson(), UpdateFlags.upsert);
423 		}
424 		else
425 		{
426 			_schema_object_id_ = BsonObjectID.generate;
427 			auto bson = this.toSchemaBson();
428 			collection.insert(bson);
429 		}
430 	}
431 
432 	/// Inserts or merges into an existing value.
433 	void merge()
434 	{
435 		if (_schema_object_id_.valid)
436 		{
437 			collection.update(Bson(["_id" : Bson(_schema_object_id_)]),
438 					Bson(["$set": this.toSchemaBson()]), UpdateFlags.upsert);
439 		}
440 		else
441 		{
442 			_schema_object_id_ = BsonObjectID.generate;
443 			auto bson = this.toSchemaBson();
444 			collection.insert(bson);
445 		}
446 	}
447 
448 	/// Removes this object from the collection. Returns false when _id of this is not set.
449 	bool remove() @safe const
450 	{
451 		if (!_schema_object_id_.valid)
452 			return false;
453 		collection.remove(Bson(["_id" : Bson(_schema_object_id_)]),
454 				DeleteFlags.SingleRemove);
455 		return true;
456 	}
457 
458 	/// Tries to find one document in the collection.
459 	/// Throws: DocumentNotFoundException if not found
460 	static Bson findOneOrThrow(Query!(typeof(this)) query)
461 	{
462 		return findOneOrThrow(query._query);
463 	}
464 
465 	/// ditto
466 	static Bson findOneOrThrow(T)(T query)
467 	{
468 		Bson found = collection.findOne(query);
469 		if (found.isNull)
470 			throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof);
471 		return found;
472 	}
473 
474 	/// Finds one element with the object id `id`.
475 	/// Throws: DocumentNotFoundException if not found
476 	static typeof(this) findById(BsonObjectID id)
477 	{
478 		return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id" : Bson(id)])));
479 	}
480 
481 	/// Finds one element with the hex id `id`.
482 	/// Throws: DocumentNotFoundException if not found
483 	static typeof(this) findById(string id)
484 	{
485 		return findById(BsonObjectID.fromString(id));
486 	}
487 
488 	/// Finds one element using a query.
489 	/// Throws: DocumentNotFoundException if not found
490 	static typeof(this) findOne(Query!(typeof(this)) query)
491 	{
492 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
493 	}
494 
495 	/// ditto
496 	static typeof(this) findOne(T)(T query)
497 	{
498 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
499 	}
500 
501 	/// 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.
502 	static Nullable!(typeof(this)) tryFindById(BsonObjectID id)
503 	{
504 		Bson found = collection.findOne(Bson(["_id" : Bson(id)]));
505 		if (found.isNull)
506 			return Nullable!(typeof(this)).init;
507 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
508 	}
509 
510 	/// ditto
511 	static Nullable!(typeof(this)) tryFindById(string id)
512 	{
513 		return tryFindById(BsonObjectID.fromString(id));
514 	}
515 
516 	/// 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.
517 	static Nullable!(typeof(this)) tryFindOne(Query!(typeof(this)) query)
518 	{
519 		return tryFindOne(query._query);
520 	}
521 
522 	/// ditto
523 	static Nullable!(typeof(this)) tryFindOne(T)(T query)
524 	{
525 		Bson found = collection.findOne(query);
526 		if (found.isNull)
527 			return Nullable!(typeof(this)).init;
528 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
529 	}
530 
531 	/// Tries to find a document by the _id field and returns a default value if it could not be found.
532 	static typeof(this) tryFindById(BsonObjectID id, typeof(this) defaultValue)
533 	{
534 		Bson found = collection.findOne(Bson(["_id" : Bson(id)]));
535 		if (found.isNull)
536 			return defaultValue;
537 		return fromSchemaBson!(typeof(this))(found);
538 	}
539 
540 	/// ditto
541 	static typeof(this) tryFindById(string id, typeof(this) defaultValue)
542 	{
543 		return tryFindById(BsonObjectID.fromString(id), defaultValue);
544 	}
545 
546 	/// Tries to find a document in this collection. It will return a default value if the document could not be found.
547 	static typeof(this) tryFindOne(Query!(typeof(this)) query, typeof(this) defaultValue)
548 	{
549 		return tryFindOne(query._query, defaultValue);
550 	}
551 
552 	/// ditto
553 	static typeof(this) tryFindOne(T)(T query, typeof(this) defaultValue)
554 	{
555 		Bson found = collection.findOne(query);
556 		if (found.isNull)
557 			return defaultValue;
558 		return fromSchemaBson!(typeof(this))(found);
559 	}
560 
561 	/// Finds one or more elements using a query.
562 	static typeof(this)[] find(Query!(typeof(this)) query, QueryFlags flags = QueryFlags.None,
563 			int num_skip = 0, int num_docs_per_chunk = 0)
564 	{
565 		return find(query._query, flags, num_skip, num_docs_per_chunk);
566 	}
567 
568 	/// ditto
569 	static typeof(this)[] find(T)(T query, QueryFlags flags = QueryFlags.None,
570 			int num_skip = 0, int num_docs_per_chunk = 0)
571 	{
572 		typeof(this)[] values;
573 		foreach (entry; collection.find(query, null, flags, num_skip, num_docs_per_chunk))
574 		{
575 			values ~= fromSchemaBson!(typeof(this))(entry);
576 		}
577 		return values;
578 	}
579 
580 	/// Finds one or more elements using a query as range.
581 	static DocumentRange!(typeof(this)) findRange(Query!(typeof(this)) query,
582 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
583 	{
584 		return findRange(query._query, flags, num_skip, num_docs_per_chunk);
585 	}
586 
587 	/// ditto
588 	static DocumentRange!(typeof(this)) findRange(T)(T query,
589 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
590 	{
591 		return DocumentRange!(typeof(this))(collection.find(serializeToBson(query),
592 				null, flags, num_skip, num_docs_per_chunk));
593 	}
594 
595 	/// Queries all elements from the collection as range.
596 	static DocumentRange!(typeof(this)) findAll()
597 	{
598 		return DocumentRange!(typeof(this))(collection.find());
599 	}
600 
601 	/// Inserts many documents at once. The resulting IDs of the symbols will be generated by the server and not known to the caller.
602 	static void insertMany(T)(T documents, InsertFlags options = InsertFlags.none)
603 		if (isInputRange!T && is(ElementType!T : typeof(this)))
604 	{
605 		import std.array : array;
606 		import std.algorithm : map;
607 
608 		if (documents.empty)
609 			return;
610 		collection.insert(documents.map!((a) {
611 				a.bsonID = BsonObjectID.init;
612 				return a.toSchemaBson;
613 			}).array, options); // .array needed because of vibe-d issue #2185
614 	}
615 
616 	/// Updates a document.
617 	static void update(U)(Query!(typeof(this)) query, U update, UpdateFlags options = UpdateFlags.none)
618 	{
619 		update(query._query, update, options);
620 	}
621 
622 	/// ditto
623 	static void update(T, U)(T query, U update, UpdateFlags options = UpdateFlags.none)
624 	{
625 		collection.update(query, update, options);
626 	}
627 
628 	/// Updates a document or inserts it when not existent. Shorthand for `update(..., UpdateFlags.upsert)`
629 	static void upsert(U)(Query!(typeof(this)) query, U upsert, UpdateFlags options = UpdateFlags.upsert)
630 	{
631 		upsert(query._query, upsert, options);
632 	}
633 
634 	/// ditto
635 	static void upsert(T, U)(T query, U update, UpdateFlags options = UpdateFlags.upsert)
636 	{
637 		collection.update(query, update, options);
638 	}
639 
640 	/// Deletes one or any amount of documents matching the selector based on the flags.
641 	static void remove(Query!(typeof(this)) query, DeleteFlags flags = DeleteFlags.none)
642 	{
643 		remove(query._query, flags);
644 	}
645 
646 	/// ditto
647 	static void remove(T)(T selector, DeleteFlags flags = DeleteFlags.none)
648 	{
649 		collection.remove(selector, flags);
650 	}
651 
652 	/// Removes all documents from this collection.
653 	static void removeAll()
654 	{
655 		collection.remove();
656 	}
657 
658 	/// Drops the entire collection and all indices in the database.
659 	static void dropTable()
660 	{
661 		collection.drop();
662 	}
663 
664 	/// Returns the count of documents in this collection matching this query.
665 	static auto count(Query!(typeof(this)) query)
666 	{
667 		return count(query._query);
668 	}
669 
670 	/// ditto
671 	static auto count(T)(T query)
672 	{
673 		return collection.count(query);
674 	}
675 
676 	/// Returns the count of documents in this collection.
677 	static auto countAll()
678 	{
679 		import vibe.data.bson : Bson;
680 
681 		return collection.count(Bson.emptyObject);
682 	}
683 
684 	/// Start of an aggregation call. Returns a pipeline with typesafe functions for modifying the pipeline and running it at the end.
685 	/// Examples:
686 	/// --------------------
687 	/// auto groupResults = Book.aggregate.groupAll([
688 	/// 	"totalPrice": Bson([
689 	/// 		"$sum": Bson([
690 	/// 			"$multiply": Bson([Bson("$price"), Bson("$quantity")])
691 	/// 		])
692 	/// 	]),
693 	/// 	"averageQuantity": Bson([
694 	/// 		"$avg": Bson("$quantity")
695 	/// 	]),
696 	/// 	"count": Bson(["$sum": Bson(1)])
697 	/// ]).run;
698 	/// --------------------
699 	static SchemaPipeline aggregate()
700 	{
701 		return SchemaPipeline(collection);
702 	}
703 }
704 
705 /// Binds a MongoCollection to a Schema. Can only be done once!
706 void register(T)(MongoCollection collection) @safe
707 {
708 	T obj = T.init;
709 
710 	static if (hasMember!(T, "_schema_collection_"))
711 	{
712 		(() @trusted {
713 			assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!");
714 			T._schema_collection_ = collection;
715 		})();
716 	}
717 
718 	foreach (memberName; __traits(allMembers, T))
719 	{
720 		static if (__traits(compiles, {
721 				static s = isVariable!(__traits(getMember, obj, memberName));
722 			}) && isVariable!(__traits(getMember, obj, memberName)))
723 		{
724 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
725 			{
726 				string name = memberName;
727 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
728 				{
729 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
730 					{
731 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
732 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
733 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
734 					}
735 
736 					IndexFlags flags = IndexFlags.None;
737 					ulong expires = 0LU;
738 					bool force;
739 
740 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoForceIndex))
741 					{
742 						force = true;
743 					}
744 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoBackground))
745 					{
746 						flags |= IndexFlags.Background;
747 					}
748 					static if (hasUDA!((__traits(getMember, obj, memberName)),
749 							mongoDropDuplicates))
750 					{
751 						flags |= IndexFlags.DropDuplicates;
752 					}
753 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoSparse))
754 					{
755 						flags |= IndexFlags.Sparse;
756 					}
757 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoUnique))
758 					{
759 						flags |= IndexFlags.Unique;
760 					}
761 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoExpire))
762 					{
763 						static assert(getUDAs!((__traits(getMember, obj, memberName)), mongoExpire).length == 1,
764 								"Member '" ~ memberName ~ "' can only have one expiry value!");
765 						flags |= IndexFlags.ExpireAfterSeconds;
766 						expires = getUDAs!((__traits(getMember, obj, memberName)), mongoExpire)[0]
767 							.seconds;
768 					}
769 
770 					if (flags != IndexFlags.None || force)
771 						collection.ensureIndex([tuple(name, 1)], flags, dur!"seconds"(expires));
772 				}
773 			}
774 		}
775 	}
776 }
777 
778 unittest
779 {
780 	struct C
781 	{
782 		int a = 4;
783 	}
784 
785 	struct B
786 	{
787 		C cref;
788 	}
789 
790 	struct A
791 	{
792 		B bref;
793 	}
794 
795 	A a;
796 	a.bref.cref.a = 5;
797 	auto bson = a.toSchemaBson();
798 	assert(bson["bref"]["cref"]["a"].get!int == 5);
799 	A b = bson.fromSchemaBson!A();
800 	assert(b.bref.cref.a == 5);
801 }
802 
803 unittest
804 {
805 	import std.digest.digest;
806 	import std.digest.sha;
807 
808 	enum Activity
809 	{
810 		High,
811 		Medium,
812 		Low
813 	}
814 
815 	enum Permission
816 	{
817 		A = 1,
818 		B = 2,
819 		C = 4
820 	}
821 
822 	struct UserSchema
823 	{
824 		string username = "Unnamed";
825 		@binaryType()
826 		string salt = "foobar";
827 		@encode("encodePassword")
828 		@binaryType()
829 		string password;
830 		@schemaName("date-created")
831 		SchemaDate dateCreated = SchemaDate.now;
832 		Activity activity = Activity.Medium;
833 		BitFlags!Permission permissions;
834 		Tuple!(string, string) name;
835 		ubyte x0;
836 		byte x1;
837 		ushort x2;
838 		short x3;
839 		uint x4;
840 		int x5;
841 		ulong x6;
842 		long x7;
843 		float x8;
844 		double x9;
845 		int[10] token;
846 
847 		Bson encodePassword(UserSchema user)
848 		{
849 			// TODO: Replace with something more secure
850 			return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt)));
851 		}
852 	}
853 
854 	auto user = UserSchema();
855 	user.password = "12345";
856 	user.username = "Bob";
857 	user.permissions = Permission.A | Permission.C;
858 	user.name = tuple("Bob", "Bobby");
859 	user.x0 = 7;
860 	user.x1 = 7;
861 	user.x2 = 7;
862 	user.x3 = 7;
863 	user.x4 = 7;
864 	user.x5 = 7;
865 	user.x6 = 7;
866 	user.x7 = 7;
867 	user.x8 = 7;
868 	user.x9 = 7;
869 	user.token[3] = 8;
870 	auto bson = user.toSchemaBson();
871 	assert(bson["username"].get!string == "Bob");
872 	assert(bson["date-created"].get!(BsonDate).value > 0);
873 	assert(bson["activity"].get!(int) == cast(int) Activity.Medium);
874 	assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar");
875 	assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt));
876 	assert(bson["permissions"].get!(int) == 5);
877 	assert(bson["name"].get!(Bson[]).length == 2);
878 	assert(bson["x0"].get!int == 7);
879 	assert(bson["x1"].get!int == 7);
880 	assert(bson["x2"].get!int == 7);
881 	assert(bson["x3"].get!int == 7);
882 	assert(bson["x4"].get!int == 7);
883 	assert(bson["x5"].get!int == 7);
884 	assert(bson["x6"].get!long == 7);
885 	assert(bson["x7"].get!long == 7);
886 	assert(bson["x8"].get!double == 7);
887 	assert(bson["x9"].get!double == 7);
888 	assert(bson["token"].get!(Bson[]).length == 10);
889 	assert(bson["token"].get!(Bson[])[3].get!int == 8);
890 
891 	auto user2 = bson.fromSchemaBson!UserSchema();
892 	assert(user2.username == user.username);
893 	assert(user2.password != user.password);
894 	assert(user2.salt == user.salt);
895 	// dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time
896 	assert(user2.dateCreated != user.dateCreated);
897 	assert(user2.activity == user.activity);
898 	assert(user2.permissions == user.permissions);
899 	assert(user2.name == user.name);
900 	assert(user2.x0 == user.x0);
901 	assert(user2.x1 == user.x1);
902 	assert(user2.x2 == user.x2);
903 	assert(user2.x3 == user.x3);
904 	assert(user2.x4 == user.x4);
905 	assert(user2.x5 == user.x5);
906 	assert(user2.x6 == user.x6);
907 	assert(user2.x7 == user.x7);
908 	assert(user2.x8 == user.x8);
909 	assert(user2.x9 == user.x9);
910 	assert(user2.token == user.token);
911 }
912 
913 // version(TestDB):
914 unittest
915 {
916 	import vibe.db.mongo.mongo;
917 	import std.digest.sha;
918 	import std.exception;
919 	import std.array;
920 
921 	auto client = connectMongoDB("127.0.0.1");
922 	auto database = client.getDatabase("test");
923 	MongoCollection users = database["users"];
924 	users.remove(); // Clears collection
925 
926 	struct User
927 	{
928 		mixin MongoSchema;
929 
930 		@mongoUnique string username;
931 		@binaryType()
932 		ubyte[] hash;
933 		@schemaName("profile-picture")
934 		string profilePicture;
935 		auto registered = SchemaDate.now;
936 	}
937 
938 	users.register!User;
939 
940 	assert(User.findAll().array.length == 0);
941 
942 	User user;
943 	user.username = "Example";
944 	user.hash = sha512Of("password123");
945 	user.profilePicture = "example-avatar.png";
946 
947 	assertNotThrown(user.save());
948 
949 	User user2;
950 	user2.username = "Bob";
951 	user2.hash = sha512Of("foobar");
952 	user2.profilePicture = "bob-avatar.png";
953 
954 	assertNotThrown(user2.save());
955 
956 	User faker;
957 	faker.username = "Example";
958 	faker.hash = sha512Of("PASSWORD");
959 	faker.profilePicture = "example-avatar.png";
960 
961 	assertThrown(faker.save());
962 	// Unique username
963 
964 	faker.username = "Example_";
965 	assertNotThrown(faker.save());
966 
967 	user.username = "NewExample";
968 	user.save();
969 
970 	auto actualFakeID = faker.bsonID;
971 	faker = User.findOne(["username" : "NewExample"]);
972 
973 	assert(actualFakeID != faker.bsonID);
974 
975 	foreach (usr; User.findAll)
976 	{
977 		usr.profilePicture = "default.png"; // Reset all profile pictures
978 		usr.save();
979 	}
980 	user = User.findOne(["username" : "NewExample"]);
981 	user2 = User.findOne(["username" : "Bob"]);
982 	faker = User.findOne(["username" : "Example_"]);
983 	assert(user.profilePicture == user2.profilePicture
984 			&& user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png");
985 
986 	User user3;
987 	user3.username = "User123";
988 	user3.hash = sha512Of("486951");
989 	user3.profilePicture = "new.png";
990 	User.upsert(["username" : "User123"], user3.toSchemaBson);
991 	user3 = User.findOne(["username" : "User123"]);
992 	assert(user3.hash == sha512Of("486951"));
993 	assert(user3.profilePicture == "new.png");
994 }
995 
996 unittest
997 {
998 	import vibe.db.mongo.mongo;
999 	import mongoschema.aliases : name, ignore, unique, binary;
1000 	import std.digest.sha;
1001 	import std.digest.md;
1002 
1003 	auto client = connectMongoDB("127.0.0.1");
1004 
1005 	struct Permission
1006 	{
1007 		string name;
1008 		int priority;
1009 	}
1010 
1011 	struct User
1012 	{
1013 		mixin MongoSchema;
1014 
1015 		@unique string username;
1016 
1017 		@binary()
1018 		ubyte[] hash;
1019 		@binary()
1020 		ubyte[] salt;
1021 
1022 		@name("profile-picture")
1023 		string profilePicture = "default.png";
1024 
1025 		Permission[] permissions;
1026 
1027 	@ignore:
1028 		int sessionID;
1029 	}
1030 
1031 	auto coll = client.getCollection("test.users2");
1032 	coll.remove();
1033 	coll.register!User;
1034 
1035 	User register(string name, string password)
1036 	{
1037 		User user;
1038 		user.username = name;
1039 		user.salt = md5Of(name).dup;
1040 		user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup;
1041 		user.permissions ~= Permission("forum.access", 1);
1042 		user.save();
1043 		return user;
1044 	}
1045 
1046 	User find(string name)
1047 	{
1048 		return User.findOne(["username" : name]);
1049 	}
1050 
1051 	User a = register("foo", "bar");
1052 	User b = find("foo");
1053 	assert(a == b);
1054 }