The Appview for the kipclip.com atproto bookmarking service
at main 91 lines 2.8 kB view raw
1// Database module using Turso/libSQL 2// Works on Deno Deploy and local development 3 4const dbUrl = Deno.env.get("TURSO_DATABASE_URL") || "file:.local/kipclip.db"; 5const isLocal = dbUrl.startsWith("file:"); 6const isTestDb = dbUrl.startsWith("libsql://test"); 7 8// For test environment with fake URL, use a mock client 9let rawDb: { 10 execute: ( 11 query: { sql: string; args: unknown[] }, 12 ) => Promise<{ rows: unknown[][] }>; 13}; 14 15if (isTestDb) { 16 // Mock client for tests - doesn't actually connect 17 console.log("✅ Using mock database (test mode)"); 18 rawDb = { 19 execute: ( 20 _query: { sql: string; args: unknown[] }, 21 ): Promise<{ rows: unknown[][] }> => { 22 // Return empty results for all queries in test mode 23 return Promise.resolve({ rows: [] }); 24 }, 25 }; 26} else { 27 // Use native client for local file, web client for remote Turso 28 const { createClient } = isLocal 29 ? await import("@libsql/client") 30 : await import("@libsql/client/web"); 31 32 const client = createClient({ 33 url: dbUrl, 34 authToken: Deno.env.get("TURSO_AUTH_TOKEN"), 35 }); 36 37 // Wrap the client to provide a consistent interface 38 // The libSQL client returns Row objects, we convert to arrays for compatibility 39 rawDb = { 40 execute: async ( 41 query: { sql: string; args: unknown[] }, 42 ): Promise<{ rows: unknown[][] }> => { 43 const result = await client.execute({ 44 sql: query.sql, 45 args: query.args as any, 46 }); 47 // Convert Row objects to arrays (Object.values) 48 const rows = result.rows.map((row) => Object.values(row)); 49 return { rows }; 50 }, 51 }; 52 53 console.log(`✅ Using ${isLocal ? "local" : "Turso"} database`); 54} 55 56export { rawDb }; 57 58// Initialize tables using migrations (with retry for transient Turso errors) 59export async function initializeTables() { 60 // Skip migrations for test database 61 if (isTestDb) { 62 console.log("⏭️ Skipping migrations (test mode)"); 63 return; 64 } 65 const { runMigrations } = await import("./migrations.ts"); 66 67 const maxRetries = 3; 68 for (let attempt = 1; attempt <= maxRetries; attempt++) { 69 try { 70 await runMigrations(); 71 return; 72 } catch (error) { 73 const isTransient = error instanceof Error && 74 (error.message.includes("502") || 75 error.message.includes("503") || 76 error.message.includes("bad gateway") || 77 error.message.includes("ECONNREFUSED") || 78 error.message.includes("connection not opened")); 79 80 if (isTransient && attempt < maxRetries) { 81 const delay = attempt * 2000; 82 console.warn( 83 `⚠️ Migration attempt ${attempt}/${maxRetries} failed (transient), retrying in ${delay}ms...`, 84 ); 85 await new Promise((r) => setTimeout(r, delay)); 86 continue; 87 } 88 throw error; 89 } 90 } 91}