@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
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cache Plugin
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
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({
|
|
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
|
-
/**
|
|
62
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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:
|
|
68
|
+
showByDefault: true, // Show by default on maintenance page
|
|
67
69
|
pluginId: 'cms',
|
|
68
70
|
});
|
|
69
71
|
}
|
|
@@ -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
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -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,
|