UNPKG

forge-sql-orm

Version:

Drizzle ORM integration for Atlassian @forge/sql. Provides a custom driver, schema migration, two levels of caching (local and global via @forge/kvs), optimistic locking, and query analysis.

210 lines 9.13 kB
import { ForgeSqlOperation, ForgeSqlOrmOptions, RovoIntegration, RovoIntegrationSetting, RovoIntegrationSettingCreator } from "./ForgeSQLQueryBuilder"; import { Result } from "@forge/sql"; import { AnyMySqlTable } from "drizzle-orm/mysql-core"; /** * Main class for Rovo integration - a secure pattern for natural-language analytics in Forge apps. * * Rovo provides a secure way to execute dynamic SQL queries with comprehensive security validations: * - Only SELECT queries are allowed * - Queries are restricted to a single table * - JOINs, subqueries, and window functions are blocked * - Row-Level Security (RLS) support for data isolation * - Post-execution validation of query results * * @class Rovo * @implements {RovoIntegration} * * @example * ```typescript * const rovo = forgeSQL.rovo(); * const settings = await rovo.rovoSettingBuilder(usersTable, accountId) * .useRLS() * .addRlsColumn(usersTable.id) * .addRlsWherePart((alias) => `${alias}.id = '${accountId}'`) * .finish() * .build(); * * const result = await rovo.dynamicIsolatedQuery( * "SELECT id, name FROM users WHERE status = 'active'", * settings * ); * ``` */ export declare class Rovo implements RovoIntegration { private readonly forgeOperations; private readonly options; /** * Creates a new Rovo instance. * * @param {ForgeSqlOperation} forgeSqlOperations - The ForgeSQL operations instance for query analysis and execution * @param {ForgeSqlOrmOptions} options - Configuration options for the ORM (e.g., logging settings) */ constructor(forgeSqlOperations: ForgeSqlOperation, options: ForgeSqlOrmOptions); /** * Parses SQL query into AST and validates it's a single SELECT statement. * * @param {string} sqlQuery - Normalized SQL query string * @returns {Select} Parsed AST of the SELECT statement * @throws {Error} If parsing fails or query is not a single SELECT statement */ private parseSqlQuery; /** * Recursively processes array or single node and extracts table names. * * @param {any} items - Array of AST nodes or single AST node * @param {string[]} tables - Accumulator array for collecting table names (modified in place) */ private extractTablesFromItems; /** * Extracts table name from table AST node. * * @param {any} node - AST node with table information * @returns {string | null} Table name in uppercase, or null if not applicable (e.g., 'dual' table) */ private extractTableName; /** * Recursively extracts all table names from SQL AST node. * Traverses FROM and JOIN clauses to find all referenced tables. * * @param {any} node - AST node to extract tables from * @returns {string[]} Array of table names in uppercase */ private extractTables; /** * Recursively checks if AST node contains scalar subqueries. * Used for security validation to prevent subquery-based attacks. * * @param {any} node - AST node to check for subqueries * @returns {boolean} True if node contains scalar subquery, false otherwise */ private hasScalarSubquery; /** * Creates a settings builder for Rovo queries using a raw table name. * * @param {string} tableName - The name of the table to query (case-insensitive) * @param {string} accountId - The account ID of the active user for RLS filtering * @returns {RovoIntegrationSettingCreator} Builder for configuring Rovo query settings * * @example * ```typescript * const builder = rovo.rovoRawSettingBuilder('users', accountId); * const settings = await builder * .addStringContextParameter('{{status}}', 'active') * .build(); * ``` */ rovoRawSettingBuilder(tableName: string, accountId: string): RovoIntegrationSettingCreator; /** * Creates a settings builder for Rovo queries using a Drizzle table object. * This is a convenience method that extracts the table name from the Drizzle table object. * * @param {AnyMySqlTable} table - The Drizzle table object * @param {string} accountId - The account ID of the active user for RLS filtering * @returns {RovoIntegrationSettingCreator} Builder for configuring Rovo query settings * * @example * ```typescript * const builder = rovo.rovoSettingBuilder(usersTable, accountId); * const settings = await builder * .useRLS() * .addRlsColumn(usersTable.id) * .addRlsWherePart((alias) => `${alias}.userId = '${accountId}'`) * .finish() * .build(); * ``` */ rovoSettingBuilder(table: AnyMySqlTable, accountId: string): RovoIntegrationSettingCreator; /** * Validates basic input parameters for the SQL query. * * @param {string} query - The SQL query string to validate * @param {string} tableName - The expected table name * @returns {string} The trimmed query string * @throws {Error} If query is empty, table name is missing, or query is not a SELECT statement */ private validateInputs; /** * Normalizes SQL query using AST parsing and stringification. * This ensures consistent formatting and validates the query structure. * * @param {string} sql - The SQL query string to normalize * @returns {string} The normalized SQL query string * @throws {Error} If parsing fails, query is not a SELECT statement, or multiple statements are detected */ private normalizeSqlString; /** * Validates that query targets the correct table. * Checks that the FROM clause references only the expected table. * * @param {string} normalized - The normalized SQL query string * @param {string} tableName - The expected table name * @throws {Error} If query does not target the expected table */ private validateTableName; /** * Validates query structure for security compliance. * Checks that only the specified table is referenced and no scalar subqueries are present. * * @param {Select} selectAst - The parsed SELECT AST node * @param {string} tableName - The expected table name * @throws {Error} If query references other tables or contains scalar subqueries */ private validateQueryStructure; /** * Validates query execution plan for security violations. * Uses EXPLAIN to detect JOINs, window functions, and references to other tables. * * @param {string} normalized - The normalized SQL query string * @param {string} tableName - The expected table name * @returns {Promise<void>} * @throws {Error} If execution plan reveals JOINs, window functions, or references to other tables */ private validateExecutionPlan; /** * Applies row-level security filtering to query. * Wraps the original query in a subquery and adds a WHERE clause with RLS conditions. * * @param {string} normalized - The normalized SQL query string * @param {RovoIntegrationSetting} settings - Rovo settings containing RLS configuration * @returns {string} The SQL query with RLS filtering applied */ private applyRLSFiltering; /** * Validates query results for RLS compliance. * Ensures that required RLS fields are present and all fields originate from the correct table. * * @param {Result<unknown>} result - The query execution result * @param {RovoIntegrationSetting} settings - Rovo settings containing RLS field requirements * @param {string} upperTableName - The expected table name in uppercase * @throws {Error} If required RLS fields are missing or fields originate from other tables */ private validateQueryResults; /** * Executes a dynamic SQL query with comprehensive security validations. * * This method performs multiple security checks: * 1. Validates that the query is a SELECT statement * 2. Ensures the query targets only the specified table * 3. Blocks JOINs, subqueries, and window functions * 4. Applies Row-Level Security filtering if enabled * 5. Validates query results to ensure security fields are present * * @param {string} dynamicSql - The SQL query to execute (must be a SELECT statement) * @param {RovoIntegrationSetting} settings - Configuration settings for the query * @returns {Promise<Result<unknown>>} Query execution result with metadata * @throws {Error} If the query violates security restrictions, parsing fails, or validation errors occur * * @example * ```typescript * const result = await rovo.dynamicIsolatedQuery( * "SELECT id, name, email FROM users WHERE status = 'active' ORDER BY name", * settings * ); * * console.log(result.rows); // Query results * console.log(result.metadata); // Query metadata * ``` */ dynamicIsolatedQuery(dynamicSql: string, settings: RovoIntegrationSetting): Promise<Result<unknown>>; } //# sourceMappingURL=Rovo.d.ts.map