UNPKG

iridium

Version:

A custom lightweight ORM for MongoDB designed for power-users

388 lines (342 loc) 16.6 kB
import {Core} from './Core'; import {Model} from './Model'; import {Plugin} from './Plugins'; import {CacheDirector} from './CacheDirector'; import * as General from './General'; import * as ModelInterfaces from './ModelInterfaces'; import * as Index from './Index'; import {Schema} from './Schema'; import {Transforms} from './Transforms'; import _ = require('lodash'); import MongoDB = require('mongodb'); import Bluebird = require('bluebird'); import Skmatc = require('skmatc'); /** * The default Iridium Instance implementation which provides methods for saving, refreshing and * removing the wrapped document from the collection, as well as integrating with Omnom, our * built in document diff processor which allows clean, atomic, document updates to be performed * without needing to write the update queries yourself. * * @param TDocument The interface representing the structure of the documents in the collection. * @param TInstance The type of instance which wraps the documents, generally the subclass of this class. * * This class will be subclassed automatically by Iridium to create a model specific instance * which takes advantage of some of v8's optimizations to boost performance significantly. * The instance returned by the model, and all of this instance's methods, will be of type * TInstance - which should represent the merger of TSchema and IInstance for best results. */ export class Instance<TDocument extends { _id?: any }, TInstance> { /** * Creates a new instance which represents the given document as a type of model * @param model The model that dictates the collection the document originated from as well as how validations are performed. * @param document The document which should be wrapped by this instance * @param isNew Whether the document is new (doesn't exist in the database) or not * @param isPartial Whether the document has only a subset of its fields populated * */ constructor(model: Model<TDocument, TInstance>, document: TDocument, isNew: boolean = true, isPartial: boolean = false) { this._model = model; this._isNew = !!isNew; this._isPartial = isPartial; this._original = document; this._modified = _.cloneDeep<TDocument>(document); _.each(model.core.plugins,(plugin: Plugin) => { if (plugin.newInstance) plugin.newInstance(this, model); }); } private _isNew: boolean; private _isPartial: boolean; private _model: Model<TDocument, TInstance>; private _original: TDocument; private _modified: TDocument; /** * Gets the underlying document representation of this instance */ get document(): TDocument { return this._modified; } [name: string]: any; /** * A function which is called whenever a new document is in the process of being inserted into the database. * @param document The document which will be inserted into the database. */ static onCreating: (document: { _id?: any }) => void; /** * A function which is called whenever a document of this type is received from the database, prior to it being * wrapped by an Instance object. * @param document The document that was retrieved from the database. */ static onRetrieved: (document: { _id?: any }) => void; /** * A function which is called whenever a new instance has been created to wrap a document. * @param instance The instance which has been created. */ static onReady: (instance: Instance<{ _id?: any }, Instance<{ _id?: any }, any>>) => void; /** * A function which is called whenever an instance's save() method is called to allow you to interrogate and/or manipulate * the changes which are being made. * * @param instance The instance to which the changes are being made * @param changes The MongoDB change object describing the changes being made to the document. */ static onSaving: (instance: Instance<{ _id?: any }, Instance<{ _id?: any }, any>>, changes: any) => void; /** * The name of the collection into which documents of this type are stored. */ static collection: string; /** * The schema used to validate documents of this type before being stored in the database. */ static schema: Schema = { _id: false }; /** * Additional which should be made available for use in the schema definition for this instance. */ static validators: Skmatc.Validator[] = [ Skmatc.create(schema => schema === MongoDB.ObjectID, function(schema, data) { return this.assert(!data || data instanceof MongoDB.ObjectID || (data._bsontype === 'ObjectID' && data.id)); }, { name: 'ObjectID validation' }) ]; /** * The transformations which should be applied to properties of documents of this type. */ static transforms: Transforms = { }; /** * The cache director used to derive unique cache keys for documents of this type. */ static cache: CacheDirector; /** * The indexes which should be managed by Iridium for the collection used by this type. */ static indexes: (Index.Index | Index.IndexSpecification)[] = []; /** * Saves any changes to this instance, using the built in diff algorithm to write the update query. * @param {function(Error, IInstance)} callback A callback which is triggered when the save operation completes * @returns {Promise<TInstance>} */ save(callback?: General.Callback<TInstance>): Bluebird<TInstance>; /** * Saves the given changes to this instance and updates the instance to match the latest database document. * @param {Object} changes The MongoDB changes object to be used when updating this instance * @param {function(Error, IInstance)} callback A callback which is triggered when the save operation completes * @returns {Promise<TInstance>} */ save(changes: Object, callback?: General.Callback<TInstance>): Bluebird<TInstance>; /** * Saves the given changes to this instance and updates the instance to match the latest database document. * @param {Object} conditions The conditions under which the update will take place - these will be merged with an _id query * @param {Object} changes The MongoDB changes object to be used when updating this instance * @param {function(Error, IInstance)} callback A callback which is triggered when the save operation completes * @returns {Promise<TInstance>} */ save(conditions: Object, changes: Object, callback?: General.Callback<TInstance>): Bluebird<TInstance>; save(...args: any[]): Bluebird<TInstance> { var callback: General.Callback<any> = null; var changes: any = null; var conditions: any = {}; Array.prototype.slice.call(args, 0).reverse().forEach((arg) => { if (typeof arg == 'function') callback = arg; else if (typeof arg == 'object') { if (!changes) changes = arg; else conditions = arg; } }); return Bluebird.resolve().then(() => { conditions = _.cloneDeep(conditions); _.merge(conditions, { _id: this._modified._id }); if (!changes) { var validation = this._model.helpers.validate(this._modified); if (validation.failed) return Bluebird.reject(validation.error).bind(this).nodeify(callback); var original = _.cloneDeep(this._original); var modified = _.cloneDeep(this._modified); changes = this._model.helpers.diff(original, modified); } if (!_.keys(changes).length) return null; return changes; }).then((changes) => { if (!changes && !this._isNew) return changes; return this._model.handlers.savingDocument(<TInstance><any>this, changes).then(() => changes); }).then((changes) => { if (!changes && !this._isNew) return false; if (this._isNew) { return new Bluebird<boolean>((resolve, reject) => { this._model.collection.insertOne(this._modified, { w: 'majority' }, (err, doc) => { if (err) return reject(err); return resolve(<any>!!doc); }); }); } else { return new Bluebird<boolean>((resolve: (changed: boolean) => void, reject) => { this._model.collection.updateOne(conditions, changes, { w: 'majority' }, (err: Error, changed: boolean) => { if(err) { err['conditions'] = conditions; err['changes'] = changes; return reject(err); } return resolve(changed); }); }); } }).catch(err => { err['original'] = this._original; err['modified'] = this._modified; return Bluebird.reject(err); }).then((changed: boolean) => { conditions = { _id: this._modified._id }; if (!changed) return this._modified; return new Bluebird<TDocument>((resolve, reject) => { this._model.collection.findOne(conditions, (err: Error, latest) => { if (err) return reject(err); return resolve(latest); }); }); }).then((latest: TDocument) => { if(!latest) { this._isNew = true; this._original = _.cloneDeep(this._modified); return Bluebird.resolve(<TInstance><any>this); } return this._model.handlers.documentReceived(conditions, latest, (value) => { this._isPartial = false; this._isNew = false; this._modified = value; this._original = _.cloneDeep(value); return <TInstance><any>this; }); }).nodeify(callback); } /** * Updates this instance to match the latest document available in the backing collection * @param {function(Error, IInstance)} callback A callback which is triggered when the update completes * @returns {Promise<TInstance>} */ update(callback?: General.Callback<TInstance>): Bluebird<TInstance> { return this.refresh(callback); } /** * Updates this instance to match the latest document available in the backing collection * @param {function(Error, IInstance)} callback A callback which is triggered when the update completes * @returns {Promise<TInstance>} */ refresh(callback?: General.Callback<TInstance>): Bluebird<TInstance> { var conditions = { _id: this._original._id }; return Bluebird.resolve().then(() => { return new Bluebird<TDocument>((resolve, reject) => { this._model.collection.findOne(conditions,(err: Error, doc: any) => { if (err) return reject(err); return resolve(doc); }); }); }).then((newDocument) => { if (!newDocument) { this._isPartial = true; this._isNew = true; this._original = _.cloneDeep<TDocument>(this._modified); return <Bluebird<TInstance>><any>this; } return this._model.handlers.documentReceived(conditions, newDocument, (doc) => { this._isNew = false; this._isPartial = false; this._original = doc; this._modified = _.cloneDeep<TDocument>(doc); return <TInstance><any>this; }); }).nodeify(callback); } /** * Removes this instance's document from the backing collection * @param {function(Error, IInstance)} callback A callback which is triggered when the operation completes * @returns {Promise<TInstance>} */ delete(callback?: General.Callback<TInstance>): Bluebird<TInstance> { return this.remove(callback); } /** * Removes this instance's document from the backing collection * @param {function(Error, IInstance)} callback A callback which is triggered when the operation completes * @returns {Promise<TInstance>} */ remove(callback?: General.Callback<TInstance>): Bluebird<TInstance> { var conditions = { _id: this._original._id }; return Bluebird.resolve().then(() => { if (this._isNew) return 0; return new Bluebird<number>((resolve, reject) => { this._model.collection.remove(conditions, { w: 'majority' },(err: Error, removed?: any) => { if (err) return reject(err); return resolve(removed); }); }); }).then((removed) => { if (removed) return this._model.cache.clear(conditions); return false; }).then(() => { this._isNew = true; return <TInstance><any>this; }).nodeify(callback); } /** * Retrieves the first element in an enumerable collection which matches the predicate * @param collection The collection from which to retrieve the element * @param predicate The function which determines whether to select an element * @returns The first element in the array which matched the predicate. */ first<T>(collection: T[], predicate: General.Predicate<T>): T; /** * Retrieves the first element in an enumerable collection which matches the predicate * @param collection The collection from which to retrieve the element * @param predicate The function which determines whether to select an element * @returns The first element in the object which matched the predicate. */ first<T>(collection: { [key: string]: T }, predicate: General.Predicate<T>): T; first<T>(collection: T[]| { [key: string]: T }, predicate: General.Predicate<T>): T { var result = null; _.each(collection,(value: T, key) => { if (predicate.call(this, value, key)) { result = value; return false; } }); return result; } /** * Retrieves a number of elements from an enumerable collection which match the predicate * @param collection The collection from which elements will be plucked * @param predicate The function which determines the elements to be plucked * @returns A new array containing the elements in the array which matched the predicate. */ select<T>(collection: T[], predicate: General.Predicate<T>): T[]; /** * Retrieves a number of elements from an enumerable collection which match the predicate * @param collection The collection from which elements will be plucked * @param predicate The function which determines the elements to be plucked * @returns An object with the properties from the collection which matched the predicate. */ select<T>(collection: { [key: string]: T }, predicate: General.Predicate<T>): { [key: string]: T }; select<T>(collection: T[]| { [key: string]: T }, predicate: General.Predicate<T>): any { var isArray = Array.isArray(collection); var results: any = isArray ? [] : {}; _.each(collection,(value: T, key) => { if (predicate.call(this, value, key)) { if (isArray) results.push(value); else results[key] = value; } }); return results; } /** * Gets the JSON representation of this instance * @returns {TDocument} */ toJSON(): any { return this.document; } /** * Gets a string representation of this instance * @returns {String} */ toString(): string { return JSON.stringify(this.document, null, 2); } }