472 lines
20 KiB
JavaScript
472 lines
20 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const { DatabaseSync } = require("node:sqlite");
|
|
const config = require("./config");
|
|
const { now, makeSlug } = require("./utils");
|
|
|
|
fs.mkdirSync(path.dirname(config.databasePath), { recursive: true });
|
|
fs.mkdirSync(config.uploadDir, { recursive: true });
|
|
fs.mkdirSync(config.backupDir, { recursive: true });
|
|
|
|
const db = new DatabaseSync(config.databasePath);
|
|
db.exec("PRAGMA journal_mode = WAL");
|
|
db.exec("PRAGMA foreign_keys = ON");
|
|
|
|
function migrate() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS administrators (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'owner',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
last_login_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
sid TEXT PRIMARY KEY,
|
|
sess TEXT NOT NULL,
|
|
expired_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
subtitle TEXT,
|
|
intro TEXT,
|
|
header_image_id INTEGER,
|
|
background_image_id INTEGER,
|
|
meta_title TEXT,
|
|
meta_description TEXT,
|
|
og_title TEXT,
|
|
og_description TEXT,
|
|
og_image_id INTEGER,
|
|
canonical_url TEXT,
|
|
search_visible INTEGER NOT NULL DEFAULT 1,
|
|
status TEXT NOT NULL DEFAULT 'published',
|
|
draft_json TEXT,
|
|
published_at TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(header_image_id) REFERENCES media(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(background_image_id) REFERENCES media(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(og_image_id) REFERENCES media(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS page_sections (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
page_id INTEGER NOT NULL,
|
|
section_key TEXT,
|
|
title TEXT,
|
|
body TEXT,
|
|
layout TEXT NOT NULL DEFAULT 'text',
|
|
image_id INTEGER,
|
|
background_image_id INTEGER,
|
|
background_color TEXT,
|
|
button_label TEXT,
|
|
button_url TEXT,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
is_published INTEGER NOT NULL DEFAULT 1,
|
|
data_json TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(page_id) REFERENCES pages(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(image_id) REFERENCES media(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(background_image_id) REFERENCES media(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS media (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
file_name TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
title TEXT,
|
|
alt_text TEXT,
|
|
category TEXT,
|
|
mime_type TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
width INTEGER,
|
|
height INTEGER,
|
|
url TEXT NOT NULL,
|
|
thumb_url TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS categories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
display_order INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
slug TEXT NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
short_description TEXT,
|
|
full_description TEXT,
|
|
category_id INTEGER,
|
|
main_image_id INTEGER,
|
|
price TEXT,
|
|
starting_price TEXT,
|
|
price_range TEXT,
|
|
contact_for_pricing INTEGER NOT NULL DEFAULT 0,
|
|
quantity TEXT,
|
|
availability_status TEXT NOT NULL DEFAULT 'Available',
|
|
custom_order_available INTEGER NOT NULL DEFAULT 0,
|
|
color_size_notes TEXT,
|
|
material_notes TEXT,
|
|
care_instructions TEXT,
|
|
featured INTEGER NOT NULL DEFAULT 0,
|
|
newly_added INTEGER NOT NULL DEFAULT 0,
|
|
on_sale INTEGER NOT NULL DEFAULT 0,
|
|
sale_text TEXT,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
is_published INTEGER NOT NULL DEFAULT 1,
|
|
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
draft_json TEXT,
|
|
date_added TEXT NOT NULL,
|
|
published_at TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(category_id) REFERENCES categories(id) ON DELETE SET NULL,
|
|
FOREIGN KEY(main_image_id) REFERENCES media(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS item_images (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
item_id INTEGER NOT NULL,
|
|
media_id INTEGER NOT NULL,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
is_main INTEGER NOT NULL DEFAULT 0,
|
|
FOREIGN KEY(item_id) REFERENCES items(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(media_id) REFERENCES media(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS item_tags (
|
|
item_id INTEGER NOT NULL,
|
|
tag_id INTEGER NOT NULL,
|
|
PRIMARY KEY(item_id, tag_id),
|
|
FOREIGN KEY(item_id) REFERENCES items(id) ON DELETE CASCADE,
|
|
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS services (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL UNIQUE,
|
|
image_id INTEGER,
|
|
short_description TEXT,
|
|
full_description TEXT,
|
|
pricing_note TEXT,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
featured INTEGER NOT NULL DEFAULT 0,
|
|
is_published INTEGER NOT NULL DEFAULT 1,
|
|
draft_json TEXT,
|
|
published_at TEXT,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(image_id) REFERENCES media(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS equipment (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT,
|
|
brand TEXT,
|
|
model TEXT,
|
|
description TEXT,
|
|
image_id INTEGER,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
is_published INTEGER NOT NULL DEFAULT 1,
|
|
updated_at TEXT NOT NULL,
|
|
FOREIGN KEY(image_id) REFERENCES media(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL,
|
|
phone TEXT,
|
|
preferred_contact TEXT,
|
|
subject TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
related_item TEXT,
|
|
related_service TEXT,
|
|
consent INTEGER NOT NULL DEFAULT 0,
|
|
source_page TEXT,
|
|
status TEXT NOT NULL DEFAULT 'unread',
|
|
spam_status TEXT NOT NULL DEFAULT 'clean',
|
|
ip_hash TEXT,
|
|
user_agent TEXT,
|
|
email_notification_status TEXT NOT NULL DEFAULT 'not_configured',
|
|
email_error TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS navigation (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
label TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
kind TEXT NOT NULL DEFAULT 'internal',
|
|
opens_new_tab INTEGER NOT NULL DEFAULT 0,
|
|
display_order INTEGER NOT NULL DEFAULT 0,
|
|
is_published INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS revisions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
record_type TEXT NOT NULL,
|
|
record_id INTEGER NOT NULL,
|
|
title TEXT,
|
|
snapshot_json TEXT NOT NULL,
|
|
created_by INTEGER,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY(created_by) REFERENCES administrators(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
admin_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
details TEXT,
|
|
ip_address TEXT,
|
|
created_at TEXT NOT NULL,
|
|
FOREIGN KEY(admin_id) REFERENCES administrators(id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS backups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
file_name TEXT NOT NULL,
|
|
file_path TEXT NOT NULL,
|
|
size_bytes INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
key TEXT PRIMARY KEY,
|
|
count INTEGER NOT NULL,
|
|
reset_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_items_public ON items(is_published, is_archived, name);
|
|
CREATE INDEX IF NOT EXISTS idx_items_flags ON items(featured, newly_added, on_sale);
|
|
CREATE INDEX IF NOT EXISTS idx_services_public ON services(is_published, display_order);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_status ON messages(status, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_page_sections_order ON page_sections(page_id, display_order);
|
|
CREATE INDEX IF NOT EXISTS idx_revisions_record ON revisions(record_type, record_id, created_at);
|
|
`);
|
|
}
|
|
|
|
function setSetting(key, value) {
|
|
db.prepare(`
|
|
INSERT INTO settings (key, value, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
`).run(key, String(value ?? ""), now());
|
|
}
|
|
|
|
function getSetting(key, fallback = "") {
|
|
const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
|
|
return row ? row.value : fallback;
|
|
}
|
|
|
|
function allSettings() {
|
|
const rows = db.prepare("SELECT key, value FROM settings").all();
|
|
return Object.fromEntries(rows.map((row) => [row.key, row.value]));
|
|
}
|
|
|
|
function ensureCategory(name) {
|
|
const slug = makeSlug(name, "category");
|
|
db.prepare(`
|
|
INSERT INTO categories (name, slug, display_order)
|
|
VALUES (?, ?, (SELECT COALESCE(MAX(display_order), 0) + 10 FROM categories))
|
|
ON CONFLICT(name) DO NOTHING
|
|
`).run(name, slug);
|
|
return db.prepare("SELECT id FROM categories WHERE name = ?").get(name).id;
|
|
}
|
|
|
|
function seed() {
|
|
const seeded = getSetting("system.seeded", "");
|
|
if (seeded) return;
|
|
|
|
const ts = now();
|
|
const settings = {
|
|
"system.seeded": ts,
|
|
"business.name": "Debbie Windler Seamstress",
|
|
"business.tagline": "Alterations, custom sewing, embroidery, and handmade items",
|
|
"business.email": "replace-with-owner-email@example.com",
|
|
"business.phone": "",
|
|
"business.facebook": "",
|
|
"business.service_area": "Service area to be supplied",
|
|
"business.hours": "By appointment",
|
|
"business.preferred_contact": "Email or website contact form",
|
|
"business.footer_text": "Warm, careful sewing work for everyday clothing and special occasions.",
|
|
"site.title": "Debbie Windler Seamstress",
|
|
"site.description": "Alterations, custom sewing, embroidery, and handmade items.",
|
|
"theme.bg": "#fffaf5",
|
|
"theme.bg_alt": "#f4e8df",
|
|
"theme.text": "#342b28",
|
|
"theme.heading": "#2d2522",
|
|
"theme.link": "#7d3f45",
|
|
"theme.button": "#7d3f45",
|
|
"theme.button_text": "#ffffff",
|
|
"theme.accent": "#7a8f73",
|
|
"theme.radius": "8",
|
|
"theme.spacing": "comfortable",
|
|
"theme.width": "1120",
|
|
"theme.heading_font": "Georgia, 'Times New Roman', serif",
|
|
"theme.body_font": "Arial, Helvetica, sans-serif",
|
|
"theme.footer_style": "light",
|
|
"home.hero.title": "Debbie Windler Seamstress",
|
|
"home.hero.subtitle": "Alterations, custom sewing, embroidery, and handmade pieces, with sample wording ready for Debbie to replace.",
|
|
"home.hero.contact": "Contact Debbie to talk through your project.",
|
|
"home.hero.image": "/uploads/sample-hero.png",
|
|
"home.hero.position": "center",
|
|
"home.hero.size": "cover",
|
|
"home.hero.overlay": "light",
|
|
"home.hero.overlay_opacity": "0.28",
|
|
"home.hero.text_color": "#2d2522",
|
|
"home.hero.button1_label": "Contact Debbie",
|
|
"home.hero.button1_url": "/about-contact#contact-form",
|
|
"home.hero.button2_label": "View Items",
|
|
"home.hero.button2_url": "/items",
|
|
"home.highlight.title": "Featured and Newly Added",
|
|
"home.highlight.intro": "Sample cards show how items can be featured, marked new, or offered as custom orders.",
|
|
"home.highlight.background_color": "#f4e8df",
|
|
"maintenance.enabled": "false",
|
|
"maintenance.message": "The website is getting a careful refresh. Please check back soon.",
|
|
"maintenance.return_note": "",
|
|
"maintenance.artwork": ""
|
|
};
|
|
for (const [key, value] of Object.entries(settings)) setSetting(key, value);
|
|
|
|
const pageInsert = db.prepare(`
|
|
INSERT INTO pages (slug, title, subtitle, intro, meta_title, meta_description, status, published_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 'published', ?, ?)
|
|
`);
|
|
const pages = [
|
|
["home", "Home", "Welcome to Debbie Windler Seamstress", "Sample home page copy can be replaced from the owner area."],
|
|
["items", "Items Available and Custom Orders", "Browse sample items alphabetically.", "Items can be available, made to order, on sale, sold as examples, seasonal, or hidden."],
|
|
["services", "Services", "Alterations, repairs, custom sewing, and embroidery.", "Service examples are seeded so Debbie can edit or remove them."],
|
|
["about-contact", "About and Contact", "A personal introduction and the full contact form.", "This page keeps Debbie's contact details editable and does not show a private home address by default."]
|
|
];
|
|
for (const page of pages) pageInsert.run(page[0], page[1], page[2], page[3], page[1], page[3], ts, ts);
|
|
|
|
const pageBySlug = db.prepare("SELECT id FROM pages WHERE slug = ?");
|
|
const sectionInsert = db.prepare(`
|
|
INSERT INTO page_sections
|
|
(page_id, section_key, title, body, layout, display_order, is_published, data_json, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
`);
|
|
sectionInsert.run(pageBySlug.get("home").id, "intro", "Sewing Services With a Personal Touch", "<p>This sample introduction can describe Debbie's experience, favorite work, and how customers should reach out.</p>", "image-right", 10, "{}", ts);
|
|
sectionInsert.run(pageBySlug.get("home").id, "contact-callout", "Have a project in mind?", "<p>Use the contact form to ask about alterations, embroidery, handmade items, or special orders.</p>", "contact-callout", 30, "{}", ts);
|
|
sectionInsert.run(pageBySlug.get("services").id, "service-note", "Not sure what to ask for?", "<p>Send a note with the garment or project details. Photos can be discussed later by email if needed.</p>", "contact-callout", 20, "{}", ts);
|
|
sectionInsert.run(pageBySlug.get("about-contact").id, "about", "About Debbie", "<p>Sample biography text: Debbie's real sewing history, experience, and photos can be added here from the owner area.</p>", "text", 10, "{}", ts);
|
|
|
|
const categories = ["Alterations", "Handmade Items", "Embroidery", "Custom Orders", "Seasonal"];
|
|
const categoryIds = Object.fromEntries(categories.map((name) => [name, ensureCategory(name)]));
|
|
const itemInsert = db.prepare(`
|
|
INSERT INTO items
|
|
(name, slug, short_description, full_description, category_id, price, starting_price, price_range, contact_for_pricing,
|
|
availability_status, custom_order_available, material_notes, care_instructions, featured, newly_added, on_sale,
|
|
sale_text, display_order, is_published, date_added, published_at, updated_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
|
|
`);
|
|
const sampleItems = [
|
|
["Baby Blanket Example", "Soft sample listing for a custom baby blanket.", "<p>Replace this with true details, sizes, fabrics, and ordering notes.</p>", "Handmade Items", "", "Contact for starting price", "", 1, "Made to Order", 1, "Fabric details to be supplied.", "Care instructions to be supplied.", 1, 1, 0, "", 10],
|
|
["Embroidered Tote Bag", "Sample tote bag entry for embroidery or custom wording.", "<p>This is sample content only and can be changed or hidden.</p>", "Embroidery", "", "", "", 1, "Custom Order", 1, "Canvas or fabric notes can go here.", "Spot clean or care note to be supplied.", 1, 1, 0, "", 20],
|
|
["Formalwear Alteration Example", "A sold/example listing showing previous-style work without checkout.", "<p>Use sold examples to show the type of work Debbie can discuss with customers.</p>", "Alterations", "", "", "", 1, "Sold", 0, "", "", 0, 0, 0, "", 30],
|
|
["Seasonal Pillow Cover", "Sample seasonal handmade item.", "<p>Mark items seasonal, available, hidden, sold, or made to order.</p>", "Seasonal", "$00 sample", "", "", 0, "Available", 1, "Material notes to be supplied.", "Care instructions to be supplied.", 1, 0, 1, "Sample sale badge", 40]
|
|
];
|
|
for (const item of sampleItems) {
|
|
itemInsert.run(item[0], makeSlug(item[0], "item"), item[1], item[2], categoryIds[item[3]], item[4], item[5], item[6], item[7], item[8], item[9], item[10], item[11], item[12], item[13], item[14], item[15], item[16], ts, ts, ts);
|
|
}
|
|
|
|
const serviceInsert = db.prepare(`
|
|
INSERT INTO services
|
|
(name, slug, short_description, full_description, pricing_note, display_order, featured, is_published, published_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
`);
|
|
[
|
|
["Clothing Alterations", "Hems, fit adjustments, and everyday garment alterations.", "<p>Sample service details. Debbie can replace this with her exact services and policies.</p>", "Pricing depends on the garment and work needed.", 10, 1],
|
|
["Zipper Repair and Replacement", "Repair or replace zippers on clothing and fabric items.", "<p>Describe accepted items and turnaround once details are known.</p>", "Contact for pricing.", 20, 1],
|
|
["Wedding Dress Alterations", "Fittings and alterations for wedding dresses and formalwear.", "<p>Sample text only. Add appointment expectations and timing when ready.</p>", "Contact early for availability.", 30, 1],
|
|
["Embroidery", "Custom embroidery projects and decorative additions.", "<p>Describe machine embroidery options, setup needs, and project limits.</p>", "Contact for pricing.", 40, 1],
|
|
["Baby Blankets", "Handmade or custom baby blanket projects.", "<p>Describe fabric choices, size options, and custom order timing.</p>", "Starting price to be supplied.", 50, 0],
|
|
["Patches Sewn Onto Clothing", "Patch placement and sewing for jackets, uniforms, and garments.", "<p>Describe accepted materials and any preparation instructions.</p>", "Contact for pricing.", 60, 0]
|
|
].forEach((service) => serviceInsert.run(service[0], makeSlug(service[0], "service"), service[1], service[2], service[3], service[4], service[5], ts, ts));
|
|
|
|
const equipmentInsert = db.prepare(`
|
|
INSERT INTO equipment (name, type, brand, model, description, display_order, is_published, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, 1, ?)
|
|
`);
|
|
equipmentInsert.run("Industrial Sewing Machine", "Sewing machine", "", "", "Sample equipment entry. Add the real brand, model, and photo when available.", 10, ts);
|
|
equipmentInsert.run("Embroidery Machine", "Embroidery machine", "", "", "Sample equipment entry for embroidery work.", 20, ts);
|
|
equipmentInsert.run("Thread Rack", "Supplies", "", "", "Sample equipment entry for thread colors and materials.", 30, ts);
|
|
|
|
const navInsert = db.prepare("INSERT INTO navigation (label, url, kind, display_order, is_published) VALUES (?, ?, 'internal', ?, 1)");
|
|
[
|
|
["Home", "/", 10],
|
|
["Items", "/items", 20],
|
|
["Services", "/services", 30],
|
|
["About and Contact", "/about-contact", 40]
|
|
].forEach((nav) => navInsert.run(...nav));
|
|
}
|
|
|
|
function saveRevision(type, id, title, snapshot, adminId) {
|
|
db.prepare(`
|
|
INSERT INTO revisions (record_type, record_id, title, snapshot_json, created_by, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`).run(type, id, title || "", JSON.stringify(snapshot), adminId || null, now());
|
|
|
|
const old = db.prepare(`
|
|
SELECT id FROM revisions
|
|
WHERE record_type = ? AND record_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT -1 OFFSET 10
|
|
`).all(type, id);
|
|
if (old.length) {
|
|
db.prepare(`DELETE FROM revisions WHERE id IN (${old.map(() => "?").join(",")})`).run(...old.map((row) => row.id));
|
|
}
|
|
}
|
|
|
|
function audit(adminId, action, details, ipAddress) {
|
|
db.prepare(`
|
|
INSERT INTO audit_logs (admin_id, action, details, ip_address, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(adminId || null, action, details || "", ipAddress || "", now());
|
|
}
|
|
|
|
migrate();
|
|
seed();
|
|
|
|
if (require.main === module) {
|
|
console.log(`Database ready at ${config.databasePath}`);
|
|
}
|
|
|
|
module.exports = {
|
|
db,
|
|
migrate,
|
|
seed,
|
|
setSetting,
|
|
getSetting,
|
|
allSettings,
|
|
ensureCategory,
|
|
saveRevision,
|
|
audit
|
|
};
|