@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
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * Cache Plugin
3
3
  *
4
- * Provides Redis caching capabilities with connection pooling and health checks.
5
- * Wraps the 'ioredis' library with a simple, reusable interface.
4
+ * Provides caching capabilities with Redis or in-memory storage.
5
+ * Supports Redis via 'ioredis' library or zero-dependency in-memory LRU cache.
6
6
  *
7
7
  * ## Features
8
- * - Connection management with automatic reconnection
8
+ * - Dual-mode: Redis (persistent, distributed) or Memory (fast, local)
9
+ * - Connection management with automatic reconnection (Redis)
10
+ * - LRU eviction with TTL expiration (Memory)
9
11
  * - Key prefixing for multi-tenant/multi-app scenarios
10
- * - TTL-based caching with setex/get operations
12
+ * - TTL-based caching with get/set operations
11
13
  * - Automatic health checks
12
14
  * - Multiple named instances support
13
15
  * - Graceful shutdown
14
16
  *
15
- * ## Usage
17
+ * ## Usage (Redis)
16
18
  *
17
19
  * ```typescript
18
20
  * import { createGateway, createCachePlugin, getCache } from '@qwickapps/server';
@@ -21,6 +23,7 @@
21
23
  * // ... config
22
24
  * plugins: [
23
25
  * createCachePlugin({
26
+ * type: 'redis',
24
27
  * url: process.env.REDIS_URL,
25
28
  * keyPrefix: 'myapp:',
26
29
  * }),
@@ -33,12 +36,24 @@
33
36
  * const user = await cache.get<User>('user:123');
34
37
  * ```
35
38
  *
39
+ * ## Usage (In-Memory)
40
+ *
41
+ * ```typescript
42
+ * // Zero external dependencies - perfect for demos and testing
43
+ * createCachePlugin({
44
+ * type: 'memory',
45
+ * keyPrefix: 'demo:',
46
+ * defaultTtl: 3600,
47
+ * maxMemoryEntries: 5000,
48
+ * })
49
+ * ```
50
+ *
36
51
  * ## Multiple Caches
37
52
  *
