Source: helpers/database/dbHelpers.js

const mongoose = require('mongoose');
const configs = require('@config');

/**
 * Checks if a collection exists in the MongoDB database.
 * @param {string} collection - The name of the collection to check.
 * @returns {string|undefined} - The name of the existing collection or undefined if the collection does not exist.
 * @throws {Error} - Throws an error if there is an issue accessing the database.
 */
function isCollectionExist(collection) {
    try {
        return mongoose.modelNames().find(model => model === collection);
    } catch(err) {
        throw new Error.Log('helpers.is_collection_exist', collection);
    }
}

/**
 * Checks if a document exists in a specific collection based on the provided filter.
 * @param {string} collectionName - The name of the collection to check.
 * @param {Object} filter - The filter object to search for the document.
 * @returns {Promise<boolean>} - A promise that resolves to true if the document exists, false otherwise.
 * @throws {Error} - Throws an error if there is an issue accessing the database.
 */
function isDocExist(collectionName, filter) {
    return new Promise((resolve, reject) => {
        mongoose.model(collectionName).exists(filter, (err, res) => {
            if (err) {
                reject(new Error.Log(err));
            }

            resolve(res);
        });
    });

}

/**
 * Retrieves the Mongoose model for a given collection name.
 * @param {string} collection - The name of the collection to retrieve the model for.
 * @returns {Model} - The Mongoose model for the specified collection.
 * @throws {Error} - Throws an error if the collection does not exist.
 */
function getCollectionModel(collection) {
    try {
        if (isCollectionExist(collection)) {
            return mongoose.model(collection);
        } else {
            throw new Error.Log('database.collection_dont_exist', collection);
        }
    } catch(err) {
        throw new Error.Log(err).append('helpers.get_collection_model', collection);
    }
}

/**
 * Creates a counter document for a specific collection if it does not already exist.
 * @param {Object} collection - The collection object containing 'name' and 'symbol' properties.
 * @throws {Error} - Throws an error if there is an issue creating the counter document.
 */
async function createCounter(collection){
    try {
        const Counters = mongoose.model(configs.database.counterCollection);
        const collCounter = await Counters.findById(collection.name);

        if (!collCounter) {
            const newCounter = new Counters({
                _id: collection.name,
                symbol: collection.symbol
            });

            await newCounter.save();
        }
    } catch(err) {
        throw new Error.Log(err).append('helpers.create_counter', collection.name);
    }
}

/**
 * Increases the counter value for a specific collection and returns the updated counter object.
 * @param {string} collection - The name of the collection to increase the counter for.
 * @returns {Object} - The updated counter object.
 * @throws {Error} - Throws an error if there is an issue increasing the counter.
 */
async function increaseCounter(collection) {
    try {
        const Counters = mongoose.model(configs.database.counterCollection);
        const counter = await Counters.findByIdAndUpdate(collection, { $inc: { value: 1 }});

        return counter.toObject();
    } catch(err) {
        throw new Error.Log(err).append('helpers.increase_counter', collection);
    }
}

/**
 * Increases the 'groupedLogs' property of a specific log document and returns the updated log object.
 * @param {string} logUID - The unique identifier of the log document to increase the property for.
 * @returns {Object} - The updated log object.
 * @throws {Error} - Throws an error if there is an issue increasing the log property.
 */
async function increaseLog(logUID) {   
    try {
        const Logs = mongoose.model(configs.database.logCollection);
        const logCounter = await Logs.findByIdAndUpdate(logUID, { $inc: { groupedLogs: 1 }});

        return logCounter.toObject();
    } catch(err) {
        throw new Error.Log(err).append('helpers.increase_log');
    }
}

/**
 * Increases specified properties of a document based on the provided filter and data object.
 * @param {string} collectionName - The name of the collection to update the document in.
 * @param {Object} filter - The filter object to match the document.
 * @param {Object} data - The data object containing properties to be incremented.
 * @returns {Object} - The updated document object.
 * @throws {Error} - Throws an error if there is an issue updating the document properties.
 */
