const initSqlJs = require('sql.js'); const fs = require('fs'); const path = require('path'); let db = null; const DB_PATH = './ka.db'; // Helper functions function getNextKANumber() { const result = db.exec('SELECT ka_number FROM articles ORDER BY id DESC LIMIT 1') if (result.length > 0 && result[0].values.length > 0) { const lastKANumber = result[0].values[0][0]; const number = parseInt(lastKANumber.replace('KA', '')) + 1; return `KA${String(number).padStart(3, '0')}`; } return 'KA001'; } async function initDb() { const SQL = await initSqlJs(); console.log('Initializing Database...'); // Loading Database if it already exists if (fs.existsSync(DB_PATH)) { const buffer = fs.readFileSync(DB_PATH); db = new SQL.Database(buffer); console.log('Database Loaded') } else { // Creating a new one if it does not console.log('Database not found. Creating new instance...') db = new SQL.Database(); // Creating Knowledge Article table db.run(` CREATE TABLE articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, ka_number TEXT UNIQUE, title TEXT, content TEXT, status TEXT DEFAULT 'published', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_by TEXT, updated_at DATETIME, updated_by TEXT ) `); db.run("CREATE INDEX idx_ka_number ON articles(ka_number)"); db.run("CREATE INDEX idx_status ON articles(status)"); // Creating Users table db.run(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, email TEXT UNIQUE NOT NULL, pass_hash TEXT, display_name TEXT, auth_provider TEXT DEFAULT 'local', entra_id TEXT, role TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); db.run("CREATE INDEX idx_email ON users(email)"); db.run("CREATE INDEX idx_entra_id ON users(entra_id)"); db.run("CREATE INDEX idx_username ON users(username)"); // Saving the created db to file const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); console.log(`Database created at ${DB_PATH}`); } return db; } /** * Gets all published articles from the database * @returns {[Object]} - returns an array of all stored articles */ function getAllArticles() { const result = db.exec("SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC"); if (result.length === 0) { return []; } const columns = result[0].columns; const rows = result[0].values; const articles = rows.map(row => { const article = {}; columns.forEach((col, index) => { article[col] = row[index]; }); return article; }) return articles; } /** * Gets the article from the database associated with a KA number * @param {string} ka_num - The KA number associated with an article * @returns {Object} - the associated article from the database */ function getArticle(ka_num) { const stmt = db.prepare('SELECT * FROM articles where ka_number = ?'); stmt.bind([ka_num]); if (stmt.step()) { const article = stmt.getAsObject(); stmt.free(); return article; } stmt.free(); return null; } /** * Creates a new entry in the articles table, atomatically assigns KA number, and returns the newly created article * @param {string} title - The title of the article * @param {string} content - The content of the article * @param {string} author - The username of the article creator * @param {string} status - the status of the created article ('draft' or 'published') * @returns {Object} - The newly created article from the database */ function createArticle(title, content, author, status = 'published') { const ka_num = getNextKANumber(); db.run( "INSERT INTO articles (ka_number, title, content, created_by, status) VALUES (?, ?, ?, ?, ?)", [ka_num, title, content, author, status] ); // Saving updated DB to file const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); return getArticle(ka_num); } /** * Updates the stored article that is associated with the KA number, and returns the updated article * @param {string} ka_num - The KA number of the article beaing updated * @param {string} title - The title of the article * @param {string} content - The content of the article * @param {string} author - The username that updated the article * @param {string} status - The status of the article ('published' or 'draft') * @returns {Object} - The newly updatd article from the database */ function updateArticle(ka_num, title, content, author, status = 'published') { db.run( "UPDATE articles SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ?, status = ? WHERE ka_number = ?", [title, content, author, status, ka_num] ); // Saving updated DB to file const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); return getArticle(ka_num); } /** * Deletes the associated article from the article table * @param {*} ka_num - The KA number associated to the article */ function deleteArticle(ka_num) { db.run( "DELETE FROM articles WHERE ka_number = ?", [ka_num] ); // Saving updated DB to file const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); // Deleting media folder if it exists const mediaDir = path.join('./media', ka_num); if (fs.existsSync(mediaDir)) { fs.rmSync(mediaDir, {recursive: true, force: true}); } } /** * Searches the article table for any words matching in the title, content, or ka number and returns an array of them * @param {string} query - The search query that will be used for looking up the assocaited articles * @returns {[Object]} - An arry of articles that match the query content */ function searchArticles(query) { const searchTerm = `%${query}%`; const stmt = db.prepare( "SELECT * FROM articles WHERE title LIKE ? OR content LIKE ? or ka_number LIKE ? ORDER BY created_at DESC" ); stmt.bind([searchTerm, searchTerm, searchTerm]); const results = []; while (stmt.step()) { results.push(stmt.getAsObject()); } stmt.free(); return results; } /** * Creates a new user and returns the newly created user object from the database * @param {string} username - The username for the newly created user * @param {string} email - The email address for the newly created user * @param {string} passHash - The hashed password for validation purposes * @param {string} display_name - The name that will be desplayed when an article is created or updated * @param {string} authProvider - the source of the authentication: 'local' or 'entra' * @param {string} entraId - The ID number for the associated entra account, can be null if auth provider is local * @param {string} role - The user's role permissions (Admin / Editor / User) * @returns {Object} - The user object of the newly created user */ function createUser(username, email, passHash, display_name, authProvider = 'local', entraId = null, role = 'User') { db.run("INSERT INTO users (username, email, pass_hash, display_name, auth_provider, entra_id, role) VALUES (?, ?, ?, ?, ?, ?, ?)", [username, email, passHash, display_name, authProvider, entraId, role] ) // Saving DB with newly created record const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); const user = getUserByEmail(email); return user; } /** * Looks up users table and returns the user object for the user profile that matches the provided email * @param {string} email - The email address used for the created user * @returns {Object} - The matching user object from the database */ function getUserByEmail(email) { const stmt = db.prepare("SELECT * FROM users WHERE email = ? LIMIT 1"); stmt.bind([email]); if (stmt.step()) { const user = stmt.getAsObject(); stmt.free(); return user; } stmt.free(); return null; } /** * Looks up the user account with the specified username from the database and returns the user object * @param {*} username - The username associated to the profile being looked up * @returns {Object} - The user object that matches the provided username */ function getUserByUsername(username) { const stmt = db.prepare("SELECT * FROM users WHERE username = ?"); stmt.bind([username]); if (stmt.step()) { const user = stmt.getAsObject(); stmt.free(); return user; } stmt.free(); return null; } /** * Looks up the user by the primary key and returns the user object * @param {Int} id - The id number of the associated user account * @returns {Object} - The user object associated to the primary key provided */ function getUserById(id) { const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); stmt.bind([id]); if (stmt.step()) { const user = stmt.getAsObject(); stmt.free(); return user; } stmt.free(); return null; } /** * Modifies the saved user's role * @param {Int} userId - The Id for the user * @param {string} newRole - The new role to be assigned * @returns {Object} - The updated user object */ function updateUserRole(userId, newRole) { db.run("UPDATE users SET role = ? WHERE id = ?", [newRole, userId] ); const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); return getUserById(userId); } /** * Deletes the user from the database * @param {Int} userId - The Id of the user to be deleted */ function deleteUser(userId) { db.run("DELETE FROM users WHERE id = ?", [userId]); const data = db.export(); fs.writeFileSync(DB_PATH. Buffer.from(data)); } /** * Gets all users currently stored in the database * @returns {[Object]} - an array of user objects */ function getAllUsers() { const result = db.exec( "SELECT id, username, email, display_name, auth_provider, role, created_at FROM users ORDER BY created_at DESC" ); if (result.length === 0) return []; const columns = result[0].columns; const rows = result[0].values; const users = rows.map(row => { const user = {}; columns.forEach((col, index) => { user[col] = row[index]; }); return user; }); return users; } /** * Return all draft articles owned by a user * @param {string} author - The author of the articles * @returns {[Object]} - An array of article objects */ function getOwnedDrafts(author) { const stmt = db.prepare("SELECT * FROM articles WHERE status = 'draft' AND created_by = ? ORDER BY created_at DESC") stmt.bind([author]); const results = []; while (stmt.step()) { results.push(stmt.getAsObject()); } stmt.free() return results; } module.exports = { initDb, getAllArticles, getArticle, createArticle, updateArticle, deleteArticle, searchArticles, createUser, getUserByUsername, getUserByEmail, getUserById, updateUserRole, deleteUser, getAllUsers, getOwnedDrafts };