import { Model, FilterQuery, UpdateQuery, Types, Document, QueryOptions, UpdateResult, DeleteResult, ClientSession } from 'mongoose'; import { UpdateOptions } from 'mongodb'; export interface WithId { _id: Types.ObjectId; id?: string; } export interface SoftDeletable { deleted?: boolean; deletedAt?: Date; } export interface RepositoryOptions { useSoftDelete?: boolean; } export interface PaginationOptions { page: number; pageSize: number; sort?: Record; } export interface PaginationResult { items: T[]; total: number; page: number; pageSize: number; totalPages: number; hasNext: boolean; hasPrev: boolean; } export interface SearchOptions { fields: string[]; query: string; caseSensitive?: boolean; } export type SoftDeleteResult = { type: 'soft'; result: UpdateResult | null }; export type HardDeleteResult = { type: 'hard'; result: DeleteResult | null }; export function toObjectId(id: string | Types.ObjectId): Types.ObjectId { if (id instanceof Types.ObjectId) { return id; } return new Types.ObjectId(id); } export function validateObjectId(id: string): boolean { return Types.ObjectId.isValid(id); } export class BaseRepository> { protected options: RepositoryOptions; constructor( protected readonly model: Model, options: RepositoryOptions = { useSoftDelete: true } ) { this.options = { useSoftDelete: true, ...options }; } protected buildFilter(filter: FilterQuery = {}): FilterQuery { if (this.options.useSoftDelete) { return { ...filter, deleted: { $ne: true } } as any; } return filter; } protected validateId(id: string | Types.ObjectId): void { if (typeof id === 'string' && !validateObjectId(id)) { throw new Error(`Invalid ObjectId: ${id}`); } } toObjectId(id: string | Types.ObjectId): Types.ObjectId { return toObjectId(id); } async findById(id: string | Types.ObjectId, projection: any = {}, options: QueryOptions = {}): Promise { this.validateId(id); return this.model.findOne(this.buildFilter({ _id: toObjectId(id) }), projection, options).exec() as unknown as Promise; } async findByIds(ids: string[] | Types.ObjectId[], projection: any = {}, options: QueryOptions = {}): Promise { ids.forEach(id => this.validateId(id)); return this.model.find(this.buildFilter({ _id: { $in: ids.map(toObjectId) } }), projection, options).exec() as unknown as Promise; } async find(filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}): Promise { return this.model.find(this.buildFilter(filter), projection, options).exec() as unknown as Promise; } async findOne(filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}): Promise { return this.model.findOne(this.buildFilter(filter), projection, options).exec() as Promise; } async findQuery(filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}): Promise { return this.model.find(this.buildFilter(filter), projection, options).exec() as unknown as Promise; } async search(searchOptions: SearchOptions, filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}): Promise { const { fields, query, caseSensitive = false } = searchOptions; const searchConditions = fields.map(field => ({ [field]: { $regex: query, $options: caseSensitive ? '' : 'i' } })); const searchFilter = { ...filter, $or: searchConditions }; return this.find(searchFilter, projection, options); } async findWithSort( filter: FilterQuery = {}, sort: Record = { createdAt: -1 }, projection: any = {}, options: QueryOptions = {} ): Promise { return this.find(filter, projection, { sort, ...options }); } async count(filter: FilterQuery = {}): Promise { return this.model.countDocuments(this.buildFilter(filter)).exec(); } async exists(filter: FilterQuery = {}): Promise { return !!(await this.model.exists(this.buildFilter(filter))); } async create(doc: Partial, session?: ClientSession): Promise { if (session) { const [result] = await this.model.create([doc], { session }); return result as T; } return this.model.create(doc) as Promise; } async createMany(docs: Partial[], session?: ClientSession): Promise { const options = session ? { session } : {}; return this.model.insertMany(docs, options) as unknown as Promise; } async updateById( id: string | Types.ObjectId, update: UpdateQuery, options: UpdateOptions & QueryOptions = { new: true, timestamps: true } ): Promise { this.validateId(id); return this.model.findByIdAndUpdate(toObjectId(id), update, options).exec() as unknown as Promise; } async updateOne( filter: FilterQuery, update: UpdateQuery, options: UpdateOptions & QueryOptions = { timestamps: true } ): Promise { return this.model.updateOne(this.buildFilter(filter), update, options).exec() as unknown as Promise; } async updateMany( filter: FilterQuery, update: UpdateQuery, options: UpdateOptions & QueryOptions = { timestamps: true } ): Promise { return this.model.updateMany(this.buildFilter(filter), update, options).exec() as unknown as Promise; } async updateManyByIds(ids: string[] | Types.ObjectId[], update: UpdateQuery, options: UpdateOptions & QueryOptions = { timestamps: true }): Promise { ids.forEach(id => this.validateId(id)); return this.model.updateMany(this.buildFilter({ _id: { $in: ids.map(toObjectId) } }), update, options).exec() as unknown as Promise; } async removeById(id: string | Types.ObjectId): Promise { this.validateId(id); if (this.options.useSoftDelete) { return this.softDeleteById(id); } return this.model.findByIdAndDelete(toObjectId(id)).exec(); } async removeMany(filter: FilterQuery): Promise { if (this.options.useSoftDelete) { return { type: 'soft', result: await this.softDeleteMany(filter) as unknown as UpdateResult | null }; } return { type: 'hard', result: await this.model.deleteMany(filter).exec() as unknown as DeleteResult | null }; } async paginate( filter: FilterQuery = {}, paginationOptions: PaginationOptions, projection: any = {}, options: QueryOptions = {}, ): Promise> { const { page, pageSize, sort = { createdAt: -1 } } = paginationOptions; const skip = (page - 1) * pageSize; const [items, total] = await Promise.all([ this.find(filter, projection, { ...options, skip, limit: pageSize, sort }), this.count(filter), ]); const totalPages = Math.ceil(total / pageSize); return { items, total, page, pageSize, totalPages, hasNext: page < totalPages, hasPrev: page > 1, }; } async aggregate(pipeline: any[], session?: ClientSession): Promise { if (this.options.useSoftDelete) { pipeline.unshift({ $match: { deleted: { $ne: true } } }); } const options = session ? { session } : {}; return this.model.aggregate(pipeline, options).exec(); } async groupBy( groupField: string, filter: FilterQuery = {}, countField = 'count' ): Promise { return this.aggregate([ { $match: filter }, { $group: { _id: `$${groupField}`, [countField]: { $sum: 1 } } }, { $sort: { [countField]: -1 } } ]); } async distinct(field: string, filter: FilterQuery = {}): Promise { return this.model.distinct(field, this.buildFilter(filter)).exec(); } async bulkWrite(ops: any[], session?: ClientSession): Promise { const options = session ? { session } : {}; return this.model.bulkWrite(ops, options); } async findNear( coordinates: [number, number], maxDistance = 5000, filter: FilterQuery = {}, ): Promise { return this.model.find({ ...this.buildFilter(filter), location: { $near: { $geometry: { type: 'Point', coordinates }, $maxDistance: maxDistance, }, }, }).exec(); } async findOneAndUpdate( filter: FilterQuery, update: UpdateQuery, options: QueryOptions = {}, ): Promise { return this.model.findOneAndUpdate( this.buildFilter(filter), update, { new: true, ...options }, ).exec() as unknown as Promise; } async upsert( filter: FilterQuery, update: UpdateQuery, docIfNew: Partial = {}, ): Promise { return this.model.findOneAndUpdate( this.buildFilter(filter), { $set: update, $setOnInsert: docIfNew }, { upsert: true, new: true }, ).exec() as unknown as Promise; } async findOrCreate( filter: FilterQuery, docIfNew: Partial = {}, ): Promise { let item = await this.findOne(filter); if (!item) item = await this.create(docIfNew); return item; } async incrementById(id: string | Types.ObjectId, field: string, value: number = 1): Promise { this.validateId(id); return this.model.findByIdAndUpdate( toObjectId(id), { $inc: { [field]: value } } as any, { new: true }, ).exec(); } async decrementById(id: string | Types.ObjectId, field: string, value: number = 1): Promise { return this.incrementById(id, field, -value); } async incrementMany(filter: FilterQuery, field: string, value: number = 1): Promise { return this.updateMany(filter, { $inc: { [field]: value } } as any); } async findRandom(filter: FilterQuery = {}, limit = 1): Promise { return this.aggregate([ { $match: filter }, { $sample: { size: limit } } ]); } async findAll( filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}, ): Promise { return this.model.find(filter, projection, options).exec() as unknown as Promise; } async softDeleteById(id: string | Types.ObjectId): Promise { if (!this.options.useSoftDelete) { throw new Error('Soft delete is not enabled for this repository'); } this.validateId(id); const update: any = { deleted: true, deletedAt: new Date() }; return this.model.findByIdAndUpdate(toObjectId(id), update, { new: true }).exec(); } async softDeleteMany(filter: FilterQuery): Promise { if (!this.options.useSoftDelete) { throw new Error('Soft delete is not enabled for this repository'); } const update: any = { deleted: true, deletedAt: new Date() }; return this.model.updateMany(this.buildFilter(filter), update).exec() as unknown as Promise; } async restoreById(id: string | Types.ObjectId): Promise { if (!this.options.useSoftDelete) { throw new Error('Soft delete is not enabled for this repository'); } this.validateId(id); return this.model.findByIdAndUpdate(toObjectId(id), { deleted: false, deletedAt: null }, { new: true }).exec(); } async restoreMany(filter: FilterQuery): Promise { if (!this.options.useSoftDelete) { throw new Error('Soft delete is not enabled for this repository'); } return this.model.updateMany( { ...filter, deleted: true } as any, { deleted: false, deletedAt: null } ).exec() as unknown as Promise; } async findDeleted( filter: FilterQuery = {}, projection: any = {}, options: QueryOptions = {}, ): Promise { if (!this.options.useSoftDelete) { throw new Error('Soft delete is not enabled for this repository'); } return this.model.find({ ...filter, deleted: true } as any, projection, options).exec() as unknown as Promise; } async hardDeleteById(id: string | Types.ObjectId): Promise { this.validateId(id); return this.model.findByIdAndDelete(toObjectId(id)).exec(); } async hardDeleteMany(filter: FilterQuery): Promise { return this.model.deleteMany(filter).exec() as unknown as Promise; } async getStats(filter: FilterQuery = {}): Promise<{ total: number; active: number; deleted: number; }> { const [total, active, deleted] = await Promise.all([ this.model.countDocuments(filter).exec(), this.count(filter), this.options.useSoftDelete ? this.model.countDocuments({ ...filter, deleted: true }).exec() : 0 ]); return { total, active, deleted }; } async findDuplicates(field: string): Promise { return this.aggregate([ { $group: { _id: `$${field}`, count: { $sum: 1 }, docs: { $push: '$_id' } } }, { $match: { count: { $gt: 1 } } } ]); } }