async function increaseDocProp(collectionName, filter, data) {   
    try {
        const DBModel = mongoose.model(collectionName);
        const doc = await DBModel.findOneAndUpdate(filter, { $inc: data });

        return doc.initialize();
    } catch(err) {
        throw new Error.Log(err).append('helpers.increase_doc_prop', collectionName, filter, data);
    }
}

/**
 * Determines the type of query based on the provided filter and the desired query type.
 * @param {*} filter - The filter object to analyze.
 * @param {string} type - The desired query type ('one' for single document, 'many' for multiple documents).
 * @returns {string} - The determined query type ('one' or 'many').
 */
function pickQueryType(filter, type) {
    let filterType = typeof filter;

    if (filterType === 'string') return 'one';
    if (filterType === 'object' && !Array.isArray(filter)) {
        switch(type) {
            case 'many': {
                return 'many';
            }
            case 'one':
            default: {
                return 'one';
            }
        }
    }

    return 'one';
}

/**
 * Validates and transforms a filter object to a format suitable for querying the database.
 * @param {string|Object} filter - The filter to be validated and transformed.
 * @returns {Object} - The transformed filter object suitable for database queries.
 * @throws {Error} - Throws an error if the filter is invalid.
 */
function treatFilter(filter) {
    let query;

    try {
        if (Boolean.isValid(filter).stringFilled()){
            query = { _id: filter };
        } else if (Boolean.isValid(filter).object().eval()) {
            query = filter;
        } else {
            throw new Error.Log('common.bad_format_param',
                'filter',
                'treatFilter',
                ['String(ObjectId._id)', 'Object'],
                filter,
                'dbHelpers.js'
            );
        }
    } catch(err) {
        throw new Error.Log(err).append('helpers.treat_filter');
    }

    return query;
}

/**
 * Finds and returns fields in a Mongoose schema that are references to other schemas.
 * @param {Object} schema - The Mongoose schema object to search for related fields.
 * @param {Array} exclude - An array of field names to exclude from the result.
 * @param {number} levels - The number of levels to search for related fields (default is 1).
 * @param {number} currentLevel - The current level of recursion (default is 1).
 * @returns {Array} - An array of objects representing related fields and their respective schemas.
 * @throws {Error} - Throws an error if there is an issue analyzing the schema.
 */
function findRelFields(schema, exclude, levels, currentLevel) {
    if (!levels) levels = 1;
    if (!currentLevel) currentLevel = 1;
    let result = [];

    if ((currentLevel + 1) > levels) return result

    try {
        if (schema) {
            Object.keys(schema.obj || {}).map(key=>{
                const curr = schema.obj[key] || {};
                const typeOf = curr.type && curr.type.schemaName || typeof curr.type; 
                const isExclude = (!exclude || !exclude.find(x => x === key));
                const refSchema = curr.ref && mongoose.model(curr.ref).schema;
                
                if (Array.isArray(curr.type)) {
                    const type = curr.type.find(x => x.schemaName === 'ObjectId')
                    if (type && isExclude && refSchema) result.push({
                        path: key, 
                        populate: findRelFields(refSchema, exclude, levels, ++currentLevel)
                    });
                }
                else if (typeOf === 'ObjectId' && isExclude){
                    result.push({
                        path: key,
                        model: curr.ref,
                        populate: currentLevel < levels ? findRelFields(refSchema, exclude, levels, ++currentLevel) : undefined
                    });
                }
                else if (typeOf === 'object' && isExclude) {
                    const subRels = findRelFields({obj: curr.type}, exclude, levels, ++currentLevel);
                    result.push({path: key, populate: subRels});
                }
            });
    
            return result;
        } else {
            return new Error.Log('common.missing_params', 'schema', 'findRelFields', 'dbHelpers.js');
        }
    } catch(err) {
        throw new Error.Log(err).append('helpers.find_rel_fields');
    }
}

module.exports = {
    createCounter,
    increaseCounter,
    increaseLog,
    increaseDocProp,
    isCollectionExist,
    isDocExist,
    getCollectionModel,
    pickQueryType,
    treatFilter,
    findRelFields
};