import { Collection } from 'discord.js'; import { connect, Model, connection, Connection } from 'mongoose'; import { Logger } from 'winston'; import ReactionModel, { Reaction } from '../models/Reaction'; import GuildModel, { Guild } from '../models/Guild'; import { MONGO_EVENTS } from '../util/constants'; import ReactionClient from '../../bot/client/ReactionClient'; let i = 0; /** * The key, model and cached collection of a database model. * @interface */ interface Combo { key: string; model: Model; cache: Collection; } /** * The Settings Provider that handles all database reads and rights. * @private */ export default class SettingsProvider { protected readonly client: ReactionClient; protected readonly guilds: Collection = new Collection(); protected readonly reactions: Collection = new Collection(); protected readonly GuildModel = GuildModel; protected readonly ReactionModel = ReactionModel; /** * * @param {GiveawayClient} client - The extended Akairo Client */ public constructor(client: ReactionClient) { this.client = client; } /** * Retuns all the collection caches. * @returns {Object} */ public get cache() { return { guilds: this.guilds, reactions: this.reactions, }; } /** * Returns the database combos * @returns {Combo[]} */ public get combos(): Combo[] { return [ { key: 'guild', model: this.GuildModel, cache: this.guilds, }, { key: 'reaction', model: this.ReactionModel, cache: this.reactions, }, ]; } /** * Creates a new database document with the provided collection name and data. * @param {string} type - The collection name * @param {object} data - The data for the new document * @returns {Docuement} */ public async new(type: 'guild', data: Partial): Promise; public async new(type: 'reaction', data: Partial): Promise; public async new(type: string, data: object): Promise { const combo = this.combos.find(c => c.key === type); if (combo) { const document = new combo.model(data); await document.save(); this.client.logger.data(`[DATABASE] Made new ${combo.model.modelName} document with ID of ${document._id}.`); combo.cache.set(document.id, document); return document; } throw Error(`"${type}" is not a valid model key.`); } /** * Updates the a database document's data. * @param {Types} type - The collection name * @param {object} key - The search paramaters for the document * @param {object} data - The data you wish to overwrite in the update * @returns {Promise} */ public async set(type: 'guild', key: Partial, data: Partial): Promise; public async set(type: 'reaction', key: Partial, data: Partial): Promise; public async set(type: string, key: object, data: object): Promise { const combo = this.combos.find(c => c.key === type); if (combo) { const document = await combo.model.findOneAndUpdate(key, { $set: data }, { new: true }); if (document) { this.client.logger.verbose(`[DATABASE] Edited ${combo.model.modelName} document with ID of ${document._id}.`); combo.cache.set(document.id, document); return document; } return null; } throw Error(`"${type}" is not a valid model key.`); } /** * Removes a database document. * @param {Types} type - The collection name * @param {object} data - The search paramaters for the document * @returns {Promise>} The document that was removed, if any. */ public async remove(type: 'guild', data: Partial): Promise; public async remove(type: 'user', data: Partial): Promise; public async remove(type: string, data: object): Promise { const combo = this.combos.find(c => c.key === type); if (combo) { const document = await combo.model.findOneAndRemove(data); if (document) { this.client.logger.verbose(`[DATABASE] Edited ${combo.model.modelName} document with ID of ${document._id}.`); combo.cache.delete(document.id); return document; } return null; } throw Error(`"${type}" is not a valid model key.`); } /** * Caching all database documents. * @returns {number} The amount of documents cached total. * @private */ private async _cacheAll(): Promise { for (const combo of this.combos) await this._cache(combo); return i; } /** * Caching each collection's documents. * @param {Combo} combo - The combo name * @returns {number} The amount of documents cached from that collection. * @private */ private async _cache(combo: Combo): Promise { const items = await combo.model.find(); for (const i of items) combo.cache.set(i.id, i); this.client.logger.verbose( `[DATABASE]: Cached ${items.length.toLocaleString('en-US')} items from ${combo.model.modelName}.`, ); return (i += items.length); } /** * Connect to the database * @param {string} url - the mongodb uri * @returns {Promise} Returns a */ private async _connect(url: string | undefined): Promise { if (url) { const start = Date.now(); try { await connect(url, { useCreateIndex: true, useNewUrlParser: true, useFindAndModify: false, useUnifiedTopology: true, }); } catch (err) { this.client.logger.error(`[DATABASE] Error when connecting to MongoDB:\n${err.stack}`); process.exit(1); } return this.client.logger.verbose(`[DATABASE] Connected to MongoDB in ${Date.now() - start}ms.`); } this.client.logger.error('[DATABASE] No MongoDB url provided!'); return process.exit(1); } /** * Adds all the listeners to the mongo connection. * @param connection - The mongoose connection * @returns {void} * @private */ private _addListeners(connection: Connection): void { for (const [event, msg] of Object.entries(MONGO_EVENTS)) { connection.on(event, () => this.client.logger.data(`[DATABASE]: ${msg}`)); } } /** * Starts the Settings Provider * @returns {SettingsProvider} */ public async init(): Promise { this._addListeners(connection); await this._connect(process.env.MONGO); this.client.logger.verbose(`[DATABASE]: Now caching ${this.combos.length} schema documents.`); await this._cacheAll(); this.client.logger.info(`[DATABASE] [LAUNCHED] Successfully connected and cached ${i} documents.`); return this; } }