@qwickapps/server 1.1.6 → 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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +5 -8
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/gateway.d.ts.map +1 -1
  6. package/dist/core/gateway.js +11 -23
  7. package/dist/core/gateway.js.map +1 -1
  8. package/dist/core/health-manager.d.ts.map +1 -1
  9. package/dist/core/health-manager.js +3 -9
  10. package/dist/core/health-manager.js.map +1 -1
  11. package/dist/core/logging.d.ts.map +1 -1
  12. package/dist/core/logging.js +1 -5
  13. package/dist/core/logging.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/cache-plugin.d.ts +219 -0
  19. package/dist/plugins/cache-plugin.d.ts.map +1 -0
  20. package/dist/plugins/cache-plugin.js +326 -0
  21. package/dist/plugins/cache-plugin.js.map +1 -0
  22. package/dist/plugins/cache-plugin.test.d.ts +8 -0
  23. package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
  24. package/dist/plugins/cache-plugin.test.js +188 -0
  25. package/dist/plugins/cache-plugin.test.js.map +1 -0
  26. package/dist/plugins/config-plugin.js +1 -1
  27. package/dist/plugins/config-plugin.js.map +1 -1
  28. package/dist/plugins/diagnostics-plugin.js +1 -1
  29. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  30. package/dist/plugins/health-plugin.js +1 -1
  31. package/dist/plugins/health-plugin.js.map +1 -1
  32. package/dist/plugins/index.d.ts +6 -0
  33. package/dist/plugins/index.d.ts.map +1 -1
  34. package/dist/plugins/index.js +4 -0
  35. package/dist/plugins/index.js.map +1 -1
  36. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  37. package/dist/plugins/logs-plugin.js +1 -3
  38. package/dist/plugins/logs-plugin.js.map +1 -1
  39. package/dist/plugins/postgres-plugin.d.ts +155 -0
  40. package/dist/plugins/postgres-plugin.d.ts.map +1 -0
  41. package/dist/plugins/postgres-plugin.js +244 -0
  42. package/dist/plugins/postgres-plugin.js.map +1 -0
  43. package/dist/plugins/postgres-plugin.test.d.ts +8 -0
  44. package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
  45. package/dist/plugins/postgres-plugin.test.js +165 -0
  46. package/dist/plugins/postgres-plugin.test.js.map +1 -0
  47. package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
  48. package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
  49. package/dist-ui/index.html +1 -1
  50. package/package.json +13 -2
  51. package/src/core/control-panel.ts +5 -8
  52. package/src/core/gateway.ts +12 -24
  53. package/src/core/health-manager.ts +3 -9
  54. package/src/core/logging.ts +1 -5
  55. package/src/index.ts +22 -0
  56. package/src/plugins/cache-plugin.test.ts +241 -0
  57. package/src/plugins/cache-plugin.ts +503 -0
  58. package/src/plugins/config-plugin.ts +1 -1
  59. package/src/plugins/diagnostics-plugin.ts +1 -1
  60. package/src/plugins/health-plugin.ts +1 -1
  61. package/src/plugins/index.ts +10 -0
  62. package/src/plugins/logs-plugin.ts +1 -3
  63. package/src/plugins/postgres-plugin.test.ts +213 -0
  64. package/src/plugins/postgres-plugin.ts +345 -0
  65. package/ui/src/api/controlPanelApi.ts +1 -1
  66. package/ui/src/pages/LogsPage.tsx +6 -10
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Control Panel</title>
7
- <script type="module" crossorigin src="/assets/index-Bk7ypbI4.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CW1BviRn.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CiizQQnb.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,10 @@
55
55
  "@types/cors": "^2.8.17",
56
56
  "@types/express": "^4.17.21",
57
57
  "@types/node": "^20.10.5",
58
+ "@types/pg": "^8.11.0",
58
59
  "@types/react": "^18.2.0",
60
+ "ioredis": "^5.4.0",
61
+ "pg": "^8.13.0",
59
62
  "@types/react-dom": "^18.2.0",
60
63
  "@vitejs/plugin-react": "^4.3.4",
61
64
  "express-openid-connect": "^2.19.3",
@@ -68,7 +71,9 @@
68
71
  },
