class AlterTable { #instance; #database; #name; #structure; constructor(instance, defaultDatabase, name){ this.#instance = instance; this.#database = defaultDatabase; this.#name = name; } /** * Selects a database for this query * @param {String} database - Name of the database * @returns {this} */ selectDatabase = (database) => { this.#database = database; return this; } /** * The new desired structure. * - Drops columns that are existing in the current table but not in this structure * - Adds columns that are missing in the current table * - Modifies all other columns where at least one datatype is not matching * @param {Structure} struc - New structure for the table * @returns */ structure = (struc) => { this.#structure = struc; return this; } execute = async () => { if (!this.#database) throw new Error(`Can't alter table ${this.#name}: Database not selected`); if (!this.#name) throw new Error(`Can't alter table on ${this.#database}: No name given`); if (!this.#instance.isConnected()) throw new Error(`Can't execute query: Instance has no connection`); if (!this.#structure) throw new Error(`Can't alter table ${this.#database}.${this.#name}: No new structure given`); const currentStruc = (await this.#instance.getStructure(this.#name, this.#database)).get(); const newStruc = this.#structure.get(); if (currentStruc.length===0) throw new Error(`Can't alter table ${this.#name}: Table does not exist`); for (let col of currentStruc){ if (!newStruc.find(newCol => newCol.Field === col.Field)){ // DELETE COLUMN await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP COLUMN \`${col.Field}\``); } } for (let col of newStruc){ const oldStrucCol = currentStruc.find((oldCol) => oldCol.Field === col.Field); if (!oldStrucCol){ // ADD COLUMN await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD COLUMN \`${col.Field}\` ${col.Type}${col.Extra?` ${col.Extra}`:""}${col.Null==="YES"?" NULL":" NOT NULL"}${col.Default?` DEFAULT '${col.Default}'`:""}`); if (col.Key){ switch(col.Key){ case "PRI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD PRIMARY KEY (${col.Field});`); break; case "MUL": console.log("HEY"); await this.#instance.queryRaw(`CREATE INDEX ${col.Field} ON ${this.#database}.${this.#name} (${col.Field})`); break; case "UNI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD UNIQUE (${col.Field});`); break; } } continue; } for (let key in col){ if (oldStrucCol[key] !== col[key]){ // UPDATE COLUMN await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} MODIFY COLUMN \`${col.Field}\` ${col.Type}${col.Extra?` ${col.Extra}`:""}${col.Null==="YES"?" NULL":" NOT NULL"}${col.Default?` DEFAULT '${col.Default}'`:""}`); if (oldStrucCol.Key){ switch(oldStrucCol.Key){ case "PRI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP PRIMARY KEY (${col.Field});`); break; case "MUL": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP INDEX ${col.Field};`); break; case "UNI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} DROP UNIQUE (${col.Field});`); break; } } if (col.Key){ switch(col.Key){ case "PRI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD PRIMARY KEY (${col.Field});`); break; case "MUL": await this.#instance.queryRaw(`CREATE INDEX ${col.Field} ON ${this.#database}.${this.#name} (${col.Field})`); break; case "UNI": await this.#instance.queryRaw(`ALTER TABLE ${this.#database}.${this.#name} ADD UNIQUE (${col.Field});`); break; } } break; } } } return await this.#instance.checkStructure(this.#name, newStruc, this.#database); } } class CreateTable { #instance; #database; #name; #structure; constructor(instance, defaultDatabase, name) { this.#instance = instance; this.#database = defaultDatabase; this.#name = name; } /** * Selects a database for this query * @param {String} database - Name of the database * @returns {this} */ selectDatabase = (database) => { this.#database = database; return this; } /** * Sets a new name * @param {String} name */ name = (name) => { this.#name = name; return this; } /** * Defines the structure for the table * @param {Structure} struc - Instance of Structure */ structure = (struc) => { this.#structure = struc; return this; } /** * Executes the prepared query * @returns */ execute = async () => { if (!this.#database) throw new Error(`Can't create table ${this.#name}: Database not selected`); if (!this.#name) throw new Error(`Can't create table on ${this.#database}: No name given`); if (!this.#instance.isConnected()) throw new Error(`Can't execute query: Instance has no connection`); if (!this.#structure) throw new Error(`Can't create table ${this.#database}.${this.#name}: No structure given`); const colObjs = this.#structure.get(); const coltypes = colObjs.map(obj => `\`${obj.Field}\` ${obj.Type}${obj.Null==="YES"?" NULL":" NOT NULL"}${obj.Default?` DEFAULT '${obj.Default}'`:""}${obj.Extra?` ${obj.Extra}`:""}`); const constraints = colObjs.map(obj => `${obj.Key?obj.Key==="PRI"?` PRIMARY KEY (${obj.Field})`:obj.Key==="MUL"?` INDEX (${obj.Field})`:obj.Key==="UNI"?` UNIQUE (${obj.Key})`:"":""}`) .filter(str => str !== ""); // Filter out empty const queryString = `CREATE TABLE ${this.#database}.${this.#name} (${coltypes}${constraints.length>0?",":""}${constraints})`; return await this.#instance.queryRaw(queryString); } } /** * @typedef {Object} ConstraintOptions * @property {Boolean} [primary] - Whether this column should be primary * @property {Boolean} [index] - Whether this column should be indexable (Faster query, slower insertion) * @property {Boolean} [null] - Whether this column is null per default * @property {Boolean} [unique] - Whether this column data should be unique * @property {String} [default] - Set's the default data for this column * @property {Boolean} [auto_increment] - Whether this column should be numerical auto_increment * @property {Boolean} [unsigned] - Only on numerical: Whether this numerical field should be unsigned */ class Structure { #columns = []; constructor(tableDescription) { if (!tableDescription) return; this.#columns = tableDescription; } /** * Drops (Removes) a column name * @param {String} name - The name of the column */ drop = (name) => { let index; for (let i = 0; i < this.#columns.length; i++){ if (this.#columns[i].Field === name) { index=i; break; } } this.#columns.splice(index, 1); return this; } get(){ return this.#columns; } /** * Adds a new column of data type 'char' to this structure * @param {String} name - Name of the column * @param {Number} size - Length of characters. Min 0, Max 255. Default 1 * @param {ConstraintOptions} options - Extra constraint options */ char = (name, size=1, options) => { if (size < 0 || size > 255){ throw new Error(`Column datatype 'char' size must be a number between 0 and 255. Received: ${size}`); } this.#columns.push(parseColumnData(name, `char(${size})`, options)); return this; } /** * Adds a new column of data type 'varchar' to this structure * @param {String} name - Name of the column * @param {Number} size - Length of characters. Min 0, Max 255. Default 1 * @param {ConstraintOptions} options - Extra constraint options */ varchar = (name, size=8, options) => { if (size < 0 || size > 255){ throw new Error(`Column datatype 'varchar' size must be a number between 0 and 65535. Received: ${size}`); } this.#columns.push(parseColumnData(name, `varchar(${size})`, options)); return this; } /** * Adds a new column of data type 'binary' to this structure * @param {String} name - Name of the column * @param {Number} size - Length of data. Min 1 * @param {ConstraintOptions} options - Extra constraint options * @returns */ binary = (name, size=1, options) => { if (size < 1){ throw new Error(`Column datatype 'binary' size must be a number above 0. Received: ${size}`); } this.#columns.push(parseColumnData(name, `binary(${size})`, options)); return this; } /** * Adds a new column of data type 'varbinary' to this structure * @param {String} name - Name of the column * @param {Number} size - Length of data. Min 1 * @param {ConstraintOptions} options - Extra constraint options * @returns */ varbinary = (name, size=1, options) => { if (size < 1){ throw new Error(`Column datatype 'varbinary' size must be a number above 0. Received: ${size}`); } this.#columns.push(parseColumnData(name, `varbinary(${size})`, options)); return this; } /** * Adds a new column of data type 'tinyblob' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ tinyblob = (name, options) => { this.#columns.push(parseColumnData(name, `tinyblob`, options)); return this; } /** * Adds a new column of data type 'tinytext' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ tinytext = (name, options) => { this.#columns.push(parseColumnData(name, `tinytext`, options)); return this; } /** * Adds a new column of data type 'text' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ text = (name, options) => { this.#columns.push(parseColumnData(name, `text`, options)); return this; } /** * Adds a new column of data type 'blob' to this structure * @param {String} name - Name of the column * @param {Number} size - Size in bytes. Min 1, Max 65535. Defaults to 65535 * @param {ConstraintOptions} options - Extra constraint options * @returns */ blob = (name, size=65535, options) => { if (size < 1 || size > 65535) throw new Error(`Column datatype 'blob' size must be a number between 1 and 65535. Received: ${size}`); this.#columns.push(parseColumnData(name, `blob(${size})`, options)); return this; } /** * Adds a new column of data type 'mediumtext' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ mediumtext = (name, options) => { this.#columns.push(parseColumnData(name, `mediumtext`, options)); return this; } /** * Adds a new column of data type 'longtext' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ longtext = (name, options) => { this.#columns.push(parseColumnData(name, `longtext`, options)); return this; } /** * Adds a new column of data type 'longblob' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ longblob = (name, options) => { this.#columns.push(parseColumnData(name, `longblob`, options)); return this; } /** * Adds a new column of data type 'enum' to this structure * @param {String} name - Name of the column * @param {Array} vals - Array of possible values * @param {ConstraintOptions} options - Extra constraint options * @returns */ enum = (name, vals=[], options) => { if (!Array.isArray(vals)) throw new Error(`Column datatype 'enum': 'vals' must be of type Array`); if (vals.length<=0) throw new Error(`Column datatype 'enum' must contain a list of possible values. Received undefined`); this.#columns.push(parseColumnData(name, `enum(${vals.map(val=>`"${val}"`)})`, options)); return this; } /** * Adds a new column of data type 'enum' to this structure * @param {String} name - Name of the column * @param {Array} vals - Array of possible values * @param {ConstraintOptions} options - Extra constraint options * @returns */ set = (name, vals=[], options) => { if (!Array.isArray(vals)) throw new Error(`Column datatype 'set': 'vals' must be of type Array`); if (vals.length<=0) throw new Error(`Column datatype 'set' must contain a list of possible values. Received undefined`); this.#columns.push(parseColumnData(name, `set(${vals.map(val=>`"${val}"`)})`, options)); return this; } /** * Adds a new column of data type 'bit' to this structure * @param {String} name - Name of the column * @param {Number} size - Min 1, Max 64. Default to 1 * @param {ConstraintOptions} options - Extra constraint options * @returns */ bit = (name, size=1, options) => { if (size < 1 || size > 64) throw new Error(`Column datatype 'bit' size must be a number between 1 and 64. Received: ${size}`); this.#columns.push(parseColumnData(name, `bit(${size})`, options)); return this; } /** * Adds a new column of data type 'tinyint' to this structure * @param {String} name - Name of the column * @param {Number} size - Min 1, Max 255. Defaults to 255 * @param {ConstraintOptions} options - Extra constraint options * @returns */ tinyint = (name, size=255, options) => { if (size < 1 || size > 255) throw new Error(`Column datatype 'tinyint' size must be a number between 1 and 255. Received: ${size}`); this.#columns.push(parseColumnData(name, `tinyint(${size})`, options)); return this; } /** * Adds a new column of data type 'bool' to this structure * @param {String} name - Name of the column * @param {ConstraintOptions} options - Extra constraint options * @returns */ bool = (name, options) => { this.#columns.push(parseColumnData(name, `bool`, options)); return this; } /** * Adds a new column of data type 'smallint' to this structure * @param {String} name - Name of the column * @param {Number} size Min 1, Max 255. Defaults to 255 * @param {ConstraintOptions} options - Extra constraint options * @returns */ smallint = (name, size=255, options) => { if (size < 1 || size > 255) throw new Error(`Column datatype 'smallint' size must be a number between 1 and 255. Received: ${size}`); this.#columns.push(parseColumnData(name, `smallint(${size})`, options)); return this; } /** * Adds a new column of data type 'mediumint' to this structure * @param {String} name - Name of the column * @param {Number} size Min 1, Max 255. Defaults to 255 * @param {ConstraintOptions} options - Extra constraint options * @returns */ mediumint = (name, size=255, options) => { if (size < 1 || size > 255) throw new Error(`Column datatype 'mediumint' size must be a number between 1 and 255. Received: ${size}`); this.#columns.push(parseColumnData(name, `mediumint(${size})`, options)); return this; } /** * Adds a new column of data type 'int' to this structure * @param {String} name - Name of the column * @param {Number} size Min 1, Max 255. Defaults to 255 * @param {ConstraintOptions} options - Extra constraint options * @returns */ int = (name, size=255, options) => { if (size < 1 || size > 255) throw new Error(`Column datatype 'int' size must be a number between 1 and 255. Received: ${size}`); this.#columns.push(parseColumnData(name, `int(${size})`, options)); return this; } /** * Adds a new column of data type 'bigint' to this structure * @param {String} name - Name of the column * @param {Number} size Min 1, Max 255. Defaults to 255 * @param {ConstraintOptions} options - Extra constraint options * @returns */ bigint = (name, size=255, options) => { if (size < 1 || size > 255) throw new Error(`Column datatype 'bigint' size must be a number between 1 and 255. Received: ${size}`); this.#columns.push(parseColumnData(name, `bigint(${size})`, options)); return this; } /** * Adds a new column of data type 'float' to this structure * @param {String} name - Name of the column * @param {Number} p - Precision. Min 1, Max 53. Defaults to 25 * @param {ConstraintOptions} options - Extra constraint options * @returns */ float = (name, p=25, options) => { if (p < 1 || p > 53) throw new Error(`Column datatype 'float' size must be a number between 1 and 53. Received: ${p}`); this.#columns.push(parseColumnData(name, `float(${p})`, options)); return this; } /** * Adds a new column of data type 'double' to this structure * @param {String} name - Name of the column * @param {Number} size - Min 1. Defaults to 16 * @param {Number} d - Double precision. Min 1. Defaults to 8 * @param {ConstraintOptions} options - Extra constraint options * @returns */ double = (name, size=16, d=8, options) => { if (size < 1) throw new Error(`Column datatype 'double' size must be greater than 0. Received: ${p}`); if (d < 1) throw new Error(`Column datatype 'double' d must be greater than 0. Received: ${p}`); this.#columns.push(parseColumnData(name, `double(${size},${d})`, options)); return this; } /** * Adds a new column of data type 'decimal' to this structure * @param {String} name - Name of the column * @param {Number} size - Min 1, Max 65. Defaults to 10 * @param {Number} d - Double precision. Min 0. Defaults to 0. * @param {ConstraintOptions} options - Extra constraint options * @returns */ decimal = (name, size=10, d=0, options) => { if (size < 1 || size > 65) throw new Error(`Column datatype 'decimal' size must be a number between 1 and 65. Received: ${size}`); if (d < 0) throw new Error(`Column datatype 'decimal' d must be positive. Received: ${d}`); this.#columns.push(parseColumnData(name, `decimal(${size},${d})`, options)); return this; } } /** * * @param {String} name * @param {String} type * @param {ConstraintOptions} options * @returns */ function parseColumnData(name, type, options={}){ if (options.primary + options.unique + options.index > 1) throw new Error(`ConstrainError: Only one of 'primary', 'unique' or 'index' allowed`); return { Field: name, Type: `${type}${options.unsigned?" UNSIGNED":""}`, Null: options.null?"YES":"NO", Key: options.primary?"PRI":options.index?"MUL":options.unique?"UNI":"", Default: options.default?options.default:null, Extra: options.auto_increment?"auto_increment":"" } } module.exports = { CreateTable, Structure, AlterTable }