UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

92 lines • 3.27 kB
/** * Thrown when a statement is not a single, read-only SELECT — i.e. it is an * INSERT/UPDATE/DELETE, DDL, a CALL, a multi-statement string, or it fails to * parse. The handler surfaces `.message` to the model so it can re-plan. */ export class SqlValidationError extends Error { constructor(message) { super(message); this.name = 'SqlValidationError'; } } /** Statement types that are categorically rejected (anything that writes or runs code). */ const FORBIDDEN_AST_TYPES = new Set([ 'insert', 'update', 'delete', 'replace', 'create', 'drop', 'alter', 'truncate', 'rename', 'load_data', 'lock', 'unlock', 'set', 'call', 'handler', 'use', 'grant', 'revoke', ]); /** * Parses SQL (MySQL dialect) and asserts it is a single SELECT, returning the * set of tables it touches. Domain-agnostic: it knows nothing about which * tables a caller may read — that is the `TableAccessPolicy`'s job. * * The `node-sql-parser` `Parser` is injected (see {@link loadSqlParser}) so this file never imports the * optional peer at module load. */ export class SqlValidator { parser; constructor(parser) { this.parser = parser; } /** * Throws `SqlValidationError` unless `sql` is exactly one SELECT statement. * On success returns the distinct base table names referenced. */ validate(sql) { let parsed; try { parsed = this.parser.astify(sql, { database: 'MySQL' }); } catch (err) { throw new SqlValidationError(`Parse error: ${err.message}`); } const statements = Array.isArray(parsed) ? parsed : [parsed]; if (statements.length !== 1) { throw new SqlValidationError('Only a single statement is allowed'); } const statement = statements[0]; const type = (statement?.type ?? '').toLowerCase(); if (type !== 'select') { if (['create', 'drop', 'alter', 'truncate', 'rename'].includes(type)) { throw new SqlValidationError('DDL is not allowed; only SELECT statements are accepted'); } if (FORBIDDEN_AST_TYPES.has(type)) { throw new SqlValidationError(`${type.toUpperCase()} is not allowed; only SELECT statements are accepted`); } throw new SqlValidationError(`Statement type "${type || 'unknown'}" is not allowed; only SELECT statements are accepted`); } return { tables: this.extractReferencedTables(sql) }; } /** * Distinct base table names a statement touches — walking CTEs, subqueries, * and joins. Backed by `node-sql-parser`'s `tableList`, which emits * `mode::db::table` strings; the table name is the last segment. */ extractReferencedTables(sql) { const raw = this.parser.tableList(sql, { database: 'MySQL' }); const tables = new Set(); for (const entry of raw) { const parts = entry.split('::'); const table = parts[parts.length - 1]; if (table && table !== 'null') tables.add(table); } return Array.from(tables); } } //# sourceMappingURL=sql-validator.js.map