The Appview for the kipclip.com atproto bookmarking service
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}