(args: string[])
| 164 | } |
| 165 | |
| 166 | export default async function dbQuery(args: string[]): Promise<void> { |
| 167 | const parsed = parseArgs(args); |
| 168 | |
| 169 | if (parsed.help === "true") { |
| 170 | console.log(`Usage: pnpm action db-query --sql "<query>" [options] |
| 171 | |
| 172 | Options: |
| 173 | --sql <query> SQL SELECT query to run (required) |
| 174 | --args <json> JSON array of positional SQL bind parameters |
| 175 | --db <path> Path to SQLite database (default: data/app.db) |
| 176 | --format json Output as JSON instead of a table |
| 177 | --limit N Append LIMIT N if not already present |
| 178 | --help Show this help message`); |
| 179 | return; |
| 180 | } |
| 181 | |
| 182 | const sql = parsed.sql; |
| 183 | if (!sql) { |
| 184 | fail('--sql is required. Example: --sql "SELECT * FROM forms"'); |
| 185 | } |
| 186 | const sqlArgs = parseSqlArgs(parsed.args); |
| 187 | |
| 188 | // Safety: only allow read-only statements. |
| 189 | // Strip leading SQL comments before checking the prefix. |
| 190 | const stripped = sql |
| 191 | .replace(/^\s*--[^\n]*\n/gm, "") |
| 192 | .replace(/\/\*[\s\S]*?\*\//g, "") |
| 193 | .trim(); |
| 194 | const upper = stripped.toUpperCase(); |
| 195 | if ( |
| 196 | !upper.startsWith("SELECT") && |
| 197 | !upper.startsWith("WITH") && |
| 198 | !upper.startsWith("EXPLAIN") && |
| 199 | !upper.startsWith("PRAGMA") |
| 200 | ) { |
| 201 | fail( |
| 202 | "Only SELECT, WITH, EXPLAIN, and PRAGMA queries are allowed. Use db-exec for writes.", |
| 203 | ); |
| 204 | } |
| 205 | assertNoSensitiveFrameworkTables(stripped, "read"); |
| 206 | |
| 207 | // Resolve database URL: --db flag → DATABASE_URL env → default file path |
| 208 | let url: string; |
| 209 | if (parsed.db) { |
| 210 | url = "file:" + path.resolve(parsed.db); |
| 211 | } else if (getDatabaseUrl()) { |
| 212 | url = getDatabaseUrl(); |
| 213 | } else { |
| 214 | url = "file:" + path.resolve(process.cwd(), "data", "app.db"); |
| 215 | } |
| 216 | |
| 217 | let finalSql = sql; |
| 218 | if ( |
| 219 | parsed.limit && |
| 220 | (upper.startsWith("SELECT") || upper.startsWith("WITH")) && |
| 221 | !/\bLIMIT\b/i.test(stripped) |
| 222 | ) { |
| 223 | const limitVal = parseInt(parsed.limit, 10); |
no test coverage detected