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

@m1abdullah/dorm

v0.6.0

Published

Django-style ORM for MongoDB: field__lookup filters compiled to aggregation pipelines, with schema-driven models and validation.

Readme

DORM

npm license CI docs

Django-style ORM for MongoDB.

📖 Documentation — guides + API reference.

DORM brings the ergonomics of the Django ORM to MongoDB in TypeScript: define a model once, then query and write through it — including Django's field__lookup filter syntax, which compiles to MongoDB aggregation pipelines (relational __ traversals become $lookup joins).

The why

I've played with Django and the DRF, and one of the things that really amazed me was the Django ORM — powerful and genuinely fun to use. It felt like every major database deserved an ORM like that. That's why this project was born.

Install

npm install @m1abdullah/dorm mongodb

mongodb is a peer dependency (DORM has no other runtime dependencies), so you always control the single copy of the driver.

Requirements. Node.js 20+ and mongodb 6+. DORM is an ESM-only package — there is no CommonJS build. Import it with ESM import syntax from an ES module (or via a dynamic import() from CommonJS); a top-level require("@m1abdullah/dorm") is not supported.

Quickstart

import { DORMClient, f } from "@m1abdullah/dorm";

const dorm = new DORMClient(process.env.MONGODB_URL!); // e.g. mongodb://localhost:27017/app

// Declare models synchronously — safe before connecting.
const User = dorm.model(
  "users",
  {
    username: f.string({ required: true }),
    email: f.string({
      required: true,
      validate: (v) => {
        if (!v.includes("@")) throw new Error("Enter a valid email.");
      },
    }),
    salary: f.number({ default: 0 }),
    active: f.boolean({ default: true }),
    supervisor: f.ref("users"), // reference to another users document
  },
  { timestamps: true },
);

const Asset = dorm.model(
  "assets",
  {
    name: f.string({ required: true }),
    price: f.number({ default: 0 }),
    assigned_to: f.ref("users"),
  },
  { timestamps: true },
);

await dorm.connect(); // open the connection once, at startup

// Create — validates, applies defaults + timestamps, returns the stored doc.
const boss = await User.objects.create({ username: "boss", email: "[email protected]", salary: 90000 });
const abed = await User.objects.create({
  username: "abed",
  email: "[email protected]",
  salary: 40000,
  supervisor: boss._id,
});
await Asset.objects.create({ name: "Macbook", price: 2000, assigned_to: abed._id });

// Query — Django-style lookups.
await User.objects.filter({ salary__gte: 50000 });        // simple lookup
await User.objects.get({ username: "abed" });             // one or throws
await User.objects.count({ active: true });

// Relational traversal — assets whose assignee's supervisor earns >= 50k.
await Asset.objects.filter({ assigned_to__supervisor__salary__gte: 50000 });

// Write.
await User.objects.update({ username: "abed" }, { salary: 60000 }); // bumps updatedAt
await User.objects.delete({ active: false });

await dorm.close();

Models & fields

dorm.model(name, schema, options?) returns a Model whose .objects manager is your query/write surface. The schema is the single source of truth: it drives type inference and runtime validation.

Field builders (f.*) all accept { required?, default?, validate?, unique?, index? }:

| Builder | Stored as | Notes | | --- | --- | --- | | f.string(opts?) | string | | | f.number(opts?) | number | rejects NaN | | f.boolean(opts?) | boolean | | | f.date(opts?) | Date | ISO strings/timestamps are coerced | | f.objectId(opts?) | ObjectId | hex strings are coerced | | f.ref(collection, opts?) | ObjectId | supplies $lookup.from for __ traversal | | f.array(element, opts?) | T[] | each element validated against element | | f.embedded(schema, opts?) | object | validated against the sub-schema (typed) | | f.enum(values, opts?) | union | value must be one of values |

const User = dorm.model("users", {
  email: f.string({ required: true, unique: true }),
  role:  f.enum(["admin", "member"]),          // typed as "admin" | "member"
  tags:  f.array(f.string()),                  // string[]
  address: f.embedded({                        // typed nested object
    city: f.string({ required: true }),
    zip:  f.string({ default: "00000" }),
  }),
});
  • default may be a value or a factory (() => new Date()) evaluated per create.
  • validate throws an Error to reject; the message surfaces on the validation error.
  • options.timestamps: true manages createdAt/updatedAt automatically.

Indexes

Fields marked unique: true or index: true become indexes when you call ensureIndexes() (do this once after connect()):

await dorm.connect();
await dorm.ensureIndexes();        // all models, or User.ensureIndexes() for one

Manager API (.objects)

| Method | Returns | | --- | --- | | all() | chainable QuerySet of every document (see Querying) | | filter(where) · exclude(where) | chainable QuerySet | | order_by(...) · limit(n) · offset(n) · select(...) | chainable QuerySet | | get(where?) | exactly one document (throws DoesNotExist / MultipleObjectsReturned) | | first() · exists() | first document | null, and a boolean | | create(data) | the stored document (validated, with _id + timestamps) | | update(where, patch) | { matched, modified } (partial-validates the patch) | | delete(where) | { deleted } | | count(where?) | number of matching documents |

Lookups

Append __lookup to a field name. Supported operators:

startswith, contains, icontains, gt, gte, lt, lte, in, nin, exists, isnull, range (inclusive [min, max]).

