/** *
*

Official MongoDB adapter for Auth.js / NextAuth.js.

* * * *
* * ## Installation * * ```bash npm2yarn * npm install @auth/mongodb-adapter mongodb * ``` * * @module @auth/mongodb-adapter */ import { ObjectId } from "mongodb" import type { Adapter, AdapterUser, AdapterAccount, AdapterSession, VerificationToken, } from "@auth/core/adapters" import type { MongoClient } from "mongodb" /** * This adapter uses https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management. * This feature is very new and requires runtime polyfills for `Symbol.asyncDispose` in order to work properly in all environments. * 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`. * * You can find more information about this feature and the polyfills in the link above. */ // @ts-expect-error read only property is not assignable Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose") /** This is the interface of the MongoDB adapter options. */ export interface MongoDBAdapterOptions { /** * The name of the {@link https://www.mongodb.com/docs/manual/core/databases-and-collections/#collections MongoDB collections}. */ collections?: { Users?: string Accounts?: string Sessions?: string VerificationTokens?: string } /** * The name you want to give to the MongoDB database */ databaseName?: string /** * Callback function for managing the closing of the MongoDB client. * This could be useful when `client` is provided as a function returning MongoClient. * It allows for more customized management of database connections, * addressing persistence, container reuse, and connection closure issues. */ onClose?: (client: MongoClient) => Promise } export const defaultCollections: Required< Required["collections"] > = { Users: "users", Accounts: "accounts", Sessions: "sessions", VerificationTokens: "verification_tokens", } export const format = { /** Takes a MongoDB object and returns a plain old JavaScript object */ from>(object: Record): T { const newObject: Record = {} for (const key in object) { const value = object[key] if (key === "_id") { newObject.id = value.toHexString() } else if (key === "userId") { newObject[key] = value.toHexString() } else { newObject[key] = value } } return newObject as T }, /** Takes a plain old JavaScript object and turns it into a MongoDB object */ to>(object: Record) { const newObject: Record = { _id: _id(object.id), } for (const key in object) { const value = object[key] if (key === "userId") newObject[key] = _id(value) else if (key === "id") continue else newObject[key] = value } return newObject as T & { _id: ObjectId } }, } /** @internal */ export function _id(hex?: string) { if (hex?.length !== 24) return new ObjectId() return new ObjectId(hex) } export function MongoDBAdapter( /** * The MongoDB client. * * The MongoDB team recommends providing a non-connected `MongoClient` instance to avoid unhandled promise rejections if the client fails to connect. * * Alternatively, you can also pass: * - A promise that resolves to a connected `MongoClient` (not recommended). * - A function, to handle more complex and custom connection strategies. * * Using a function combined with `options.onClose`, can be useful when you want a more advanced and customized connection strategy to address challenges related to persistence, container reuse, and connection closure. */ client: | MongoClient | Promise | (() => MongoClient | Promise), options: MongoDBAdapterOptions = {} ): Adapter { const { collections } = options const { from, to } = format const getDb = async () => { const _client: MongoClient = await (typeof client === "function" ? client() : client) const _db = _client.db(options.databaseName) const c = { ...defaultCollections, ...collections } return { U: _db.collection(c.Users), A: _db.collection(c.Accounts), S: _db.collection(c.Sessions), V: _db.collection(c?.VerificationTokens), [Symbol.asyncDispose]: async () => { await options.onClose?.(_client) }, } } return { async createUser(data) { const user = to(data) await using db = await getDb() await db.U.insertOne(user) return from(user) }, async getUser(id) { await using db = await getDb() const user = await db.U.findOne({ _id: _id(id) }) if (!user) return null return from(user) }, async getUserByEmail(email) { await using db = await getDb() const user = await db.U.findOne({ email }) if (!user) return null return from(user) }, async getUserByAccount(provider_providerAccountId) { await using db = await getDb() const account = await db.A.findOne(provider_providerAccountId) if (!account) return null const user = await db.U.findOne({ _id: new ObjectId(account.userId) }) if (!user) return null return from(user) }, async updateUser(data) { const { _id, ...user } = to(data) await using db = await getDb() const result = await db.U.findOneAndUpdate( { _id }, { $set: user }, { returnDocument: "after" } ) return from(result!) }, async deleteUser(id) { const userId = _id(id) await using db = await getDb() await Promise.all([ db.A.deleteMany({ userId: userId as any }), db.S.deleteMany({ userId: userId as any }), db.U.deleteOne({ _id: userId }), ]) }, linkAccount: async (data) => { const account = to(data) await using db = await getDb() await db.A.insertOne(account) return account }, async unlinkAccount(provider_providerAccountId) { await using db = await getDb() const account = await db.A.findOneAndDelete(provider_providerAccountId) return from(account!) }, async getSessionAndUser(sessionToken) { await using db = await getDb() const session = await db.S.findOne({ sessionToken }) if (!session) return null const user = await db.U.findOne({ _id: new ObjectId(session.userId) }) if (!user) return null return { user: from(user), session: from(session), } }, async createSession(data) { const session = to(data) await using db = await getDb() await db.S.insertOne(session) return from(session) }, async updateSession(data) { const { _id, ...session } = to(data) await using db = await getDb() const updatedSession = await db.S.findOneAndUpdate( { sessionToken: session.sessionToken }, { $set: session }, { returnDocument: "after" } ) return from(updatedSession!) }, async deleteSession(sessionToken) { await using db = await getDb() const session = await db.S.findOneAndDelete({ sessionToken, }) return from(session!) }, async createVerificationToken(data) { await using db = await getDb() await db.V.insertOne(to(data)) return data }, async useVerificationToken(identifier_token) { await using db = await getDb() const verificationToken = await db.V.findOneAndDelete(identifier_token) if (!verificationToken) return null const { _id, ...rest } = verificationToken return rest }, } }