const mysql = require("mysql"); const Select = require("./lib/Select"); const Insert = require("./lib/Insert"); const Delete = require("./lib/Delete"); const Update = require("./lib/Update"); const { CreateTable, Structure, AlterTable } = require("./lib/Tables"); /** * @typedef {Object} InstanceOptions * @property {String} [charset] - Charset to use * @property {String} [defaultDatabase] - The default database * @property {Boolean} [multipleStatements] - Whether multiple statements should be allowed in a single query * @property {Boolean} [insecureAuth] - Whether insecure authentication methods should be allowed * @property {String} [customIdentifier] - Sets a custom identifier for this instance * @property {Boolean} [isDefault] - Whether this instance is returned by default via 'getInstance' */ /** * Exported as instance */ class awSQL { #instances = {}; // Holds instances. Key is identifier/name. #default; // Holds the identifier of the default instance (the one used if identifier is omitted) /** * Creates a new instance (database connection) * @param {String} hostname - Hostname where the database is located * @param {String} username - Username to login with * @param {String} password * @param {InstanceOptions} [options] * @returns {Instance} */ createInstance(hostname="localhost", username, password, options = { charset:"utf8mb4", defaultDatabase: false, multipleStatements: false, insecureAuth: false, customIdentifier: false, isDefault: false, }){ const identifier = options.customIdentifier||`${username}@${hostname}`; // Set identifier to given identifier or [username]@[hostname] // If an instance with that identifier exists, throw error if (this.#instances[identifier]) throw new Error(`Can't create new instance with identifier "${identifier}": An instance with the same name already exists`); const instance = new Instance(hostname, username, password, options.charset, options.defaultDatabase, options.multipleStatements, options.insecureAuth); this.#instances[identifier] = instance; // Store instance if (options.createAsDefault) this.#default = identifier; // If this instance was created with default option set it as the default instance return instance; } /** * Returns an instance matching the given identifier * * Returns default (or first) instance if no identifier is given * @param {String} [identifier] - Identifier of the instance to get * @returns {Instance} */ getInstance(identifier) { if (Object.keys(this.#instances).length===0) return undefined; // If no instance is found at all return -> Safety return // If no identifier is set return default or first instance if (!identifier) return this.#default? this.#instances[this.#default] // If default exists get that : this.#instances[Object.keys(this.#instances)[0]]; // Otherwise get instance with that identifier return this.#instances[identifier]; // If identifier given return that instance } /** * Returns a list of the identifiers of all instances * @returns {Array} */ listInstances(){ return Object.keys(this.#instances); } /** * Deletes an instance (and closes any open connection) * @param {String} identifier - Identifier of the instance to delete * @returns {true} */ deleteInstance(identifier){ if (!identifier) throw new Error("Can't delete Instance: No identifier set"); if (!this.#instances[identifier]) throw new Error(`Can't delete Instance '${identifier}': No Instance`); this.#instances[identifier].destroy(); // Memory: Close connection if (this.#default === identifier) this.#default = undefined; // If this instance was default, clear it from default delete this.#instances[identifier]; return true; } } /** * An awSQL-Instance */ class Instance { #user; #password; #host; #insecureAuth; #multipleStatements; #charset; #connection; #selectedDatabase; constructor(hostname="localhost", username, password, charset="utf8mb4", defaultDatabase=false, multipleStatements=false, insecureAuth=false){ this.#host = hostname; this.#user = username; this.#password = password; this.#charset = charset; this.#multipleStatements = multipleStatements; this.#insecureAuth = insecureAuth; this.#selectedDatabase = defaultDatabase||username; } /** * Connects the instance * @returns {undefined} */ connect(){ return new Promise((resolve, reject) => { const connection = mysql.createConnection({ // Create a new mysql-connection user: this.#user, password: this.#password, host: this.#host, insecureAuth: this.#insecureAuth, multipleStatements: this.#multipleStatements, charset: this.#charset }); this.#connection = connection; // Store the connection connection.connect((err) =>{ if (err) throw err; resolve(`Connected to ${this.#host} with user ${this.#user}`); }); connection.on("error", (err) => { if (!err.fatal){ return; } this.destroy(); // Memory: Destroy if connection errored this.connect(); // And try to reconnect }) }) } /** * Destroys the instance * @returns {true} */ destroy(){ if (this.#connection) this.#connection.end(); this.#connection = undefined; return true; } /** * Performs a raw query * @param {String} queryString The sql query string to perform. * @param {Array} values - An array holding all replacable ?-values from left to right. * @returns {Any} - The individual result of your query */ queryRaw(queryString, values){ return new Promise((resolve, reject) => { if (!this.#connection) throw new Error("Querying failed: No connection"); this.#connection.query(queryString, values, (err, result) => { if (err) throw err; resolve(result); }) }) } /** * Returns a list of database names the user has access to * @param {Boolean} excludeSchema - Whether to exclude the default database 'information_schema' * @returns {Array} */ async getDatabases (excludeSchema=false){ let dbs = await this.queryRaw("SHOW DATABASES;"); if (excludeSchema) dbs = dbs.filter((db)=>db.Database!=="information_schema") return dbs.map(db => db.Database); } /** * Selects a default database for future queries * @param {String} name - Name of the database * @returns {this} */ selectDatabase(name){ this.#selectedDatabase = name; return this; } /** * Returns a list of tables for the selected database * - 'multipleStatements' must be active for this to work * @param {String} [database] - Database to select. Can be empty as long as a default database was set with 'selectDatabase' * @returns {Array} */ async getTables(database){ if (!this.#multipleStatements) throw new Error("getTables: multipleStatements must be set to 'true' in instance options"); if (!this.#selectedDatabase && !database) throw new Error("getTables: No database selected"); const tables = (await this.queryRaw(`USE ${database||this.#selectedDatabase}; SHOW TABLES;`))[1]; return tables.map(table => table[`Tables_in_${database||this.#selectedDatabase}`]); } /** * Prepares a new select query * @param {String} from - Name of the table * @param {...String} [columns] - Name of columns to select. Leave empty to select all * @returns {Select} */ select(from, ...columns){ return new Select(this, this.#selectedDatabase, from, columns); } /** * Prepares a new query to insert data * @param {String} into - Name of the table to insert into * @returns {Insert} */ insert(into){ return new Insert(this, this.#selectedDatabase, into); } /** * Prepares a new delete query * @param {String} from - Name of the table to delete from * @returns {Delete} */ delete(from){ return new Delete(this, this.#selectedDatabase, from); } /** * Prepares a new update query * @param {String} table - Name of the table to update data in * @returns {Update} */ update(table){ return new Update(this, this.#selectedDatabase, table); } /** * Drops a whole database * - Requires admin privileges * @param {String} database - Name of the database to drop * @returns {Any} */ async dropDatabase (database){ return await this.queryRaw(`DROP DATABASE ${database};`); } /** * Drops a whole table * @param {String} table - Name of the table to drop * @returns {Any} */ async dropTable(table){ if (!this.#selectedDatabase) throw new Error(`Can't drop table '${table}': Database not set`); return await this.queryRaw(`DROP TABLE ${this.#selectedDatabase}.${table}`); } /** * Creates a new database * - Requires admin privileges * @param {String} name - Name of the database to create * @returns {Any} */ async createDatabase(name){ return await this.queryRaw(`CREATE DATABASE ${name};`); } /** * Prepares to create a new table * @param {String} name - Name of the table to create * @returns {CreateTable} */ createTable(name){ return new CreateTable(this, this.#selectedDatabase, name); } /** * Alters a table and updates to the new given structure. * * @param {String} name * @returns {AlterTable} */ alterTable(name){ return new AlterTable(this, this.#selectedDatabase, name); } /** * Prepares to create a new table-structure * @returns {Structure} */ createStructure(){ return new Structure(); } /** * Returns the structure object of a table * @param {String} table - Name of table to get structure of * @param {String} [database] - Name of the underlying database * @returns {Structure} */ async getStructure(table, database){ if (!this.#selectedDatabase && !database) throw new Error(`Can't get structure of table ${table}: Database not selected`); return new Structure(await this.queryRaw(`DESCRIBE ${database||this.#selectedDatabase}.${table};`)); } /** * Checks the structure of a table * @param {String} table - Name of the table * @param {Structure} desiredStructure - Structure to check against * @param {String} [database] - Name of the database. If omitted, uses default database * @returns {CheckResult} */ async checkStructure(table, desiredStructure, database){ if (!this.#selectedDatabase && !database) throw new Error(`Can't get structure of table ${table}: Database not selected`); const dbStruc = (await this.getStructure(table, database||this.#selectedDatabase)).get(); // Get current structure -> Array const result = { errors: [], passed: [] } for (let col of desiredStructure){ const dbCol = dbStruc.find((dbCol) => col.Field === dbCol.Field); // Check if the current table has the field if (!dbCol) { result.errors.push(`${col.Field}: Missing completely`); continue; } let breakOut = false; for (let key in col){ if (col[key] !== dbCol[key]){ // If the current key has a different value result.errors.push(`${dbCol.Field}.${key}: Required ${col[key]}, got ${dbCol[key]}`); breakOut=true; } } if (!breakOut){ // If no errors happened result.passed.push(`${col.Field}`); } } return result; } /** * Returns total amount of rows of a table * @param {String} table - Table name * @returns {Any} */ async total(table){ return await new Select(this, this.#selectedDatabase, table).count(true).execute(); } /** * Returns if connection is established * @returns {Boolean} */ isConnected(){ if (this.#connection) return true; return true; } } /** * @typedef {Object} CheckResult * @property {Array} errors - String representation of errors found * @property {Array} passed - String representation of passed checks */ const awSQLInstance = new awSQL(); module.exports = {awSQLInstance, Structure}; /** * @exports awSQLInstance */