SierraSoftworks/Iridium

View on GitHub
lib/Model.ts

Summary

Maintainability
F
3 days
Test Coverage
import * as  MongoDB from "mongodb";
import * as Bluebird from "bluebird";
import * as util from "util";
import * as  _ from "lodash";
import * as Skmatc from "skmatc";

import {Core} from "./Core";
import {Instance} from "./Instance";
import {Schema} from "./Schema";
import {Hooks} from "./Hooks";
import {Plugin} from "./Plugins";
import {Cache} from "./Cache";
import {CacheDirector} from "./CacheDirector";
import {CacheOnID} from "./cacheControllers/IDDirector";
import * as General from "./General";
import {Cursor} from "./Cursor";
import * as Index from "./Index";
import * as ModelOptions from "./ModelOptions";
import {Conditions} from "./Conditions";
import {Changes} from "./Changes";

import {Omnom} from "./utils/Omnom";
import {ModelCache} from "./ModelCache";
import {ModelHelpers} from "./ModelHelpers";
import {ModelHandlers} from "./ModelHandlers";
import * as ModelInterfaces from "./ModelInterfaces";
import {ModelSpecificInstance} from "./ModelSpecificInstance";
import {InstanceImplementation} from "./InstanceInterface";
import {Transforms, DefaultTransforms} from "./Transforms";
import * as AggregationPipeline from "./Aggregate";
import {MapFunction, ReduceFunction, MapReducedDocument, MapReduceFunctions, MapReduceOptions} from "./MapReduce";

/**
 * An Iridium Model which represents a structured MongoDB collection.
 * Models expose the methods you will generally use to query those collections, and ensure that
 * the results of those queries are returned as {TInstance} instances.
 *
 * @param TDocument The interface used to determine the schema of documents in the collection.
 * @param TInstance The interface or class used to represent collection documents in the JS world.
 *
 * @class
 */
export class Model<TDocument extends { _id?: any }, TInstance> {
    /**
     * Creates a new Iridium model representing a given ISchema and backed by a collection whose name is specified
     * @param core The Iridium core that this model should use for database access
     * @param instanceType The class which will be instantiated for each document retrieved from the database
     * @constructor
     */
    constructor(core: Core, instanceType: InstanceImplementation<TDocument, TInstance>) {
        if (!(core instanceof Core)) throw new Error("You failed to provide a valid Iridium core for this model");
        if (typeof instanceType !== "function") throw new Error("You failed to provide a valid instance constructor for this model");
        if (typeof instanceType.collection !== "string" || !instanceType.collection) throw new Error("You failed to provide a valid collection name for this model");
        if (!_.isPlainObject(instanceType.schema) || instanceType.schema._id === undefined) throw new Error("You failed to provide a valid schema for this model");

        this._core = core;

        this.loadExternal(instanceType);
        this.onNewModel();
        this.loadInternal();
    }

    /**
     * Loads any externally available properties (generally accessed using public getters/setters).
     */
    private loadExternal(instanceType: InstanceImplementation<TDocument, TInstance>) {
        this._collection = instanceType.collection;
        this._schema = instanceType.schema;
        this._hooks = instanceType;
        this._cacheDirector = instanceType.cache || new CacheOnID();
        this._transforms = instanceType.transforms || {};
        this._validators = instanceType.validators || [];
        this._indexes = instanceType.indexes || [];

        if(!this._schema._id) this._schema._id = MongoDB.ObjectID;

        if(this._schema._id === MongoDB.ObjectID && !this._transforms._id)
            this._transforms._id = DefaultTransforms.ObjectID;

        if ((<Function>instanceType).prototype instanceof Instance)
            this._Instance = ModelSpecificInstance(this, instanceType);
        else
            this._Instance = instanceType.bind(undefined, this);
    }

    /**
     * Loads any internally (protected/private) properties and helpers only used within Iridium itself.
     */
    private loadInternal() {
        this._cache = new ModelCache(this);
        this._helpers = new ModelHelpers(this);
        this._handlers = new ModelHandlers(this);
    }

    /**
     * Process any callbacks and plugin delegation for the creation of this model.
     * It will generally be called whenever a new Iridium Core is created, however is
     * more specifically tied to the lifespan of the models themselves.
     */
    private onNewModel() {
        this._core.plugins.forEach(plugin => plugin.newModel && plugin.newModel(this));
    }

