import mongoose, { type ClientSession } from "mongoose"; interface MongooseCache { conn: typeof mongoose | null; promise: Promise | null; } declare global { var mongoose: MongooseCache | undefined; } let cached = global.mongoose; if (!cached) { cached = global.mongoose = { conn: null, promise: null }; } /** * Mongoose 9 propagates the active session through AsyncLocalStorage, so * queries inside withTx() pick it up even if a `{ session }` option is missed. * The DAL still passes it explicitly — this is a safety net, not the mechanism. * * Must be set before connecting. */ mongoose.set("transactionAsyncLocalStorage", true); /** * autoIndex creates MISSING indexes only — it never alters an existing * definition, so editing an index in src/models/ is not enough. Run * `npm run sync-indexes` after any index change. Off in production, where * building an index on a live collection is a deliberate operation. */ mongoose.set("autoIndex", process.env.NODE_ENV !== "production"); export async function connectToDatabase(): Promise { const MONGO_URI = process.env.MONGODB_URI || process.env.MONGO_URI; if (!MONGO_URI) { throw new Error( "Please define MONGO_URI or MONGODB_URI in your .env.local file" ); } if (cached!.conn) { return cached!.conn; } if (!cached!.promise) { cached!.promise = mongoose .connect(MONGO_URI, { dbName: "sihjantaparty" }) .then((mongooseInstance) => { return mongooseInstance; }); } try { cached!.conn = await cached!.promise; } catch (e) { cached!.promise = null; throw e; } return cached!.conn; } /* ------------------------------------------------------------------ */ /* Transactions */ /* ------------------------------------------------------------------ */ /** MongoDB's error codeName when the deployment has no replica set. */ function isNoTransactionSupport(e: unknown): boolean { const msg = e instanceof Error ? e.message : String(e); return ( msg.includes("Transaction numbers are only allowed on a replica set") || msg.includes("Transactions are not supported") || msg.includes("IllegalOperation") ); } /** * Runs `fn` inside a MongoDB transaction, retrying transient errors (mongoose's * connection.transaction() handles TransientTransactionError and * UnknownTransactionCommitResult for us). * * WHY THIS WRAPPER EXISTS — read before "fixing" a transaction error locally. * * Transactions require a replica set. Atlas is one; a bare local `mongod` is * not, and it fails with "Transaction numbers are only allowed on a replica set * member or mongos". The tempting five-second fix is to delete the transaction * wrapper to unblock local work — which silently ships the non-atomic version, * whose failure modes (a 7-member team, one user on two teams) are * unrecoverable data corruption discovered mid-hackathon. * * So this fails loudly with instructions instead of degrading. There is no * fallback path on purpose. To develop locally, run a single-node replica set: * * mongod --replSet rs0 --dbpath ./data * mongosh --eval "rs.initiate()" * * or point MONGO_URI at Atlas. */ export async function withTx( fn: (session: ClientSession) => Promise ): Promise { const conn = await connectToDatabase(); try { return await conn.connection.transaction(fn); } catch (e) { if (isNoTransactionSupport(e)) { throw new Error( "This write needs a MongoDB transaction, but the current deployment " + "is a standalone server. Transactions require a replica set.\n\n" + " Local: mongod --replSet rs0 --dbpath ./data\n" + ' mongosh --eval "rs.initiate()"\n' + " Or: point MONGO_URI at your Atlas cluster.\n\n" + "Do NOT remove the transaction to work around this — the writes it " + "guards are what stop a team reaching 7 members or a user joining " + "two teams at once.", { cause: e } ); } throw e; } } export default connectToDatabase;