lib/instance.js
'use strict';
const _ = require('lodash');
const Promise = require('bluebird');
const util = require('util');
module.exports = Instance;
const Document = require("./document.js");
const Key = require("./key/key.js");
const Operation = require("./operation.js");
const InstanceError = require("./error/instanceError.js");
const KeyError = require("./error/keyError.js");
const ValidationError = require("./error/validationError.js");
const StorageError = require("./error/storageError");
const StorageAdapter = require("./storageAdapter.js");
const HookTypes = require("./hook.js").types;
const associations = require('./associations.js');
const relationType = require('./relationType.js');
/**
* Model Instance constructor & prototype
*
* @constructor
* @extends Document
*
* @param {mixed} data
* @param {Object} [options]
* @param {Key} [options.key] - `Key` instance representing primary key of the document
* @param {boolean} [options.isNewRecord=true]
* @param {CAS} [options.cas] - If the item on the server contains a different CAS value, the operation will fail. Note that if this option is undefined, no comparison will be performed.
*
* @throws {DocumentError}
* @throws {InstanceError}
* @throws {ValidationError}
* @throws {Error}
*/
function Instance(data, options) {
Document.call(this, {
data : data,
key : options.key,
cas : options.cas,
storage : this.Model.storage,
isNewRecord : options.isNewRecord
});
const self = this;
if (!(options.key instanceof Key)) {
throw new InstanceError("`options.key` must be instace of Key");
}
Object.defineProperty(this, "_schemaSettings", {
value: this.Model.options.schemaSettings,
writable: false,
enumerable: false,
configurable: false
});
//Instance which is currently perssisted in a bucket
Object.defineProperty(this, "_original", {
value: null,
writable: true,
enumerable: false,
configurable: false
});
//define `id` (dynamic part of document's `key`) getter property on instance
const idPropName = this._schemaSettings.doc.idPropertyName;
Object.defineProperty(this, idPropName, {
enumerable: true,
get: function() {
return this.getKey().getId();
},
configurable: false
});
const schema = this.Model.options.schema;
if (this.Model._dataHasObjectStructure()
&& !schema.hasOwnProperty('relation')
) {
this.options.data = this.options.data || {};
//bind properties from data schema to this instance so properties are
//accessible through instance.propertyName
bindDataProperties.apply(this, [this, schema.properties]);
//bind internal properties
Object.defineProperty(this.options.data, idPropName, {
enumerable: true,
get: function() {
return self.getKey().getId();
},
configurable: false
});
const typePropName = this._schemaSettings.doc.typePropertyName;
Object.defineProperty(this.options.data, typePropName, {
enumerable: true,
get: function() {
return self.Model.name;
},
configurable: false
});
}
}
//Inherit Document prototype
Instance.prototype = Object.create(Document.prototype);
Instance.prototype.constructor = Instance;
Object.defineProperty(Instance.prototype, "super", {
value: Document.prototype
});
/**
* @name Instance#Model
* @instance
* @type {Model}
*/
/**
* validates & tries to parse data values according to defined schema
*
* @throws {ValidationError}
* @return {undefined}
*/
Instance.prototype.sanitize = function() {
const validator = this.Model.validator;
this.Model.runHooks(HookTypes.beforeValidate, this);
const result = validator.validate(this.Model.name, this.getData());
if (!result) {
let err = validator.errors.shift() || {};
throw new ValidationError(`${err.dataPath} ${err.message}`);
}
this.Model.runHooks(HookTypes.afterValidate, this);
};
/**
* @private
*
* @param {Object} options
* @param {String} options.index - index name
* @return {Document}
*/
Instance.prototype._buildRefDocument = function(options) {
return this.getGeneratedKey().bind(this).then(function(key) {
const doc = new Document({
key: this.Model.buildRefDocKey(null, {
index: options.index,
parse: false
}),
data: key.toString(),
storage: this.getStorageAdapter(),
reference: this
});
return doc;
});
};
/**
* @private
*
* @return {undefined}
*/
Instance.prototype._initRelations = function() {
const idPropName = this._schemaSettings.doc.idPropertyName;
const modelManager = this.Model._modelManager;
const relations = this.Model.relations;
const dataValues = this.getData();
for (let i = 0, len = relations.length; i < len; i++) {
let rel = relations[i];
let deserialize = associations[rel.method || relationType.REF].deserialize;
//model's schema can be set up in a way that it only contains
//relation to another Document (Model) at root of the data schema.
//In that case "rel.path === null" and
//this.getData() == associated object
//eg: this.getData() == {"_id": "User_some-unique-id-of-associated-doc"}
let relData = rel.path.length ? _.get(dataValues, rel.path) : dataValues;
let arrayOfRelations = true;
//unify data interface
if (!Array.isArray(relData)) {
relData = [relData];
arrayOfRelations = false;
}
for (let y = 0, len2 = relData.length; y < len2; y++) {
if (relData[y] instanceof Instance || _.isNil(relData[y])) continue;
let assoc = deserialize.call(this, rel.type, relData[y]);
if (arrayOfRelations) {
relData[y] = assoc;
} else if (!rel.path.length) {
this.setData(assoc);
} else {
_.set(dataValues, rel.path, assoc);
}
}
}
};
/**
* converts object's associations to json objects with single property with value
* of key string
* returns object's serialized data
*
* @override
* @function
* @return {Promise<{mixed}>}
*/
Instance.prototype.getSerializedData = Promise.method(function() {
const relations = this.Model.relations;
const instance = this;
const modelManager = this.Model._modelManager;
if ( !this.Model._dataHasJsonStructure()
&& !_.isPlainObject(this.Model.options.schema.relation)
|| !relations.length
) {
return this._cloneData();
}
let data = this._cloneData();
return Promise.map(relations, function(rel) {
let serialize = associations[rel.method || relationType.REF].serialize;
//model's schema can be set up in a way that it only contains
//relation to another Document (Model) at root of the data schema.
//In that case "rel.path == []" and
//this.getData() == association object
//eg: this.getData() == {"_id": "User_some-unique-id-of-associated-doc"}
let assoc = rel.path.length ? _.get(data, rel.path) : data;
//unify data interface
let arrayOfRelations = true;
if (!Array.isArray(assoc)) {
assoc = [assoc];
arrayOfRelations = false;
}
return Promise.map(assoc, function(association, index) {
let Model = modelManager.get(rel.type);
/* istanbul ignore if */
if (!(association instanceof Model.Instance)) {
//should not happen
throw TypeError(`association is not instanceof ${Model.name}.Instance`);
}
return serialize.call(instance, association).then(function(serializedData) {
if (arrayOfRelations) {
assoc[index] = serializedData;
} else if (!rel.path.length) {
data = serializedData;
} else {
_.set(data, rel.path, serializedData);
}
return null;
});
});
}).then(function() {
//must be explicitly in "then closure function"
return data;
});
});
/**
* populates model's referenced associations
*
* @example
*
* user.populate('friends'); //single path
*
* user.populate([
* 'friends',
* 'apps'
* ]); // multiple paths
*
* user.populate({
* path: 'friends',
* populate: 'friends'
* }); // Populates user's friends and friends of the friends
*
* // You can combine these three styles to describe exactly what you want to populate
*
* @param {String|Array|Object} include
* @param {Object} [options]
* @param {Boolean} [options.skipPopulated=true] - if false, associations will be reloaded even though they've been already loaded
* @param {Boolean} [options.getOrFail=false] - if true, when there is an association which can not be found, the populate method will return rejected promise with the error
*
* @function
* @return {Instance}
*/
Instance.prototype.populate = Promise.method(function(include, options) {
const instance = this;
const defaults = {
skipPopulated: true,
getOrFail: false
};
options = Object.assign(defaults, _.clone(options));
//normalize `include` argument
if (!Array.isArray(include)) {
include = [include];
}
return Promise.map(include, function(path) {
var populate;
var association;
//normalize path
if (_.isPlainObject(path)) {
populate = path.populate;
path = path.path;
}
if (typeof path !== 'string' && !_.isNil(path)) {
throw new InstanceError('Invalid `include` argument received for the `populate` method');
}
if (path === '' || _.isNil(path)) {
association = instance.getData();
} else {
association = _.get(instance.getData(), path);
}
if (association instanceof Array) {
return Promise.map(association, function(assoc) {
return _refreshAssociation(assoc, populate, options);
});
} else if (association instanceof Instance) {
return _refreshAssociation(association, populate, options);
} else {
throw new InstanceError(
instance.Model.name + "'s data " + (path ? 'located at ' + path : '') +
"is not " + instance.Model.name + "'s Instance object"
);
}
}).return(instance);
});
/**
* @private
*/
function _refreshAssociation(assoc, populate, options) {
if ( assoc.hasCAS()
&& !assoc.options.isNewRecord
&& options.skipPopulated
) {
return null;
}
return assoc.refresh()
.then(function(assoc) {
if (!_.isNil(populate)) {
return assoc.populate(populate, options);
}
return assoc;
})
.catch(function(err) {
if ( err instanceof StorageError
&& err.code === StorageAdapter.errorCodes.keyNotFound
&& !options.getOrFail
) {
return null;
}
return Promise.reject(err);
});
}
/**
* @param {Function} [customizer]
*
* @private
* @return {Mixed}
*/
Instance.prototype._cloneData = function(customizer) {
return _.cloneDeepWith(this.getData(), function(val) {
if (customizer instanceof Function) {
let resolved = customizer(val);
if (resolved !== undefined) {
return resolved;
}
}
if (val instanceof Instance) return val;
});
};
/**
* @private
* @return {Mixed}
*/
Instance.prototype._cloneOptions = function() {
return _.cloneDeepWith(this.options, function(val) {
if (val instanceof Key) return val.clone();
});
};
/**
* @return {Instance}
*/
Instance.prototype.clone = function() {
const self = this;
const clone = new this.Model.Instance(this._cloneData(), self._cloneOptions());
clone._original = new this.Model.Instance(
this._cloneData(),
self._cloneOptions()
);
return clone;
};
/**
* override `Document.prototype.setData`
*
* @example
*
* instance.setData({some: 'data'}) // bulk set enumerable properties of provided data object
* instance.setData('username', 'fogine') //writes to `username` property on data object
*
* @param {string} [property]
* @param {mixed} data
* @return {Instance}
*/
Instance.prototype.setData = function() {
if (arguments.length == 1 && _.isPlainObject(arguments[0])) {
let data = arguments[0];
let keys = Object.keys(data);
keys = _.without(keys,
this._schemaSettings.doc.idPropertyName,
this._schemaSettings.doc.typePropertyName
);
for (let i = 0, len = keys.length; i < len; i++) {
this.options.data[keys[i]] = data[keys[i]];
}
return this;
} else {
return this.super.setData.apply(this, arguments);
}
};
/**
* Returns true when property value is modified.
* (differs from what is stored in a bucket)
* When no property path is provided, it checks whether the whole dataset
* is equal to its settled (persisted) counterpart
*
* @example
*
* instance.isDirty();
* // or
* instance.isDirty('path.to.nested.property');
* // or
* instance.isDirty(['path', 'to', 'nested', 'property']);
*
* @param {Array<String>|String} path - property path
* @return {Boolean}
*/
Instance.prototype.isDirty = function(path) {
if (this.options.isNewRecord || this._original === null) {
return true;
} else if (path) {
return _.get(this.getData(), path) !== _.get(this._original.getData(), path);
} else if (this.Model._dataHasJsonStructure()) {
return !_.isEqual(this.getData(), this._original.getData());
} else {
return this.getData() !== this._original.getData();
}
};
/**
* Explicitly fetches itself from upstream.
* Works also with destroyed documents which have been soft-deleted due to paranoid=true option
*
* @return {Promise<Instance>}
*/
Instance.prototype.refresh = function() {
return this.super.refresh.call(
this
).bind(this).then(function(instance) {
//convert timestamps fields to Date objects etc..
this.Model.validator.validate(
this.Model.POST_FETCH_SCHEMA_ID,
this.getData()
);
setOriginal.call(this);
this._initRelations();
return this;
});
};
/**
* @private
* @throws {DocumentError}
* @return {undefined}
*/
function setOriginal() {
if (this._original !== null) {
this._original.setCAS(this.getCAS());
this._original.setData(this._cloneData());
this._original.options.isNewRecord = false;
this._original.options = this._cloneOptions();
this._original.setKey(this.getKey().clone());
}
}
/**
* returns collection of reference {@link Document Documents} of the Model instance.
*
* @function
* @return {Promise<Array<Document>>}
*/
Instance.prototype.getRefDocs = Promise.method(function() {
if (!this.Model._dataHasJsonStructure()) {
return Promise.resolve([]);
}
const refDocs = this.Model.options.indexes.refDocs;
const refDocIndexNames = Object.keys(refDocs);
const out = [];
return Promise.resolve(refDocIndexNames).bind(this).map(function(indexName) {
const refDocOptions = refDocs[indexName];
return this._buildRefDocument({
index: indexName
}).tap(function(doc) {
return doc.getGeneratedKey();
}).then(function(doc) {
out.push(doc);
}).catch(KeyError, function(err) {
if (refDocOptions.required === false) {
return null;
}
return Promise.reject(err);
});
}).then(function(refDocs) {
return out;
});
});
/**
* returns two collections of reference documents one of which are changed `current` and are going to be
* persisted to db and other collection of outdated ref docs `old` which should be removed
* from bucket in order to fulfill update process of a refdoc indexes
*
* @private
* @function
* @return {Object}
*/
Instance.prototype._getDirtyRefDocs = Promise.method(function() {
const self = this;
const out = {
current: [],//collection of current ref-documents which will be persisted to the bucket
old: [] //collection of outdated ref-documents which will be removed from the bucket
};
if (!this.Model._dataHasJsonStructure()) {
return Promise.resolve(out);
}
const refDocs = self.Model.options.indexes.refDocs;
const refDocIndexNames = Object.keys(refDocs);
//pupulate `out` collection
return Promise.map(refDocIndexNames, function(indexName) {
const refDocOptions = refDocs[indexName];
const docPool = {};
//create instance of the reference document with most recent data
//from the Instance
return self._buildRefDocument({
index: indexName
}).bind(docPool).then(function(doc) {
this.doc = doc;
//create instance of the reference document with data which are
//currently persisted in bucket
return self._original._buildRefDocument({
index: indexName
});
}).then(function(oldDoc) {
this.oldDoc = oldDoc;
const keyPool = {
key: this.doc.getGeneratedKey().reflect(),
oldKey: this.oldDoc.getGeneratedKey().reflect()
}
return Promise.props(keyPool);
}).then(function(result) {
const key = this.doc.getKey();
const oldKey = this.oldDoc.getKey();
//if rejected, check if expected error has been throwed, otherwise fail
if( result.oldKey.isRejected()
&& !(result.oldKey.reason() instanceof KeyError)
) {
return Promise.reject(result.oldKey.reason());
}
//if rejected, check if expected error has been throwed, otherwise fail
if( result.key.isRejected()
&& !(result.key.reason() instanceof KeyError)
) {
return Promise.reject(result.key.reason());
}
const required = refDocOptions.required;
if (result.key.isFulfilled() && result.oldKey.isFulfilled()) {
if (key.toString() !== oldKey.toString()) {
out.current.push(this.doc);
out.old.push(this.oldDoc);
}
return out;
} else if (required === false) {
if(!key.isGenerated() && oldKey.isGenerated()) {
out.old.push(this.oldDoc);
} else if(!oldKey.isGenerated() && key.isGenerated()) {
out.current.push(this.doc);
}
return out;
} else {
let reason = null;
if (result.key.isRejected()) {
reason = result.key.reason();
} else {
reason = result.oldKey.reason();
}
return Promise.reject(reason);
}
});
}).return(out);
});
/**
* shortcut method for calling {@link Instance#replace} / {@link Instance#insert}. if fresh instance is initialized
* and the data are not persisted to bucket yet, `insert` is called. If the Instance
* is already persisted, data are updated with `replace` method
*
* @function
* @param {Object} [options] - either {@link StorageAdapter#insert} or {@link StorageAdapter#replace} options depending on whether the document is being saved for the first time
* @return {Promise<Instance>}
*/
Instance.prototype.save = Promise.method(function(options) {
if (this.options.isNewRecord) {
return this.insert(options);
}
return this.replace(options);
});
/**
* This is similar to seting data on the instance and then calling {@link Instance#save},
* (respectively {@link Instance#replace}) however in this case when the operation fails,
* the Model's instance data are restored to the previous state.
*
* @param {Object} data
* @param {Object} [options] - see {@link StorageAdapter#replace} for available options
*
* @function
* @return {Instance}
*/
Instance.prototype.update = Promise.method(function(data, options) {
const self = this;
const backup = this._cloneData();
self.setData(data);
return this.save(options).catch(function(err) {
self.setData(backup);
const currentData = self.getData();
_.difference(
Object.keys(data),
Object.keys(backup)
).forEach(function(propName) {
delete currentData[propName];
});
return Promise.reject(err);
});
});
/**
* deletes the document and all related reference documents from a bucket
*
* @param {Object} [options] - See `StorageAdapter.remove` for available options
* @param {Boolean} [options.force=false] - performs destoy operation even with no `cas` value set
*
* @function
* @return {Promise}
*/
Instance.prototype.destroy = Promise.method(function(options) {
const self = this;
options = options || {};
if (!this.hasCAS() && !options.force) {
return Promise.reject(new InstanceError('Can not destroy a Model instance without the `cas` value being set'));
}
return this.Model.runHooks(HookTypes.beforeDestroy, this, options).then(function() {
return self.getRefDocs();
}).bind({}).then(function(docs) {//remove ref docs
this.docs = docs;
this.timestamps = self._touchTimestamps(undefined, {touchDeletedAt: true});
return self.getStorageAdapter()
.bulkRemoveSync(docs, options);
}).then(function() {//remove the doc
const opt = _.assign({cas: self.getCAS()}, options);
if (self.Model.options.paranoid) {
return self.super.replace.call(self, opt);
}
return self.remove(opt);
}).then(function(result) {// resolve successful destroy operation
if (!self.Model.options.paranoid) {
self.options.isNewRecord = true;
}
return self.Model.runHooks(HookTypes.afterDestroy, self, options).return(self);
}).catch(StorageError, function(err) {// rollback deleted refdocs
let docs = this.docs;
const failedIndex = this.docs.indexOf(err.doc);
if (failedIndex !== -1) {
docs = this.docs.slice(0, failedIndex);
}
return _rollback.call(self, {
err : err,
operation : Operation.REMOVE,
docs : docs,
timestamps : this.timestamps
});
});
});
/**
* saves NEW document (fails if the document already exists in a bucket) to storage
* with all its reference documents, if an error occurs,
* an attempt for rollback is made, if the rollback fails,
* the `afterFailedRollback` hook is triggered.
*
* @param {Object} [options] - See `StorageAdapter.insert` for available options
*
* @function
* @return {Promise<Instance>}
*/
Instance.prototype.insert = Promise.method(function(options) {
const self = this;
let report
, timestampsBck;
//touch timestamps, backup previous timestamps values
timestampsBck = this._touchTimestamps();
//run beforeCreate hooks
return this.Model.runHooks(HookTypes.beforeCreate, this, options).then(function() {
//validate
try {
self.sanitize();
} catch(e) {
self._touchTimestamps(timestampsBck);
return Promise.reject(e);
}
//begin insert process
return self.getRefDocs();
}).bind({}).then(function(docs) {//Insert ref docs
this.docs = docs;
this.timestamps = timestampsBck;
return self.getStorageAdapter().bulkInsertSync(docs, options);
}).then(function() {//Insert the doc
return self.super.insert.call(self, options);
}).then(function(result) {//Resolve successful `insert` operation
self.options.isNewRecord = false;
setOriginal.call(self);
return self.Model.runHooks(HookTypes.afterCreate, self, options).return(self);
}).catch(StorageError, function(err) {//try to rollback inserted refdocs
var docs = this.docs;
const failedIndex = this.docs.indexOf(err.doc);
if (failedIndex !== -1) {
docs = this.docs.slice(0, failedIndex);
}
return _rollback.call(self, {
err : err,
operation : Operation.INSERT,
docs : docs,
timestamps : this.timestamps
});
});
});
/**
* replaces (updates) current document (fails if the document with the key does not exists) in a bucket
* and synchronizes reference documents (indexes). If an error occur,
* an attempt for rollback is made, if the rollback fails,
* the `afterFailedRollback` hook is triggered for every document an atempt for rollback failed (includes failed refdocs operations)
*
* @param {Object} [options] - See `StorageAdapter.replace` for available options
* @param {Boolean} [options.force=false] - performs replace operation even with no `cas` value set
*
* @function
* @return {Promise<Instance>}
*/
Instance.prototype.replace = Promise.method(function(options) {
const self = this;
let timestampsBck;
options = options || {};
if (!this.hasCAS() && !options.force) {
return Promise.reject(new InstanceError('Can not call the replace (update) method on a Model instance without the `cas` value being set'));
}
//touch timestamps, backup previous timestamps values
timestampsBck = this._touchTimestamps();
//run beforeUpdate hooks
return this.Model.runHooks(HookTypes.beforeUpdate, this, options).then(function() {
//validate & sanitize data
try {
self.sanitize();
} catch(e) {
self._touchTimestamps(timestampsBck);
return Promise.reject(e);
}
//begin update process
return self._getDirtyRefDocs();
}).bind({}).then(function(docs) {
this.refDocs = docs.current;
this.oldRefDocs = docs.old;
return null;
}).then(function() {
return self.getStorageAdapter()
.bulkInsertSync(this.refDocs, options);
}).then(function() {
this.timestamps = timestampsBck;
return self.super.replace.call(self, options);
}).then(function(result) {
return self.getStorageAdapter()
.bulkRemove(this.oldRefDocs, options);
}).each(function(result, index, length) {
if (result.isRejected()) {
if (self.Model.listenerCount('afterFailedIndexRemoval')) {
//TODO explain in the documentation that if a listener of this type is attached,
//an user is supposed to handle state of promise resolved/rejected
return self.Model.runHooks(HookTypes.afterFailedIndexRemoval, result.reason());
} else {
return Promise.reject(result.reason());
}
}
return null;
}).then(function() {
setOriginal.call(self);
return self.Model.runHooks(HookTypes.afterUpdate, self, options).return(self);
}).catch(StorageError, function(err) {//BEGIN rollback
var docs = this.refDocs;
const failedIndex = this.refDocs.indexOf(err.doc);
if (failedIndex !== -1) {
docs = this.refDocs.slice(0, failedIndex);
}
return _rollback.call(self, {
err : err,
operation : Operation.REPLACE,
docs : docs,
timestamps : this.timestamps
});
});
});
/**
* @private
*
* @param {Object} [options]
* @param {StorageError} [options.err] - the error which triggered rollback operation
* @param {string} [options.operation] - see `./operation.js` for available values
* @param {Array} [options.docs] - documents which should be restored/removed
* @param {Object} [options.timestamps] - Instance timestamp values will be reverted to these values
* @return {Promise.reject<Error>}
*/
function _rollback(options) {
options.instance = this;
const timestamps = options.timestamps;
delete options.timestamps;
var rollbackMethod;
switch (options.operation) {
case Operation.INSERT:
case Operation.REPLACE:
rollbackMethod = 'bulkRemove';
break;
case Operation.REMOVE:
rollbackMethod = 'bulkInsert';
break;
}
return this.Model.runHooks(HookTypes.beforeRollback, options).bind(options).then(function() {
this.instance._touchTimestamps(timestamps);
const storage = this.instance.getStorageAdapter();
return storage[rollbackMethod](this.docs);
}).each(function(result, index, length) {
if (result.isRejected()) {
this.instance.Model.runHooks(HookTypes.afterFailedRollback, result.reason(), this);
}
return null;
}).then(function() {
return this.instance.Model.runHooks(HookTypes.afterRollback, this);
}).return(Promise.reject(options.err));
};
/**
* touches underlaying document and all its reference documents
* when an Error occurs, it tries to touch all remaining documents before failing
*
* @throws StorageError
* @param {integer} expiry - time in seconds
* @param {Object} options - see {@link StorageAdapter#touch} for available options
* @function
* @return {Promise<Instance>}
*/
Instance.prototype.touch = Promise.method(function(expiry, options) {
var ops = [];
ops.push(this.super.touch.call(this, expiry, options));
return this.getRefDocs().each(function(refDoc) {
ops.push(refDoc.touch(expiry, options));
}).then(function() {
return Promise.all(ops);
}).return(this);
});
/**
* @throws InstanceError
* @return {Object}
*/
Instance.prototype.toJSON = function() {
const data = this._cloneData();
if (this.Model._dataHasObjectStructure()) {
var typePropName = this._schemaSettings.doc.typePropertyName;
delete data[typePropName];
}
return data;
};
/**
* @param {Object} obj - object to which properties will be bind to
* @param {Object} properties
* @param {boolean} [forceRebinding=false] - if it's true, existing properties on the instance are redefined
*
* @private
* @return {undefined}
*
*/
function bindDataProperties(obj, properties, forceRebinding) {
const keys = Object.keys(properties);
for (let i = 0, len = keys.length; i < len; i++) {
let name = keys[i];
if (!obj.hasOwnProperty(name) || forceRebinding === true) {
let prop = properties[name];
Object.defineProperty(obj, name, {
enumerable: true,
configurable: true,
set: function(value) {
this.setData(name, value);
},
get: function() {
return this.getData(name);
}
});
}
}
}
/**
* @private
*
* if `timestamps` object is provided, current timestamps are overwriten with
* `timestamps` provided and old timestamp values are returned.
* if no `timestamps` object is provied, timestamps values are touched and
* old timestamp values are returned
*
* @param {Object} timestamps - [optional]
* @param {Object} [options] - [optional]
* @param {boolean} [options.touchDeletedAt=false] - Applies only if `timestamps`
* parameter is not provided
* @return {Object} - old timestamp values
*/
Instance.prototype._touchTimestamps = function(timestamps, options) {
options = options || {};
if ( !this.Model._dataHasObjectStructure()
|| (!this.Model.options.timestamps && !this.Model.options.paranoid)
|| (options.touchDeletedAt === true && this.Model.options.paranoid !== true)
) {
return;
}
let data = this.getData();
let propNames = this.Model._getTimestampPropertyNames();
var oldTimestamps = {};
if (this.Model.options.timestamps) {
oldTimestamps[propNames.createdAt] = data[propNames.createdAt];
oldTimestamps[propNames.updatedAt] = data[propNames.updatedAt];
}
oldTimestamps[propNames.deletedAt] = data[propNames.deletedAt];
if (_.isPlainObject(timestamps)) {
_.assign(data, timestamps);
return oldTimestamps;
}
const now = new Date();
if (this.Model.options.timestamps) {
if (!data[propNames.createdAt]) {
data[propNames.createdAt] = now;
}
data[propNames.updatedAt] = now;
}
if (options.touchDeletedAt === true) {
if (!data[propNames.deletedAt]) {
data[propNames.deletedAt] = now;
}
}
return oldTimestamps;
};
/*
* @private
* @return {string}
*/
Instance.prototype.inspect = function() {
var key = this.options && this.options.key;
var cas = key && this.options.cas;
var out = '[object CouchbaseInstance:\n';
out += " key: '" + key + "'\n";
out += " cas: " + cas;
out += "]";
return out;
};