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