mongest-nestjs-service
TypeScript icon, indicating that this package has built-in type declarations

0.3.1 • Public • Published

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

Readme

Keywords

none

Package Sidebar

Install

npm i mongest-nestjs-service

Weekly Downloads

2

Version

0.3.1

License

MIT

Unpacked Size

100 kB

Total Files

13

Last publish

Collaborators

  • oodelally