All files / src/databases/mongo initMongoDb.ts

27.36% Statements 55/201
100% Branches 0/0
0% Functions 0/1
27.36% Lines 55/201

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 2011x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                                                                                                                                                                                                    
 
import mongoose from 'mongoose'
import { error } from '../../error'
import { mongoCreateDao } from './mongoCreateDao'
import { MongoDbConfigModels, MongoDbConfig, Definition, DbConfigsObj } from '../../types/core.types'
import { MongoDaoParsed, DaoMethodsMongo } from './types/mongoDbTypes'
import { C, ENV, objEntries } from 'topkat-utils'
import { luigi } from '../../cli/helpers/luigi.bot'
import { event } from '../../event'
import type { AllDbIds, DbIds } from '../../cache/dbs/index.generated'
import { newSystemCtx } from '../../ctx'
 
const { NODE_ENV } = ENV()
const env: Env = NODE_ENV
 
declare global {
    interface GDeventNames extends NewEventType<'database.connected', []> { }
}
 
type ErrParams = Parameters<typeof error.serverError>
 
export type ModelAdditionalFields = {
    /** Will init a mongoose session and start a mongo transaction, the session is then stored in the ctx
    so all next DB calls are automatically using the transaction. So you'll have nothing to do except ending
    the transaction, which will trigger an admin alert if not closed for 30 seconds
    * * Make sure you await it
    */
    startTransaction(ctx: Ctx): Promise<void>, // mongoose.mongo.ClientSession could be returned but it's of no use and can be misleading
    /** You have to call it after starting a transaction whenever the transaction is success or
    in a try / catch block to undo the transaction
    * * The endTransaction will take care of transactionCommit() or transactionAbort() depending on the status
    * * Make sure you await it
    */
    endTransaction(ctx: Ctx, status: 'success'): Promise<void>,
    endTransaction(ctx: Ctx, status: 'error', errMsg: ErrParams[0], errOptions: ErrParams[1]): Promise<void>,
    endTransaction(ctx: Ctx, status: 'error', errMsg: false): Promise<void>,
    mongooseConnection: mongoose.Connection
    mongooseModels: { [modelNames: string]: mongoose.Model<any> }
}
 
export type ModelsConfigCache<AllModels extends Record<string, any> = any> = {
    [dbId: string]: {
        db: {
            [ModelName in keyof AllModels]: DaoMethodsMongo<AllModels[ModelName]>
        } & ModelAdditionalFields
        dbConfigs: MongoDbConfig
    }
}
 
let nbDatabaseConnected = 0
let nbDatabaseTotal = 0
let displayConnexionWarning1 = true
let displayConnexionWarning2 = true
 
