// 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. // ------ const each = require('lodash/each'); const extend = require('lodash/extend'); const assign = require('lodash/assign'); const toArray = require('lodash/toArray'); const helpers = require('../util/helpers'); const { isString, isFunction, isObject } = require('../util/is'); class TableBuilder { constructor(client, method, tableName, tableNameLike, fn) { this.client = client; this._fn = fn; this._method = method; this._schemaName = undefined; this._tableName = tableName; this._tableNameLike = tableNameLike; this._statements = []; this._single = {}; if (!tableNameLike && !isFunction(this._fn)) { throw new TypeError( 'A callback function must be supplied to calls against `.createTable` ' + 'and `.table`' ); } } setSchema(schemaName) { this._schemaName = schemaName; } // Convert the current tableBuilder object "toSQL" // giving us additional methods if we're altering // rather than creating the table. toSQL() { if (this._method === 'alter') { extend(this, AlterMethods); } // With 'create table ... like' callback function is useless. if (this._fn) { this._fn.call(this, this); } return this.client.tableCompiler(this).toSQL(); } // The "timestamps" call is really just sets the `created_at` and `updated_at` columns. timestamps(useTimestamps, defaultToNow, useCamelCase) { if (isObject(useTimestamps)) { ({ useTimestamps, defaultToNow, useCamelCase } = useTimestamps); } const method = useTimestamps === true ? 'timestamp' : 'datetime'; const createdAt = this[method](useCamelCase ? 'createdAt' : 'created_at'); const updatedAt = this[method](useCamelCase ? 'updatedAt' : 'updated_at'); if (defaultToNow === true) { const now = this.client.raw('CURRENT_TIMESTAMP'); createdAt.notNullable().defaultTo(now); updatedAt.notNullable().defaultTo(now); } } // Set the comment value for a table, they're only allowed to be called // once per table. comment(value) { if (typeof value !== 'string') { throw new TypeError('Table comment must be string'); } 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. foreign(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; }, deferrable: (type) => { const unSupported = [ 'mysql', 'mssql', 'redshift', 'mysql2', 'oracledb', ]; if (unSupported.indexOf(this.client.dialect) !== -1) { throw new Error(`${this.client.dialect} does not support deferrable`); } foreignData.deferrable = type; return returnObj; }, _columnBuilder(builder) { extend(builder, returnObj); returnObj = builder; return builder; }, }; return returnObj; } check(checkPredicate, bindings, constraintName) { this._statements.push({ grouping: 'checks', args: [checkPredicate, bindings, constraintName], }); return this; } } [ // 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', ].forEach((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) { methods.forEach(function (method) { TableBuilder.prototype[method] = function (value) { if (this.client.dialect !== dialect) { throw new Error( `Knex only supports ${method} statement with ${dialect}.` ); } if (this._method === 'alter') { throw new Error( `Knex does not support altering the ${method} outside of create ` + `table, please use knex.raw statement.` ); } this._single[method] = value; }; }); }); helpers.addQueryContext(TableBuilder); // 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', // Geometry 'geometry', 'geography', 'point', // 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. columnTypes.forEach((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; }; }); 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() { // arguments[0] = useCamelCase return this.dropColumns( arguments[0] === true ? ['createdAt', 'updatedAt'] : ['created_at', 'updated_at'] ); }, setNullable(column) { this._statements.push({ grouping: 'alterTable', method: 'setNullable', args: [column], }); return this; }, check(checkPredicate, bindings, constraintName) { this._statements.push({ grouping: 'alterTable', method: 'check', args: [checkPredicate, bindings, constraintName], }); }, dropChecks() { this._statements.push({ grouping: 'alterTable', method: 'dropChecks', args: toArray(arguments), }); }, dropNullable(column) { this._statements.push({ grouping: 'alterTable', method: 'dropNullable', args: [column], }); return this; }, // 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; }; TableBuilder.extend = (methodName, fn) => { if ( Object.prototype.hasOwnProperty.call(TableBuilder.prototype, methodName) ) { throw new Error( `Can't extend TableBuilder with existing method ('${methodName}').` ); } assign(TableBuilder.prototype, { [methodName]: fn }); }; module.exports = TableBuilder;