diff --git a/auth.ts b/auth.ts index 255a7f29..1059f2e3 100644 --- a/auth.ts +++ b/auth.ts @@ -3,10 +3,10 @@ import Discord from 'next-auth/providers/discord' import { addUser } from './lib/db/users' import { AccountType, type User } from './types/User' import { MongoDBAdapter } from '@auth/mongodb-adapter' -import { dbClient } from './lib/db/db' import type { NextAuthConfig } from 'next-auth' import { findOneTyped } from './lib/db/dbTyped' import { Types } from './types/Components' +import clientPromise from './lib/db/mongoDB' export const authOptions: NextAuthConfig = { providers: [Discord], @@ -85,7 +85,7 @@ export const authOptions: NextAuthConfig = { }, // A database is optional, but required to persist accounts in a database - adapter: MongoDBAdapter(dbClient(), { + adapter: MongoDBAdapter(clientPromise, { collections: { Users: 'nextauth_users', Sessions: 'nextauth_sessions', diff --git a/lib/db/db.ts b/lib/db/db.ts index ad4fe268..77eadbb4 100644 --- a/lib/db/db.ts +++ b/lib/db/db.ts @@ -1,7 +1,7 @@ // docker run --name index-db -d -p 27017:27017 mongo -import { MongoClient } from 'mongodb' import { hasOwnProperty } from '../utils' import { cleanId, polluteId } from './utils' +import clientPromise from './mongoDB' const uri = 'DATABASE_URL' in process.env @@ -11,8 +11,6 @@ if (typeof uri !== 'string') { throw Error('Unable to connect to DB due to missing DATABASE_URL') } -export const dbClient = () => new MongoClient(uri, { maxPoolSize: 5 }) - export async function exportData(isAdmin = false) { if (isAdmin) { return { @@ -34,10 +32,8 @@ export async function exportData(isAdmin = false) { } export async function getAll(collection: string): Promise { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') let data = await db.collection(collection).find().toArray() - client.close() if (data.length > 0 && hasOwnProperty(data[0], 'name')) { data = data.sort((a, b) => (a.name < b.name ? -1 : 1)) } @@ -49,21 +45,18 @@ export async function find( collection: string, query: Record ): Promise { - const client = await dbClient().connect() - const db = client.db('index') - const data = await db.collection(collection).find(polluteId(query)).toArray() - client.close() - return cleanId(data) + const db = (await clientPromise).db('index') + return cleanId( + await db.collection(collection).find(polluteId(query)).toArray() + ) } export async function findOne( collection: string, query: Record ): Promise { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const found = await db.collection(collection).findOne(polluteId(query)) - client.close() if (found === null) { return null } @@ -74,10 +67,8 @@ export async function count( collection: string, query: Record = {} ): Promise { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const data = await db.collection(collection).countDocuments(polluteId(query)) - client.close() return data } @@ -92,15 +83,13 @@ export async function insert( collection: string, data: Record ): Promise { - const client = await dbClient().connect() let tries = 1 while (tries < 3) { try { - const db = client.db('index') + const db = (await clientPromise).db('index') data.createdAt = new Date() data.lastModified = new Date() const { insertedId } = await db.collection(collection).insertOne(data) - client.close() return insertedId.toString() } catch (error) { console.error( @@ -112,7 +101,6 @@ export async function insert( tries++ } } - client.close() throw Error('Unable to insert entry into ' + collection + ' after 3 retires') } @@ -121,16 +109,14 @@ export async function updateOne( query: Record, data: Record ) { - const client = await dbClient().connect() let tries = 1 while (tries < 3) { try { - const db = client.db('index') + const db = (await clientPromise).db('index') await db.collection(collection).updateOne(polluteId(query), { $set: data, $currentDate: { lastModified: true }, }) - client.close() return } catch (error) { console.error( @@ -139,7 +125,6 @@ export async function updateOne( tries++ } } - client.close() throw Error('Unable to update entry of ' + collection + ' after 3 retires') } @@ -147,13 +132,11 @@ export async function deleteOne( collection: string, query: Record ) { - const client = await dbClient().connect() let tries = 1 while (tries < 3) { try { - const db = client.db('index') + const db = (await clientPromise).db('index') await db.collection(collection).deleteOne(polluteId(query)) - client.close() return } catch (error) { console.error( @@ -165,6 +148,5 @@ export async function deleteOne( tries++ } } - client.close() throw Error('Unable to delete entry from ' + collection + ' after 3 retires') } diff --git a/lib/db/itemScreenshots.ts b/lib/db/itemScreenshots.ts index 1b413ef0..9bf79111 100644 --- a/lib/db/itemScreenshots.ts +++ b/lib/db/itemScreenshots.ts @@ -1,6 +1,6 @@ -import { dbClient } from './db' import { GridFSBucket } from 'mongodb' import { Readable } from 'stream' +import clientPromise from './mongoDB' export function bufferToStream(buffer: Buffer) { let stream = new Readable() @@ -32,8 +32,7 @@ export async function addItemScreenshot(img: Uint8Array, itemId: string) { imgStream.push(img) imgStream.push(null) - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const bucket = new GridFSBucket(db, { bucketName: 'itemScreenshots', }) @@ -57,51 +56,38 @@ export async function addItemScreenshot(img: Uint8Array, itemId: string) { stream.on('finish', resolve) imgStream.on('error', reject) }) - client.close() } export async function getItemScreenshotBuffer(itemId: string) { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const bucket = new GridFSBucket(db, { bucketName: 'itemScreenshots', }) - const data = await streamToBuffer(bucket.openDownloadStreamByName(itemId)) - client.close() - return data + return await streamToBuffer(bucket.openDownloadStreamByName(itemId)) } export async function screenshotExists(itemId: string) { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const bucket = new GridFSBucket(db, { bucketName: 'itemScreenshots', }) const cursor = await bucket.find({ filename: itemId }) - const data = await cursor.hasNext() - client.close() - return data + return await cursor.hasNext() } export async function clearAllScreenshots() { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const bucket = new GridFSBucket(db, { bucketName: 'itemScreenshots', }) - const data = await bucket.drop() - client.close() - return data + return await bucket.drop() } export async function listScreenshotsOfItem(itemId: string) { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const bucket = new GridFSBucket(db, { bucketName: 'itemScreenshots', }) - const data = bucket.find({ filename: itemId }) - client.close() - return data + return bucket.find({ filename: itemId }) } diff --git a/lib/db/mongoDB.ts b/lib/db/mongoDB.ts new file mode 100644 index 00000000..c683a714 --- /dev/null +++ b/lib/db/mongoDB.ts @@ -0,0 +1,44 @@ +import { MongoClient } from 'mongodb' + +const uri = ( + 'DATABASE_URL' in process.env + ? process.env.DATABASE_URL + : 'mongodb://localhost' +) as string +if (typeof uri !== 'string') { + throw Error('Unable to connect to DB due to missing DATABASE_URL') +} + +declare global { + var _mongoClientPromise: Promise +} + +class Singleton { + private static _instance: Singleton + private client: MongoClient + private clientPromise: Promise + private constructor() { + this.client = new MongoClient(uri, { + maxPoolSize: 5, + directConnection: true, + }) + this.clientPromise = this.client.connect() + if (process.env.NODE_ENV === 'development') { + // In development mode, use a global variable so that the value + // is preserved across module reloads caused by HMR (Hot Module Replacement). + global._mongoClientPromise = this.clientPromise + } + } + + public static get instance() { + if (!this._instance) { + this._instance = new Singleton() + } + return this._instance.clientPromise + } +} +const clientPromise = Singleton.instance + +// Export a module-scoped MongoClient promise. By doing this in a +// separate module, the client can be shared across functions. +export default clientPromise diff --git a/lib/db/views.ts b/lib/db/views.ts index ce2af4b6..58743ebc 100644 --- a/lib/db/views.ts +++ b/lib/db/views.ts @@ -1,15 +1,15 @@ -import { count, dbClient, deleteOne, find, findOne, getAll, insert } from './db' +import { count, deleteOne, find, findOne, getAll, insert } from './db' import { cleanId } from './utils' import { Types } from '../../types/Components' import { findOneTyped } from './dbTyped' +import clientPromise from './mongoDB' export async function getViews() { return await getAll('views') } export async function getLastViews(type: Types, n: number) { - const client = await dbClient().connect() - const db = client.db('index') + const db = (await clientPromise).db('index') const data = cleanId( await db .collection('views') @@ -18,7 +18,6 @@ export async function getLastViews(type: Types, n: number) { .limit(n) .toArray() ) - client.close() console.log('Found', data.length, 'entries in views table for', type) // count what has been popular recently