464 lines
19 KiB
JavaScript
464 lines
19 KiB
JavaScript
// 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;
|
|
|
|
// Helper Functions
|
|
// Function to run and handle SQL errors
|
|
function runSQL(sqlQuery, connection, callback = (err, rows) => {
|
|
log.ERROR(err);
|
|
throw err;
|
|
}) {
|
|
// Start the MySQL Connection
|
|
connection.query(sqlQuery, (err, rows) => {
|
|
if (err) {
|
|
log.ERROR("SQL Error:", err)
|
|
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) {
|
|
this.connection = mysql.createPool({
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASS,
|
|
database: process.env.DB_NAME
|
|
});
|
|
|
|
this.dbTable = _dbTable;
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
exports.UserStorage = class UserStorage extends Storage {
|
|
constructor() {
|
|
super(accountsTable);
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
super(transactionsTable);
|
|
}
|
|
|
|
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() {
|
|
super(rssFeedsTable);
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
super(rssPostsTable);
|
|
}
|
|
|
|
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);
|
|
})
|
|
}
|
|
}
|