Highly ambitious ATProtocol AppView service and sdks
at main 181 lines 5.0 kB view raw
1import type { SessionAdapter, SessionData } from "../types.ts"; 2import { DatabaseSync } from "node:sqlite"; 3 4interface SessionTable { 5 session_id: string; 6 user_id: string; 7 handle: string | null; 8 is_authenticated: number; 9 data: string | null; 10 created_at: number; 11 expires_at: number; 12 last_accessed_at: number; 13} 14 15export class SQLiteAdapter implements SessionAdapter { 16 private db: DatabaseSync; 17 18 constructor(databasePath: string) { 19 // Handle sqlite:// URLs or direct paths 20 const dbPath = databasePath.startsWith("sqlite://") 21 ? databasePath.slice(9) 22 : databasePath; 23 24 this.db = new DatabaseSync(dbPath); 25 this.initializeDatabase(); 26 } 27 28 private initializeDatabase() { 29 this.db.exec(` 30 CREATE TABLE IF NOT EXISTS sessions ( 31 session_id TEXT PRIMARY KEY, 32 user_id TEXT NOT NULL, 33 handle TEXT, 34 is_authenticated INTEGER NOT NULL DEFAULT 1, 35 data TEXT, -- JSON string 36 created_at INTEGER NOT NULL, 37 expires_at INTEGER NOT NULL, 38 last_accessed_at INTEGER NOT NULL 39 ) 40 `); 41 42 // Index for cleanup operations 43 this.db.exec(` 44 CREATE INDEX IF NOT EXISTS idx_sessions_expires_at 45 ON sessions(expires_at) 46 `); 47 48 // Index for user lookups 49 this.db.exec(` 50 CREATE INDEX IF NOT EXISTS idx_sessions_user_id 51 ON sessions(user_id) 52 `); 53 } 54 55 get(sessionId: string): Promise<SessionData | null> { 56 const stmt = this.db.prepare(` 57 SELECT * FROM sessions 58 WHERE session_id = ? 59 `); 60 61 const row = stmt.get(sessionId) as SessionTable | undefined; 62 if (!row) return Promise.resolve(null); 63 64 return Promise.resolve(this.rowToSessionData(row)); 65 } 66 67 set(sessionId: string, data: SessionData): Promise<void> { 68 const stmt = this.db.prepare(` 69 INSERT OR REPLACE INTO sessions 70 (session_id, user_id, handle, is_authenticated, data, created_at, expires_at, last_accessed_at) 71 VALUES (?, ?, ?, ?, ?, ?, ?, ?) 72 `); 73 74 stmt.run( 75 sessionId, 76 data.userId, 77 data.handle || null, 78 data.isAuthenticated ? 1 : 0, 79 data.data ? JSON.stringify(data.data) : null, 80 data.createdAt, 81 data.expiresAt, 82 data.lastAccessedAt 83 ); 84 return Promise.resolve(); 85 } 86 87 update(sessionId: string, updates: Partial<SessionData>): Promise<boolean> { 88 const setParts: string[] = []; 89 const values: (string | number | null)[] = []; 90 91 if (updates.userId !== undefined) { 92 setParts.push("user_id = ?"); 93 values.push(updates.userId); 94 } 95 96 if (updates.handle !== undefined) { 97 setParts.push("handle = ?"); 98 values.push(updates.handle); 99 } 100 101 if (updates.isAuthenticated !== undefined) { 102 setParts.push("is_authenticated = ?"); 103 values.push(updates.isAuthenticated ? 1 : 0); 104 } 105 106 if (updates.data !== undefined) { 107 setParts.push("data = ?"); 108 values.push(updates.data ? JSON.stringify(updates.data) : null); 109 } 110 111 if (updates.expiresAt !== undefined) { 112 setParts.push("expires_at = ?"); 113 values.push(updates.expiresAt); 114 } 115 116 if (updates.lastAccessedAt !== undefined) { 117 setParts.push("last_accessed_at = ?"); 118 values.push(updates.lastAccessedAt); 119 } 120 121 if (setParts.length === 0) return Promise.resolve(false); 122 123 values.push(sessionId); 124 125 const stmt = this.db.prepare(` 126 UPDATE sessions 127 SET ${setParts.join(", ")} 128 WHERE session_id = ? 129 `); 130 131 const result = stmt.run(...values); 132 return Promise.resolve(Number(result.changes) > 0); 133 } 134 135 delete(sessionId: string): Promise<void> { 136 const stmt = this.db.prepare("DELETE FROM sessions WHERE session_id = ?"); 137 stmt.run(sessionId); 138 return Promise.resolve(); 139 } 140 141 cleanup(expiresBeforeMs: number): Promise<number> { 142 const stmt = this.db.prepare("DELETE FROM sessions WHERE expires_at < ?"); 143 const result = stmt.run(expiresBeforeMs); 144 return Promise.resolve(Number(result.changes)); 145 } 146 147 exists(sessionId: string): Promise<boolean> { 148 const stmt = this.db.prepare( 149 "SELECT 1 FROM sessions WHERE session_id = ? LIMIT 1" 150 ); 151 return Promise.resolve(stmt.get(sessionId) !== undefined); 152 } 153 154 private rowToSessionData(row: SessionTable): SessionData { 155 return { 156 sessionId: row.session_id, 157 userId: row.user_id, 158 handle: row.handle || undefined, 159 isAuthenticated: Boolean(row.is_authenticated), 160 data: row.data ? JSON.parse(row.data) : undefined, 161 createdAt: row.created_at, 162 expiresAt: row.expires_at, 163 lastAccessedAt: row.last_accessed_at, 164 }; 165 } 166 167 // SQLite-specific methods 168 close(): void { 169 this.db.close(); 170 } 171 172 vacuum(): void { 173 this.db.exec("VACUUM"); 174 } 175 176 getSessionsByUser(userId: string): SessionData[] { 177 const stmt = this.db.prepare("SELECT * FROM sessions WHERE user_id = ?"); 178 const rows = stmt.all(userId) as unknown as SessionTable[]; 179 return rows.map(row => this.rowToSessionData(row)); 180 } 181}