    private _helpers: ModelHelpers<TDocument, TInstance>;
    /**
     * Provides helper methods used by Iridium for common tasks
     * @returns A set of helper methods which are used within Iridium for common tasks
     */
    get helpers(): ModelHelpers<TDocument, TInstance> {
        return this._helpers;
    }

    private _handlers: ModelHandlers<TDocument, TInstance>;
    /**
     * Provides helper methods used by Iridium for hook delegation and common processes
     * @returns A set of helper methods which perform common event and response handling tasks within Iridium.
     */
    get handlers(): ModelHandlers<TDocument, TInstance> {
        return this._handlers;
    }

    private _hooks: Hooks<TDocument, TInstance> = {};

    /**
     * Gets the even hooks subscribed on this model for a number of different state changes.
     * These hooks are primarily intended to allow lifecycle manipulation logic to be added
     * in the user's model definition, allowing tasks such as the setting of default values
     * or automatic client-side joins to take place.
     */
    get hooks(): Hooks<TDocument, TInstance> {
        return this._hooks;
    }

    private _schema: Schema;
    /**
     * Gets the schema dictating the data structure represented by this model.
     * The schema is used by skmatc to validate documents before saving to the database, however
     * until MongoDB 3.1 becomes widely available (with server side validation support) we are
     * limited in our ability to validate certain types of updates. As such, these validations
     * act more as a data-integrity check than anything else, unless you purely make use of Omnom
     * updates within instances.
     * @public
     * @returns The defined validation schema for this model
     */
    get schema(): Schema {
        return this._schema;
    }

    private _core: Core;
    /**
     * Gets the Iridium core that this model is associated with.
     * @public
     * @returns The Iridium core that this model is bound to
     */
    get core(): Core {
        return this._core;
    }

    private _collection: string;
    /**
     * Gets the underlying MongoDB collection from which this model's documents are retrieved.
     * You can make use of this object if you require any low level access to the MongoDB collection,
     * however we recommend you make use of the Iridium methods whereever possible, as we cannot
     * guarantee the accuracy of the type definitions for the underlying MongoDB driver.
     * @public
     * @returns {Collection}
     */
    get collection(): MongoDB.Collection {
        return this.core.connection.collection(this._collection);
    }

    /**
     * Gets the name of the underlying MongoDB collection from which this model's documents are retrieved
     * @public
     */
    get collectionName(): string {
        return this._collection;
    }

    /**
     * Sets the name of the underlying MongoDB collection from which this model's documents are retrieved
     * @public
     */
    set collectionName(value: string) {
        this._collection = value;
    }

    private _cacheDirector: CacheDirector;
    /**
     * Gets the cache controller which dictates which queries will be cached, and under which key
     * @public
     * @returns {CacheDirector}
     */
    get cacheDirector(): CacheDirector {
        return this._cacheDirector;
    }

    private _cache: ModelCache;
    /**
     * Gets the cache responsible for storing objects for quick retrieval under certain conditions
     * @public
     * @returns {ModelCache}
     */
    get cache(): ModelCache {
        return this._cache;
    }

    private _Instance: ModelInterfaces.ModelSpecificInstanceConstructor<TDocument, TInstance>;

    /**
     * Gets the constructor responsible for creating instances for this model
     */
    get Instance(): ModelInterfaces.ModelSpecificInstanceConstructor<TDocument, TInstance> {
        return this._Instance;
    }

    private _transforms: Transforms;

    /**
     * Gets the transforms which are applied whenever a document is received from the database, or
     * prior to storing a document in the database. Tasks such as converting an ObjectID to a string
     * and vice versa are all listed in this object.
     */
    get transforms() {
        return this._transforms;
    }

    private _validators: Skmatc.Validator[];

    /**
     * Gets the custom validation types available for this model. These validators are added to the
     * default skmatc validators, as well as those available through plugins, for use when checking
     * your instances.
     */
    get validators() {
        return this._validators;
    }

    private _indexes: (Index.Index | Index.IndexSpecification)[];

    /**
     * Gets the indexes which Iridium will manage on this model's database collection.
     */
    get indexes() {
        return this._indexes;
    }

