@qwickapps/server 1.0.0 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -80
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +37 -45
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +32 -13
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +150 -99
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +167 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +3 -9
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +1 -5
- package/dist/core/logging.js.map +1 -1
- package/dist/core/types.d.ts +104 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache-plugin.d.ts +219 -0
- package/dist/plugins/cache-plugin.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.js +326 -0
- package/dist/plugins/cache-plugin.js.map +1 -0
- package/dist/plugins/cache-plugin.test.d.ts +8 -0
- package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.test.js +188 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -0
- package/dist/plugins/config-plugin.js +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +1 -1
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/frontend-app-plugin.d.ts +39 -0
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -0
- package/dist/plugins/frontend-app-plugin.js +176 -0
- package/dist/plugins/frontend-app-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +5 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +1 -3
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +155 -0
- package/dist/plugins/postgres-plugin.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.js +244 -0
- package/dist/plugins/postgres-plugin.js.map +1 -0
- package/dist/plugins/postgres-plugin.test.d.ts +8 -0
- package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.test.js +165 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -0
- package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
- package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +18 -2
- package/src/core/control-panel.ts +41 -53
- package/src/core/gateway.ts +193 -124
- package/src/core/guards.ts +190 -0
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +1 -5
- package/src/core/types.ts +115 -9
- package/src/index.ts +40 -0
- package/src/plugins/cache-plugin.test.ts +241 -0
- package/src/plugins/cache-plugin.ts +503 -0
- package/src/plugins/config-plugin.ts +1 -1
- package/src/plugins/diagnostics-plugin.ts +1 -1
- package/src/plugins/frontend-app-plugin.ts +211 -0
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +13 -0
- package/src/plugins/logs-plugin.ts +1 -3
- package/src/plugins/postgres-plugin.test.ts +213 -0
- package/src/plugins/postgres-plugin.ts +345 -0
- package/ui/src/api/controlPanelApi.ts +1 -1
- package/ui/src/pages/LogsPage.tsx +6 -10
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Guards for @qwickapps/server
|
|
3
|
+
*
|
|
4
|
+
* Provides authentication middleware for protecting routes.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
10
|
+
import type {
|
|
11
|
+
RouteGuardConfig,
|
|
12
|
+
BasicAuthGuardConfig,
|
|
13
|
+
SupabaseAuthGuardConfig,
|
|
14
|
+
Auth0GuardConfig,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a route guard middleware from configuration
|
|
19
|
+
*/
|
|
20
|
+
export function createRouteGuard(config: RouteGuardConfig): RequestHandler {
|
|
21
|
+
switch (config.type) {
|
|
22
|
+
case 'none':
|
|
23
|
+
return (_req, _res, next) => next();
|
|
24
|
+
case 'basic':
|
|
25
|
+
return createBasicAuthGuard(config);
|
|
26
|
+
case 'supabase':
|
|
27
|
+
return createSupabaseGuard(config);
|
|
28
|
+
case 'auth0':
|
|
29
|
+
return createAuth0Guard(config);
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unknown guard type: ${(config as any).type}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create basic auth guard middleware
|
|
37
|
+
*/
|
|
38
|
+
function createBasicAuthGuard(config: BasicAuthGuardConfig): RequestHandler {
|
|
39
|
+
const expectedAuth = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
|
|
40
|
+
const realm = config.realm || 'Protected';
|
|
41
|
+
const excludePaths = config.excludePaths || [];
|
|
42
|
+
|
|
43
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
44
|
+
// Check if path is excluded
|
|
45
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authHeader = req.headers.authorization;
|
|
50
|
+
if (authHeader === expectedAuth) {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
55
|
+
res.status(401).json({
|
|
56
|
+
error: 'Unauthorized',
|
|
57
|
+
message: 'Authentication required.',
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create Supabase auth guard middleware
|
|
64
|
+
*
|
|
65
|
+
* Validates JWT tokens from Supabase Auth
|
|
66
|
+
*/
|
|
67
|
+
function createSupabaseGuard(config: SupabaseAuthGuardConfig): RequestHandler {
|
|
68
|
+
const excludePaths = config.excludePaths || [];
|
|
69
|
+
|
|
70
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
71
|
+
// Check if path is excluded
|
|
72
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
73
|
+
return next();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const authHeader = req.headers.authorization;
|
|
77
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
78
|
+
return res.status(401).json({
|
|
79
|
+
error: 'Unauthorized',
|
|
80
|
+
message: 'Missing or invalid authorization header. Expected: Bearer <token>',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const token = authHeader.substring(7);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Validate the JWT with Supabase
|
|
88
|
+
const response = await fetch(`${config.supabaseUrl}/auth/v1/user`, {
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${token}`,
|
|
91
|
+
apikey: config.supabaseAnonKey,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
return res.status(401).json({
|
|
97
|
+
error: 'Unauthorized',
|
|
98
|
+
message: 'Invalid or expired token.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const user = await response.json();
|
|
103
|
+
(req as any).user = user;
|
|
104
|
+
next();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return res.status(401).json({
|
|
107
|
+
error: 'Unauthorized',
|
|
108
|
+
message: 'Failed to validate token.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create Auth0 guard middleware
|
|
116
|
+
*
|
|
117
|
+
* Uses express-openid-connect for Auth0 authentication
|
|
118
|
+
*/
|
|
119
|
+
function createAuth0Guard(config: Auth0GuardConfig): RequestHandler {
|
|
120
|
+
// Lazy-load express-openid-connect to avoid requiring it when not used
|
|
121
|
+
let authMiddleware: RequestHandler | null = null;
|
|
122
|
+
|
|
123
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
124
|
+
// Lazy initialize the middleware
|
|
125
|
+
if (!authMiddleware) {
|
|
126
|
+
try {
|
|
127
|
+
const { auth } = await import('express-openid-connect');
|
|
128
|
+
authMiddleware = auth({
|
|
129
|
+
authRequired: true,
|
|
130
|
+
auth0Logout: true,
|
|
131
|
+
secret: config.secret,
|
|
132
|
+
baseURL: config.baseUrl,
|
|
133
|
+
clientID: config.clientId,
|
|
134
|
+
issuerBaseURL: `https://${config.domain}`,
|
|
135
|
+
clientSecret: config.clientSecret,
|
|
136
|
+
idpLogout: true,
|
|
137
|
+
routes: {
|
|
138
|
+
login: config.routes?.login || '/login',
|
|
139
|
+
logout: config.routes?.logout || '/logout',
|
|
140
|
+
callback: config.routes?.callback || '/callback',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return res.status(500).json({
|
|
145
|
+
error: 'Configuration Error',
|
|
146
|
+
message: 'Auth0 is not properly configured. Install express-openid-connect package.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if path is excluded
|
|
152
|
+
const excludePaths = config.excludePaths || [];
|
|
153
|
+
if (excludePaths.some(path => req.path.startsWith(path))) {
|
|
154
|
+
return next();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply Auth0 middleware
|
|
158
|
+
authMiddleware!(req, res, next);
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Helper to check if a request is authenticated (for use in handlers)
|
|
164
|
+
*/
|
|
165
|
+
export function isAuthenticated(req: Request): boolean {
|
|
166
|
+
// Check for Auth0 session
|
|
167
|
+
if ((req as any).oidc?.isAuthenticated?.()) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Check for Supabase user
|
|
171
|
+
if ((req as any).user) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the authenticated user from the request
|
|
179
|
+
*/
|
|
180
|
+
export function getAuthenticatedUser(req: Request): any | null {
|
|
181
|
+
// Check for Auth0 user
|
|
182
|
+
if ((req as any).oidc?.user) {
|
|
183
|
+
return (req as any).oidc.user;
|
|
184
|
+
}
|
|
185
|
+
// Check for Supabase user
|
|
186
|
+
if ((req as any).user) {
|
|
187
|
+
return (req as any).user;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
@@ -40,10 +40,7 @@ export class HealthManager {
|
|
|
40
40
|
|
|
41
41
|
this.intervals.set(check.name, timer);
|
|
42
42
|
|
|
43
|
-
this.logger.
|
|
44
|
-
type: check.type,
|
|
45
|
-
interval,
|
|
46
|
-
});
|
|
43
|
+
this.logger.debug(`Health check registered: ${check.name} (${check.type}, ${interval}ms)`)
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
/**
|
|
@@ -101,10 +98,7 @@ export class HealthManager {
|
|
|
101
98
|
lastChecked: new Date(),
|
|
102
99
|
});
|
|
103
100
|
|
|
104
|
-
this.logger.warn(`
|
|
105
|
-
error: message,
|
|
106
|
-
latency,
|
|
107
|
-
});
|
|
101
|
+
this.logger.warn(`Health check failed: ${name} - ${message}`);
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
|
|
@@ -222,6 +216,6 @@ export class HealthManager {
|
|
|
222
216
|
clearInterval(timer);
|
|
223
217
|
}
|
|
224
218
|
this.intervals.clear();
|
|
225
|
-
this.logger.
|
|
219
|
+
this.logger.debug('Health manager shutdown complete');
|
|
226
220
|
}
|
|
227
221
|
}
|
package/src/core/logging.ts
CHANGED
|
@@ -135,12 +135,8 @@ class LoggingSubsystem {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
this.initialized = true;
|
|
138
|
-
this.rootLogger.
|
|
139
|
-
logDir: this.config.logDir,
|
|
138
|
+
this.rootLogger.debug('Logging initialized', {
|
|
140
139
|
level: this.config.level,
|
|
141
|
-
fileLogging: this.config.fileLogging,
|
|
142
|
-
consoleOutput: this.config.consoleOutput,
|
|
143
|
-
usingPino: this.rootLogger.isUsingPino(),
|
|
144
140
|
});
|
|
145
141
|
}
|
|
146
142
|
|
package/src/core/types.ts
CHANGED
|
@@ -4,7 +4,110 @@
|
|
|
4
4
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Application, RequestHandler, Router } from 'express';
|
|
7
|
+
import type { Application, RequestHandler, Router, Request, Response, NextFunction } from 'express';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Route guard types for protecting routes
|
|
11
|
+
*/
|
|
12
|
+
export type RouteGuardType = 'none' | 'basic' | 'supabase' | 'auth0';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Basic auth guard configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface BasicAuthGuardConfig {
|
|
18
|
+
type: 'basic';
|
|
19
|
+
/** Username for basic auth */
|
|
20
|
+
username: string;
|
|
21
|
+
/** Password for basic auth */
|
|
22
|
+
password: string;
|
|
23
|
+
/** Realm name for the WWW-Authenticate header */
|
|
24
|
+
realm?: string;
|
|
25
|
+
/** Paths to exclude from authentication (e.g., ['/health']) */
|
|
26
|
+
excludePaths?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Supabase auth guard configuration
|
|
31
|
+
*/
|
|
32
|
+
export interface SupabaseAuthGuardConfig {
|
|
33
|
+
type: 'supabase';
|
|
34
|
+
/** Supabase project URL */
|
|
35
|
+
supabaseUrl: string;
|
|
36
|
+
/** Supabase anon key */
|
|
37
|
+
supabaseAnonKey: string;
|
|
38
|
+
/** Paths to exclude from authentication */
|
|
39
|
+
excludePaths?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Auth0 guard configuration
|
|
44
|
+
*/
|
|
45
|
+
export interface Auth0GuardConfig {
|
|
46
|
+
type: 'auth0';
|
|
47
|
+
/** Auth0 domain (e.g., 'myapp.auth0.com') */
|
|
48
|
+
domain: string;
|
|
49
|
+
/** Auth0 client ID */
|
|
50
|
+
clientId: string;
|
|
51
|
+
/** Auth0 client secret */
|
|
52
|
+
clientSecret: string;
|
|
53
|
+
/** Base URL of the application */
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
/** Session secret for cookie encryption */
|
|
56
|
+
secret: string;
|
|
57
|
+
/** Auth routes configuration */
|
|
58
|
+
routes?: {
|
|
59
|
+
login?: string;
|
|
60
|
+
logout?: string;
|
|
61
|
+
callback?: string;
|
|
62
|
+
};
|
|
63
|
+
/** Paths to exclude from authentication */
|
|
64
|
+
excludePaths?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* No authentication guard
|
|
69
|
+
*/
|
|
70
|
+
export interface NoAuthGuardConfig {
|
|
71
|
+
type: 'none';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Union type for all guard configurations
|
|
76
|
+
*/
|
|
77
|
+
export type RouteGuardConfig =
|
|
78
|
+
| NoAuthGuardConfig
|
|
79
|
+
| BasicAuthGuardConfig
|
|
80
|
+
| SupabaseAuthGuardConfig
|
|
81
|
+
| Auth0GuardConfig;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mount path configuration for applications
|
|
85
|
+
*/
|
|
86
|
+
export interface MountConfig {
|
|
87
|
+
/** Path where this app is mounted (e.g., '/', '/cpanel', '/app') */
|
|
88
|
+
path: string;
|
|
89
|
+
/** Route guard configuration for this mount point */
|
|
90
|
+
guard?: RouteGuardConfig;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Frontend app configuration
|
|
95
|
+
*/
|
|
96
|
+
export interface FrontendAppConfig {
|
|
97
|
+
/** Mount configuration */
|
|
98
|
+
mount: MountConfig;
|
|
99
|
+
/** Redirect to another URL instead of serving content */
|
|
100
|
+
redirectUrl?: string;
|
|
101
|
+
/** Path to static files to serve */
|
|
102
|
+
staticPath?: string;
|
|
103
|
+
/** Landing page HTML (used if no staticPath or redirectUrl) */
|
|
104
|
+
landingPage?: {
|
|
105
|
+
title: string;
|
|
106
|
+
heading?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
links?: Array<{ label: string; url: string }>;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
8
111
|
|
|
9
112
|
/**
|
|
10
113
|
* Control Panel Configuration
|
|
@@ -26,14 +129,17 @@ export interface ControlPanelConfig {
|
|
|
26
129
|
favicon?: string;
|
|
27
130
|
};
|
|
28
131
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Mount path for the control panel.
|
|
134
|
+
* Defaults to '/cpanel'.
|
|
135
|
+
*/
|
|
136
|
+
mountPath?: string;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Route guard for the control panel.
|
|
140
|
+
* Defaults to basic auth in production.
|
|
141
|
+
*/
|
|
142
|
+
guard?: RouteGuardConfig;
|
|
37
143
|
|
|
38
144
|
/** Optional: CORS configuration */
|
|
39
145
|
cors?: {
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,13 @@ export { createControlPanel } from './core/control-panel.js';
|
|
|
12
12
|
export { createGateway } from './core/gateway.js';
|
|
13
13
|
export { HealthManager } from './core/health-manager.js';
|
|
14
14
|
|
|
15
|
+
// Guards exports
|
|
16
|
+
export {
|
|
17
|
+
createRouteGuard,
|
|
18
|
+
isAuthenticated,
|
|
19
|
+
getAuthenticatedUser,
|
|
20
|
+
} from './core/guards.js';
|
|
21
|
+
|
|
15
22
|
// Logging exports
|
|
16
23
|
export {
|
|
17
24
|
initializeLogging,
|
|
@@ -33,6 +40,15 @@ export type {
|
|
|
33
40
|
ConfigDisplayOptions,
|
|
34
41
|
Logger,
|
|
35
42
|
DiagnosticsReport,
|
|
43
|
+
// New mount path and guard types
|
|
44
|
+
RouteGuardType,
|
|
45
|
+
RouteGuardConfig,
|
|
46
|
+
BasicAuthGuardConfig,
|
|
47
|
+
SupabaseAuthGuardConfig,
|
|
48
|
+
Auth0GuardConfig,
|
|
49
|
+
NoAuthGuardConfig,
|
|
50
|
+
MountConfig,
|
|
51
|
+
FrontendAppConfig,
|
|
36
52
|
} from './core/types.js';
|
|
37
53
|
export type {
|
|
38
54
|
GatewayConfig,
|
|
@@ -46,10 +62,34 @@ export {
|
|
|
46
62
|
createLogsPlugin,
|
|
47
63
|
createConfigPlugin,
|
|
48
64
|
createDiagnosticsPlugin,
|
|
65
|
+
createFrontendAppPlugin,
|
|
66
|
+
// Database plugins
|
|
67
|
+
createPostgresPlugin,
|
|
68
|
+
getPostgres,
|
|
69
|
+
hasPostgres,
|
|
70
|
+
// Backward compatibility aliases (deprecated)
|
|
71
|
+
createPostgresPlugin as createDatabasePlugin,
|
|
72
|
+
getPostgres as getDatabase,
|
|
73
|
+
hasPostgres as hasDatabase,
|
|
74
|
+
// Cache plugins
|
|
75
|
+
createCachePlugin,
|
|
76
|
+
getCache,
|
|
77
|
+
hasCache,
|
|
49
78
|
} from './plugins/index.js';
|
|
50
79
|
export type {
|
|
51
80
|
HealthPluginConfig,
|
|
52
81
|
LogsPluginConfig,
|
|
53
82
|
ConfigPluginConfig,
|
|
54
83
|
DiagnosticsPluginConfig,
|
|
84
|
+
FrontendAppPluginConfig,
|
|
85
|
+
// Database plugin types
|
|
86
|
+
PostgresPluginConfig,
|
|
87
|
+
PostgresInstance,
|
|
88
|
+
TransactionCallback,
|
|
89
|
+
// Backward compatibility aliases (deprecated)
|
|
90
|
+
PostgresPluginConfig as DatabasePluginConfig,
|
|
91
|
+
PostgresInstance as DatabaseInstance,
|
|
92
|
+
// Cache plugin types
|
|
93
|
+
CachePluginConfig,
|
|
94
|
+
CacheInstance,
|
|
55
95
|
} from './plugins/index.js';
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Plugin Tests
|
|
3
|
+
*
|
|
4
|
+
* Note: These tests use mocks since we don't want to require a real Redis instance.
|
|
5
|
+
* Integration tests should be run separately with a real Redis instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
// Mock ioredis before importing the plugin
|
|
11
|
+
vi.mock('ioredis', () => {
|
|
12
|
+
const mockClient = {
|
|
13
|
+
get: vi.fn().mockResolvedValue(null),
|
|
14
|
+
setex: vi.fn().mockResolvedValue('OK'),
|
|
15
|
+
del: vi.fn().mockResolvedValue(1),
|
|
16
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
17
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
18
|
+
ttl: vi.fn().mockResolvedValue(3600),
|
|
19
|
+
incr: vi.fn().mockResolvedValue(1),
|
|
20
|
+
incrby: vi.fn().mockResolvedValue(5),
|
|
21
|
+
keys: vi.fn().mockResolvedValue([]),
|
|
22
|
+
info: vi.fn().mockResolvedValue('used_memory_human:1.5M\n'),
|
|
23
|
+
dbsize: vi.fn().mockResolvedValue(100),
|
|
24
|
+
ping: vi.fn().mockResolvedValue('PONG'),
|
|
25
|
+
quit: vi.fn().mockResolvedValue('OK'),
|
|
26
|
+
on: vi.fn(),
|
|
27
|
+
status: 'ready',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
default: vi.fn(() => mockClient),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
createCachePlugin,
|
|
37
|
+
getCache,
|
|
38
|
+
hasCache,
|
|
39
|
+
type CachePluginConfig,
|
|
40
|
+
} from './cache-plugin.js';
|
|
41
|
+
|
|
42
|
+
describe('Cache Plugin', () => {
|
|
43
|
+
const mockConfig: CachePluginConfig = {
|
|
44
|
+
url: 'redis://localhost:6379',
|
|
45
|
+
keyPrefix: 'test:',
|
|
46
|
+
defaultTtl: 3600,
|
|
47
|
+
healthCheck: false, // Disable for unit tests
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockContext = {
|
|
51
|
+
config: { productName: 'Test', port: 3000 },
|
|
52
|
+
app: {} as any,
|
|
53
|
+
router: {} as any,
|
|
54
|
+
logger: {
|
|
55
|
+
debug: vi.fn(),
|
|
56
|
+
info: vi.fn(),
|
|
57
|
+
warn: vi.fn(),
|
|
58
|
+
error: vi.fn(),
|
|
59
|
+
},
|
|
60
|
+
registerHealthCheck: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
vi.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
// Clean up any registered instances
|
|
69
|
+
if (hasCache('test')) {
|
|
70
|
+
const cache = getCache('test');
|
|
71
|
+
await cache.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('createCachePlugin', () => {
|
|
76
|
+
it('should create a plugin with correct name', () => {
|
|
77
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
78
|
+
expect(plugin.name).toBe('cache:test');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should use "default" as instance name when not specified', () => {
|
|
82
|
+
const plugin = createCachePlugin(mockConfig);
|
|
83
|
+
expect(plugin.name).toBe('cache:default');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have low order number (initialize early)', () => {
|
|
87
|
+
const plugin = createCachePlugin(mockConfig);
|
|
88
|
+
expect(plugin.order).toBeLessThan(10);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('onInit', () => {
|
|
93
|
+
it('should register the cache instance', async () => {
|
|
94
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
95
|
+
await plugin.onInit?.(mockContext as any);
|
|
96
|
+
|
|
97
|
+
expect(hasCache('test')).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should log debug message on successful connection', async () => {
|
|
101
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
102
|
+
await plugin.onInit?.(mockContext as any);
|
|
103
|
+
|
|
104
|
+
expect(mockContext.logger.debug).toHaveBeenCalledWith(
|
|
105
|
+
expect.stringContaining('connected')
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should register health check when enabled', async () => {
|
|
110
|
+
const configWithHealth = { ...mockConfig, healthCheck: true };
|
|
111
|
+
const plugin = createCachePlugin(configWithHealth, 'test');
|
|
112
|
+
await plugin.onInit?.(mockContext as any);
|
|
113
|
+
|
|
114
|
+
expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
name: 'redis',
|
|
117
|
+
type: 'custom',
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use custom health check name when provided', async () => {
|
|
123
|
+
const configWithCustomName = {
|
|
124
|
+
...mockConfig,
|
|
125
|
+
healthCheck: true,
|
|
126
|
+
healthCheckName: 'custom-cache',
|
|
127
|
+
};
|
|
128
|
+
const plugin = createCachePlugin(configWithCustomName, 'test');
|
|
129
|
+
await plugin.onInit?.(mockContext as any);
|
|
130
|
+
|
|
131
|
+
expect(mockContext.registerHealthCheck).toHaveBeenCalledWith(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
name: 'custom-cache',
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('getCache', () => {
|
|
140
|
+
it('should return registered instance', async () => {
|
|
141
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
142
|
+
await plugin.onInit?.(mockContext as any);
|
|
143
|
+
|
|
144
|
+
const cache = getCache('test');
|
|
145
|
+
expect(cache).toBeDefined();
|
|
146
|
+
expect(cache.get).toBeDefined();
|
|
147
|
+
expect(cache.set).toBeDefined();
|
|
148
|
+
expect(cache.delete).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should throw error for unregistered instance', () => {
|
|
152
|
+
expect(() => getCache('nonexistent')).toThrow(
|
|
153
|
+
'Cache instance "nonexistent" not found'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('hasCache', () => {
|
|
159
|
+
it('should return false for unregistered instance', () => {
|
|
160
|
+
expect(hasCache('nonexistent')).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return true for registered instance', async () => {
|
|
164
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
165
|
+
await plugin.onInit?.(mockContext as any);
|
|
166
|
+
|
|
167
|
+
expect(hasCache('test')).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('CacheInstance', () => {
|
|
172
|
+
it('should get value and parse JSON', async () => {
|
|
173
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
174
|
+
await plugin.onInit?.(mockContext as any);
|
|
175
|
+
|
|
176
|
+
const cache = getCache('test');
|
|
177
|
+
// Mock will return null by default
|
|
178
|
+
const result = await cache.get('key');
|
|
179
|
+
expect(result).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should set value with JSON stringification', async () => {
|
|
183
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
184
|
+
await plugin.onInit?.(mockContext as any);
|
|
185
|
+
|
|
186
|
+
const cache = getCache('test');
|
|
187
|
+
await cache.set('key', { foo: 'bar' }, 3600);
|
|
188
|
+
// Just verify it doesn't throw
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return cache stats', async () => {
|
|
192
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
193
|
+
await plugin.onInit?.(mockContext as any);
|
|
194
|
+
|
|
195
|
+
const cache = getCache('test');
|
|
196
|
+
const stats = await cache.getStats();
|
|
197
|
+
expect(stats).toHaveProperty('connected');
|
|
198
|
+
expect(stats).toHaveProperty('keyCount');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should check if key exists', async () => {
|
|
202
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
203
|
+
await plugin.onInit?.(mockContext as any);
|
|
204
|
+
|
|
205
|
+
const cache = getCache('test');
|
|
206
|
+
const exists = await cache.exists('key');
|
|
207
|
+
expect(typeof exists).toBe('boolean');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should get TTL for a key', async () => {
|
|
211
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
212
|
+
await plugin.onInit?.(mockContext as any);
|
|
213
|
+
|
|
214
|
+
const cache = getCache('test');
|
|
215
|
+
const ttl = await cache.ttl('key');
|
|
216
|
+
expect(typeof ttl).toBe('number');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should increment a value', async () => {
|
|
220
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
221
|
+
await plugin.onInit?.(mockContext as any);
|
|
222
|
+
|
|
223
|
+
const cache = getCache('test');
|
|
224
|
+
const value = await cache.incr('counter');
|
|
225
|
+
expect(typeof value).toBe('number');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('onShutdown', () => {
|
|
230
|
+
it('should close client and unregister instance', async () => {
|
|
231
|
+
const plugin = createCachePlugin(mockConfig, 'test');
|
|
232
|
+
await plugin.onInit?.(mockContext as any);
|
|
233
|
+
|
|
234
|
+
expect(hasCache('test')).toBe(true);
|
|
235
|
+
|
|
236
|
+
await plugin.onShutdown?.();
|
|
237
|
+
|
|
238
|
+
expect(hasCache('test')).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|