Skip to content

Commit

Permalink
feat: Improve error handling in mongoose init on startup (#264)
Browse files Browse the repository at this point in the history
* Throw error for missing `admin` db configuration.
* Improve error logging for failed model initialization.
* Add option to not fail on `IndexOptionsConflict` errors.
* Exit process on startup errors.
  • Loading branch information
jrassa authored Aug 22, 2023
1 parent 318edb8 commit 2447cf9
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 79 deletions.
1 change: 1 addition & 0 deletions config/env/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ module.exports = {
db: {
admin: 'mongodb://localhost/node-rest-starter-dev'
},
mongooseFailOnIndexOptionsConflict: true,

/**
* Environment Settings
Expand Down
141 changes: 81 additions & 60 deletions src/lib/mongoose.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import path from 'path';

import _ from 'lodash';
import mongoose, { Connection, ConnectOptions, Mongoose } from 'mongoose';
import mongoose, {
Connection,
ConnectOptions,
Mongoose,
Model
} from 'mongoose';

import { logger } from './bunyan';
import config from '../config';

// Set the mongoose debugging option based on the configuration, defaulting to false
const mongooseDebug = config.mongooseLogging ?? false;

logger.info(`Mongoose: Setting debug to ${mongooseDebug}`);
mongoose.set('debug', mongooseDebug);
type MongooseDbSpec = {
name: string;
connectionString: string;
options: ConnectOptions;
};

// Load the mongoose models
export const loadModels = async () => {
Expand Down Expand Up @@ -57,12 +62,14 @@ export const dbs: Record<string, Connection | Mongoose> = {};

// Initialize Mongoose, returns a promise
export const connect = async () => {
const dbSpecs: Array<{
name: string;
connectionString: string;
options: ConnectOptions;
}> = [];
let defaultDbSpec;
// Set the mongoose debugging option based on the configuration, defaulting to false
const mongooseDebug = config.mongooseLogging ?? false;

logger.info(`Mongoose: Setting debug to ${mongooseDebug}`);
mongoose.set('debug', mongooseDebug);

const dbSpecs: Array<MongooseDbSpec> = [];
let defaultDbSpec: MongooseDbSpec;

// Organize the dbs we need to connect
for (const dbSpec in config.db) {
Expand All @@ -73,55 +80,52 @@ export const connect = async () => {
}
}

// Check for required admin db config
if (!defaultDbSpec) {
throw Error('Required `admin` db not configured');
}

// Connect to the default db to kick off the process
if (defaultDbSpec) {
try {
await mongoose.connect(
defaultDbSpec.connectionString,
defaultDbSpec.options
);

logger.info(`Mongoose: Connected to "${defaultDbSpec.name}" default db`);

// store it in the db list
dbs[defaultDbSpec.name] = mongoose;

// Connect to the rest of the dbs
await Promise.all(
dbSpecs.map(async (spec) => {
// Create the secondary connection
dbs[spec.name] = await mongoose
.createConnection(spec.connectionString, spec.options)
.asPromise();
logger.info(`Mongoose: Connected to "${spec.name}" db`);
})
);

logger.debug('Loading mongoose models...');
// Since all the db connections worked, we will load the mongoose models
await loadModels();
logger.debug('Loaded all mongoose models!');

// Ensure that all mongoose models are initialized
// before responding with the connections(s)
await Promise.all(
Object.entries(dbs).map(([key, conn]) => {
logger.debug(`Initializing all models for ${key}`);
return Promise.all(
Object.entries(conn.models).map(([name, aModel]) => {
logger.debug(`Initializing model ${name}`);
return aModel.init();
})
);
})
);

// Return the dbs since everything succeeded
return dbs;
} catch (err) {
logger.fatal('Mongoose: Could not connect to admin db');
throw err;
}
try {
dbs[defaultDbSpec.name] = await mongoose.connect(
defaultDbSpec.connectionString,
defaultDbSpec.options
);

logger.info(`Mongoose: Connected to "${defaultDbSpec.name}" default db`);

// Connect to the rest of the dbs
await Promise.all(
dbSpecs.map(async (spec: MongooseDbSpec) => {
// Create the secondary connection
dbs[spec.name] = await mongoose
.createConnection(spec.connectionString, spec.options)
.asPromise();
logger.info(`Mongoose: Connected to "${spec.name}" db`);
})
);

// Since all the db connections worked, we will load the mongoose models
logger.debug('Mongoose: Loading mongoose models...');
await loadModels();
logger.debug('Mongoose: Loaded all mongoose models!');

// Ensure that all mongoose models are initialized
// before responding with the connections(s)
await Promise.all(
Object.entries(dbs).flatMap(([key, conn]) => {
logger.debug(`Mongoose: Initializing all models for "${key}" db`);
return Object.entries(conn.models).map(([name, aModel]) =>
initializeModel(name, aModel)
);
})
);

// Return the dbs since everything succeeded
return dbs;
} catch (err) {
logger.fatal('Mongoose: Could not connect to admin db');
throw err;
}
};

Expand All @@ -139,6 +143,23 @@ export const disconnect = () => {
return Promise.all(promises);
};

async function initializeModel(name: string, model: Model<unknown>) {
logger.debug(`Mongoose: Initializing model ${name}`);
try {
return await model.init();
} catch (err) {
logger.error(
`Mongoose: Error creating index for ${name}: ${err.codeName} - ${err.message}`
);
if (
config.mongooseFailOnIndexOptionsConflict ||
err.codeName !== 'IndexOptionsConflict'
) {
throw err;
}
}
}

function isMongoose(connection: Mongoose | Connection): connection is Mongoose {
return (connection as Mongoose).disconnect !== undefined;
}
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ startupFn()
})
.catch((error) => {
logger.fatal(error, 'Startup initialization failed.');
// non-zero exit code to let the process know that we've failed
process.exit(1);
});
30 changes: 11 additions & 19 deletions src/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,21 @@ import socketio from './lib/socket.io';
export default async function () {
logger.info('Starting initialization of Node.js server');

// Init mongoose connection(s)
const db = await mongoose.connect();

try {
logger.info(
'Mongoose connected, proceeding with application configuration'
);
// Init agenda.ts scheduler
await agenda.init();

// Init agenda.ts scheduler
await agenda.init();
// Initialize express
const app = await express.init(db.admin as Mongoose);

// Initialize express
const app = await express.init(db.admin as Mongoose);
// Create a new HTTP server
logger.info('Creating HTTP Server');
const server = http.createServer(app);

// Create a new HTTP server
logger.info('Creating HTTP Server');
const server = http.createServer(app);
// Initialize socket.io
await socketio.init(server, db.admin as Mongoose);

// Initialize socket.io
await socketio.init(server, db.admin as Mongoose);

return server;
} catch (err) {
logger.fatal('Express initialization failed.');
return Promise.reject(err);
}
return server;
}

0 comments on commit 2447cf9

Please sign in to comment.