Source: schema.js

/**
 * Vertebrae Inc
 * @package Cassandra-ORM
 * @imports cassandra-driver
 * @exports Schema
 */
"use strict";

const types = require('./cassandra').types;
const dataTypes = types.dataTypes;
const format = require('util').format;

/**
 * Create a new Cassandra Schema to be attached to the models
 * @memberof Cassandra
 * @param {object} columns - A list of key:values representative of field:type
 * @param {object} options - A list of schema specific options, primary/composite keys
 * @example <caption>Create a new Schema Object</caption>
 * var testSchema = new Cassandra.Schema({
 *   id: {default: Cassandra.uuid, required: true},
 *   name: 'text',
 *   age: 'int',
 *   username: 'text'
 * }, {
 *   primaryKeys: ['username'],
 *   views: {
 *     byName: {
 *       primaryKeys: ['name'],//, 'username'], is implied
 *       orderBy: {
 *         name: 'desc',
 *         username: 'asc'
 *       }
 *     }
 *   }
 * });
 */
class Schema {

    constructor(columns, options) {
        options = options || {};
        var schema = this;
        schema.model = {};
        schema.statics = {};
        schema.options = options;
        schema.required = {}; //list of required columns
        schema.defaults = {}; //list of default functions
        schema.columns = []; //list of columns as they are mapped
        schema.columnIndex = []; //list of columns as they are stored
        schema.columnMap = {}; //maps case-sensitive model fields
        schema.dataMap = {}; //maps case-sensitive model fields
        schema.collections = {}; //maps set,map,list collection types
        if (!options.primaryKeys || !Array.isArray(options.primaryKeys) || options.primaryKeys.length === 0) {
            throw new Error('Schema expects to have option "primaryKeys" of type array');
        }
        schema._prepareColumns(columns);
        schema._qualifyPrimaryKeys();
        if (options.views) {
            schema._qualifyViews();
        }
    }

    qualifyType(type) {
        //if type is object instead of string, then get the type property
        if (!dataTypes[type]) {
            throw new TypeError('Cassandra data type not supported: "' + type + '"');
        }
    }
    /**
     * Prepare Schema columns and validate their types this will set the schema's
     * columns array list and set the "default", "required", and "type" settings
     * for the columns, populating {@link Cassandra.Schema.columns|schema.columns},
     * {@link Cassandra.Schema.required}, {@link Cassandra.Schema.defaults}.
     * This will check the list of types against the {@link Cassandra.types.dataTypes}
     * @param {object} columns - A list of key:values representative of field:type
     * @see {@link https://github.com/datastax/nodejs-driver/blob/master/lib/types/index.js|CassandraDriver.types}
     */
    _prepareColumns(columns) {
        if (!columns) {
            throw new Error('Must provide an object structure of your schema');
        }
        var schema = this,
            column,
            type;
        for (let field in columns) {
            column = columns[field];
            type = column.type || column;
            //assume type is an object of set,map,list
            if (type && !type.length) {
                //if type is object instead of string, then get the type property
                let typeObj = type;
                if (typeObj.set) {
                    type = 'set';
                    schema.qualifyType(typeObj.set);
                } else if (typeObj.list) {
                    type = 'list';
                    schema.qualifyType(typeObj.list);
                } else if (typeObj.map) {
                    type = 'map';
                    typeObj.map.forEach(schema.qualifyType);
                }
                schema.collections[field] = typeObj[type];
            }
            schema.qualifyType(type);
            schema.model[field] = type;
            if (column.required) {
                schema.required[field] = 1;
            }
            if (undefined !== column.default) {
                schema.defaults[field] = column.default;
            }
            let mapping = field.toLowerCase();
            schema.columns.push(field);
            schema.columnIndex.push(mapping);
            schema.columnMap[field] = mapping;
            schema.dataMap[mapping] = field;
        }
        schema.columns = schema.columns.sort();
    }

    /**
     * Validates each view defined by the schema's options
     * @param {object} views - an object of defined views
     * @throws Error - invalid view settings
     * @returns {undefined}
     */
    _qualifyViews(views) {
        var schema = this;
        views = views || schema.options.views;
        for (let viewName in views) {
            schema.qualifyView(viewName, views[viewName]);
        }
    }

    /**
     * Qualify a single view by name
     * @param {string} viewName - the name of the materialized view, this will be convert to <columnFamilyName>___<viewName>
     * @param {object} viewConfig - a valid view configuration
     * @returns {boolean} true - if successful
     * @throws Error - invalid view settings
     */
    qualifyView(viewName, viewConfig) {
        var schema = this;
        var model = schema.model;
        var view = viewConfig;
        if (!view.select && !view.primaryKeys) {
            throw new Error(format('Views must specify at least a "primaryKeys" '
                + 'property to create from table; view: %s', viewName));
        }
        if (view.select) {
            for (let column of view.select) {
                if (!model[column]) {
                    throw new Error(format(
                        'Could not add materialized view, undefined '
                        + 'column in select array; view: %s, column: %s',
                        viewName,
                        column
                    ));
                }
            }
        }
        if (view.primaryKeys) {
            schema._qualifyPrimaryKeys(view.primaryKeys);
        }
        if (view.orderBy) {
            for (let column in view.orderBy) {
                if (!model[column]) {
                    throw new Error(format(
                        'Could not add materialized view, undefined '
                        + 'column in primaryKeys array; view: %s, column: %s',
                        viewName,
                        column
                    ));
                }
            }
        }
        return true;
    }

    /**
     * Validates keys against the prepared model
     * @param {array} primaryKeys - an array of primary keys
     */
    _qualifyPrimaryKeys(primaryKeys) {
        var schema = this;
        var model = schema.model;
        primaryKeys = primaryKeys || schema.options.primaryKeys;
        //composite
        if (Array.isArray(primaryKeys[0])) {
            primaryKeys = primaryKeys[0].concat(primaryKeys.slice(1));
        }
        for (let key of primaryKeys) {
            if (!model[key]) {
                throw new Error('Invalid Primary Key, column not '
                    + 'found in schema model; @key: ' + key);
            }
        }
    }
}

module.exports = Schema;