346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
import { inspect } from "node:util";
|
|
import { MongoClient, MongoClientOptions, Db, Document, WithId, ObjectId, Filter, IndexSpecification } from "mongodb";
|
|
import { IServer, ILogger, LoggerClientOptions } from "./interfaces/index.js";
|
|
|
|
type Credentials = {
|
|
URI?: string,
|
|
user?: string,
|
|
password?: string,
|
|
host?: string,
|
|
port?: number,
|
|
database: string,
|
|
authDb?: string
|
|
}
|
|
|
|
type MongoQuery = {
|
|
_id?: unknown,
|
|
[key: string]: unknown
|
|
}
|
|
|
|
export type MongoOptions = {
|
|
credentials: Credentials,
|
|
loggerOptions?: LoggerClientOptions,
|
|
client?: MongoClientOptions,
|
|
load?: boolean
|
|
}
|
|
|
|
/**
|
|
* A dedicated class to locally wrap the mongodb API wrapper
|
|
*
|
|
* @class MongoDB
|
|
*/
|
|
class MongoDB {
|
|
|
|
#_database: string;
|
|
#config: MongoOptions;
|
|
#logger: ILogger;
|
|
#URI: string;
|
|
|
|
#db: Db | null;
|
|
#_client: MongoClient;
|
|
|
|
constructor (server: IServer, config: MongoOptions) {
|
|
|
|
if (!server)
|
|
throw new Error('Missing reference to server!');
|
|
if (!config)
|
|
throw new Error('No config options provided!');
|
|
|
|
const { user, password, host, port, database, URI, authDb } = config.credentials;
|
|
if ((!host?.length || !port || !database?.length) && !URI)
|
|
throw new Error(`Must provide host, port, and database OR URI parameters!`);
|
|
|
|
this.#config = config;
|
|
this.#db = null; // DB connection
|
|
this.#_database = database; // Which database to connect to
|
|
|
|
this.#logger = server.createLogger(this, config.loggerOptions);
|
|
|
|
if (URI) {
|
|
this.#URI = URI;
|
|
} else {
|
|
let AUTH_DB = authDb;
|
|
const auth = user ? `${user}:${password}@` : '';
|
|
if (!AUTH_DB && auth) {
|
|
this.#logger.warn(`No explicit auth db provided with MONGO_AUTH_DB, will attempt to use MONGO_DB for auth source`);
|
|
AUTH_DB = authDb;
|
|
} else if (!auth) {
|
|
this.#logger.warn(`No auth provided, proceeding without`);
|
|
}
|
|
|
|
this.#URI = `mongodb://${auth}${host}:${port}/${AUTH_DB || ''}?readPreference=secondaryPreferred`;
|
|
}
|
|
|
|
this.#_client = new MongoClient(this.#URI, this.#config.client);
|
|
|
|
// TODO figure out reconnecting to DB when connection fails
|
|
this.#_client.on('error', (error) => this.#logger.error(`MongoDB error:\n${error.stack}`))
|
|
.on('timeout', () => this.#logger.warn(`MongoDB timed out`))
|
|
.on('close', () => this.#logger.info(`MongoDB client disconnected`))
|
|
.on('open', () => this.#logger.info(`MongoDB client connected`));
|
|
|
|
}
|
|
|
|
get database () {
|
|
return this.#_database;
|
|
}
|
|
|
|
get client () {
|
|
return this.#_client;
|
|
}
|
|
|
|
/**
|
|
* Initialises the connection to Mongo
|
|
*
|
|
* @memberof MongoDB
|
|
*/
|
|
async init () {
|
|
|
|
if (!this.#config.load)
|
|
return this.#logger.info('Not loading MongoDB');
|
|
|
|
if (this.#db)
|
|
throw new Error('Database already connected');
|
|
|
|
this.#logger.status(`Initializing database connection to ${this.#_client.options.hosts}`);
|
|
|
|
await this.#_client.connect();
|
|
this.#logger.debug(`Connected, selecting DB`);
|
|
this.#db = await this.#_client.db(this.#_database);
|
|
|
|
this.#logger.status('MongoDB ready');
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
async close () {
|
|
if (!this.#db)
|
|
return;
|
|
|
|
this.#logger.status('Closing database connection');
|
|
await this.#_client.close();
|
|
this.#db = null;
|
|
this.#logger.status('Database closed');
|
|
}
|
|
|
|
get mongoClient () {
|
|
return this.#_client;
|
|
}
|
|
|
|
/**
|
|
* Find and return the first match
|
|
*
|
|
* @param {String} db The collection in which the data is to be updated
|
|
* @param {Object} query The filter that is used to find the data
|
|
* @returns {Array} An array containing the corresponding objects for the query
|
|
* @memberof Database
|
|
*/
|
|
async find<T extends Document> (db: string, query: MongoQuery, options?: object): Promise<WithId<T>[]> {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
|
|
if (typeof query._id === 'string')
|
|
query._id = new ObjectId(query._id);
|
|
|
|
this.#logger.debug(`Incoming find query for ${db} with parameters ${inspect(query)}`);
|
|
|
|
const cursor = this.#db.collection<T>(db).find(query as Filter<T>, options);
|
|
return cursor.toArray();
|
|
|
|
}
|
|
|
|
/**
|
|
* Find and return the first match
|
|
*
|
|
* @param {String} db The collection in which the data is to be updated
|
|
* @param {Object} query The filter that is used to find the data
|
|
* @returns {Object} An object containing the queried data
|
|
* @memberof Database
|
|
*/
|
|
async findOne<T extends Document> (db: string, query: MongoQuery, options = {}): Promise<WithId<T> | null> {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
|
|
if (typeof query._id === 'string' && query._id.length === 12)
|
|
query._id = new ObjectId(query._id);
|
|
|
|
this.#logger.debug(`Incoming findOne query for ${db} with parameters ${inspect(query)}`);
|
|
const result = await this.#db.collection<T>(db).findOne(query as Filter<T>, options);
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Update any and all filter matches.
|
|
*
|
|
* @param {String} db The collection in which the data is to be updated
|
|
* @param {Object} filter The filter that is used to find the data
|
|
* @param {Object} data The updated data
|
|
* @returns {WriteResult} Object containing the followint counts: Matched, Upserted, Modified
|
|
* @memberof Database
|
|
*/
|
|
async updateMany (db: string, filter: MongoQuery, data: Document, upsert = false) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (!filter)
|
|
throw new Error(`Cannot run update many without a filter, if you mean to update every single document, pass an empty object`);
|
|
if (typeof filter._id === 'string')
|
|
filter._id = new ObjectId(filter._id);
|
|
|
|
this.#logger.debug(`Incoming update query for '${db}' with parameters\n${inspect(filter)}\nand data\n${inspect(data)}`);
|
|
const result = await this.#db.collection(db).updateMany(filter as Filter<Document>, { $set: data }, { upsert });
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the first filter match.
|
|
*
|
|
* @param {String} db The collection in which the data is to be updated
|
|
* @param {Object} filter The filter that is used to find the data
|
|
* @param {Object} data The updated data
|
|
* @returns {WriteResult} Object containing the followint counts: Matched, Upserted, Modified
|
|
* @memberof Database
|
|
*/
|
|
async updateOne (db: string, filter: MongoQuery, data: Document, upsert = false) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (typeof filter._id === 'string')
|
|
filter._id = new ObjectId(filter._id);
|
|
|
|
this.#logger.debug(`Incoming updateOne query for ${db} with parameters ${inspect(filter)}`);
|
|
const result = await this.#db.collection(db).updateOne(filter as Filter<Document>, { $set: data }, { upsert });
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Insert document.
|
|
*
|
|
* @param {String} db The collection in which the data is to be updated
|
|
* @param {Object} filter The filter that is used to find the data
|
|
* @param {Object} data The updated data
|
|
* @returns {WriteResult} Object containing the followint counts: Matched, Upserted, Modified
|
|
* @memberof Database
|
|
*/
|
|
async insertOne (db: string, data: Document) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (typeof data._id === 'string')
|
|
data._id = new ObjectId(data._id);
|
|
|
|
this.#logger.debug(`Incoming insertOne query for ${db} with parameters ${inspect(data)}`);
|
|
const result = await this.#db.collection(db).insertOne(data);
|
|
return result;
|
|
|
|
}
|
|
|
|
async deleteOne (db: string, filter: Document) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (typeof filter._id === 'string')
|
|
filter._id = new ObjectId(filter._id);
|
|
|
|
this.#logger.debug(`Incoming deleteOne query for ${db} with parameters ${inspect(filter)}`);
|
|
const result = await this.#db.collection(db).deleteOne(filter);
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Push data to an array
|
|
*
|
|
* @param {string} db The collection to query
|
|
* @param {object} filter The filter to find the document to update
|
|
* @param {object} data The data to be pushed
|
|
* @param {boolean} [upsert=false]
|
|
* @returns
|
|
* @memberof Database
|
|
*/
|
|
async push (db: string, filter: Document, data: object, upsert = false) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (typeof filter._id === 'string')
|
|
filter._id = new ObjectId(filter._id);
|
|
|
|
this.#logger.debug(`Incoming push query for ${db}, with upsert ${upsert} and with parameters ${inspect(filter)} and data ${inspect(data)}`);
|
|
const result = await this.#db.collection(db).updateOne(filter, { $push: data }, { upsert });
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Find a random element from a database
|
|
*
|
|
* @param {string} db The collection to query
|
|
* @param {object} [filter={}] The filtering object to narrow down the sample pool
|
|
* @param {number} [amount=1] Amount of items to return
|
|
* @returns {object}
|
|
* @memberof Database
|
|
*/
|
|
random (db: string, filter: Document = {}, amount = 1) {
|
|
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (typeof db !== 'string')
|
|
throw new TypeError('Expecting collection name for the first argument');
|
|
if (typeof filter._id === 'string')
|
|
filter._id = new ObjectId(filter._id);
|
|
|
|
this.#logger.debug(`Incoming random query for ${db} with parameters ${inspect(filter)} and amount ${amount}`);
|
|
|
|
if (amount > 100)
|
|
amount = 100;
|
|
|
|
const cursor = this.#db.collection(db).aggregate([{ $match: filter }, { $sample: { size: amount } }]);
|
|
return cursor.toArray();
|
|
|
|
}
|
|
|
|
stats (options = {}) {
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
const result = this.#db.stats(options);
|
|
return result;
|
|
}
|
|
|
|
collection (coll: string) {
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
return this.#db.collection(coll);
|
|
}
|
|
|
|
async ensureIndex (collection: string, indices: IndexSpecification = []) {
|
|
if (!this.#db)
|
|
throw new Error(`MongoDB not connected`);
|
|
if (!(indices instanceof Array))
|
|
indices = [ indices ];
|
|
await this.#db.collection(collection).createIndex(indices);
|
|
}
|
|
|
|
}
|
|
|
|
export { MongoDB }; |