Files
mcp-outline-postgresql/src/utils/query-builder.ts
Emanuel Almeida b4ba42cbf1 feat: Add production-ready utilities and performance improvements
Security & Data Integrity:
- Centralized transaction helper with deadlock retry (exponential backoff)
- SafeQueryBuilder for safe parameterized queries
- Zod-based input validation middleware
- Audit logging to Outline's events table

Performance:
- Cursor-based pagination for large datasets
- Pool monitoring with configurable alerts
- Database index migrations for optimal query performance

Changes:
- Refactored bulk-operations, desk-sync, export-import to use centralized transaction helper
- Added 7 new utility modules (audit, monitoring, pagination, query-builder, transaction, validation)
- Created migrations/001_indexes.sql with 40+ recommended indexes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 15:23:32 +00:00

278 lines
6.2 KiB
TypeScript

/**
* MCP Outline PostgreSQL - Safe Query Builder
* Helper for parameterized queries to prevent SQL injection
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { sanitizeInput, isValidUUID } from './security.js';
/**
* Safe query builder with automatic parameter handling
*/
export class SafeQueryBuilder {
private params: any[] = [];
private paramIndex = 1;
private conditions: string[] = [];
/**
* Add a parameter and return its placeholder
*/
addParam(value: any): string {
this.params.push(value);
return `$${this.paramIndex++}`;
}
/**
* Get current parameter index (useful for manual queries)
*/
getNextIndex(): number {
return this.paramIndex;
}
/**
* Build ILIKE condition (case-insensitive search)
*/
buildILike(column: string, value: string): string {
const sanitized = sanitizeInput(value);
return `${column} ILIKE ${this.addParam(`%${sanitized}%`)}`;
}
/**
* Build exact ILIKE condition (no wildcards)
*/
buildILikeExact(column: string, value: string): string {
const sanitized = sanitizeInput(value);
return `${column} ILIKE ${this.addParam(sanitized)}`;
}
/**
* Build ILIKE condition with prefix match
*/
buildILikePrefix(column: string, value: string): string {
const sanitized = sanitizeInput(value);
return `${column} ILIKE ${this.addParam(`${sanitized}%`)}`;
}
/**
* Build IN clause for array of values
*/
buildIn(column: string, values: any[]): string {
if (values.length === 0) {
return 'FALSE'; // Empty IN clause
}
return `${column} = ANY(${this.addParam(values)})`;
}
/**
* Build NOT IN clause
*/
buildNotIn(column: string, values: any[]): string {
if (values.length === 0) {
return 'TRUE'; // Empty NOT IN clause
}
return `${column} != ALL(${this.addParam(values)})`;
}
/**
* Build equals condition
*/
buildEquals(column: string, value: any): string {
return `${column} = ${this.addParam(value)}`;
}
/**
* Build not equals condition
*/
buildNotEquals(column: string, value: any): string {
return `${column} != ${this.addParam(value)}`;
}
/**
* Build greater than condition
*/
buildGreaterThan(column: string, value: any): string {
return `${column} > ${this.addParam(value)}`;
}
/**
* Build greater than or equals condition
*/
buildGreaterThanOrEquals(column: string, value: any): string {
return `${column} >= ${this.addParam(value)}`;
}
/**
* Build less than condition
*/
buildLessThan(column: string, value: any): string {
return `${column} < ${this.addParam(value)}`;
}
/**
* Build less than or equals condition
*/
buildLessThanOrEquals(column: string, value: any): string {
return `${column} <= ${this.addParam(value)}`;
}
/**
* Build BETWEEN condition
*/
buildBetween(column: string, from: any, to: any): string {
return `${column} BETWEEN ${this.addParam(from)} AND ${this.addParam(to)}`;
}
/**
* Build IS NULL condition
*/
buildIsNull(column: string): string {
return `${column} IS NULL`;
}
/**
* Build IS NOT NULL condition
*/
buildIsNotNull(column: string): string {
return `${column} IS NOT NULL`;
}
/**
* Build UUID equals condition with validation
*/
buildUUIDEquals(column: string, uuid: string): string {
if (!isValidUUID(uuid)) {
throw new Error(`Invalid UUID: ${uuid}`);
}
return `${column} = ${this.addParam(uuid)}`;
}
/**
* Build UUID IN clause with validation
*/
buildUUIDIn(column: string, uuids: string[]): string {
for (const uuid of uuids) {
if (!isValidUUID(uuid)) {
throw new Error(`Invalid UUID: ${uuid}`);
}
}
return this.buildIn(column, uuids);
}
/**
* Add a condition to the internal conditions array
*/
addCondition(condition: string): this {
this.conditions.push(condition);
return this;
}
/**
* Add a condition if value is truthy
*/
addConditionIf(condition: string, value: any): this {
if (value !== undefined && value !== null && value !== '') {
this.conditions.push(condition);
}
return this;
}
/**
* Build WHERE clause from accumulated conditions
*/
buildWhereClause(separator = ' AND '): string {
if (this.conditions.length === 0) {
return '';
}
return `WHERE ${this.conditions.join(separator)}`;
}
/**
* Get all parameters
*/
getParams(): any[] {
return this.params;
}
/**
* Get conditions array
*/
getConditions(): string[] {
return this.conditions;
}
/**
* Reset builder state
*/
reset(): this {
this.params = [];
this.paramIndex = 1;
this.conditions = [];
return this;
}
/**
* Clone builder (useful for subqueries)
*/
clone(): SafeQueryBuilder {
const clone = new SafeQueryBuilder();
clone.params = [...this.params];
clone.paramIndex = this.paramIndex;
clone.conditions = [...this.conditions];
return clone;
}
}
/**
* Create a new SafeQueryBuilder instance
*/
export function createQueryBuilder(): SafeQueryBuilder {
return new SafeQueryBuilder();
}
/**
* Build a simple SELECT query
*/
export function buildSelectQuery(
table: string,
columns: string[],
builder: SafeQueryBuilder,
options?: {
orderBy?: string;
orderDirection?: 'ASC' | 'DESC';
limit?: number;
offset?: number;
}
): { query: string; params: any[] } {
const columnsStr = columns.join(', ');
const whereClause = builder.buildWhereClause();
let query = `SELECT ${columnsStr} FROM ${table} ${whereClause}`;
if (options?.orderBy) {
const direction = options.orderDirection || 'DESC';
query += ` ORDER BY ${options.orderBy} ${direction}`;
}
if (options?.limit) {
query += ` LIMIT ${builder.addParam(options.limit)}`;
}
if (options?.offset) {
query += ` OFFSET ${builder.addParam(options.offset)}`;
}
return { query, params: builder.getParams() };
}
/**
* Build a COUNT query
*/
export function buildCountQuery(
table: string,
builder: SafeQueryBuilder
): { query: string; params: any[] } {
const whereClause = builder.buildWhereClause();
const query = `SELECT COUNT(*) as count FROM ${table} ${whereClause}`;
return { query, params: builder.getParams() };
}