User.objects.filter({ username__startswith: "ab" });
User.objects.filter({ salary__range: [40000, 80000] });
User.objects.filter({ supervisor__isnull: true });

Filter keys are fully typed: your editor autocompletes valid field__lookup keys for a model, checks each value against the field's type, and flags typos — salary__gte expects a number, username__startswith a string, and so on. _id is always available, and createdAt/updatedAt keys appear on models with timestamps: true. Regex-based lookups escape their input, and only the operators above are recognized — arbitrary MongoDB operators can't be injected through filter keys.

Typing note (deep relational keys): the value on a deep relational key like assignee__email__icontains is accepted loosely (typed as unknown) in the filter — ref__… keys go through a relational passthrough, so you won't get autocomplete/value-checking on the nested part the way you do for a top-level field like email__icontains. It compiles and runs correctly; it's just not strictly typed yet (that's a roadmap item).

Relational traversal: chain __ through f.ref fields to join across collections. Register both models on the same client so DORM can resolve the join targets. Note that traversal uses an inner join ($unwind), so documents whose reference is null/unmatched are excluded.

Filter values for _id and f.ref/f.objectId fields are auto-coerced from hex strings to ObjectId, so filter({ _id: req.params.id }) just works.

Querying (chaining)

all(), filter(), and exclude() return a lazy, chainable QuerySet that executes only on a terminal operation. Because a QuerySet is awaitable, await Model.objects.filter({...}) still resolves to the array — chaining is purely additive.

// build up a query; nothing runs until it's awaited
const page = await Task.objects
  .filter({ done: false })
  .exclude({ priority__gte: 4 })
  .order_by("-priority", "title") // `-` = descending
  .offset(20)
  .limit(10);

await User.objects.filter({ active: true }).first();   // one doc or null
await Task.objects.filter({ done: false }).exists();   // boolean
await Task.objects.filter({ done: false }).count();    // number
await Task.objects.filter({ done: false }).select("title", "priority"); // projection

// queryset-level writes use the chained filter:
await Task.objects.filter({ done: true }).update({ archived: true });
await Task.objects.exclude({ starred: true }).delete();

| Chaining | Terminal | | --- | --- | | filter(where) · exclude(where) | await → documents | | order_by(...fields) | first() → doc | null | | limit(n) · offset(n) | exists() → boolean | | select(...fields) | count() · get(where?) | | populate(...refs) | update(patch) · delete() |

populate() hydrates ref fields — it replaces each id with the referenced document (outer join, so documents with a missing ref are kept):

const [post] = await Post.objects.filter({ id }).populate("author");
post.author; // the full user document, not just an ObjectId

exclude() supports a single relational (ref) hop — e.g. exclude({ author__name: "spam" }) removes posts whose author matches, while keeping posts with no author (Django semantics). Multi-hop relational excludes aren't supported yet.

Lifecycle hooks & transactions

Register pre/post hooks for create | update | delete. A pre hook can mutate the payload or throw to abort:

User.pre("create", (doc) => { doc.email = doc.email.toLowerCase(); });
User.post("create", (user) => audit("user.created", user._id));
User.pre("delete", (ids) => audit("user.deleting", ids));

Run several writes atomically with dorm.transaction (requires a replica set). Pass the session to each write:

await dorm.transaction(async (session) => {
  const user = await User.objects.create({ email }, { session });
  await Account.objects.filter({ owner: user._id }).session(session).update({ active: true });
});

Errors

  • DormValidationError — thrown by create/update on invalid input. It collects all failing fields in .errors ({ field, code, message, … }[]); .toJsonResponse() returns { message, errors }. The top-level .field/.code mirror the first error. Codes: DORM_ERROR_1 (required), DORM_ERROR_2 (type), DORM_ERROR_3 (custom validate hook), DORM_ERROR_4 (unique — an app-level check on unique fields that complements the DB index).
  • DoesNotExist / MultipleObjectsReturned — thrown by get.

Scripts

npm run build          # emit dist/ (JS + .d.ts)
npm run typecheck      # tsc --noEmit
npm test               # vitest (unit + in-memory-Mongo integration)
npm run test:unit      # pure unit tests only (no MongoDB needed)
npm run test:coverage  # full suite with a coverage report (text + HTML + lcov)
npm run lint           # eslint

Releasing

Publishing is automated. On a push to main, the Publish workflow publishes to npm only if package.json's version isn't already on the registry. To cut a release:

  1. Bump the version (npm version patch|minor|major).
  2. Merge to main.

The workflow runs prepublishOnly (typecheck → unit tests → build) before publishing, and authenticates to npm via a trusted publisher (GitHub OIDC) — no NPM_TOKEN secret required. Because the repo is public, npm generates provenance attestations automatically on each publish.

Roadmap

Two parts of the type surface are intentionally loose today and are explicitly outside the 1.0 type-stability guarantee — they may be tightened in a minor release (see the versioning policy):

  • Deep relational filter keysref__… traversal values are accepted as unknown. Runtime behavior is fully supported; only the static types are loose.
  • populate() result typing — the hydrated field is a generic object.

A fully-typed, opt-in schema-registry API (letting ref fields carry their target type, so filter keys and populate() infer automatically) is planned as an additive, post-1.0 feature.

See examples/ for a runnable Express app.

Contributing

Contributions are welcome — see CONTRIBUTING.md for setup, conventions, and the versioning policy. Notable changes are tracked in CHANGELOG.md.