@groundbrick/db-postgres 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +747 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +18 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +177 -0
- package/dist/client.js.map +1 -0
- package/dist/factory.d.ts +19 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +45 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/migrator.d.ts +23 -0
- package/dist/migrator.d.ts.map +1 -0
- package/dist/migrator.js +162 -0
- package/dist/migrator.js.map +1 -0
- package/dist/transaction.d.ts +15 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +60 -0
- package/dist/transaction.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
# @groundbrick/db-postgres
|
|
2
|
+
|
|
3
|
+
Cliente PostgreSQL para o microframework TypeScript com connection pooling, monitoramento de saúde, gerenciamento de transações e sistema de migrações completo.
|
|
4
|
+
|
|
5
|
+
## Features Implementadas
|
|
6
|
+
|
|
7
|
+
- **🔗 Connection Pooling** - Usando `pg` (node-postgres) com configuração avançada
|
|
8
|
+
- **🏭 Singleton Factory** - Gerenciamento centralizado de instâncias de clientes
|
|
9
|
+
- **🔄 Transaction Management** - Transações automáticas e manuais com BaseTransaction
|
|
10
|
+
- **📊 Health Monitoring** - Verificações de saúde com métricas de timing
|
|
11
|
+
- **🗃️ Migration System** - Sistema completo com UP/DOWN scripts e checksum
|
|
12
|
+
- **⚙️ TypeScript Support** - Tipagem completa para queries e configurações
|
|
13
|
+
- **🛡️ Error Handling** - Tipos de erro específicos (ConnectionError, QueryError, etc.)
|
|
14
|
+
- **📈 Pool Monitoring** - Informações detalhadas sobre estado do pool de conexões
|
|
15
|
+
|
|
16
|
+
## Instalação
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @groundbrick/db-postgres
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Peer Dependencies
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install pg
|
|
26
|
+
npm install --save-dev @types/pg
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Configuração Básica
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// src/database.ts
|
|
35
|
+
import { PostgresFactory, PostgresConfig } from '@groundbrick/db-postgres';
|
|
36
|
+
import { createLogger } from '@groundbrick/logger';
|
|
37
|
+
|
|
38
|
+
const config: PostgresConfig = {
|
|
39
|
+
host: process.env.DB_HOST || 'localhost',
|
|
40
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
41
|
+
database: process.env.DB_NAME || 'myapp',
|
|
42
|
+
user: process.env.DB_USER || 'postgres',
|
|
43
|
+
password: process.env.DB_PASSWORD || 'secret',
|
|
44
|
+
|
|
45
|
+
// Pool configuration
|
|
46
|
+
max: 10,
|
|
47
|
+
min: 2,
|
|
48
|
+
connectionTimeoutMillis: 60000,
|
|
49
|
+
idleTimeoutMillis: 30000,
|
|
50
|
+
|
|
51
|
+
// SSL configuration (production)
|
|
52
|
+
ssl: process.env.NODE_ENV === 'production' ? {
|
|
53
|
+
rejectUnauthorized: true
|
|
54
|
+
} : false
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const logger = createLogger({ context: 'database' });
|
|
58
|
+
export const dbClient = PostgresFactory.getInstance(config, logger);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Inicialização e Queries
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// src/server.ts
|
|
65
|
+
import { dbClient } from './database.js';
|
|
66
|
+
|
|
67
|
+
// Inicializar conexão
|
|
68
|
+
await dbClient.initialize();
|
|
69
|
+
|
|
70
|
+
// Query simples
|
|
71
|
+
const users = await dbClient.query<{ id: number; name: string; email: string }>(
|
|
72
|
+
'SELECT id, name, email FROM users WHERE active = $1',
|
|
73
|
+
[true]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(`Found ${users.rowCount} users:`, users.rows);
|
|
77
|
+
|
|
78
|
+
// Cleanup gracioso
|
|
79
|
+
process.on('SIGTERM', async () => {
|
|
80
|
+
await dbClient.close();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Transações
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Transação automática (recomendado)
|
|
89
|
+
const result = await dbClient.transaction(async (trx) => {
|
|
90
|
+
// Log de auditoria
|
|
91
|
+
await trx.query('INSERT INTO audit_logs (action, user_id) VALUES ($1, $2)',
|
|
92
|
+
['USER_CREATED', 123]);
|
|
93
|
+
|
|
94
|
+
// Criar usuário
|
|
95
|
+
const user = await trx.query<{ id: number }>(
|
|
96
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id',
|
|
97
|
+
['João Silva', 'joao@example.com']
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Criar perfil associado
|
|
101
|
+
await trx.query('INSERT INTO user_profiles (user_id, settings) VALUES ($1, $2)',
|
|
102
|
+
[user.rows[0].id, '{}']);
|
|
103
|
+
|
|
104
|
+
return user.rows[0];
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log('Usuário criado:', result);
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// ou manual (não recomendado)
|
|
111
|
+
|
|
112
|
+
const transaction = await client.transaction();
|
|
113
|
+
try {
|
|
114
|
+
await transaction.query('INSERT INTO logs (event) VALUES ($1)', ['MANUAL_TX']);
|
|
115
|
+
await transaction.query('UPDATE users SET active = true WHERE id = $1', [42]);
|
|
116
|
+
await transaction.commit();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
await transaction.rollback();
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### PostgresFactory
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
interface PostgresConfig {
|
|
128
|
+
// Conexão básica
|
|
129
|
+
host: string;
|
|
130
|
+
port: number;
|
|
131
|
+
database: string;
|
|
132
|
+
user: string;
|
|
133
|
+
password: string;
|
|
134
|
+
|
|
135
|
+
// Pool de conexões
|
|
136
|
+
max?: number; // Máximo de conexões (padrão: 10)
|
|
137
|
+
min?: number; // Mínimo de conexões (padrão: 2)
|
|
138
|
+
connectionTimeoutMillis?: number; // Timeout de conexão (padrão: 60000)
|
|
139
|
+
idleTimeoutMillis?: number; // Timeout de idle (padrão: 30000)
|
|
140
|
+
|
|
141
|
+
// SSL (produção)
|
|
142
|
+
ssl?: boolean | {
|
|
143
|
+
rejectUnauthorized?: boolean;
|
|
144
|
+
ca?: string;
|
|
145
|
+
cert?: string;
|
|
146
|
+
key?: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// PostgreSQL específico
|
|
150
|
+
statement_timeout?: number;
|
|
151
|
+
query_timeout?: number;
|
|
152
|
+
application_name?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Singleton Factory
|
|
156
|
+
class PostgresDatabaseFactory {
|
|
157
|
+
static getInstance(): PostgresDatabaseFactory;
|
|
158
|
+
getInstance(config: DatabaseConfig, logger?: Logger): DatabaseClient;
|
|
159
|
+
closeInstance(): Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Instância padrão para conveniência
|
|
163
|
+
const PostgresFactory: PostgresDatabaseFactory;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### PostgresClient
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
class PostgresClient implements DatabaseClient {
|
|
170
|
+
// Ciclo de vida
|
|
171
|
+
initialize(): Promise<void>;
|
|
172
|
+
close(): Promise<void>;
|
|
173
|
+
isReady(): boolean;
|
|
174
|
+
|
|
175
|
+
// Queries
|
|
176
|
+
query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
177
|
+
transaction<T>(callback: (trx: PostgresTransaction) => Promise<T>): Promise<T>;
|
|
178
|
+
|
|
179
|
+
// Monitoramento
|
|
180
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
181
|
+
getConnectionInfo(): { total: number; idle: number; waiting: number };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface QueryResult<T> {
|
|
185
|
+
rows: T[];
|
|
186
|
+
rowCount: number;
|
|
187
|
+
fields: Array<{ name: string; dataTypeID: number }>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface HealthCheckResult {
|
|
191
|
+
status: 'healthy' | 'unhealthy';
|
|
192
|
+
message: string;
|
|
193
|
+
timestamp: Date;
|
|
194
|
+
responseTime: number;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### PostgresTransaction
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
class PostgresTransaction extends BaseTransaction {
|
|
202
|
+
query<T>(sql: string, params?: any[]): Promise<QueryResult<T>>;
|
|
203
|
+
commit(): Promise<void>;
|
|
204
|
+
rollback(): Promise<void>;
|
|
205
|
+
isCompleted(): boolean;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Uso manual (não recomendado)
|
|
209
|
+
const trx = await dbClient.transaction();
|
|
210
|
+
try {
|
|
211
|
+
await trx.query('INSERT INTO logs (event) VALUES ($1)', ['START']);
|
|
212
|
+
await trx.query('UPDATE counters SET value = value + 1');
|
|
213
|
+
await trx.commit();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
await trx.rollback();
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Sistema de Migrações
|
|
221
|
+
|
|
222
|
+
### PostgresMigrator
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { PostgresMigrator } from '@groundbrick/db-postgres';
|
|
226
|
+
|
|
227
|
+
const migrator = new PostgresMigrator(
|
|
228
|
+
dbClient,
|
|
229
|
+
'./migrations',
|
|
230
|
+
logger.child('migrator')
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Aplicar todas as migrações pendentes
|
|
234
|
+
await migrator.migrate();
|
|
235
|
+
|
|
236
|
+
// Rollback das últimas N migrações
|
|
237
|
+
await migrator.rollback(2);
|
|
238
|
+
|
|
239
|
+
// Verificar status
|
|
240
|
+
const status = await migrator.getStatus();
|
|
241
|
+
console.log(`Migrações: ${status.applied}/${status.total} aplicadas`);
|
|
242
|
+
console.log('Pendentes:', status.pending);
|
|
243
|
+
|
|
244
|
+
// Listar migrações aplicadas
|
|
245
|
+
const applied = await migrator.getApplied();
|
|
246
|
+
console.log('Aplicadas:', applied.map(m => `${m.id} - ${m.description}`));
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Estrutura de Arquivos de Migração
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
migrations/
|
|
253
|
+
├── 20241201_001_create_users_table.sql
|
|
254
|
+
├── 20241201_002_add_user_profiles.sql
|
|
255
|
+
├── 20241202_001_add_indexes.sql
|
|
256
|
+
└── 20241203_001_add_audit_logs.sql
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Formato de Arquivo de Migração
|
|
260
|
+
|
|
261
|
+
```sql
|
|
262
|
+
-- Criar tabela de usuários
|
|
263
|
+
CREATE TABLE users (
|
|
264
|
+
id SERIAL PRIMARY KEY,
|
|
265
|
+
name VARCHAR(100) NOT NULL,
|
|
266
|
+
email VARCHAR(100) UNIQUE NOT NULL,
|
|
267
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
268
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
272
|
+
CREATE INDEX idx_users_created_at ON users(created_at);
|
|
273
|
+
|
|
274
|
+
-- DOWN
|
|
275
|
+
DROP INDEX IF EXISTS idx_users_created_at;
|
|
276
|
+
DROP INDEX IF EXISTS idx_users_email;
|
|
277
|
+
DROP TABLE IF EXISTS users;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Regras importantes:**
|
|
281
|
+
- Arquivo deve ter seção `UP` (antes de `-- DOWN`) e seção `DOWN` (após `-- DOWN`)
|
|
282
|
+
- Primeira linha com `--` vira a descrição da migração
|
|
283
|
+
- Arquivos aplicados em ordem lexicográfica
|
|
284
|
+
- Rollback executa a seção `DOWN` em ordem reversa
|
|
285
|
+
|
|
286
|
+
### Exemplos de Migrações
|
|
287
|
+
|
|
288
|
+
```sql
|
|
289
|
+
-- migrations/20241201_001_create_users_table.sql
|
|
290
|
+
-- Criar tabela inicial de usuários
|
|
291
|
+
CREATE TABLE users (
|
|
292
|
+
id SERIAL PRIMARY KEY,
|
|
293
|
+
name VARCHAR(100) NOT NULL,
|
|
294
|
+
email VARCHAR(100) UNIQUE NOT NULL,
|
|
295
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
296
|
+
role VARCHAR(20) DEFAULT 'user',
|
|
297
|
+
active BOOLEAN DEFAULT true,
|
|
298
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
299
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
-- DOWN
|
|
303
|
+
DROP TABLE IF EXISTS users;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```sql
|
|
307
|
+
-- migrations/20241201_002_add_user_profiles.sql
|
|
308
|
+
-- Adicionar tabela de perfis de usuário
|
|
309
|
+
CREATE TABLE user_profiles (
|
|
310
|
+
id SERIAL PRIMARY KEY,
|
|
311
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
312
|
+
first_name VARCHAR(50),
|
|
313
|
+
last_name VARCHAR(50),
|
|
314
|
+
phone VARCHAR(20),
|
|
315
|
+
avatar_url TEXT,
|
|
316
|
+
settings JSONB DEFAULT '{}',
|
|
317
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
318
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
CREATE UNIQUE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
|
|
322
|
+
|
|
323
|
+
-- DOWN
|
|
324
|
+
DROP INDEX IF EXISTS idx_user_profiles_user_id;
|
|
325
|
+
DROP TABLE IF EXISTS user_profiles;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Exemplos Práticos
|
|
329
|
+
|
|
330
|
+
### Configuração com Variáveis de Ambiente
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// src/config/database.ts
|
|
334
|
+
import { PostgresConfig } from '@groundbrick/db-postgres';
|
|
335
|
+
|
|
336
|
+
export const dbConfig: PostgresConfig = {
|
|
337
|
+
host: process.env.DB_HOST || 'localhost',
|
|
338
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
339
|
+
database: process.env.DB_NAME!,
|
|
340
|
+
user: process.env.DB_USER!,
|
|
341
|
+
password: process.env.DB_PASSWORD!,
|
|
342
|
+
|
|
343
|
+
// Pool otimizado para produção
|
|
344
|
+
max: parseInt(process.env.DB_POOL_MAX || '10'),
|
|
345
|
+
min: parseInt(process.env.DB_POOL_MIN || '2'),
|
|
346
|
+
connectionTimeoutMillis: parseInt(process.env.DB_CONNECTION_TIMEOUT || '60000'),
|
|
347
|
+
idleTimeoutMillis: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'),
|
|
348
|
+
|
|
349
|
+
// SSL em produção
|
|
350
|
+
ssl: process.env.NODE_ENV === 'production' ? {
|
|
351
|
+
rejectUnauthorized: process.env.DB_SSL_REJECT_UNAUTHORIZED === 'true',
|
|
352
|
+
ca: process.env.DB_SSL_CA,
|
|
353
|
+
cert: process.env.DB_SSL_CERT,
|
|
354
|
+
key: process.env.DB_SSL_KEY
|
|
355
|
+
} : false,
|
|
356
|
+
|
|
357
|
+
// Configurações PostgreSQL
|
|
358
|
+
application_name: process.env.APP_NAME || 'microframework-app',
|
|
359
|
+
statement_timeout: 30000,
|
|
360
|
+
query_timeout: 10000
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Repository Pattern
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// src/repositories/UserRepository.ts
|
|
368
|
+
import { DatabaseClient, QueryResult } from '@groundbrick/db-postgres';
|
|
369
|
+
|
|
370
|
+
export interface User {
|
|
371
|
+
id: number;
|
|
372
|
+
name: string;
|
|
373
|
+
email: string;
|
|
374
|
+
active: boolean;
|
|
375
|
+
created_at: Date;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export class UserRepository {
|
|
379
|
+
constructor(private db: DatabaseClient) {}
|
|
380
|
+
|
|
381
|
+
async findById(id: number): Promise<User | null> {
|
|
382
|
+
const result = await this.db.query<User>(
|
|
383
|
+
'SELECT * FROM users WHERE id = $1',
|
|
384
|
+
[id]
|
|
385
|
+
);
|
|
386
|
+
return result.rows[0] || null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
390
|
+
const result = await this.db.query<User>(
|
|
391
|
+
'SELECT * FROM users WHERE email = $1',
|
|
392
|
+
[email]
|
|
393
|
+
);
|
|
394
|
+
return result.rows[0] || null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async findPaginated(limit: number, offset: number): Promise<{ users: User[]; total: number }> {
|
|
398
|
+
const [usersResult, countResult] = await Promise.all([
|
|
399
|
+
this.db.query<User>(
|
|
400
|
+
'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
|
|
401
|
+
[limit, offset]
|
|
402
|
+
),
|
|
403
|
+
this.db.query<{ count: string }>(
|
|
404
|
+
'SELECT COUNT(*) as count FROM users'
|
|
405
|
+
)
|
|
406
|
+
]);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
users: usersResult.rows,
|
|
410
|
+
total: parseInt(countResult.rows[0].count)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async create(userData: Omit<User, 'id' | 'created_at'>): Promise<User> {
|
|
415
|
+
const result = await this.db.query<User>(
|
|
416
|
+
`INSERT INTO users (name, email, active)
|
|
417
|
+
VALUES ($1, $2, $3)
|
|
418
|
+
RETURNING *`,
|
|
419
|
+
[userData.name, userData.email, userData.active]
|
|
420
|
+
);
|
|
421
|
+
return result.rows[0];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async update(id: number, userData: Partial<Omit<User, 'id' | 'created_at'>>): Promise<User | null> {
|
|
425
|
+
const setClause = Object.keys(userData)
|
|
426
|
+
.map((key, index) => `${key} = $${index + 2}`)
|
|
427
|
+
.join(', ');
|
|
428
|
+
|
|
429
|
+
const values = [id, ...Object.values(userData)];
|
|
430
|
+
|
|
431
|
+
const result = await this.db.query<User>(
|
|
432
|
+
`UPDATE users SET ${setClause}, updated_at = NOW()
|
|
433
|
+
WHERE id = $1
|
|
434
|
+
RETURNING *`,
|
|
435
|
+
values
|
|
436
|
+
);
|
|
437
|
+
return result.rows[0] || null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async delete(id: number): Promise<boolean> {
|
|
441
|
+
const result = await this.db.query(
|
|
442
|
+
'DELETE FROM users WHERE id = $1',
|
|
443
|
+
[id]
|
|
444
|
+
);
|
|
445
|
+
return (result.rowCount || 0) > 0;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Service com Transações
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
// src/services/UserService.ts
|
|
454
|
+
import { DatabaseClient } from '@groundbrick/db-postgres';
|
|
455
|
+
import { UserRepository } from '../repositories/UserRepository.js';
|
|
456
|
+
|
|
457
|
+
export class UserService {
|
|
458
|
+
constructor(
|
|
459
|
+
private db: DatabaseClient,
|
|
460
|
+
private userRepo: UserRepository
|
|
461
|
+
) {}
|
|
462
|
+
|
|
463
|
+
async createUserWithProfile(userData: {
|
|
464
|
+
name: string;
|
|
465
|
+
email: string;
|
|
466
|
+
firstName: string;
|
|
467
|
+
lastName: string;
|
|
468
|
+
}): Promise<{ user: User; profile: UserProfile }> {
|
|
469
|
+
return this.db.transaction(async (trx) => {
|
|
470
|
+
// Criar usuário
|
|
471
|
+
const user = await trx.query<User>(
|
|
472
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
473
|
+
[userData.name, userData.email]
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Criar perfil
|
|
477
|
+
const profile = await trx.query<UserProfile>(
|
|
478
|
+
`INSERT INTO user_profiles (user_id, first_name, last_name)
|
|
479
|
+
VALUES ($1, $2, $3) RETURNING *`,
|
|
480
|
+
[user.rows[0].id, userData.firstName, userData.lastName]
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// Log de auditoria
|
|
484
|
+
await trx.query(
|
|
485
|
+
'INSERT INTO audit_logs (action, entity_type, entity_id, details) VALUES ($1, $2, $3, $4)',
|
|
486
|
+
['CREATE', 'user', user.rows[0].id, JSON.stringify(userData)]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
user: user.rows[0],
|
|
491
|
+
profile: profile.rows[0]
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Health Check e Monitoring
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// src/health.ts
|
|
502
|
+
import { dbClient } from './database.js';
|
|
503
|
+
|
|
504
|
+
export async function checkDatabaseHealth() {
|
|
505
|
+
const health = await dbClient.healthCheck();
|
|
506
|
+
const connectionInfo = dbClient.getConnectionInfo();
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
database: {
|
|
510
|
+
status: health.status,
|
|
511
|
+
message: health.message,
|
|
512
|
+
responseTime: health.responseTime,
|
|
513
|
+
timestamp: health.timestamp
|
|
514
|
+
},
|
|
515
|
+
connectionPool: {
|
|
516
|
+
total: connectionInfo.total,
|
|
517
|
+
idle: connectionInfo.idle,
|
|
518
|
+
waiting: connectionInfo.waiting,
|
|
519
|
+
utilization: `${Math.round((connectionInfo.total - connectionInfo.idle) / connectionInfo.total * 100)}%`
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/routes/health.ts (Express)
|
|
525
|
+
app.get('/health', async (req, res) => {
|
|
526
|
+
try {
|
|
527
|
+
const health = await checkDatabaseHealth();
|
|
528
|
+
const statusCode = health.database.status === 'healthy' ? 200 : 503;
|
|
529
|
+
res.status(statusCode).json(health);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
res.status(503).json({
|
|
532
|
+
database: { status: 'unhealthy', message: error.message },
|
|
533
|
+
connectionPool: { total: 0, idle: 0, waiting: 0, utilization: '0%' }
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Error Handling
|
|
540
|
+
|
|
541
|
+
### Tipos de Erro Disponíveis
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
import {
|
|
545
|
+
ConnectionError,
|
|
546
|
+
QueryError,
|
|
547
|
+
TransactionError,
|
|
548
|
+
MigrationError
|
|
549
|
+
} from '@groundbrick/db-postgres';
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
await dbClient.query('SELECT * FROM users');
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error instanceof ConnectionError) {
|
|
555
|
+
console.error('Erro de conexão:', error.message);
|
|
556
|
+
// Tentar reconectar ou usar fallback
|
|
557
|
+
} else if (error instanceof QueryError) {
|
|
558
|
+
console.error('Erro na query:', error.message);
|
|
559
|
+
console.error('SQL:', error.sql);
|
|
560
|
+
console.error('Params:', error.params);
|
|
561
|
+
} else if (error instanceof TransactionError) {
|
|
562
|
+
console.error('Erro na transação:', error.message);
|
|
563
|
+
// Transaction já foi automaticamente revertida
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Padrão Singleton e Múltiplos Clientes
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// Múltiplos clientes para diferentes bases
|
|
572
|
+
const mainDbClient = PostgresFactory.getInstance({
|
|
573
|
+
host: 'localhost',
|
|
574
|
+
database: 'main_app',
|
|
575
|
+
user: 'app_user',
|
|
576
|
+
password: 'secret'
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const analyticsDbClient = PostgresFactory.getInstance({
|
|
580
|
+
host: 'analytics-server',
|
|
581
|
+
database: 'analytics',
|
|
582
|
+
user: 'analytics_user',
|
|
583
|
+
password: 'secret'
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// Clients são automaticamente reutilizados se config for idêntica
|
|
587
|
+
const sameClient = PostgresFactory.getInstance({
|
|
588
|
+
host: 'localhost',
|
|
589
|
+
database: 'main_app',
|
|
590
|
+
user: 'app_user',
|
|
591
|
+
password: 'secret'
|
|
592
|
+
}); // Retorna a mesma instância de mainDbClient
|
|
593
|
+
|
|
594
|
+
// Cleanup de todas as conexões
|
|
595
|
+
await PostgresDatabaseFactory.getInstance().closeInstance();
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## Configuração de Ambiente
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
# .env
|
|
602
|
+
# Configuração do Banco
|
|
603
|
+
DB_HOST=localhost
|
|
604
|
+
DB_PORT=5432
|
|
605
|
+
DB_NAME=microframework_app
|
|
606
|
+
DB_USER=postgres
|
|
607
|
+
DB_PASSWORD=your_password
|
|
608
|
+
|
|
609
|
+
# SSL (produção)
|
|
610
|
+
DB_SSL=true
|
|
611
|
+
DB_SSL_REJECT_UNAUTHORIZED=true
|
|
612
|
+
DB_SSL_CA=/path/to/ca.pem
|
|
613
|
+
DB_SSL_CERT=/path/to/cert.pem
|
|
614
|
+
DB_SSL_KEY=/path/to/key.pem
|
|
615
|
+
|
|
616
|
+
# Pool de Conexões
|
|
617
|
+
DB_POOL_MAX=10
|
|
618
|
+
DB_POOL_MIN=2
|
|
619
|
+
DB_CONNECTION_TIMEOUT=60000
|
|
620
|
+
DB_IDLE_TIMEOUT=30000
|
|
621
|
+
|
|
622
|
+
# Configuração da Aplicação
|
|
623
|
+
APP_NAME=microframework-app
|
|
624
|
+
NODE_ENV=production
|
|
625
|
+
LOG_LEVEL=info
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## Migration CLI Script
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// scripts/migrate.ts
|
|
632
|
+
import { dbClient } from '../src/database.js';
|
|
633
|
+
import { PostgresMigrator } from '@groundbrick/db-postgres';
|
|
634
|
+
import { createLogger } from '@groundbrick/logger';
|
|
635
|
+
|
|
636
|
+
const logger = createLogger({ context: 'migration-cli' });
|
|
637
|
+
const migrator = new PostgresMigrator(dbClient, './migrations', logger);
|
|
638
|
+
|
|
639
|
+
async function main() {
|
|
640
|
+
const command = process.argv[2];
|
|
641
|
+
|
|
642
|
+
await dbClient.initialize();
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
switch (command) {
|
|
646
|
+
case 'up':
|
|
647
|
+
await migrator.migrate();
|
|
648
|
+
break;
|
|
649
|
+
case 'down':
|
|
650
|
+
const steps = parseInt(process.argv[3]) || 1;
|
|
651
|
+
await migrator.rollback(steps);
|
|
652
|
+
break;
|
|
653
|
+
case 'status':
|
|
654
|
+
const status = await migrator.getStatus();
|
|
655
|
+
console.log(`Migrations: ${status.applied}/${status.total}`);
|
|
656
|
+
if (status.pending.length > 0) {
|
|
657
|
+
console.log('Pending:', status.pending);
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
console.log('Usage: npm run migrate [up|down|status] [steps]');
|
|
662
|
+
}
|
|
663
|
+
} finally {
|
|
664
|
+
await dbClient.close();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
main().catch(console.error);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
```json
|
|
672
|
+
// package.json scripts
|
|
673
|
+
{
|
|
674
|
+
"scripts": {
|
|
675
|
+
"migrate": "tsx scripts/migrate.ts",
|
|
676
|
+
"migrate:up": "npm run migrate up",
|
|
677
|
+
"migrate:down": "npm run migrate down",
|
|
678
|
+
"migrate:status": "npm run migrate status"
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
## Integração com Express Adapter
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// src/server.ts - Integração com @groundbrick/express-adapter
|
|
687
|
+
import { ExpressApp } from '@groundbrick/express-adapter';
|
|
688
|
+
import { dbClient } from './database.js';
|
|
689
|
+
|
|
690
|
+
const app = new ExpressApp({
|
|
691
|
+
port: 3000,
|
|
692
|
+
cors: { origin: true }
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Middleware de database (personalizado)
|
|
696
|
+
app.getApp().use((req, res, next) => {
|
|
697
|
+
req.db = dbClient;
|
|
698
|
+
next();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Health check endpoint
|
|
702
|
+
app.addRoute({
|
|
703
|
+
method: 'get',
|
|
704
|
+
path: '/health',
|
|
705
|
+
handler: async (req, res) => {
|
|
706
|
+
const health = await checkDatabaseHealth();
|
|
707
|
+
const statusCode = health.database.status === 'healthy' ? 200 : 503;
|
|
708
|
+
res.status(statusCode).json(health);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Aplicar migrações na inicialização
|
|
713
|
+
const migrator = new PostgresMigrator(dbClient, './migrations', logger);
|
|
714
|
+
await dbClient.initialize();
|
|
715
|
+
await migrator.migrate();
|
|
716
|
+
|
|
717
|
+
await app.start();
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## Melhores Práticas
|
|
721
|
+
|
|
722
|
+
1. **Use o padrão Singleton** para evitar múltiplas conexões desnecessárias
|
|
723
|
+
2. **Sempre inicialize** o cliente antes de usar (`await dbClient.initialize()`)
|
|
724
|
+
3. **Use transações** para operações que modificam múltiplas tabelas
|
|
725
|
+
4. **Implemente health checks** para monitoramento de produção
|
|
726
|
+
5. **Configure SSL** adequadamente em produção
|
|
727
|
+
6. **Use Repository pattern** para organizar queries
|
|
728
|
+
7. **Monitore o pool** de conexões em produção
|
|
729
|
+
8. **Teste migrações** em ambiente de desenvolvimento primeiro
|
|
730
|
+
9. **Implemente error handling** específico para cada tipo de erro
|
|
731
|
+
10. **Use TypeScript** para tipagem de queries e configurações
|
|
732
|
+
|
|
733
|
+
## Dependencies
|
|
734
|
+
|
|
735
|
+
### Required
|
|
736
|
+
- `@groundbrick/logger` - Sistema de logging
|
|
737
|
+
- `@groundbrick/db-core` - Abstrações de banco de dados
|
|
738
|
+
|
|
739
|
+
### Peer Dependencies
|
|
740
|
+
- `pg` - Driver PostgreSQL para Node.js (^8.11.0)
|
|
741
|
+
|
|
742
|
+
### Dev Dependencies
|
|
743
|
+
- `@types/pg` - Tipos TypeScript para pg
|
|
744
|
+
|
|
745
|
+
## License
|
|
746
|
+
|
|
747
|
+
MIT
|