tablebuilder.js 6.71 KB

// TableBuilder

// Takes the function passed to the "createTable" or "table/editTable"
// functions and calls it with the "TableBuilder" as both the context and
// the first argument. Inside this function we can specify what happens to the
// method, pushing everything we want to do onto the "allStatements" array,
// which is then compiled into sql.
// ------
import { extend, each, toArray, isString, isFunction } from 'lodash'
import * as helpers from '../helpers';

function TableBuilder(client, method, tableName, fn) {
  this.client = client
  this._fn = fn;
  this._method = method;
  this._schemaName = undefined;
  this._tableName = tableName;
  this._statements = [];
  this._single = {};

  if(!isFunction(this._fn)) {
    throw new TypeError(
      'A callback function must be supplied to calls against `.createTable` ' +
      'and `.table`'
    );
  }
}

TableBuilder.prototype.setSchema = function(schemaName) {
  this._schemaName = schemaName;
};

// Convert the current tableBuilder object "toSQL"
// giving us additional methods if we're altering
// rather than creating the table.
TableBuilder.prototype.toSQL = function() {
  if (this._method === 'alter') {
    extend(this, AlterMethods);
  }
  this._fn.call(this, this);
  return this.client.tableCompiler(this).toSQL();
};

each([

  // Each of the index methods can be called individually, with the
  // column name to be used, e.g. table.unique('column').
  'index', 'primary', 'unique',

  // Key specific
  'dropPrimary', 'dropUnique', 'dropIndex', 'dropForeign'

], function(method) {
  TableBuilder.prototype[method] = function() {
    this._statements.push({
      grouping: 'alterTable',
      method,
      args: toArray(arguments)
    });
    return this;
  };
});

// Warn for dialect-specific table methods, since that's the
// only time these are supported.
const specialMethods = {
  mysql: ['engine', 'charset', 'collate'],
  postgresql: ['inherits']
};
each(specialMethods, function(methods, dialect) {
  each(methods, function(method) {
    TableBuilder.prototype[method] = function(value) {
      if (this.client.dialect !== dialect) {
        helpers.warn(`Knex only supports ${method} statement with ${dialect}.`);
      }
      if (this._method === 'alter') {
        helpers.warn(
          `Knex does not support altering the ${method} outside of create ` +
          `table, please use knex.raw statement.`
        );
      }
      this._single[method] = value;
    };
  });
});

// Each of the column types that we can add, we create a new ColumnBuilder
// instance and push it onto the statements array.
const columnTypes = [

  // Numeric
  'tinyint',
  'smallint',
  'mediumint',
  'int',
  'bigint',
  'decimal',
  'float',
  'double',
  'real',
  'bit',
  'boolean',
  'serial',

  // Date / Time
  'date',
  'datetime',
  'timestamp',
  'time',
  'year',

  // String
  'char',
  'varchar',
  'tinytext',
  'tinyText',
  'text',
  'mediumtext',
  'mediumText',
  'longtext',
  'longText',
  'binary',
  'varbinary',
  'tinyblob',
  'tinyBlob',
  'mediumblob',
  'mediumBlob',
  'blob',
  'longblob',
  'longBlob',
  'enum',
  'set',

  // Increments, Aliases, and Additional
  'bool',
  'dateTime',
  'increments',
  'bigincrements',
  'bigIncrements',
  'integer',
  'biginteger',
  'bigInteger',
  'string',
  'json',
  'jsonb',
  'uuid',
  'enu',
  'specificType'
];

// For each of the column methods, create a new "ColumnBuilder" interface,
// push it onto the "allStatements" stack, and then return the interface,
// with which we can add indexes, etc.
each(columnTypes, function(type) {
  TableBuilder.prototype[type] = function() {
    const args = toArray(arguments);
    const builder = this.client.columnBuilder(this, type, args);
    this._statements.push({
      grouping: 'columns',
      builder
    });
    return builder;
  };
});

// The "timestamps" call is really just sets the `created_at` and `updated_at` columns.
TableBuilder.prototype.timestamps = function timestamps() {
  const method = (arguments[0] === true) ? 'timestamp' : 'datetime';
  const createdAt = this[method]('created_at');
  const updatedAt = this[method]('updated_at');
  if (arguments[1] === true) {
    const now = this.client.raw('CURRENT_TIMESTAMP');
    createdAt.notNullable().defaultTo(now);
    updatedAt.notNullable().defaultTo(now);
  }
  return;
}

// Set the comment value for a table, they're only allowed to be called
// once per table.
TableBuilder.prototype.comment = function(value) {
  this._single.comment = value;
};

// Set a foreign key on the table, calling
// `table.foreign('column_name').references('column').on('table').onDelete()...
// Also called from the ColumnBuilder context when chaining.
TableBuilder.prototype.foreign = function(column, keyName) {
  const foreignData = {column: column, keyName: keyName};
  this._statements.push({
    grouping: 'alterTable',
    method: 'foreign',
    args: [foreignData]
  });
  let returnObj = {
    references(tableColumn) {
      let pieces;
      if (isString(tableColumn)) {
        pieces = tableColumn.split('.');
      }
      if (!pieces || pieces.length === 1) {
        foreignData.references = pieces ? pieces[0] : tableColumn;
        return {
          on(tableName) {
            if (typeof tableName !== 'string') {
              throw new TypeError(`Expected tableName to be a string, got: ${typeof tableName}`);
            }
            foreignData.inTable = tableName;
            return returnObj;
          },
          inTable() {
            return this.on.apply(this, arguments);
          }
        };
      }
      foreignData.inTable = pieces[0];
      foreignData.references = pieces[1];
      return returnObj;
    },
    withKeyName(keyName) {
      foreignData.keyName = keyName;
      return returnObj;
    },
    onUpdate(statement) {
      foreignData.onUpdate = statement;
      return returnObj;
    },
    onDelete(statement) {
      foreignData.onDelete = statement;
      return returnObj;
    },
    _columnBuilder(builder) {
      extend(builder, returnObj);
      returnObj = builder;
      return builder;
    }
  };
  return returnObj;
}

const AlterMethods = {

  // Renames the current column `from` the current
  // TODO: this.column(from).rename(to)
  renameColumn(from, to) {
    this._statements.push({
      grouping: 'alterTable',
      method: 'renameColumn',
      args: [from, to]
    });
    return this;
  },

  dropTimestamps() {
    return this.dropColumns(['created_at', 'updated_at']);
  }

  // TODO: changeType
};

// Drop a column from the current table.
// TODO: Enable this.column(columnName).drop();
AlterMethods.dropColumn =
AlterMethods.dropColumns = function() {
  this._statements.push({
    grouping: 'alterTable',
    method: 'dropColumn',
    args: toArray(arguments)
  });
  return this;
};


export default TableBuilder;