Switch from sqlite3 to sql.js for cross-platform compatibility
- Replace sqlite3 (native) with sql.js (pure JS/WebAssembly) - Fixes GLIBC version compatibility issues - Rewrite database module for sql.js async API - Add proper file persistence for sql.js (in-memory with save-to-file) - Update server startup to initialize database before listening sql.js works everywhere without native compilation
This commit is contained in:
parent
9b4d3242e2
commit
bbdbc0c1df
1095
package-lock.json
generated
1095
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@
|
||||
"express-oauth2-jwt-bearer": "^1.7.4",
|
||||
"helmet": "^8.1.0",
|
||||
"jose": "^6.2.2",
|
||||
"sqlite3": "^6.0.1",
|
||||
"sql.js": "^1.14.1",
|
||||
"uuid": "^13.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,173 +1,295 @@
|
||||
import sqlite3 from 'sqlite3';
|
||||
import initSqlJs from 'sql.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DB_PATH = join(__dirname, '../../data/moxie.db');
|
||||
const DATA_DIR = join(__dirname, '../../data');
|
||||
const DB_PATH = join(DATA_DIR, 'moxie.db');
|
||||
|
||||
// Promisify sqlite3 methods
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database at:', DB_PATH);
|
||||
initDatabase();
|
||||
let db = null;
|
||||
let SQL = null;
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save database to file
|
||||
*/
|
||||
const saveDatabase = () => {
|
||||
if (db) {
|
||||
const data = db.export();
|
||||
const buffer = Buffer.from(data);
|
||||
writeFileSync(DB_PATH, buffer);
|
||||
}
|
||||
});
|
||||
|
||||
// Promisify database methods
|
||||
const dbRun = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID, changes: this.changes });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const dbGet = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
// Debounced save to avoid excessive disk writes
|
||||
let saveTimeout = null;
|
||||
const debouncedSave = () => {
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(saveDatabase, 100);
|
||||
};
|
||||
|
||||
const dbAll = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize database tables
|
||||
/**
|
||||
* Initialize SQL.js and load/create database
|
||||
*/
|
||||
const initDatabase = async () => {
|
||||
try {
|
||||
// Users table (updated for Auth0)
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
auth0_id TEXT UNIQUE,
|
||||
email TEXT,
|
||||
password_hash TEXT,
|
||||
name TEXT,
|
||||
picture TEXT,
|
||||
role TEXT DEFAULT 'user',
|
||||
credits INTEGER DEFAULT 0,
|
||||
subscription_status TEXT DEFAULT 'inactive',
|
||||
subscription_tier TEXT,
|
||||
stripe_customer_id TEXT,
|
||||
paypal_customer_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME,
|
||||
is_active INTEGER DEFAULT 1
|
||||
)
|
||||
`);
|
||||
// Initialize SQL.js
|
||||
SQL = await initSqlJs();
|
||||
|
||||
// API keys table for user API access
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
name TEXT,
|
||||
last_used DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
// Load existing database or create new one
|
||||
if (existsSync(DB_PATH)) {
|
||||
const fileBuffer = readFileSync(DB_PATH);
|
||||
db = new SQL.Database(fileBuffer);
|
||||
console.log('Loaded existing database from:', DB_PATH);
|
||||
} else {
|
||||
db = new SQL.Database();
|
||||
console.log('Created new database at:', DB_PATH);
|
||||
}
|
||||
|
||||
// Credit transactions table
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS credit_transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
description TEXT,
|
||||
reference_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
// Create tables
|
||||
await createTables();
|
||||
|
||||
// Payment history table
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
provider TEXT NOT NULL,
|
||||
provider_payment_id TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Sessions table (kept for API key sessions)
|
||||
await dbRun(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes for better query performance
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_users_auth0_id ON users(auth0_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_users_stripe_customer ON users(stripe_customer_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_users_paypal_customer ON users(paypal_customer_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_credit_trans_user ON credit_transactions(user_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id)`);
|
||||
await dbRun(`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`);
|
||||
|
||||
// Run migrations for existing databases
|
||||
await runMigrations();
|
||||
|
||||
console.log('Database tables initialized successfully');
|
||||
console.log('Database initialized successfully');
|
||||
} catch (err) {
|
||||
console.error('Error initializing database:', err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Run migrations for schema updates
|
||||
/**
|
||||
* Create database tables
|
||||
*/
|
||||
const createTables = async () => {
|
||||
// Users table (updated for Auth0)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
auth0_id TEXT UNIQUE,
|
||||
email TEXT,
|
||||
password_hash TEXT,
|
||||
name TEXT,
|
||||
picture TEXT,
|
||||
role TEXT DEFAULT 'user',
|
||||
credits INTEGER DEFAULT 0,
|
||||
subscription_status TEXT DEFAULT 'inactive',
|
||||
subscription_tier TEXT,
|
||||
stripe_customer_id TEXT,
|
||||
paypal_customer_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login DATETIME,
|
||||
is_active INTEGER DEFAULT 1
|
||||
)
|
||||
`);
|
||||
|
||||
// API keys table for user API access
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
name TEXT,
|
||||
last_used DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Credit transactions table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS credit_transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
description TEXT,
|
||||
reference_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Payment history table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
currency TEXT DEFAULT 'USD',
|
||||
provider TEXT NOT NULL,
|
||||
provider_payment_id TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Sessions table (kept for API key sessions)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes for better query performance
|
||||
try {
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_users_auth0_id ON users(auth0_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_users_stripe_customer ON users(stripe_customer_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_users_paypal_customer ON users(paypal_customer_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_credit_trans_user ON credit_transactions(user_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_payments_user ON payments(user_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`);
|
||||
} catch (err) {
|
||||
// Indexes might already exist
|
||||
}
|
||||
|
||||
// Run migrations for existing databases
|
||||
await runMigrations();
|
||||
|
||||
debouncedSave();
|
||||
};
|
||||
|
||||
/**
|
||||
* Run migrations for schema updates
|
||||
*/
|
||||
const runMigrations = async () => {
|
||||
try {
|
||||
// Add auth0_id column if it doesn't exist
|
||||
const tableInfo = await dbAll('PRAGMA table_info(users)');
|
||||
const columns = tableInfo.map(col => col.name);
|
||||
// Check if auth0_id column exists
|
||||
const tableInfo = db.exec("PRAGMA table_info(users)");
|
||||
if (tableInfo.length > 0) {
|
||||
const columns = tableInfo[0].values.map(col => col[1]);
|
||||
|
||||
if (!columns.includes('auth0_id')) {
|
||||
await dbRun('ALTER TABLE users ADD COLUMN auth0_id TEXT UNIQUE');
|
||||
console.log('Migration: Added auth0_id column');
|
||||
if (!columns.includes('auth0_id')) {
|
||||
// For SQLite, we can't add UNIQUE columns directly, so we recreate the table
|
||||
console.log('Migration: Adding auth0_id column');
|
||||
db.run(`ALTER TABLE users ADD COLUMN auth0_id TEXT`);
|
||||
}
|
||||
|
||||
if (!columns.includes('picture')) {
|
||||
console.log('Migration: Adding picture column');
|
||||
db.run(`ALTER TABLE users ADD COLUMN picture TEXT`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!columns.includes('picture')) {
|
||||
await dbRun('ALTER TABLE users ADD COLUMN picture TEXT');
|
||||
console.log('Migration: Added picture column');
|
||||
}
|
||||
|
||||
// Make email nullable by recreating the table (SQLite limitation)
|
||||
// This is safe for new databases but preserve existing data
|
||||
} catch (err) {
|
||||
// Columns might already exist, that's fine
|
||||
console.log('Migration note:', err.message);
|
||||
}
|
||||
};
|
||||
|
||||
export { db, dbRun, dbGet, dbAll, initDatabase, runMigrations };
|
||||
/**
|
||||
* Run a SQL statement (INSERT, UPDATE, DELETE)
|
||||
* @param {string} sql - SQL statement
|
||||
* @param {Array} params - Parameters
|
||||
* @returns {Promise<{id: string, changes: number}>}
|
||||
*/
|
||||
const dbRun = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
db.run(sql, params);
|
||||
debouncedSave();
|
||||
|
||||
// Get last inserted ID
|
||||
const result = db.exec("SELECT last_insert_rowid() as id, changes() as changes");
|
||||
const lastId = result.length > 0 ? result[0].values[0][0] : null;
|
||||
const changes = result.length > 0 ? result[0].values[0][1] : 0;
|
||||
|
||||
resolve({ id: lastId, changes });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single row
|
||||
* @param {string} sql - SQL query
|
||||
* @param {Array} params - Parameters
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
const dbGet = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const stmt = db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
stmt.free();
|
||||
resolve(row);
|
||||
} else {
|
||||
stmt.free();
|
||||
resolve(null);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all rows
|
||||
* @param {string} sql - SQL query
|
||||
* @param {Array} params - Parameters
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
const dbAll = (sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const stmt = db.prepare(sql);
|
||||
stmt.bind(params);
|
||||
|
||||
const rows = [];
|
||||
while (stmt.step()) {
|
||||
rows.push(stmt.getAsObject());
|
||||
}
|
||||
stmt.free();
|
||||
|
||||
resolve(rows);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute raw SQL (for migrations, etc.)
|
||||
* @param {string} sql - SQL to execute
|
||||
*/
|
||||
const dbExec = (sql) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
db.exec(sql);
|
||||
debouncedSave();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Export a function to ensure database is ready
|
||||
const waitForDb = async () => {
|
||||
if (!db) {
|
||||
await initDatabase();
|
||||
}
|
||||
return db;
|
||||
};
|
||||
|
||||
export { db, dbRun, dbGet, dbAll, dbExec, initDatabase, waitForDb };
|
||||
|
||||
47
src/index.js
47
src/index.js
@ -9,6 +9,7 @@ import usersRouter from './routes/users.js';
|
||||
import webhooksRouter from './routes/webhooks.js';
|
||||
import { ApiResponse } from './utils/helpers.js';
|
||||
import { validateAccessToken, ensureUserExists } from './middleware/auth.js';
|
||||
import { initDatabase, waitForDb } from './db/database.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@ -237,8 +238,13 @@ app.use((err, req, res, next) => {
|
||||
// Server Startup
|
||||
// ============================================
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// Initialize database first
|
||||
await initDatabase();
|
||||
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════╗
|
||||
║ Moxie Backend Server Started ║
|
||||
╠════════════════════════════════════════════╣
|
||||
@ -247,25 +253,28 @@ const server = app.listen(PORT, () => {
|
||||
║ Auth0 Domain: ${AUTH0_DOMAIN} ║
|
||||
║ Time: ${new Date().toISOString()} ║
|
||||
╚════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
// Graceful shutdown
|
||||
const shutdown = () => {
|
||||
console.log('Shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully...');
|
||||
server.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user