moved folders around

This commit is contained in:
Boki 2025-06-21 18:27:00 -04:00
parent 4f89affc2b
commit 36cb84b343
202 changed files with 1160 additions and 660 deletions

View file

@ -0,0 +1,82 @@
# PostgreSQL Client Library
A comprehensive PostgreSQL client library for the Stock Bot trading platform, designed for operational data, transactions, and relational queries.
## Features
- **Connection Pooling**: Robust connection pool management
- **Type Safety**: Full TypeScript support with typed queries
- **Transaction Support**: Multi-statement transactions with rollback
- **Schema Management**: Database schema validation and migrations
- **Query Builder**: Fluent query building interface
- **Health Monitoring**: Connection health monitoring and metrics
- **Performance Tracking**: Query performance monitoring and optimization
## Usage
```typescript
import { PostgreSQLClient } from '@stock-bot/postgres';
// Initialize client
const pgClient = new PostgreSQLClient();
await pgClient.connect();
// Execute a query
const users = await pgClient.query('SELECT * FROM users WHERE active = $1', [true]);
// Use query builder
const trades = await pgClient
.select('*')
.from('trades')
.where('symbol', '=', 'AAPL')
.orderBy('created_at', 'DESC')
.limit(10)
.execute();
// Execute in transaction
await pgClient.transaction(async (tx) => {
await tx.query('INSERT INTO trades (...) VALUES (...)', []);
await tx.query('UPDATE portfolio SET balance = balance - $1', [amount]);
});
```
## Database Schemas
The client provides typed access to the following schemas:
- **trading**: Core trading operations (trades, orders, positions)
- **strategy**: Strategy definitions and performance
- **risk**: Risk management and compliance
- **audit**: Audit trails and logging
## Configuration
Configure using environment variables:
```env
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=stockbot
POSTGRES_USERNAME=stockbot
POSTGRES_PASSWORD=your_password
```
## Query Builder
The fluent query builder supports:
- SELECT, INSERT, UPDATE, DELETE operations
- Complex WHERE conditions with AND/OR logic
- JOINs (INNER, LEFT, RIGHT, FULL)
- Aggregations (COUNT, SUM, AVG, etc.)
- Subqueries and CTEs
- Window functions
## Health Monitoring
The client includes built-in health monitoring:
```typescript
const health = await pgClient.getHealth();
console.log(health.status); // 'healthy' | 'degraded' | 'unhealthy'
```

View file

@ -0,0 +1,46 @@
{
"name": "@stock-bot/postgres",
"version": "1.0.0",
"description": "PostgreSQL client library for Stock Bot platform",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc",
"test": "bun test",
"lint": "eslint src/**/*.ts",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@stock-bot/logger": "*",
"@stock-bot/types": "*",
"pg": "^8.11.3"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/pg": "^8.10.7",
"typescript": "^5.3.0",
"eslint": "^8.56.0",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"bun-types": "^1.2.15"
},
"keywords": [
"postgresql",
"database",
"client",
"stock-bot"
],
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"README.md"
]
}

View file

