javascript - Zdieľať Mongoose pripojenie medzi repozitármi v TypeScript monorepe


Moja štruktúra MonoRepo

Úplný príklad a testovateľný kód je tu:

https://gist.github.com/SohamRoyNoel/c50fac02c0f07d3816e8f31797478013

Zámer: Chcem vytiahnuť databázu z úrovne app1 a app2 a umiestniť ju do spoločného priečinka databázy. Takže app1 a app2 alebo appN odovzdajú reťazec pripojenia a trieda zo zdieľaného repozitára nadviaže spojenie s databázou.

Problém: Ak získam túto databázu pripojením sa na common, nenačítava modely v app1 a app2. Vidím hodnotu readyState: 1, čo znamená, že Mongo je pripojené, ale nie som si istý, prečo sa modely nenačítavajú.

Nepoužívam Lerna ani NX. Je to jednoduchý TS projekt. Každý projekt má tsConfig a package.json, ale všetky spoločné kódy/služby sú v priečinku common.
Tento mono repozitár obsahuje iba backendový kód.

Teraz je jednoduchý spôsob, ako sa pripájam k databáze z úrovne jednotlivých aplikácií, nasledovný:

const dbConnectionConfig = {
    url: MONGO_URI ?? '',
    options: {
        dbName: MASTER_DATABASE_NAME,
    },
};

export class Database {
    private static instance: Database;
    private isConnected: boolean = false;
    private constructor() {}
    public static async getInstance(): Promise<Database> {
        if (!Database.instance) {
            Database.instance = new Database();
            await Database.instance.establishConnection();
        }
        return Database.instance;
    }
    private async establishConnection(): Promise<void> {
        if (this.isConnected) return;
        try {
            if (NODE_ENV !== 'production') { set('debug', true) }
            await connect(dbConnectionConfig.url, dbConnectionConfig.options);
            this.isConnected = true;
        } catch (err) {
            throw err;
        }
    }
}

Potom v app.ts v priečinkoch app1 a app2 to riešim takto:

    public static getInstance({ routes = [] }: { routes?: IRoute[] } = {}): App {
        if (!App.instance) {
            App.instance = new App(routes);
            Database.getInstance().catch((err) => {
                logger.error('Database connection failed', err);
                process.exit(1);
            });
        }
        return App.instance;
    }

Nasledujem vzor repository, vo všeobecnosti uchovávam základnú repository a základné modely takto:

export class BaseRepository<T> implements IBaseRepository<T> {
    private model: Model<T>;
    constructor(model: Model<T>) { this.model = model }
    async findAll(query: any): Promise<T[]> { return this.model.find(query) };
}

a

export class BaseSchema<T extends { [key: string]: any } = any> extends Schema {
    constructor(schema: Record<string, any> = {}) {
        const baseFields = {
            isDeleted: { type: SchemaTypes.Boolean, required: true, default: false },
            createdBy: { type: SchemaTypes.String, required: false },
            updatedBy: { type: SchemaTypes.String, required: false },
        };
        const combinedSchema = { ...baseFields, ...schema };
        super(combinedSchema, { timestamps: true, discriminatorKey: 'kind' });
    }
}

Týmto spôsobom rozširujem tieto triedy do app1, app2, appN ...

interface XRepositoryInterface {}
export class XRepository extends BaseRepository<IX> implements XRepositoryInterface {
    constructor() {
        super(XModel);
    }
}

a

const XSchema = new BaseSchema<IX>({
    userId: { type: SchemaTypes.String, ref: 'user', required: true },
    version: { type: SchemaTypes.String, required: false },
});
export const XModel = model<IX>(COLLECTION_NAME.X, XSchema);

Môže mi s tým niekto prosím pomôcť?

Odpoveď č. 1

Problém pravdepodobne spočíva v tom, že mongoose modely sú registrované na každú inštanciu pripojenia, a v nastavení monorepa, kde každá aplikácia (app1, app2, atď.) môže mať svoju vlastnú inštanciu, modely nie sú registrované správne. Budete musieť veľmi dôrazne zabezpečiť, aby sa používala iba jedna inštancia:

1 ) Upraviť triedu common/Database Namiesto spoliehania sa na predvolenú inštanciu Mongoose výslovne vytvorte a exportujte jedno spoločné pripojenie.

export class Database {
    private static instance: Database;
    private connection!: Connection;
    private constructor() {}

    public static async getInstance(): Promise<Database> {
        if (!Database.instance) {
            Database.instance = new Database();
            await Database.instance.establishConnection();
        }
        return Database.instance;
    }

