Source: model-instance.js

/**
 * Vertebrae Inc
 * @package Cassandra-ORM
 * @exports Model
 */
"use strict";

const format = require('util').format;

const defaultTypeMap = {
    set: Array,
    list: Array,
    map: Object
};
/**
 * Creates a new ModelInstance object based on your model's schema, this
 * class extends the model.Factory class object, which is what's returned
 * everytime you instantiate a new Model(ColumnFamily)
 * @memberof Cassandra.Model
 * @param {object} dataObject - an object containing column:values that will initialize the instance
 * @param {object} model - the model this instance is based off of
 */
class ModelInstance {

    constructor(object, model, bypass) {
        var instance = this;
        //keep these hidden from enumerators
        Object.defineProperties(instance, {
            object: {
                value: object || {},
                writable: false
            },
            model: {
                value: model,
                writable: false
            },
            views: {
                value: model.views,
                writable: false
            },
            __$$synced: {
                value: false,
                writable: true,
                enumerable: false
            }
        });

        instance._prepareObject(bypass);
    }

    /**
     * This is called at instantiation of each new ModelInstance
     * and will store the qualified data in a secret place.
     * This data store and the instance's enumerable properties(columns)
     * is updated on MOST successful actions except for when deleting
     * the entire object from the table, then the data is left intact.
     * {@link Cassandra.Model.ModelInstance#restore|restore()} will restore the instances
     * column values to the values in the data store;
     * @params {boolean} bypass - do not check defaults/required, just set object's values
     */
    _prepareObject(bypass) {
        var instance = this,
            save = {},
            object = instance.object,
            model = instance.model,
            schema = model.schema,
            columns = schema.columns,
            columnMap = schema.columnMap,
            required = schema.required,
            defaults = schema.defaults;
        for (let column of columns) {
            //if bypass, use mapped key for result parsing
            let mappedKey = columnMap[column];
            let type = schema.model[column];
            let objectValue = bypass ? object[mappedKey] : object[column];
            if (bypass) {
                if (!objectValue && defaultTypeMap[type]) {
                    objectValue = new defaultTypeMap[type]();
                }
                instance[column] = save[column] = objectValue;
                continue;
            }
            if (undefined !== objectValue) {
                save[column] = objectValue;
            } else {
                let defaultVal = defaults[column];
                //if a default is set and no value is provided
                if (undefined !== defaultVal) {
                    //if it has a apply property
                    if (typeof defaultVal === 'function') {
                        save[column] = defaultVal(object);
                    } else {
                        save[column] = defaultVal;
                    }
                } else if (!bypass && required[column]) {
                    throw new Error(format(
                        'Column value is required > column: %s, columnFamily: %s',
                        column,
                        model.name
                    ));
                } else if (!objectValue && defaultTypeMap[type]) {
                    save[column] = new defaultTypeMap[type]();
                }
            }
            instance[column] = save[column];
        }
        Object.defineProperty(instance, '__$$object', {
            value: save,
            writable: false
        });
    }

    /**
     * Instanced Models have the ability to rollback failed
     * writes, for example, if you attempt to "model.save(...)"
     * and this produces an errror, in your callback you can
     * then determine if you wish to restore the object data
     * model back to the values it has saved from the last sync
     * with your database or continue on with the local data
     * @example <caption>Restore last data synced</caption>
     * //user schema: { name: 'text' }, { primaryKeys: [ 'name' ] }
     * user = new TestModel({name: 'foo'});
     * //we should not be able to update a primary key
     * user.name = 'bar';
     * user.save((err) => {
     *   if (err) {
     *     //user.name == 'bar'
     *     user.restore();
     *     //user.name == 'foo'
     *   }
     * });
     */
    restore() {
        var instance = this;
        var data = instance.__$$object;
        for (let column in data) {
            instance[column] = data[column];
        }
    }

    /**
     * Save the current instance's data model to the table
     * this will create a new row if you have altered a primary key
     * or are saving an object for the first time with {@link Cassandra.Model.ModelInstance#save|save()}
     * @param {function} callback - receives err
     */
    sync(callback) {
        var instance = this;
        var model = instance.model;
        var schema = model.schema;
        var data = instance.__$$object;
        var columns = schema.columns;
        var insert = {};
        for (let column of columns) {
            insert[column] = instance[column];
        }
        model.insert(insert, (err, res) => {
            if (!err) {
                //insert local data
                for (let column in insert) {
                    data[column] = insert[column];
                }
                instance.__$$synced = true;
            }
            callback(err);
        });
    }

    /**
     * Save values altered since instantiation
     * @param {function} callback - receives err
     */
    save(callback) {
        var instance = this;
        var synced = instance.__$$synced;
        if (!synced) {
            return instance.sync(callback);
        }
        var model = instance.model;
        var primaries = model.primaries;
        var schema = model.schema;
        var data = instance.__$$object;
        var columns = schema.columns;
        var update = {};
        var where = {};
        for (let column of columns) {
            if (primaries[column]) {
                where[column] = data[column];
            }
            if (data[column] !== instance[column]) {
                update[column] = instance[column];
            }
        }
        model.update(where, update, (err, res) => {
            if (!err) {
                //update local data
                for (let column in update) {
                    data[column] = update[column];
                }
            }
            callback(err);
        });
    }

    /**
     * Delete columns or entire rows from a model instance. When deleting
     * a deleteObject of columns, the projected columns will be set to null
     * on the instance and the data store. This is essentially the same as
     * setting values to null and using save.
     * Currently, this method doesn't support using timestamps but it will soon
     * @todo support @usingTimestamp/delete options
     * @param {array} deleteObject - an array of columns to delete [optional]
     * @param {function} callback - receives err
     */
    delete(deleteObject, callback) {
        var instance = this;
        var model = instance.model;
        var data = instance.__$$object;
        var keys = model.primaryKeys;
        var where = {};
        if (deleteObject && typeof deleteObject === 'function') {
            callback = deleteObject;
            deleteObject = null;
        }
        var callbackHandler = (err, result) => {
                if (!err) {
                    instance.__$$synced = false;
                    //only update deleteObject fields, leave data intact
                    if (deleteObject) {
                        for (let column in deleteObject) {
                            data[column] = null;
                        }
                    }
                }
                callback(err);
            };
        for (let key of keys) {
            where[key] = data[key];
        }
        if (deleteObject) {
            model.delete(where, deleteObject, callbackHandler);
        } else {
            model.delete(where, callbackHandler);
        }
    }
}


module.exports = ModelInstance;