-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnotes-api-server.js
More file actions
322 lines (284 loc) · 12 KB
/
Copy pathnotes-api-server.js
File metadata and controls
322 lines (284 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import {fileURLToPath} from 'url';
import {CouchDbNoteRepository} from './db/couchdb-note-repository.js';
import {MongoDbNoteRepository} from './db/mongodb-note-repository.js';
import {createNotesRouter} from './routes/notes-routes.js';
import { createLogger } from './logger.js';
// Load environment variables
dotenv.config();
// Get configuration from environment variables
export const HOST = process.env.HOST || '0.0.0.0';
export const PORT = process.env.PORT || 3000;
export const DB_VENDOR = process.env.DB_VENDOR || 'couchdb';
// CouchDB configuration
export const COUCHDB_URL = process.env.COUCHDB_URL || 'http://admin:password@localhost:5984';
export const COUCHDB_DB_NAME = process.env.COUCHDB_DB_NAME || 'notes_db';
// MongoDB configuration
export const MONGODB_URL = process.env.MONGODB_URL || 'mongodb://localhost:27017';
export const MONGODB_DB_NAME = process.env.MONGODB_DB_NAME || 'notes_db';
// Get the directory name
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const log = createLogger('NotesServer');
/**
* NotesServer class encapsulates server lifecycle and eliminates module-level state.
* Provides a clean, testable interface for the Notes API server with support for
* both CouchDB and MongoDB backends.
*
* @class
* @example
* const server = new NotesServer();
* await server.initializeApp();
* server.startServer();
*/
export class NotesServer {
/**
* Create a new NotesServer instance
* @constructor
* @returns {NotesServer} New NotesServer instance
* @example
* const server = new NotesServer();
*/
constructor() {
/**
* HTTP server instance
* @type {?http.Server}
* @private
*/
this.server = null;
/**
* Express application instance
* @type {express.Application}
*/
this.app = express();
}
/**
* Create the appropriate repository based on the DB_VENDOR environment variable.
* Supports both CouchDB and MongoDB implementations.
*
* @returns {CouchDbNoteRepository|MongoDbNoteRepository} The configured repository instance
* @throws {Error} When an unsupported database vendor is specified
* @example
* // With DB_VENDOR='mongodb'
* const repo = server.createNoteRepository(); // Returns MongoDbNoteRepository
*
* // With DB_VENDOR='couchdb' (default)
* const repo = server.createNoteRepository(); // Returns CouchDbNoteRepository
*/
createNoteRepository() {
// Use current environment variable value, not cached constant
const dbVendor = process.env.DB_VENDOR || 'couchdb';
if (dbVendor === 'mongodb') {
log.info('Using MongoDB as the database vendor');
return new MongoDbNoteRepository(MONGODB_URL, MONGODB_DB_NAME);
} else {
log.info('Using CouchDB as the database vendor');
return new CouchDbNoteRepository(COUCHDB_URL, COUCHDB_DB_NAME);
}
}
/**
* Graceful shutdown function that handles cleanup and ensures all connections are closed properly.
* Implements a timeout mechanism to force shutdown if graceful shutdown takes too long.
*
* @param {number} [timeout=10000] - Timeout in milliseconds before forcing shutdown
* @returns {void}
* @example
* // Manual shutdown
* server.gracefulShutdown();
*
* // With custom timeout
* server.gracefulShutdown(5000); // 5 second timeout
*/
gracefulShutdown(timeout = 10000) {
log.info('Shutting down gracefully...');
if (this.server) {
// Store timeout ID so we can clear it if shutdown completes
const forceShutdownTimeout = setTimeout(
// istanbul ignore next: timing-based forced shutdown path is CLI-only and hard to simulate deterministically
() => {
log.error('Could not close connections in time, forcefully shutting down');
process.exit(1);
}, timeout);
this.server.close(() => {
log.info('HTTP server closed');
// Clear the force shutdown timeout since we completed gracefully
clearTimeout(forceShutdownTimeout);
// Close database connections, etc.
process.exit(0);
});
} else {
process.exit(0);
}
}
/**
* Initialize the Express application with middleware, routes, and error handling.
* Sets up the complete application stack including security, CORS, static files,
* and API routes.
*
* @param {?NoteRepository} [noteRepository=null] - Optional repository instance for dependency injection (useful for testing)
* @returns {Promise<{app: express.Application, repository: NoteRepository}>} Initialized app and repository
* @throws {Error} When repository initialization fails
* @example
* // Default initialization
* const { app, repository } = await server.initializeApp();
*
* // With custom repository (for testing)
* const mockRepo = new MockNoteRepository();
* const { app, repository } = await server.initializeApp(mockRepo);
*/
async initializeApp(noteRepository = null) {
try {
// Clear any existing middleware/routes for testing
this.app._router = undefined;
// Security middleware
this.app.use(helmet());
// CORS middleware for cross-origin requests
this.app.use(cors());
// Middleware for parsing JSON bodies
this.app.use(express.json());
// Serve static files from the public directory
this.app.use(express.static(path.join(__dirname, 'public')));
// Use provided repository or create default one
const repository = noteRepository || this.createNoteRepository();
// Initialize the repository
await repository.init();
log.info('Repository initialized successfully');
// Create and mount the notes router
const notesRouter = createNotesRouter(repository);
this.app.use('/api/notes', notesRouter);
// Add a simple health check endpoint
this.app.get('/health', (req, res) => {
res.status(200).json({status: 'ok'});
});
// Serve the index.html file for the root route
this.app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// 404 handler for undefined routes
this.app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// Error handling middleware
this.app.use((err, req, res, next) => {
log.error('Unhandled error:', err);
// Handle JSON parsing errors
if (err.type === 'entity.parse.failed') {
return res.status(400).json({ error: 'Invalid JSON' });
}
// Default to 500 for other errors
res.status(500).json({error: 'Internal server error'});
});
return { app: this.app, repository };
} catch (error) {
log.error('Failed to initialize application:', error);
throw error;
}
}
/**
* Start the HTTP server and set up signal handlers for graceful shutdown.
* The server will listen on the configured HOST and PORT.
*
* @returns {http.Server} The started HTTP server instance
* @throws {Error} When server fails to start
* @example
* const server = new NotesServer();
* await server.initializeApp();
* const httpServer = server.startServer();
* console.log('Server started successfully');
*/
startServer() {
// Convert PORT to number to ensure correct type for the test
const port = parseInt(PORT, 10);
this.server = this.app.listen(port, HOST, () => {
log.info(`Notes API server is running at http://${HOST}:${port}`);
log.info('Available endpoints:');
log.info(' GET / - Web UI for notes management');
log.info(' GET /api/notes - Get all notes');
log.info(' GET /api/notes/:id - Get a note by ID');
log.info(' POST /api/notes - Create a new note');
log.info(' PUT /api/notes/:id - Update a note');
log.info(' DELETE /api/notes/:id - Delete a note');
log.info(' GET /health - Health check');
log.info('\nOpen your browser at http://localhost:' + port + ' to use the Notes UI');
});
// Handle graceful shutdown
process.on('SIGTERM', () => this.gracefulShutdown());
process.on('SIGINT', () => this.gracefulShutdown());
return this.server;
}
}
// Create global instance for backward compatibility
const globalNotesServer = new NotesServer();
/*
* Convenience exports for backward compatibility
* These provide direct access to a global NotesServer instance.
* For better testability and control, use the NotesServer class directly.
*/
/**
* Global Express application instance for backward compatibility
* @type {express.Application}
* @deprecated Use NotesServer class for better control
* @example
* import { app } from './notes-api-server.js';
* // Note: Prefer using NotesServer class directly
*/
export const app = globalNotesServer.app;
/**
* Create repository instance using global server (backward compatibility)
* @returns {CouchDbNoteRepository|MongoDbNoteRepository} The configured repository instance
* @deprecated Use NotesServer.createNoteRepository() for better testability
* @example
* import { createNoteRepository } from './notes-api-server.js';
* const repo = createNoteRepository();
*/
export const createNoteRepository = () => globalNotesServer.createNoteRepository();
/**
* Graceful shutdown using global server (backward compatibility)
* @param {number} [timeout=10000] - Timeout in milliseconds before forcing shutdown
* @returns {void}
* @deprecated Use NotesServer.gracefulShutdown() for better control
* @example
* import { gracefulShutdown } from './notes-api-server.js';
* gracefulShutdown();
*/
export const gracefulShutdown = () => globalNotesServer.gracefulShutdown();
/**
* Initialize application using global server (backward compatibility)
* @param {?NoteRepository} [noteRepository=null] - Optional repository instance for dependency injection
* @returns {Promise<{app: express.Application, repository: NoteRepository}>} Initialized app and repository
* @deprecated Use NotesServer.initializeApp() for better testability
* @example
* import { initializeApp } from './notes-api-server.js';
* const { app, repository } = await initializeApp();
*/
export const initializeApp = (noteRepository = null) => globalNotesServer.initializeApp(noteRepository);
/**
* Start server using global server instance (backward compatibility)
* @returns {http.Server} The started HTTP server instance
* @deprecated Use NotesServer.startServer() for better control
* @example
* import { startServer } from './notes-api-server.js';
* const server = startServer();
*/
export const startServer = () => globalNotesServer.startServer();
// Exported entrypoint function for testability and CLI usage
export async function mainEntry() {
const notesServer = new NotesServer();
try {
await notesServer.initializeApp();
notesServer.startServer();
} catch (error) {
log.error('Application startup failed:', error);
process.exit(1);
}
}
// Only start the server if this file is run directly (not imported)
/* istanbul ignore next: CLI-only startup path not executed in tests */
if (import.meta.url === `file://${process.argv[1]}`) {
// Use void to avoid unhandled promise warnings
void mainEntry();
}