    /**
     * Retrieves all documents in the collection and wraps them as instances
     * @param {function(Error, TInstance[])} callback An optional callback which will be triggered when results are available
     * @returns {Promise<TInstance[]>}
     */
    find(): Cursor<TDocument, TInstance>;
    /**
     * Returns all documents in the collection which match the conditions and wraps them as instances
     * @param {Object} conditions The MongoDB query dictating which documents to return
     * @returns {Promise<TInstance[]>}
     */
    find(conditions: { _id?: string; } | Conditions | string): Cursor<TDocument, TInstance>;
    /**
     * Returns all documents in the collection which match the conditions
     * @param {Object} conditions The MongoDB query dictating which documents to return
     * @param {Object} fields The fields to include or exclude from the document
     * @returns {Promise<TInstance[]>}
     */
    find(conditions: { _id?: string; } | Conditions | string, fields: { [name: string]: number }): Cursor<TDocument, TInstance>;
    find(conditions?: { _id?: string; } | Conditions | string, fields?: any): Cursor<TDocument, TInstance> {
        conditions = conditions || {};

        if (!_.isPlainObject(conditions)) conditions = { _id: conditions };
        conditions = this._helpers.convertToDB(conditions);

        let cursor = this.collection.find(conditions);

        if(fields)
            cursor = cursor.project(fields);

        return new Cursor<TDocument, TInstance>(this, conditions, cursor);
    }

