multibridge
Version:
A multi-database connection framework with centralized configuration
297 lines (296 loc) • 12.3 kB
JavaScript
;
/**
* 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");
}