mongest-nestjs-service
v0.3.1
Published
Delightfully-typed Mongoose-wrapper NestJS service
Downloads
28
Readme
Mongest Service (BETA)
Delightfully-typed Mongoose-wrapper NestJS-service
This is a BETA, and therefore you may encounter bugs. Please post an issue if needed.
Note: If you happen to use GraphQL and optionally ReactAdmin, use also ra-data-graphql-simple-mongest-resolver, which includes automatic resolver boilerplates.
TL;DR
- NestJS Service that delicately wraps Mongoose methods for your favorite entities.
- All returned documents are leans, but casted as instance of their entity class.
- Amazing discriminator-based polymorphism!
- Precise and safe typing in and out for all mongoose functions (sensitive to projection!).
- Fully overridable and expandable NestJS service.
Setup
Install (if not already there) the peer dependencies:
npm install --save mongodb mongoose @nestjs/mongoose
Then install the mongest-nestjs-service
lib:
npm install --save mongest-nestjs-service
Now you can create your entity and your service:
@Schema()
export class Cat {
_id!: ObjectId;
@Prop({ required: true, type: String })
name!: string;
}
@Injectable()
export class CatsService extends BuildMongestNestJsService(Cat) {}
// or...
@Injectable()
export class CatsService extends BuildMongestNestJsService(Cat) {
constructor(@InjectModel(Cat.name) public model: Model<Cat>) {
// If you ever need to override the constructor (e.g. to add additional dependencies),
// dont forget to explicitely inject the model to super().
super(model);
}
async myCustomMethod(): Promise<Cat[]> {
return await this.find({ name: 'pogo' });
}
}
Features
Better method argument typing
Mongoose is often too lineant on what parameters are accepted (e.g. the QueryOptions
god-object).
Mongest service methods only accept options that are really supported by the mongo driver. In addition, pagination and sorting can be expressed in a more concise way.
const cats = await catService.find(
{ name: /pogo/i },
{
projection: { name: 1 },
skip: 1,
limit: 1,
sort: { name: 1 }
},
);
Better method lean return typing
Mongoose return typing is often too constraining for no apparent good reason. It is also too linear when using projections.
Mongest service documents are always lean instances of the entity class (except aggregate()
method).
In addition, when a projection is used, the return type automatically excludes the non-projected fields, so that you will encounter a typing error if you try to use them by mistake.
Projected typing supports local field reference ({foo: '$bar'}
), string substitution ({foo: 'bar'}
), and operators $
, $slice
, $elemMatch
. See more about Mongest Projection.
const cat = await catService.findOne(
{ name: /pogo/i },
{
projection: { name: 1 },
},
);
if (cat) {
const isCatInstance = cat instanceof Cat; // true
const age = cats.age; // << TypeError: Property 'age' does not exist on type '{ name: string; _id: ObjectId; }'
}
Polymorphism
If you use mongoose schema discriminators, Mongest service will cast each document according to its type.
export enum CatKind {
StrayCat = 'StrayCat',
HomeCat = 'HomeCat',
}
@Schema({ discriminatorKey: 'kind' })
export class Cat {
kind!: CatKind;
@Prop({ required: true, type: String })
name!: string;
}
@Schema()
export class StrayCat extends Cat {
@Prop({ required: true, type: Number })
territorySize!: number;
}
@Schema()
export class HomeCat extends Cat {
@Prop({ required: true, type: String })
humanSlave!: string;
}
const strayCat: StrayCat = { name: 'Billy', kind: 'StrayCat', territorySize: 45 }
const homeCat: HomeCat = { name: 'Pogo', kind: 'HomeCat', humanSlave: 'Pascal' }
await catService.insertMany([strayCat, homeCat])
const cat = await catService.find({});
for (const cat of cats) {
cat // <= Type is Cat
cat instanceof Cat; // true
cat.name // <= Available for all cats.
cat.kind // <= Available for all cats.
if (cat instanceof StrayCat) {
cat // <= Type is StrayCat
cat.territorySize // <= Now available !
}
if (cat instanceof HomeCat) {
cat // <= Type is HomeCat
cat.humanSlave // <= Now available !
}
}
Polymorphism, projection, and typing
While TS will not let you use non-projected fields, under the hood the docs are still instances of their leaf class, so you can still cast them with isEntityInstanceOf(...)
.
const strayCat: StrayCat = { name: 'Billy', kind: 'StrayCat', territorySize: 45 }
const homeCat: HomeCat = { name: 'Pogo', kind: 'HomeCat', humanSlave: 'Pascal' }
await catService.insertMany([strayCat, HomeCat])
const cats = await catService.find(
{},
{
projection: { name: 1, territorySize: 1 },
}
);
for (const cat of cats) {
cat // <= Type is { name: string, territorySize: unknown }
if (isEntityInstanceOf(cat, StrayCat)) {
cat // <= Type is { name: 1, territorySize: number }
cat.territorySize // <= number
}
if (isEntityInstanceOf(cat, HomeCat)) {
cat // <= Type is { name: 1 }
cat.humanSlave // <= Error (not projected)
}
}
Note that it would still work with the vanilla if (cat instanceof StrayCat)
, but you will lose the typing information about non-projected fields.
Limitations
- Because lean documents are used systematically, things like virtual fields or
populate()
are not directly possible. Of course if you really need one of these fancy mongoose features, you can always invoke the model's methods directly (e.g.service.model.findOne().populate('myRefField').exec()
).
FAQ
Projections
"My non-optional field is marked as | undefined
in the projected document"
This happens when it is impossible to guess whether the projection is an inclusion or an exclusion.
// BAD
// Type `true` is widened to `boolean`. Impossible to guess.
this.catService.find({}, {projection: { name: true } })
const projectName = true; // Widened to `boolean`. Impossible to guess.
this.catService.find({}, {projection: { name: projectName } })
// GOOD
// Type `true` is not widened to `boolean`. Projection can be guessed.
this.catService.find({}, {projection: { name: true } as const })
// BETTER
// Type `1` is not widened to `number`. Projection can be guessed.
// This works because projections accept `1 | 0` and not `number`.
this.catService.find({}, {projection: { name: 1 } })
More about Typescript's type-widening: https://www.typescriptlang.org/play#example/type-widening-and-narrowing