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