595 lines
17 KiB
JavaScript
595 lines
17 KiB
JavaScript
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)");
|
|
|
|
// Creating tags table and its junction table
|
|
db.run(`
|
|
CREATE TABLE tags (
|
|
id INTEGER PRIMAY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
db.run('CREATE INDEX idx_tag_name ON tags(name)');
|
|
|
|
db.run(`
|
|
CREATE TABLE article_tags (
|
|
article_id INTEGER,
|
|
tag_id INTEGER,
|
|
PRIMARY KEY (article_id, tag_id),
|
|
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
// Creating categories table and its junction table
|
|
db.run(`
|
|
CREATE TABLE categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
db.run('CREATE INDEX idx_category_name ON categories(name');
|
|
|
|
db.run(`
|
|
CREATE TABLE article_categories (
|
|
article_id INTEGER,
|
|
category_id INTEGER,
|
|
PRIMARY KEY (article_id, category_id),
|
|
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
|
|
)
|
|
`);
|
|
|
|
// 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 {Array<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 {Array<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 {Array<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 {Array<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;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets all categories
|
|
* @returns {Array<Object>} - Array of category objects
|
|
*/
|
|
function getAllCategories() {
|
|
const result = db.exec("SELECT * FROM categories ORDER BY name ASC");
|
|
|
|
if (result.length === 0) return [];
|
|
|
|
const columns = result[0].columns;
|
|
const rows = result[0].values;
|
|
|
|
return rows.map(row => {
|
|
const category = {};
|
|
columns.forEach((col, index) => {
|
|
category[col] = row[index];
|
|
});
|
|
return category;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the existing category object, or create it if it doesn't.
|
|
* @param {String} name - the name of the category to obtain
|
|
* @returns {Object} - The category object from the database
|
|
*/
|
|
function getOrCreateCategory(name) {
|
|
const stmt = db.prepare("SELECT * FROM categories WHERE name = ?");
|
|
stmt.bind([name]);
|
|
|
|
if (stmt.step()) {
|
|
const category = stmt.getAsObject();
|
|
stmt.free();
|
|
return category;
|
|
}
|
|
stmt.free();
|
|
|
|
db.run("INSERT INTO categories (name) VALUES (?)", [name]);
|
|
const data = db.export();
|
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
|
|
|
return getOrCreateCategory(name);
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets all the tags in the database
|
|
* @returns {Array<Object>} - an array of tag objects
|
|
*/
|
|
function getAllTags() {
|
|
const result = db.exec("SELECT * FROM tags ORDER BY name ASC");
|
|
|
|
if (result.length === 0) return [];
|
|
|
|
const columns = result[0].columns;
|
|
const rows = result[0].values;
|
|
|
|
return rows.map(row => {
|
|
const tag = {};
|
|
columns.forEach((col, index) => {
|
|
tag[col] = row[index];
|
|
});
|
|
return tag;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the tag object from the database if it exists, and create it if it doesn't
|
|
* @param {string} name - the name of the tag
|
|
* @returns {Object} - the tag object from the database
|
|
*/
|
|
function getOrCreateTag(name) {
|
|
const stmt = db.prepare("SELECT * FROM tags WHERE name = ?");
|
|
stmt.bind([name]);
|
|
|
|
if(stmt.step()) {
|
|
const tag = stmt.getAsObject();
|
|
stmt.free()
|
|
return tag;
|
|
}
|
|
stmt.free();
|
|
|
|
db.run("INSERT INTO tags (name) VALUES (?)",[name]);
|
|
const data = db.export();
|
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
|
|
|
return getOrCreateTag(name);
|
|
}
|
|
|
|
/**
|
|
* Set categories for an article
|
|
* @param {Int} articleId - Artilce ID
|
|
* @param {Array<string>} categoryNames - An array of category names
|
|
*/
|
|
function setArticleCategories(articleId, categoryNames) {
|
|
db.run("DELETE FROM article_categories WHERE article_id = ?", [articleId]);
|
|
|
|
categoryNames.forEach(name => {
|
|
const category = getOrCreateCategory(name);
|
|
db.run("INSERT INTO article_categories (article_id, category_id) VALUES (?, ?)",
|
|
[articleId, category.id]
|
|
);
|
|
});
|
|
|
|
const data = db.export();
|
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
|
}
|
|
|
|
/**
|
|
* Set the tags for an article
|
|
* @param {Int} articleId - Article ID
|
|
* @param {Array<string>} tagNames - An array of tag names
|
|
*/
|
|
function setArticleTags(articleId, tagNames) {
|
|
db.run("DELETE FROM article_tags WHERE article_id = ?", [articleId]);
|
|
|
|
tagNames.forEach(name => {
|
|
const tag = getOrCreateTag(name);
|
|
db.run("INSERT INTO article_tags (article_id, tag_id) VALUES (?, ?)", [articleId, tag.id]);
|
|
});
|
|
|
|
const data = db.export();
|
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
|
}
|
|
|
|
/**
|
|
* Get the categories of an article
|
|
* @param {int} articleId - Article ID
|
|
* @returns {Array<string>} - An array of category names
|
|
*/
|
|
function getArticleCategories(articleId) {
|
|
const result = db.exec(`
|
|
SELECT c.name FROM categories c
|
|
JOIN article_categories ac ON c.id = ac.category_id
|
|
WHERE ac.article_id = ?
|
|
ORDER BY c.name ASC
|
|
`, [articleId]);
|
|
|
|
if (result.length === 0) return [];
|
|
return result[0].values.map(row => row[0]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the tags on an article
|
|
* @param {Int} articleId - Article ID
|
|
* @returns {Array<string>} - An array of tag names
|
|
*/
|
|
function getArticleTags(articleId) {
|
|
const result = db.exec(`
|
|
SELECT t.name FROM tags t
|
|
JOIN article_tags at ON t.id = at.tag_id
|
|
WHERE ta.article_id = ?
|
|
ORDER BY t.name ASC
|
|
`, [articleId]);
|
|
|
|
if(result.length === 0) return [];
|
|
return result[0].values.map(row => row[0]);
|
|
}
|
|
|
|
module.exports = {
|
|
initDb,
|
|
getAllArticles,
|
|
getArticle,
|
|
createArticle,
|
|
updateArticle,
|
|
deleteArticle,
|
|
searchArticles,
|
|
createUser,
|
|
getUserByUsername,
|
|
getUserByEmail,
|
|
getUserById,
|
|
updateUserRole,
|
|
deleteUser,
|
|
getAllUsers,
|
|
getOwnedDrafts,
|
|
getAllCategories,
|
|
getAllTags,
|
|
getOrCreateCategory,
|
|
getOrCreateTag,
|
|
getArticleCategories,
|
|
getArticleTags,
|
|
setArticleCategories,
|
|
setArticleTags
|
|
}; |