1 module mongoschema; 2 3 import vibe.data.bson; 4 import vibe.db.mongo.collection; 5 import vibe.db.mongo.connection; 6 import std.datetime; 7 import std.traits; 8 import core.time; 9 import std.typecons : tuple, Nullable; 10 11 // Bson Attributes 12 13 /// Will ignore the variables and not encode/decode them. 14 enum schemaIgnore; 15 /// Custom encode function. `func` is the name of the function which must be present as child. 16 struct encode 17 { /++ Function name (needs to be member function) +/ string func; 18 } 19 /// Custom decode function. `func` is the name of the function which must be present as child. 20 struct decode 21 { /++ Function name (needs to be member function) +/ string func; 22 } 23 /// Encodes the value as binary value. Must be an array with one byte wide elements. 24 struct binaryType 25 { /++ Type to encode +/ BsonBinData.Type type = BsonBinData.Type.generic; 26 } 27 /// Custom name for special characters. 28 struct schemaName 29 { /++ Custom replacement name +/ string name; 30 } 31 32 // Mongo Attributes 33 /// Will create an index with (by default) no flags. 34 enum mongoForceIndex; 35 /// Background index construction allows read and write operations to continue while building the index. 36 enum mongoBackground; 37 /// Drops duplicates in the database. Only for Mongo versions less than 3.0 38 enum mongoDropDuplicates; 39 /// Sparse indexes are like non-sparse indexes, except that they omit references to documents that do not include the indexed field. 40 enum mongoSparse; 41 /// MongoDB allows you to specify a unique constraint on an index. These constraints prevent applications from inserting documents that have duplicate values for the inserted fields. 42 enum mongoUnique; 43 /// TTL indexes expire documents after the specified number of seconds has passed since the indexed field value; i.e. the expiration threshold is the indexed field value plus the specified number of seconds. 44 /// Field must be a SchemaDate/BsonDate. You must update the time using collMod. 45 struct mongoExpire 46 { 47 /// 48 this(long seconds) 49 { 50 this.seconds = cast(ulong) seconds; 51 } 52 /// 53 this(ulong seconds) 54 { 55 this.seconds = seconds; 56 } 57 /// 58 this(Duration time) 59 { 60 seconds = cast(ulong) time.total!"msecs"; 61 } 62 /// 63 ulong seconds; 64 } 65 66 private template isVariable(alias T) 67 { 68 enum isVariable = !is(T) && is(typeof(T)) && !isCallable!T 69 && !is(T == void) && !__traits(isStaticFunction, T) && !__traits(isOverrideFunction, T) 70 && !__traits(isFinalFunction, T) && !__traits(isAbstractFunction, T) 71 && !__traits(isVirtualFunction, T) && !__traits(isVirtualMethod, 72 T) && !is(ReturnType!T); 73 } 74 75 private template isVariable(T) 76 { 77 enum isVariable = false; // Types are no variables 78 } 79 80 private template getUDAs(alias symbol, alias attribute) 81 { 82 import std.typetuple : Filter; 83 84 enum isDesiredUDA(alias S) = is(typeof(S) == attribute); 85 alias getUDAs = Filter!(isDesiredUDA, __traits(getAttributes, symbol)); 86 } 87 88 private Bson memberToBson(T)(T member) 89 { 90 static if (__traits(hasMember, T, "toBson") && is(ReturnType!(typeof(T.toBson)) == Bson)) 91 { 92 // Custom defined toBson 93 return T.toBson(member); 94 } 95 else static if (is(T == Json)) 96 { 97 return Bson.fromJson(member); 98 } 99 else static if (is(T == BsonBinData) || is(T == BsonObjectID) 100 || is(T == BsonDate) || is(T == BsonTimestamp) 101 || is(T == BsonRegex) || is(T == typeof(null))) 102 { 103 return Bson(member); 104 } 105 else static if (!isBasicType!T && !isArray!T && !is(T == enum) 106 && !is(T == Bson) && !isAssociativeArray!T) 107 { 108 // Mixed in MongoSchema 109 return member.toSchemaBson(); 110 } 111 else // Generic value 112 { 113 static if (is(T == enum)) 114 { // Enum value 115 return Bson(cast(OriginalType!T) member); 116 } 117 else static if (isArray!(T) && !isSomeString!T) 118 { // Arrays of anything except strings 119 Bson[] values; 120 foreach (val; member) 121 values ~= memberToBson(val); 122 return Bson(values); 123 } 124 else static if (isAssociativeArray!T) 125 { // Associative Arrays (Objects) 126 Bson[string] values; 127 foreach (name, val; member) 128 values[name] = memberToBson(val); 129 return Bson(values); 130 } 131 else static if (is(T == Bson)) 132 { // Already a Bson object 133 return member; 134 } 135 else static if (__traits(compiles, { Bson(member); })) 136 { // Check if this can be passed 137 return Bson(member); 138 } 139 else 140 { 141 pragma(msg, "Warning falling back to serializeToBson for type " ~ T.stringof); 142 return serializeToBson(member); 143 } 144 } 145 } 146 147 private T bsonToMember(T)(auto ref T member, Bson value) 148 { 149 static if (__traits(hasMember, T, "fromBson") && is(ReturnType!(typeof(T.fromBson)) == T)) 150 { 151 // Custom defined toBson 152 return T.fromBson(value); 153 } 154 else static if (is(T == Json)) 155 { 156 return Bson.fromJson(value); 157 } 158 else static if (is(T == BsonBinData) || is(T == BsonObjectID) 159 || is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex)) 160 { 161 return value.get!T; 162 } 163 else static if (!isBasicType!T && !isArray!T && !is(T == enum) 164 && !is(T == Bson) && !isAssociativeArray!T) 165 { 166 // Mixed in MongoSchema 167 return value.fromSchemaBson!T(); 168 } 169 else // Generic value 170 { 171 static if (is(T == enum)) 172 { // Enum value 173 return cast(T) value.get!(OriginalType!T); 174 } 175 else static if (isArray!T && !isSomeString!T) 176 { // Arrays of anything except strings 177 alias Type = typeof(member[0]); 178 T values; 179 foreach (val; value) 180 { 181 values ~= bsonToMember!Type(Type.init, val); 182 } 183 return values; 184 } 185 else static if (isAssociativeArray!T) 186 { // Associative Arrays (Objects) 187 T values; 188 alias ValType = ValueType!T; 189 foreach (name, val; value) 190 values[name] = bsonToMember!ValType(ValType.init, val); 191 return values; 192 } 193 else static if (is(T == Bson)) 194 { // Already a Bson object 195 return value; 196 } 197 else static if (__traits(compiles, { value.get!T(); })) 198 { // Check if this can be passed 199 return value.get!T(); 200 } 201 else 202 { 203 pragma(msg, "Warning falling back to deserializeBson for type " ~ T.stringof); 204 return deserializeBson!T(value); 205 } 206 } 207 } 208 209 /// Generates a Bson document from a struct/class object 210 Bson toSchemaBson(T)(T obj) 211 { 212 static if (__traits(compiles, cast(T) null) && __traits(compiles, { 213 T foo = null; 214 })) 215 { 216 if (obj is null) 217 return Bson(null); 218 } 219 220 Bson data = Bson.emptyObject; 221 222 static if (hasMember!(T, "_schema_object_id_")) 223 { 224 if (obj.bsonID.valid) 225 data["_id"] = obj.bsonID; 226 } 227 228 foreach (memberName; __traits(allMembers, T)) 229 { 230 static if (memberName == "_schema_object_id_") 231 continue; 232 else static if (__traits(compiles, { 233 static s = isVariable!(__traits(getMember, obj, memberName)); 234 }) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, { 235 static s = __traits(getMember, T, memberName); 236 }) // No static members 237 && __traits(compiles, { 238 typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, 239 obj, memberName); 240 })) 241 { 242 static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 243 { 244 string name = memberName; 245 Bson value; 246 static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 247 { 248 static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 249 { 250 static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 251 .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 252 name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 253 } 254 255 static if (hasUDA!((__traits(getMember, obj, memberName)), encode)) 256 { 257 static assert(getUDAs!((__traits(getMember, obj, memberName)), encode).length == 1, 258 "Member '" ~ memberName ~ "' can only have one encoder!"); 259 mixin("value = obj." ~ getUDAs!((__traits(getMember, 260 obj, memberName)), encode)[0].func ~ "(obj);"); 261 } 262 else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 263 { 264 static assert(isArray!(typeof((__traits(getMember, obj, 265 memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 266 "Binary member '" ~ memberName 267 ~ "' can only be an array of 1 byte values"); 268 static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1, 269 "Binary member '" ~ memberName ~ "' can only have one type!"); 270 BsonBinData.Type type = getUDAs!((__traits(getMember, 271 obj, memberName)), binaryType)[0].type; 272 value = Bson(BsonBinData(type, 273 cast(immutable(ubyte)[])(__traits(getMember, obj, memberName)))); 274 } 275 else 276 { 277 static if (__traits(compiles, { 278 __traits(hasMember, typeof((__traits(getMember, 279 obj, memberName))), "toBson"); 280 }) && __traits(hasMember, typeof((__traits(getMember, obj, 281 memberName))), "toBson") && !is(ReturnType!(typeof((__traits(getMember, 282 obj, memberName)).toBson)) == Bson)) 283 pragma(msg, "Warning: ", typeof((__traits(getMember, obj, memberName))).stringof, 284 ".toBson does not return a vibe.data.bson.Bson struct!"); 285 286 value = memberToBson(__traits(getMember, obj, memberName)); 287 } 288 data[name] = value; 289 } 290 } 291 } 292 } 293 294 return data; 295 } 296 297 /// Generates a struct/class object from a Bson node 298 T fromSchemaBson(T)(Bson bson) 299 { 300 static if (__traits(compiles, cast(T) null) && __traits(compiles, { 301 T foo = null; 302 })) 303 { 304 if (bson.isNull) 305 return null; 306 } 307 T obj = T.init; 308 309 static if (hasMember!(T, "_schema_object_id_")) 310 { 311 if (!bson.tryIndex("_id").isNull) 312 obj.bsonID = bson["_id"].get!BsonObjectID; 313 } 314 315 foreach (memberName; __traits(allMembers, T)) 316 { 317 static if (memberName == "_schema_object_id_") 318 continue; 319 else static if (__traits(compiles, { 320 static s = isVariable!(__traits(getMember, obj, memberName)); 321 }) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, { 322 static s = __traits(getMember, T, memberName); 323 }) // No static members 324 && __traits(compiles, { 325 typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, 326 obj, memberName); 327 })) 328 { 329 static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 330 { 331 string name = memberName; 332 static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 333 { 334 static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 335 { 336 static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 337 .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 338 name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 339 } 340 341 // compile time code will still be generated but not run at runtime 342 if (bson.tryIndex(name).isNull) 343 continue; 344 345 static if (hasUDA!((__traits(getMember, obj, memberName)), decode)) 346 { 347 static assert(getUDAs!((__traits(getMember, obj, memberName)), decode).length == 1, 348 "Member '" ~ memberName ~ "' can only have one decoder!"); 349 mixin("obj." ~ memberName ~ " = obj." ~ getUDAs!((__traits(getMember, 350 obj, memberName)), decode)[0].func ~ "(bson);"); 351 } 352 else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 353 { 354 static assert(isArray!(typeof((__traits(getMember, obj, 355 memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 356 "Binary member '" ~ memberName 357 ~ "' can only be an array of 1 byte values"); 358 static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1, 359 "Binary member '" ~ memberName ~ "' can only have one type!"); 360 assert(bson[name].type == Bson.Type.binData); 361 auto data = bson[name].get!(BsonBinData).rawData; 362 mixin("obj." ~ memberName ~ " = cast(typeof(obj." ~ memberName ~ ")) data;"); 363 } 364 else 365 { 366 mixin( 367 "obj." ~ memberName ~ " = bsonToMember(obj." 368 ~ memberName ~ ", bson[name]);"); 369 } 370 } 371 } 372 } 373 } 374 375 return obj; 376 } 377 378 struct DocumentRange(Schema) 379 { 380 private MongoCursor!(Bson, Bson, typeof(null)) _cursor; 381 382 package this(MongoCursor!(Bson, Bson, typeof(null)) cursor) 383 { 384 _cursor = cursor; 385 } 386 387 /** 388 Returns true if there are no more documents for this cursor. 389 390 Throws: An exception if there is a query or communication error. 391 */ 392 @property bool empty() 393 { 394 return _cursor.empty; 395 } 396 397 /** 398 Returns the current document of the response. 399 400 Use empty and popFront to iterate over the list of documents using an 401 input range interface. Note that calling this function is only allowed 402 if empty returns false. 403 */ 404 @property Schema front() 405 { 406 return fromSchemaBson!Schema(_cursor.front); 407 } 408 409 /** 410 Controls the order in which the query returns matching documents. 411 412 This method must be called before starting to iterate, or an exeption 413 will be thrown. If multiple calls to $(D sort()) are issued, only 414 the last one will have an effect. 415 416 Params: 417 order = A BSON object convertible value that defines the sort order 418 of the result. This BSON object must be structured according to 419 the MongoDB documentation (see below). 420 421 Returns: Reference to the modified original curser instance. 422 423 Throws: 424 An exception if there is a query or communication error. 425 Also throws if the method was called after beginning of iteration. 426 427 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort) 428 */ 429 auto sort(T)(T order) 430 { 431 _cursor.sort(serializeToBson(order)); 432 return this; 433 } 434 435 /** 436 Limits the number of documents that the cursor returns. 437 438 This method must be called before beginnig iteration in order to have 439 effect. If multiple calls to limit() are made, the one with the lowest 440 limit will be chosen. 441 442 Params: 443 count = The maximum number number of documents to return. A value 444 of zero means unlimited. 445 446 Returns: the same cursor 447 448 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit) 449 */ 450 auto limit(size_t count) 451 { 452 _cursor.limit(count); 453 return this; 454 } 455 456 /** 457 Skips a given number of elements at the beginning of the cursor. 458 459 This method must be called before beginnig iteration in order to have 460 effect. If multiple calls to skip() are made, the one with the maximum 461 number will be chosen. 462 463 Params: 464 count = The number of documents to skip. 465 466 Returns: the same cursor 467 468 See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip) 469 */ 470 auto skip(int count) 471 { 472 _cursor.skip(count); 473 return this; 474 } 475 476 /** 477 Advances the cursor to the next document of the response. 478 479 Note that calling this function is only allowed if empty returns false. 480 */ 481 void popFront() 482 { 483 _cursor.popFront(); 484 } 485 486 /** 487 Iterates over all remaining documents. 488 489 Note that iteration is one-way - elements that have already been visited 490 will not be visited again if another iteration is done. 491 492 Throws: An exception if there is a query or communication error. 493 */ 494 int opApply(int delegate(Schema doc) del) 495 { 496 while (!_cursor.empty) 497 { 498 auto doc = _cursor.front; 499 _cursor.popFront(); 500 if (auto ret = del(fromSchemaBson!Schema(doc))) 501 return ret; 502 } 503 return 0; 504 } 505 } 506 507 class DocumentNotFoundException : Exception 508 { 509 this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe 510 { 511 super(msg, file, line); 512 } 513 } 514 515 /// Mixin for functions for interacting with Mongo collections. 516 mixin template MongoSchema() 517 { 518 static MongoCollection _schema_collection_; 519 private BsonObjectID _schema_object_id_; 520 521 /// Returns: the _id value (if set by save or find) 522 @property ref BsonObjectID bsonID() 523 { 524 return _schema_object_id_; 525 } 526 527 /// Inserts or updates an existing value. 528 void save() 529 { 530 if (_schema_object_id_.valid) 531 { 532 _schema_collection_.update(Bson(["_id" : Bson(_schema_object_id_)]), 533 this.toSchemaBson(), UpdateFlags.upsert); 534 } 535 else 536 { 537 _schema_object_id_ = BsonObjectID.generate; 538 auto bson = this.toSchemaBson(); 539 _schema_collection_.insert(bson); 540 } 541 } 542 543 /// Removes this object from the collection. Returns false when _id of this is not set. 544 bool remove() 545 { 546 if (!_schema_object_id_.valid) 547 return false; 548 _schema_collection_.remove(Bson(["_id" : Bson(_schema_object_id_)]), 549 DeleteFlags.SingleRemove); 550 return true; 551 } 552 553 static auto findOneOrThrow(T)(T query) 554 { 555 Bson found = _schema_collection_.findOne(query); 556 if (found.isNull) 557 throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof); 558 return found; 559 } 560 561 /// Finds one element with the object id `id` 562 static typeof(this) findById(BsonObjectID id) 563 { 564 return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id" : Bson(id)]))); 565 } 566 567 /// Finds one element with the hex id `id` 568 static typeof(this) findById(string id) 569 { 570 return findById(BsonObjectID.fromString(id)); 571 } 572 573 /// Finds one element using a query. 574 static typeof(this) findOne(T)(T query) 575 { 576 return fromSchemaBson!(typeof(this))(findOneOrThrow(query)); 577 } 578 579 static Nullable!(typeof(this)) tryFindById(BsonObjectID id) 580 { 581 Bson found = _schema_collection_.findOne(Bson(["_id" : Bson(id)])); 582 if (found.isNull) 583 return Nullable!(typeof(this)).init; 584 return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found)); 585 } 586 587 static Nullable!(typeof(this)) tryFindById(string id) 588 { 589 return tryFindById(BsonObjectID.fromString(id)); 590 } 591 592 static Nullable!(typeof(this)) tryFindOne(T)(T query) 593 { 594 Bson found = _schema_collection_.findOne(query); 595 if (found.isNull) 596 return Nullable!(typeof(this)).init; 597 return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found)); 598 } 599 600 /// Finds one or more elements using a query. 601 static typeof(this)[] find(T)(T query, QueryFlags flags = QueryFlags.None, 602 int num_skip = 0, int num_docs_per_chunk = 0) 603 { 604 typeof(this)[] values; 605 foreach (entry; _schema_collection_.find(query, null, flags, num_skip, num_docs_per_chunk)) 606 { 607 values ~= fromSchemaBson!(typeof(this))(entry); 608 } 609 return values; 610 } 611 612 /// Queries all elements from the collection. 613 deprecated("use findAll instead") static typeof(this)[] find() 614 { 615 typeof(this)[] values; 616 foreach (entry; _schema_collection_.find()) 617 { 618 values ~= fromSchemaBson!(typeof(this))(entry); 619 } 620 return values; 621 } 622 623 /// Finds one or more elements using a query as range. 624 static DocumentRange!(typeof(this)) findRange(T)(T query, 625 QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0) 626 { 627 return DocumentRange!(typeof(this))(_schema_collection_.find(query, 628 null, flags, num_skip, num_docs_per_chunk)); 629 } 630 631 /// Queries all elements from the collection as range. 632 static DocumentRange!(typeof(this)) findAll() 633 { 634 return DocumentRange!(typeof(this))(_schema_collection_.find()); 635 } 636 637 /// Updates a document. 638 static void update(T, U)(T query, U update, UpdateFlags options = UpdateFlags.none) 639 { 640 _schema_collection_.update(query, update, options); 641 } 642 643 /// Updates a document or inserts it when not existent. Shorthand for `update(..., UpdateFlags.upsert)` 644 static void upsert(T, U)(T query, U update, UpdateFlags options = UpdateFlags.upsert) 645 { 646 _schema_collection_.update(query, update, options); 647 } 648 } 649 650 /// Binds a MongoCollection to a Schema. Can only be done once! 651 void register(T)(MongoCollection collection) 652 { 653 T obj = T.init; 654 655 static if (hasMember!(T, "_schema_collection_")) 656 { 657 assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!"); 658 T._schema_collection_ = collection; 659 } 660 661 foreach (memberName; __traits(allMembers, T)) 662 { 663 static if (__traits(compiles, { 664 static s = isVariable!(__traits(getMember, obj, memberName)); 665 }) && isVariable!(__traits(getMember, obj, memberName))) 666 { 667 static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 668 { 669 string name = memberName; 670 static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 671 { 672 static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 673 { 674 static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 675 .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 676 name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 677 } 678 679 IndexFlags flags = IndexFlags.None; 680 ulong expires = 0LU; 681 bool force; 682 683 static if (hasUDA!((__traits(getMember, obj, memberName)), mongoForceIndex)) 684 { 685 force = true; 686 } 687 static if (hasUDA!((__traits(getMember, obj, memberName)), mongoBackground)) 688 { 689 flags |= IndexFlags.Background; 690 } 691 static if (hasUDA!((__traits(getMember, obj, memberName)), 692 mongoDropDuplicates)) 693 { 694 flags |= IndexFlags.DropDuplicates; 695 } 696 static if (hasUDA!((__traits(getMember, obj, memberName)), mongoSparse)) 697 { 698 flags |= IndexFlags.Sparse; 699 } 700 static if (hasUDA!((__traits(getMember, obj, memberName)), mongoUnique)) 701 { 702 flags |= IndexFlags.Unique; 703 } 704 static if (hasUDA!((__traits(getMember, obj, memberName)), mongoExpire)) 705 { 706 static assert(getUDAs!((__traits(getMember, obj, memberName)), mongoExpire).length == 1, 707 "Member '" ~ memberName ~ "' can only have one expiry value!"); 708 flags |= IndexFlags.ExpireAfterSeconds; 709 expires = getUDAs!((__traits(getMember, obj, memberName)), mongoExpire)[0] 710 .seconds; 711 } 712 713 if (flags != IndexFlags.None || force) 714 collection.ensureIndex([tuple(name, 1)], flags, dur!"seconds"(expires)); 715 } 716 } 717 } 718 } 719 } 720 721 /// Class serializing to a bson date containing a special `now` value that gets translated to the current time when converting to bson. 722 final struct SchemaDate 723 { 724 public: 725 /// 726 this(BsonDate date) 727 { 728 _time = date.value; 729 } 730 731 /// 732 this(long time) 733 { 734 _time = time; 735 } 736 737 /// 738 @property auto time() 739 { 740 return _time; 741 } 742 743 /// 744 static Bson toBson(SchemaDate date) 745 { 746 if (date._time == -1) 747 { 748 return Bson(BsonDate.fromStdTime(Clock.currStdTime())); 749 } 750 else 751 { 752 return Bson(BsonDate(date._time)); 753 } 754 } 755 756 /// 757 static SchemaDate fromBson(Bson bson) 758 { 759 return SchemaDate(bson.get!BsonDate.value); 760 } 761 762 /// Magic value setting the date to the current time stamp when serializing. 763 static SchemaDate now() 764 { 765 return SchemaDate(-1); 766 } 767 768 private: 769 long _time; 770 } 771 772 unittest 773 { 774 struct C 775 { 776 int a = 4; 777 } 778 779 struct B 780 { 781 C cref; 782 } 783 784 struct A 785 { 786 B bref; 787 } 788 789 A a; 790 a.bref.cref.a = 5; 791 auto bson = a.toSchemaBson(); 792 assert(bson["bref"]["cref"]["a"].get!int == 5); 793 A b = bson.fromSchemaBson!A(); 794 assert(b.bref.cref.a == 5); 795 } 796 797 unittest 798 { 799 import std.digest.digest; 800 import std.digest.sha; 801 802 enum Activity 803 { 804 High, 805 Medium, 806 Low 807 } 808 809 struct UserSchema 810 { 811 string username = "Unnamed"; 812 @binaryType() 813 string salt = "foobar"; 814 @encode("encodePassword") 815 @binaryType() 816 string password; 817 @schemaName("date-created") 818 SchemaDate dateCreated = SchemaDate.now; 819 Activity activity = Activity.Medium; 820 821 Bson encodePassword(UserSchema user) 822 { 823 // TODO: Replace with something more secure 824 return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt))); 825 } 826 } 827 828 auto user = UserSchema(); 829 user.password = "12345"; 830 user.username = "Bob"; 831 auto bson = user.toSchemaBson(); 832 assert(bson["username"].get!string == "Bob"); 833 assert(bson["date-created"].get!(BsonDate).value > 0); 834 assert(bson["activity"].get!(int) == cast(int) Activity.Medium); 835 assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar"); 836 assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt)); 837 838 auto user2 = bson.fromSchemaBson!UserSchema(); 839 assert(user2.username == user.username); 840 assert(user2.password != user.password); 841 assert(user2.salt == user.salt); 842 // dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time 843 assert(user2.dateCreated != user.dateCreated); 844 assert(user2.activity == user.activity); 845 } 846 847 unittest 848 { 849 import vibe.db.mongo.mongo; 850 import std.digest.sha; 851 import std.exception; 852 import std.array; 853 854 auto client = connectMongoDB("localhost"); 855 auto database = client.getDatabase("test"); 856 MongoCollection users = database["users"]; 857 users.remove(); // Clears collection 858 859 struct User 860 { 861 mixin MongoSchema; 862 863 @mongoUnique string username; 864 @binaryType() 865 ubyte[] hash; 866 @schemaName("profile-picture") 867 string profilePicture; 868 auto registered = SchemaDate.now; 869 } 870 871 users.register!User; 872 873 assert(User.findAll().array.length == 0); 874 875 User user; 876 user.username = "Example"; 877 user.hash = sha512Of("password123"); 878 user.profilePicture = "example-avatar.png"; 879 880 assertNotThrown(user.save()); 881 882 User user2; 883 user2.username = "Bob"; 884 user2.hash = sha512Of("foobar"); 885 user2.profilePicture = "bob-avatar.png"; 886 887 assertNotThrown(user2.save()); 888 889 User faker; 890 faker.username = "Example"; 891 faker.hash = sha512Of("PASSWORD"); 892 faker.profilePicture = "example-avatar.png"; 893 894 assertThrown(faker.save()); 895 // Unique username 896 897 faker.username = "Example_"; 898 assertNotThrown(faker.save()); 899 900 user.username = "NewExample"; 901 user.save(); 902 903 auto actualFakeID = faker.bsonID; 904 faker = User.findOne(["username" : "NewExample"]); 905 906 assert(actualFakeID != faker.bsonID); 907 908 foreach (usr; User.findAll) 909 { 910 usr.profilePicture = "default.png"; // Reset all profile pictures 911 usr.save(); 912 } 913 user = User.findOne(["username" : "NewExample"]); 914 user2 = User.findOne(["username" : "Bob"]); 915 faker = User.findOne(["username" : "Example_"]); 916 assert(user.profilePicture == user2.profilePicture 917 && user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png"); 918 919 User user3; 920 user3.username = "User123"; 921 user3.hash = sha512Of("486951"); 922 user3.profilePicture = "new.png"; 923 User.upsert(["username" : "User123"], user3.toSchemaBson); 924 user3 = User.findOne(["username" : "User123"]); 925 assert(user3.hash == sha512Of("486951")); 926 assert(user3.profilePicture == "new.png"); 927 } 928 929 unittest 930 { 931 import vibe.db.mongo.mongo; 932 import mongoschema.aliases : name, ignore, unique, binary; 933 import std.digest.sha; 934 import std.digest.md; 935 936 auto client = connectMongoDB("localhost"); 937 938 struct Permission 939 { 940 string name; 941 int priority; 942 } 943 944 struct User 945 { 946 mixin MongoSchema; 947 948 @unique string username; 949 950 @binary() 951 ubyte[] hash; 952 @binary() 953 ubyte[] salt; 954 955 @name("profile-picture") 956 string profilePicture = "default.png"; 957 958 Permission[] permissions; 959 960 @ignore: 961 int sessionID; 962 } 963 964 auto coll = client.getCollection("test.users2"); 965 coll.remove(); 966 coll.register!User; 967 968 User register(string name, string password) 969 { 970 User user; 971 user.username = name; 972 user.salt = md5Of(name).dup; 973 user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup; 974 user.permissions ~= Permission("forum.access", 1); 975 user.save(); 976 return user; 977 } 978 979 User find(string name) 980 { 981 return User.findOne(["username" : name]); 982 } 983 984 User a = register("foo", "bar"); 985 User b = find("foo"); 986 assert(a == b); 987 }