@nest-extended/mongoose
The Mongoose adapter: a generic CRUD service plus query helpers, ObjectId utilities and a MongoDB-aware exception filter.
$npm install @nest-extended/core @nest-extended/mongoose @nest-extended/decorators nestjs-cls
Key exports
| Export | Type | Purpose |
|---|---|---|
NestService<M, D, E> | class | CRUD service — _find, _get, _create, _patch, _remove, and their event-firing find / get / create / patch / remove counterparts |
nestify() | function | Apply $select / $populate / $sort / $limit / $skip to a query |
rawQuery() | function | Convert query params to a MongoDB filter (ObjectId + $regex aware) |
EnsureObjectId() | function | Validate / convert a string to an ObjectId |
GlobalExceptionFilter | filter | Handles HttpException, Mongoose, Zod and MongoServerError |
Usage
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { NestService } from '@nest-extended/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
@Injectable()
export class CatsService extends NestService<Cat, CatDocument> {
constructor(
@InjectModel(Cat.name) private readonly catModel: Model<CatDocument>,
) {
super(catModel);
}
}Keep the injected model on the instance (
private readonly catModel), as the generator does.NestServiceholds its own private copy, so this is how you reach Mongoose directly — see Raw queries and aggregation.
await catsService.find({
name: { $regex: 'kitty', $options: 'i' },
age: { $gt: 5 },
$populate: 'owner',
$sort: { createdAt: -1 },
$limit: 10,
});Service events
Since 1.5.0 this service also exposes find / get / create / patch / remove — the
same operations, but they dispatch to an attached events class. Name it as the last generic
to have emit() type-checked:
export class CatsService extends NestService<Cat, CatDocument, CatsEvents> {}See Service Events.
Raw queries and aggregation
find covers filtering, pagination and relations. For everything Mongoose can do beyond
that — aggregation pipelines, distinct, bulkWrite, change streams, transactions — use
the injected model directly. NestService never gets in the way.
@Injectable()
export class CatsService extends NestService<Cat, CatDocument> {
constructor(
@InjectModel(Cat.name) private readonly catModel: Model<CatDocument>,
) {
super(catModel);
}
/** Average age per breed, with the owner joined in. */
async statsByBreed() {
return this.catModel.aggregate([
{ $match: { deleted: { $ne: true } } },
{
$lookup: {
from: 'owners',
localField: 'owner',
foreignField: '_id',
as: 'owner',
},
},
{ $unwind: { path: '$owner', preserveNullAndEmptyArrays: true } },
{
$group: {
_id: '$breed',
avgAge: { $avg: '$age' },
total: { $sum: 1 },
owners: { $addToSet: '$owner.name' },
},
},
{ $sort: { avgAge: -1 } },
]);
}
/** Anything else on the model works the same way. */
async breeds() {
return this.catModel.distinct('breed', { deleted: { $ne: true } });
}
async retireOldCats(age: number) {
return this.catModel.bulkWrite([
{
updateMany: {
filter: { age: { $gte: age }, deleted: { $ne: true } },
update: { $set: { retired: true } },
},
},
]);
}
}Drop to the native driver with this.catModel.collection, and reach the connection —
for transactions or db.command() — with this.catModel.db.
Three things the adapter stops doing
Raw access bypasses the wrapper entirely, so it is on you to:
- Filter soft-deleted documents.
{ deleted: { $ne: true } }is exactly what the adapter merges in for you — see Soft Delete & Auditing. - Fire events. An aggregation is not
find, so no hook runs. Callthis.emit('statsByBreed', result)yourself if listeners should know — see Service Events. - Shape the response.
aggregate()returns a plain array, not the{ total, $limit, $skip, data }envelope, and its documents are not hydrated Mongoose documents unless you ask for them.
Generated schemas set
select: falseondeleted,deletedAt,deletedByandupdatedBy. That is a query projection — aggregation pipelines ignore it, so those fields do appear in$project/$groupoutput unless you exclude them.