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>
This commit is contained in:
277
src/utils/query-builder.ts
Normal file
277
src/utils/query-builder.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 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() };
|
||||
}
|
||||
Reference in New Issue
Block a user