// 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"); // Storage Specific Modules // MySQL const mysql = require("mysql"); // 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.connect(); connection.query(sqlQuery, (err, rows) => { if (err) { log.ERROR("SQL Error:", err) callback(err, undefined); } log.DEBUG("RunSQL Returned Rows:", rows); callback(undefined, rows); }) //connection. } class RSSRecord { /** * * @param {*} _id * @param {*} _title * @param {*} _link * @param {*} _category */ constructor(_id, _title, _link, _category) { this.id = _id; this.title = _title; this.link= _link; this.category = _category; } getId() { return this.id; } get(key) { if (!Object.keys(this).includes(key)) throw new Error("Key is invalid", key); return this[key] } } exports.RSSRecord = RSSRecord; exports.Storage = 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 }); // Set the DB Table for later use this.dbTable = _dbTable } /** * 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 (!toBeSaved[0].fields?.title) 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, (err, record) => { if (err) callback(err, undefined); newRecords.push(record); if (toBeSaved.length === 1) { log.DEBUG("One record to callback with:", record); callback(undefined, record); } }) } if (!toBeSaved.length === 1) { callback(undefined, newRecords); } } /** * 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) callback(Error("No entry ID given"), undefined); this.getRecordBy('id', entryID, (err, entryRecord) => { this.removeEntry(entryRecord.id, (err, results) => { if (err) callback(err, undefined); callback(undefined, results); }); }); } /** * 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) callback(new Error("No title given when checking for title"), undefined); const sqlQuery = `SELECT * FROM ${this.dbTable} WHERE title = '${title}'`; runSQL(sqlQuery, this.connection, (err, rows) => { if (err) callback(err, undefined); if (rows[0]?.title) callback(undefined, true); else callback(undefined, false); }) } /** * Get a record by a specified key * @param {*} key The key to search for * @param {*} keyValue The value of the key to search for */ getRecordBy(key, keyValue, callback) { const validKeys = ["link", "title", "category", "id"]; if (!validKeys.includes(key)) callback(new Error("Given key not valid"), undefined); const sqlQuery = `SELECT * FROM ${this.dbTable} WHERE ${key} = '${keyValue}'`; runSQL(sqlQuery, this.connection, (err, rows) => { if (err) callback(err, undefined); if (rows[0]?.title) callback(undefined, rows[0]); else callback(undefined, false); }) } /** * 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) { callback(new Error("Entry object malformed, check the object before saving it"), undefined) } const sqlQuery = `INSERT INTO ${this.dbTable} (title, link, category) VALUES ('${entryObject.title}', '${entryObject.link}', '${entryObject.category}');`; log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`) runSQL(sqlQuery, this.connection, (err, rows) => { if (err) callback(err, undefined); 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) callback(new Error("No title given before updating"), undefined); queryParams.push(`title = '${entryObject.title}'`); if (!entryObject.link) callback(new Error("No link given before updating"), undefined); queryParams.push(`link = '${entryObject.link}'`); if (entryObject.category) queryParams.push(`category = '${entryObject.category}'`); 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) callback(err, undefined); callback(undefined, rows); }) } /** * Delete the given entry from the storage medium * @param {string} title 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(title, callback) { if (!title) { callback(new Error("No entry title given before deleting"), undefined) } const sqlQuery = `DELETE FROM ${this.dbTable} WHERE title = '${title}';`; runSQL(sqlQuery, this.connection, (err, rows) => { if (err) callback(err, undefined); 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, callback) { log.DEBUG(`Return record for these values: ID: '${_id}', Title: '${_title}', Category: '${_category}', Link: '${_link}'`) if (!_link && !_title) callback(new Error("No link or title given when creating a record"), undefined); let entryObject = { "title": _title, "link": _link } if (_category) entryObject.category = _category; if (_id) { entryObject.id = _id; this.updateEntry(entryObject, (err, rows) => { if (err) callback(err, undefined); this.getRecordBy('id', entryObject.id, (err, record) => { if (err) callback(err, undefined); callback(undefined, new RSSRecord(record.id, record.title, record.link, record.category)); }) }) } 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) callback(err, undefined); this.getRecordBy("title", entryObject.title, (err, record) => { if (err) callback(err, undefined); callback(undefined, new RSSRecord(record.id, record.title, record.link, record.category)); }) }); } else{ this.updateEntry(entryObject, (err, rows) => { if (err) callback(err, undefined); this.getRecordBy('title', entryObject.title, (err, record) => { if (err) callback(err, undefined); callback(undefined, new RSSRecord(record.id, record.title, record.link, record.category)); }) }) } }) } } /** * Get all records stored * @param {function} callback */ getAllRecords(callback) { log.INFO("Getting all records"); const sqlQuery = `SELECT * FROM ${this.dbTable}` let rssRecords = []; runSQL(sqlQuery, this.connection, (err, rows) => { if (err) callback(err, undefined); for (const row of rows) { log.DEBUG("Row from SQL query:", row); rssRecords.push(new RSSRecord(row.id, row.title, row.link, row.category)); } log.DEBUG("All records:", rssRecords); callback(undefined, rssRecords); }); } }