Initial Emmelia merge
This commit is contained in:
500
Server/libStorage.js
Normal file
500
Server/libStorage.js
Normal file
@@ -0,0 +1,500 @@
|
||||
// Customizable storage module for any mode of storage
|
||||
// Update the functions here to change the storage medium
|
||||
|
||||
// Import modules
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libStorage");
|
||||
const { RSSSourceRecord, RSSPostRecord } = require("./utilities/recordHelper");
|
||||
|
||||
// Storage Specific Modules
|
||||
// MySQL
|
||||
const mysql = require("mysql");
|
||||
|
||||
const rssFeedsTable = process.env.DB_RSS_FEEDS_TABLE;
|
||||
const rssPostsTable = process.env.DB_RSS_POSTS_TABLE;
|
||||
const accountsTable = process.env.DB_ACCOUNTS_TABLE;
|
||||
const transactionsTable = process.env.DB_TRANSACTIONS_TABLE;
|
||||
const pricingTable = process.env.DB_PRICING_TABLE;
|
||||
|
||||
var Connection = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
connectionLimit: 10
|
||||
});
|
||||
|
||||
// Helper Functions
|
||||
/**
|
||||
* Function to run and handle SQL errors
|
||||
* @param {string} sqlQuery The SQL query string
|
||||
* @param {*} connection The SQL connection to be used to query
|
||||
* @param {function} callback The callback function to be called with an error or the results
|
||||
* @param {number} _retry Set by error retry, increments the number a query has been retried to increase wait time and track a specific query
|
||||
*/
|
||||
function runSQL(sqlQuery, connection, callback = (err, rows) => {
|
||||
log.ERROR(err);
|
||||
throw err;
|
||||
}, _retry = 0) {
|
||||
// Start the MySQL Connection
|
||||
if (!connection) connection = Connection;
|
||||
connection.query(sqlQuery, (err, rows) => {
|
||||
if (err) {
|
||||
if (err.code === "EHOSTUNREACH") {
|
||||
// DB Connection is unavailable
|
||||
let retryTimeout;
|
||||
switch(_retry){
|
||||
case 0:
|
||||
retryTimeout = 30000;
|
||||
break;
|
||||
case retry < 15:
|
||||
retryTimeout = 30000 + retry * 15000;
|
||||
break;
|
||||
default:
|
||||
log.ERROR("Retried Database 15 times over, please check connection status and restart the app", sqlQuery, err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
log.WARN(`Database connection is unavailable, waiting ${ retryTimeout / 1000 } seconds...`);
|
||||
_retry += 1
|
||||
// Wait for the retry timeout before trying the query again
|
||||
setTimeout(runSQL(sqlQuery, connection, callback, _retry));
|
||||
}
|
||||
else return callback(err, undefined);
|
||||
}
|
||||
log.VERBOSE(`SQL result for query '${sqlQuery}':`, rows);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted date time string from now for MySQL
|
||||
*
|
||||
* @returns Date string for now formatted for MySQL
|
||||
*/
|
||||
function returnMysqlTime(){
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
class Storage {
|
||||
constructor(_dbTable, _connection) {
|
||||
this.dbTable = _dbTable;
|
||||
this.connection = _connection;
|
||||
this.validKeys = [];
|
||||
|
||||
var sqlQuery = `SHOW COLUMNS FROM ${this.dbTable};`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return log.ERROR("Error getting column names: ", err);
|
||||
if (rows){
|
||||
for (const validKey of rows){
|
||||
this.validKeys.push(validKey.Field);
|
||||
}
|
||||
log.VERBOSE(`Database rows for '${this.dbTable}': `, rows);
|
||||
log.DEBUG(`Keys for '${this.dbTable}': `, this.validKeys);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to delete an entry using the storage method configured
|
||||
* @param {} entryID The ID of the entry to be deleted
|
||||
* @param {function} callback The callback function to be called with the record when deleted
|
||||
*/
|
||||
destroy(entryID, callback) {
|
||||
if (!entryID) return callback(Error("No entry ID given"), undefined);
|
||||
|
||||
this.getRecordBy('id', entryID, (err, entryRecord) => {
|
||||
this.removeEntry(entryRecord.id, (err, results) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a record by a specified key
|
||||
* @param {*} key The key to search for
|
||||
* @param {*} keyValue The value of the key to search for
|
||||
* @param {*} callback The callback function
|
||||
*/
|
||||
getRecordBy(key, keyValue, callback) {
|
||||
if (!this.validKeys.includes(key)) return callback(new Error("Given key not valid", key), undefined);
|
||||
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable} WHERE ${key} = "${keyValue}"`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows[0]?.[key]) return callback(undefined, rows[0]);
|
||||
else return callback(undefined, false);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all records stored
|
||||
* @param {function} callback
|
||||
*/
|
||||
getAllRecords(callback) {
|
||||
log.INFO("Getting all records from: ", this.dbTable);
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all unique rows in the given key
|
||||
* @param {*} key
|
||||
* @param {*} callback
|
||||
*/
|
||||
getUniqueByKey(key, callback){
|
||||
log.INFO("Getting all unique values in column: ", key);
|
||||
const sqlQuery = `SELECT DISTINCT ${key} FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
try {
|
||||
this.connection.end();
|
||||
}
|
||||
catch (err) {
|
||||
log.ERROR("Error closing connection :", this.connection, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.UserStorage = class UserStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(accountsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new account to the database
|
||||
* @param {*} _discordAccountId The Discord ID the the user
|
||||
* @param {*} callback The callback to be sent
|
||||
* @callback Error|Array|*
|
||||
*/
|
||||
saveAccount(_discordAccountId, callback){
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (discord_account_id, balance) VALUES ("${_discordAccountId}", ${0});`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows?.affectedRows > 0) return callback(undefined, rows);
|
||||
return callback(undefined, undefined);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check or return the balance of a given account ID
|
||||
*
|
||||
* @param {*} _tokensToBeUsed The amount of tokens to be used, set to 0 to return the balance
|
||||
* @param {*} _account_id The account ID to check or return the balance of
|
||||
* @param {*} callback
|
||||
*/
|
||||
checkBalance(_tokensToBeUsed, _account_id, callback) {
|
||||
if (!_account_id) return callback(new Error("Account not specified when checking account balance"), undefined);
|
||||
log.DEBUG("Tokens to verify against balance", _tokensToBeUsed, _account_id);
|
||||
if (!_tokensToBeUsed && !_tokensToBeUsed >= 0) return callback(new Error("Specified tokens are invalid when checking account balance"), undefined);
|
||||
this.getRecordBy('account_id', _account_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
// Check to see if the account has a balance greater than what was given
|
||||
if(_tokensToBeUsed > 0){
|
||||
if (record?.balance && record.balance > _tokensToBeUsed) return callback(undefined, true);
|
||||
else return callback(undefined, false);
|
||||
}
|
||||
|
||||
return callback(undefined, record.balance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's account Balance
|
||||
*
|
||||
* @param {string} _updateType The type of update to make to the account [ withdraw | deposit ]
|
||||
* @param {number} _updateAmount The amount to update the account
|
||||
* @param {number} _account_id The ID of the discord account to update
|
||||
* @param {function} callback The callback function to call with the results
|
||||
* @returns Result from the SQL query or false
|
||||
*/
|
||||
updateBalance(_updateType, _updateAmount, _discord_account_id, callback){
|
||||
var sqlQuery = "";
|
||||
switch(_updateType){
|
||||
case "withdraw":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance-${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
case "deposit":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance+${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
default:
|
||||
log.ERROR('Update type not valid: ', _updateType);
|
||||
return callback(new Error("Update type not valid"));
|
||||
}
|
||||
|
||||
if(!sqlQuery) return callback(new Error("SQL Query empty"), undefined);
|
||||
|
||||
log.DEBUG("Updating Balance with SQL Query: ", sqlQuery);
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (!rows?.affectedRows > 0) return callback(new Error("Error updating Balance", rows), undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.TransactionStorage = class TransactionStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(transactionsTable, connection);
|
||||
}
|
||||
|
||||
createTransaction(transaction, callback){
|
||||
var sqlQuery = `INSERT INTO ${this.dbTable} (transaction_id, account_id, discord_tokens_used, provider_tokens_used, provider_id, order_date) VALUES ("${transaction.transaction_id}", "${transaction.account_id}", "${transaction.discord_tokens_used}", "${transaction.provider_tokens_used}", "${transaction.provider_id}", "${returnMysqlTime()}");`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows?.affectedRows > 0) return callback(undefined, rows);
|
||||
return callback(undefined, undefined);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.FeedStorage = class FeedStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssFeedsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to save a new entry using the storage method configured
|
||||
* @param {Array} toBeSaved Entry or Entries to be added
|
||||
* @param {function} callback The callback function to be called with the record when saved
|
||||
*/
|
||||
create(toBeSaved, callback) {
|
||||
log.DEBUG("To be saved:", toBeSaved);
|
||||
log.DEBUG("to be saved length:", toBeSaved.length);
|
||||
// If the request was for the Feeds Table
|
||||
if (!toBeSaved[0].fields?.title) return callback(Error("No title given"), undefined);
|
||||
let newRecords = []
|
||||
for (var entry of toBeSaved) {
|
||||
entry = entry.fields;
|
||||
log.DEBUG("Entry:", entry);
|
||||
this.returnRecord(undefined, entry.title, entry.link, entry.category, entry.guild_id, entry.channel_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
newRecords.push(record);
|
||||
if (toBeSaved.length === 1) {
|
||||
log.DEBUG("One record to callback with:", record);
|
||||
return callback(undefined, record);
|
||||
}
|
||||
}, false) // Do not update the if it exists
|
||||
}
|
||||
if (!toBeSaved.length === 1) {
|
||||
return callback(undefined, newRecords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if an entry exists in the storage method configured
|
||||
* @param {*} title The title of the entry to check if it exists
|
||||
* @returns {true|false|*}
|
||||
*/
|
||||
checkForTitle(title, callback) {
|
||||
if (!title) return callback(new Error("No title given when checking for title"), undefined)
|
||||
|
||||
this.getRecordBy("title", title, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
saveEntry(entryObject, callback) {
|
||||
log.DEBUG("Saving entry:", entryObject);
|
||||
if (!entryObject?.title || !entryObject?.link || !entryObject?.category) {
|
||||
return callback(new Error("Entry object malformed, check the object before saving it"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (title, link, category, guild_id, channel_id) VALUES ("${entryObject.title}", "${entryObject.link}", "${entryObject.category}", "${entryObject.guild_id}", "${entryObject.channel_id}");`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
updateEntry(entryObject, callback) {
|
||||
let queryParams = [];
|
||||
if (!entryObject.title) return callback(new Error("No title given before updating"), undefined);
|
||||
queryParams.push(`title = "${entryObject.title}"`);
|
||||
if (!entryObject.link) return callback(new Error("No link given before updating"), undefined);
|
||||
queryParams.push(`link = "${entryObject.link}"`);
|
||||
if (entryObject.category) queryParams.push(`category = "${entryObject.category}"`);
|
||||
if (entryObject.guild_id) queryParams.push(`guild_id = "${entryObject.guild_id}"`);
|
||||
if (entryObject.channel_id) queryParams.push(`channel_id = "${entryObject.channel_id}"`);
|
||||
|
||||
let sqlQuery = `UPDATE ${this.dbTable} SET`;
|
||||
|
||||
let i = 0;
|
||||
for (const param of queryParams) {
|
||||
if (i === queryParams.length-1) {
|
||||
sqlQuery = `${sqlQuery} ${param}`
|
||||
i += 1;
|
||||
}
|
||||
else {
|
||||
sqlQuery = `${sqlQuery} ${param},`
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sqlQuery = `${sqlQuery} WHERE title = "${entryObject.title}";`
|
||||
|
||||
log.DEBUG(`Updating entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given entry from the storage medium
|
||||
* @param {*} id The title of the entry to be deleted
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
removeEntry(id, callback) {
|
||||
if (!id) {
|
||||
return callback(new Error("No entry id given before deleting"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `DELETE FROM ${this.dbTable} WHERE id = "${id}";`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows[0]);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a record class for the given information, if there's no ID, it will create it
|
||||
* @param {*} _id The ID / line number of the record in the storage medium (OPT)
|
||||
* @param {*} _title The title of the record
|
||||
* @param {*} _link The link to the RSS feed
|
||||
* @param {*} _category The category of the record
|
||||
* @param {*} callback Callback function to return an error or the record
|
||||
*/
|
||||
returnRecord(_id, _title, _link, _category, _guild_id, _channel_id, callback, updateEnabled = true) {
|
||||
log.DEBUG(`Return record for these values: ID: '${_id}', Title: '${_title}', Category: '${_category}', Link: '${_link}', Guild: '${_guild_id}', Channel:'${_channel_id}', Update Enabled: `, updateEnabled)
|
||||
if (!_link && !_title && !_guild_id && !_channel_id) return callback(new Error("No link or title given when creating a record"), undefined);
|
||||
let entryObject = {
|
||||
"title": _title,
|
||||
"link": _link,
|
||||
"guild_id": _guild_id,
|
||||
"channel_id": _channel_id
|
||||
}
|
||||
if (_category) entryObject.category = _category;
|
||||
|
||||
if (_id) {
|
||||
entryObject.id = _id;
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('id', entryObject.id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.checkForTitle(_title, (err, titleExists) => {
|
||||
if (!titleExists) {
|
||||
log.DEBUG("Entry doesn't exist, making one now", entryObject);
|
||||
this.saveEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy("title", entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
});
|
||||
}
|
||||
else{
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('title', entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.PostStorage = class PostStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssPostsTable, connection);
|
||||
}
|
||||
|
||||
savePost(_postObject, callback){
|
||||
const tempCreationDate = returnMysqlTime();
|
||||
log.DEBUG("Saving Post Object:", _postObject);
|
||||
if (!_postObject?.postId || !_postObject?.link) {
|
||||
return callback(new Error("Post object malformed, check the object before saving it", _postObject), undefined)
|
||||
}
|
||||
|
||||
if (_postObject.link.length > 250) _postObject.link = _postObject.link.substring(0, 250);
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (post_guid, post_link, post_sent_date) VALUES ("${_postObject.postId}","${_postObject.link}","${tempCreationDate}");`;
|
||||
|
||||
log.DEBUG(`Adding new post with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user