    /**
     * Retrieves a single document from the collection and wraps it as an instance
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(id: string, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(conditions: { _id?: string; } | Conditions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(id: string, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    get(conditions: { _id?: string; } | Conditions, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    get(...args: any[]): Bluebird<TInstance> {
        return this.findOne.apply(this, args);
    }

    /**
     * Retrieves a single document from the collection and wraps it as an instance
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(callback?: General.Callback<TInstance>): Bluebird<TInstance|null>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(id: string, callback?: General.Callback<TInstance>): Bluebird<TInstance|null>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(conditions: { _id?: string; } | Conditions, callback?: General.Callback<TInstance>): Bluebird<TInstance|null>;
    /**
     * Retrieves a single document from the collection with the given ID and wraps it as an instance
     * @param {any} id The document's unique _id field value in downstream format
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(id: string, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance|null>;
    /**
     * Retrieves a single document from the collection which matches the conditions
     * @param {Object} conditions The MongoDB query dictating which document to return
     * @param {QueryOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback An optional callback which will be triggered when a result is available
     * @returns {Promise<TInstance>}
     */
    findOne(conditions: { _id?: string; } | Conditions, options: ModelOptions.QueryOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance|null>;
    findOne(...args: any[]): Bluebird<TInstance|null> {
        let conditions: { _id?: any, [key: string]: any }|undefined;
        let options: ModelOptions.QueryOptions|undefined;
        let callback: General.Callback<TInstance>|undefined;

        for (let argI = 0; argI < args.length; argI++) {
            if (typeof args[argI] === "function") callback = callback || args[argI];
            else if (_.isPlainObject(args[argI])) {
                if (conditions) options = args[argI];
                else conditions = args[argI];
            }
            else conditions = { _id: args[argI] };
        }

        conditions = conditions || {};
        options = options || {};

        _.defaults(options, {
            cache: true
        });

        return Bluebird.resolve().bind(this).then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return this._cache.get<TDocument>(conditions);
        }).then((cachedDocument: TDocument) => {
            if (cachedDocument) return cachedDocument;
            return new Bluebird<TDocument|null>((resolve, reject) => {
                let cursor = this.collection.find(conditions);

                if(options!.sort)
                    cursor = cursor.sort(options!.sort!);

                if(typeof options!.skip === "number")
                    cursor = cursor.skip(options!.skip!);

                cursor = cursor.limit(1);

                if(options!.fields)
                    cursor = cursor.project(options!.fields!);

                return cursor.next((err, result) => {
                    if (err) return reject(err);
                    return resolve(<TDocument|null>result);
                });
            });
        }).then((document) => {
            if (!document) return Bluebird.resolve(null);
            return this._handlers.documentReceived(conditions, document, (document, isNew?, isPartial?) => this._helpers.wrapDocument(document, isNew, isPartial), options);
        }).nodeify(callback);
    }

    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument, options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument[], callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    create(objects: TDocument[], options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    create(...args: any[]): Bluebird<any> {
        return this.insert.apply(this, args);
    }

    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts an object into the collection after validating it against this model's schema
     * @param {Object} object The object to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance)} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument, options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance>): Bluebird<TInstance>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {function(Error, TInstance[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument[], callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    /**
     * Inserts the objects into the collection after validating them against this model's schema
     * @param {Object[]} objects The objects to insert into the collection
     * @param {CreateOptions} options The options dictating how this function behaves
     * @param {function(Error, TInstance[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<TInstance>}
     */
    insert(objects: TDocument[], options: ModelOptions.CreateOptions, callback?: General.Callback<TInstance[]>): Bluebird<TInstance[]>;
    insert(objs: TDocument | TDocument[], ...args: any[]): Bluebird<any> {
        let objects: TDocument[];
        let options: ModelOptions.CreateOptions = {};
        let callback: General.Callback<any>|undefined = undefined;
        if (typeof args[0] === "function") callback = args[0];
        else {
            options = args[0];
            callback = args[1];
        }

        if (Array.isArray(objs))
            objects = <TDocument[]>objs;
        else
            objects = <TDocument[]>[objs];

        options = options || {};
        _.defaults(options, <ModelOptions.CreateOptions>{
            w: "majority",
            forceServerObjectId: true
        });

        return Bluebird.resolve().then(() => {
            let queryOptions = { w: options.w, upsert: options.upsert, new: true };

            if (options.upsert) {
                let docs = this._handlers.creatingDocuments(objects);
                return docs.map((object: { _id: any; }) => {
                    return new Bluebird<any[]>((resolve, reject) => {
                        this.collection.findOneAndUpdate({ _id: object._id || { $exists: false }}, object, {
                            upsert: options.upsert,
                            returnOriginal: false
                        }, (err, result) => {
                            if (err) return reject(err);
                            return resolve(result.value);
                        });
                    });
                });
            }
            else
                return this._handlers.creatingDocuments(objects).then(objects => _.chunk(objects, 1000)).map((objects: any[]) => {
                    return new Bluebird<any[]>((resolve, reject) => {
                        this.collection.insertMany(objects, queryOptions, (err, result) => {
                            if (err) return reject(err);
                            return resolve(result.ops);
                        });
                    });
                }).then(results => _.flatten(results));
        }).map((inserted: any) => {
            return this._handlers.documentReceived(null, inserted, (document, isNew?, isPartial?) => this._helpers.wrapDocument(document, isNew, isPartial), { cache: options.cache });
        }).then((results: TInstance[]) => {
            if (Array.isArray(objs)) return results;
            return results[0];
        }).nodeify(callback);
    }

    /**
     * Updates the documents in the backing collection which match the conditions using the given update instructions
     * @param {Object} conditions The conditions which determine which documents will be updated
     * @param {Object} changes The changes to make to the documents
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     */
    update(conditions: { _id?: string; } | Conditions | string, changes: Changes, callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Updates the documents in the backing collection which match the conditions using the given update instructions
     * @param {Object} conditions The conditions which determine which documents will be updated
     * @param {Object} changes The changes to make to the documents
     * @param {UpdateOptions} options The options which dictate how this function behaves
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     */
    update(conditions: { _id?: string; } | Conditions | string, changes: Changes, options: ModelOptions.UpdateOptions, callback?: General.Callback<number>): Bluebird<number>;
    update(conditions: { _id?: string; } | Conditions | string, changes: Changes, options?: ModelOptions.UpdateOptions, callback?: General.Callback<number>): Bluebird<number> {
        if (typeof options === "function") {
            callback = <General.Callback<number>>options;
            options = {};
        }

        const opts = options || {};

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        _.defaults(opts, {
            w: "majority",
            multi: true
        });

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number>((resolve, reject) => {
                const callback = (err: Error, response: MongoDB.UpdateWriteOpResult) => {
                    if (err) return reject(err);

                    // New MongoDB 2.6+ response type
                    if (response.result && response.result.nModified !== undefined) return resolve(response.result.nModified);

                    // Legacy response type
                    return resolve(response.result.n);
                }

                if (opts.multi)
                    return this.collection.updateMany(conditions, changes, opts, callback);

                return this.collection.updateOne(conditions, changes, opts, callback)
            })
        }).nodeify(callback);
    }

    /**
     * Counts the number of documents in the collection
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    count(callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Counts the number of documents in the collection which match the conditions provided
     * @param {Object} conditions The conditions which determine whether an object is counted or not
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    count(conditions: { _id?: string; } | Conditions, callback?: General.Callback<number>): Bluebird<number>;
    count(conds?: any, callback?: General.Callback<number>): Bluebird<number> {
        let conditions: { _id?: string; } | Conditions = conds;
        if (typeof conds === "function") {
            callback = <General.Callback<number>>conds;
            conditions = {};
        }

        conditions = conditions || {};

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number>((resolve, reject) => {
                this.collection.count(conditions, (err, results) => {
                    if (err) return reject(err);
                    return resolve(results);
                });
            });
        }).nodeify(callback);
    }

    /**
     * Removes all documents from the collection
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Removes all documents from the collection which match the conditions
     * @param {Object} conditions The conditions determining whether an object is removed or not
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(conditions: { _id?: string; } | Conditions | any, callback?: General.Callback<number>): Bluebird<number>;
    /**
     * Removes all documents from the collection which match the conditions
     * @param {Object} conditions The conditions determining whether an object is removed or not
     * @param {Object} options The options controlling the way in which the function behaves
     * @param {function(Error, Number)} callback A callback which is triggered when the operation completes
     * @returns {Promise<number>}
     */
    remove(conditions: { _id?: string; } | Conditions | string, options: ModelOptions.RemoveOptions, callback?: General.Callback<number>): Bluebird<number>;
    remove(conds?: any, options?: ModelOptions.RemoveOptions, callback?: General.Callback<number>): Bluebird<number> {
        let conditions: { _id?: string; } | Conditions = conds;

        if (typeof options === "function") {
            callback = <General.Callback<number>>options;
            options = {};
        }

        if (typeof conds === "function") {
            callback = <General.Callback<number>>conds;
            options = {};
            conditions = {};
        }

        conditions = conditions || {};
        options = options || {};

        _.defaults(options, {
            w: "majority"
        });

        if (!_.isPlainObject(conditions)) conditions = {
            _id: conditions
        };

        return Bluebird.resolve().then(() => {
            conditions = this._helpers.convertToDB(conditions);

            return new Bluebird<number|undefined>((resolve, reject) => {
                if(options!.single) return this.collection.deleteOne(conditions, options!, (err, response) => {
                    if (err) return reject(err);
                    return resolve(response.result.n);
                });

                this.collection.deleteMany(conditions, options!, (err, response) => {
                    if (err) return reject(err);
                    return resolve(response.result.n);
                });
            });
        }).then((count) => {
            if (count === undefined) return Bluebird.resolve<number>(0);
            if (count === 1) this._cache.clear(conditions);
            return Bluebird.resolve<number>(count);
        }).nodeify(callback);
    }

    aggregate<T>(pipeline: AggregationPipeline.Stage[]): Bluebird<T[]> {
        return new Bluebird<T[]>((resolve, reject) => {
            this.collection.aggregate(pipeline, (err, results) => {
                if(err) return reject(err);
                return resolve(results);
            });
        });
    }

    /**
     * Runs a mapReduce operation in MongoDB and returns the contents of the resulting collection.
     * @param functions The mapReduce functions which will be passed to MongoDB to complete the operation.
     * @param options Options used to configure how MongoDB runs the mapReduce operation on your collection.
     * @return A promise which completes when the mapReduce operation has written its results to the provided collection.
     */
    mapReduce<Key, Value>(functions: MapReduceFunctions<TDocument, Key, Value>, options: MapReduceOptions): Bluebird<MapReducedDocument<Key, Value>[]>;
    /**
     * Runs a mapReduce operation in MongoDB and writes the results to a collection.
     * @param instanceType An Iridium.Instance type whichThe mapReduce functions which will be passed to MongoDB to complete the operation.
     * @param options Options used to configure how MongoDB runs the mapReduce operation on your collection.
     * @return A promise which completes when the mapReduce operation has written its results to the provided collection.
     */
    mapReduce<Key, Value>(instanceType: InstanceImplementation<MapReducedDocument<Key, Value>, any>,
        options: MapReduceOptions): Bluebird<void>;
    mapReduce<Key, Value>(functions: InstanceImplementation<MapReducedDocument<Key, Value>, any> |
        MapReduceFunctions<TDocument, Key, Value>, options: MapReduceOptions) {
        type fn = MapReduceFunctions<TDocument, Key, Value>;
        type instance = InstanceImplementation<MapReducedDocument<Key, Value>, any>

        if ((<fn>functions).map) {
            return new Bluebird<MapReducedDocument<Key, Value>[]>((resolve, reject) => {
                if (options.out && options.out != "inline")
                    return reject(new Error("Expected inline mapReduce output mode for this method signature"));
                let opts = <MongoDB.MapReduceOptions>options;
                opts.out = { inline: 1 };
                this.collection.mapReduce((<fn>functions).map, (<fn>functions).reduce, opts, function (err, data) {
                    if (err) return reject(err);
                    return resolve(data);
                });
            })
        }
        else {
            let instanceType = <instance>functions;
            return new Bluebird<void>((resolve, reject) => {
                if (options.out && options.out == "inline")
                    return reject(new Error("Expected a non-inline mapReduce output mode for this method signature"));
                if (!instanceType.mapReduceOptions)
                    return reject(new Error("Expected mapReduceOptions to be specified on the instance type"));
                let opts = <MongoDB.MapReduceOptions>options;
                let out : {[op: string]: string} = {};
                out[(<string>options.out)] = instanceType.collection;
                opts.out = out;
                this.collection.mapReduce(instanceType.mapReduceOptions.map, instanceType.mapReduceOptions.reduce, opts, (err, data) => {
                    if (err) return reject(err);
                    return resolve();
                });
            })
        }
    }

    /**
     * Ensures that the given index is created for the collection
     * @param {Object} specification The index specification object used by MongoDB
     * @param {function(Error, String)} callback A callback which is triggered when the operation completes
     * @returns {Promise<String>} The name of the index
     */
    ensureIndex(specification: Index.IndexSpecification, callback?: General.Callback<string>): Bluebird<string>;
    /**
     * Ensures that the given index is created for the collection
     * @param {Object} specification The index specification object used by MongoDB
     * @param {MongoDB.IndexOptions} options The options dictating how the index is created and behaves
     * @param {function(Error, String)} callback A callback which is triggered when the operation completes
     * @returns {Promise<String>} The name of the index
     */
    ensureIndex(specification: Index.IndexSpecification, options: MongoDB.IndexOptions, callback?: General.Callback<string>): Bluebird<string>;
    ensureIndex(specification: Index.IndexSpecification, options?: MongoDB.IndexOptions, callback?: General.Callback<string>): Bluebird<string> {
        if (typeof options === "function") {
            callback = <General.Callback<any>>options;
            options = {};
        }

        return new Bluebird<string>((resolve, reject) => {
            this.collection.createIndex(specification, options || {}, (err: Error, name: any) => {
                if (err) return reject(err);
                return resolve(name);
            });
        }).nodeify(callback);
    }

    /**
     * Ensures that all indexes defined in the model's options are created
     * @param {function(Error, String[])} callback A callback which is triggered when the operation completes
     * @returns {Promise<String[]>} The names of the indexes
     */
    ensureIndexes(callback?: General.Callback<string[]>): Bluebird<string[]> {
        return Bluebird.resolve(this._indexes).map((index: Index.Index | Index.IndexSpecification) => {
            return this.ensureIndex((<Index.Index>index).spec || <Index.IndexSpecification>index, (<Index.Index>index).options || {});
        }).nodeify(callback);
    }

    /**
     * Drops the index with the specified name if it exists in the collection
     * @param {String} name The name of the index to remove
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the index was dropped
     */
    dropIndex(name: string, callback?: General.Callback<boolean>): Bluebird<boolean>;
    /**
     * Drops the index if it exists in the collection
     * @param {IndexSpecification} index The index to remove
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the index was dropped
     */
    dropIndex(index: Index.IndexSpecification, callback?: General.Callback<boolean>): Bluebird<boolean>;
    dropIndex(specification: string | Index.IndexSpecification, callback?: General.Callback<boolean>): Bluebird<boolean> {
        let index: string|undefined;

        if (typeof (specification) === "string") index = <string>specification;
        else {
            index = _(<Index.IndexSpecification>specification).map((direction: number, key: string) => `${key}_${direction}`).reduce<string>((x, y) => `${x}_${y}`);
        }

        if (!index)
            return Bluebird.reject(new Error("Expected a valid index name to be provided"));

        return new Bluebird<boolean>((resolve, reject) => {
            this.collection.dropIndex(index!, (err: Error, result: { ok: number }) => {
                if (err) return reject(err);
                return resolve(<any>!!result.ok);
            });
        }).nodeify(callback);
    }

    /**
     * Removes all indexes (except for _id) from the collection
     * @param {function(Error, Boolean)} callback A callback which is triggered when the operation completes
     * @returns {Promise<Boolean>} Whether the indexes were dropped
     */
    dropIndexes(callback?: General.Callback<boolean>): Bluebird<boolean> {
        return new Bluebird<any>((resolve, reject) => {
            this.collection.dropIndexes((err, count) => {
                if (err) return reject(err);
                return resolve(count);
            });
        }).nodeify(callback);
    }
}