@qwickapps/server 1.1.6 → 1.1.9
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 +1 -1
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +5 -8
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +5 -0
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +390 -28
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +3 -9
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +2 -6
- package/dist/core/logging.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache-plugin.d.ts +219 -0
- package/dist/plugins/cache-plugin.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.js +326 -0
- package/dist/plugins/cache-plugin.js.map +1 -0
- package/dist/plugins/cache-plugin.test.d.ts +8 -0
- package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.test.js +188 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -0
- package/dist/plugins/config-plugin.js +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +1 -1
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +1 -3
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +155 -0
- package/dist/plugins/postgres-plugin.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.js +244 -0
- package/dist/plugins/postgres-plugin.js.map +1 -0
- package/dist/plugins/postgres-plugin.test.d.ts +8 -0
- package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.test.js +165 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -0
- package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
- package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +13 -2
- package/src/core/control-panel.ts +5 -8
- package/src/core/gateway.ts +412 -30
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +2 -6
- package/src/index.ts +22 -0
- package/src/plugins/cache-plugin.test.ts +241 -0
- package/src/plugins/cache-plugin.ts +503 -0
- package/src/plugins/config-plugin.ts +1 -1
- package/src/plugins/diagnostics-plugin.ts +1 -1
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +10 -0
- package/src/plugins/logs-plugin.ts +1 -3
- package/src/plugins/postgres-plugin.test.ts +213 -0
- package/src/plugins/postgres-plugin.ts +345 -0
- package/ui/src/api/controlPanelApi.ts +1 -1
- package/ui/src/pages/LogsPage.tsx +6 -10
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Plugin Tests
|
|
3
|
+
*
|
|
4
|
+
* Note: These tests use mocks since we don't want to require a real database.
|
|
5
|
+
* Integration tests should be run separately with a real PostgreSQL instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
// Mock pg before importing the plugin
|
|
11
|
+
vi.mock('pg', () => {
|
|
12
|
+
const mockClient = {
|
|
13
|
+
query: vi.fn().mockResolvedValue({ rows: [] }),
|
|
14
|
+
release: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mockPool = {
|
|
18
|
+
connect: vi.fn().mockResolvedValue(mockClient),
|
|
19
|
+
query: vi.fn().mockResolvedValue({ rows: [] }),
|
|
20
|
+
end: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
on: vi.fn(),
|
|
22
|
+
totalCount: 5,
|
|
23
|
+
idleCount: 3,
|
|
24
|
+
waitingCount: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
default: {
|
|
29
|
+
Pool: vi.fn(() => mockPool),
|
|
30
|
+
},
|
|
31
|
+
Pool: vi.fn(() => mockPool),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
createPostgresPlugin,
|
|
37
|
+
getPostgres,
|
|
38
|
+
hasPostgres,
|
|
39
|
+
type PostgresPluginConfig,
|
|
40
|
+
} from './postgres-plugin.js';
|
|
41
|
+
|
|
42
|
+
describe('PostgreSQL Plugin', () => {
|
|
43
|
+
const mockConfig: PostgresPluginConfig = {
|
|
44
|
+
url: 'postgresql://test:test@localhost:5432/testdb',
|
|
45
|
+
maxConnections: 10,
|
|
46
|
+
healthCheck: false, // Disable for unit tests
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const mockContext = {
|
|
50
|
+
config: { productName: 'Test', port: 3000 },
|
|
51
|
+
app: {} as any,
|
|
52
|
+
router: {} as any,
|
|
53
|
+
logger: {
|
|
54
|
+
debug: vi.fn(),
|
|
55
|
+
info: vi.fn(),
|
|
56
|
+
warn: vi.fn(),
|
|
57
|
+
error: vi.fn(),
|
|
58
|
+
},
|
|
59
|
+
registerHealthCheck: vi.fn(),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(async () => {
|
|
67
|
+
// Clean up any registered instances
|
|
68
|
+
if (hasPostgres('test')) {
|
|
69
|
+
const db = getPostgres('test');
|
|
70
|
+
await db.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('createPostgresPlugin', () => {
|
|
75
|
+
it('should create a plugin with correct name', () => {
|
|
76
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
77
|
+
expect(plugin.name).toBe('postgres:test');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use "default" as instance name when not specified', () => {
|
|
81
|
+
const plugin = createPostgresPlugin(mockConfig);
|
|
82
|
+
expect(plugin.name).toBe('postgres:default');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should have low order number (initialize early)', () => {
|
|
86
|
+
const plugin = createPostgresPlugin(mockConfig);
|
|
87
|
+
expect(plugin.order).toBeLessThan(10);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('onInit', () => {
|
|
92
|
+
it('should register the postgres instance', async () => {
|
|
93
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
94
|
+
await plugin.onInit?.(mockContext as any);
|
|
95
|
+
|
|
96
|
+
expect(hasPostgres('test')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should log debug message on successful connection', async () => {
|
|
100
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
101
|
+
await plugin.onInit?.(mockContext as any);
|
|
102
|
+
|
|
103
|
+
expect(mockContext.logger.debug).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining('connected')
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should register health check when enabled', async () => {
|
|
109
|
+
const configWithHealth = { ...mockConfig, healthCheck: true };
|
|
110
|
+
const plugin = createPostgresPlugin(configWithHealth, 'test');
|
|
111
|
+
await plugin.onInit?.(mockContext as any);
|
|
112
|
+
|
|
113
|
+
expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({
|
|
115
|
+
name: 'postgres',
|
|
116
|
+
type: 'custom',
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should use custom health check name when provided', async () => {
|
|
122
|
+
const configWithCustomName = {
|
|
123
|
+
...mockConfig,
|
|
124
|
+
healthCheck: true,
|
|
125
|
+
healthCheckName: 'custom-db',
|
|
126
|
+
};
|
|
127
|
+
const plugin = createPostgresPlugin(configWithCustomName, 'test');
|
|
128
|
+
await plugin.onInit?.(mockContext as any);
|
|
129
|
+
|
|
130
|
+
expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
name: 'custom-db',
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('getPostgres', () => {
|
|
139
|
+
it('should return registered instance', async () => {
|
|
140
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
141
|
+
await plugin.onInit?.(mockContext as any);
|
|
142
|
+
|
|
143
|
+
const db = getPostgres('test');
|
|
144
|
+
expect(db).toBeDefined();
|
|
145
|
+
expect(db.query).toBeDefined();
|
|
146
|
+
expect(db.queryOne).toBeDefined();
|
|
147
|
+
expect(db.transaction).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should throw error for unregistered instance', () => {
|
|
151
|
+
expect(() => getPostgres('nonexistent')).toThrow(
|
|
152
|
+
'PostgreSQL instance "nonexistent" not found'
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('hasPostgres', () => {
|
|
158
|
+
it('should return false for unregistered instance', () => {
|
|
159
|
+
expect(hasPostgres('nonexistent')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return true for registered instance', async () => {
|
|
163
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
164
|
+
await plugin.onInit?.(mockContext as any);
|
|
165
|
+
|
|
166
|
+
expect(hasPostgres('test')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('PostgresInstance', () => {
|
|
171
|
+
it('should execute query and return rows', async () => {
|
|
172
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
173
|
+
await plugin.onInit?.(mockContext as any);
|
|
174
|
+
|
|
175
|
+
const db = getPostgres('test');
|
|
176
|
+
const result = await db.query('SELECT 1');
|
|
177
|
+
expect(result).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return null from queryOne when no rows', async () => {
|
|
181
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
182
|
+
await plugin.onInit?.(mockContext as any);
|
|
183
|
+
|
|
184
|
+
const db = getPostgres('test');
|
|
185
|
+
const result = await db.queryOne('SELECT 1');
|
|
186
|
+
expect(result).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return pool stats', async () => {
|
|
190
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
191
|
+
await plugin.onInit?.(mockContext as any);
|
|
192
|
+
|
|
193
|
+
const db = getPostgres('test');
|
|
194
|
+
const stats = db.getStats();
|
|
195
|
+
expect(stats).toHaveProperty('total');
|
|
196
|
+
expect(stats).toHaveProperty('idle');
|
|
197
|
+
expect(stats).toHaveProperty('waiting');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('onShutdown', () => {
|
|
202
|
+
it('should close pool and unregister instance', async () => {
|
|
203
|
+
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
204
|
+
await plugin.onInit?.(mockContext as any);
|
|
205
|
+
|
|
206
|
+
expect(hasPostgres('test')).toBe(true);
|
|
207
|
+
|
|
208
|
+
await plugin.onShutdown?.();
|
|
209
|
+
|
|
210
|
+
expect(hasPostgres('test')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides PostgreSQL database connection pooling and health checks.
|
|
5
|
+
* Wraps the 'pg' library with a simple, reusable interface.
|
|
6
|
+
*
|
|
7
|
+
* ## Features
|
|
8
|
+
* - Connection pooling with configurable limits
|
|
9
|
+
* - Automatic health checks with pool stats
|
|
10
|
+
* - Transaction helpers
|
|
11
|
+
* - Multiple named instances support
|
|
12
|
+
* - Graceful shutdown
|
|
13
|
+
*
|
|
14
|
+
* ## Usage
|
|
15
|
+
*
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { createGateway, createPostgresPlugin, getPostgres } from '@qwickapps/server';
|
|
18
|
+
*
|
|
19
|
+
* const gateway = createGateway({
|
|
20
|
+
* // ... config
|
|
21
|
+
* plugins: [
|
|
22
|
+
* createPostgresPlugin({
|
|
23
|
+
* url: process.env.DATABASE_URL,
|
|
24
|
+
* maxConnections: 20,
|
|
25
|
+
* }),
|
|
26
|
+
* ],
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // In your service code:
|
|
30
|
+
* const db = getPostgres();
|
|
31
|
+
* const users = await db.query<User>('SELECT * FROM users WHERE active = $1', [true]);
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ## Multiple Databases
|
|
35
|
+
*
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Register multiple databases with different names
|
|
38
|
+
* createPostgresPlugin({ url: primaryUrl }, 'primary');
|
|
39
|
+
* createPostgresPlugin({ url: replicaUrl }, 'replica');
|
|
40
|
+
*
|
|
41
|
+
* // Access by name
|
|
42
|
+
* const primary = getPostgres('primary');
|
|
43
|
+
* const replica = getPostgres('replica');
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import pg from 'pg';
|
|
50
|
+
import type { ControlPanelPlugin, PluginContext } from '../core/types.js';
|
|
51
|
+
|
|
52
|
+
const { Pool } = pg;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for the PostgreSQL plugin
|
|
56
|
+
*/
|
|
57
|
+
export interface PostgresPluginConfig {
|
|
58
|
+
/** Database connection URL (e.g., postgresql://user:pass@host:5432/db) */
|
|
59
|
+
url: string;
|
|
60
|
+
|
|
61
|
+
/** Maximum number of clients in the pool (default: 20) */
|
|
62
|
+
maxConnections?: number;
|
|
63
|
+
|
|
64
|
+
/** Minimum number of clients in the pool (default: 2) */
|
|
65
|
+
minConnections?: number;
|
|
66
|
+
|
|
67
|
+
/** Idle timeout in milliseconds - close idle clients after this time (default: 30000) */
|
|
68
|
+
idleTimeoutMs?: number;
|
|
69
|
+
|
|
70
|
+
/** Connection timeout in milliseconds - fail if can't connect within this time (default: 5000) */
|
|
71
|
+
connectionTimeoutMs?: number;
|
|
72
|
+
|
|
73
|
+
/** Statement timeout in milliseconds - cancel queries taking longer (default: none) */
|
|
74
|
+
statementTimeoutMs?: number;
|
|
75
|
+
|
|
76
|
+
/** Register a health check for this database (default: true) */
|
|
77
|
+
healthCheck?: boolean;
|
|
78
|
+
|
|
79
|
+
/** Name for the health check (default: 'postgres') */
|
|
80
|
+
healthCheckName?: string;
|
|
81
|
+
|
|
82
|
+
/** Health check interval in milliseconds (default: 30000) */
|
|
83
|
+
healthCheckInterval?: number;
|
|
84
|
+
|
|
85
|
+
/** Called when a client connects (for setup like setting search_path) */
|
|
86
|
+
onConnect?: (client: pg.PoolClient) => Promise<void>;
|
|
87
|
+
|
|
88
|
+
/** Called on pool errors */
|
|
89
|
+
onError?: (error: Error) => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Transaction callback function
|
|
94
|
+
*/
|
|
95
|
+
export type TransactionCallback<T> = (client: pg.PoolClient) => Promise<T>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* PostgreSQL instance returned by the plugin
|
|
99
|
+
*/
|
|
100
|
+
export interface PostgresInstance {
|
|
101
|
+
/** Get a client from the pool (remember to release it!) */
|
|
102
|
+
getClient(): Promise<pg.PoolClient>;
|
|
103
|
+
|
|
104
|
+
/** Execute a query and return rows */
|
|
105
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
106
|
+
|
|
107
|
+
/** Execute a query and return first row or null */
|
|
108
|
+
queryOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
|
|
109
|
+
|
|
110
|
+
/** Execute a query and return the full result (includes rowCount, etc.) */
|
|
111
|
+
queryRaw(sql: string, params?: unknown[]): Promise<pg.QueryResult>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Execute multiple queries in a transaction
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const result = await db.transaction(async (client) => {
|
|
119
|
+
* await client.query('INSERT INTO users (name) VALUES ($1)', ['Alice']);
|
|
120
|
+
* await client.query('INSERT INTO audit_log (action) VALUES ($1)', ['user_created']);
|
|
121
|
+
* return { success: true };
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
transaction<T>(callback: TransactionCallback<T>): Promise<T>;
|
|
126
|
+
|
|
127
|
+
/** Get the underlying pool (for advanced use cases) */
|
|
128
|
+
getPool(): pg.Pool;
|
|
129
|
+
|
|
130
|
+
/** Get pool statistics */
|
|
131
|
+
getStats(): { total: number; idle: number; waiting: number };
|
|
132
|
+
|
|
133
|
+
/** Close all connections */
|
|
134
|
+
close(): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Global registry of PostgreSQL instances by name
|
|
138
|
+
const instances = new Map<string, PostgresInstance>();
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get a PostgreSQL instance by name
|
|
142
|
+
*
|
|
143
|
+
* @param name - Instance name (default: 'default')
|
|
144
|
+
* @returns The PostgreSQL instance
|
|
145
|
+
* @throws Error if the instance is not registered
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* const db = getPostgres();
|
|
150
|
+
* const users = await db.query<User>('SELECT * FROM users');
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function getPostgres(name = 'default'): PostgresInstance {
|
|
154
|
+
const instance = instances.get(name);
|
|
155
|
+
if (!instance) {
|
|
156
|
+
throw new Error(`PostgreSQL instance "${name}" not found. Did you register the postgres plugin?`);
|
|
157
|
+
}
|
|
158
|
+
return instance;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if a PostgreSQL instance is registered
|
|
163
|
+
*
|
|
164
|
+
* @param name - Instance name (default: 'default')
|
|
165
|
+
* @returns true if the instance exists
|
|
166
|
+
*/
|
|
167
|
+
export function hasPostgres(name = 'default'): boolean {
|
|
168
|
+
return instances.has(name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a PostgreSQL plugin
|
|
173
|
+
*
|
|
174
|
+
* @param config - PostgreSQL configuration
|
|
175
|
+
* @param instanceName - Name for this PostgreSQL instance (default: 'default')
|
|
176
|
+
* @returns A control panel plugin
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* createPostgresPlugin({
|
|
181
|
+
* url: process.env.DATABASE_URL,
|
|
182
|
+
* maxConnections: 20,
|
|
183
|
+
* healthCheck: true,
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function createPostgresPlugin(
|
|
188
|
+
config: PostgresPluginConfig,
|
|
189
|
+
instanceName = 'default'
|
|
190
|
+
): ControlPanelPlugin {
|
|
191
|
+
let pool: pg.Pool | null = null;
|
|
192
|
+
|
|
193
|
+
const createInstance = (): PostgresInstance => {
|
|
194
|
+
if (!pool) {
|
|
195
|
+
pool = new Pool({
|
|
196
|
+
connectionString: config.url,
|
|
197
|
+
max: config.maxConnections ?? 20,
|
|
198
|
+
min: config.minConnections ?? 2,
|
|
199
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30000,
|
|
200
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,
|
|
201
|
+
statement_timeout: config.statementTimeoutMs,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Handle pool errors
|
|
205
|
+
pool.on('error', (err) => {
|
|
206
|
+
if (config.onError) {
|
|
207
|
+
config.onError(err);
|
|
208
|
+
} else {
|
|
209
|
+
console.error(`[database:${instanceName}] Pool error:`, err.message);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Call onConnect for each new client
|
|
214
|
+
if (config.onConnect) {
|
|
215
|
+
pool.on('connect', (client) => {
|
|
216
|
+
config.onConnect!(client).catch((err) => {
|
|
217
|
+
console.error(`[database:${instanceName}] onConnect error:`, err.message);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const instance: PostgresInstance = {
|
|
224
|
+
async getClient(): Promise<pg.PoolClient> {
|
|
225
|
+
if (!pool) throw new Error('Database pool not initialized');
|
|
226
|
+
return pool.connect();
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
230
|
+
if (!pool) throw new Error('Database pool not initialized');
|
|
231
|
+
const result = await pool.query(sql, params);
|
|
232
|
+
return result.rows as T[];
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
async queryOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
236
|
+
const rows = await instance.query<T>(sql, params);
|
|
237
|
+
return rows[0] ?? null;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async queryRaw(sql: string, params?: unknown[]): Promise<pg.QueryResult> {
|
|
241
|
+
if (!pool) throw new Error('Database pool not initialized');
|
|
242
|
+
return pool.query(sql, params);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async transaction<T>(callback: TransactionCallback<T>): Promise<T> {
|
|
246
|
+
const client = await instance.getClient();
|
|
247
|
+
try {
|
|
248
|
+
await client.query('BEGIN');
|
|
249
|
+
const result = await callback(client);
|
|
250
|
+
await client.query('COMMIT');
|
|
251
|
+
return result;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
await client.query('ROLLBACK');
|
|
254
|
+
throw err;
|
|
255
|
+
} finally {
|
|
256
|
+
client.release();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
getPool(): pg.Pool {
|
|
261
|
+
if (!pool) throw new Error('Database pool not initialized');
|
|
262
|
+
return pool;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
getStats(): { total: number; idle: number; waiting: number } {
|
|
266
|
+
return {
|
|
267
|
+
total: pool?.totalCount ?? 0,
|
|
268
|
+
idle: pool?.idleCount ?? 0,
|
|
269
|
+
waiting: pool?.waitingCount ?? 0,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async close(): Promise<void> {
|
|
274
|
+
if (pool) {
|
|
275
|
+
await pool.end();
|
|
276
|
+
pool = null;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return instance;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
name: `postgres:${instanceName}`,
|
|
286
|
+
order: 5, // Initialize early, before other plugins that may need DB
|
|
287
|
+
|
|
288
|
+
async onInit(context: PluginContext): Promise<void> {
|
|
289
|
+
const { registerHealthCheck, logger } = context;
|
|
290
|
+
|
|
291
|
+
// Create and register the instance
|
|
292
|
+
const instance = createInstance();
|
|
293
|
+
instances.set(instanceName, instance);
|
|
294
|
+
|
|
295
|
+
// Test connection
|
|
296
|
+
try {
|
|
297
|
+
await instance.query('SELECT 1');
|
|
298
|
+
logger.debug(`PostgreSQL "${instanceName}" connected`);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
logger.error(`PostgreSQL "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Register health check if enabled
|
|
305
|
+
if (config.healthCheck !== false) {
|
|
306
|
+
registerHealthCheck({
|
|
307
|
+
name: config.healthCheckName ?? 'postgres',
|
|
308
|
+
type: 'custom',
|
|
309
|
+
interval: config.healthCheckInterval ?? 30000,
|
|
310
|
+
timeout: 5000,
|
|
311
|
+
check: async () => {
|
|
312
|
+
const start = Date.now();
|
|
313
|
+
try {
|
|
314
|
+
await instance.query('SELECT 1');
|
|
315
|
+
const stats = instance.getStats();
|
|
316
|
+
return {
|
|
317
|
+
healthy: true,
|
|
318
|
+
latency: Date.now() - start,
|
|
319
|
+
details: {
|
|
320
|
+
pool: stats,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return {
|
|
325
|
+
healthy: false,
|
|
326
|
+
latency: Date.now() - start,
|
|
327
|
+
details: {
|
|
328
|
+
error: err instanceof Error ? err.message : String(err),
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
async onShutdown(): Promise<void> {
|
|
338
|
+
const instance = instances.get(instanceName);
|
|
339
|
+
if (instance) {
|
|
340
|
+
await instance.close();
|
|
341
|
+
instances.delete(instanceName);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -202,11 +202,9 @@ export function LogsPage() {
|
|
|
202
202
|
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 100 }}>
|
|
203
203
|
Level
|
|
204
204
|
</TableCell>
|
|
205
|
-
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</TableCell>
|
|
209
|
-
)}
|
|
205
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 120 }}>
|
|
206
|
+
Component
|
|
207
|
+
</TableCell>
|
|
210
208
|
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
211
209
|
Message
|
|
212
210
|
</TableCell>
|
|
@@ -230,11 +228,9 @@ export function LogsPage() {
|
|
|
230
228
|
}}
|
|
231
229
|
/>
|
|
232
230
|
</TableCell>
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
</TableCell>
|
|
237
|
-
)}
|
|
231
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', fontSize: '0.75rem' }}>
|
|
232
|
+
{log.namespace || '-'}
|
|
233
|
+
</TableCell>
|
|
238
234
|
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)', fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
|
239
235
|
{log.message}
|
|
240
236
|
</TableCell>
|