added proper video embedding and added catagory and tag db foundation
This commit is contained in:
parent
05ce0c052d
commit
f0ebd192a1
88
client/kb-frontend/package-lock.json
generated
88
client/kb-frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.26.2",
|
||||
"quill-blot-formatter": "^1.0.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-quill": "^2.0.0"
|
||||
@ -26,21 +27,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz",
|
||||
"integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==",
|
||||
"version": "4.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.27.0.tgz",
|
||||
"integrity": "sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.13.2"
|
||||
"@azure/msal-common": "15.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "15.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz",
|
||||
"integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==",
|
||||
"version": "15.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.3.tgz",
|
||||
"integrity": "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
@ -854,9 +855,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -866,7 +867,7 @@
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
@ -1524,9 +1525,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
|
||||
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz",
|
||||
"integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@ -1545,9 +1546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1565,11 +1566,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
"electron-to-chromium": "^1.5.249",
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@ -1636,9 +1637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||
"version": "1.0.30001759",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
|
||||
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1783,6 +1784,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
@ -1832,9 +1842,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.260",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
|
||||
"integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
|
||||
"version": "1.5.266",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
|
||||
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -2938,6 +2948,18 @@
|
||||
"quill-delta": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-blot-formatter": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz",
|
||||
"integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"deepmerge": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"quill": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-delta": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||
@ -3215,9 +3237,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -3256,9 +3278,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
|
||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
|
||||
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.26.2",
|
||||
"quill-blot-formatter": "^1.0.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-quill": "^2.0.0"
|
||||
|
||||
@ -1,10 +1,61 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import Quill from 'quill';
|
||||
import ReactQuill from 'react-quill';
|
||||
import 'react-quill/dist/quill.snow.css';
|
||||
import './ArticleEdit.css';
|
||||
import MediaGallery from './MediaGallery';
|
||||
import BlotFormatter from 'quill-blot-formatter';
|
||||
|
||||
function ArticleEditor ({ article, onSave, onCancel, token }) {
|
||||
Quill.register('modules/blotFormatter', BlotFormatter);
|
||||
const BlockEmbed = Quill.import('blots/block/embed');
|
||||
|
||||
class VideoBlot extends BlockEmbed {
|
||||
static create(value) {
|
||||
const node = super.create();
|
||||
node.setAttribute('controls', '');
|
||||
node.setAttribute('style', 'max-width: 100%; height: auto;');
|
||||
node.setAttribute('width', '100%');
|
||||
|
||||
const source = document.createElement('source');
|
||||
source.setAttribute('src', value);
|
||||
source.setAttribute('type', 'video/mp4');
|
||||
node.appendChild(source);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
static value(node) {
|
||||
const source = node.querySelector('source');
|
||||
return source ? source.getAttribute('src') : '';
|
||||
}
|
||||
|
||||
static formats(node) {
|
||||
return {
|
||||
width: node.getAttribute('width'),
|
||||
height: node.getAttribute('height')
|
||||
};
|
||||
}
|
||||
|
||||
format(name, value) {
|
||||
if (name === 'width' || name === 'height') {
|
||||
if (value) {
|
||||
this.domNode.setAttribute(name, value);
|
||||
} else {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
} else {
|
||||
super.format(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VideoBlot.blotName = 'video';
|
||||
VideoBlot.tagName = 'video';
|
||||
|
||||
Quill.register(VideoBlot);
|
||||
|
||||
|
||||
function ArticleEditor({ article, onSave, onCancel, token }) {
|
||||
const [title, setTitle] = useState(article.title);
|
||||
const [content, setContent] = useState(article.content);
|
||||
const [isGalleryOpen, setIsGalleryOpen] = useState(false);
|
||||
@ -43,7 +94,7 @@ function ArticleEditor ({ article, onSave, onCancel, token }) {
|
||||
if (mediaItem.type === 'image') {
|
||||
quill.insertEmbed(range.index, 'image', mediaUrl);
|
||||
} else {
|
||||
const videoHtml = `<video controls style="max-width: 100%; height: auto;"><source src="${mediaUrl}" type="video/mp4">Your browser does not support video playback.</video>`;
|
||||
quill.insertEmbed(range.index, 'video', mediaUrl);
|
||||
quill.clipboard.dangerouslyPasteHTML(range.index, videoHtml);
|
||||
}
|
||||
|
||||
@ -54,10 +105,42 @@ function ArticleEditor ({ article, onSave, onCancel, token }) {
|
||||
setIsGalleryOpen(!isGalleryOpen);
|
||||
};
|
||||
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'indent': '-1' }, { 'indent': '+1' }],
|
||||
['link'],
|
||||
['clean']
|
||||
],
|
||||
blotFormatter: {
|
||||
specs: [
|
||||
BlotFormatter.DefaultSpec
|
||||
],
|
||||
overlay: {
|
||||
style: {
|
||||
border: '2px solid #3498db'
|
||||
}
|
||||
}
|
||||
},
|
||||
clipboard: {
|
||||
matchVisual: false
|
||||
}
|
||||
};
|
||||
|
||||
const formats = [
|
||||
'header',
|
||||
'bold', 'italic', 'underline', 'strike',
|
||||
'list', 'bullet', 'indent',
|
||||
'link', 'image', 'video',
|
||||
'width', 'height'
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='article-editor'>
|
||||
<div className='editor-header'>
|
||||
<h2>{ article.status === 'draft' ? 'Editing Draft: ' : 'Editing: '}
|
||||
<h2>{article.status === 'draft' ? 'Editing Draft: ' : 'Editing: '}
|
||||
{article.ka_number}
|
||||
</h2>
|
||||
{article.status === 'draft' && (
|
||||
@ -80,6 +163,8 @@ function ArticleEditor ({ article, onSave, onCancel, token }) {
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
ref={quillRef}
|
||||
modules={modules}
|
||||
formats={formats}
|
||||
/>
|
||||
|
||||
<div className='editor-buttons'>
|
||||
|
||||
221
server/db.js
221
server/db.js
@ -72,6 +72,48 @@ async function initDb() {
|
||||
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));
|
||||
@ -83,7 +125,7 @@ async function initDb() {
|
||||
|
||||
/**
|
||||
* Gets all published articles from the database
|
||||
* @returns {[Object]} - returns an array of all stored articles
|
||||
* @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");
|
||||
@ -197,7 +239,7 @@ function deleteArticle(ka_num) {
|
||||
/**
|
||||
* 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
|
||||
* @returns {Array<Object>} - An arry of articles that match the query content
|
||||
*/
|
||||
function searchArticles(query) {
|
||||
const searchTerm = `%${query}%`;
|
||||
@ -326,7 +368,7 @@ function deleteUser(userId) {
|
||||
|
||||
/**
|
||||
* Gets all users currently stored in the database
|
||||
* @returns {[Object]} - an array of user objects
|
||||
* @returns {Array<Object>} - an array of user objects
|
||||
*/
|
||||
function getAllUsers() {
|
||||
const result = db.exec(
|
||||
@ -351,7 +393,7 @@ function getAllUsers() {
|
||||
/**
|
||||
* Return all draft articles owned by a user
|
||||
* @param {string} author - The author of the articles
|
||||
* @returns {[Object]} - An array of article objects
|
||||
* @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")
|
||||
@ -365,6 +407,167 @@ function getOwnedDrafts(author) {
|
||||
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,
|
||||
@ -380,5 +583,13 @@ module.exports = {
|
||||
updateUserRole,
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
getOwnedDrafts
|
||||
getOwnedDrafts,
|
||||
getAllCategories,
|
||||
getAllTags,
|
||||
getOrCreateCategory,
|
||||
getOrCreateTag,
|
||||
getArticleCategories,
|
||||
getArticleTags,
|
||||
setArticleCategories,
|
||||
setArticleTags
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user