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:
Z User 2026-03-27 22:38:36 +00:00
parent 9b4d3242e2
commit bbdbc0c1df
4 changed files with 298 additions and 1252 deletions

1095
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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 };

View File

@ -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) => {