2025-03-23 21:00:08 +01:00

377 lines
9.2 KiB
JavaScript

// 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;