javascript - Berbagi koneksi mongoose antara repositori dalam monorepo TypeScript
Struktur MonoRepo saya
Contoh lengkap dan kode yang dapat diuji ada di sini:
https://gist.github.com/SohamRoyNoel/c50fac02c0f07d3816e8f31797478013
Tujuan: Saya ingin memindahkan database dari level app1 dan app2 ke folder database umum. Jadi app1 dan app2 atau appN akan meneruskan string koneksi dan kelas dari repositori bersama akan membuat koneksi ke DB.
Masalah: Jika saya mengakses DB ini dengan menghubungkan ke common, model di app1 dan app2 tidak dimuat. Saya bisa melihat nilai readyState: 1, yang berarti mongo terhubung, tetapi saya tidak yakin mengapa modelnya tidak dimuat.
Saya tidak menggunakan Lerna atau NX. Ini adalah proyek TS sederhana. Setiap proyek memiliki tsConfig dan package.json tetapi semua kode/layanan umum ada di folder common.
Mono repo ini hanya berisi kode backend.
Sekarang cara sederhana saya menghubungkan ke DB dari tingkat aplikasi individu adalah seperti di bawah ini:
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;
}
}
}
Kemudian di app.ts dalam folder app1 dan app2, saya menangani seperti ini:
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;
}
Saya mengikuti pola repository, umumnya saya menyimpan repository dasar dan model dasar seperti berikut:
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) };
}
dan
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' });
}
}
Inilah cara saya memperluas kelas-kelas ini ke app1, app2, appN ...
interface XRepositoryInterface {}
export class XRepository extends BaseRepository<IX> implements XRepositoryInterface {
constructor() {
super(XModel);
}
}
dan
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);
Apakah ada yang bisa membantu saya dengan ini?
Jawaban #1
Masalahnya kemungkinan adalah, bahwa model mongoose didaftarkan per instance koneksi, dan dalam pengaturan monorepo di mana setiap aplikasi (app1, app2, dll.) mungkin memiliki instance sendiri, model-model tersebut tidak didaftarkan dengan benar. Anda perlu memastikan dengan sangat jelas untuk hanya menggunakan satu instance:
1) Modifikasi kelas common/Database Alih-alih mengandalkan instance Mongoose default, buat dan ekspor secara eksplisit satu koneksi yang dibagikan.
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 ) Di common/models/XModel.ts, pastikan bahwa model didaftarkan pada koneksi bersama.
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 ) pastikan XRepository.ts mengambil model dari koneksi bersama.
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 ) Pastikan app1/app.ts dan app2/app.ts menginisialisasi database sebelum menggunakan repositori.
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);
}
}
Karena initializeApp bersifat async tetapi dua konstruktor app.ts Anda tidak bisa - Anda dapat menginisialisasi koneksi db di latar belakang (itu akan akhirnya ada):
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);
}
}
Namun, lebih rapi jika membuat instance aplikasi dalam root-level await. Alih-alih const app=App.getInstance(); lakukan 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;
}
Pendekatan Alternatif (Menunggu DB secara Blokir)
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);
}
})();
Jawaban #2
Mengikuti langkah-langkah ini seharusnya menyelesaikan masalah model yang tidak memuat:
Tentukan model secara sinkron setelah koneksi database terjalin. Hapus pembungkus IIFE async di sekitar pembuatan model.
Berkas: 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);
Di kelas App Anda, panggil schema setelah membuat koneksi database untuk memastikan model terdaftar pada instance yang terhubung.
Berkas: Apps/app1/src/app.ts
private static initializeModels() {
console.log('✅ Registering Mongoose models...');
require('../schema/user.schema'); // This loads and defines models synchronously
}
Hapus inisialisasi model async dari repositori karena model sekarang didefinisikan secara sinkron.
Berkas: 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;
}
}
Pastikan layanan menginisialisasi repositori secara sinkron, menghindari konstruktor async.
File: 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;
Inisialisasi layanan secara sinkron karena mereka tidak lagi memerlukan pengaturan secara async.
Berkas: 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);
}
}
Pastikan koneksi database terhubung sebelum menginisialisasi model dan memulai aplikasi.
Berkas: 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();
Dengan menyusun kode untuk mendefinisikan model secara sinkron setelah membuat koneksi database, Anda memastikan bahwa semua dependensi diinisialisasi dengan benar dalam urutan yang tepat, sehingga menyelesaikan masalah model yang tidak dimuat.