    private async establishConnection(): Promise<void> {
        if (this.connection && this.connection.readyState === 1) return;
        try {
            this.connection = await mongoose.createConnection(dbConnectionConfig.url, dbConnectionConfig.options);
            console.log('MongoDB connected successfully.');
        } catch (err) {
            console.error('MongoDB connection failed:', err);
            throw err;
        }
    }

    public getConnection(): Connection {
        if (!this.connection) throw new Error("Database connection is not established yet.");
        return this.connection;
    }
}

2) V súbore common/models/XModel.ts sa uistite, že modely sú registrované na zdieľanom pripojení.

import { Schema, SchemaTypes, Connection, Model, Document } from 'mongoose';
import { Database } from '../database';

interface IX extends Document {
    userId: string;
    version?: string;
    isDeleted?: boolean;
    createdBy?: string;
    updatedBy?: string;
}

const XSchema = new Schema<IX>({
    userId: { type: SchemaTypes.String, ref: 'user', required: true },
    version: { type: SchemaTypes.String, required: false },
    isDeleted: { type: SchemaTypes.Boolean, default: false },
    createdBy: { type: SchemaTypes.String, required: false },
    updatedBy: { type: SchemaTypes.String, required: false },
}, { timestamps: true });

export async function getXModel(): Promise<Model<IX>> {
    const db = await Database.getInstance();
    const connection = db.getConnection();

    // Ensure model is only registered once per connection
    return connection.models['X'] || connection.model<IX>('X', XSchema);
}

3 ) zabezpečte, aby XRepository.ts získaval model z zdieľaného pripojenia.

import { Model } from 'mongoose';
import { BaseRepository } from '../base/BaseRepository';
import { getXModel } from '../models/XModel';

export class XRepository extends BaseRepository<IX> {
    private static instance: XRepository;
    private constructor(model: Model<IX>) { super(model); }

    public static async getInstance(): Promise<XRepository> {
        if (!XRepository.instance) {
            const model = await getXModel();
            XRepository.instance = new XRepository(model);
        }
        return XRepository.instance;
    }
}

4) Uistite sa, že app1/app.ts a app2/app.ts inicializujú databázu pred použitím repozitárov.

import { Database } from '../common/database';
import { XRepository } from '../common/repositories/XRepository';

// this must be called before any app is started
export async function initializeApp() {
    try {
        // Ensure database is connected
        await Database.getInstance();

        // Initialize repositories
        const xRepository = await XRepository.getInstance();

        console.log('App initialized successfully');
    } catch (error) {
        console.error('App initialization failed:', error);
        process.exit(1);
    }
}

Pretože initializeApp je asynchrónna, ale vaše dva konštruktory app.ts nemôžu byť - môžete inicializovať pripojenie k databáze na pozadí (nakoniec tam bude):

public static getInstance({ routes = [] }: { routes?: IRoute[] } = {}): App {
    if (!App.instance) {
        App.instance = new App(routes);
        App.instance.initializeAsync(); // <---
    }
    return App.instance;
}

private async initializeAsync() {
    try {
        await Database.getInstance();
        logger.info('DB connected');
    } catch (err) {
        logger.error('DB connection failed', err);
        process.exit(1);
    }
}

Avšak čistejšie by bolo vytvoriť inštancie aplikácie v await na úrovni koreňa. Namiesto const app=App.getInstance(); urobte const app=await App.createInstance();:

public static async createInstance({ routes = [] }: { routes?: IRoute[] } = {}): Promise<App> {
    if (!App.instance) {
        App.instance = new App(routes);
        try {
            await Database.getInstance();
            logger.info('Database connected successfully');
        } catch (err) {
            logger.error('Database connection failed', err);
            process.exit(1);
        }
    }
    return App.instance;
}

Alternatívny prístup (Blokujúce čakanie na DB)

Common/src/database/mongo/index.ts
import mongoose, { Connection } from 'mongoose';

const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/mydatabase';

let connected: Connection | null = null;
let isConnecting = true;

// IIFE to get a db connection singleton
(async () => {
    try {
        if (!connected) {
            const db = await mongoose.connect(MONGO_URI, {
                useNewUrlParser: true,
                useUnifiedTopology: true,
            });

            connected = db.connection;
            isConnecting = false; // Mark as initialized
            console.log(`MongoDB Connected: ${db.connection.host}`);

            db.connection.on('open', () => {
                console.log('MongoDB connection is open.');
                console.log('Registered models:', Object.keys(mongoose.models));
            });

            db.connection.on('error', (err) => {
                console.error('MongoDB error:', err);
                process.exit(1);
            });
        }
        return connected;
    } catch (error) {
        console.error('MongoDB connection error:', error);
        process.exit(1);
    }
})();

