@manojkmfsi/monodog 1.0.25 → 1.1.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/config/swagger-config.js +345 -0
- package/dist/constants/index.js +26 -0
- package/dist/constants/middleware.js +71 -0
- package/dist/constants/port.js +20 -0
- package/dist/constants/security.js +67 -0
- package/dist/middleware/dashboard-startup.js +15 -18
- package/dist/middleware/security.js +10 -9
- package/dist/middleware/server-startup.js +12 -11
- package/dist/middleware/swagger-middleware.js +54 -0
- package/dist/routes/health-routes.js +1 -1
- package/dist/routes/package-routes.js +1 -1
- package/dist/services/health-service.js +84 -64
- package/dist/services/package-service.js +23 -1
- package/monodog-dashboard/dist/assets/{index-746f6c13.js → index-45e19f29.js} +1 -1
- package/monodog-dashboard/dist/index.html +1 -1
- package/package.json +5 -2
- package/src/config/swagger-config.ts +344 -0
- package/src/constants/index.ts +13 -0
- package/src/constants/middleware.ts +83 -0
- package/src/constants/port.ts +20 -0
- package/src/constants/security.ts +78 -0
- package/src/middleware/dashboard-startup.ts +30 -18
- package/src/middleware/security.ts +18 -9
- package/src/middleware/server-startup.ts +22 -11
- package/src/middleware/swagger-middleware.ts +57 -0
- package/src/routes/health-routes.ts +1 -1
- package/src/routes/package-routes.ts +1 -1
- package/src/services/health-service.ts +103 -79
- package/src/services/package-service.ts +27 -1
- package/src/types/swagger-jsdoc.d.ts +15 -0
|
@@ -6,6 +6,15 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
6
6
|
import helmet from 'helmet';
|
|
7
7
|
import cors, { CorsOptions } from 'cors';
|
|
8
8
|
import type { MonodogConfig } from '../types/config';
|
|
9
|
+
import {
|
|
10
|
+
REQUEST_TIMEOUT,
|
|
11
|
+
RESPONSE_TIMEOUT,
|
|
12
|
+
CORS_API_METHODS,
|
|
13
|
+
CORS_ALLOWED_HEADERS,
|
|
14
|
+
DEFAULT_LOCALHOST,
|
|
15
|
+
WILDCARD_ADDRESS,
|
|
16
|
+
HTTP_PROTOCOL,
|
|
17
|
+
} from '../constants';
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Create Helmet security middleware with Content Security Policy
|
|
@@ -30,8 +39,8 @@ export function createApiCorsMiddleware(dashboardUrl: string) {
|
|
|
30
39
|
const corsOptions: CorsOptions = {
|
|
31
40
|
origin: dashboardUrl,
|
|
32
41
|
credentials: true,
|
|
33
|
-
methods: [
|
|
34
|
-
allowedHeaders: [
|
|
42
|
+
methods: [...CORS_API_METHODS],
|
|
43
|
+
allowedHeaders: [...CORS_ALLOWED_HEADERS],
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
return cors(corsOptions);
|
|
@@ -53,8 +62,8 @@ export function createDashboardCorsMiddleware() {
|
|
|
53
62
|
*/
|
|
54
63
|
export function createTimeoutMiddleware() {
|
|
55
64
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
56
|
-
req.setTimeout(
|
|
57
|
-
res.setTimeout(
|
|
65
|
+
req.setTimeout(REQUEST_TIMEOUT);
|
|
66
|
+
res.setTimeout(RESPONSE_TIMEOUT);
|
|
58
67
|
next();
|
|
59
68
|
};
|
|
60
69
|
}
|
|
@@ -66,16 +75,16 @@ export function buildApiUrl(
|
|
|
66
75
|
host: string,
|
|
67
76
|
port: number
|
|
68
77
|
): string {
|
|
69
|
-
const apiHost = host ===
|
|
70
|
-
return
|
|
78
|
+
const apiHost = host === WILDCARD_ADDRESS ? DEFAULT_LOCALHOST : host;
|
|
79
|
+
return `${HTTP_PROTOCOL}${apiHost}:${port}`;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/**
|
|
74
83
|
* Build dashboard URL based on config
|
|
75
84
|
*/
|
|
76
85
|
export function buildDashboardUrl(config: MonodogConfig): string {
|
|
77
|
-
const dashboardHost = config.dashboard.host ===
|
|
78
|
-
?
|
|
86
|
+
const dashboardHost = config.dashboard.host === WILDCARD_ADDRESS
|
|
87
|
+
? DEFAULT_LOCALHOST
|
|
79
88
|
: config.dashboard.host;
|
|
80
|
-
return
|
|
89
|
+
return `${HTTP_PROTOCOL}${dashboardHost}:${config.dashboard.port}`;
|
|
81
90
|
}
|
|
@@ -19,15 +19,23 @@ import {
|
|
|
19
19
|
buildApiUrl,
|
|
20
20
|
buildDashboardUrl,
|
|
21
21
|
} from './security';
|
|
22
|
+
import { setupSwaggerDocs } from './swagger-middleware';
|
|
22
23
|
|
|
23
24
|
import packageRouter from '../routes/package-routes';
|
|
24
25
|
import commitRouter from '../routes/commit-routes';
|
|
25
26
|
import healthRouter from '../routes/health-routes';
|
|
26
27
|
import configRouter from '../routes/config-routes';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
import {
|
|
29
|
+
PORT_MIN,
|
|
30
|
+
PORT_MAX,
|
|
31
|
+
PORT_VALIDATION_ERROR_MESSAGE,
|
|
32
|
+
BODY_PARSER_LIMIT,
|
|
33
|
+
SUCCESS_SERVER_START,
|
|
34
|
+
ERROR_PORT_IN_USE,
|
|
35
|
+
ERROR_PERMISSION_DENIED,
|
|
36
|
+
MESSAGE_GRACEFUL_SHUTDOWN,
|
|
37
|
+
MESSAGE_SERVER_CLOSED,
|
|
38
|
+
} from '../constants';
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Validate port number
|
|
@@ -36,7 +44,7 @@ function validatePort(port: string | number): number {
|
|
|
36
44
|
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
37
45
|
|
|
38
46
|
if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
|
|
39
|
-
throw new Error(
|
|
47
|
+
throw new Error(PORT_VALIDATION_ERROR_MESSAGE(PORT_MIN, PORT_MAX));
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
return portNum;
|
|
@@ -62,11 +70,14 @@ function createApp(rootPath: string): Express {
|
|
|
62
70
|
app.use(createApiCorsMiddleware(dashboardUrl));
|
|
63
71
|
|
|
64
72
|
// Body parser
|
|
65
|
-
app.use(json({ limit:
|
|
73
|
+
app.use(json({ limit: BODY_PARSER_LIMIT }));
|
|
66
74
|
|
|
67
75
|
// HTTP request logging with Morgan
|
|
68
76
|
app.use(httpLogger);
|
|
69
77
|
|
|
78
|
+
// Setup Swagger documentation
|
|
79
|
+
setupSwaggerDocs(app);
|
|
80
|
+
|
|
70
81
|
// Routes
|
|
71
82
|
app.use('/api/packages', packageRouter);
|
|
72
83
|
app.use('/api/commits/', commitRouter);
|
|
@@ -97,7 +108,7 @@ export function startServer(rootPath: string): void {
|
|
|
97
108
|
const app = createApp(rootPath);
|
|
98
109
|
|
|
99
110
|
const server = app.listen(validatedPort, host, () => {
|
|
100
|
-
console.log(
|
|
111
|
+
console.log(SUCCESS_SERVER_START(host, validatedPort));
|
|
101
112
|
AppLogger.info('API endpoints available:', {
|
|
102
113
|
endpoints: [
|
|
103
114
|
'GET /api/health',
|
|
@@ -115,10 +126,10 @@ export function startServer(rootPath: string): void {
|
|
|
115
126
|
|
|
116
127
|
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
117
128
|
if (err.code === 'EADDRINUSE') {
|
|
118
|
-
AppLogger.error(
|
|
129
|
+
AppLogger.error(ERROR_PORT_IN_USE(validatedPort), err);
|
|
119
130
|
process.exit(1);
|
|
120
131
|
} else if (err.code === 'EACCES') {
|
|
121
|
-
AppLogger.error(
|
|
132
|
+
AppLogger.error(ERROR_PERMISSION_DENIED(validatedPort), err);
|
|
122
133
|
process.exit(1);
|
|
123
134
|
} else {
|
|
124
135
|
AppLogger.error('Server failed to start:', err);
|
|
@@ -128,9 +139,9 @@ export function startServer(rootPath: string): void {
|
|
|
128
139
|
|
|
129
140
|
// Graceful shutdown
|
|
130
141
|
process.on('SIGTERM', () => {
|
|
131
|
-
AppLogger.info(
|
|
142
|
+
AppLogger.info(MESSAGE_GRACEFUL_SHUTDOWN);
|
|
132
143
|
server.close(() => {
|
|
133
|
-
AppLogger.info(
|
|
144
|
+
AppLogger.info(MESSAGE_SERVER_CLOSED);
|
|
134
145
|
process.exit(0);
|
|
135
146
|
});
|
|
136
147
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swagger Documentation Middleware
|
|
3
|
+
* Sets up Swagger UI for API documentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import swaggerUi from 'swagger-ui-express';
|
|
7
|
+
import swaggerJsDoc from 'swagger-jsdoc';
|
|
8
|
+
import type { Express, Request, Response } from 'express';
|
|
9
|
+
import { swaggerOptions } from '../config/swagger-config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Setup Swagger documentation endpoint
|
|
13
|
+
* @param app Express application instance
|
|
14
|
+
*/
|
|
15
|
+
export function setupSwaggerDocs(app: Express): void {
|
|
16
|
+
try {
|
|
17
|
+
const specs = swaggerJsDoc(swaggerOptions) as Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
// Serve raw Swagger JSON FIRST (before the middleware catches all /api-docs paths)
|
|
20
|
+
app.get('/api-docs/swagger.json', (_req: Request, res: Response) => {
|
|
21
|
+
res.setHeader('Content-Type', 'application/json');
|
|
22
|
+
res.send(specs);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Serve Swagger UI at /api-docs
|
|
26
|
+
app.use(
|
|
27
|
+
'/api-docs',
|
|
28
|
+
swaggerUi.serve,
|
|
29
|
+
swaggerUi.setup(specs, {
|
|
30
|
+
swaggerOptions: {
|
|
31
|
+
url: '/api-docs/swagger.json',
|
|
32
|
+
persistAuthorization: true,
|
|
33
|
+
displayOperationId: true,
|
|
34
|
+
filter: true,
|
|
35
|
+
showExtensions: true,
|
|
36
|
+
},
|
|
37
|
+
customCss: `
|
|
38
|
+
.swagger-ui .topbar {
|
|
39
|
+
background-color: #2c3e50;
|
|
40
|
+
}
|
|
41
|
+
.swagger-ui .info .title {
|
|
42
|
+
color: #2c3e50;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
}
|
|
45
|
+
.swagger-ui .btn-box .btn {
|
|
46
|
+
background-color: #2c3e50;
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
customCssUrl: 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.min.css',
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
console.log('Swagger documentation available at /api-docs');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Failed to setup Swagger documentation:', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
import { PackageHealthRepository, PackageRepository } from '../repositories';
|
|
13
13
|
import type { TransformedPackageHealth, HealthResponse, PackageHealthModel } from '../types/database';
|
|
14
14
|
|
|
15
|
+
// Track in-flight health refresh requests to prevent duplicates
|
|
16
|
+
let inFlightHealthRefresh: Promise<HealthResponse> | null = null;
|
|
17
|
+
|
|
15
18
|
export const getHealthSummaryService = async (): Promise<HealthResponse> => {
|
|
16
19
|
const packageHealthData = await PackageHealthRepository.findAll() as PackageHealthModel[];
|
|
17
20
|
console.log('packageHealthData -->', packageHealthData.length);
|
|
@@ -55,83 +58,104 @@ export const getHealthSummaryService = async (): Promise<HealthResponse> => {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export const healthRefreshService = async (rootDir: string) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
61
|
+
// If a health refresh is already in progress, return the in-flight promise
|
|
62
|
+
if (inFlightHealthRefresh) {
|
|
63
|
+
console.log('Health refresh already in progress, returning cached promise');
|
|
64
|
+
return inFlightHealthRefresh;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create and store the health refresh promise
|
|
68
|
+
inFlightHealthRefresh = (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const packages = scanMonorepo(rootDir);
|
|
71
|
+
console.log('packages -->', packages.length);
|
|
72
|
+
const healthMetrics = await Promise.all(
|
|
73
|
+
packages.map(async pkg => {
|
|
74
|
+
try {
|
|
75
|
+
// Await each health check function since they return promises
|
|
76
|
+
const buildStatus = await funCheckBuildStatus(pkg);
|
|
77
|
+
const testCoverage = 0; //await funCheckTestCoverage(pkg); // skip test coverage for now
|
|
78
|
+
const lintStatus = await funCheckLintStatus(pkg);
|
|
79
|
+
const securityAudit = await funCheckSecurityAudit(pkg);
|
|
80
|
+
// Calculate overall health score
|
|
81
|
+
const overallScore = calculatePackageHealth(
|
|
82
|
+
buildStatus,
|
|
83
|
+
testCoverage,
|
|
84
|
+
lintStatus,
|
|
85
|
+
securityAudit
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const health = {
|
|
89
|
+
buildStatus: buildStatus,
|
|
90
|
+
testCoverage: testCoverage,
|
|
91
|
+
lintStatus: lintStatus,
|
|
92
|
+
securityAudit: securityAudit,
|
|
93
|
+
overallScore: overallScore.overallScore,
|
|
94
|
+
};
|
|
95
|
+
const packageStatus =
|
|
96
|
+
health.overallScore >= 80
|
|
97
|
+
? 'healthy'
|
|
98
|
+
: health.overallScore >= 60 && health.overallScore < 80
|
|
99
|
+
? 'warning'
|
|
100
|
+
: 'error';
|
|
101
|
+
|
|
102
|
+
console.log(pkg.name, '-->', health, packageStatus);
|
|
103
|
+
|
|
104
|
+
await PackageHealthRepository.upsert({
|
|
105
|
+
packageName: pkg.name,
|
|
106
|
+
packageOverallScore: overallScore.overallScore,
|
|
107
|
+
packageBuildStatus: buildStatus,
|
|
108
|
+
packageTestCoverage: testCoverage,
|
|
109
|
+
packageLintStatus: lintStatus,
|
|
110
|
+
packageSecurity: securityAudit,
|
|
111
|
+
packageDependencies: '',
|
|
112
|
+
});
|
|
113
|
+
// update related package status as well
|
|
114
|
+
await PackageRepository.updateStatus(pkg.name, packageStatus);
|
|
115
|
+
return {
|
|
116
|
+
packageName: pkg.name,
|
|
117
|
+
health,
|
|
118
|
+
isHealthy: health.overallScore >= 80,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
packageName: pkg.name,
|
|
123
|
+
health: {
|
|
124
|
+
"buildStatus": "",
|
|
125
|
+
"testCoverage": 0,
|
|
126
|
+
"lintStatus": "",
|
|
127
|
+
"securityAudit": "",
|
|
128
|
+
"overallScore": 0
|
|
129
|
+
},
|
|
130
|
+
isHealthy: false,
|
|
131
|
+
error: 'Failed to fetch health metrics1',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result: HealthResponse = {
|
|
138
|
+
packages: healthMetrics.filter(h => !h.error),
|
|
139
|
+
summary: {
|
|
140
|
+
total: packages.length,
|
|
141
|
+
healthy: healthMetrics.filter(h => h.isHealthy).length,
|
|
142
|
+
unhealthy: healthMetrics.filter(h => !h.isHealthy).length,
|
|
143
|
+
averageScore:
|
|
144
|
+
healthMetrics.filter(h => h.health).length > 0
|
|
145
|
+
? healthMetrics
|
|
146
|
+
.filter(h => h.health)
|
|
147
|
+
.reduce((sum, h) => sum + h.health!.overallScore, 0) /
|
|
148
|
+
healthMetrics.filter(h => h.health).length
|
|
149
|
+
: 0,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
} finally {
|
|
155
|
+
// Clear the in-flight promise after completion
|
|
156
|
+
inFlightHealthRefresh = null;
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
|
|
160
|
+
return inFlightHealthRefresh;
|
|
137
161
|
}
|
|
@@ -160,7 +160,33 @@ export const refreshPackagesService = async (rootPath: string) => {
|
|
|
160
160
|
await storePackage(pkg);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
// Return transformed packages like getPackagesService
|
|
164
|
+
const dbPackages = await PackageRepository.findAll();
|
|
165
|
+
const transformedPackages = dbPackages.map((pkg: PackageModel) => {
|
|
166
|
+
const transformedPkg = { ...pkg };
|
|
167
|
+
|
|
168
|
+
transformedPkg.maintainers = pkg.maintainers
|
|
169
|
+
? JSON.parse(pkg.maintainers)
|
|
170
|
+
: [];
|
|
171
|
+
|
|
172
|
+
transformedPkg.scripts = pkg.scripts ? JSON.parse(pkg.scripts) : {};
|
|
173
|
+
transformedPkg.repository = pkg.repository
|
|
174
|
+
? JSON.parse(pkg.repository)
|
|
175
|
+
: {};
|
|
176
|
+
|
|
177
|
+
transformedPkg.dependencies = pkg.dependencies
|
|
178
|
+
? JSON.parse(pkg.dependencies)
|
|
179
|
+
: [];
|
|
180
|
+
transformedPkg.devDependencies = pkg.devDependencies
|
|
181
|
+
? JSON.parse(pkg.devDependencies)
|
|
182
|
+
: [];
|
|
183
|
+
transformedPkg.peerDependencies = pkg.peerDependencies
|
|
184
|
+
? JSON.parse(pkg.peerDependencies)
|
|
185
|
+
: [];
|
|
186
|
+
return transformedPkg;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return transformedPackages;
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
export const getPackageDetailService = async (name: string) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for swagger-jsdoc
|
|
3
|
+
* Provides TypeScript support for swagger-jsdoc module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module 'swagger-jsdoc' {
|
|
7
|
+
interface SwaggerOptions {
|
|
8
|
+
definition?: Record<string, unknown>;
|
|
9
|
+
apis?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function swaggerJsDoc(options: SwaggerOptions): Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export = swaggerJsDoc;
|
|
15
|
+
}
|