@200systems/mf-db-postgres
Cliente PostgreSQL para o microframework TypeScript com connection pooling, monitoramento de saúde, gerenciamento de transações e sistema de migrações completo.
Features Implementadas
- š Connection Pooling - Usando
pg (node-postgres) com configuração avançada
- š Singleton Factory - Gerenciamento centralizado de instĆ¢ncias de clientes
- š Transaction Management - TransaƧƵes automĆ”ticas e manuais com BaseTransaction
- š Health Monitoring - VerificaƧƵes de saĆŗde com mĆ©tricas de timing
- šļø Migration System - Sistema completo com UP/DOWN scripts e checksum
- āļø TypeScript Support - Tipagem completa para queries e configuraƧƵes
- š”ļø Error Handling - Tipos de erro especĆficos (ConnectionError, QueryError, etc.)
- š Pool Monitoring - InformaƧƵes detalhadas sobre estado do pool de conexƵes
Instalação
npm install @200systems/mf-db-postgres
Peer Dependencies
npm install pg
npm install --save-dev @types/pg
Quick Start
1. Configuração BÔsica
import { PostgresFactory, PostgresConfig } from '@200systems/mf-db-postgres';
import { createLogger } from '@200systems/mf-logger';
const config: PostgresConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'secret',
max: 10,
min: 2,
connectionTimeoutMillis: 60000,
idleTimeoutMillis: 30000,
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: true
} : false
};
const logger = createLogger({ context: 'database' });
export const dbClient = PostgresFactory.getInstance(config, logger);
2. Inicialização e Queries
import { dbClient } from './database.js';
await dbClient.initialize();
const users = await dbClient.query<{ id: number; name: string; email: string }>(
'SELECT id, name, email FROM users WHERE active = $1',
[true]
);
console.log(`Found ${users.rowCount} users:`, users.rows);
process.on('SIGTERM', async () => {
await dbClient.close();
process.exit(0);
});
3. TransaƧƵes
const result = await dbClient.transaction(async (trx) => {
await trx.query('INSERT INTO audit_logs (action, user_id) VALUES ($1, $2)',
['USER_CREATED', 123]);
const user = await trx.query<{ id: number }>(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id',
['João Silva', 'joao@example.com']
);
await trx.query('INSERT INTO user_profiles (user_id, settings) VALUES ($1, $2)',
[user.rows[0].id, '{}']);
return user.rows[0];
});
console.log('UsuƔrio criado:', result);
const transaction = await client.transaction();
try {
await transaction.query('INSERT INTO logs (event) VALUES ($1)', ['MANUAL_TX']);
await transaction.query('UPDATE users SET active = true WHERE id = $1', [42]);
await transaction.commit();
} catch (error) {
await transaction.rollback();
}
API Reference
PostgresFactory
interface PostgresConfig {
host: string;
port: number;
database: string;
user: string;
password: string;
max?: number;
min?: number;
connectionTimeoutMillis?: number;
idleTimeoutMillis?: number;
ssl?: boolean | {
rejectUnauthorized?: boolean;
ca?: string;
cert?: string;
key?: string;
};
statement_timeout?: number;
query_timeout?: number;
application_name?: string;
}
class PostgresDatabaseFactory {
static getInstance(): PostgresDatabaseFactory;
getInstance(config: DatabaseConfig, logger?: Logger): DatabaseClient;
closeInstance(): Promise<void>;
}
const PostgresFactory: PostgresDatabaseFactory;
PostgresClient
class PostgresClient implements DatabaseClient {
initialize(): Promise<void>;
close(): Promise<void>;
isReady(): boolean;
query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>;
transaction<T>(callback: (trx: PostgresTransaction) => Promise<T>): Promise<T>;
healthCheck(): Promise<HealthCheckResult>;
getConnectionInfo(): { total: number; idle: number; waiting: number };
}
interface QueryResult<T> {
rows: T[];
rowCount: number;
fields: Array<{ name: string; dataTypeID: number }>;
}
interface HealthCheckResult {
status: 'healthy' | 'unhealthy';
message: string;
timestamp: Date;
responseTime: number;
}
PostgresTransaction
class PostgresTransaction extends BaseTransaction {
query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>;
commit(): Promise<void>;
rollback(): Promise<void>;
isCompleted(): boolean;
}
const trx = await dbClient.transaction();
try {
await trx.query('INSERT INTO logs (event) VALUES ($1)', ['START']);
await trx.query('UPDATE counters SET value = value + 1');
await trx.commit();
} catch (error) {
await trx.rollback();
throw error;
}
Sistema de MigraƧƵes
PostgresMigrator
import { PostgresMigrator } from '@200systems/mf-db-postgres';
const migrator = new PostgresMigrator(
dbClient,
'./migrations',
logger.child('migrator')
);
await migrator.migrate();
await migrator.rollback(2);
const status = await migrator.getStatus();
console.log(`MigraƧƵes: ${status.applied}/${status.total} aplicadas`);
console.log('Pendentes:', status.pending);
const applied = await migrator.getApplied();
console.log('Aplicadas:', applied.map(m => `${m.id} - ${m.description}`));
Estrutura de Arquivos de Migração
migrations/
āāā 20241201_001_create_users_table.sql
āāā 20241201_002_add_user_profiles.sql
āāā 20241202_001_add_indexes.sql
āāā 20241203_001_add_audit_logs.sql
Formato de Arquivo de Migração
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);
DROP INDEX IF EXISTS idx_users_created_at;
DROP INDEX IF EXISTS idx_users_email;
DROP TABLE IF EXISTS users;
Regras importantes:
- Arquivo deve ter seção
UP (antes de -- DOWN) e seção DOWN (após -- DOWN)
- Primeira linha com
-- vira a descrição da migração
- Arquivos aplicados em ordem lexicogrƔfica
- Rollback executa a seção
DOWN em ordem reversa
Exemplos de MigraƧƵes
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'user',
active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DROP TABLE IF EXISTS users;
CREATE TABLE user_profiles (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
first_name VARCHAR(50),
last_name VARCHAR(50),
phone VARCHAR(20),
avatar_url TEXT,
settings JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
DROP INDEX IF EXISTS idx_user_profiles_user_id;
DROP TABLE IF EXISTS user_profiles;
Exemplos PrƔticos
Configuração com VariÔveis de Ambiente
import { PostgresConfig } from '@200systems/mf-db-postgres';
export const dbConfig: PostgresConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
max: parseInt(process.env.DB_POOL_MAX || '10'),
min: parseInt(process.env.DB_POOL_MIN || '2'),
connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '60000'),
idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'),
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: process.env.DB_SSL_REJECT_UNAUTHORIZED === 'true',
ca: process.env.DB_SSL_CA,
cert: process.env.DB_SSL_CERT,
key: process.env.DB_SSL_KEY
} : false,
application_name: process.env.APP_NAME || 'microframework-app',
statement_timeout: 30000,
query_timeout: 10000
};
Repository Pattern
import { DatabaseClient, QueryResult } from '@200systems/mf-db-postgres';
export interface User {
id: number;
name: string;
email: string;
active: boolean;
created_at: Date;
}
export class UserRepository {
constructor(private db: DatabaseClient) {}
async findById(id: number): Promise<User | null> {
const result = await this.db.query<User>(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query<User>(
'SELECT * FROM users WHERE email = $1',
[email]
);
return result.rows[0] || null;
}
async findPaginated(limit: number, offset: number): Promise<{ users: User[]; total: number }> {
const [usersResult, countResult] = await Promise.all([
this.db.query<User>(
'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
[limit, offset]
),
this.db.query<{ count: string }>(
'SELECT COUNT(*) as count FROM users'
)
]);
return {
users: usersResult.rows,
total: parseInt(countResult.rows[0].count)
};
}
async create(userData: Omit<User, 'id' | 'created_at'>): Promise<User> {
const result = await this.db.query<User>(
`INSERT INTO users (name, email, active)
VALUES ($1, $2, $3)
RETURNING *`,
[userData.name, userData.email, userData.active]
);
return result.rows[0];
}
async update(id: number, userData: Partial<Omit<User, 'id' | 'created_at'>>): Promise<User | null> {
const setClause = Object.keys(userData)
.map((key, index) => `${key} = $${index + 2}`)
.join(', ');
const values = [id, ...Object.values(userData)];
const result = await this.db.query<User>(
`UPDATE users SET ${setClause}, updated_at = NOW()
WHERE id = $1
RETURNING *`,
values
);
return result.rows[0] || null;
}
async delete(id: number): Promise<boolean> {
const result = await this.db.query(
'DELETE FROM users WHERE id = $1',
[id]
);
return (result.rowCount || 0) > 0;
}
}
Service com TransaƧƵes
import { DatabaseClient } from '@200systems/mf-db-postgres';
import { UserRepository } from '../repositories/UserRepository.js';
export class UserService {
constructor(
private db: DatabaseClient,
private userRepo: UserRepository
) {}
async createUserWithProfile(userData: {
name: string;
email: string;
firstName: string;
lastName: string;
}): Promise<{ user: User; profile: UserProfile }> {
return this.db.transaction(async (trx) => {
const user = await trx.query<User>(
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
[userData.name, userData.email]
);
const profile = await trx.query<UserProfile>(
`INSERT INTO user_profiles (user_id, first_name, last_name)
VALUES ($1, $2, $3) RETURNING *`,
[user.rows[0].id, userData.firstName, userData.lastName]
);
await trx.query(
'INSERT INTO audit_logs (action, entity_type, entity_id, details) VALUES ($1, $2, $3, $4)',
['CREATE', 'user', user.rows[0].id, JSON.stringify(userData)]
);
return {
user: user.rows[0],
profile: profile.rows[0]
};
});
}
}
Health Check e Monitoring
import { dbClient } from './database.js';
export async function checkDatabaseHealth() {
const health = await dbClient.healthCheck();
const connectionInfo = dbClient.getConnectionInfo();
return {
database: {
status: health.status,
message: health.message,
responseTime: health.responseTime,
timestamp: health.timestamp
},
connectionPool: {
total: connectionInfo.total,
idle: connectionInfo.idle,
waiting: connectionInfo.waiting,
utilization: `${Math.round((connectionInfo.total - connectionInfo.idle) / connectionInfo.total * 100)}%`
}
};
}
app.get('/health', async (req, res) => {
try {
const health = await checkDatabaseHealth();
const statusCode = health.database.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
} catch (error) {
res.status(503).json({
database: { status: 'unhealthy', message: error.message },
connectionPool: { total: 0, idle: 0, waiting: 0, utilization: '0%' }
});
}
});
Error Handling
Tipos de Erro DisponĆveis
import {
ConnectionError,
QueryError,
TransactionError,
MigrationError
} from '@200systems/mf-db-postgres';
try {
await dbClient.query('SELECT * FROM users');
} catch (error) {
if (error instanceof ConnectionError) {
console.error('Erro de conexão:', error.message);
} else if (error instanceof QueryError) {
console.error('Erro na query:', error.message);
console.error('SQL:', error.sql);
console.error('Params:', error.params);
} else if (error instanceof TransactionError) {
console.error('Erro na transação:', error.message);
}
}
Padrão Singleton e Múltiplos Clientes
const mainDbClient = PostgresFactory.getInstance({
host: 'localhost',
database: 'main_app',
user: 'app_user',
password: 'secret'
});
const analyticsDbClient = PostgresFactory.getInstance({
host: 'analytics-server',
database: 'analytics',
user: 'analytics_user',
password: 'secret'
});
const sameClient = PostgresFactory.getInstance({
host: 'localhost',
database: 'main_app',
user: 'app_user',
password: 'secret'
});
await PostgresDatabaseFactory.getInstance().closeInstance();
Configuração de Ambiente
DB_HOST=localhost
DB_PORT=5432
DB_NAME=microframework_app
DB_USER=postgres
DB_PASSWORD=your_password
DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
DB_SSL_CA=/path/to/ca.pem
DB_SSL_CERT=/path/to/cert.pem
DB_SSL_KEY=/path/to/key.pem
DB_POOL_MAX=10
DB_POOL_MIN=2
DB_CONNECTION_TIMEOUT=60000
DB_IDLE_TIMEOUT=30000
APP_NAME=microframework-app
NODE_ENV=production
LOG_LEVEL=info
Migration CLI Script
import { dbClient } from '../src/database.js';
import { PostgresMigrator } from '@200systems/mf-db-postgres';
import { createLogger } from '@200systems/mf-logger';
const logger = createLogger({ context: 'migration-cli' });
const migrator = new PostgresMigrator(dbClient, './migrations', logger);
async function main() {
const command = process.argv[2];
await dbClient.initialize();
try {
switch (command) {
case 'up':
await migrator.migrate();
break;
case 'down':
const steps = parseInt(process.argv[3]) || 1;
await migrator.rollback(steps);
break;
case 'status':
const status = await migrator.getStatus();
console.log(`Migrations: ${status.applied}/${status.total}`);
if (status.pending.length > 0) {
console.log('Pending:', status.pending);
}
break;
default:
console.log('Usage: npm run migrate [up|down|status] [steps]');
}
} finally {
await dbClient.close();
}
}
main().catch(console.error);
{
"scripts": {
"migrate": "tsx scripts/migrate.ts",
"migrate:up": "npm run migrate up",
"migrate:down": "npm run migrate down",
"migrate:status": "npm run migrate status"
}
}
Integração com Express Adapter
import { ExpressApp } from '@200systems/mf-express-adapter';
import { dbClient } from './database.js';
const app = new ExpressApp({
port: 3000,
cors: { origin: true }
});
app.getApp().use((req, res, next) => {
req.db = dbClient;
next();
});
app.addRoute({
method: 'get',
path: '/health',
handler: async (req, res) => {
const health = await checkDatabaseHealth();
const statusCode = health.database.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
}
});
const migrator = new PostgresMigrator(dbClient, './migrations', logger);
await dbClient.initialize();
await migrator.migrate();
await app.start();
Melhores PrƔticas
- Use o padrão Singleton para evitar múltiplas conexões desnecessÔrias
- Sempre inicialize o cliente antes de usar (
await dbClient.initialize())
- Use transações para operações que modificam múltiplas tabelas
- Implemente health checks para monitoramento de produção
- Configure SSL adequadamente em produção
- Use Repository pattern para organizar queries
- Monitore o pool de conexões em produção
- Teste migraƧƵes em ambiente de desenvolvimento primeiro
- Implemente error handling especĆfico para cada tipo de erro
- Use TypeScript para tipagem de queries e configuraƧƵes
Dependencies
Required
@200systems/mf-logger - Sistema de logging
@200systems/mf-db-core - AbstraƧƵes de banco de dados
Peer Dependencies
pg - Driver PostgreSQL para Node.js (^8.11.0)
Dev Dependencies
@types/pg - Tipos TypeScript para pg
License
MIT