Add settings page, port to turso

This commit is contained in:
2025-07-12 20:10:39 -04:00
parent 446e3abb0d
commit 07f190bce2
17 changed files with 3356 additions and 92 deletions

109
scripts/export-data.js Normal file
View File

@@ -0,0 +1,109 @@
import { execSync } from "child_process";
import { readFileSync, writeFileSync, existsSync } from "fs";
async function exportData() {
console.log("📦 Exporting data from local SQLite database...\n");
try {
// Check if local database exists
if (!existsSync("./db.sqlite")) {
console.error("❌ Local database db.sqlite not found!");
process.exit(1);
}
console.log("✅ Found local database");
// Create SQL dump
console.log("🔄 Creating SQL dump...");
const dumpPath = "./data_export.sql";
try {
execSync(`sqlite3 db.sqlite ".dump" > ${dumpPath}`, { stdio: "inherit" });
console.log("✅ SQL dump created");
} catch (error) {
console.error(
"❌ Failed to create SQL dump. Make sure sqlite3 is installed.",
);
process.exit(1);
}
// Read and filter the dump file
console.log("🔍 Extracting data statements...");
const dumpContent = readFileSync(dumpPath, "utf8");
const lines = dumpContent.split("\n");
// Extract only INSERT statements for beenvoice tables
const dataStatements = [];
// Add header comment
dataStatements.push("-- beenvoice Data Export");
dataStatements.push("-- Generated: " + new Date().toISOString());
dataStatements.push(
"-- Run these INSERT statements in your Turso database",
);
dataStatements.push("");
// Extract table data in proper order (for foreign keys)
const tableOrder = [
"beenvoice_user",
"beenvoice_account",
"beenvoice_session",
"beenvoice_client",
"beenvoice_business",
"beenvoice_invoice",
"beenvoice_invoice_item",
];
for (const tableName of tableOrder) {
const tableStatements = lines.filter(
(line) =>
line.startsWith(`INSERT INTO ${tableName}`) ||
line.startsWith(`INSERT INTO \`${tableName}\``),
);
if (tableStatements.length > 0) {
dataStatements.push(
`-- Data for ${tableName} (${tableStatements.length} records)`,
);
dataStatements.push(...tableStatements);
dataStatements.push("");
}
}
// Write clean export file
const exportContent = dataStatements.join("\n");
writeFileSync("./beenvoice_data_export.sql", exportContent);
// Count total records
const totalInserts = dataStatements.filter((line) =>
line.startsWith("INSERT"),
).length;
console.log(`\n🎉 Data export completed!`);
console.log(` 📄 File: beenvoice_data_export.sql`);
console.log(` 📊 Total records: ${totalInserts}`);
console.log(`\n📋 Manual steps to complete migration:`);
console.log(` 1. Run: bun run db:push (to create tables in Turso)`);
console.log(
` 2. Copy the INSERT statements from beenvoice_data_export.sql`,
);
console.log(` 3. Run them in your Turso database`);
console.log(
`\n💡 Or use turso db shell beenvoice < beenvoice_data_export.sql`,
);
// Clean up temp file
try {
execSync(`rm ${dumpPath}`);
} catch (e) {
// Cleanup failed, that's okay
}
} catch (error) {
console.error(
"\n❌ Export failed:",
error instanceof Error ? error.message : String(error),
);
process.exit(1);
}
}
exportData().catch(console.error);

View File

