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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devindex/mongoose-kit

v1.0.0

Published

Mongoose model builder and helpers

Readme

@devindex/mongoose-kit

Mongoose model builder and helpers, parameterized by data — every project decision stays in the app. No connection, no global state, no HTTP.

Installation

npm install @devindex/mongoose-kit

Requires mongoose@>=8 as a peer dependency and Node >=22.

Usage

// Import from the package root
import { ModelBuilder, collectionName, serialize, helpers } from '@devindex/mongoose-kit';

// Or import directly from a module
import ModelBuilder from '@devindex/mongoose-kit/model';
import { buildToObjectOptions } from '@devindex/mongoose-kit/serialize';
import { isObjectId } from '@devindex/mongoose-kit/helpers';

ModelBuilder

const Document = ModelBuilder.createModel({
  name: 'ApiDocuments',            // → collection "apiDocuments"
  schema: {
    name: { type: String, required: true },
    active: { type: Boolean, private: true },
    items: { type: [ModelBuilder.subSchema({ name: { type: String } })] },
  },
});

const doc = new Document({ name: 'Sergio', active: true });
JSON.stringify(doc); // '{"name":"Sergio","createdAt":"...","updatedAt":"...","id":"..."}'

Defaults applied to every schema:

  • collection: collectionName(name)ApiDocumentsapiDocuments, set explicitly instead of through the global mongoose.pluralize(). Sub-schemas never get one. Pass schemaOptions.collection to override.
  • timestamps: true
  • autoIndex: false, autoCreate: false — index and collection creation belong to whoever owns the connection, not to the model.
  • toObject with versionKey: false
  • toJSON from buildToObjectOptionsversionKey: false, getters: true, virtuals: false, _id exposed as the string id, private fields dropped
  • _id: false implies id: false
  • the capturePreSaveState plugin — snapshots wasNew, modifiedPaths and directModifiedPaths for post-save hooks — unless the capturePreSaveState argument is false. Sub-schemas never get it: mongoose runs sub-document pre-save hooks on the parent's save, and the parent's own snapshot already reports what changed

Fields declared private: true are stripped from toJSON, along with _id (exposed as the string id). Passing schemaOptions.toJSON keeps those options as they are and skips the transform — unless a jsonHandler comes with them, which restores the transform and discards schemaOptions.toJSON.

API

  • new ModelBuilder({ name?, schema, schemaOptions?, jsonHandler?, includesBase?, capturePreSaveState? })schemaOptions is merged over the defaults and handed to mongoose.Schema; the other arguments are the builder's own. includesBase: false skips baseSchemaDefinition()
  • ModelBuilder.createModel(args) — build and compile in one step
  • ModelBuilder.subSchema(schema, schemaOptions?, jsonHandler?) — sub-document schema: _id, no timestamps, no base fields, no capturePreSaveState. Pass _id: false in schemaOptions to drop the identifier
  • ModelBuilder.ObjectIdmongoose.Schema.Types.ObjectId
  • .setToJSON(options) / .setToObject(options) / .setJSONHandler(handler)
  • .toModel() — compile the model, requires name
  • .getPrivateFields() — paths declared private: true
  • collectionName(name) — the naming convention, a named export of the package root and /model rather than a ModelBuilder member, exported for reuse

Extension points

Override baseSchemaDefinition() to inject fields into every model, or prepareSchemaDefinition(schema, includesBase) when position matters:

class TenantModelBuilder extends ModelBuilder {
  prepareSchemaDefinition(schemaDefinition, includesBase) {
    if (!includesBase) return schemaDefinition;
    return {
      customerId: { type: String, private: true },
      ...schemaDefinition,
      meta: { type: Map, of: {} },
    };
  }
}

serialize

  • buildToObjectOptions(options?, params?)toObject/toJSON options that expose _id as a string id, drop params.hide fields — overridable per call through opts.hide — and run params.handler(doc, result, opts). Defaults to getters: true, virtuals: false and versionKey: false; options is merged last and wins
  • capturePreSaveState(schema) — plugin that snapshots isNew, modifiedPaths() and directModifiedPaths() into doc.$locals before saving, so post-save hooks can still read them
  • isModified(doc, path | paths) — whether any of the paths changed in that save
  • isDirectModified(doc, path | paths) — same, restricted to paths set directly: writing contact.email marks contact as modified, but only contact.email as directly modified
  • wasNew(doc) — whether that save was an insert; doc.isNew is already false by the time post-save hooks run

All three readers require the plugin, which ModelBuilder applies by default — pass capturePreSaveState: false to the constructor to skip it. It writes $locals.wasNew, $locals.modifiedPaths and $locals.directModifiedPathswasNew being the key mongoose itself documents for this idiom.

schema.post('save', (doc) => {
  if (wasNew(doc)) return sendWelcome(doc);
  if (isModified(doc, ['email', 'phone'])) notifyContactChange(doc);
  if (isDirectModified(doc, 'address.zip')) revalidateShipping(doc);
});

helpers

  • isObjectId(value) — strict check, round-trips the value instead of trusting ObjectId.isValid
  • toObjectId(value) — build an ObjectId
  • isUUID(value) — canonical UUID string, any version 1–8
  • isDuplicateKeyError(error) — MongoDB error 11000, wrapped in cause or not
  • duplicateKeyFields(error) — the colliding field names, so a duplicate can become a precise domain error

License

MIT