| |
| |
| |
| |
| |
| |
| |
|
|
| import { RuntimeDatabase } from '@/lib/vfs/adapters/runtime-database'; |
| import { DatabaseAPI, DatabaseAPIOptions } from './types'; |
|
|
| |
| |
| |
| |
| const FORBIDDEN_TABLES = [ |
| 'sqlite_', |
| '_migrations', |
| 'site_info', |
| 'edge_functions', |
| 'function_logs', |
| 'server_functions', |
| 'secrets', |
| 'pageviews', |
| 'interactions', |
| 'sessions', |
| 'files', |
| 'file_tree_nodes', |
| ]; |
|
|
| |
| |
| |
| const DDL_KEYWORDS = ['create', 'drop', 'alter', 'truncate']; |
|
|
| |
| |
| |
| const DANGEROUS_KEYWORDS = ['attach', 'detach', 'vacuum', 'reindex']; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function createDatabaseAPI( |
| deploymentDb: RuntimeDatabase, |
| options: DatabaseAPIOptions = {} |
| ): DatabaseAPI { |
| let queryCount = 0; |
| const maxQueries = options.maxQueries ?? 100; |
| const readOnly = options.readOnly ?? false; |
|
|
| |
| |
| |
| |
| const validateSQL = (sql: string): void => { |
| const lowerSQL = sql.toLowerCase().trim(); |
|
|
| |
| for (const keyword of DANGEROUS_KEYWORDS) { |
| if (lowerSQL.includes(keyword)) { |
| throw new Error(`SQL keyword "${keyword}" is not allowed`); |
| } |
| } |
|
|
| |
| for (const table of FORBIDDEN_TABLES) { |
| |
| |
| const tableRegex = new RegExp(`\\b${table.replace('_', '_?')}\\w*\\b`, 'i'); |
| if (tableRegex.test(lowerSQL)) { |
| throw new Error(`Access to system table "${table}" is not allowed`); |
| } |
| } |
|
|
| |
| if (readOnly) { |
| for (const keyword of DDL_KEYWORDS) { |
| if (lowerSQL.startsWith(keyword)) { |
| throw new Error(`DDL statements (${keyword.toUpperCase()}) not allowed in read-only mode`); |
| } |
| } |
| } |
|
|
| |
| queryCount++; |
| if (queryCount > maxQueries) { |
| throw new Error(`Query limit exceeded (max ${maxQueries} queries per execution)`); |
| } |
| }; |
|
|
| |
| |
| |
| const executeQuery = <T>(sql: string, params?: unknown[]): T[] => { |
| validateSQL(sql); |
|
|
| try { |
| const result = deploymentDb.executeRawSQL(sql, params); |
|
|
| |
| return result.rows.map(row => { |
| const obj: Record<string, unknown> = {}; |
| result.columns.forEach((col, i) => { |
| obj[col] = row[i]; |
| }); |
| return obj as T; |
| }); |
| } catch (error) { |
| |
| const message = error instanceof Error ? error.message : 'Query failed'; |
| throw new Error(`Database error: ${message}`); |
| } |
| }; |
|
|
| |
| |
| |
| const executeRun = (sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint } => { |
| validateSQL(sql); |
|
|
| if (readOnly) { |
| throw new Error('Database is in read-only mode'); |
| } |
|
|
| try { |
| const result = deploymentDb.executeRawSQL(sql, params); |
| return { |
| changes: result.rowsAffected, |
| lastInsertRowid: 0, |
| }; |
| } catch (error) { |
| const message = error instanceof Error ? error.message : 'Query failed'; |
| throw new Error(`Database error: ${message}`); |
| } |
| }; |
|
|
| return { |
| query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] { |
| return executeQuery<T>(sql, params); |
| }, |
|
|
| run(sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint } { |
| return executeRun(sql, params); |
| }, |
|
|
| all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] { |
| return executeQuery<T>(sql, params); |
| }, |
| }; |
| } |
|
|