@@ -0,0 +1,184 @@
import { createClient } from "@libsql/client";
import { readFileSync, existsSync } from "fs";
// Read .env file directly
function loadEnvVars() {
const envPath = "./.env";
if (!existsSync(envPath)) {
console.error("❌ .env file not found!");
process.exit(1);
}
const envContent = readFileSync(envPath, "utf8");
const envVars = /** @type {Record<string, string>} */ ({});
envContent.split("\n").forEach((line) => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...valueParts] = trimmed.split("=");
if (key) {
const value = valueParts.join("=").replace(/^["']|["']$/g, "");
envVars[key.trim()] = value.trim();
}
}
});
return envVars;
}
async function importData() {
console.log("🚀 Importing data to live Turso database...\n");
try {
// Load environment variables
console.log("🔧 Loading environment variables...");
const envVars = loadEnvVars();
if (!envVars.DATABASE_URL || !envVars.DATABASE_AUTH_TOKEN) {
console.error(
"❌ Missing DATABASE_URL or DATABASE_AUTH_TOKEN in .env file",
);
console.log(
"💡 Make sure your .env file contains your Turso credentials",
);
process.exit(1);
}
console.log("✅ Environment variables loaded");
// Check if export file exists
const exportFile = "./beenvoice_data_export.sql";
if (!existsSync(exportFile)) {
console.error("❌ Export file not found!");
console.log(
"💡 Run 'bun run db:export-data' first to create the export file",
);
process.exit(1);
}
console.log("✅ Found data export file");
// Connect to Turso
console.log("🔗 Connecting to live Turso database...");
const tursoClient = createClient({
url: envVars.DATABASE_URL,
authToken: envVars.DATABASE_AUTH_TOKEN,
});
console.log("✅ Connected to Turso");
// Read the export file
console.log("📖 Reading export file...");
const sqlContent = readFileSync(exportFile, "utf8");
const lines = sqlContent.split("\n");
// Filter for INSERT statements only
const insertStatements = lines.filter((line) =>
line.trim().startsWith("INSERT INTO beenvoice_"),
);
console.log(`📊 Found ${insertStatements.length} data records to import`);
if (insertStatements.length === 0) {
console.log("⚠️ No INSERT statements found in export file");
process.exit(0);
}
// Clear existing data first (in reverse foreign key order)
console.log("🗑️ Clearing existing data...");
const tablesToClear = [
"beenvoice_invoice_item",
"beenvoice_invoice",
"beenvoice_business",
"beenvoice_client",
"beenvoice_session",
"beenvoice_account",
"beenvoice_user",
];
for (const table of tablesToClear) {
try {
await tursoClient.execute(`DELETE FROM ${table}`);
console.log(` ✅ Cleared ${table}`);
} catch (error) {
console.log(
` ⏭️ Skipped ${table} (${error instanceof Error ? error.message : String(error)})`,
);
}
}
// Execute INSERT statements
console.log("📤 Importing data...");
let successCount = 0;
let errorCount = 0;
for (let i = 0; i < insertStatements.length; i++) {
const statementLine = insertStatements[i];
if (!statementLine) continue;
const statement = statementLine.trim();
try {
await tursoClient.execute(statement);
successCount++;
// Show progress every 50 records
if (successCount % 50 === 0) {
console.log(
` 📝 Imported ${successCount}/${insertStatements.length} records...`,
);
}
} catch (error) {
errorCount++;
if (errorCount <= 5) {
// Only show first 5 errors
console.error(
` ❌ Error importing record ${i + 1}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
// Verify the import
console.log("\n🔍 Verifying import...");
const tables = [
"beenvoice_user",
"beenvoice_client",
"beenvoice_business",
"beenvoice_invoice",
"beenvoice_invoice_item",
];
let totalRecords = 0;
for (const table of tables) {
try {
const result = await tursoClient.execute(
`SELECT COUNT(*) as count FROM ${table}`,
);
const count = parseInt(String(result.rows[0]?.count || 0));
if (count > 0) {
console.log(` 📊 ${table}: ${count} records`);
totalRecords += count;
}
} catch (error) {
console.log(` ⏭️ ${table}: not accessible`);
}
}
console.log(`\n🎉 Import completed!`);
console.log(`${successCount} records imported successfully`);
if (errorCount > 0) {
console.log(` ⚠️ ${errorCount} records had errors`);
}
console.log(` 📊 ${totalRecords} total records now in live database`);
console.log(`\n💡 Your local data is now live on Turso!`);
console.log(`💡 Your Vercel deployment will use this data.`);
} catch (error) {
console.error(
"\n❌ Import failed:",
error instanceof Error ? error.message : String(error),
);
process.exit(1);
} finally {
console.log("🔌 Done!");
}
}
importData().catch(console.error);

252
scripts/migrate-direct.js Normal file
View File

@@ -0,0 +1,252 @@
import { createClient } from "@libsql/client";
import { execSync } from "child_process";
import { readFileSync, writeFileSync, unlinkSync, existsSync } from "fs";
// Read .env file directly
function loadEnvVars() {
const envPath = "./.env";
if (!existsSync(envPath)) {
console.error("❌ .env file not found!");
process.exit(1);
}
const envContent = readFileSync(envPath, "utf8");
const envVars = /** @type {Record<string, string>} */ ({});
envContent.split("\n").forEach((line) => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...valueParts] = trimmed.split("=");
if (key) {
const value = valueParts.join("=").replace(/^["']|["']$/g, ""); // Remove quotes
envVars[key.trim()] = value.trim();
}
}
});
return envVars;
}
async function migrateToTurso() {
console.log("🚀 Pushing local SQLite data to live Turso database...\n");
try {
// Load environment variables
console.log("🔧 Loading environment variables...");
const envVars = loadEnvVars();
if (!envVars.DATABASE_URL || !envVars.DATABASE_AUTH_TOKEN) {
console.error(
"❌ Missing DATABASE_URL or DATABASE_AUTH_TOKEN in .env file",
);
console.log("💡 Make sure your .env file contains:");
console.log(" DATABASE_URL=libsql://your-database-url");
console.log(" DATABASE_AUTH_TOKEN=your-auth-token");
process.exit(1);
}
console.log("✅ Environment variables loaded");
// Check if local database exists
console.log("📁 Checking local database...");
if (!existsSync("./db.sqlite")) {
console.error("❌ Local database db.sqlite not found!");
process.exit(1);
}
console.log("✅ Found local database");
// Create SQL dump of local database
console.log("📦 Creating SQL dump from local database...");
const dumpPath = "./temp_dump.sql";
try {
execSync(`sqlite3 db.sqlite ".dump" > ${dumpPath}`, { stdio: "inherit" });
console.log("✅ SQL dump created");
} catch (error) {
console.error(
"❌ Failed to create SQL dump. Make sure sqlite3 is installed.",
);
process.exit(1);
}
// Read and filter the dump file
console.log("🔍 Processing SQL dump...");
const dumpContent = readFileSync(dumpPath, "utf8");
// Split into lines and filter for beenvoice tables
const lines = dumpContent.split("\n");
const filteredLines = [];
let inBeenvoiceTable = false;
for (const line of lines) {
// Skip PRAGMA and TRANSACTION statements
if (
line.startsWith("PRAGMA") ||
line.startsWith("BEGIN TRANSACTION") ||
line.startsWith("COMMIT")
) {
continue;
}
// Check if we're starting a beenvoice table
if (
line.startsWith("CREATE TABLE `beenvoice_") ||
line.startsWith("CREATE TABLE beenvoice_")
) {
inBeenvoiceTable = true;
filteredLines.push(line);
continue;
}
// Check if we're inserting into a beenvoice table
if (
line.startsWith("INSERT INTO beenvoice_") ||
line.startsWith("INSERT INTO `beenvoice_")
) {
filteredLines.push(line);
continue;
}
// If we were in a beenvoice table and hit another CREATE TABLE, we're done with that table
if (
inBeenvoiceTable &&
line.startsWith("CREATE TABLE") &&
!line.includes("beenvoice_")
) {
inBeenvoiceTable = false;
}
// If we're in a beenvoice table, include the line
if (inBeenvoiceTable) {
filteredLines.push(line);
}
}
console.log(`✅ Filtered ${filteredLines.length} SQL statements`);
// Connect to Turso
console.log("🔗 Connecting to live Turso database...");
const tursoClient = createClient({
url: envVars.DATABASE_URL,
authToken: envVars.DATABASE_AUTH_TOKEN,
});
console.log("✅ Connected to Turso");
// Clear existing data from beenvoice tables (in reverse order for foreign keys)
console.log("🗑️ Clearing existing data...");
const tablesToClear = [
"beenvoice_invoice_item",
"beenvoice_invoice",
"beenvoice_client",
"beenvoice_business",
"beenvoice_session",
"beenvoice_account",
"beenvoice_user",
];
for (const table of tablesToClear) {
try {
await tursoClient.execute(`DELETE FROM ${table}`);
console.log(` ✅ Cleared ${table}`);
} catch (error) {
console.log(
` ⏭️ Skipped ${table} (doesn't exist or error: ${error instanceof Error ? error.message : String(error)})`,
);
}
}
// Execute the filtered SQL statements
console.log("📤 Pushing data to Turso...");
let successCount = 0;
let errorCount = 0;
let insertCount = 0;
for (const line of filteredLines) {
const trimmed = line.trim();
if (!trimmed || trimmed === "") continue;
try {
await tursoClient.execute(trimmed);
successCount++;
// Count and show progress for inserts
if (trimmed.startsWith("INSERT")) {
insertCount++;
if (insertCount % 20 === 0) {
console.log(` 📝 Inserted ${insertCount} records...`);
}
}
} catch (error) {
errorCount++;
if (trimmed.startsWith("CREATE TABLE")) {
console.log(
` ⚠️ Table already exists: ${trimmed.substring(0, 50)}...`,
);
} else {
console.error(
` ❌ Error executing: ${trimmed.substring(0, 50)}...`,
);
console.error(
` Error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
// Verify the migration
console.log("\n🔍 Verifying migration...");
const tables = [
"beenvoice_user",
"beenvoice_client",
"beenvoice_business",
"beenvoice_invoice",
"beenvoice_invoice_item",
];
let totalRecords = 0;
for (const table of tables) {
try {
const result = await tursoClient.execute(
`SELECT COUNT(*) as count FROM ${table}`,
);
const count = String(result.rows[0]?.count || 0);
console.log(` 📊 ${table}: ${count} records`);
totalRecords += parseInt(count);
} catch (error) {
console.log(` ⏭️ ${table}: table doesn't exist`);
}
}
console.log(`\n🎉 Migration completed successfully!`);
console.log(`${successCount} SQL statements executed`);
console.log(` 📝 ${insertCount} data records inserted`);
console.log(` 📊 ${totalRecords} total records in live database`);
if (errorCount > 0) {
console.log(
` ⚠️ ${errorCount} statements had errors (likely table creation conflicts)`,
);
}
console.log(`\n💡 Your local data is now live on Turso!`);
console.log(`💡 Your Vercel deployment will use this data.`);
} catch (error) {
console.error(
"\n❌ Migration failed:",
error instanceof Error ? error.message : String(error),
);
console.error("Full error:", error);
process.exit(1);
} finally {
// Cleanup
try {
if (existsSync("./temp_dump.sql")) {
unlinkSync("./temp_dump.sql");
console.log("🧹 Cleaned up temporary files");
}
} catch (e) {
// File cleanup failed, that's okay
}
console.log("🔌 Done!");
}
}
migrateToTurso().catch(console.error);

211
scripts/migrate-simple.js Normal file
View File

@@ -0,0 +1,211 @@
import { createClient } from "@libsql/client";
import { execSync } from "child_process";
import { readFileSync, writeFileSync, unlinkSync, existsSync } from "fs";
import { env } from "../src/env.js";
async function migrateToTurso() {
console.log("🚀 Pushing local SQLite data to live Turso database...\n");
try {
// Check if local database exists
console.log("📁 Checking local database...");
const dbExists = existsSync("./db.sqlite");
if (!dbExists) {
console.error("❌ Local database db.sqlite not found!");
process.exit(1);
}
console.log("✅ Found local database");
// Create SQL dump of local database
console.log("📦 Creating SQL dump from local database...");
const dumpPath = "./temp_dump.sql";
try {
execSync(`sqlite3 db.sqlite ".dump" > ${dumpPath}`, { stdio: "inherit" });
console.log("✅ SQL dump created");
} catch (error) {
console.error(
"❌ Failed to create SQL dump. Make sure sqlite3 is installed.",
);
process.exit(1);
}
// Read and filter the dump file
console.log("🔍 Processing SQL dump...");
const dumpContent = readFileSync(dumpPath, "utf8");
// Split into lines and filter for beenvoice tables
const lines = dumpContent.split("\n");
const filteredLines = [];
let inBeenvoiceTable = false;
for (const line of lines) {
// Skip PRAGMA and TRANSACTION statements
if (
line.startsWith("PRAGMA") ||
line.startsWith("BEGIN TRANSACTION") ||
line.startsWith("COMMIT")
) {
continue;
}
// Check if we're starting a beenvoice table
if (
line.startsWith("CREATE TABLE `beenvoice_") ||
line.startsWith("CREATE TABLE beenvoice_")
) {
inBeenvoiceTable = true;
filteredLines.push(line);
continue;
}
// Check if we're inserting into a beenvoice table
if (
line.startsWith("INSERT INTO beenvoice_") ||
line.startsWith("INSERT INTO `beenvoice_")
) {
filteredLines.push(line);
continue;
}
// If we were in a beenvoice table and hit another CREATE TABLE, we're done with that table
if (
inBeenvoiceTable &&
line.startsWith("CREATE TABLE") &&
!line.includes("beenvoice_")
) {
inBeenvoiceTable = false;
}
// If we're in a beenvoice table, include the line
if (inBeenvoiceTable) {
filteredLines.push(line);
}
}
console.log(`✅ Filtered ${filteredLines.length} SQL statements`);
// Connect to Turso
console.log("🔗 Connecting to live Turso database...");
if (!env.DATABASE_URL || !env.DATABASE_AUTH_TOKEN) {
console.error("❌ Missing DATABASE_URL or DATABASE_AUTH_TOKEN");
console.log("💡 Make sure your .env file has the Turso credentials");
process.exit(1);
}
const tursoClient = createClient({
url: env.DATABASE_URL,
authToken: env.DATABASE_AUTH_TOKEN,
});
console.log("✅ Connected to Turso");
// Clear existing data from beenvoice tables
console.log("🗑️ Clearing existing data...");
const tablesToClear = [
"beenvoice_invoice_item",
"beenvoice_invoice",
"beenvoice_client",
"beenvoice_business",
"beenvoice_session",
"beenvoice_account",
"beenvoice_user",
];
for (const table of tablesToClear) {
try {
await tursoClient.execute(`DELETE FROM ${table}`);
console.log(` ✅ Cleared ${table}`);
} catch (error) {
// Table might not exist, that's okay
console.log(` ⏭️ Skipped ${table} (doesn't exist)`);
}
}
// Execute the filtered SQL statements
console.log("📤 Pushing data to Turso...");
let successCount = 0;
let errorCount = 0;
for (const line of filteredLines) {
const trimmed = line.trim();
if (!trimmed || trimmed === "") continue;
try {
await tursoClient.execute(trimmed);
successCount++;
// Show progress for inserts
if (trimmed.startsWith("INSERT")) {
const match = trimmed.match(/INSERT INTO (\w+)/);
if (match && successCount % 10 === 0) {
console.log(` 📝 Inserted ${successCount} records...`);
}
}
} catch (error) {
errorCount++;
if (trimmed.startsWith("CREATE TABLE")) {
console.log(
` ⚠️ Table already exists: ${trimmed.substring(0, 50)}...`,
);
} else {
console.error(
` ❌ Error executing: ${trimmed.substring(0, 50)}...`,
);
console.error(
` Error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
// Verify the migration
console.log("\n🔍 Verifying migration...");
const tables = [
"beenvoice_user",
"beenvoice_client",
"beenvoice_invoice",
"beenvoice_invoice_item",
];
for (const table of tables) {
try {
const result = await tursoClient.execute(
`SELECT COUNT(*) as count FROM ${table}`,
);
const count = result.rows[0]?.count || 0;
console.log(` 📊 ${table}: ${count} records`);
} catch (error) {
console.log(` ⏭️ ${table}: table doesn't exist`);
}
}
console.log(`\n🎉 Migration completed!`);
console.log(`${successCount} statements executed successfully`);
if (errorCount > 0) {
console.log(
` ⚠️ ${errorCount} statements had errors (likely table creation conflicts)`,
);
}
console.log(`\n💡 Your local data is now live on Turso!`);
console.log(`💡 Your Vercel deployment will use this data.`);
} catch (error) {
console.error(
"\n❌ Migration failed:",
error instanceof Error ? error.message : String(error),
);
process.exit(1);
} finally {
// Cleanup
try {
unlinkSync("./temp_dump.sql");
console.log("🧹 Cleaned up temporary files");
} catch (e) {
// File might not exist, that's okay
}
console.log("🔌 Done!");
}
}
migrateToTurso().catch(console.error);

View File

@@ -0,0 +1,92 @@
import { createClient } from "@libsql/client";
import Database from "better-sqlite3";
import { env } from "../src/env.js";
async function migrateToTurso() {
console.log("🚀 Pushing local data to live Turso database...\n");
// Connect to local SQLite database
const localDb = new Database("./db.sqlite");
console.log("✅ Connected to local database");
// Connect to live Turso database using existing env vars
const tursoClient = createClient({
url: env.DATABASE_URL,
authToken: env.DATABASE_AUTH_TOKEN,
});
console.log("✅ Connected to live Turso database");
try {
// Get all tables with data
const tables = localDb
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'beenvoice_%'",
)
.all();
console.log(`\n📋 Found ${tables.length} tables to migrate:`);
tables.forEach((table) => console.log(` - ${table.name}`));
// Migration order to handle foreign key constraints
const migrationOrder = [
"beenvoice_user",
"beenvoice_account",
"beenvoice_session",
"beenvoice_client",
"beenvoice_business",
"beenvoice_invoice",
"beenvoice_invoice_item",
];
for (const tableName of migrationOrder) {
if (!tables.find((t) => t.name === tableName)) {
console.log(`⏭️ Skipping ${tableName} (not found locally)`);
continue;
}
console.log(`\n📦 Processing ${tableName}...`);
// Get local data
const localData = localDb.prepare(`SELECT * FROM ${tableName}`).all();
console.log(` Found ${localData.length} local records`);
if (localData.length === 0) {
console.log(` ✅ No data to migrate`);
continue;
}
// Clear remote table first
await tursoClient.execute(`DELETE FROM ${tableName}`);
console.log(` 🗑️ Cleared remote table`);
// Insert all local data
for (const row of localData) {
const columns = Object.keys(row);
const values = Object.values(row);
const placeholders = columns.map(() => "?").join(", ");
const sql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
await tursoClient.execute({
sql,
args: values,
});
}
console.log(` ✅ Pushed ${localData.length} records to live database`);
}
console.log("\n🎉 Migration completed!");
console.log("💡 Local data is now live on Turso");
console.log("💡 Your Vercel deployment will use this data");
} catch (error) {
console.error("\n❌ Migration failed:", error.message);
process.exit(1);
} finally {
localDb.close();
tursoClient.close();
console.log("\n🔌 Connections closed");
}
}
migrateToTurso().catch(console.error);