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.tsimport 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.
