@qwickapps/server 1.0.0 → 1.1.7
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 +179 -80
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +37 -45
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +32 -13
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +150 -99
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +167 -0
- package/dist/core/guards.js.map +1 -0
- 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 +1 -5
- package/dist/core/logging.js.map +1 -1
- package/dist/core/types.d.ts +104 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -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/frontend-app-plugin.d.ts +39 -0
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
- package/dist/plugins/frontend-app-plugin.js +176 -0
- package/dist/plugins/frontend-app-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +5 -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 +18 -2
- package/src/core/control-panel.ts +41 -53
- package/src/core/gateway.ts +193 -124
- package/src/core/guards.ts +190 -0
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +1 -5
- package/src/core/types.ts +115 -9
- package/src/index.ts +40 -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/frontend-app-plugin.ts +211 -0
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +13 -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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend App Plugin
|
|
3
|
+
*
|
|
4
|
+
* Plugin for serving a frontend application at the root path (/).
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Redirect to another URL
|
|
7
|
+
* - Serve static files
|
|
8
|
+
* - Display a landing page
|
|
9
|
+
*
|
|
10
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import type { ControlPanelPlugin, FrontendAppConfig } from '../core/types.js';
|
|
17
|
+
import { createRouteGuard } from '../core/guards.js';
|
|
18
|
+
|
|
19
|
+
export interface FrontendAppPluginConfig {
|
|
20
|
+
/** Redirect to another URL */
|
|
21
|
+
redirectUrl?: string;
|
|
22
|
+
/** Path to static files to serve */
|
|
23
|
+
staticPath?: string;
|
|
24
|
+
/** Landing page configuration */
|
|
25
|
+
landingPage?: {
|
|
26
|
+
title: string;
|
|
27
|
+
heading?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
links?: Array<{ label: string; url: string }>;
|
|
30
|
+
branding?: {
|
|
31
|
+
logo?: string;
|
|
32
|
+
primaryColor?: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** Route guard configuration */
|
|
36
|
+
guard?: FrontendAppConfig['mount']['guard'];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a frontend app plugin that handles the root path
|
|
41
|
+
*/
|
|
42
|
+
export function createFrontendAppPlugin(config: FrontendAppPluginConfig): ControlPanelPlugin {
|
|
43
|
+
return {
|
|
44
|
+
name: 'frontend-app',
|
|
45
|
+
order: 0, // Run first to capture root path
|
|
46
|
+
|
|
47
|
+
async onInit(context) {
|
|
48
|
+
const { app, logger } = context;
|
|
49
|
+
|
|
50
|
+
// Apply guard if configured
|
|
51
|
+
if (config.guard && config.guard.type !== 'none') {
|
|
52
|
+
const guardMiddleware = createRouteGuard(config.guard);
|
|
53
|
+
// Apply guard only to root path
|
|
54
|
+
app.use('/', (req, res, next) => {
|
|
55
|
+
// Skip if not at root
|
|
56
|
+
if (req.path !== '/' && !req.path.startsWith('/?')) {
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
59
|
+
guardMiddleware(req, res, next);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Priority 1: Redirect
|
|
64
|
+
if (config.redirectUrl) {
|
|
65
|
+
logger.info(`Frontend app: Redirecting / to ${config.redirectUrl}`);
|
|
66
|
+
app.get('/', (_req, res) => {
|
|
67
|
+
res.redirect(config.redirectUrl!);
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Priority 2: Serve static files
|
|
73
|
+
if (config.staticPath && existsSync(config.staticPath)) {
|
|
74
|
+
logger.info(`Frontend app: Serving static files from ${config.staticPath}`);
|
|
75
|
+
app.use('/', express.static(config.staticPath));
|
|
76
|
+
|
|
77
|
+
// SPA fallback
|
|
78
|
+
app.get('/', (_req, res) => {
|
|
79
|
+
res.sendFile(resolve(config.staticPath!, 'index.html'));
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Priority 3: Landing page
|
|
85
|
+
if (config.landingPage) {
|
|
86
|
+
logger.info(`Frontend app: Serving landing page`);
|
|
87
|
+
app.get('/', (_req, res) => {
|
|
88
|
+
const html = generateLandingPageHtml(config.landingPage!);
|
|
89
|
+
res.type('html').send(html);
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Default: Simple welcome page
|
|
95
|
+
logger.info(`Frontend app: Serving default welcome page`);
|
|
96
|
+
app.get('/', (_req, res) => {
|
|
97
|
+
const html = generateLandingPageHtml({
|
|
98
|
+
title: context.config.productName,
|
|
99
|
+
heading: `Welcome to ${context.config.productName}`,
|
|
100
|
+
description: 'Your application is running.',
|
|
101
|
+
links: [
|
|
102
|
+
{ label: 'Control Panel', url: context.config.mountPath || '/cpanel' },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
res.type('html').send(html);
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate landing page HTML
|
|
113
|
+
*/
|
|
114
|
+
function generateLandingPageHtml(config: NonNullable<FrontendAppPluginConfig['landingPage']>): string {
|
|
115
|
+
const primaryColor = config.branding?.primaryColor || '#6366f1';
|
|
116
|
+
|
|
117
|
+
const linksHtml = (config.links || [])
|
|
118
|
+
.map(
|
|
119
|
+
(link) =>
|
|
120
|
+
`<a href="${link.url}" class="link">${link.label}</a>`
|
|
121
|
+
)
|
|
122
|
+
.join('');
|
|
123
|
+
|
|
124
|
+
return `<!DOCTYPE html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="UTF-8">
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
129
|
+
<title>${config.title}</title>
|
|
130
|
+
<style>
|
|
131
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
132
|
+
body {
|
|
133
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
134
|
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
135
|
+
color: #e2e8f0;
|
|
136
|
+
min-height: 100vh;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
}
|
|
141
|
+
.container {
|
|
142
|
+
text-align: center;
|
|
143
|
+
max-width: 600px;
|
|
144
|
+
padding: 2rem;
|
|
145
|
+
}
|
|
146
|
+
${config.branding?.logo ? `
|
|
147
|
+
.logo {
|
|
148
|
+
width: 80px;
|
|
149
|
+
height: 80px;
|
|
150
|
+
margin-bottom: 1.5rem;
|
|
151
|
+
}
|
|
152
|
+
` : ''}
|
|
153
|
+
h1 {
|
|
154
|
+
font-size: 2.5rem;
|
|
155
|
+
color: ${primaryColor};
|
|
156
|
+
margin-bottom: 1rem;
|
|
157
|
+
}
|
|
158
|
+
p {
|
|
159
|
+
font-size: 1.125rem;
|
|
160
|
+
color: #94a3b8;
|
|
161
|
+
margin-bottom: 2rem;
|
|
162
|
+
line-height: 1.6;
|
|
163
|
+
}
|
|
164
|
+
.links {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
gap: 1rem;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
}
|
|
170
|
+
.link {
|
|
171
|
+
display: inline-block;
|
|
172
|
+
padding: 0.875rem 2rem;
|
|
173
|
+
background: ${primaryColor};
|
|
174
|
+
color: white;
|
|
175
|
+
text-decoration: none;
|
|
176
|
+
border-radius: 0.5rem;
|
|
177
|
+
font-weight: 500;
|
|
178
|
+
transition: all 0.2s;
|
|
179
|
+
}
|
|
180
|
+
.link:hover {
|
|
181
|
+
transform: translateY(-2px);
|
|
182
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.3);
|
|
183
|
+
}
|
|
184
|
+
.footer {
|
|
185
|
+
position: fixed;
|
|
186
|
+
bottom: 1rem;
|
|
187
|
+
left: 0;
|
|
188
|
+
right: 0;
|
|
189
|
+
text-align: center;
|
|
190
|
+
color: #64748b;
|
|
191
|
+
font-size: 0.875rem;
|
|
192
|
+
}
|
|
193
|
+
.footer a {
|
|
194
|
+
color: ${primaryColor};
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<div class="container">
|
|
201
|
+
${config.branding?.logo ? `<img src="${config.branding.logo}" alt="Logo" class="logo">` : ''}
|
|
202
|
+
<h1>${config.heading || config.title}</h1>
|
|
203
|
+
${config.description ? `<p>${config.description}</p>` : ''}
|
|
204
|
+
${linksHtml ? `<div class="links">${linksHtml}</div>` : ''}
|
|
205
|
+
</div>
|
|
206
|
+
<div class="footer">
|
|
207
|
+
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps</a>
|
|
208
|
+
</div>
|
|
209
|
+
</body>
|
|
210
|
+
</html>`;
|
|
211
|
+
}
|
|
@@ -29,7 +29,7 @@ export function createHealthPlugin(config: HealthPluginConfig): ControlPanelPlug
|
|
|
29
29
|
registerHealthCheck(check);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
logger.
|
|
32
|
+
logger.debug(`Registered ${config.checks.length} health checks`);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -15,3 +15,16 @@ export type { ConfigPluginConfig } from './config-plugin.js';
|
|
|
15
15
|
|
|
16
16
|
export { createDiagnosticsPlugin } from './diagnostics-plugin.js';
|
|
17
17
|
export type { DiagnosticsPluginConfig } from './diagnostics-plugin.js';
|
|
18
|
+
|
|
19
|
+
export { createFrontendAppPlugin } from './frontend-app-plugin.js';
|
|
20
|
+
export type { FrontendAppPluginConfig } from './frontend-app-plugin.js';
|
|
21
|
+
|
|
22
|
+
export { createPostgresPlugin, getPostgres, hasPostgres } from './postgres-plugin.js';
|
|
23
|
+
export type { PostgresPluginConfig, PostgresInstance, TransactionCallback } from './postgres-plugin.js';
|
|
24
|
+
|
|
25
|
+
// Backward compatibility aliases (deprecated)
|
|
26
|
+
export { createPostgresPlugin as createDatabasePlugin, getPostgres as getDatabase, hasPostgres as hasDatabase } from './postgres-plugin.js';
|
|
27
|
+
export type { PostgresPluginConfig as DatabasePluginConfig, PostgresInstance as DatabaseInstance } from './postgres-plugin.js';
|
|
28
|
+
|
|
29
|
+
export { createCachePlugin, getCache, hasCache } from './cache-plugin.js';
|
|
30
|
+
export type { CachePluginConfig, CacheInstance } from './cache-plugin.js';
|
|
@@ -156,9 +156,7 @@ export function createLogsPlugin(config: LogsPluginConfig = {}): ControlPanelPlu
|
|
|
156
156
|
|
|
157
157
|
async onInit(context: PluginContext): Promise<void> {
|
|
158
158
|
const sources = getSources();
|
|
159
|
-
context.logger.
|
|
160
|
-
sources: sources.map((s) => ({ name: s.name, type: s.type, path: s.path })),
|
|
161
|
-
});
|
|
159
|
+
context.logger.debug(`Logs plugin initialized with ${sources.length} sources`);
|
|
162
160
|
},
|
|
163
161
|
};
|
|
164
162
|
}
|
|
@@ -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
|
+
});
|