69
72
  "peerDependencies": {
70
73
  "@qwickapps/react-framework": ">=1.0.0",
71
- "express-openid-connect": ">=2.0.0"
74
+ "express-openid-connect": ">=2.0.0",
75
+ "ioredis": ">=5.0.0",
76
+ "pg": ">=8.0.0"
72
77
  },
73
78
  "peerDependenciesMeta": {
74
79
  "@qwickapps/react-framework": {
@@ -76,6 +81,12 @@
76
81
  },
77
82
  "express-openid-connect": {
78
83
  "optional": true
84
+ },
85
+ "ioredis": {
86
+ "optional": true
87
+ },
88
+ "pg": {
89
+ "optional": true
79
90
  }
80
91
  },
81
92
  "keywords": [
@@ -169,7 +169,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
169
169
  logger.debug(`Dashboard config: mountPath=${mountPath}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
170
170
 
171
171
  if (useRichUI) {
172
- logger.info(`Serving rich React UI from ${effectiveUiPath} at ${mountPath}`);
172
+ logger.debug(`Serving React UI from ${effectiveUiPath}`);
173
173
  // Serve static assets from dist-ui at the mount path
174
174
  app.use(mountPath, express.static(effectiveUiPath));
175
175
 
@@ -189,7 +189,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
189
189
  });
190
190
  }
191
191
  } else {
192
- logger.info(`Serving basic HTML dashboard at ${mountPath}`);
192
+ logger.debug(`Serving basic HTML dashboard`);
193
193
  const dashboardPath = mountPath === '/' ? '/' : mountPath;
194
194
  app.get(dashboardPath, (_req: Request, res: Response) => {
195
195
  const html = generateDashboardHtml(config, healthManager.getResults(), mountPath);
@@ -209,7 +209,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
209
209
 
210
210
  // Register plugin
211
211
  const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
212
- logger.info(`Registering plugin: ${plugin.name}`);
212
+ logger.debug(`Registering plugin: ${plugin.name}`);
213
213
 
214
214
  // Register routes
215
215
  if (plugin.routes) {
@@ -238,7 +238,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
238
238
  }
239
239
 
240
240
  registeredPlugins.push(plugin);
241
- logger.info(`Plugin registered: ${plugin.name}`);
241
+ logger.debug(`Plugin registered: ${plugin.name}`);
242
242
  };
243
243
 
244
244
  // Get diagnostics report
@@ -276,10 +276,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
276
276
 
277
277
  return new Promise((resolve) => {
278
278
  server = app.listen(config.port, () => {
279
- logger.info(`Control panel started on port ${config.port}`);
280
- logger.info(`Dashboard: http://localhost:${config.port}${mountPath}`);
281
- logger.info(`Health: http://localhost:${config.port}${apiBasePath}/health`);
282
- logger.info(`Diagnostics: http://localhost:${config.port}${apiBasePath}/diagnostics`);
279
+ logger.info(`Control panel listening on port ${config.port}`);
283
280
  resolve();
284
281
  });
285
282
  });
@@ -441,39 +441,27 @@ export function createGateway(
441
441
  const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
442
442
 
443
443
  // Log startup info
444
- logger.info('');
445
- logger.info('========================================');
446
- logger.info(` ${config.productName} Gateway`);
447
- logger.info('========================================');
448
- logger.info('');
449
- logger.info(` Gateway Port: ${gatewayPort} (public)`);
450
- logger.info(` Service Port: ${servicePort} (internal)`);
451
- logger.info('');
452
-
444
+ logger.info(`${config.productName} Gateway`);
445
+ logger.info(`Gateway Port: ${gatewayPort} (public)`);
446
+ logger.info(`Service Port: ${servicePort} (internal)`);
447
+
453
448
  if (guardConfig && guardConfig.type === 'basic') {
454
- logger.info(' Control Panel Auth: HTTP Basic Auth');
455
- logger.info(' ----------------------------------------');
456
- logger.info(` Username: ${guardConfig.username}`);
457
- logger.info(' ----------------------------------------');
449
+ logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
458
450
  } else if (guardConfig && guardConfig.type !== 'none') {
459
- logger.info(` Control Panel Auth: ${guardConfig.type}`);
451
+ logger.info(`Control Panel Auth: ${guardConfig.type}`);
460
452
  } else {
461
- logger.info(' Control Panel Auth: None (not recommended)');
453
+ logger.info('Control Panel Auth: None (not recommended)');
462
454
  }
463
455
 
464
- logger.info('');
465
- logger.info(' Endpoints:');
466
456
  if (config.frontendApp) {
467
- logger.info(` GET / - Frontend App`);
457
+ logger.info(`Frontend App: GET /`);
468
458
  }
469
- logger.info(` GET ${controlPanelPath.padEnd(20)} - Control Panel UI`);
470
- logger.info(` GET ${apiBasePath}/health - Gateway health`);
471
- logger.info(` GET /health - Service health (proxied)`);
459
+ logger.info(`Control Panel UI: GET ${controlPanelPath.padEnd(20)}`);
460
+ logger.info(`Gateway Health: GET ${apiBasePath}/health`);
461
+ logger.info(`Service Health: GET /health`);
472
462
  for (const apiPath of proxyPaths) {
473
- logger.info(` * ${apiPath}/* - Service API (proxied)`);
463
+ logger.info(`Service API: * ${apiPath}/*`);
474
464
  }
475
- logger.info('========================================');
476
- logger.info('');
477
465
  };
