Spaces:
Sleeping
Sleeping
File size: 7,303 Bytes
92a138e f4623fe 92a138e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const path = require('path');
const fs = require('fs');
const logger = require('./logger');
const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../data/resume_builder.db');
// Ensure data directory exists
const dataDir = path.dirname(DB_PATH);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
let db;
async function getDb() {
if (!db) {
db = await open({
filename: DB_PATH,
driver: sqlite3.Database
});
// Enable WAL mode and foreign keys
await db.run('PRAGMA journal_mode = WAL');
await db.run('PRAGMA foreign_keys = ON');
await db.run('PRAGMA synchronous = NORMAL');
logger.info(`SQLite connected: ${DB_PATH}`);
await initSchema(db);
}
return db;
}
async function initSchema(db) {
await db.exec(`
-- βββ Users βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
phone TEXT,
avatar_url TEXT,
plan TEXT NOT NULL DEFAULT 'free',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- βββ Templates ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL DEFAULT 'modern',
layout TEXT NOT NULL DEFAULT 'single',
thumbnail_url TEXT,
html_content TEXT NOT NULL,
css_content TEXT NOT NULL,
is_ats_safe INTEGER NOT NULL DEFAULT 1,
is_premium INTEGER NOT NULL DEFAULT 0,
tags TEXT NOT NULL DEFAULT '[]',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- βββ Resumes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS resumes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
template_id TEXT REFERENCES templates(id),
title TEXT NOT NULL DEFAULT 'Untitled Resume',
slug TEXT,
content TEXT NOT NULL DEFAULT '{}',
design TEXT NOT NULL DEFAULT '{}',
section_order TEXT NOT NULL DEFAULT '[]',
ats_score INTEGER DEFAULT 0,
ats_feedback TEXT DEFAULT '[]',
is_public INTEGER NOT NULL DEFAULT 0,
last_exported TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_resumes_user ON resumes(user_id);
-- βββ Resume Versions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS resume_versions (
id TEXT PRIMARY KEY,
resume_id TEXT NOT NULL REFERENCES resumes(id) ON DELETE CASCADE,
version_num INTEGER NOT NULL,
snapshot TEXT NOT NULL,
label TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_versions_resume ON resume_versions(resume_id);
-- βββ Exports ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS exports (
id TEXT PRIMARY KEY,
resume_id TEXT NOT NULL REFERENCES resumes(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
format TEXT NOT NULL DEFAULT 'docx',
file_path TEXT NOT NULL,
file_size INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_exports_resume ON exports(resume_id);
-- βββ Import Jobs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS import_jobs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
resume_id TEXT REFERENCES resumes(id) ON DELETE SET NULL,
source_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
extracted TEXT DEFAULT '{}',
error TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- βββ Writing Tips βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATE TABLE IF NOT EXISTS writing_tips (
id TEXT PRIMARY KEY,
section TEXT NOT NULL,
tip TEXT NOT NULL,
example TEXT,
tags TEXT DEFAULT '[]'
);
`);
await db.run(`
INSERT OR IGNORE INTO users (id, email, password, name, plan)
VALUES ('guest_user', 'guest@resumepro.local', 'none', 'Guest User', 'free');
`);
try {
await db.exec('ALTER TABLE users ADD COLUMN phone TEXT');
} catch (err) {
// Column likely already exists
}
logger.info('Database schema initialized and guest user ensured');
// Auto-seed if empty
const templateCount = await db.get('SELECT COUNT(*) as count FROM templates');
if (templateCount.count === 0) {
logger.info('Templates table is empty. Triggering auto-seed...');
try {
// We require it here to avoid circular dependency if any
const { templates, writingTips } = require('../scripts/seedData');
for (const tmpl of templates) {
await db.run(
`INSERT INTO templates (id, name, description, category, layout, thumbnail_url, html_content, css_content, is_ats_safe, is_premium, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
tmpl.id, tmpl.name, tmpl.description, tmpl.category, tmpl.layout, tmpl.thumbnail_url, tmpl.html_content, tmpl.css_content, tmpl.is_ats_safe, tmpl.is_premium, tmpl.tags
);
}
for (const tip of writingTips) {
await db.run('INSERT INTO writing_tips (id, section, tip, example) VALUES (?, ?, ?, ?)', tip.id, tip.section, tip.tip, tip.example);
}
logger.info(`Successfully auto-seeded ${templates.length} templates.`);
} catch (seedErr) {
logger.error('Auto-seed failed:', seedErr);
}
}
}
module.exports = { getDb };
|