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 }