478
466
 
479
467
  const stop = async (): Promise<void> => {
@@ -40,10 +40,7 @@ export class HealthManager {
40
40
 
41
41
  this.intervals.set(check.name, timer);
42
42
 
43
- this.logger.info(`[HealthManager] Registered health check: ${check.name}`, {
44
- type: check.type,
45
- interval,
46
- });
43
+ this.logger.debug(`Health check registered: ${check.name} (${check.type}, ${interval}ms)`)
47
44
  }
48
45
 
49
46
  /**
@@ -101,10 +98,7 @@ export class HealthManager {
101
98
  lastChecked: new Date(),
102
99
  });
103
100
 
104
- this.logger.warn(`[HealthManager] Health check failed: ${name}`, {
105
- error: message,
106
- latency,
107
- });
101
+ this.logger.warn(`Health check failed: ${name} - ${message}`);
108
102
  }
109
103
  }
110
104
 
@@ -222,6 +216,6 @@ export class HealthManager {
222
216
  clearInterval(timer);
223
217
  }
224
218
  this.intervals.clear();
225
- this.logger.info('[HealthManager] Shutdown complete');
219
+ this.logger.debug('Health manager shutdown complete');
226
220
  }
227
221
  }
@@ -135,12 +135,8 @@ class LoggingSubsystem {
135
135
  }
136
136
 
137
137
  this.initialized = true;
138
- this.rootLogger.info('Logging subsystem initialized', {
139
- logDir: this.config.logDir,
138
+ this.rootLogger.debug('Logging initialized', {
140
139
  level: this.config.level,
141
- fileLogging: this.config.fileLogging,
142
- consoleOutput: this.config.consoleOutput,
143
- usingPino: this.rootLogger.isUsingPino(),
144
140
  });
145
141
  }
146
142
 
package/src/index.ts CHANGED
@@ -63,6 +63,18 @@ export {
63
63
  createConfigPlugin,
64
64
  createDiagnosticsPlugin,
65
65
  createFrontendAppPlugin,
66
+ // Database plugins
67
+ createPostgresPlugin,
68
+ getPostgres,
69
+ hasPostgres,
70
+ // Backward compatibility aliases (deprecated)
71
+ createPostgresPlugin as createDatabasePlugin,
72
+ getPostgres as getDatabase,
73
+ hasPostgres as hasDatabase,
74
+ // Cache plugins
75
+ createCachePlugin,
76
+ getCache,
77
+ hasCache,
66
78
  } from './plugins/index.js';
67
79
  export type {
68
80
  HealthPluginConfig,
@@ -70,4 +82,14 @@ export type {
70
82
  ConfigPluginConfig,
71
83
  DiagnosticsPluginConfig,
72
84
  FrontendAppPluginConfig,
85
+ // Database plugin types
86
+ PostgresPluginConfig,
87
+ PostgresInstance,
88
+ TransactionCallback,
89
+ // Backward compatibility aliases (deprecated)
90
+ PostgresPluginConfig as DatabasePluginConfig,
91
+ PostgresInstance as DatabaseInstance,
92
+ // Cache plugin types
93
+ CachePluginConfig,
94
+ CacheInstance,
73
95
  } from './plugins/index.js';
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Cache Plugin Tests
3
+ *
4
+ * Note: These tests use mocks since we don't want to require a real Redis instance.
5
+ * Integration tests should be run separately with a real Redis instance.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+
10
+ // Mock ioredis before importing the plugin
11
+ vi.mock('ioredis', () => {
12
+ const mockClient = {
13
+ get: vi.fn().mockResolvedValue(null),
14
+ setex: vi.fn().mockResolvedValue('OK'),
15
+ del: vi.fn().mockResolvedValue(1),
16
+ exists: vi.fn().mockResolvedValue(1),
17
+ expire: vi.fn().mockResolvedValue(1),
18
+ ttl: vi.fn().mockResolvedValue(3600),
19
+ incr: vi.fn().mockResolvedValue(1),
20
+ incrby: vi.fn().mockResolvedValue(5),
21
+ keys: vi.fn().mockResolvedValue([]),
22
+ info: vi.fn().mockResolvedValue('used_memory_human:1.5M\n'),
23
+ dbsize: vi.fn().mockResolvedValue(100),
24
+ ping: vi.fn().mockResolvedValue('PONG'),
25
+ quit: vi.fn().mockResolvedValue('OK'),
26
+ on: vi.fn(),
27
+ status: 'ready',
28
+ };
29
+
30
+ return {
31
+ default: vi.fn(() => mockClient),
32
+ };
33
+ });
34
+
35
+ import {
36
+ createCachePlugin,
37
+ getCache,
38
+ hasCache,
39
+ type CachePluginConfig,
40
+ } from './cache-plugin.js';
41
+
42
+ describe('Cache Plugin', () => {
43
+ const mockConfig: CachePluginConfig = {
44
+ url: 'redis://localhost:6379',
45
+ keyPrefix: 'test:',
46
+ defaultTtl: 3600,
47
+ healthCheck: false, // Disable for unit tests
48
+ };
49
+
50
+ const mockContext = {
51
+ config: { productName: 'Test', port: 3000 },
52
+ app: {} as any,
53
+ router: {} as any,
54
+ logger: {
55
+ debug: vi.fn(),
56
+ info: vi.fn(),
57
+ warn: vi.fn(),
58
+ error: vi.fn(),
59
+ },
60
+ registerHealthCheck: vi.fn(),
61
+ };
62
+
63
+ beforeEach(() => {
64
+ vi.clearAllMocks();
65
+ });
66
+
67
+ afterEach(async () => {
68
+ // Clean up any registered instances
69
+ if (hasCache('test')) {
70
+ const cache = getCache('test');
71
+ await cache.close();
72
+ }
73
+ });
74
+
75
+ describe('createCachePlugin', () => {
76
+ it('should create a plugin with correct name', () => {
77
+ const plugin = createCachePlugin(mockConfig, 'test');
78
+ expect(plugin.name).toBe('cache:test');
79
+ });
80
+
81
+ it('should use "default" as instance name when not specified', () => {
82
+ const plugin = createCachePlugin(mockConfig);
83
+ expect(plugin.name).toBe('cache:default');
84
+ });
85
+
86
+ it('should have low order number (initialize early)', () => {
87
+ const plugin = createCachePlugin(mockConfig);
88
+ expect(plugin.order).toBeLessThan(10);
89
+ });
90
+ });
91
+
92
+ describe('onInit', () => {
93
+ it('should register the cache instance', async () => {
94
+ const plugin = createCachePlugin(mockConfig, 'test');
95
+ await plugin.onInit?.(mockContext as any);
96
+
97
+ expect(hasCache('test')).toBe(true);
98
+ });
99
+
100
+ it('should log debug message on successful connection', async () => {
101
+ const plugin = createCachePlugin(mockConfig, 'test');
102
+ await plugin.onInit?.(mockContext as any);
103
+
104
+ expect(mockContext.logger.debug).toHaveBeenCalledWith(
105
+ expect.stringContaining('connected')
106
+ );
107
+ });
108
+
109
+ it('should register health check when enabled', async () => {
110
+ const configWithHealth = { ...mockConfig, healthCheck: true };
111
+ const plugin = createCachePlugin(configWithHealth, 'test');
112
+ await plugin.onInit?.(mockContext as any);
113
+
114
+ expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ name: 'redis',
117
+ type: 'custom',
118
+ })
119
+ );
120
+ });
121
+
122
+ it('should use custom health check name when provided', async () => {
123
+ const configWithCustomName = {
124
+ ...mockConfig,
125
+ healthCheck: true,
126
+ healthCheckName: 'custom-cache',
127
+ };
128
+ const plugin = createCachePlugin(configWithCustomName, 'test');
129
+ await plugin.onInit?.(mockContext as any);
130
+
131
+ expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
132
+ expect.objectContaining({
133
+ name: 'custom-cache',
134
+ })
135
+ );
136
+ });
137
+ });
138
+
139
+ describe('getCache', () => {
140
+ it('should return registered instance', async () => {
141
+ const plugin = createCachePlugin(mockConfig, 'test');
142
+ await plugin.onInit?.(mockContext as any);
143
+
144
+ const cache = getCache('test');
145
+ expect(cache).toBeDefined();
146
+ expect(cache.get).toBeDefined();
147
+ expect(cache.set).toBeDefined();
148
+ expect(cache.delete).toBeDefined();
149
+ });
150
+
151
+ it('should throw error for unregistered instance', () => {
152
+ expect(() => getCache('nonexistent')).toThrow(
153
+ 'Cache instance "nonexistent" not found'
154
+ );
155
+ });
156
+ });
157
+
158
+ describe('hasCache', () => {
159
+ it('should return false for unregistered instance', () => {
160
+ expect(hasCache('nonexistent')).toBe(false);
161
+ });
162
+
163
+ it('should return true for registered instance', async () => {
164
+ const plugin = createCachePlugin(mockConfig, 'test');
165
+ await plugin.onInit?.(mockContext as any);
166
+
167
+ expect(hasCache('test')).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe('CacheInstance', () => {
172
+ it('should get value and parse JSON', async () => {
173
+ const plugin = createCachePlugin(mockConfig, 'test');
174
+ await plugin.onInit?.(mockContext as any);
175
+
176
+ const cache = getCache('test');
177
+ // Mock will return null by default
178
+ const result = await cache.get('key');
179
+ expect(result).toBeNull();
180
+ });
181
+
182
+ it('should set value with JSON stringification', async () => {
183
+ const plugin = createCachePlugin(mockConfig, 'test');
184
+ await plugin.onInit?.(mockContext as any);
185
+
186
+ const cache = getCache('test');
187
+ await cache.set('key', { foo: 'bar' }, 3600);
188
+ // Just verify it doesn't throw
189
+ });
190
+
191
+ it('should return cache stats', async () => {
192
+ const plugin = createCachePlugin(mockConfig, 'test');
193
+ await plugin.onInit?.(mockContext as any);
194
+
195
+ const cache = getCache('test');
196
+ const stats = await cache.getStats();
197
+ expect(stats).toHaveProperty('connected');
198
+ expect(stats).toHaveProperty('keyCount');
199
+ });
200
+
201
+ it('should check if key exists', async () => {
202
+ const plugin = createCachePlugin(mockConfig, 'test');
203
+ await plugin.onInit?.(mockContext as any);
204
+
205
+ const cache = getCache('test');
206
+ const exists = await cache.exists('key');
207
+ expect(typeof exists).toBe('boolean');
208
+ });
209
+
210
+ it('should get TTL for a key', async () => {
211
+ const plugin = createCachePlugin(mockConfig, 'test');
212
+ await plugin.onInit?.(mockContext as any);
213
+
214
+ const cache = getCache('test');
215
+ const ttl = await cache.ttl('key');
216
+ expect(typeof ttl).toBe('number');
217
+ });
218
+
219
+ it('should increment a value', async () => {
220
+ const plugin = createCachePlugin(mockConfig, 'test');
221
+ await plugin.onInit?.(mockContext as any);
222
+
223
+ const cache = getCache('test');
224
+ const value = await cache.incr('counter');
225
+ expect(typeof value).toBe('number');
226
+ });
227
+ });
228
+
229
+ describe('onShutdown', () => {
230
+ it('should close client and unregister instance', async () => {
231
+ const plugin = createCachePlugin(mockConfig, 'test');
232
+ await plugin.onInit?.(mockContext as any);
233
+
234
+ expect(hasCache('test')).toBe(true);
235
+
236
+ await plugin.onShutdown?.();
237
+
238
+ expect(hasCache('test')).toBe(false);
239
+ });
240
+ });
241
+ });