@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.
@@ -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: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
34
- allowedHeaders: ['Content-Type', 'Authorization'],
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(30000);
57
- res.setTimeout(30000);
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 === '0.0.0.0' ? 'localhost' : host;
70
- return `http://${apiHost}:${port}`;
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 === '0.0.0.0'
78
- ? 'localhost'
86
+ const dashboardHost = config.dashboard.host === WILDCARD_ADDRESS
87
+ ? DEFAULT_LOCALHOST
79
88
  : config.dashboard.host;
80
- return `http://${dashboardHost}:${config.dashboard.port}`;
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
- // Security constants
29
- const PORT_MIN = 1024;
30
- const PORT_MAX = 65535;
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(`Port must be between ${PORT_MIN} and ${PORT_MAX}`);
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: '1mb' }));
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(`Backend server listening on http://${host}:${validatedPort}`);
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(`Port ${validatedPort} is already in use. Please specify a different port.`, err);
129
+ AppLogger.error(ERROR_PORT_IN_USE(validatedPort), err);
119
130
  process.exit(1);
120
131
  } else if (err.code === 'EACCES') {
121
- AppLogger.error(`Permission denied to listen on port ${validatedPort}. Use a port above 1024.`, err);
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('SIGTERM signal received: closing HTTP server');
142
+ AppLogger.info(MESSAGE_GRACEFUL_SHUTDOWN);
132
143
  server.close(() => {
133
- AppLogger.info('HTTP server closed');
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
+ }
@@ -5,7 +5,7 @@ const healthRouter = express.Router();
5
5
 
6
6
  healthRouter
7
7
  .route('/refresh')
8
- .get(refreshHealth);
8
+ .post(refreshHealth);
9
9
 
10
10
  healthRouter
11
11
  .route('/packages')
@@ -5,7 +5,7 @@ const packageRouter = express.Router();
5
5
 
6
6
  packageRouter
7
7
  .route('/refresh')
8
- .get(refreshPackages);
8
+ .post(refreshPackages);
9
9
 
10
10
  packageRouter
11
11
  .route('/update-config')
@@ -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
- const packages = scanMonorepo(rootDir);
59
- console.log('packages -->', packages.length);
60
- const healthMetrics = await Promise.all(
61
- packages.map(async pkg => {
62
- try {
63
- // Await each health check function since they return promises
64
- const buildStatus = await funCheckBuildStatus(pkg);
65
- const testCoverage = 0; //await funCheckTestCoverage(pkg); // skip test coverage for now
66
- const lintStatus = await funCheckLintStatus(pkg);
67
- const securityAudit = await funCheckSecurityAudit(pkg);
68
- // Calculate overall health score
69
- const overallScore = calculatePackageHealth(
70
- buildStatus,
71
- testCoverage,
72
- lintStatus,
73
- securityAudit
74
- );
75
-
76
- const health = {
77
- buildStatus: buildStatus,
78
- testCoverage: testCoverage,
79
- lintStatus: lintStatus,
80
- securityAudit: securityAudit,
81
- overallScore: overallScore.overallScore,
82
- };
83
- const packageStatus =
84
- health.overallScore >= 80
85
- ? 'healthy'
86
- : health.overallScore >= 60 && health.overallScore < 80
87
- ? 'warning'
88
- : 'error';
89
-
90
- console.log(pkg.name, '-->', health, packageStatus);
91
-
92
- await PackageHealthRepository.upsert({
93
- packageName: pkg.name,
94
- packageOverallScore: overallScore.overallScore,
95
- packageBuildStatus: buildStatus,
96
- packageTestCoverage: testCoverage,
97
- packageLintStatus: lintStatus,
98
- packageSecurity: securityAudit,
99
- packageDependencies: '',
100
- });
101
- // update related package status as well
102
- await PackageRepository.updateStatus(pkg.name, packageStatus);
103
- return {
104
- packageName: pkg.name,
105
- health,
106
- isHealthy: health.overallScore >= 80,
107
- };
108
- } catch (error) {
109
- return {
110
- packageName: pkg.name,
111
- health: {
112
- "buildStatus": "",
113
- "testCoverage": 0,
114
- "lintStatus": "",
115
- "securityAudit": "",
116
- "overallScore": 0
117
- },
118
- isHealthy: false,
119
- error: 'Failed to fetch health metrics1',
120
- };
121
- }
122
- })
123
- );
124
- return {
125
- packages: healthMetrics.filter(h => !h.error),
126
- summary: {
127
- total: packages.length,
128
- healthy: healthMetrics.filter(h => h.isHealthy).length,
129
- unhealthy: healthMetrics.filter(h => !h.isHealthy).length,
130
- averageScore:
131
- healthMetrics
132
- .filter(h => h.health)
133
- .reduce((sum, h) => sum + h.health!.overallScore, 0) /
134
- healthMetrics.filter(h => h.health).length,
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
- return packages;
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
+ }