// Helper function to get the connection synchronously with blocking wait
export const connectDB = (): Connection => {
    if (!connected) {
        let patience = 11;

        while (isConnecting && --patience) {
            console.log('Waiting for MongoDB connection...');
            const now = Date.now();
            while (Date.now() - now < 1000) {} // waste time
        }
    
        if (!connected) {
            throw new Error('Database not initialized. Ensure connectDB is awaited before use.');
        }
    }
    return connected;
};

server.ts:

import { App } from './app';
import { connectDB } from '../Common/src/database/mongo/index'

(async function bootstrap() {
    try {
        connectDB(); // wait for db
        const app = await App.getInstance({ routes: Routes });
        app.listen();
    } catch (err) {
        console.error('Failed to start application:', err);
        process.exit(1);
    }
})();

Odpoveď č. 2

Nasledovaním týchto krokov by sa mal vyriešiť problém s nenačítavaním modelov:

Definujte modely synchronne, keď je nadviazané pripojenie k databáze. Odstráňte asynchrónne IIFE obaly okolo vytvárania modelov.

Súbor: Apps/app1/src/schema/user.schema.ts

import { model } from 'mongoose';
import { BaseSchema } from '@common/base.schema';
import { COLLECTION_NAME } from 'const/database/collection.const';
import { IUser } from '../interfaces/user.interface';

const userSchema = new BaseSchema<IUser>({
    mobileNumber: {
        type: String,
        required: true,
    }
});

userSchema.index({ mobileNumber: 1 });

// Synchronously define and export the model
export const UserModel = model<IUser>(COLLECTION_NAME.USER, userSchema);

Vo vašej triede App vyžadujte schémy po nadviazaní pripojenia k databáze, aby ste zaistili, že modely sú zaregistrované na pripojenej inštancii.

Súbor: Apps/app1/src/app.ts

private static initializeModels() {
    console.log('✅ Registering Mongoose models...');
    require('../schema/user.schema'); // This loads and defines models synchronously
}

Odstráňte asynchrónnu inicializáciu modelu z repozitárov, pretože modely sú teraz definované synchronne.

Súbor: Apps/app1/src/repositories/user.repository.ts

import { UserModel } from '../schema/user.schema';
import { BaseRepository } from '@common/base.repository';
import { IUser } from '../interfaces/user.interface';

export class UserRepository extends BaseRepository<IUser> {
    private static instance: UserRepository;

    private constructor() {
        super(UserModel); // Directly use the synchronously defined model
    }

    public static getInstance(): UserRepository {
        if (!UserRepository.instance) {
            UserRepository.instance = new UserRepository();
        }
        return UserRepository.instance;
    }
}

Zabezpečte, aby sa služby inicializovali repozitára synchronne, vyhýbajúc sa asynchrónnym konštruktorom.

Súbor: Apps/app1/src/services/user.service.ts

import { UserRepository } from '../repositories/user.repository';

class UserService {
    private userRepository: UserRepository;

    constructor() {
        this.userRepository = UserRepository.getInstance(); // Synchronous initialization
    }

    public async findUserByB2CId(id: string): Promise<IUser> {
        return this.userRepository.findOne({ b2cUserId: id, isActive: true });
    }
}

export default UserService;

Inicializujte služby synchronne, pretože už nevyžadujú asynchrónne nastavenie.

Súbor: Apps/app1/src/routes/user.route.ts

export class UserRoute implements IRoute {
    public path = '/api/user';
    public router: Router = Router();
    public userService = new UserService();
    public userController = new UserController(this.userService);

    constructor() {
        this.initializeRoutes();
    }

    private initializeRoutes() {
        this.router.get(`${this.path}/b2c/:id`, this.userController.getUserByB2cId);
    }
}

Uistite sa, že spojenie s databázou je nadviazané pred inicializáciou modelov a spustením aplikácie.

Súbor: Apps/app1/src/server.ts

async function bootstrap() {
    try {
        // Establish database connection first
        await Database.getInstance();
        const app = await App.getInstance({ routes: Routes });
        app.listen();
    } catch (err) {
        console.error('Failed to start application:', err);
        process.exit(1);
    }
}

bootstrap();

Štruktúrovaním kódu tak, aby sa modely definovali synchronne po nadviazaní spojenia s databázou, zabezpečíte, že všetky závislosti sú správne inicializované v správnom poradí, čím sa vyrieši problém s nenačítanými modelmi.

Súvisiace články