@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.
Files changed (80) hide show
  1. package/README.md +179 -80
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +37 -45
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/gateway.d.ts +32 -13
  6. package/dist/core/gateway.d.ts.map +1 -1
  7. package/dist/core/gateway.js +150 -99
  8. package/dist/core/gateway.js.map +1 -1
  9. package/dist/core/guards.d.ts +22 -0
  10. package/dist/core/guards.d.ts.map +1 -0
  11. package/dist/core/guards.js +167 -0
  12. package/dist/core/guards.js.map +1 -0
  13. package/dist/core/health-manager.d.ts.map +1 -1
  14. package/dist/core/health-manager.js +3 -9
  15. package/dist/core/health-manager.js.map +1 -1
  16. package/dist/core/logging.d.ts.map +1 -1
  17. package/dist/core/logging.js +1 -5
  18. package/dist/core/logging.js.map +1 -1
  19. package/dist/core/types.d.ts +104 -11
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +9 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/plugins/cache-plugin.d.ts +219 -0
  26. package/dist/plugins/cache-plugin.d.ts.map +1 -0
  27. package/dist/plugins/cache-plugin.js +326 -0
  28. package/dist/plugins/cache-plugin.js.map +1 -0
  29. package/dist/plugins/cache-plugin.test.d.ts +8 -0
  30. package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
  31. package/dist/plugins/cache-plugin.test.js +188 -0
  32. package/dist/plugins/cache-plugin.test.js.map +1 -0
  33. package/dist/plugins/config-plugin.js +1 -1
  34. package/dist/plugins/config-plugin.js.map +1 -1
  35. package/dist/plugins/diagnostics-plugin.js +1 -1
  36. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  37. package/dist/plugins/frontend-app-plugin.d.ts +39 -0
  38. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
  39. package/dist/plugins/frontend-app-plugin.js +176 -0
  40. package/dist/plugins/frontend-app-plugin.js.map +1 -0
  41. package/dist/plugins/health-plugin.js +1 -1
  42. package/dist/plugins/health-plugin.js.map +1 -1
  43. package/dist/plugins/index.d.ts +8 -0
  44. package/dist/plugins/index.d.ts.map +1 -1
  45. package/dist/plugins/index.js +5 -0
  46. package/dist/plugins/index.js.map +1 -1
  47. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  48. package/dist/plugins/logs-plugin.js +1 -3
  49. package/dist/plugins/logs-plugin.js.map +1 -1
  50. package/dist/plugins/postgres-plugin.d.ts +155 -0
  51. package/dist/plugins/postgres-plugin.d.ts.map +1 -0
  52. package/dist/plugins/postgres-plugin.js +244 -0
  53. package/dist/plugins/postgres-plugin.js.map +1 -0
  54. package/dist/plugins/postgres-plugin.test.d.ts +8 -0
  55. package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
  56. package/dist/plugins/postgres-plugin.test.js +165 -0
  57. package/dist/plugins/postgres-plugin.test.js.map +1 -0
  58. package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
  59. package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
  60. package/dist-ui/index.html +1 -1
  61. package/package.json +18 -2
  62. package/src/core/control-panel.ts +41 -53
  63. package/src/core/gateway.ts +193 -124
  64. package/src/core/guards.ts +190 -0
  65. package/src/core/health-manager.ts +3 -9
  66. package/src/core/logging.ts +1 -5
  67. package/src/core/types.ts +115 -9
  68. package/src/index.ts +40 -0
  69. package/src/plugins/cache-plugin.test.ts +241 -0
  70. package/src/plugins/cache-plugin.ts +503 -0
  71. package/src/plugins/config-plugin.ts +1 -1
  72. package/src/plugins/diagnostics-plugin.ts +1 -1
  73. package/src/plugins/frontend-app-plugin.ts +211 -0
  74. package/src/plugins/health-plugin.ts +1 -1
  75. package/src/plugins/index.ts +13 -0
  76. package/src/plugins/logs-plugin.ts +1 -3
  77. package/src/plugins/postgres-plugin.test.ts +213 -0
  78. package/src/plugins/postgres-plugin.ts +345 -0
  79. package/ui/src/api/controlPanelApi.ts +1 -1
  80. 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.info(`[HealthPlugin] Registered ${config.checks.length} health checks`);
32
+ logger.debug(`Registered ${config.checks.length} health checks`);
33
33
  },
34
34
  };
35
35
  }
@@ -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.info(`Initialized with ${sources.length} sources`, {
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
+ });