File size: 4,184 Bytes
78664f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import mongoose, { type ClientSession } from "mongoose";

interface MongooseCache {
  conn: typeof mongoose | null;
  promise: Promise<typeof mongoose> | 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<typeof mongoose> {
  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<T>(
  fn: (session: ClientSession) => Promise<T>
): Promise<T> {
  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;