UNPKG

multibridge

Version:

A multi-database connection framework with centralized configuration

297 lines (296 loc) 12.3 kB
"use strict"; /** * Cassandra ORM adapter for MultiBridge * Provides an ORM-like interface using cassandra-driver * Supports: Cassandra */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getCassandraClient = getCassandraClient; exports.executeCQL = executeCQL; exports.createTable = createTable; exports.insert = insert; exports.select = select; exports.update = update; exports.remove = remove; exports.closeCassandraClient = closeCassandraClient; exports.closeAllCassandraClients = closeAllCassandraClients; const cassandra_driver_1 = require("cassandra-driver"); const base_1 = require("./base"); const loggers_1 = __importDefault(require("../utils/loggers")); const errors_1 = require("../utils/errors"); /** * Sanitize and validate Cassandra identifier (keyspace, table, column names) * CQL identifiers can contain alphanumeric, underscore, and must not be a reserved word */ function sanitizeCQLIdentifier(identifier, type) { // Validate identifier contains only safe characters if (!/^[a-zA-Z0-9_]+$/.test(identifier)) { throw new errors_1.ValidationError(`Invalid ${type} name: ${identifier}. Only alphanumeric and underscore are allowed.`); } // Check for reserved words (basic check) const reservedWords = new Set([ "key", "keyspace", "table", "column", "select", "insert", "update", "delete", "create", "alter", "drop", "use", "from", "where", "and", "or", "not", ]); if (reservedWords.has(identifier.toLowerCase())) { throw new errors_1.ValidationError(`Invalid ${type} name: ${identifier}. Cannot use reserved CQL keywords.`); } // Use double quotes for case sensitivity and to escape if needed // Replace any double quotes with escaped quotes const escaped = identifier.replace(/"/g, '""'); return `"${escaped}"`; } // Cache for Cassandra clients per tenant const cassandraClients = new Map(); /** * Get or create a Cassandra client for the current tenant * * @returns Cassandra Client instance configured for the current tenant * * @example * ```typescript * await runWithTenant(tenant, async () => { * const client = await getCassandraClient(); * const result = await client.execute("SELECT * FROM users WHERE id = ?", [userId]); * }); * ``` */ async function getCassandraClient() { const { tenant, connectionData, dbType } = await (0, base_1.getTenantConnection)(); // Validate database type (0, base_1.validateORMSupport)(dbType, ["cassandra"]); const cacheKey = `${tenant.appid}-${tenant.orgid}-${tenant.appdbname}`; // Check cache const cached = cassandraClients.get(cacheKey); if (cached) { loggers_1.default.debug(`Reusing cached Cassandra client for ${cacheKey}`); return cached; } // Get Cassandra client from MultiBridge connection const client = connectionData.connection; // Cache the client cassandraClients.set(cacheKey, client); loggers_1.default.info(`Created Cassandra client for ${cacheKey}`, { dbType, keyspace: tenant.appdbname, }); return client; } /** * Execute a CQL query with prepared statement * * @param query - CQL query string * @param params - Query parameters * @param options - Execution options * @returns Query result */ async function executeCQL(query, params, options) { const client = await getCassandraClient(); const executeOptions = { prepare: options?.prepare !== false, // Default to prepared statements consistency: options?.consistency || cassandra_driver_1.types.consistencies.localQuorum, }; try { return await client.execute(query, params || [], executeOptions); } catch (error) { loggers_1.default.error(`Error executing CQL query: ${error.message}`, { query: query.substring(0, 100), error: error.stack, }); throw new errors_1.QueryError(`Cassandra query execution failed: ${error.message}`, { query: query.substring(0, 100), originalError: error, }); } } /** * Create a table based on model definition * * @param model - Model definition * @param ifNotExists - Add IF NOT EXISTS clause */ async function createTable(model, ifNotExists = true) { const client = await getCassandraClient(); const { tenant } = await (0, base_1.getTenantConnection)(); // Sanitize identifiers const keyspace = sanitizeCQLIdentifier(model.keyspace || tenant.appdbname, "keyspace"); const tableName = sanitizeCQLIdentifier(model.tableName, "table"); // Validate and sanitize partition keys const sanitizedPartitionKeys = model.partitionKeys.map((key) => sanitizeCQLIdentifier(key, "column")); // Build partition key clause const partitionKeyClause = `PRIMARY KEY (${sanitizedPartitionKeys.join(", ")})`; // Build clustering key clause if present let primaryKeyClause = partitionKeyClause; if (model.clusteringKeys && model.clusteringKeys.length > 0) { const sanitizedClusteringKeys = model.clusteringKeys.map((key) => sanitizeCQLIdentifier(key, "column")); primaryKeyClause = `PRIMARY KEY ((${sanitizedPartitionKeys.join(", ")}), ${sanitizedClusteringKeys.join(", ")})`; } // Build column definitions with sanitized names const columnDefs = Object.entries(model.columns) .map(([name, type]) => { const sanitizedName = sanitizeCQLIdentifier(name, "column"); // Validate type is a safe CQL type if (!/^[a-zA-Z0-9_<>\[\]()]+$/.test(type)) { throw new errors_1.ValidationError(`Invalid CQL type: ${type}`); } return `${sanitizedName} ${type}`; }) .join(", "); const ifNotExistsClause = ifNotExists ? "IF NOT EXISTS" : ""; const query = ` CREATE TABLE ${ifNotExistsClause} ${keyspace}.${tableName} ( ${columnDefs}, ${primaryKeyClause} ) `.trim(); try { await client.execute(query, [], { prepare: false }); loggers_1.default.info(`Created table ${keyspace}.${tableName}`); } catch (error) { loggers_1.default.error(`Error creating table: ${error.message}`, { table: model.tableName, keyspace: model.keyspace || tenant.appdbname, error: error.stack, }); throw error; } } /** * Insert a row into a table * * @param tableName - Table name * @param data - Data to insert (column -> value mapping) * @param keyspace - Optional keyspace (defaults to tenant's keyspace) * @param ttl - Optional TTL in seconds */ async function insert(tableName, data, keyspace, ttl) { const { tenant } = await (0, base_1.getTenantConnection)(); const targetKeyspace = keyspace || tenant.appdbname; // Sanitize identifiers const sanitizedKeyspace = sanitizeCQLIdentifier(targetKeyspace, "keyspace"); const sanitizedTable = sanitizeCQLIdentifier(tableName, "table"); const sanitizedColumns = Object.keys(data).map((col) => sanitizeCQLIdentifier(col, "column")); const values = Object.values(data); const placeholders = sanitizedColumns.map(() => "?").join(", "); let query = `INSERT INTO ${sanitizedKeyspace}.${sanitizedTable} (${sanitizedColumns.join(", ")}) VALUES (${placeholders})`; if (ttl && ttl > 0) { query += ` USING TTL ${ttl}`; } return await executeCQL(query, values); } /** * Select rows from a table * * @param tableName - Table name * @param whereClause - WHERE clause (e.g., "id = ? AND name = ?") * @param params - Parameters for WHERE clause * @param keyspace - Optional keyspace (defaults to tenant's keyspace) * @param limit - Optional LIMIT clause * @param allowFiltering - Enable ALLOW FILTERING (use with caution) */ async function select(tableName, whereClause, params, keyspace, limit, allowFiltering = false) { const { tenant } = await (0, base_1.getTenantConnection)(); const targetKeyspace = keyspace || tenant.appdbname; // Sanitize identifiers const sanitizedKeyspace = sanitizeCQLIdentifier(targetKeyspace, "keyspace"); const sanitizedTable = sanitizeCQLIdentifier(tableName, "table"); let query = `SELECT * FROM ${sanitizedKeyspace}.${sanitizedTable}`; // Note: whereClause should be provided by the caller and use parameterized queries // We validate it doesn't contain dangerous patterns if (whereClause) { // Basic validation - whereClause should only contain column names and operators if (/[;'"]/.test(whereClause)) { throw new errors_1.ValidationError("WHERE clause contains potentially dangerous characters. Use parameterized queries."); } query += ` WHERE ${whereClause}`; } if (limit && limit > 0) { query += ` LIMIT ${limit}`; } if (allowFiltering) { query += ` ALLOW FILTERING`; } return await executeCQL(query, params); } /** * Update rows in a table * * @param tableName - Table name * @param data - Data to update (column -> value mapping) * @param whereClause - WHERE clause (e.g., "id = ?") * @param whereParams - Parameters for WHERE clause * @param keyspace - Optional keyspace (defaults to tenant's keyspace) * @param ttl - Optional TTL in seconds */ async function update(tableName, data, whereClause, whereParams, keyspace, ttl) { const { tenant } = await (0, base_1.getTenantConnection)(); const targetKeyspace = keyspace || tenant.appdbname; // Sanitize identifiers const sanitizedKeyspace = sanitizeCQLIdentifier(targetKeyspace, "keyspace"); const sanitizedTable = sanitizeCQLIdentifier(tableName, "table"); const sanitizedColumns = Object.keys(data).map((col) => sanitizeCQLIdentifier(col, "column")); const setClause = sanitizedColumns.map((col) => `${col} = ?`).join(", "); const values = [...Object.values(data), ...whereParams]; // Validate whereClause if (/[;'"]/.test(whereClause)) { throw new errors_1.ValidationError("WHERE clause contains potentially dangerous characters. Use parameterized queries."); } let query = `UPDATE ${sanitizedKeyspace}.${sanitizedTable} SET ${setClause} WHERE ${whereClause}`; if (ttl && ttl > 0) { query += ` USING TTL ${ttl}`; } return await executeCQL(query, values); } /** * Delete rows from a table * * @param tableName - Table name * @param whereClause - WHERE clause (e.g., "id = ?") * @param params - Parameters for WHERE clause * @param keyspace - Optional keyspace (defaults to tenant's keyspace) */ async function remove(tableName, whereClause, params, keyspace) { const { tenant } = await (0, base_1.getTenantConnection)(); const targetKeyspace = keyspace || tenant.appdbname; // Sanitize identifiers const sanitizedKeyspace = sanitizeCQLIdentifier(targetKeyspace, "keyspace"); const sanitizedTable = sanitizeCQLIdentifier(tableName, "table"); // Validate whereClause if (/[;'"]/.test(whereClause)) { throw new errors_1.ValidationError("WHERE clause contains potentially dangerous characters. Use parameterized queries."); } const query = `DELETE FROM ${sanitizedKeyspace}.${sanitizedTable} WHERE ${whereClause}`; return await executeCQL(query, params); } /** * Close Cassandra client for a specific tenant */ async function closeCassandraClient(tenant) { if (!tenant) { const { tenant: currentTenant } = await (0, base_1.getTenantConnection)(); tenant = currentTenant; } const cacheKey = `${tenant.appid}-${tenant.orgid}-${tenant.appdbname}`; const client = cassandraClients.get(cacheKey); if (client) { await client.shutdown(); cassandraClients.delete(cacheKey); loggers_1.default.debug(`Closed Cassandra client for ${cacheKey}`); } } /** * Close all Cassandra clients */ async function closeAllCassandraClients() { const closePromises = Array.from(cassandraClients.values()).map((client) => client.shutdown().catch((error) => { loggers_1.default.warn(`Error closing Cassandra client: ${error.message}`); })); await Promise.all(closePromises); cassandraClients.clear(); loggers_1.default.info("All Cassandra clients closed"); }