@qwickapps/server 1.7.0 → 1.7.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/CHANGELOG.md +53 -0
- package/README.md +13 -116
- package/dist/src/core/control-panel.d.ts.map +1 -1
- package/dist/src/core/control-panel.js +6 -4
- package/dist/src/core/control-panel.js.map +1 -1
- package/dist/src/core/gateway.d.ts.map +1 -1
- package/dist/src/core/gateway.js +24 -2
- package/dist/src/core/gateway.js.map +1 -1
- package/dist/src/core/plugin-registry.d.ts +15 -2
- package/dist/src/core/plugin-registry.d.ts.map +1 -1
- package/dist/src/core/plugin-registry.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.js +29 -0
- package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
- package/dist/src/plugins/auth/auth-plugin.d.ts.map +1 -1
- package/dist/src/plugins/auth/auth-plugin.js +4 -2
- package/dist/src/plugins/auth/auth-plugin.js.map +1 -1
- package/dist/src/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/src/plugins/auth/env-config.js +1 -0
- package/dist/src/plugins/auth/env-config.js.map +1 -1
- package/dist/src/plugins/bans/index.d.ts +1 -1
- package/dist/src/plugins/bans/index.d.ts.map +1 -1
- package/dist/src/plugins/bans/index.js +1 -1
- package/dist/src/plugins/bans/index.js.map +1 -1
- package/dist/src/plugins/bans/stores/in-memory-store.d.ts +34 -0
- package/dist/src/plugins/bans/stores/in-memory-store.d.ts.map +1 -0
- package/dist/src/plugins/bans/stores/in-memory-store.js +97 -0
- package/dist/src/plugins/bans/stores/in-memory-store.js.map +1 -0
- package/dist/src/plugins/bans/stores/index.d.ts +1 -0
- package/dist/src/plugins/bans/stores/index.d.ts.map +1 -1
- package/dist/src/plugins/bans/stores/index.js +1 -0
- package/dist/src/plugins/bans/stores/index.js.map +1 -1
- package/dist/src/plugins/cache-plugin.d.ts +35 -16
- package/dist/src/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/src/plugins/cache-plugin.js +299 -20
- package/dist/src/plugins/cache-plugin.js.map +1 -1
- package/dist/src/plugins/cms/cms-plugin.d.ts.map +1 -1
- package/dist/src/plugins/cms/cms-plugin.js +3 -1
- package/dist/src/plugins/cms/cms-plugin.js.map +1 -1
- package/dist/src/plugins/entitlements/index.d.ts +1 -1
- package/dist/src/plugins/entitlements/index.d.ts.map +1 -1
- package/dist/src/plugins/entitlements/index.js +1 -1
- package/dist/src/plugins/entitlements/index.js.map +1 -1
- package/dist/src/plugins/entitlements/sources/in-memory-source.d.ts +9 -0
- package/dist/src/plugins/entitlements/sources/in-memory-source.d.ts.map +1 -0
- package/dist/src/plugins/entitlements/sources/in-memory-source.js +65 -0
- package/dist/src/plugins/entitlements/sources/in-memory-source.js.map +1 -0
- package/dist/src/plugins/entitlements/sources/index.d.ts +1 -0
- package/dist/src/plugins/entitlements/sources/index.d.ts.map +1 -1
- package/dist/src/plugins/entitlements/sources/index.js +1 -0
- package/dist/src/plugins/entitlements/sources/index.js.map +1 -1
- package/dist/src/plugins/health-plugin.d.ts.map +1 -1
- package/dist/src/plugins/health-plugin.js +1 -0
- package/dist/src/plugins/health-plugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +4 -4
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +4 -4
- package/dist/src/plugins/index.js.map +1 -1
- package/dist/src/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/src/plugins/logs-plugin.js +49 -1
- package/dist/src/plugins/logs-plugin.js.map +1 -1
- package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
- package/dist/src/plugins/maintenance-plugin.js +39 -0
- package/dist/src/plugins/maintenance-plugin.js.map +1 -1
- package/dist/src/plugins/notifications/notifications-plugin.d.ts.map +1 -1
- package/dist/src/plugins/notifications/notifications-plugin.js +1 -0
- package/dist/src/plugins/notifications/notifications-plugin.js.map +1 -1
- package/dist/src/plugins/postgres-plugin.d.ts +3 -1
- package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/src/plugins/postgres-plugin.js +18 -8
- package/dist/src/plugins/postgres-plugin.js.map +1 -1
- package/dist/src/plugins/tenants/index.d.ts +1 -1
- package/dist/src/plugins/tenants/index.d.ts.map +1 -1
- package/dist/src/plugins/tenants/index.js +1 -1
- package/dist/src/plugins/tenants/index.js.map +1 -1
- package/dist/src/plugins/tenants/stores/in-memory-store.d.ts +59 -0
- package/dist/src/plugins/tenants/stores/in-memory-store.d.ts.map +1 -0
- package/dist/src/plugins/tenants/stores/in-memory-store.js +257 -0
- package/dist/src/plugins/tenants/stores/in-memory-store.js.map +1 -0
- package/dist/src/plugins/tenants/stores/index.d.ts +8 -0
- package/dist/src/plugins/tenants/stores/index.d.ts.map +1 -0
- package/dist/src/plugins/tenants/stores/index.js +8 -0
- package/dist/src/plugins/tenants/stores/index.js.map +1 -0
- package/dist/src/plugins/tenants/tenants-plugin.js +3 -3
- package/dist/src/plugins/tenants/tenants-plugin.js.map +1 -1
- package/dist/src/plugins/users/index.d.ts +1 -1
- package/dist/src/plugins/users/index.d.ts.map +1 -1
- package/dist/src/plugins/users/index.js +1 -1
- package/dist/src/plugins/users/index.js.map +1 -1
- package/dist/src/plugins/users/stores/in-memory-store.d.ts +36 -0
- package/dist/src/plugins/users/stores/in-memory-store.d.ts.map +1 -0
- package/dist/src/plugins/users/stores/in-memory-store.js +122 -0
- package/dist/src/plugins/users/stores/in-memory-store.js.map +1 -0
- package/dist/src/plugins/users/stores/index.d.ts +1 -0
- package/dist/src/plugins/users/stores/index.d.ts.map +1 -1
- package/dist/src/plugins/users/stores/index.js +1 -0
- package/dist/src/plugins/users/stores/index.js.map +1 -1
- package/dist/ui/src/api/controlPanelApi.d.ts +10 -1
- package/dist/ui/src/api/controlPanelApi.d.ts.map +1 -1
- package/dist/ui/src/api/controlPanelApi.js.map +1 -1
- package/dist/ui/src/dashboard/PluginWidgetRenderer.d.ts +3 -1
- package/dist/ui/src/dashboard/PluginWidgetRenderer.d.ts.map +1 -1
- package/dist/ui/src/dashboard/PluginWidgetRenderer.js +5 -1
- package/dist/ui/src/dashboard/PluginWidgetRenderer.js.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.js +13 -1
- package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts +11 -0
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +77 -0
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.d.ts +10 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.js +14 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.d.ts +10 -0
- package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.js +14 -0
- package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts +11 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +96 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +10 -0
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +55 -0
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/ServiceControlWidget.d.ts +10 -0
- package/dist/ui/src/dashboard/widgets/ServiceControlWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/ServiceControlWidget.js +14 -0
- package/dist/ui/src/dashboard/widgets/ServiceControlWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts +6 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.js +6 -0
- package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
- package/dist/ui/src/pages/DashboardPage.js +1 -1
- package/dist/ui/src/pages/DashboardPage.js.map +1 -1
- package/dist-ui/assets/{index-lm1yX6UD.js → index-8y0jDGcd.js} +112 -112
- package/dist-ui/assets/{index-lm1yX6UD.js.map → index-8y0jDGcd.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/index.js +3109 -2774
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/src/api/controlPanelApi.d.ts +10 -1
- package/dist-ui-lib/src/dashboard/PluginWidgetRenderer.d.ts +3 -1
- package/dist-ui-lib/src/dashboard/widgets/CacheMaintenanceWidget.d.ts +10 -0
- package/dist-ui-lib/src/dashboard/widgets/DatabaseOpsWidget.d.ts +9 -0
- package/dist-ui-lib/src/dashboard/widgets/EnvironmentConfigWidget.d.ts +9 -0
- package/dist-ui-lib/src/dashboard/widgets/LogsMaintenanceWidget.d.ts +10 -0
- package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +9 -0
- package/dist-ui-lib/src/dashboard/widgets/ServiceControlWidget.d.ts +9 -0
- package/dist-ui-lib/src/dashboard/widgets/index.d.ts +6 -0
- package/package.json +5 -2
- package/src/core/control-panel.ts +6 -4
- package/src/core/gateway.ts +25 -2
- package/src/core/plugin-registry.ts +15 -2
- package/src/index.ts +53 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +30 -0
- package/src/plugins/auth/auth-plugin.ts +4 -2
- package/src/plugins/auth/env-config.ts +1 -0
- package/src/plugins/bans/index.ts +1 -1
- package/src/plugins/bans/stores/in-memory-store.ts +106 -0
- package/src/plugins/bans/stores/index.ts +1 -0
- package/src/plugins/cache-plugin.test.ts +2 -2
- package/src/plugins/cache-plugin.ts +331 -30
- package/src/plugins/cms/cms-plugin.ts +3 -1
- package/src/plugins/entitlements/index.ts +1 -1
- package/src/plugins/entitlements/sources/in-memory-source.ts +76 -0
- package/src/plugins/entitlements/sources/index.ts +1 -0
- package/src/plugins/health-plugin.ts +1 -0
- package/src/plugins/index.ts +4 -1
- package/src/plugins/logs-plugin.ts +55 -1
- package/src/plugins/maintenance-plugin.ts +43 -0
- package/src/plugins/notifications/notifications-plugin.ts +1 -0
- package/src/plugins/postgres-plugin.test.ts +2 -2
- package/src/plugins/postgres-plugin.ts +20 -9
- package/src/plugins/tenants/index.ts +1 -1
- package/src/plugins/tenants/stores/in-memory-store.ts +335 -0
- package/src/plugins/tenants/stores/index.ts +13 -0
- package/src/plugins/tenants/tenants-plugin.ts +3 -3
- package/src/plugins/users/index.ts +1 -1
- package/src/plugins/users/stores/in-memory-store.ts +140 -0
- package/src/plugins/users/stores/index.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/pg-mem-pool.ts +33 -0
- package/ui/src/api/controlPanelApi.ts +10 -1
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +8 -0
- package/ui/src/dashboard/builtInWidgets.tsx +19 -1
- package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +195 -0
- package/ui/src/dashboard/widgets/DatabaseOpsWidget.tsx +29 -0
- package/ui/src/dashboard/widgets/EnvironmentConfigWidget.tsx +29 -0
- package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +247 -0
- package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +128 -0
- package/ui/src/dashboard/widgets/ServiceControlWidget.tsx +29 -0
- package/ui/src/dashboard/widgets/index.ts +6 -0
- package/ui/src/pages/DashboardPage.tsx +2 -2
- package/ui/src/pages/MaintenancePage.tsx +1 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync, readFileSync, statSync } from 'fs';
|
|
10
|
+
import { existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs';
|
|
11
11
|
import { resolve } from 'path';
|
|
12
12
|
import type { Request, Response } from 'express';
|
|
13
13
|
import type { Plugin, PluginConfig, PluginRegistry } from '../core/plugin-registry.js';
|
|
@@ -165,6 +165,60 @@ export function createLogsPlugin(config: LogsPluginConfig = {}): Plugin {
|
|
|
165
165
|
},
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
// Register /clear route (slug prefix added automatically by framework)
|
|
169
|
+
registry.addRoute({
|
|
170
|
+
method: 'post',
|
|
171
|
+
path: '/clear',
|
|
172
|
+
pluginId: 'logs',
|
|
173
|
+
handler: (req: Request, res: Response) => {
|
|
174
|
+
try {
|
|
175
|
+
const sources = getSources();
|
|
176
|
+
const sourceName = (req.body.source as string) || sources[0]?.name;
|
|
177
|
+
const source = sources.find((s) => s.name === sourceName);
|
|
178
|
+
|
|
179
|
+
if (!source) {
|
|
180
|
+
return res.status(404).json({ error: `Source "${sourceName}" not found` });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (source.type !== 'file' || !source.path) {
|
|
184
|
+
return res.status(400).json({ error: 'Clear only available for file sources' });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const resolvedPath = resolve(source.path);
|
|
188
|
+
if (!existsSync(resolvedPath)) {
|
|
189
|
+
return res.status(404).json({ error: `Log file not found: ${source.path}` });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Truncate the file (clear contents but keep file)
|
|
193
|
+
truncateSync(resolvedPath, 0);
|
|
194
|
+
logger.info(`Cleared log file: ${source.name} (${source.path})`);
|
|
195
|
+
|
|
196
|
+
return res.json({
|
|
197
|
+
success: true,
|
|
198
|
+
message: `Log file "${source.name}" cleared successfully`,
|
|
199
|
+
source: source.name,
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
logger.error('Failed to clear log file', { error });
|
|
203
|
+
return res.status(500).json({
|
|
204
|
+
error: 'Failed to clear log file',
|
|
205
|
+
message: error instanceof Error ? error.message : String(error),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Register maintenance widget
|
|
212
|
+
registry.addWidget({
|
|
213
|
+
id: 'logs-maintenance',
|
|
214
|
+
title: 'Log Management',
|
|
215
|
+
component: 'LogsMaintenanceWidget',
|
|
216
|
+
type: 'maintenance',
|
|
217
|
+
priority: 50,
|
|
218
|
+
showByDefault: true,
|
|
219
|
+
pluginId: 'logs',
|
|
220
|
+
});
|
|
221
|
+
|
|
168
222
|
const sources = getSources();
|
|
169
223
|
logger.debug(`Logs plugin initialized with ${sources.length} sources`);
|
|
170
224
|
},
|
|
@@ -362,22 +362,65 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
|
|
|
362
362
|
});
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// Register maintenance widgets
|
|
366
|
+
if (config.enableSeeds !== false) {
|
|
367
|
+
registry.addWidget({
|
|
368
|
+
id: 'seed-management',
|
|
369
|
+
title: 'Seed Management',
|
|
370
|
+
component: 'SeedManagementWidget',
|
|
371
|
+
type: 'maintenance',
|
|
372
|
+
priority: 10,
|
|
373
|
+
showByDefault: true, // Show by default on maintenance page
|
|
374
|
+
pluginId: 'maintenance',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
365
378
|
// TODO: Register service control routes
|
|
366
379
|
if (config.enableServiceControl !== false) {
|
|
367
380
|
logger.debug('Service control enabled');
|
|
368
381
|
// Routes will be added in #703
|
|
382
|
+
|
|
383
|
+
registry.addWidget({
|
|
384
|
+
id: 'service-control',
|
|
385
|
+
title: 'Service Control',
|
|
386
|
+
component: 'ServiceControlWidget',
|
|
387
|
+
type: 'maintenance',
|
|
388
|
+
priority: 20,
|
|
389
|
+
showByDefault: false,
|
|
390
|
+
pluginId: 'maintenance',
|
|
391
|
+
});
|
|
369
392
|
}
|
|
370
393
|
|
|
371
394
|
// TODO: Register environment variable management routes
|
|
372
395
|
if (config.enableEnvManagement !== false) {
|
|
373
396
|
logger.debug('Environment variable management enabled');
|
|
374
397
|
// Routes will be added in #704
|
|
398
|
+
|
|
399
|
+
registry.addWidget({
|
|
400
|
+
id: 'environment-config',
|
|
401
|
+
title: 'Environment Configuration',
|
|
402
|
+
component: 'EnvironmentConfigWidget',
|
|
403
|
+
type: 'maintenance',
|
|
404
|
+
priority: 30,
|
|
405
|
+
showByDefault: false,
|
|
406
|
+
pluginId: 'maintenance',
|
|
407
|
+
});
|
|
375
408
|
}
|
|
376
409
|
|
|
377
410
|
// TODO: Register database operation routes
|
|
378
411
|
if (config.enableDatabaseOps !== false) {
|
|
379
412
|
logger.debug('Database operations enabled');
|
|
380
413
|
// Routes will be added in #705
|
|
414
|
+
|
|
415
|
+
registry.addWidget({
|
|
416
|
+
id: 'database-ops',
|
|
417
|
+
title: 'Database Operations',
|
|
418
|
+
component: 'DatabaseOpsWidget',
|
|
419
|
+
type: 'maintenance',
|
|
420
|
+
priority: 40,
|
|
421
|
+
showByDefault: false,
|
|
422
|
+
pluginId: 'maintenance',
|
|
423
|
+
});
|
|
381
424
|
}
|
|
382
425
|
|
|
383
426
|
// Register UI page
|
|
@@ -378,6 +378,7 @@ export function createNotificationsPlugin(config: NotificationsPluginConfig): Pl
|
|
|
378
378
|
id: 'notifications-stats',
|
|
379
379
|
title: 'Notifications',
|
|
380
380
|
component: 'NotificationsStatsWidget',
|
|
381
|
+
type: 'status',
|
|
381
382
|
priority: 25, // After ServiceHealthWidget (10) and AuthStatusWidget (20)
|
|
382
383
|
showByDefault: true,
|
|
383
384
|
pluginId: 'notifications',
|
|
@@ -118,12 +118,12 @@ describe('PostgreSQL Plugin', () => {
|
|
|
118
118
|
expect(hasPostgres('test')).toBe(true);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
it('should log
|
|
121
|
+
it('should log info message on successful connection', async () => {
|
|
122
122
|
const plugin = createPostgresPlugin(mockConfig, 'test');
|
|
123
123
|
await plugin.onStart({}, mockRegistry);
|
|
124
124
|
|
|
125
125
|
const logger = mockRegistry.getLogger('postgres:test');
|
|
126
|
-
expect(logger.
|
|
126
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
127
127
|
expect.stringContaining('connected')
|
|
128
128
|
);
|
|
129
129
|
});
|
|
@@ -56,7 +56,10 @@ const { Pool } = pg;
|
|
|
56
56
|
*/
|
|
57
57
|
export interface PostgresPluginConfig {
|
|
58
58
|
/** Database connection URL (e.g., postgresql://user:pass@host:5432/db) */
|
|
59
|
-
url
|
|
59
|
+
url?: string;
|
|
60
|
+
|
|
61
|
+
/** Pre-configured pg.Pool instance (alternative to url) */
|
|
62
|
+
pool?: pg.Pool;
|
|
60
63
|
|
|
61
64
|
/** Maximum number of clients in the pool (default: 20) */
|
|
62
65
|
maxConnections?: number;
|
|
@@ -193,14 +196,22 @@ export function createPostgresPlugin(
|
|
|
193
196
|
|
|
194
197
|
const createInstance = (): PostgresInstance => {
|
|
195
198
|
if (!pool) {
|
|
196
|
-
pool
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
if (config.pool) {
|
|
200
|
+
// Use pre-configured pool (e.g., pg-mem for testing)
|
|
201
|
+
pool = config.pool;
|
|
202
|
+
} else if (config.url) {
|
|
203
|
+
// Create pool from URL
|
|
204
|
+
pool = new Pool({
|
|
205
|
+
connectionString: config.url,
|
|
206
|
+
max: config.maxConnections ?? 20,
|
|
207
|
+
min: config.minConnections ?? 2,
|
|
208
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? 30000,
|
|
209
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,
|
|
210
|
+
statement_timeout: config.statementTimeoutMs,
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error('PostgresPluginConfig must have either url or pool');
|
|
214
|
+
}
|
|
204
215
|
|
|
205
216
|
// Handle pool errors
|
|
206
217
|
pool.on('error', (err) => {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export { createTenantsPlugin, getTenantStore, autoCreateUserTenant } from './tenants-plugin.js';
|
|
12
12
|
|
|
13
13
|
// Store implementations
|
|
14
|
-
export { postgresTenantStore } from './stores/
|
|
14
|
+
export { postgresTenantStore, inMemoryTenantStore } from './stores/index.js';
|
|
15
15
|
|
|
16
16
|
// Types
|
|
17
17
|
export type {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory Tenant Store for Demo/Testing
|
|
3
|
+
*
|
|
4
|
+
* Implements the TenantStore interface with in-memory storage.
|
|
5
|
+
* Pre-populated with demo tenants and memberships.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Tenant,
|
|
10
|
+
TenantStore,
|
|
11
|
+
CreateTenantInput,
|
|
12
|
+
UpdateTenantInput,
|
|
13
|
+
TenantSearchParams,
|
|
14
|
+
TenantListResponse,
|
|
15
|
+
TenantMembership,
|
|
16
|
+
CreateTenantMembershipInput,
|
|
17
|
+
UpdateTenantMembershipInput,
|
|
18
|
+
TenantWithMembership,
|
|
19
|
+
TenantType,
|
|
20
|
+
} from '../types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default demo user ID used for pre-populated tenants.
|
|
24
|
+
* Matches the user ID assigned by basic auth guard.
|
|
25
|
+
*/
|
|
26
|
+
export const DEMO_USER_ID = 'basic-auth-user';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Logger interface for in-memory stores.
|
|
30
|
+
*/
|
|
31
|
+
export interface InMemoryStoreLogger {
|
|
32
|
+
info: (message: string) => void;
|
|
33
|
+
debug?: (message: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for creating an in-memory tenant store.
|
|
38
|
+
*/
|
|
39
|
+
export interface InMemoryTenantStoreOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Demo user ID for pre-populated tenants.
|
|
42
|
+
* @default 'basic-auth-user'
|
|
43
|
+
*/
|
|
44
|
+
demoUserId?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Optional logger for store operations.
|
|
48
|
+
* If not provided, uses console.log as fallback.
|
|
49
|
+
*/
|
|
50
|
+
logger?: InMemoryStoreLogger;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates an in-memory tenant store for demo/testing purposes.
|
|
55
|
+
* Pre-populated with 4 demo tenants (organization, group, department, user).
|
|
56
|
+
*
|
|
57
|
+
* This store is NOT suitable for production use - data is lost on restart.
|
|
58
|
+
* Use postgresTenantsStore for production deployments.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Configuration options for the store
|
|
61
|
+
* @returns TenantStore implementation with in-memory storage
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { createInMemoryTenantStore } from '@qwickapps/server';
|
|
66
|
+
*
|
|
67
|
+
* const store = createInMemoryTenantStore({
|
|
68
|
+
* demoUserId: 'test-user-123',
|
|
69
|
+
* logger: console,
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* await store.initialize();
|
|
73
|
+
* const tenants = await store.getTenantsForUser('test-user-123');
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function createInMemoryTenantStore(
|
|
77
|
+
options: InMemoryTenantStoreOptions = {}
|
|
78
|
+
): TenantStore {
|
|
79
|
+
const demoUserId = options.demoUserId || DEMO_USER_ID;
|
|
80
|
+
const logger = options.logger || {
|
|
81
|
+
info: (msg: string) => console.log(msg),
|
|
82
|
+
debug: (msg: string) => console.log(msg),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const tenants = new Map<string, Tenant>();
|
|
86
|
+
const memberships = new Map<string, TenantMembership>();
|
|
87
|
+
let tenantIdCounter = 1;
|
|
88
|
+
let membershipIdCounter = 1;
|
|
89
|
+
|
|
90
|
+
// Pre-populate demo tenants
|
|
91
|
+
const demoTenants: Array<{ name: string; type: TenantType; owner_id: string }> = [
|
|
92
|
+
{ name: 'Acme Corporation', type: 'organization', owner_id: demoUserId },
|
|
93
|
+
{ name: 'Engineering Team', type: 'group', owner_id: demoUserId },
|
|
94
|
+
{ name: 'Finance Department', type: 'department', owner_id: demoUserId },
|
|
95
|
+
{ name: 'Demo User Workspace', type: 'user', owner_id: demoUserId },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
demoTenants.forEach((t) => {
|
|
99
|
+
const id = `tenant-${tenantIdCounter++}`;
|
|
100
|
+
const tenant: Tenant = {
|
|
101
|
+
id,
|
|
102
|
+
name: t.name,
|
|
103
|
+
type: t.type,
|
|
104
|
+
owner_id: t.owner_id,
|
|
105
|
+
metadata: {},
|
|
106
|
+
created_at: new Date(),
|
|
107
|
+
updated_at: new Date(),
|
|
108
|
+
};
|
|
109
|
+
tenants.set(id, tenant);
|
|
110
|
+
|
|
111
|
+
// Auto-create membership for owner
|
|
112
|
+
const membershipId = `membership-${membershipIdCounter++}`;
|
|
113
|
+
const membership: TenantMembership = {
|
|
114
|
+
id: membershipId,
|
|
115
|
+
tenant_id: id,
|
|
116
|
+
user_id: t.owner_id,
|
|
117
|
+
role: 'owner',
|
|
118
|
+
joined_at: new Date(),
|
|
119
|
+
};
|
|
120
|
+
memberships.set(membershipId, membership);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
name: 'in-memory',
|
|
125
|
+
|
|
126
|
+
async initialize(): Promise<void> {
|
|
127
|
+
logger.info('[InMemoryTenantStore] Initialized with demo tenants');
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async getById(id: string): Promise<Tenant | null> {
|
|
131
|
+
return tenants.get(id) || null;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async getByIds(ids: string[]): Promise<Tenant[]> {
|
|
135
|
+
return ids.map((id) => tenants.get(id)).filter((t): t is Tenant => t !== undefined);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async getByName(name: string): Promise<Tenant | null> {
|
|
139
|
+
for (const tenant of tenants.values()) {
|
|
140
|
+
if (tenant.name.toLowerCase() === name.toLowerCase()) {
|
|
141
|
+
return tenant;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async create(input: CreateTenantInput): Promise<Tenant> {
|
|
148
|
+
const id = `tenant-${tenantIdCounter++}`;
|
|
149
|
+
const tenant: Tenant = {
|
|
150
|
+
id,
|
|
151
|
+
name: input.name,
|
|
152
|
+
type: input.type,
|
|
153
|
+
owner_id: input.owner_id,
|
|
154
|
+
metadata: input.metadata || {},
|
|
155
|
+
created_at: new Date(),
|
|
156
|
+
updated_at: new Date(),
|
|
157
|
+
};
|
|
158
|
+
tenants.set(id, tenant);
|
|
159
|
+
|
|
160
|
+
// Auto-create membership for owner
|
|
161
|
+
const membershipId = `membership-${membershipIdCounter++}`;
|
|
162
|
+
const membership: TenantMembership = {
|
|
163
|
+
id: membershipId,
|
|
164
|
+
tenant_id: id,
|
|
165
|
+
user_id: input.owner_id,
|
|
166
|
+
role: 'owner',
|
|
167
|
+
joined_at: new Date(),
|
|
168
|
+
};
|
|
169
|
+
memberships.set(membershipId, membership);
|
|
170
|
+
|
|
171
|
+
return tenant;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async update(id: string, input: UpdateTenantInput): Promise<Tenant | null> {
|
|
175
|
+
const tenant = tenants.get(id);
|
|
176
|
+
if (!tenant) return null;
|
|
177
|
+
|
|
178
|
+
const updated: Tenant = {
|
|
179
|
+
...tenant,
|
|
180
|
+
...input,
|
|
181
|
+
id: tenant.id, // Preserve ID
|
|
182
|
+
created_at: tenant.created_at, // Preserve created_at
|
|
183
|
+
updated_at: new Date(),
|
|
184
|
+
};
|
|
185
|
+
tenants.set(id, updated);
|
|
186
|
+
return updated;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async delete(id: string): Promise<boolean> {
|
|
190
|
+
// Delete all memberships for this tenant
|
|
191
|
+
for (const [key, membership] of memberships.entries()) {
|
|
192
|
+
if (membership.tenant_id === id) {
|
|
193
|
+
memberships.delete(key);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return tenants.delete(id);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async search(params: TenantSearchParams = {}): Promise<TenantListResponse> {
|
|
200
|
+
let result = Array.from(tenants.values());
|
|
201
|
+
|
|
202
|
+
if (params.query) {
|
|
203
|
+
const query = params.query.toLowerCase();
|
|
204
|
+
result = result.filter((t) => t.name.toLowerCase().includes(query));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (params.type) {
|
|
208
|
+
result = result.filter((t) => t.type === params.type);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (params.owner_id) {
|
|
212
|
+
result = result.filter((t) => t.owner_id === params.owner_id);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sortBy = params.sortBy || 'created_at';
|
|
216
|
+
const sortOrder = params.sortOrder || 'desc';
|
|
217
|
+
|
|
218
|
+
result.sort((a, b) => {
|
|
219
|
+
const aVal = a[sortBy as keyof Tenant];
|
|
220
|
+
const bVal = b[sortBy as keyof Tenant];
|
|
221
|
+
if (aVal == null || bVal == null) return 0;
|
|
222
|
+
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
|
|
223
|
+
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
|
|
224
|
+
return 0;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const total = result.length;
|
|
228
|
+
const page = params.page || 1;
|
|
229
|
+
const limit = params.limit || 20;
|
|
230
|
+
const offset = (page - 1) * limit;
|
|
231
|
+
result = result.slice(offset, offset + limit);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
tenants: result,
|
|
235
|
+
total,
|
|
236
|
+
page,
|
|
237
|
+
limit,
|
|
238
|
+
totalPages: Math.ceil(total / limit),
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async getTenantsForUser(userId: string): Promise<TenantWithMembership[]> {
|
|
243
|
+
const userMemberships = Array.from(memberships.values()).filter(
|
|
244
|
+
(m) => m.user_id === userId
|
|
245
|
+
);
|
|
246
|
+
const results: TenantWithMembership[] = [];
|
|
247
|
+
for (const membership of userMemberships) {
|
|
248
|
+
const tenant = tenants.get(membership.tenant_id);
|
|
249
|
+
if (tenant) {
|
|
250
|
+
results.push({
|
|
251
|
+
...tenant,
|
|
252
|
+
user_role: membership.role,
|
|
253
|
+
membership,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return results;
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
async getTenantForUser(
|
|
261
|
+
tenantId: string,
|
|
262
|
+
userId: string
|
|
263
|
+
): Promise<TenantWithMembership | null> {
|
|
264
|
+
const membership = await this.getMembership(tenantId, userId);
|
|
265
|
+
if (!membership) return null;
|
|
266
|
+
|
|
267
|
+
const tenant = tenants.get(tenantId);
|
|
268
|
+
if (!tenant) return null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
...tenant,
|
|
272
|
+
user_role: membership.role,
|
|
273
|
+
membership,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async getMembers(tenantId: string): Promise<TenantMembership[]> {
|
|
278
|
+
return Array.from(memberships.values()).filter((m) => m.tenant_id === tenantId);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async getMembership(tenantId: string, userId: string): Promise<TenantMembership | null> {
|
|
282
|
+
for (const membership of memberships.values()) {
|
|
283
|
+
if (membership.tenant_id === tenantId && membership.user_id === userId) {
|
|
284
|
+
return membership;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async addMember(input: CreateTenantMembershipInput): Promise<TenantMembership> {
|
|
291
|
+
const id = `membership-${membershipIdCounter++}`;
|
|
292
|
+
const membership: TenantMembership = {
|
|
293
|
+
id,
|
|
294
|
+
tenant_id: input.tenant_id,
|
|
295
|
+
user_id: input.user_id,
|
|
296
|
+
role: input.role,
|
|
297
|
+
joined_at: new Date(),
|
|
298
|
+
};
|
|
299
|
+
memberships.set(id, membership);
|
|
300
|
+
return membership;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
async updateMember(
|
|
304
|
+
tenantId: string,
|
|
305
|
+
userId: string,
|
|
306
|
+
input: UpdateTenantMembershipInput
|
|
307
|
+
): Promise<TenantMembership | null> {
|
|
308
|
+
for (const membership of memberships.values()) {
|
|
309
|
+
if (membership.tenant_id === tenantId && membership.user_id === userId) {
|
|
310
|
+
const updated: TenantMembership = {
|
|
311
|
+
...membership,
|
|
312
|
+
...input,
|
|
313
|
+
};
|
|
314
|
+
memberships.set(membership.id, updated);
|
|
315
|
+
return updated;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async removeMember(tenantId: string, userId: string): Promise<boolean> {
|
|
322
|
+
for (const [key, membership] of memberships.entries()) {
|
|
323
|
+
if (membership.tenant_id === tenantId && membership.user_id === userId) {
|
|
324
|
+
memberships.delete(key);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async shutdown(): Promise<void> {
|
|
332
|
+
logger.info('[InMemoryTenantStore] Shutdown');
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant Stores Index
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { postgresTenantStore } from './postgres-store.js';
|
|
8
|
+
export {
|
|
9
|
+
createInMemoryTenantStore as inMemoryTenantStore,
|
|
10
|
+
DEMO_USER_ID,
|
|
11
|
+
type InMemoryStoreLogger,
|
|
12
|
+
type InMemoryTenantStoreOptions,
|
|
13
|
+
} from './in-memory-store.js';
|
|
@@ -33,7 +33,7 @@ let currentRegistry: PluginRegistry | null = null;
|
|
|
33
33
|
*/
|
|
34
34
|
export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
35
35
|
const debug = config.debug || false;
|
|
36
|
-
const apiPrefix = config.apiPrefix || '
|
|
36
|
+
const apiPrefix = config.apiPrefix || ''; // Empty string to avoid double slashes in template literals
|
|
37
37
|
const apiEnabled = config.apiEnabled !== false;
|
|
38
38
|
|
|
39
39
|
function log(message: string, data?: Record<string, unknown>) {
|
|
@@ -118,7 +118,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
118
118
|
// List/Search tenants
|
|
119
119
|
registry.addRoute({
|
|
120
120
|
method: 'get',
|
|
121
|
-
path: apiPrefix,
|
|
121
|
+
path: apiPrefix || '/',
|
|
122
122
|
pluginId: 'tenants',
|
|
123
123
|
handler: async (req: Request, res: Response) => {
|
|
124
124
|
try {
|
|
@@ -201,7 +201,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
201
201
|
// Create tenant
|
|
202
202
|
registry.addRoute({
|
|
203
203
|
method: 'post',
|
|
204
|
-
path: apiPrefix,
|
|
204
|
+
path: apiPrefix || '/',
|
|
205
205
|
pluginId: 'tenants',
|
|
206
206
|
handler: async (req: Request, res: Response) => {
|
|
207
207
|
try {
|