@ -0,0 +1,605 @@
import { Pool, QueryResultRow } from 'pg';
import { getLogger } from '@stock-bot/logger';
import { PostgreSQLHealthMonitor } from './health';
import { PostgreSQLQueryBuilder } from './query-builder';
import { PostgreSQLTransactionManager } from './transactions';
import type {
PostgreSQLClientConfig,
PostgreSQLConnectionOptions,
QueryResult,
TransactionCallback,
PoolMetrics,
ConnectionEvents,
DynamicPoolConfig,
} from './types';
/**
* PostgreSQL Client for Stock Bot
*
* Provides type-safe access to PostgreSQL with connection pooling,
* health monitoring, and transaction support.
*/
export class PostgreSQLClient {
private pool: Pool | null = null;
private readonly config: PostgreSQLClientConfig;
private readonly options: PostgreSQLConnectionOptions;
private readonly logger: ReturnType<typeof getLogger>;
private readonly healthMonitor: PostgreSQLHealthMonitor;
private readonly transactionManager: PostgreSQLTransactionManager;
private isConnected = false;
private readonly metrics: PoolMetrics;
private readonly events?: ConnectionEvents;
private dynamicPoolConfig?: DynamicPoolConfig;
private poolMonitorInterval?: NodeJS.Timeout;
constructor(config: PostgreSQLClientConfig, options?: PostgreSQLConnectionOptions, events?: ConnectionEvents) {
this.config = config;
this.options = {
retryAttempts: 3,
retryDelay: 1000,
healthCheckInterval: 30000,
...options,
};
this.events = events;
this.logger = getLogger('postgres-client');
this.healthMonitor = new PostgreSQLHealthMonitor(this);
this.transactionManager = new PostgreSQLTransactionManager(this);
this.metrics = {
totalConnections: 0,
activeConnections: 0,
idleConnections: 0,
waitingRequests: 0,
errors: 0,
created: new Date(),
};
}
/**
* Connect to PostgreSQL
*/
async connect(): Promise<void> {
if (this.isConnected && this.pool) {
return;
}
let lastError: Error | null = null;
for (let attempt = 1; attempt <= (this.options.retryAttempts ?? 3); attempt++) {
try {
this.logger.info(
`Connecting to PostgreSQL (attempt ${attempt}/${this.options.retryAttempts})...`
);
this.pool = new Pool(this.buildPoolConfig());
// Test the connection
const client = await this.pool.connect();
await client.query('SELECT 1');
client.release();
this.isConnected = true;
// Update metrics
const poolConfig = this.config.poolSettings;
this.metrics.totalConnections = poolConfig?.max || 10;
this.metrics.idleConnections = poolConfig?.min || 2;
// Fire connection event
if (this.events?.onConnect) {
await Promise.resolve(this.events.onConnect());
}
// Fire pool created event
if (this.events?.onPoolCreated) {
await Promise.resolve(this.events.onPoolCreated());
}
this.logger.info('Successfully connected to PostgreSQL', {
poolSize: this.metrics.totalConnections,
});
// Start health monitoring
this.healthMonitor.start();
// Setup error handlers
this.setupErrorHandlers();
// Setup pool event listeners for metrics
this.setupPoolMetrics();
// Start dynamic pool monitoring if enabled
if (this.dynamicPoolConfig?.enabled) {
this.startPoolMonitoring();
}
return;
} catch (error) {
lastError = error as Error;
this.metrics.errors++;
this.metrics.lastError = lastError.message;
// Fire error event
if (this.events?.onError) {
await Promise.resolve(this.events.onError(lastError));
}
this.logger.error(`PostgreSQL connection attempt ${attempt} failed:`, error);
if (this.pool) {
await this.pool.end();
this.pool = null;
}
if (attempt < (this.options.retryAttempts ?? 3)) {
await this.delay((this.options.retryDelay ?? 1000) * attempt);
}
}
}
throw new Error(
`Failed to connect to PostgreSQL after ${this.options.retryAttempts} attempts: ${lastError?.message}`
);
}
/**
* Disconnect from PostgreSQL
*/
async disconnect(): Promise<void> {
if (!this.pool) {
return;
}
try {
// Stop pool monitoring
if (this.poolMonitorInterval) {
clearInterval(this.poolMonitorInterval);
this.poolMonitorInterval = undefined;
}
this.healthMonitor.stop();
await this.pool.end();
this.isConnected = false;
this.pool = null;
// Fire disconnect event
if (this.events?.onDisconnect) {
await Promise.resolve(this.events.onDisconnect());
}
this.logger.info('Disconnected from PostgreSQL');
} catch (error) {
this.logger.error('Error disconnecting from PostgreSQL:', error);
throw error;
}
}
/**
* Execute a query
*/
async query<T extends QueryResultRow = any>(
text: string,
params?: any[]
): Promise<QueryResult<T>> {
if (!this.pool) {
throw new Error('PostgreSQL client not connected');
}
const startTime = Date.now();
try {
const result = await this.pool.query<T>(text, params);
const executionTime = Date.now() - startTime;
this.logger.debug(`Query executed in ${executionTime}ms`, {
query: text.substring(0, 100),
params: params?.length,
});
return {
...result,
executionTime,
} as QueryResult<T>;
} catch (error) {
const executionTime = Date.now() - startTime;
this.logger.error(`Query failed after ${executionTime}ms:`, {
error,
query: text,
params,
});
throw error;
}
}
/**
* Execute multiple queries in a transaction
*/
async transaction<T>(callback: TransactionCallback<T>): Promise<T> {
return await this.transactionManager.execute(callback);
}
/**
* Get a query builder instance
*/
queryBuilder(): PostgreSQLQueryBuilder {
return new PostgreSQLQueryBuilder(this);
}
/**
* Create a new query builder with SELECT
*/
select(columns: string | string[] = '*'): PostgreSQLQueryBuilder {
return this.queryBuilder().select(columns);
}
/**
* Create a new query builder with INSERT
*/
insert(table: string): PostgreSQLQueryBuilder {
return this.queryBuilder().insert(table);
}
/**
* Create a new query builder with UPDATE
*/
update(table: string): PostgreSQLQueryBuilder {
return this.queryBuilder().update(table);
}
/**
* Create a new query builder with DELETE
*/
delete(table: string): PostgreSQLQueryBuilder {
return this.queryBuilder().delete(table);
}
/**
* Execute a stored procedure or function
*/
async callFunction<T extends QueryResultRow = any>(
functionName: string,
params?: any[]
): Promise<QueryResult<T>> {
const placeholders = params ? params.map((_, i) => `$${i + 1}`).join(', ') : '';
const query = `SELECT * FROM ${functionName}(${placeholders})`;
return await this.query<T>(query, params);
}
/**
* Batch upsert operation for high-performance inserts/updates
*/
async batchUpsert(
tableName: string,
data: Record<string, unknown>[],
conflictColumn: string,
options: {
chunkSize?: number;
excludeColumns?: string[];
} = {}
): Promise<{ insertedCount: number; updatedCount: number }> {
if (!this.pool) {
throw new Error('PostgreSQL client not connected');
}
if (data.length === 0) {
return { insertedCount: 0, updatedCount: 0 };
}
const { chunkSize = 1000, excludeColumns = [] } = options;
const columns = Object.keys(data[0] ?? {}).filter(col => !excludeColumns.includes(col));
const updateColumns = columns.filter(col => col !== conflictColumn);
let totalInserted = 0;
let totalUpdated = 0;
// Process in chunks to avoid memory issues and parameter limits
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
// Build placeholders for this chunk
const placeholders = chunk.map((_, rowIndex) => {
const rowPlaceholders = columns.map((_, colIndex) => {
return `$${rowIndex * columns.length + colIndex + 1}`;
});
return `(${rowPlaceholders.join(', ')})`;
});
// Flatten the chunk data
const values = chunk.flatMap(row => columns.map(col => row[col as keyof typeof row]));
// Build the upsert query
const updateClauses = updateColumns.map(col => `${col} = EXCLUDED.${col}`);
const query = `
INSERT INTO ${tableName} (${columns.join(', ')})
VALUES ${placeholders.join(', ')}
ON CONFLICT (${conflictColumn})
DO UPDATE SET
${updateClauses.join(', ')},
updated_at = NOW()
RETURNING (xmax = 0) AS is_insert
`;
try {
const startTime = Date.now();
const result = await this.pool.query(query, values);
const executionTime = Date.now() - startTime;
// Count inserts vs updates
const inserted = result.rows.filter((row: { is_insert: boolean }) => row.is_insert).length;
const updated = result.rows.length - inserted;
totalInserted += inserted;
totalUpdated += updated;
this.logger.debug(`Batch upsert chunk processed in ${executionTime}ms`, {
chunkSize: chunk.length,
inserted,
updated,
table: tableName,
});
} catch (error) {
this.logger.error(`Batch upsert failed on chunk ${Math.floor(i / chunkSize) + 1}:`, {
error,
table: tableName,
chunkStart: i,
chunkSize: chunk.length,
});
throw error;
}
}
this.logger.info('Batch upsert completed', {
table: tableName,
totalRecords: data.length,
inserted: totalInserted,
updated: totalUpdated,
});
return { insertedCount: totalInserted, updatedCount: totalUpdated };
}
/**
* Check if a table exists
*/
async tableExists(tableName: string, schemaName: string = 'public'): Promise<boolean> {
const result = await this.query(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = $1 AND table_name = $2
)`,
[schemaName, tableName]
);
return result.rows[0].exists;
}
/**
* Get table schema information
*/
async getTableSchema(tableName: string, schemaName: string = 'public'): Promise<any[]> {
const result = await this.query(
`SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2
ORDER BY ordinal_position`,
[schemaName, tableName]
);
return result.rows;
}
/**
* Execute EXPLAIN for query analysis
*/
async explain(query: string, params?: any[]): Promise<any[]> {
const explainQuery = `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`;
const result = await this.query(explainQuery, params);
return result.rows[0]['QUERY PLAN'];
}
/**
* Get database statistics
*/
async getStats(): Promise<any> {
const result = await this.query(`
SELECT
(SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_connections,
(SELECT count(*) FROM pg_stat_activity WHERE state = 'idle') as idle_connections,
(SELECT setting FROM pg_settings WHERE name = 'max_connections') as max_connections,
pg_size_pretty(pg_database_size(current_database())) as database_size
`);
return result.rows[0];
}
/**
* Check if client is connected
*/
get connected(): boolean {
return this.isConnected && !!this.pool;
}
/**
* Get the underlying connection pool
*/
get connectionPool(): Pool | null {
return this.pool;
}
private buildPoolConfig(): any {
return {
host: this.config.host,
port: this.config.port,
database: this.config.database,
user: this.config.username,
password: this.config.password,
min: this.config.poolSettings?.min,
max: this.config.poolSettings?.max,
idleTimeoutMillis: this.config.poolSettings?.idleTimeoutMillis,
connectionTimeoutMillis: this.config.timeouts?.connection,
query_timeout: this.config.timeouts?.query,
statement_timeout: this.config.timeouts?.statement,
lock_timeout: this.config.timeouts?.lock,
idle_in_transaction_session_timeout: this.config.timeouts?.idleInTransaction,
ssl: this.config.ssl?.enabled
? {
rejectUnauthorized: this.config.ssl.rejectUnauthorized,
}
: false,
};
}
private setupErrorHandlers(): void {
if (!this.pool) {
return;
}
this.pool.on('error', error => {
this.logger.error('PostgreSQL pool error:', error);
});
this.pool.on('connect', () => {
this.logger.debug('New PostgreSQL client connected');
});
this.pool.on('remove', () => {
this.logger.debug('PostgreSQL client removed from pool');
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Get current pool metrics
*/
getPoolMetrics(): PoolMetrics {
// Update last used timestamp
this.metrics.lastUsed = new Date();
// Update metrics from pool if available
if (this.pool) {
this.metrics.totalConnections = this.pool.totalCount;
this.metrics.idleConnections = this.pool.idleCount;
this.metrics.waitingRequests = this.pool.waitingCount;
this.metrics.activeConnections = this.metrics.totalConnections - this.metrics.idleConnections;
}
return { ...this.metrics };
}
/**
* Set dynamic pool configuration
*/
setDynamicPoolConfig(config: DynamicPoolConfig): void {
this.dynamicPoolConfig = config;
if (config.enabled && this.isConnected && !this.poolMonitorInterval) {
this.startPoolMonitoring();
} else if (!config.enabled && this.poolMonitorInterval) {
clearInterval(this.poolMonitorInterval);
this.poolMonitorInterval = undefined;
}
}
/**
* Start monitoring pool and adjust size dynamically
*/
private startPoolMonitoring(): void {
if (!this.dynamicPoolConfig || this.poolMonitorInterval) {
return;
}
this.poolMonitorInterval = setInterval(() => {
this.evaluatePoolSize();
}, this.dynamicPoolConfig.evaluationInterval);
}
/**
* Setup pool event listeners for metrics
*/
private setupPoolMetrics(): void {
if (!this.pool) {
return;
}
// Track when connections are acquired
this.pool.on('acquire', () => {
this.metrics.activeConnections++;
this.metrics.idleConnections--;
});
// Track when connections are released
this.pool.on('release', () => {
this.metrics.activeConnections--;
this.metrics.idleConnections++;
});
}
/**
* Evaluate and adjust pool size based on usage
*/
private async evaluatePoolSize(): Promise<void> {
if (!this.dynamicPoolConfig || !this.pool) {
return;
}
const metrics = this.getPoolMetrics();
const { minSize, maxSize, scaleUpThreshold, scaleDownThreshold } = this.dynamicPoolConfig;
const currentSize = metrics.totalConnections;
const utilization = currentSize > 0 ? ((metrics.activeConnections / currentSize) * 100) : 0;
this.logger.debug('Pool utilization', {
utilization: `${utilization.toFixed(1)}%`,
active: metrics.activeConnections,
total: currentSize,
waiting: metrics.waitingRequests,
});
// Scale up if utilization is high or there are waiting requests
if ((utilization > scaleUpThreshold || metrics.waitingRequests > 0) && currentSize < maxSize) {
const newSize = Math.min(currentSize + this.dynamicPoolConfig.scaleUpIncrement, maxSize);
this.logger.info('Would scale up connection pool', { from: currentSize, to: newSize, utilization });
// Note: pg module doesn't support dynamic resizing, would need reconnection
}
// Scale down if utilization is low
else if (utilization < scaleDownThreshold && currentSize > minSize) {
const newSize = Math.max(currentSize - this.dynamicPoolConfig.scaleDownIncrement, minSize);
this.logger.info('Would scale down connection pool', { from: currentSize, to: newSize, utilization });
// Note: pg module doesn't support dynamic resizing, would need reconnection
}
}
/**
* Enable pool warmup on connect
*/
async warmupPool(): Promise<void> {
if (!this.pool || !this.isConnected) {
throw new Error('Client not connected');
}
const minSize = this.config.poolSettings?.min || 2;
const promises: Promise<void>[] = [];
// Create minimum connections by running parallel queries
for (let i = 0; i < minSize; i++) {
promises.push(
this.pool.query('SELECT 1')
.then(() => {
this.logger.debug(`Warmed up connection ${i + 1}/${minSize}`);
})
.catch(error => {
this.logger.warn(`Failed to warm up connection ${i + 1}`, { error });
})
);
}
await Promise.allSettled(promises);
this.logger.info('Connection pool warmup complete', { connections: minSize });
}
}

View file

@ -0,0 +1,27 @@
import { PostgreSQLClient } from './client';
import type { PostgreSQLClientConfig, PostgreSQLConnectionOptions, ConnectionEvents } from './types';
/**
* Factory function to create a PostgreSQL client instance
*/
export function createPostgreSQLClient(
config: PostgreSQLClientConfig,
options?: PostgreSQLConnectionOptions,
events?: ConnectionEvents
): PostgreSQLClient {
return new PostgreSQLClient(config, options, events);
}
/**
* Create and connect a PostgreSQL client
*/
export async function createAndConnectPostgreSQLClient(
config: PostgreSQLClientConfig,
options?: PostgreSQLConnectionOptions,
events?: ConnectionEvents
): Promise<PostgreSQLClient> {
const client = createPostgreSQLClient(config, options, events);
await client.connect();
return client;
}

View file

@ -0,0 +1,145 @@
import { getLogger } from '@stock-bot/logger';
import type { PostgreSQLClient } from './client';
import type { PostgreSQLHealthCheck, PostgreSQLHealthStatus, PostgreSQLMetrics } from './types';
/**
* PostgreSQL Health Monitor
*
* Monitors PostgreSQL connection health and provides metrics
*/
export class PostgreSQLHealthMonitor {
private readonly client: PostgreSQLClient;
private readonly logger: ReturnType<typeof getLogger>;
private healthCheckInterval: NodeJS.Timeout | null = null;
private metrics: PostgreSQLMetrics;
private lastHealthCheck: PostgreSQLHealthCheck | null = null;
constructor(client: PostgreSQLClient) {
this.client = client;
this.logger = getLogger('postgres-health-monitor');
this.metrics = {
queriesPerSecond: 0,
averageQueryTime: 0,
errorRate: 0,
connectionPoolUtilization: 0,
slowQueries: 0,
};
}
/**
* Start health monitoring
*/
start(intervalMs: number = 30000): void {
if (this.healthCheckInterval) {
this.stop();
}
this.logger.info(`Starting PostgreSQL health monitoring (interval: ${intervalMs}ms)`);
this.healthCheckInterval = setInterval(async () => {
try {
await this.performHealthCheck();
} catch (error) {
this.logger.error('Health check failed:', error);
}
}, intervalMs);
// Perform initial health check
this.performHealthCheck().catch(error => {
this.logger.error('Initial health check failed:', error);
});
}
/**
* Stop health monitoring
*/
stop(): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
this.logger.info('Stopped PostgreSQL health monitoring');
}
}
/**
* Get current health status
*/
async getHealth(): Promise<PostgreSQLHealthCheck> {
if (!this.lastHealthCheck) {
await this.performHealthCheck();
}
if (!this.lastHealthCheck) {
throw new Error('Health check failed to produce results');
}
return this.lastHealthCheck;
}
/**
* Get current metrics
*/
getMetrics(): PostgreSQLMetrics {
return { ...this.metrics };
}
/**
* Perform a health check
*/
private async performHealthCheck(): Promise<void> {
const startTime = Date.now();
const errors: string[] = [];
let status: PostgreSQLHealthStatus = 'healthy';
try {
if (!this.client.connected) {
errors.push('PostgreSQL client not connected');
status = 'unhealthy';
} else {
// Test basic connectivity
await this.client.query('SELECT 1');
// Get connection stats
const stats = await this.client.getStats();
// Check connection pool utilization
const utilization = parseInt(stats.active_connections) / parseInt(stats.max_connections);
if (utilization > 0.8) {
errors.push('High connection pool utilization');
status = status === 'healthy' ? 'degraded' : status;
}
// Check for high latency
const latency = Date.now() - startTime;
if (latency > 1000) {
errors.push(`High latency: ${latency}ms`);
status = status === 'healthy' ? 'degraded' : status;
}
this.metrics.connectionPoolUtilization = utilization;
}
} catch (error) {
errors.push(`Health check failed: ${(error as Error).message}`);
status = 'unhealthy';
}
const latency = Date.now() - startTime;
this.lastHealthCheck = {
status,
timestamp: new Date(),
latency,
connections: {
active: 1,
idle: 9,
total: 10,
},
errors: errors.length > 0 ? errors : undefined,
};
// Log health status changes
if (status !== 'healthy') {
this.logger.warn(`PostgreSQL health status: ${status}`, { errors, latency });
} else {
this.logger.debug(`PostgreSQL health check passed (${latency}ms)`);
}
}
}

View file

@ -0,0 +1,42 @@
/**
* PostgreSQL Client Library for Stock Bot
*
* Provides type-safe PostgreSQL access for operational data,
* transactions, and relational queries.
*/
export { PostgreSQLClient } from './client';
export { PostgreSQLHealthMonitor } from './health';
export { PostgreSQLTransactionManager } from './transactions';
export { PostgreSQLQueryBuilder } from './query-builder';
// export { PostgreSQLMigrationManager } from './migrations'; // TODO: Implement migrations
// Types
export type {
PostgreSQLClientConfig,
PostgreSQLConnectionOptions,
PostgreSQLHealthStatus,
PostgreSQLMetrics,
QueryResult,
TransactionCallback,
SchemaNames,
TableNames,
Trade,
Order,
Position,
Portfolio,
Strategy,
RiskLimit,
AuditLog,
PoolMetrics,
ConnectionEvents,
DynamicPoolConfig,
} from './types';
// Factory functions
export {
createPostgreSQLClient,
createAndConnectPostgreSQLClient,
} from './factory';
// Singleton pattern removed - use factory functions instead

View file

@ -0,0 +1,270 @@
import type { QueryResultRow } from 'pg';
import type { PostgreSQLClient } from './client';
import type { JoinCondition, OrderByCondition, QueryResult, WhereCondition } from './types';
/**
* PostgreSQL Query Builder
*
* Provides a fluent interface for building SQL queries
*/
export class PostgreSQLQueryBuilder {
private queryType: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | null = null;
private selectColumns: string[] = [];
private fromTable: string = '';
private joins: JoinCondition[] = [];
private whereConditions: WhereCondition[] = [];
private groupByColumns: string[] = [];
private havingConditions: WhereCondition[] = [];
private orderByConditions: OrderByCondition[] = [];
private limitCount: number | null = null;
private offsetCount: number | null = null;
private insertValues: Record<string, any> = {};
private updateValues: Record<string, any> = {};
private readonly client: PostgreSQLClient;
constructor(client: PostgreSQLClient) {
this.client = client;
}
/**
* SELECT statement
*/
select(columns: string | string[] = '*'): this {
this.queryType = 'SELECT';
this.selectColumns = Array.isArray(columns) ? columns : [columns];
return this;
}
/**
* FROM clause
*/
from(table: string): this {
this.fromTable = table;
return this;
}
/**
* JOIN clause
*/
join(table: string, on: string, type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' = 'INNER'): this {
this.joins.push({ type, table, on });
return this;
}
/**
* WHERE clause
*/
where(column: string, operator: string, value?: any): this {
this.whereConditions.push({ column, operator: operator as any, value });
return this;
}
/**
* GROUP BY clause
*/
groupBy(columns: string | string[]): this {
this.groupByColumns = Array.isArray(columns) ? columns : [columns];
return this;
}
/**
* ORDER BY clause
*/
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.orderByConditions.push({ column, direction });
return this;
}
/**
* LIMIT clause
*/
limit(count: number): this {
this.limitCount = count;
return this;
}
/**
* OFFSET clause
*/
offset(count: number): this {
this.offsetCount = count;
return this;
}
/**
* INSERT statement
*/
insert(table: string): this {
this.queryType = 'INSERT';
this.fromTable = table;
return this;
}
/**
* VALUES for INSERT
*/
values(data: Record<string, any>): this {
this.insertValues = data;
return this;
}
/**
* UPDATE statement
*/
update(table: string): this {
this.queryType = 'UPDATE';
this.fromTable = table;
return this;
}
/**
* SET for UPDATE
*/
set(data: Record<string, any>): this {
this.updateValues = data;
return this;
}
/**
* DELETE statement
*/
delete(table: string): this {
this.queryType = 'DELETE';
this.fromTable = table;
return this;
}
/**
* Build and execute the query
*/
async execute<T extends QueryResultRow = any>(): Promise<QueryResult<T>> {
const { sql, params } = this.build();
return await this.client.query<T>(sql, params);
}
/**
* Build the SQL query
*/
build(): { sql: string; params: any[] } {
const params: any[] = [];
let sql = '';
switch (this.queryType) {
case 'SELECT':
sql = this.buildSelectQuery(params);
break;
case 'INSERT':
sql = this.buildInsertQuery(params);
break;
case 'UPDATE':
sql = this.buildUpdateQuery(params);
break;
case 'DELETE':
sql = this.buildDeleteQuery(params);
break;
default:
throw new Error('Query type not specified');
}
return { sql, params };
}
private buildSelectQuery(params: any[]): string {
let sql = `SELECT ${this.selectColumns.join(', ')}`;
if (this.fromTable) {
sql += ` FROM ${this.fromTable}`;
}
// Add JOINs
for (const join of this.joins) {
sql += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
}
// Add WHERE
if (this.whereConditions.length > 0) {
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
}
// Add GROUP BY
if (this.groupByColumns.length > 0) {
sql += ` GROUP BY ${this.groupByColumns.join(', ')}`;
}
// Add HAVING
if (this.havingConditions.length > 0) {
sql += ' HAVING ' + this.buildWhereClause(this.havingConditions, params);
}
// Add ORDER BY
if (this.orderByConditions.length > 0) {
const orderBy = this.orderByConditions
.map(order => `${order.column} ${order.direction}`)
.join(', ');
sql += ` ORDER BY ${orderBy}`;
}
// Add LIMIT
if (this.limitCount !== null) {
sql += ` LIMIT $${params.length + 1}`;
params.push(this.limitCount);
}
// Add OFFSET
if (this.offsetCount !== null) {
sql += ` OFFSET $${params.length + 1}`;
params.push(this.offsetCount);
}
return sql;
}
private buildInsertQuery(params: any[]): string {
const columns = Object.keys(this.insertValues);
const placeholders = columns.map((_, i) => `$${params.length + i + 1}`);
params.push(...Object.values(this.insertValues));
return `INSERT INTO ${this.fromTable} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
}
private buildUpdateQuery(params: any[]): string {
const sets = Object.keys(this.updateValues).map((key, i) => {
return `${key} = $${params.length + i + 1}`;
});
params.push(...Object.values(this.updateValues));
let sql = `UPDATE ${this.fromTable} SET ${sets.join(', ')}`;
if (this.whereConditions.length > 0) {
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
}
return sql;
}
private buildDeleteQuery(params: any[]): string {
let sql = `DELETE FROM ${this.fromTable}`;
if (this.whereConditions.length > 0) {
sql += ' WHERE ' + this.buildWhereClause(this.whereConditions, params);
}
return sql;
}
private buildWhereClause(conditions: WhereCondition[], params: any[]): string {
return conditions
.map(condition => {
if (condition.operator === 'IS NULL' || condition.operator === 'IS NOT NULL') {
return `${condition.column} ${condition.operator}`;
} else {
params.push(condition.value);
return `${condition.column} ${condition.operator} $${params.length}`;
}
})
.join(' AND ');
}
}