38
53
  * ```typescript
39
54
  * // Register multiple caches with different names
40
55
  * createCachePlugin({ url: primaryUrl, keyPrefix: 'session:' }, 'sessions');
41
- * createCachePlugin({ url: cacheUrl, keyPrefix: 'cache:' }, 'content');
56
+ * createCachePlugin({ type: 'memory', keyPrefix: 'cache:' }, 'content');
42
57
  *
43
58
  * // Access by name
44
59
  * const sessions = getCache('sessions');
@@ -48,6 +63,7 @@
48
63
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
49
64
  */
50
65
 
66
+ import type { Request, Response } from 'express';
51
67
  import type { Plugin, PluginConfig, PluginRegistry } from '../core/plugin-registry.js';
52
68
 
53
69
  // Dynamic import for ioredis (optional peer dependency)
@@ -58,8 +74,11 @@ type RedisOptions = import('ioredis').RedisOptions;
58
74
  * Configuration for the cache plugin
59
75
  */
60
76
  export interface CachePluginConfig {
61
- /** Redis connection URL (e.g., redis://localhost:6379) */
62
- url: string;
77
+ /** Cache type: 'redis' or 'memory' (default: 'redis' if url provided, 'memory' otherwise) */
78
+ type?: 'redis' | 'memory';
79
+
80
+ /** Redis connection URL (required for type='redis', e.g., redis://localhost:6379) */
81
+ url?: string;
63
82
 
64
83
  /** Key prefix for all cache operations (default: '') */
65
84
  keyPrefix?: string;
@@ -67,34 +86,37 @@ export interface CachePluginConfig {
67
86
  /** Default TTL in seconds for set operations (default: 3600 = 1 hour) */
68
87
  defaultTtl?: number;
69
88
 
70
- /** Maximum number of retry attempts (default: 3) */
89
+ /** Maximum number of entries for memory cache (default: 10000, only used for type='memory') */
90
+ maxMemoryEntries?: number;
91
+
92
+ /** Maximum number of retry attempts (default: 3, only used for type='redis') */
71
93
  maxRetries?: number;
72
94
 
73
- /** Retry delay in milliseconds (default: 1000) */
95
+ /** Retry delay in milliseconds (default: 1000, only used for type='redis') */
74
96
  retryDelayMs?: number;
75
97
 
76
- /** Connection timeout in milliseconds (default: 5000) */
98
+ /** Connection timeout in milliseconds (default: 5000, only used for type='redis') */
77
99
  connectTimeoutMs?: number;
78
100
 
79
- /** Command timeout in milliseconds (default: 5000) */
101
+ /** Command timeout in milliseconds (default: 5000, only used for type='redis') */
80
102
  commandTimeoutMs?: number;
81
103
 
82
104
  /** Register a health check for this cache (default: true) */
83
105
  healthCheck?: boolean;
84
106
 
85
- /** Name for the health check (default: 'redis') */
107
+ /** Name for the health check (default: 'redis' or 'memory') */
86
108
  healthCheckName?: string;
87
109
 
88
110
  /** Health check interval in milliseconds (default: 30000) */
89
111
  healthCheckInterval?: number;
90
112
 
91
- /** Called when connection is ready */
113
+ /** Called when connection is ready (only used for type='redis') */
92
114
  onConnect?: () => void;
93
115
 
94
- /** Called on connection errors */
116
+ /** Called on connection errors (only used for type='redis') */
95
117
  onError?: (error: Error) => void;
96
118
 
97
- /** Enable lazy connect - don't connect until first command (default: false) */
119
+ /** Enable lazy connect - don't connect until first command (default: false, only used for type='redis') */
98
120
  lazyConnect?: boolean;
99
121
  }
100
122
 
@@ -219,6 +241,94 @@ export interface CacheInstance {
219
241
  close(): Promise<void>;
220
242
  }
221
243
 
244
+ /**
245
+ * Simple LRU Cache implementation for in-memory fallback
246
+ */
247
+ class LRUCache<T> {
248
+ private cache = new Map<string, { value: T; expiresAt: number }>();
249
+ private maxSize: number;
250
+
251
+ constructor(maxSize: number) {
252
+ this.maxSize = maxSize;
253
+ }
254
+
255
+ get(key: string): T | null {
256
+ const entry = this.cache.get(key);
257
+ if (!entry) return null;
258
+
259
+ // Check expiration
260
+ if (entry.expiresAt <= Date.now()) {
261
+ this.cache.delete(key);
262
+ return null;
263
+ }
264
+
265
+ // Move to end (most recently used)
266
+ this.cache.delete(key);
267
+ this.cache.set(key, entry);
268
+
269
+ return entry.value;
270
+ }
271
+
272
+ set(key: string, value: T, ttlMs: number): void {
273
+ // Remove oldest entries if at capacity
274
+ while (this.cache.size >= this.maxSize) {
275
+ const firstKey = this.cache.keys().next().value;
276
+ if (firstKey) {
277
+ this.cache.delete(firstKey);
278
+ } else {
279
+ break;
280
+ }
281
+ }
282
+
283
+ this.cache.set(key, {
284
+ value,
285
+ expiresAt: Date.now() + ttlMs,
286
+ });
287
+ }
288
+
289
+ delete(key: string): boolean {
290
+ return this.cache.delete(key);
291
+ }
292
+
293
+ has(key: string): boolean {
294
+ const entry = this.cache.get(key);
295
+ if (!entry) return false;
296
+ if (entry.expiresAt <= Date.now()) {
297
+ this.cache.delete(key);
298
+ return false;
299
+ }
300
+ return true;
301
+ }
302
+
303
+ keys(pattern: string): string[] {
304
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
305
+ const result: string[] = [];
306
+ for (const key of this.cache.keys()) {
307
+ if (regex.test(key)) {
308
+ const entry = this.cache.get(key);
309
+ if (entry && entry.expiresAt > Date.now()) {
310
+ result.push(key);
311
+ }
312
+ }
313
+ }
314
+ return result;
315
+ }
316
+
317
+ size(): number {
318
+ // Clean up expired entries
319
+ for (const [key, entry] of this.cache.entries()) {
320
+ if (entry.expiresAt <= Date.now()) {
321
+ this.cache.delete(key);
322
+ }
323
+ }
324
+ return this.cache.size;
325
+ }
326
+
327
+ clear(): void {
328
+ this.cache.clear();
329
+ }
330
+ }
331
+
222
332
  // Global registry of cache instances by name
223
333
  const instances = new Map<string, CacheInstance>();
224
334
 
@@ -274,7 +384,11 @@ export function createCachePlugin(
274
384
  config: CachePluginConfig,
275
385
  instanceName = 'default'
276
386
  ): Plugin {
387
+ // Determine cache type
388
+ const cacheType = config.type || (config.url ? 'redis' : 'memory');
389
+
277
390
  let client: Redis | null = null;
391
+ let lru: LRUCache<string> | null = null;
278
392
  const prefix = config.keyPrefix ?? '';
279
393
  const defaultTtl = config.defaultTtl ?? 3600;
280
394
  const pluginId = `cache:${instanceName}`;
@@ -283,7 +397,129 @@ export function createCachePlugin(
283
397
  const unprefixKey = (key: string): string =>
284
398
  prefix && key.startsWith(prefix) ? key.slice(prefix.length) : key;
285
399
 
286
- const createInstance = async (): Promise<CacheInstance> => {
400
+ const createMemoryInstance = (): CacheInstance => {
401
+ const maxEntries = config.maxMemoryEntries || 10000;
402
+ lru = new LRUCache<string>(maxEntries);
403
+
404
+ return {
405
+ async get<T = unknown>(key: string): Promise<T | null> {
406
+ if (!lru) throw new Error('Cache not initialized');
407
+ const value = lru.get(prefixKey(key));
408
+ if (value === null) return null;
409
+ try {
410
+ return JSON.parse(value) as T;
411
+ } catch {
412
+ return value as unknown as T;
413
+ }
414
+ },
415
+
416
+ async getRaw(key: string): Promise<string | null> {
417
+ if (!lru) throw new Error('Cache not initialized');
418
+ return lru.get(prefixKey(key));
419
+ },
420
+
421
+ async set<T = unknown>(key: string, value: T, ttlSeconds?: number): Promise<void> {
422
+ if (!lru) throw new Error('Cache not initialized');
423
+ const ttl = (ttlSeconds ?? defaultTtl) * 1000; // Convert to ms
424
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
425
+ lru.set(prefixKey(key), serialized, ttl);
426
+ },
427
+
428
+ async setRaw(key: string, value: string, ttlSeconds?: number): Promise<void> {
429
+ if (!lru) throw new Error('Cache not initialized');
430
+ const ttl = (ttlSeconds ?? defaultTtl) * 1000;
431
+ lru.set(prefixKey(key), value, ttl);
432
+ },
433
+
434
+ async delete(key: string): Promise<boolean> {
435
+ if (!lru) throw new Error('Cache not initialized');
436
+ return lru.delete(prefixKey(key));
437
+ },
438
+
439
+ async deletePattern(pattern: string): Promise<number> {
440
+ if (!lru) throw new Error('Cache not initialized');
441
+ const keys = lru.keys(prefixKey(pattern));
442
+ keys.forEach(k => lru!.delete(k));
443
+ return keys.length;
444
+ },
445
+
446
+ async exists(key: string): Promise<boolean> {
447
+ if (!lru) throw new Error('Cache not initialized');
448
+ return lru.has(prefixKey(key));
449
+ },
450
+
451
+ async expire(key: string, ttlSeconds: number): Promise<boolean> {
452
+ if (!lru) throw new Error('Cache not initialized');
453
+ const value = lru.get(prefixKey(key));
454
+ if (value === null) return false;
455
+ lru.set(prefixKey(key), value, ttlSeconds * 1000);
456
+ return true;
457
+ },
458
+
459
+ async ttl(key: string): Promise<number> {
460
+ if (!lru) throw new Error('Cache not initialized');
461
+ // Memory cache doesn't track TTL separately
462
+ return lru.has(prefixKey(key)) ? -1 : -2;
463
+ },
464
+
465
+ async incr(key: string, delta = 1): Promise<number> {
466
+ if (!lru) throw new Error('Cache not initialized');
467
+ const current = lru.get(prefixKey(key));
468
+ const value = current ? parseInt(current, 10) + delta : delta;
469
+ lru.set(prefixKey(key), value.toString(), defaultTtl * 1000);
470
+ return value;
471
+ },
472
+
473
+ async keys(pattern: string): Promise<string[]> {
474
+ if (!lru) throw new Error('Cache not initialized');
475
+ return lru.keys(prefixKey(pattern)).map(unprefixKey);
476
+ },
477
+
478
+ async scanKeys(pattern: string): Promise<string[]> {
479
+ // Memory cache can use same implementation as keys()
480
+ return this.keys(pattern);
481
+ },
482
+
483
+ async flush(): Promise<number> {
484
+ if (!lru) throw new Error('Cache not initialized');
485
+ if (!prefix) {
486
+ throw new Error('Cannot flush without a keyPrefix configured');
487
+ }
488
+ const keys = lru.keys(`${prefix}*`);
489
+ keys.forEach(k => lru!.delete(k));
490
+ return keys.length;
491
+ },
492
+
493
+ async getStats(): Promise<{ connected: boolean; keyCount: number; usedMemory?: string }> {
494
+ if (!lru) {
495
+ return { connected: false, keyCount: 0 };
496
+ }
497
+ return {
498
+ connected: true,
499
+ keyCount: lru.size(),
500
+ usedMemory: undefined,
501
+ };
502
+ },
503
+
504
+ getClient(): Redis {
505
+ throw new Error('Memory cache does not have a Redis client');
506
+ },
507
+
508
+ async close(): Promise<void> {
509
+ if (lru) {
510
+ lru.clear();
511
+ lru = null;
512
+ }
513
+ },
514
+ };
515
+ };
516
+
517
+ const createRedisInstance = async (): Promise<CacheInstance> => {
518
+ // Validate Redis URL is provided
519
+ if (!config.url) {
520
+ throw new Error('Redis URL is required for Redis cache type');
521
+ }
522
+
287
523
  // Dynamic import of ioredis
288
524
  const { default: Redis } = await import('ioredis');
289
525
 
@@ -468,42 +704,50 @@ export function createCachePlugin(
468
704
 
469
705
  return {
470
706
  id: pluginId,
471
- name: `Redis Cache (${instanceName})`,
707
+ name: `${cacheType === 'memory' ? 'Memory' : 'Redis'} Cache (${instanceName})`,
472
708
  version: '1.0.0',
473
709
 
474
710
  async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
475
711
  const logger = registry.getLogger(pluginId);
476
712
 
477
- // Create and register the instance
478
- const instance = await createInstance();
713
+ // Create and register the instance based on type
714
+ const instance = cacheType === 'memory'
715
+ ? createMemoryInstance()
716
+ : await createRedisInstance();
479
717
  instances.set(instanceName, instance);
480
718
 
481
- // Test connection
482
- try {
483
- // Ping to verify connection
484
- await instance.getClient().ping();
485
- logger.debug(`Cache "${instanceName}" connected`);
486
- } catch (err) {
487
- logger.error(`Cache "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
488
- throw err;
719
+ // Test connection (skip for memory, test for Redis)
720
+ if (cacheType === 'redis') {
721
+ try {
722
+ await instance.getClient().ping();
723
+ logger.debug(`Cache "${instanceName}" connected`);
724
+ } catch (err) {
725
+ logger.error(`Cache "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
726
+ throw err;
727
+ }
728
+ } else {
729
+ logger.debug(`Cache "${instanceName}" initialized (in-memory)`);
489
730
  }
490
731
 
491
732
  // Register health check if enabled
492
733
  if (config.healthCheck !== false) {
493
734
  registry.registerHealthCheck({
494
- name: config.healthCheckName ?? 'redis',
735
+ name: config.healthCheckName ?? (cacheType === 'memory' ? 'memory-cache' : 'redis'),
495
736
  type: 'custom',
496
737
  interval: config.healthCheckInterval ?? 30000,
497
738
  timeout: 5000,
498
739
  check: async () => {
499
740
  const start = Date.now();
500
741
  try {
501
- await instance.getClient().ping();
742
+ if (cacheType === 'redis') {
743
+ await instance.getClient().ping();
744
+ }
502
745
  const stats = await instance.getStats();
503
746
  return {
504
747
  healthy: true,
505
748
  latency: Date.now() - start,
506
749
  details: {
750
+ type: cacheType,
507
751
  connected: stats.connected,
508
752
  keyCount: stats.keyCount,
509
753
  usedMemory: stats.usedMemory,
@@ -521,6 +765,63 @@ export function createCachePlugin(
521
765
  },
522
766
  });
523
767
  }
768
+
769
+ // Register maintenance routes (only for default instance to avoid conflicts)
770
+ if (instanceName === 'default') {
771
+ // GET /stats - Cache statistics
772
+ registry.addRoute({
773
+ method: 'get',
774
+ path: '/stats',
775
+ pluginId: 'cache',
776
+ handler: async (_req: Request, res: Response) => {
777
+ try {
778
+ const stats = await instance.getStats();
779
+ return res.json(stats);
780
+ } catch (error) {
781
+ logger.error('Failed to get cache stats', { error });
782
+ return res.status(500).json({
783
+ error: 'Failed to get cache stats',
784
+ message: error instanceof Error ? error.message : String(error),
785
+ });
786
+ }
787
+ },
788
+ });
789
+
790
+ // POST /flush - Clear cache
791
+ registry.addRoute({
792
+ method: 'post',
793
+ path: '/flush',
794
+ pluginId: 'cache',
795
+ handler: async (_req: Request, res: Response) => {
796
+ try {
797
+ const deletedCount = await instance.flush();
798
+ logger.info(`Flushed cache: ${deletedCount} keys deleted`);
799
+ return res.json({
800
+ success: true,
801
+ message: `Cache flushed successfully`,
802
+ deletedCount,
803
+ });
804
+ } catch (error) {
805
+ logger.error('Failed to flush cache', { error });
806
+ return res.status(500).json({
807
+ error: 'Failed to flush cache',
808
+ message: error instanceof Error ? error.message : String(error),
809
+ });
810
+ }
811
+ },
812
+ });
813
+
814
+ // Register maintenance widget
815
+ registry.addWidget({
816
+ id: 'cache-maintenance',
817
+ title: 'Cache Management',
818
+ component: 'CacheMaintenanceWidget',
819
+ type: 'maintenance',
820
+ priority: 60,
821
+ showByDefault: true,
822
+ pluginId: 'cache',
823
+ });
824
+ }
524
825
  },
525
826
 
526
827
  async onStop(): Promise<void> {
@@ -50,6 +50,7 @@ export function createCMSPlugin(config: CMSPluginConfig): Plugin {
50
50
  id: 'cms-status',
51
51
  title: 'Payload CMS',
52
52
  component: 'CMSStatusWidget',
53
+ type: 'status',
53
54
  priority: 15, // After ServiceHealthWidget (10)
54
55
  showByDefault: true,
55
56
  pluginId: 'cms',
@@ -62,8 +63,9 @@ export function createCMSPlugin(config: CMSPluginConfig): Plugin {
62
63
  id: 'cms-maintenance',
63
64
  title: 'CMS Service Control',
64
65
  component: 'CMSMaintenanceWidget',
66
+ type: 'maintenance',
65
67
  priority: 10,
66
- showByDefault: false, // Only shown on maintenance page
68
+ showByDefault: true, // Show by default on maintenance page
67
69
  pluginId: 'cms',
68
70
  });
69
71
  }
@@ -33,7 +33,7 @@ export {
33
33
  } from './entitlements-plugin.js';
34
34
 
35
35
  // Sources
36
- export { postgresEntitlementSource } from './sources/index.js';
36
+ export { postgresEntitlementSource, inMemoryEntitlementSource } from './sources/index.js';
37
37
 
38
38
  // Types
39
39
  export type {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * In-memory Entitlement Source for Demo/Testing
3
+ *
4
+ * Implements the EntitlementSource interface with in-memory storage.
5
+ * Pre-populated with demo data for testing and showcase purposes.
6
+ */
7
+
8
+ import type { EntitlementSource, EntitlementDefinition } from '../types.js';
9
+
10
+ export function createInMemoryEntitlementSource(): EntitlementSource {
11
+ const userEntitlements = new Map<string, string[]>();
12
+ const availableEntitlements: EntitlementDefinition[] = [
13
+ { id: '1', name: 'premium', category: 'subscription', description: 'Premium subscription tier' },
14
+ { id: '2', name: 'pro', category: 'subscription', description: 'Professional subscription tier' },
15
+ { id: '3', name: 'enterprise', category: 'subscription', description: 'Enterprise subscription tier' },
16
+ { id: '4', name: 'beta-access', category: 'features', description: 'Access to beta features' },
17
+ { id: '5', name: 'api-access', category: 'features', description: 'API access enabled' },
18
+ { id: '6', name: 'support-priority', category: 'support', description: 'Priority support access' },
19
+ ];
20
+
21
+ // Pre-populate some demo data
22
+ userEntitlements.set('demo@example.com', ['premium', 'api-access']);
23
+ userEntitlements.set('pro@example.com', ['pro', 'beta-access', 'api-access']);
24
+ userEntitlements.set('enterprise@example.com', ['enterprise', 'beta-access', 'api-access', 'support-priority']);
25
+
26
+ return {
27
+ name: 'in-memory',
28
+ description: 'In-memory entitlement source for demo/testing',
29
+ readonly: false,
30
+
31
+ async initialize() {
32
+ console.log('[InMemorySource] Initialized with demo data');
33
+ },
34
+
35
+ async getEntitlements(identifier: string): Promise<string[]> {
36
+ return userEntitlements.get(identifier.toLowerCase()) || [];
37
+ },
38
+
39
+ async getAllAvailable(): Promise<EntitlementDefinition[]> {
40
+ return availableEntitlements;
41
+ },
42
+
43
+ async getUsersWithEntitlement(entitlement: string) {
44
+ const emails: string[] = [];
45
+ userEntitlements.forEach((ents, email) => {
46
+ if (ents.includes(entitlement)) {
47
+ emails.push(email);
48
+ }
49
+ });
50
+ return { emails, total: emails.length };
51
+ },
52
+
53
+ async addEntitlement(identifier: string, entitlement: string) {
54
+ const email = identifier.toLowerCase();
55
+ const current = userEntitlements.get(email) || [];
56
+ if (!current.includes(entitlement)) {
57
+ current.push(entitlement);
58
+ userEntitlements.set(email, current);
59
+ }
60
+ },
61
+
62
+ async removeEntitlement(identifier: string, entitlement: string) {
63
+ const email = identifier.toLowerCase();
64
+ const current = userEntitlements.get(email) || [];
65
+ const index = current.indexOf(entitlement);
66
+ if (index > -1) {
67
+ current.splice(index, 1);
68
+ userEntitlements.set(email, current);
69
+ }
70
+ },
71
+
72
+ async shutdown() {
73
+ console.log('[InMemorySource] Shutdown');
74
+ },
75
+ };
76
+ }
@@ -7,3 +7,4 @@
7
7
  */
8
8
 
9
9
  export { postgresEntitlementSource } from './postgres-source.js';
10
+ export { createInMemoryEntitlementSource as inMemoryEntitlementSource } from './in-memory-source.js';
@@ -39,6 +39,7 @@ export function createHealthPlugin(config: HealthPluginConfig): Plugin {
39
39
  id: 'service-health',
40
40
  title: 'Service Health',
41
41
  component: 'ServiceHealthWidget',
42
+ type: 'status',
42
43
  priority: 10,
43
44
  showByDefault: true,
44
45
  pluginId: 'health',
@@ -93,6 +93,7 @@ export {
93
93
  linkUserIdentifiers,
94
94
  findOrCreateUser,
95
95
  postgresUserStore,
96
+ inMemoryUserStore,
96
97
  } from './users/index.js';
97
98
  export type {
98
99
  UsersPluginConfig,
@@ -111,7 +112,7 @@ export type {
111
112
  } from './users/index.js';
112
113
 
113
114
  // Tenants plugin (multi-tenant data isolation, depends on Users)
114
- export { createTenantsPlugin, getTenantStore, postgresTenantStore } from './tenants/index.js';
115
+ export { createTenantsPlugin, getTenantStore, postgresTenantStore, inMemoryTenantStore } from './tenants/index.js';
115
116
  export type {
116
117
  TenantsPluginConfig,
117
118
  TenantStore,
@@ -139,6 +140,7 @@ export {
139
140
  unbanUser,
140
141
  listActiveBans,
141
142
  postgresBanStore,
143
+ inMemoryBanStore,
142
144
  } from './bans/index.js';
143
145
  export type {
144
146
  BansPluginConfig,
@@ -198,6 +200,7 @@ export {
198
200
  requireAnyEntitlement,
199
201
  requireAllEntitlements,
200
202
  postgresEntitlementSource,
203
+ inMemoryEntitlementSource,
201
204
  } from './entitlements/index.js';
202
205
  export type {
203
206
  EntitlementsPluginConfig,