import Database from 'better-sqlite3'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const dbPath = path.join(__dirname, 'data.db'); export const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); // safe migrations — ignore if column already exists for (const sql of [ "ALTER TABLE articles ADD COLUMN status TEXT DEFAULT 'published'", "ALTER TABLE articles ADD COLUMN key_data TEXT", ]) { try { db.exec(sql) } catch {} } db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS articles ( id TEXT PRIMARY KEY, title TEXT NOT NULL, category TEXT, type TEXT, author TEXT, author_role TEXT, author_initial TEXT, publish_date TEXT, pages INTEGER DEFAULT 0, price INTEGER DEFAULT 0, is_free INTEGER DEFAULT 1, summary TEXT, body TEXT, cover_image TEXT, tags TEXT, featured INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_articles_created ON articles(created_at DESC); CREATE INDEX IF NOT EXISTS idx_articles_featured ON articles(featured); CREATE TABLE IF NOT EXISTS risk_signals ( id TEXT PRIMARY KEY, quote TEXT NOT NULL, name TEXT NOT NULL, date TEXT, level TEXT NOT NULL CHECK (level IN ('critical','high','medium','low','opportunity')), sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_risks_sort ON risk_signals(sort_order, created_at); CREATE TABLE IF NOT EXISTS events ( id TEXT PRIMARY KEY, title_fa TEXT NOT NULL, title_en TEXT, date_fa TEXT, date_en TEXT, month_fa TEXT, month_en TEXT, year TEXT, type TEXT CHECK(type IN ('conference','exhibition','seminar','international')), location_fa TEXT, location_en TEXT, city_fa TEXT, city_en TEXT, description_fa TEXT, description_en TEXT, registration_open INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_events_sort ON events(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS vision_items ( id TEXT PRIMARY KEY, icon TEXT, title_fa TEXT NOT NULL, title_en TEXT, desc_fa TEXT, desc_en TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_vision_sort ON vision_items(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS advisory_board ( id TEXT PRIMARY KEY, name_fa TEXT NOT NULL, name_en TEXT, role_fa TEXT, role_en TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_advisory_sort ON advisory_board(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS team_members ( id TEXT PRIMARY KEY, name_fa TEXT NOT NULL, name_en TEXT, role_fa TEXT, role_en TEXT, bio_fa TEXT, bio_en TEXT, expertise TEXT, email TEXT, initial TEXT, photo TEXT, report_count INTEGER DEFAULT 0, is_expert INTEGER DEFAULT 0, expert_room_fa TEXT, expert_room_en TEXT, telegram TEXT, linkedin TEXT, twitter TEXT, website TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_team_sort ON team_members(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS plans ( id TEXT PRIMARY KEY, name_fa TEXT NOT NULL, name_en TEXT, price INTEGER DEFAULT 0, period_fa TEXT, period_en TEXT, features TEXT, badge_fa TEXT, badge_en TEXT, is_featured INTEGER DEFAULT 0, cta_fa TEXT, cta_en TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_plans_sort ON plans(sort_order); CREATE TABLE IF NOT EXISTS radar_items ( id TEXT PRIMARY KEY, category TEXT CHECK(category IN ('market','tech','commodity','geo','energy')), img TEXT, author_name TEXT, author_avatar TEXT, title_fa TEXT, date_fa TEXT, excerpt_fa TEXT, title_en TEXT, date_en TEXT, excerpt_en TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_radar_sort ON radar_items(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS radar_pages ( slug TEXT PRIMARY KEY, source_categories TEXT, label_fa TEXT, label_en TEXT, latest_heading_fa TEXT, latest_heading_en TEXT, featured_title_fa TEXT, featured_title_en TEXT, featured_date_fa TEXT, featured_date_en TEXT, featured_img TEXT, banner_kicker_fa TEXT, banner_kicker_en TEXT, banner_title_fa TEXT, banner_title_en TEXT, banner_desc_fa TEXT, banner_desc_en TEXT, banner_book_img TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_radar_pages_sort ON radar_pages(sort_order); CREATE TABLE IF NOT EXISTS banners ( id TEXT PRIMARY KEY, overline_fa TEXT, overline_en TEXT, title_fa TEXT NOT NULL, title_en TEXT, subtitle_fa TEXT, subtitle_en TEXT, cta_label_fa TEXT, cta_label_en TEXT, cta_url TEXT, image TEXT, position TEXT DEFAULT 'hero', active INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_banners_active ON banners(active, sort_order); CREATE TABLE IF NOT EXISTS factory_reports ( id TEXT PRIMARY KEY, img TEXT, tag_fa TEXT, tag_en TEXT, title_fa TEXT NOT NULL, title_en TEXT, date_fa TEXT, date_en TEXT, excerpt_fa TEXT, excerpt_en TEXT, scroll_dir TEXT CHECK(scroll_dir IN ('left','right','up')) DEFAULT 'up', sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_factory_reports_sort ON factory_reports(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS integrations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT, logo TEXT, accent TEXT, grid_col INTEGER DEFAULT 1, grid_row INTEGER DEFAULT 1, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_integrations_sort ON integrations(sort_order, created_at DESC); CREATE TABLE IF NOT EXISTS institute_stats ( id TEXT PRIMARY KEY, label_fa TEXT NOT NULL, label_en TEXT, value INTEGER DEFAULT 0, suffix_fa TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_institute_stats_sort ON institute_stats(sort_order); CREATE TABLE IF NOT EXISTS market_prices ( id TEXT PRIMARY KEY, name TEXT NOT NULL, value REAL DEFAULT 0, unit TEXT, change REAL DEFAULT 0, change_percent REAL DEFAULT 0, trend TEXT CHECK(trend IN ('up','down','flat')) DEFAULT 'flat', sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_market_prices_sort ON market_prices(sort_order); CREATE TABLE IF NOT EXISTS market_chart_points ( id TEXT PRIMARY KEY, series TEXT NOT NULL CHECK(series IN ('history','export','comparison')), label TEXT NOT NULL, value REAL DEFAULT 0, meta TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_chart_points_series ON market_chart_points(series, sort_order); CREATE TABLE IF NOT EXISTS prices ( symbol TEXT PRIMARY KEY, name TEXT NOT NULL, value REAL, unit TEXT, change_value REAL, change_pct REAL, source_url TEXT, fetched_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS members ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, phone TEXT, avatar TEXT, is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_members_email ON members(email); CREATE TABLE IF NOT EXISTS purchases ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE, article_id TEXT NOT NULL REFERENCES articles(id) ON DELETE CASCADE, amount INTEGER NOT NULL DEFAULT 0, purchased_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(member_id, article_id) ); CREATE INDEX IF NOT EXISTS idx_purchases_member ON purchases(member_id, purchased_at DESC); `); /* idempotent column migrations for already-created tables */ function ensureColumn(table, col, def) { const cols = db.prepare(`PRAGMA table_info(${table})`).all(); if (!cols.some((c) => c.name === col)) { db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`); } } ensureColumn('banners', 'overline_fa', 'TEXT'); ensureColumn('banners', 'overline_en', 'TEXT'); export function rowToPrice(row) { if (!row) return null; return { symbol: row.symbol, name: row.name, value: row.value, unit: row.unit, changeValue: row.change_value, changePct: row.change_pct, sourceUrl: row.source_url, fetchedAt: row.fetched_at, }; } export function upsertPrice(p) { db.prepare(` INSERT INTO prices (symbol, name, value, unit, change_value, change_pct, source_url, fetched_at) VALUES (@symbol, @name, @value, @unit, @change_value, @change_pct, @source_url, datetime('now')) ON CONFLICT(symbol) DO UPDATE SET name = excluded.name, value = excluded.value, unit = excluded.unit, change_value = excluded.change_value, change_pct = excluded.change_pct, source_url = excluded.source_url, fetched_at = excluded.fetched_at `).run({ symbol: p.symbol, name: p.name, value: p.value ?? null, unit: p.unit ?? null, change_value: p.changeValue ?? null, change_pct: p.changePct ?? null, source_url: p.sourceUrl ?? null, }); } export function rowToEvent(row) { if (!row) return null; return { id: row.id, titleFa: row.title_fa, titleEn: row.title_en, dateFa: row.date_fa, dateEn: row.date_en, monthFa: row.month_fa, monthEn: row.month_en, year: row.year, type: row.type, locationFa: row.location_fa, locationEn: row.location_en, cityFa: row.city_fa, cityEn: row.city_en, descriptionFa: row.description_fa, descriptionEn: row.description_en, registrationOpen: !!row.registration_open, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToTeamMember(row) { if (!row) return null; return { id: row.id, nameFa: row.name_fa, nameEn: row.name_en, roleFa: row.role_fa, roleEn: row.role_en, bioFa: row.bio_fa, bioEn: row.bio_en, expertise: row.expertise ? JSON.parse(row.expertise) : [], email: row.email, initial: row.initial, photo: row.photo, reportCount: row.report_count, isExpert: !!row.is_expert, expertRoomFa: row.expert_room_fa, expertRoomEn: row.expert_room_en, telegram: row.telegram, linkedin: row.linkedin, twitter: row.twitter, website: row.website, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToPlan(row) { if (!row) return null; return { id: row.id, nameFa: row.name_fa, nameEn: row.name_en, price: row.price, periodFa: row.period_fa, periodEn: row.period_en, features: row.features ? JSON.parse(row.features) : [], badgeFa: row.badge_fa, badgeEn: row.badge_en, isFeatured: !!row.is_featured, ctaFa: row.cta_fa, ctaEn: row.cta_en, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToRiskSignal(row) { if (!row) return null; return { id: row.id, quote: row.quote, name: row.name, date: row.date, level: row.level, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToFactoryReport(row) { if (!row) return null; return { id: row.id, img: row.img, tagFa: row.tag_fa, tagEn: row.tag_en, titleFa: row.title_fa, titleEn: row.title_en, dateFa: row.date_fa, dateEn: row.date_en, excerptFa: row.excerpt_fa, excerptEn: row.excerpt_en, scrollDir: row.scroll_dir, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToIntegration(row) { if (!row) return null; return { id: row.id, name: row.name, icon: row.icon, logo: row.logo, accent: row.accent, gridCol: row.grid_col, gridRow: row.grid_row, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToInstituteStat(row) { if (!row) return null; return { id: row.id, labelFa: row.label_fa, labelEn: row.label_en, value: row.value, suffixFa: row.suffix_fa, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToMarketPrice(row) { if (!row) return null; return { id: row.id, name: row.name, value: row.value, unit: row.unit, change: row.change, changePercent: row.change_percent, trend: row.trend, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToMarketChartPoint(row) { if (!row) return null; return { id: row.id, series: row.series, label: row.label, value: row.value, meta: row.meta, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToVisionItem(row) { if (!row) return null; return { id: row.id, icon: row.icon, titleFa: row.title_fa, titleEn: row.title_en, descFa: row.desc_fa, descEn: row.desc_en, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToAdvisoryMember(row) { if (!row) return null; return { id: row.id, nameFa: row.name_fa, nameEn: row.name_en, roleFa: row.role_fa, roleEn: row.role_en, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToRadarItem(row) { if (!row) return null; return { id: row.id, category: row.category, img: row.img, authorName: row.author_name, authorAvatar: row.author_avatar, titleFa: row.title_fa, dateFa: row.date_fa, excerptFa: row.excerpt_fa, titleEn: row.title_en, dateEn: row.date_en, excerptEn: row.excerpt_en, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToRadarPage(row) { if (!row) return null; return { slug: row.slug, sourceCategories: row.source_categories ? JSON.parse(row.source_categories) : [], labelFa: row.label_fa, labelEn: row.label_en, latestHeadingFa: row.latest_heading_fa, latestHeadingEn: row.latest_heading_en, featuredTitleFa: row.featured_title_fa, featuredTitleEn: row.featured_title_en, featuredDateFa: row.featured_date_fa, featuredDateEn: row.featured_date_en, featuredImg: row.featured_img, bannerKickerFa: row.banner_kicker_fa, bannerKickerEn: row.banner_kicker_en, bannerTitleFa: row.banner_title_fa, bannerTitleEn: row.banner_title_en, bannerDescFa: row.banner_desc_fa, bannerDescEn: row.banner_desc_en, bannerBookImg: row.banner_book_img, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToBanner(row) { if (!row) return null; return { id: row.id, overlineFa: row.overline_fa, overlineEn: row.overline_en, titleFa: row.title_fa, titleEn: row.title_en, subtitleFa: row.subtitle_fa, subtitleEn: row.subtitle_en, ctaLabelFa: row.cta_label_fa, ctaLabelEn: row.cta_label_en, ctaUrl: row.cta_url, image: row.image, position: row.position, active: !!row.active, sortOrder: row.sort_order, createdAt: row.created_at, updatedAt: row.updated_at, }; } export function rowToArticle(row) { if (!row) return null; return { id: row.id, title: row.title, category: row.category, type: row.type, author: row.author, authorRole: row.author_role, authorInitial: row.author_initial, publishDate: row.publish_date, pages: row.pages, price: row.price, isFree: !!row.is_free, summary: row.summary, body: row.body, coverImage: row.cover_image, tags: row.tags ? JSON.parse(row.tags) : [], featured: !!row.featured, createdAt: row.created_at, updatedAt: row.updated_at, }; }