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