UNPKG

@variablesoftware/mock-d1

Version:

🎛️🗂️🧠 Mock D1 Database implementation for testing Cloudflare Workers

105 lines (104 loc) 4.69 kB
import { log } from '@variablesoftware/logface'; import { d1Error } from './errors.js'; const UNSUPPORTED_SQL_PATTERNS = [ /\bIN\b/i, /\bNOT\s+IN\b/i, /\bBETWEEN\b/i, /\bLIKE\b/i, /\bGLOB\b/i, /\bREGEXP\b/i, /\bANY\b/i, /\bALL\b/i, /\bSOME\b/i, /\bEXISTS\b/i, /\bIS\s+DISTINCT\s+FROM\b/i, /\bESCAPE\b/i ]; /** * Checks if the given SQL command is supported by D1. * * Validation funnel: * 1. Trim the SQL statement. * 2. Explicitly allow CREATE TABLE (with or without IF NOT EXISTS, with or without columns). * - This is handled first to avoid false positives from unsupported pattern checks (e.g., "IN" in "IF NOT EXISTS"). * 3. For all other statements, check for unsupported SQL patterns (e.g., IN, LIKE, BETWEEN, etc.). * - If any unsupported pattern is found, the statement is rejected. * 4. Allow other primary SQL operations (INSERT, SELECT, UPDATE, DELETE, DROP, TRUNCATE, ALTER) if they do not contain unsupported patterns. * 5. Reject everything else. * * @param sql - The SQL statement string. * @returns True if the SQL command is supported and does not contain unsupported features, false otherwise. */ export function validateSQLSyntax(sql) { const trimmed = sql.trim(); // 1. Explicitly allow CREATE TABLE (with or without IF NOT EXISTS) // This must be first to avoid false positives from unsupported pattern checks. if (/^CREATE\s+TABLE(\s+IF\s+NOT\s+EXISTS)?\s+([A-Za-z_][\w$]*|"[^"]+")(\s*\(.*\))?\s*;?$/i.test(trimmed)) { return true; } // 1b. Explicitly allow valid INSERT INTO ... VALUES ... (robust, allow whitespace/comments) if (/^INSERT\s+INTO\s+([A-Za-z_][\w$]*|"[^"]+")\s*\([^)]*\)\s*VALUES\s*\([^)]*\)\s*;?$/ims.test(trimmed)) { return true; } // 2. For all other statements, check for unsupported patterns for (const pattern of UNSUPPORTED_SQL_PATTERNS) { if (pattern.test(trimmed)) { return false; } } // 3. Allow other primary operations if (/^(INSERT|SELECT|UPDATE|DELETE|DROP|TRUNCATE|ALTER)\b/i.test(trimmed)) { return true; } // 4. Reject everything else return false; } /** * Centralized SQL validation for D1 mock engine. * Throws a D1-like error if the SQL is not a supported command or contains unsupported features/operators. * Optionally skips malformed SQL checks (for prepare-time validation only). * @param sql - The SQL statement string. * @param opts - Optional options object. */ export function validateSqlOrThrow(sql, opts) { const trimmed = sql.trim(); // D1: Any malformed or unsupported CREATE (including non-TABLE CREATE) must throw UNSUPPORTED_SQL if (/^CREATE\b/i.test(trimmed) && !/^CREATE\s+TABLE\b/i.test(trimmed)) { log.error('SQL validation failed: Malformed or unsupported CREATE statement.', { sql }); throw d1Error('UNSUPPORTED_SQL', 'UNSUPPORTED_SQL'); } // For CREATE TABLE, malformed or unsupported must throw UNSUPPORTED_SQL if (/^CREATE\s+TABLE\b/i.test(trimmed) && !validateSQLSyntax(sql)) { log.error('SQL validation failed: Malformed or unsupported CREATE TABLE.', { sql }); throw d1Error('UNSUPPORTED_SQL', 'UNSUPPORTED_SQL'); } if (!validateSQLSyntax(sql)) { log.error('SQL validation failed: Unsupported SQL command or feature.', { sql }); throw d1Error('UNSUPPORTED_SQL', 'UNSUPPORTED_SQL'); } if (!opts?.skipMalformed) { // Malformed SQL checks (very basic, only for top-level statement type) if (/^SELECT\b/i.test(trimmed) && !/FROM\b/i.test(trimmed)) { throw d1Error('MALFORMED_SELECT', 'MALFORMED_SELECT'); } if (/^INSERT\b/i.test(trimmed) && !/VALUES\b/i.test(trimmed)) { throw d1Error('MALFORMED_INSERT', 'MALFORMED_INSERT'); } if (/^DELETE\b/i.test(trimmed) && !/FROM\b/i.test(trimmed)) { throw d1Error('MALFORMED_DELETE', 'MALFORMED_DELETE'); } if (/^UPDATE\b/i.test(trimmed) && !/SET\b/i.test(trimmed)) { throw d1Error('MALFORMED_UPDATE', 'MALFORMED_UPDATE'); } // Only non-TABLE DROP/TRUNCATE/ALTER should throw their respective MALFORMED_* errors if (/^DROP\b/i.test(trimmed) && !/TABLE\b/i.test(trimmed)) { throw d1Error('MALFORMED_DROP', 'MALFORMED_DROP'); } if (/^TRUNCATE\b/i.test(trimmed) && !/TABLE\b/i.test(trimmed)) { throw d1Error('MALFORMED_TRUNCATE', 'MALFORMED_TRUNCATE'); } if (/^ALTER\b/i.test(trimmed) && !/TABLE\b/i.test(trimmed)) { throw d1Error('MALFORMED_ALTER', 'MALFORMED_ALTER'); } } }