Skip to content

Commit

Permalink
More graceful exit when localdatabase is not reachable
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-burel committed Mar 30, 2021
1 parent 0d1d0be commit a0af190
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 42 deletions.
69 changes: 53 additions & 16 deletions src/api/middlewares/mongoConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
*/

import { Request, Response } from "express";
import mongoose from "mongoose";
import mongoose, { ConnectOptions } from "mongoose";
// Import mongoose models here
import "~/api/mongoose/models";
import { debugMongo } from "~/lib/debuggers";

export async function closeDbConnection() {
try {
Expand All @@ -20,41 +21,77 @@ export async function closeDbConnection() {
}
}

import debug from "debug";
const debugMongo = debug("vns:mongo");

// trigger the initial connection on app startup
export const connectToDb = async (mongoUri: string) => {
// Based on https://github.com/vercel/next.js/blob/canary/examples/with-mongodb/util/mongodb.js
// We need to globally cache Mongoose connection promise so that it's reused by all calls to connectToDb
// => this avoid unexpectedly creating multiple connections + the promise is shared so .then/.catch are called as expected
interface MongooseCache {
connectPromise: Promise<any> | null;
}
interface GlobalWithMongoose extends NodeJS.Global {
mongooseCache: MongooseCache | undefined;
}
const globalNode: GlobalWithMongoose = {
mongooseCache: undefined,
...global,
};
let mongooseCache = globalNode.mongooseCache; // shared promise, so "then" chains are called correctly for all code trying to connect (avoids race conditions)
if (!mongooseCache) {
globalNode.mongooseCache = { connectPromise: null };
mongooseCache = globalNode.mongooseCache;
}
export const connectToDb = async (
mongoUri: string,
options?: ConnectOptions
) => {
if (mongooseCache?.connectPromise) await mongooseCache.connectPromise;
if (![1, 2].includes(mongoose.connection.readyState)) {
debugMongo("Call mongoose connect");
return await mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
(mongooseCache as MongooseCache).connectPromise = mongoose.connect(
mongoUri,
{
useNewUrlParser: true,
useUnifiedTopology: true,
...(options || {}),
}
);
}
debugMongo("Ran connectToDb, already connected or connecting to Mongo");
return false;
};

const mongoConnectionMiddleware = () => {
const mongoUri = process.env.MONGO_URI;
if (!mongoUri) throw new Error("MONGO_URI env variable is not defined");
const mongoConnectionMiddleware = (mongoUri: string) => {
// init the first database connection on server startup
connectToDb(mongoUri);
const isLocalMongo = mongoUri.match(/localhost/);
connectToDb(mongoUri, {
serverSelectionTimeoutMS: isLocalMongo ? 3000 : undefined,
}).catch((err) => {
console.error(
`\nCould not connect to Mongo database on URI ${mongoUri} during route initialization.`
);
if (isLocalMongo) {
console.error("Did you forget to run 'yarn run start:mongo'?\n");
}
console.error(err);
});
// mongoose.set("useFindAndModify", false);

// then return a middleware that checks the connection on every call
return async (req: Request, res: Response, next) => {
try {
// To debug the number of connections in Mongo client: db.serverStatus().connections
await connectToDb(mongoUri);

// Do not forget to close connection on finish and close events
// NOTE: actually we don't need this. Db connection close should happen on lambda destruction instead.
// res.on("finish", closeDbConnection);
// res.on("close", closeDbConnection);
next();
} catch (err) {
console.error(
`\nCould not connect to Mongo database on URI ${mongoUri} during request.`
);
if (isLocalMongo) {
console.error("Did you forget to run 'yarn run start:mongo'?\n");
}
console.error(err);
res.status(500);
res.send("Could not connect to db");
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/debuggers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import debug from "debug";
export const debugMongo = debug("vn:mongo");
74 changes: 50 additions & 24 deletions src/pages/api/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { ApolloServer, gql } from "apollo-server-express";
import { makeExecutableSchema, mergeSchemas } from "graphql-tools";
import { buildApolloSchema } from "@vulcanjs/graphql";

import mongoConnection from "~/api/middlewares/mongoConnection";
import mongoConnection, {
connectToDb,
} from "~/api/middlewares/mongoConnection";
import corsOptions from "~/api/cors";
import { contextBase, contextFromReq } from "~/api/context";
import seedDatabase from "~/api/seed";
import models from "~/models";
import { debugMongo } from "~/lib/debuggers";

/**
* Example graphQL schema and resolvers generated using Vulcan declarative approach
Expand Down Expand Up @@ -53,28 +56,9 @@ const customSchema = makeExecutableSchema({ typeDefs, resolvers });
// NOTE: schema stitching can cause a bad developer experience with errors
const mergedSchema = mergeSchemas({ schemas: [vulcanSchema, customSchema] });

// Seed
// TODO: what is the best pattern to seed in a serverless context?
// We pass the default graphql context to the seed function,
// so it can access our models
seedDatabase(contextBase);
// also seed restaurant manually to demo a custom server
const seedRestaurants = async () => {
const db = mongoose.connection;
const count = await db.collection("restaurants").countDocuments();
if (count === 0) {
db.collection("restaurants").insertMany([
{
name: "The Restaurant at the End of the Universe",
},
{ name: "The Last Supper" },
{ name: "Shoney's" },
{ name: "Big Bang Burger" },
{ name: "Fancy Eats" },
]);
}
};
seedRestaurants();
const mongoUri = process.env.MONGO_URI;
if (!mongoUri) throw new Error("MONGO_URI env variable is not defined");
const isLocalMongo = mongoUri.match(/localhost/);

// Define the server (using Express for easier middleware usage)
const server = new ApolloServer({
Expand All @@ -99,7 +83,7 @@ const gqlPath = "/api/graphql";
// setup cors
app.use(gqlPath, cors(corsOptions));
// init the db
app.use(gqlPath, mongoConnection());
app.use(gqlPath, mongoConnection(mongoUri));

server.applyMiddleware({ app, path: "/api/graphql" });

Expand All @@ -110,3 +94,45 @@ export const config = {
bodyParser: false,
},
};

// Seed in development
// In production, we expect you to seed the database manually
if (process.env.NODE_ENV === "development") {
connectToDb(mongoUri, {
serverSelectionTimeoutMS: isLocalMongo ? 3000 : undefined,
}) // fail the seed early during development
.then(() => {
debugMongo("Connected to db, seeding admin and restaurants");
// TODO: what is the best pattern to seed in a serverless context?
// We pass the default graphql context to the seed function,
// so it can access our models
seedDatabase(contextBase);
// also seed restaurant manually to demo a custom server
const seedRestaurants = async () => {
const db = mongoose.connection;
const count = await db.collection("restaurants").countDocuments();
if (count === 0) {
db.collection("restaurants").insertMany([
{
name: "The Restaurant at the End of the Universe",
},
{ name: "The Last Supper" },
{ name: "Shoney's" },
{ name: "Big Bang Burger" },
{ name: "Fancy Eats" },
]);
}
};
seedRestaurants();
})
.catch((err) => {
console.error(
`\nCould not connect to Mongo database on URI ${mongoUri} during seed step.`
);
if (isLocalMongo) {
console.error("Did you forget to run 'yarn run start:mongo'?\n");
}
console.error(err);
process.exit(1);
});
}
3 changes: 1 addition & 2 deletions tests/vns/mongoDocker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import {
connectToDb,
closeDbConnection,
} from "../../src/api/middlewares/mongoConnection";
import { debugMongo } from "../../src/lib/debuggers";
import { spawn } from "child_process";
import debug from "debug";
const debugMongo = debug("vn:mongo");
// TODO: setup dotenv like in Next
// @see https://github.com/VulcanJS/vulcan-next-starter/issues/47
if (!process.env.MONGO_URI) {
Expand Down

0 comments on commit a0af190

Please sign in to comment.