@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.
Files changed (200) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +13 -116
  3. package/dist/src/core/control-panel.d.ts.map +1 -1
  4. package/dist/src/core/control-panel.js +6 -4
  5. package/dist/src/core/control-panel.js.map +1 -1
  6. package/dist/src/core/gateway.d.ts.map +1 -1
  7. package/dist/src/core/gateway.js +24 -2
  8. package/dist/src/core/gateway.js.map +1 -1
  9. package/dist/src/core/plugin-registry.d.ts +15 -2
  10. package/dist/src/core/plugin-registry.d.ts.map +1 -1
  11. package/dist/src/core/plugin-registry.js.map +1 -1
  12. package/dist/src/index.d.ts +2 -2
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +9 -3
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
  17. package/dist/src/plugins/api-keys/stores/postgres-store.js +29 -0
  18. package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
  19. package/dist/src/plugins/auth/auth-plugin.d.ts.map +1 -1
  20. package/dist/src/plugins/auth/auth-plugin.js +4 -2
  21. package/dist/src/plugins/auth/auth-plugin.js.map +1 -1
  22. package/dist/src/plugins/auth/env-config.d.ts.map +1 -1
  23. package/dist/src/plugins/auth/env-config.js +1 -0
  24. package/dist/src/plugins/auth/env-config.js.map +1 -1
  25. package/dist/src/plugins/bans/index.d.ts +1 -1
  26. package/dist/src/plugins/bans/index.d.ts.map +1 -1
  27. package/dist/src/plugins/bans/index.js +1 -1
  28. package/dist/src/plugins/bans/index.js.map +1 -1
  29. package/dist/src/plugins/bans/stores/in-memory-store.d.ts +34 -0
  30. package/dist/src/plugins/bans/stores/in-memory-store.d.ts.map +1 -0
  31. package/dist/src/plugins/bans/stores/in-memory-store.js +97 -0
  32. package/dist/src/plugins/bans/stores/in-memory-store.js.map +1 -0
  33. package/dist/src/plugins/bans/stores/index.d.ts +1 -0
  34. package/dist/src/plugins/bans/stores/index.d.ts.map +1 -1
  35. package/dist/src/plugins/bans/stores/index.js +1 -0
  36. package/dist/src/plugins/bans/stores/index.js.map +1 -1
  37. package/dist/src/plugins/cache-plugin.d.ts +35 -16
  38. package/dist/src/plugins/cache-plugin.d.ts.map +1 -1
  39. package/dist/src/plugins/cache-plugin.js +299 -20
  40. package/dist/src/plugins/cache-plugin.js.map +1 -1
  41. package/dist/src/plugins/cms/cms-plugin.d.ts.map +1 -1
  42. package/dist/src/plugins/cms/cms-plugin.js +3 -1
  43. package/dist/src/plugins/cms/cms-plugin.js.map +1 -1
  44. package/dist/src/plugins/entitlements/index.d.ts +1 -1
  45. package/dist/src/plugins/entitlements/index.d.ts.map +1 -1
  46. package/dist/src/plugins/entitlements/index.js +1 -1
  47. package/dist/src/plugins/entitlements/index.js.map +1 -1
  48. package/dist/src/plugins/entitlements/sources/in-memory-source.d.ts +9 -0
  49. package/dist/src/plugins/entitlements/sources/in-memory-source.d.ts.map +1 -0
  50. package/dist/src/plugins/entitlements/sources/in-memory-source.js +65 -0
  51. package/dist/src/plugins/entitlements/sources/in-memory-source.js.map +1 -0
  52. package/dist/src/plugins/entitlements/sources/index.d.ts +1 -0
  53. package/dist/src/plugins/entitlements/sources/index.d.ts.map +1 -1
  54. package/dist/src/plugins/entitlements/sources/index.js +1 -0
  55. package/dist/src/plugins/entitlements/sources/index.js.map +1 -1
  56. package/dist/src/plugins/health-plugin.d.ts.map +1 -1
  57. package/dist/src/plugins/health-plugin.js +1 -0
  58. package/dist/src/plugins/health-plugin.js.map +1 -1
  59. package/dist/src/plugins/index.d.ts +4 -4
  60. package/dist/src/plugins/index.d.ts.map +1 -1
  61. package/dist/src/plugins/index.js +4 -4
  62. package/dist/src/plugins/index.js.map +1 -1
  63. package/dist/src/plugins/logs-plugin.d.ts.map +1 -1
  64. package/dist/src/plugins/logs-plugin.js +49 -1
  65. package/dist/src/plugins/logs-plugin.js.map +1 -1
  66. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  67. package/dist/src/plugins/maintenance-plugin.js +39 -0
  68. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  69. package/dist/src/plugins/notifications/notifications-plugin.d.ts.map +1 -1
  70. package/dist/src/plugins/notifications/notifications-plugin.js +1 -0
  71. package/dist/src/plugins/notifications/notifications-plugin.js.map +1 -1
  72. package/dist/src/plugins/postgres-plugin.d.ts +3 -1
  73. package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
  74. package/dist/src/plugins/postgres-plugin.js +18 -8
  75. package/dist/src/plugins/postgres-plugin.js.map +1 -1
  76. package/dist/src/plugins/tenants/index.d.ts +1 -1
  77. package/dist/src/plugins/tenants/index.d.ts.map +1 -1
  78. package/dist/src/plugins/tenants/index.js +1 -1
  79. package/dist/src/plugins/tenants/index.js.map +1 -1
  80. package/dist/src/plugins/tenants/stores/in-memory-store.d.ts +59 -0
  81. package/dist/src/plugins/tenants/stores/in-memory-store.d.ts.map +1 -0
  82. package/dist/src/plugins/tenants/stores/in-memory-store.js +257 -0
  83. package/dist/src/plugins/tenants/stores/in-memory-store.js.map +1 -0
  84. package/dist/src/plugins/tenants/stores/index.d.ts +8 -0
  85. package/dist/src/plugins/tenants/stores/index.d.ts.map +1 -0
  86. package/dist/src/plugins/tenants/stores/index.js +8 -0
  87. package/dist/src/plugins/tenants/stores/index.js.map +1 -0
  88. package/dist/src/plugins/tenants/tenants-plugin.js +3 -3
  89. package/dist/src/plugins/tenants/tenants-plugin.js.map +1 -1
  90. package/dist/src/plugins/users/index.d.ts +1 -1
  91. package/dist/src/plugins/users/index.d.ts.map +1 -1
  92. package/dist/src/plugins/users/index.js +1 -1
  93. package/dist/src/plugins/users/index.js.map +1 -1
  94. package/dist/src/plugins/users/stores/in-memory-store.d.ts +36 -0
  95. package/dist/src/plugins/users/stores/in-memory-store.d.ts.map +1 -0
  96. package/dist/src/plugins/users/stores/in-memory-store.js +122 -0
  97. package/dist/src/plugins/users/stores/in-memory-store.js.map +1 -0
  98. package/dist/src/plugins/users/stores/index.d.ts +1 -0
  99. package/dist/src/plugins/users/stores/index.d.ts.map +1 -1
  100. package/dist/src/plugins/users/stores/index.js +1 -0
  101. package/dist/src/plugins/users/stores/index.js.map +1 -1
  102. package/dist/ui/src/api/controlPanelApi.d.ts +10 -1
  103. package/dist/ui/src/api/controlPanelApi.d.ts.map +1 -1
  104. package/dist/ui/src/api/controlPanelApi.js.map +1 -1
  105. package/dist/ui/src/dashboard/PluginWidgetRenderer.d.ts +3 -1
  106. package/dist/ui/src/dashboard/PluginWidgetRenderer.d.ts.map +1 -1
  107. package/dist/ui/src/dashboard/PluginWidgetRenderer.js +5 -1
  108. package/dist/ui/src/dashboard/PluginWidgetRenderer.js.map +1 -1
  109. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  110. package/dist/ui/src/dashboard/builtInWidgets.js +13 -1
  111. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  112. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts +11 -0
  113. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -0
  114. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +77 -0
  115. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -0
  116. package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.d.ts +10 -0
  117. package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.d.ts.map +1 -0
  118. package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.js +14 -0
  119. package/dist/ui/src/dashboard/widgets/DatabaseOpsWidget.js.map +1 -0
  120. package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.d.ts +10 -0
  121. package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.d.ts.map +1 -0
  122. package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.js +14 -0
  123. package/dist/ui/src/dashboard/widgets/EnvironmentConfigWidget.js.map +1 -0
  124. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts +11 -0
  125. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -0
  126. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +96 -0
  127. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -0
  128. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +10 -0
  129. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -0
  130. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +55 -0
  131. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -0
  132. package/dist/ui/src/dashboard/widgets/ServiceControlWidget.d.ts +10 -0
  133. package/dist/ui/src/dashboard/widgets/ServiceControlWidget.d.ts.map +1 -0
  134. package/dist/ui/src/dashboard/widgets/ServiceControlWidget.js +14 -0
  135. package/dist/ui/src/dashboard/widgets/ServiceControlWidget.js.map +1 -0
  136. package/dist/ui/src/dashboard/widgets/index.d.ts +6 -0
  137. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  138. package/dist/ui/src/dashboard/widgets/index.js +6 -0
  139. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  140. package/dist/ui/src/pages/DashboardPage.js +1 -1
  141. package/dist/ui/src/pages/DashboardPage.js.map +1 -1
  142. package/dist-ui/assets/{index-lm1yX6UD.js → index-8y0jDGcd.js} +112 -112
  143. package/dist-ui/assets/{index-lm1yX6UD.js.map → index-8y0jDGcd.js.map} +1 -1
  144. package/dist-ui/index.html +1 -1
  145. package/dist-ui-lib/index.js +3109 -2774
  146. package/dist-ui-lib/index.js.map +1 -1
  147. package/dist-ui-lib/src/api/controlPanelApi.d.ts +10 -1
  148. package/dist-ui-lib/src/dashboard/PluginWidgetRenderer.d.ts +3 -1
  149. package/dist-ui-lib/src/dashboard/widgets/CacheMaintenanceWidget.d.ts +10 -0
  150. package/dist-ui-lib/src/dashboard/widgets/DatabaseOpsWidget.d.ts +9 -0
  151. package/dist-ui-lib/src/dashboard/widgets/EnvironmentConfigWidget.d.ts +9 -0
  152. package/dist-ui-lib/src/dashboard/widgets/LogsMaintenanceWidget.d.ts +10 -0
  153. package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +9 -0
  154. package/dist-ui-lib/src/dashboard/widgets/ServiceControlWidget.d.ts +9 -0
  155. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +6 -0
  156. package/package.json +5 -2
  157. package/src/core/control-panel.ts +6 -4
  158. package/src/core/gateway.ts +25 -2
  159. package/src/core/plugin-registry.ts +15 -2
  160. package/src/index.ts +53 -0
  161. package/src/plugins/api-keys/stores/postgres-store.ts +30 -0
  162. package/src/plugins/auth/auth-plugin.ts +4 -2
  163. package/src/plugins/auth/env-config.ts +1 -0
  164. package/src/plugins/bans/index.ts +1 -1
  165. package/src/plugins/bans/stores/in-memory-store.ts +106 -0
  166. package/src/plugins/bans/stores/index.ts +1 -0
  167. package/src/plugins/cache-plugin.test.ts +2 -2
  168. package/src/plugins/cache-plugin.ts +331 -30
  169. package/src/plugins/cms/cms-plugin.ts +3 -1
  170. package/src/plugins/entitlements/index.ts +1 -1
  171. package/src/plugins/entitlements/sources/in-memory-source.ts +76 -0
  172. package/src/plugins/entitlements/sources/index.ts +1 -0
  173. package/src/plugins/health-plugin.ts +1 -0
  174. package/src/plugins/index.ts +4 -1
  175. package/src/plugins/logs-plugin.ts +55 -1
  176. package/src/plugins/maintenance-plugin.ts +43 -0
  177. package/src/plugins/notifications/notifications-plugin.ts +1 -0
  178. package/src/plugins/postgres-plugin.test.ts +2 -2
  179. package/src/plugins/postgres-plugin.ts +20 -9
  180. package/src/plugins/tenants/index.ts +1 -1
  181. package/src/plugins/tenants/stores/in-memory-store.ts +335 -0
  182. package/src/plugins/tenants/stores/index.ts +13 -0
  183. package/src/plugins/tenants/tenants-plugin.ts +3 -3
  184. package/src/plugins/users/index.ts +1 -1
  185. package/src/plugins/users/stores/in-memory-store.ts +140 -0
  186. package/src/plugins/users/stores/index.ts +1 -0
  187. package/src/testing/index.ts +1 -0
  188. package/src/testing/pg-mem-pool.ts +33 -0
  189. package/ui/src/api/controlPanelApi.ts +10 -1
  190. package/ui/src/dashboard/PluginWidgetRenderer.tsx +8 -0
  191. package/ui/src/dashboard/builtInWidgets.tsx +19 -1
  192. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +195 -0
  193. package/ui/src/dashboard/widgets/DatabaseOpsWidget.tsx +29 -0
  194. package/ui/src/dashboard/widgets/EnvironmentConfigWidget.tsx +29 -0
  195. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +247 -0
  196. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +128 -0
  197. package/ui/src/dashboard/widgets/ServiceControlWidget.tsx +29 -0
  198. package/ui/src/dashboard/widgets/index.ts +6 -0
  199. package/ui/src/pages/DashboardPage.tsx +2 -2
  200. 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 debug message on successful connection', async () => {
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.debug).toHaveBeenCalledWith(
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: string;
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 = new Pool({
197
- connectionString: config.url,
198
- max: config.maxConnections ?? 20,
199
- min: config.minConnections ?? 2,
200
- idleTimeoutMillis: config.idleTimeoutMs ?? 30000,
201
- connectionTimeoutMillis: config.connectionTimeoutMs ?? 5000,
202
- statement_timeout: config.statementTimeoutMs,
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/postgres-store.js';
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 {
@@ -41,4 +41,4 @@ export type {
41
41
  } from './types.js';
42
42
 
43
43
  // Stores
44
- export { postgresUserStore } from './stores/index.js';
44
+ export { postgresUserStore, inMemoryUserStore } from './stores/index.js';