Skip to content

Commit 16f5274

Browse files
fix(adapter-mongodb): Improve MongoDB connection handling for serverless environments (#9459)
Co-authored-by: Nico Domino <[email protected]>
1 parent f62ece2 commit 16f5274

File tree

2 files changed

+147
-40
lines changed

2 files changed

+147
-40
lines changed

packages/adapter-mongodb/src/index.ts

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ import type {
2525
} from "@auth/core/adapters"
2626
import type { MongoClient } from "mongodb"
2727

28+
/**
29+
* This adapter uses https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management.
30+
* This feature is very new and requires runtime polyfills for `Symbol.asyncDispose` in order to work properly in all environments.
31+
* It is also required to set in the `tsconfig.json` file the compilation target to `es2022` or below and configure the `lib` option to include `esnext` or `esnext.disposable`.
32+
*
33+
* You can find more information about this feature and the polyfills in the link above.
34+
*/
35+
// @ts-expect-error read only property is not assignable
36+
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose")
37+
2838
/** This is the interface of the MongoDB adapter options. */
2939
export interface MongoDBAdapterOptions {
3040
/**
@@ -40,6 +50,14 @@ export interface MongoDBAdapterOptions {
4050
* The name you want to give to the MongoDB database
4151
*/
4252
databaseName?: string
53+
/**
54+
* Callback function for managing the closing of the MongoDB client.
55+
* This could be useful in serverless environments, especially when `client`
56+
* is provided as a function returning Promise<MongoClient>, not just a simple promise.
57+
* It allows for more sophisticated management of database connections,
58+
* addressing persistence, container reuse, and connection closure issues.
59+
*/
60+
onClose?: (client: MongoClient) => Promise<void>
4361
}
4462

4563
export const defaultCollections: Required<
@@ -89,83 +107,95 @@ export function _id(hex?: string) {
89107
}
90108

91109
export function MongoDBAdapter(
92-
client: Promise<MongoClient>,
110+
/**
111+
* The MongoDB client. You can either pass a promise that resolves to a `MongoClient` or a function that returns a promise that resolves to a `MongoClient`.
112+
* Using a function that returns a `Promise<MongoClient>` could be useful in serverless environments, particularly when combined with `options.onClose`, to efficiently handle database connections and address challenges with persistence, container reuse, and connection closure.
113+
* These functions enable either straightforward open-close database connections or more complex caching and connection reuse strategies.
114+
*/
115+
client: Promise<MongoClient> | (() => Promise<MongoClient>),
93116
options: MongoDBAdapterOptions = {}
94117
): Adapter {
95118
const { collections } = options
96119
const { from, to } = format
97120

98-
const db = (async () => {
99-
const _db = (await client).db(options.databaseName)
121+
const getDb = async () => {
122+
const _client = await (typeof client === "function" ? client() : client)
123+
const _db = _client.db(options.databaseName)
100124
const c = { ...defaultCollections, ...collections }
101125
return {
102126
U: _db.collection<AdapterUser>(c.Users),
103127
A: _db.collection<AdapterAccount>(c.Accounts),
104128
S: _db.collection<AdapterSession>(c.Sessions),
105129
V: _db.collection<VerificationToken>(c?.VerificationTokens),
130+
[Symbol.asyncDispose]: async () => {
131+
await options.onClose?.(_client)
132+
},
106133
}
107-
})()
134+
}
108135

109136
return {
110137
async createUser(data) {
111138
const user = to<AdapterUser>(data)
112-
await (await db).U.insertOne(user)
139+
await using db = await getDb()
140+
await db.U.insertOne(user)
113141
return from<AdapterUser>(user)
114142
},
115143
async getUser(id) {
116-
const user = await (await db).U.findOne({ _id: _id(id) })
144+
await using db = await getDb()
145+
const user = await db.U.findOne({ _id: _id(id) })
117146
if (!user) return null
118147
return from<AdapterUser>(user)
119148
},
120149
async getUserByEmail(email) {
121-
const user = await (await db).U.findOne({ email })
150+
await using db = await getDb()
151+
const user = await db.U.findOne({ email })
122152
if (!user) return null
123153
return from<AdapterUser>(user)
124154
},
125155
async getUserByAccount(provider_providerAccountId) {
126-
const account = await (await db).A.findOne(provider_providerAccountId)
156+
await using db = await getDb()
157+
const account = await db.A.findOne(provider_providerAccountId)
127158
if (!account) return null
128-
const user = await (
129-
await db
130-
).U.findOne({ _id: new ObjectId(account.userId) })
159+
const user = await db.U.findOne({ _id: new ObjectId(account.userId) })
131160
if (!user) return null
132161
return from<AdapterUser>(user)
133162
},
134163
async updateUser(data) {
135164
const { _id, ...user } = to<AdapterUser>(data)
136-
137-
const result = await (
138-
await db
139-
).U.findOneAndUpdate({ _id }, { $set: user }, { returnDocument: "after" })
165+
await using db = await getDb()
166+
const result = await db.U.findOneAndUpdate(
167+
{ _id },
168+
{ $set: user },
169+
{ returnDocument: "after" }
170+
)
140171

141172
return from<AdapterUser>(result!)
142173
},
143174
async deleteUser(id) {
144175
const userId = _id(id)
145-
const m = await db
176+
await using db = await getDb()
146177
await Promise.all([
147-
m.A.deleteMany({ userId: userId as any }),
148-
m.S.deleteMany({ userId: userId as any }),
149-
m.U.deleteOne({ _id: userId }),
178+
db.A.deleteMany({ userId: userId as any }),
179+
db.S.deleteMany({ userId: userId as any }),
180+
db.U.deleteOne({ _id: userId }),
150181
])
151182
},
152183
linkAccount: async (data) => {
153184
const account = to<AdapterAccount>(data)
154-
await (await db).A.insertOne(account)
185+
await using db = await getDb()
186+
await db.A.insertOne(account)
155187
return account
156188
},
157189
async unlinkAccount(provider_providerAccountId) {
158-
const account = await (
159-
await db
160-
).A.findOneAndDelete(provider_providerAccountId)
190+
await using db = await getDb()
191+
const account = await db.A.findOneAndDelete(provider_providerAccountId)
161192
return from<AdapterAccount>(account!)
162193
},
163194
async getSessionAndUser(sessionToken) {
164-
const session = await (await db).S.findOne({ sessionToken })
195+
await using db = await getDb()
196+
const session = await db.S.findOne({ sessionToken })
165197
if (!session) return null
166-
const user = await (
167-
await db
168-
).U.findOne({ _id: new ObjectId(session.userId) })
198+
const user = await db.U.findOne({ _id: new ObjectId(session.userId) })
169199
if (!user) return null
170200
return {
171201
user: from<AdapterUser>(user),
@@ -174,38 +204,35 @@ export function MongoDBAdapter(
174204
},
175205
async createSession(data) {
176206
const session = to<AdapterSession>(data)
177-
await (await db).S.insertOne(session)
207+
await using db = await getDb()
208+
await db.S.insertOne(session)
178209
return from<AdapterSession>(session)
179210
},
180211
async updateSession(data) {
181212
const { _id, ...session } = to<AdapterSession>(data)
182-
183-
const updatedSession = await (
184-
await db
185-
).S.findOneAndUpdate(
213+
await using db = await getDb()
214+
const updatedSession = await db.S.findOneAndUpdate(
186215
{ sessionToken: session.sessionToken },
187216
{ $set: session },
188217
{ returnDocument: "after" }
189218
)
190219
return from<AdapterSession>(updatedSession!)
191220
},
192221
async deleteSession(sessionToken) {
193-
const session = await (
194-
await db
195-
).S.findOneAndDelete({
222+
await using db = await getDb()
223+
const session = await db.S.findOneAndDelete({
196224
sessionToken,
197225
})
198226
return from<AdapterSession>(session!)
199227
},
200228
async createVerificationToken(data) {
201-
await (await db).V.insertOne(to(data))
229+
await using db = await getDb()
230+
await db.V.insertOne(to(data))
202231
return data
203232
},
204233
async useVerificationToken(identifier_token) {
205-
const verificationToken = await (
206-
await db
207-
).V.findOneAndDelete(identifier_token)
208-
234+
await using db = await getDb()
235+
const verificationToken = await db.V.findOneAndDelete(identifier_token)
209236
if (!verificationToken) return null
210237
const { _id, ...rest } = verificationToken
211238
return rest
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { runBasicTests } from "utils/adapter"
2+
import { defaultCollections, format, MongoDBAdapter, _id } from "../src"
3+
import { MongoClient } from "mongodb"
4+
import { expect, test, vi } from "vitest"
5+
6+
const name = "serverless-test"
7+
const clientPromise = new MongoClient(
8+
`mongodb://localhost:27017/${name}`
9+
).connect()
10+
11+
const onClose = vi.fn(async (client: MongoClient) => {
12+
await client.close()
13+
})
14+
15+
let mongoClientCount = 0
16+
17+
runBasicTests({
18+
adapter: MongoDBAdapter(
19+
async () => {
20+
const client = await new MongoClient(
21+
`mongodb://localhost:27017/${name}`
22+
).connect()
23+
mongoClientCount++
24+
return client
25+
},
26+
{
27+
onClose,
28+
}
29+
),
30+
db: {
31+
async disconnect() {
32+
const client = await clientPromise
33+
await client.db().dropDatabase()
34+
await client.close()
35+
},
36+
async user(id) {
37+
const client = await clientPromise
38+
const user = await client
39+
.db()
40+
.collection(defaultCollections.Users)
41+
.findOne({ _id: _id(id) })
42+
43+
if (!user) return null
44+
return format.from(user)
45+
},
46+
async account(provider_providerAccountId) {
47+
const client = await clientPromise
48+
const account = await client
49+
.db()
50+
.collection(defaultCollections.Accounts)
51+
.findOne(provider_providerAccountId)
52+
if (!account) return null
53+
return format.from(account)
54+
},
55+
async session(sessionToken) {
56+
const client = await clientPromise
57+
const session = await client
58+
.db()
59+
.collection(defaultCollections.Sessions)
60+
.findOne({ sessionToken })
61+
if (!session) return null
62+
return format.from(session)
63+
},
64+
async verificationToken(identifier_token) {
65+
const client = await clientPromise
66+
const token = await client
67+
.db()
68+
.collection(defaultCollections.VerificationTokens)
69+
.findOne(identifier_token)
70+
if (!token) return null
71+
const { _id, ...rest } = token
72+
return rest
73+
},
74+
},
75+
})
76+
77+
test("all the connections are closed", () => {
78+
expect(mongoClientCount).toBeGreaterThan(0)
79+
expect(onClose).toHaveBeenCalledTimes(mongoClientCount)
80+
})

0 commit comments

Comments
 (0)