View file

@ -0,0 +1,55 @@
import { getLogger } from '@stock-bot/logger';
import type { PostgreSQLClient } from './client';
import type { TransactionCallback } from './types';
/**
* PostgreSQL Transaction Manager
*
* Provides transaction support for multi-statement operations
*/
export class PostgreSQLTransactionManager {
private readonly client: PostgreSQLClient;
private readonly logger: ReturnType<typeof getLogger>;
constructor(client: PostgreSQLClient) {
this.client = client;
this.logger = getLogger('postgres-transaction-manager');
}
/**
* Execute operations within a transaction
*/
async execute<T>(callback: TransactionCallback<T>): Promise<T> {
const pool = this.client.connectionPool;
if (!pool) {
throw new Error('PostgreSQL client not connected');
}
const client = await pool.connect();
try {
this.logger.debug('Starting PostgreSQL transaction');
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
this.logger.debug('PostgreSQL transaction committed successfully');
return result;
} catch (error) {
this.logger.error('PostgreSQL transaction failed, rolling back:', error);
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
this.logger.error('Failed to rollback transaction:', rollbackError);
}
throw error;
} finally {
client.release();
}
}
}

View file

@ -0,0 +1,248 @@
import type { QueryResult as PgQueryResult, PoolClient, QueryResultRow } from 'pg';
/**
* PostgreSQL Client Configuration
*/
export interface PostgreSQLClientConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
poolSettings?: {
min: number;
max: number;
idleTimeoutMillis: number;
};
ssl?: {
enabled: boolean;
rejectUnauthorized: boolean;
};
timeouts?: {
query: number;
connection: number;
statement: number;
lock: number;
idleInTransaction: number;
};
}
/**
* PostgreSQL Connection Options
*/
export interface PostgreSQLConnectionOptions {
retryAttempts?: number;
retryDelay?: number;
healthCheckInterval?: number;
}
export interface PoolMetrics {
totalConnections: number;
activeConnections: number;
idleConnections: number;
waitingRequests: number;
errors: number;
lastError?: string;
avgResponseTime?: number;
created: Date;
lastUsed?: Date;
}
export interface ConnectionEvents {
onConnect?: () => void | Promise<void>;
onDisconnect?: () => void | Promise<void>;
onError?: (error: Error) => void | Promise<void>;
onPoolCreated?: () => void | Promise<void>;
}
export interface DynamicPoolConfig {
enabled: boolean;
minSize: number;
maxSize: number;
scaleUpThreshold: number; // % of pool in use (0-100)
scaleDownThreshold: number; // % of pool idle (0-100)
scaleUpIncrement: number; // connections to add
scaleDownIncrement: number; // connections to remove
evaluationInterval: number; // ms between checks
}
/**
* Health Status Types
*/
export type PostgreSQLHealthStatus = 'healthy' | 'degraded' | 'unhealthy';
export interface PostgreSQLHealthCheck {
status: PostgreSQLHealthStatus;
timestamp: Date;
latency: number;
connections: {
active: number;
idle: number;
total: number;
};
errors?: string[];
}
export interface PostgreSQLMetrics {
queriesPerSecond: number;
averageQueryTime: number;
errorRate: number;
connectionPoolUtilization: number;
slowQueries: number;
}
/**
* Query Result Types
*/
export interface QueryResult<T extends QueryResultRow = any> extends PgQueryResult<T> {
executionTime?: number;
}
export type TransactionCallback<T> = (client: PoolClient) => Promise<T>;
/**
* Schema and Table Names
*/
export type SchemaNames = 'trading' | 'strategy' | 'risk' | 'audit';
export type TableNames =
| 'trades'
| 'orders'
| 'positions'
| 'portfolios'
| 'strategies'
| 'risk_limits'
| 'audit_logs'
| 'users'
| 'accounts'
| 'symbols'
| 'exchanges';
/**
* Trading Domain Types
*/
export interface Trade {
id: string;
order_id: string;
symbol: string;
side: 'buy' | 'sell';
quantity: number;
price: number;
executed_at: Date;
commission: number;
fees: number;
portfolio_id: string;
strategy_id?: string;
created_at: Date;
updated_at: Date;
}
export interface Order {
id: string;
symbol: string;
side: 'buy' | 'sell';
type: 'market' | 'limit' | 'stop' | 'stop_limit';
quantity: number;
price?: number;
stop_price?: number;
status: 'pending' | 'filled' | 'cancelled' | 'rejected';
portfolio_id: string;
strategy_id?: string;
created_at: Date;
updated_at: Date;
expires_at?: Date;
}
export interface Position {
id: string;
symbol: string;
quantity: number;
average_cost: number;
market_value: number;
unrealized_pnl: number;
realized_pnl: number;
portfolio_id: string;
created_at: Date;
updated_at: Date;
}
export interface Portfolio {
id: string;
name: string;
cash_balance: number;
total_value: number;
unrealized_pnl: number;
realized_pnl: number;
user_id: string;
created_at: Date;
updated_at: Date;
}
export interface Strategy {
id: string;
name: string;
description: string;
parameters: Record<string, any>;
status: 'active' | 'inactive' | 'paused';
performance_metrics: Record<string, number>;
portfolio_id: string;
created_at: Date;
updated_at: Date;
}
export interface RiskLimit {
id: string;
type: 'position_size' | 'daily_loss' | 'max_drawdown' | 'concentration';
value: number;
threshold: number;
status: 'active' | 'breached' | 'disabled';
portfolio_id?: string;
strategy_id?: string;
created_at: Date;
updated_at: Date;
}
export interface AuditLog {
id: string;
action: string;
entity_type: string;
entity_id: string;
old_values?: Record<string, any>;
new_values?: Record<string, any>;
user_id?: string;
ip_address?: string;
user_agent?: string;
timestamp: Date;
}
/**
* Query Builder Types
*/
export interface WhereCondition {
column: string;
operator:
| '='
| '!='
| '>'
| '<'
| '>='
| '<='
| 'IN'
| 'NOT IN'
| 'LIKE'
| 'ILIKE'
| 'IS NULL'
| 'IS NOT NULL';
value?: any;
}
export interface JoinCondition {
type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
table: string;
on: string;
}
export interface OrderByCondition {
column: string;
direction: 'ASC' | 'DESC';
}

View file

@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true
},
"include": ["src/**/*"],
"references": [
{ "path": "../../core/logger" },
{ "path": "../../core/types" }
]
}