import { MongoClient, Db, Collection, ObjectId, Filter, UpdateFilter } from 'mongodb';
// ============================================
// Configuration
// ============================================
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
const dbName = process.env.MONGODB_DB || 'myapp';
let client: MongoClient;
let db: Db;
export async function connectDB(): Promise<Db> {
if (db) return db;
client = new MongoClient(uri);
await client.connect();
db = client.db(dbName);
console.log('Connected to MongoDB');
return db;
}
export async function disconnectDB(): Promise<void> {
if (client) {
await client.close();
console.log('Disconnected from MongoDB');
}
}
// ============================================
// Types
// ============================================
export interface BaseDocument {
_id?: ObjectId;
createdAt?: Date;
updatedAt?: Date;
}
export interface User extends BaseDocument {
email: string;
name: string;
passwordHash: string;
role: 'user' | 'admin';
profile?: {
avatar?: string;
bio?: string;
};
}
export interface Post extends BaseDocument {
authorId: ObjectId;
title: string;
content: string;
tags: string[];
published: boolean;
views: number;
}
// ============================================
// Base Repository
// ============================================
export abstract class MongoRepository<T extends BaseDocument> {
protected collection!: Collection<T>;
constructor(protected collectionName: string) { }
protected async getCollection(): Promise<Collection<T>> {
if (!this.collection) {
const database = await connectDB();
this.collection = database.collection<T>(this.collectionName);
}
return this.collection;
}
async findById(id: string | ObjectId): Promise<T | null> {
const col = await this.getCollection();
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
return col.findOne({ _id: objectId } as Filter<T>);
}
async findAll(limit = 100, skip = 0): Promise<T[]> {
const col = await this.getCollection();
return col.find().sort({ createdAt: -1 }).skip(skip).limit(limit).toArray();
}
async create(data: Omit<T, '_id' | 'createdAt' | 'updatedAt'>): Promise<T> {
const col = await this.getCollection();
const doc = {
...data,
createdAt: new Date(),
updatedAt: new Date(),
} as T;
const result = await col.insertOne(doc as any);
return { ...doc, _id: result.insertedId } as T;
}
async update(id: string | ObjectId, data: Partial<T>): Promise<T | null> {
const col = await this.getCollection();
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
const updateDoc: UpdateFilter<T> = {
$set: { ...data, updatedAt: new Date() } as any
};
await col.updateOne({ _id: objectId } as Filter<T>, updateDoc);
return this.findById(objectId);
}
async delete(id: string | ObjectId): Promise<boolean> {
const col = await this.getCollection();
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
const result = await col.deleteOne({ _id: objectId } as Filter<T>);
return result.deletedCount > 0;
}
async count(filter: Filter<T> = {}): Promise<number> {
const col = await this.getCollection();
return col.countDocuments(filter);
}
}
// ============================================
// User Repository
// ============================================
export class UserRepository extends MongoRepository<User> {
constructor() {
super('users');
}
async findByEmail(email: string): Promise<User | null> {
const col = await this.getCollection();
return col.findOne({ email });
}
async findByRole(role: 'user' | 'admin'): Promise<User[]> {
const col = await this.getCollection();
return col.find({ role }).toArray();
}
async updateProfile(id: string, profile: User['profile']): Promise<User | null> {
return this.update(id, { profile } as Partial<User>);
}
async search(query: string): Promise<User[]> {
const col = await this.getCollection();
return col.find({
$or: [
{ name: { $regex: query, $options: 'i' } },
{ email: { $regex: query, $options: 'i' } }
]
}).limit(20).toArray();
}
async createIndexes(): Promise<void> {
const col = await this.getCollection();
await col.createIndex({ email: 1 }, { unique: true });
await col.createIndex({ name: 'text', email: 'text' });
}
}
// ============================================
// Post Repository
// ============================================
export class PostRepository extends MongoRepository<Post> {
constructor() {
super('posts');
}
async findByAuthor(authorId: string | ObjectId): Promise<Post[]> {
const col = await this.getCollection();
const objectId = typeof authorId === 'string' ? new ObjectId(authorId) : authorId;
return col.find({ authorId: objectId }).sort({ createdAt: -1 }).toArray();
}
async findPublished(limit = 20): Promise<Post[]> {
const col = await this.getCollection();
return col.find({ published: true }).sort({ createdAt: -1 }).limit(limit).toArray();
}
async findByTag(tag: string): Promise<Post[]> {
const col = await this.getCollection();
return col.find({ tags: tag, published: true }).toArray();
}
async incrementViews(id: string): Promise<void> {
const col = await this.getCollection();
await col.updateOne(
{ _id: new ObjectId(id) },
{ $inc: { views: 1 } }
);
}
async publish(id: string): Promise<Post | null> {
return this.update(id, { published: true });
}
async getPopular(limit = 10): Promise<Post[]> {
const col = await this.getCollection();
return col.find({ published: true }).sort({ views: -1 }).limit(limit).toArray();
}
async createIndexes(): Promise<void> {
const col = await this.getCollection();
await col.createIndex({ authorId: 1 });
await col.createIndex({ tags: 1 });
await col.createIndex({ published: 1, createdAt: -1 });
await col.createIndex({ title: 'text', content: 'text' });
}
}
// ============================================
// Aggregation Examples
// ============================================
export async function getPostStats() {
const col = (await connectDB()).collection<Post>('posts');
return col.aggregate([
{
$group: {
_id: '$published',
count: { $sum: 1 },
totalViews: { $sum: '$views' },
avgViews: { $avg: '$views' }
}
}
]).toArray();
}
export async function getTopAuthors(limit = 5) {
const col = (await connectDB()).collection<Post>('posts');
return col.aggregate([
{ $match: { published: true } },
{
$group: {
_id: '$authorId',
postCount: { $sum: 1 },
totalViews: { $sum: '$views' }
}
},
{ $sort: { postCount: -1 } },
{ $limit: limit },
{
$lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'author'
}
},
{ $unwind: '$author' }
]).toArray();
}
// ============================================
// Exports
// ============================================
export const userRepo = new UserRepository();
export const postRepo = new PostRepository();