export async function mongoInitDb(
    dbName: keyof DbIds,
    dbId: AllDbIds,
    modelsConfigCache: ModelsConfigCache,
    connectionConfig: Omit<DbConfigsObj, 'connexionString'> & { connexionString: string },
    daoConfigsParsed: { [k: string]: MongoDaoParsed<any> },
    modelsGenerated: { [modelName: string]: Definition<any, 'def', 'def', false> }
) {

    nbDatabaseTotal++

    const { connexionString, mongooseOptions = {} } = connectionConfig


    const isLocalDb = connexionString.includes('127.0.0.1') || connexionString.includes('localhost')
    const hasNoReplicaSet = isLocalDb && !connexionString.includes('replicaSet')

    //----------------------------------------
    // MONGO SETUP AND CONNEXION
    //----------------------------------------

    mongooseOptions.connectTimeoutMS ??= env !== 'production' && env !== 'preprod' ? env === 'build' ? 2147483647 : 30000 : 1000 * 60 * 7 // avoid error when setting a breakpoint
    const mongooseConnection = mongoose.createConnection(connexionString, mongooseOptions)

    setTimeout(() => {
        if (displayConnexionWarning1) {
            luigi.warn(`Loading database for 5.000.000.000 nanoseconds...\n\nHave you started your db ?`)
            displayConnexionWarning1 = false
        }
    }, 5000)

    setTimeout(() => {
        if (displayConnexionWarning2) {
            luigi.warn(`🔮 blip...bloup...checking my crystal ball...Mmmh....I feel a database connexion error will throw soon...`)
            displayConnexionWarning2 = false
        }
    }, 20000)

    mongooseConnection.on('error', err => {
        const lessVerboseErr = { message: err?.message }
        if (env !== 'build') {
            error.serverError(`mongoDatabaseConnexionError`, { err: lessVerboseErr, dbId, dbName })
            C.log('\n\n')
            luigi.say([
                `Senior advice here => please check that you have a database running at ${connexionString.replace(/:[^@]+@/, '****************')}.\nTips: Use 'run-rs' npm package to easily start mongoDb with replica sets locally.\n\n`,
                `Blip..bloup... There is 94% chances that you forget to start your database.\nPlease check that you have a database running at ${connexionString.replace(/:[^@]+@/, '****************')}.\nTips: Use 'run-rs' npm package to easily start mongoDb with replica sets locally.\n\n`,
            ])
        }
    })

    mongooseConnection.on('connected', () => {
        C.log(C.primary(`✓ DB connected: ${dbId} > ${connexionString.includes('127.0.0') ? 'localhost' : connexionString?.split('@')?.[1]}${connexionString.replace(/^.*(\/[^/]+)$/, '$1').replace(/\?[^?]+$/, '')}`))
        nbDatabaseConnected++
        if (nbDatabaseConnected >= nbDatabaseTotal) {
            displayConnexionWarning1 = false
            displayConnexionWarning2 = false
            event.emit('database.connected', newSystemCtx())
        }

    })

    const schemas = {} as { [k in AllDbIds]: mongoose.Schema }
    const mongooseModels = {} as { [k in AllDbIds]: mongoose.Model<any> }
    const typedDatabase = {} as { [k in AllDbIds]: Awaited<ReturnType<typeof mongoCreateDao>> }
    const dbConfs: MongoDbConfigModels = {}

    for (const [modelName, models] of objEntries(modelsGenerated)) {
        //----------------------------------------
        // SETUP SCHEMAS
        //----------------------------------------
        schemas[modelName] = new mongoose.Schema(models._getMongoType())
        if (process.env.NODE_ENV !== 'build') mongooseModels[modelName] = mongooseConnection.model(modelName, schemas[modelName]) as any

        //----------------------------------------
        // BUILD DAO
        //----------------------------------------
        typedDatabase[modelName] = await mongoCreateDao(mongooseModels[modelName], dbId, dbName, modelName as any)

        //----------------------------------------
        // BUILD DB CONFIGS
        //----------------------------------------
        dbConfs[modelName] = {
            model: modelsGenerated[modelName],
            dao: typedDatabase[modelName],
            daoConfig: daoConfigsParsed[modelName],
        }
    }


    modelsConfigCache[dbId] ??= {} as ModelsConfigCache[string]
    modelsConfigCache[dbId].dbConfigs = {
        dbType: 'mongo',
        models: dbConfs,
        mongooseConnection,
        schemas,
        mongooseModels,
        daoConfigsParsed,
    } satisfies Omit<MongoDbConfig, 'modelTypeFile'>

    const modelAdditionalFields: ModelAdditionalFields = {
        startTransaction: async ctx => {
            if (hasNoReplicaSet) {
                if (ctx.env !== 'development') {
                    throw ctx.error.serverError('cannotRunAtransactionWithNoReplicaSetInDatabase')
                } else {
                    return C.warning('!!WARNING!! ReplicaSet not activated. Please use `run-rs -v 4.0.0 --shell -h 127.0.0.1` to start the database in local')
                }
            }
            if (ctx.transactionSession) throw ctx.error.serverError('mongooseTransactionAlreadyInProgressWithSameCtx')
            const session = await mongooseConnection.startSession()
            ctx.transactionSession = session
            setTimeout(() => {
                // if a transaction is taking too much time to process, we alert the administrators
                // we don't throw since it's probably a sensitive operation in progress and we don't
                // want to mess it up
                if (ctx.transactionSession) ctx.error.serverError('mongooseTransactionTimeout')
            }, 30 * 1000)
            await session.startTransaction()
        },
        endTransaction: async (ctx, status = 'success', ...params) => {
            if (!ctx.transactionSession) {
                const [errMsg, errOptions = {}] = params as ErrParams
                ctx.error.serverError(errMsg, errOptions)
                throw ctx.error.serverError('mongooseTransactionNotStarted', { additionalInfos: `This can be because you ended transaction twice (if you are in a try catch check that you don't have ended in the body and in the catch clause` })
            }
            const session = ctx.transactionSession
            delete ctx.transactionSession
            if (hasNoReplicaSet) return
            if (status === 'error') {
                await session.abortTransaction()
                await session.endSession()
                const [errMsg, errOptions = {}] = params as ErrParams
                throw ctx.error.serverError(errMsg, errOptions)
            } else {
                await session.commitTransaction()
                await session.endSession()
            }
        },
        mongooseConnection,
        mongooseModels,
    }

    modelsConfigCache[dbId].db = {
        ...typedDatabase,
        ...modelAdditionalFields as any,
    }
}