diff --git a/src/api/middlewares/mongoConnection.ts b/src/api/middlewares/mongoConnection.ts index 40c161c8..f552e7a0 100644 --- a/src/api/middlewares/mongoConnection.ts +++ b/src/api/middlewares/mongoConnection.ts @@ -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 { @@ -20,27 +21,57 @@ 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 | 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 @@ -48,13 +79,19 @@ const mongoConnectionMiddleware = () => { 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"); } diff --git a/src/lib/debuggers.ts b/src/lib/debuggers.ts new file mode 100644 index 00000000..dea70437 --- /dev/null +++ b/src/lib/debuggers.ts @@ -0,0 +1,2 @@ +import debug from "debug"; +export const debugMongo = debug("vn:mongo"); diff --git a/src/pages/api/graphql.ts b/src/pages/api/graphql.ts index 92f99c22..13ae27a7 100644 --- a/src/pages/api/graphql.ts +++ b/src/pages/api/graphql.ts @@ -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 @@ -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({ @@ -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" }); @@ -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); + }); +} diff --git a/tests/vns/mongoDocker.test.ts b/tests/vns/mongoDocker.test.ts index 8413a958..0a0c80ff 100644 --- a/tests/vns/mongoDocker.test.ts +++ b/tests/vns/mongoDocker.test.ts @@ -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) {