npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mongest

v0.1.2

Published

Delightfully-typed Mongoose-wrapper

Downloads

56

Readme

Mongest NestJS Service (BETA)

Delightfully-typed Mongoose-wrapper service

This is a BETA, and therefore you may encounter bugs. Please post an issue if needed.

Note: If you use NestJS, use

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

  • 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.

Setup

Install (if not already there) the peer dependencies:

npm install --save mongodb mongoose

Then install the mongest lib:

npm install --save mongest

Now you can create your entity and your service:


export class Cat {
  _id!: ObjectId;
  kind!: CatKind;
  name!: string;
}

export const CatSchema = new Schema(
  {
    kind: { type: String, enum: CatKind },
    name: String,
  },
  { discriminatorKey: 'kind' },
);

export const CatModel = mongoose.model(Cat.name, CatSchema);

export class CatsService extends BuildMongestService(Cat) {
  async myCustomMethod(): Promise<Cat[]> {
    return await this.find({ name: 'pogo' });
  }
}

const catService = new CatsService(CatModel);

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',
}

// Base Cat
export class Cat {
  _id!: ObjectId;
  kind!: CatKind;
  name!: string;
}
export const CatSchema = new Schema(
  {
    kind: { type: String, enum: CatKind },
    name: String,
  },
  { discriminatorKey: 'kind' },
);
export const CatModel = mongoose.model(Cat.name, CatSchema);
export class CatsService extends BuildMongestService(Cat) {}

// Home Cat
export class HomeCat extends Cat {
  humanSlave!: string;
}
export const HomeCatSchema = new Schema(
  {
    humanSlave: String,
  },
  { discriminatorKey: 'kind' },
);
registerEntityClassForSchema(HomeCat, HomeCatSchema);
export const HomeCatModel = CatModel.discriminator(HomeCat.name, HomeCatSchema);
export class HomeCatsService extends BuildMongestService(HomeCat) {}

// Stray Cat
export class StrayCat extends Cat {
  territorySize!: number;
  enemyCount?: number;
}
export const StrayCatSchema = new Schema(
  {
    territorySize: Number,
    enemyCount: { type: Number, required: false },
  },
  { discriminatorKey: 'kind' },
);
registerEntityClassForSchema(StrayCat, StrayCatSchema);
export const StrayCatModel = CatModel.discriminator(StrayCat.name, StrayCatSchema);
export class StrayCatsService extends BuildMongestService(StrayCat) {}

// Usage
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