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

@perryts/odm

v0.1.1

Published

Value-oriented MongoDB ODM for TypeScript. Zod schemas, ref/populate, lifecycle hooks, declarative indexes (compound, unique, 2dsphere, TTL). Sits on @perryts/mongodb. Zero native deps. Compiles to a native binary via Perry (LLVM AOT).

Readme

@perryts/odm

Value-oriented MongoDB ODM for TypeScript. Sits on @perryts/mongodb. Zero native dependencies. Compiles to a native binary via Perry (LLVM AOT).

npm install @perryts/odm @perryts/mongodb zod
import { z } from 'zod';
import { MongoClient } from '@perryts/mongodb';
import { defineModel, ref, before, initModels, type Ref } from '@perryts/odm';
import bcrypt from 'bcrypt';

const UserSchema = z.object({
  email: z.string().toLowerCase().trim(),
  password: z.string().optional(),
  passwordHash: z.string().optional(),
});
const User = defineModel('users', UserSchema, {
  indexes: [{ keys: { email: 1 }, unique: true }],
});
before(User, 'insert', async (doc) => {
  if (doc.password) {
    doc.passwordHash = await bcrypt.hash(doc.password, 10);
    doc.password = undefined;
  }
});

const ItemSchema = z.object({
  title: z.string(),
  owner: ref('users'),
  location: z.object({
    type: z.literal('Point'),
    coordinates: z.tuple([z.number(), z.number()]),
  }),
  expireAt: z.date().optional(),
});
const Item = defineModel('items', ItemSchema, {
  indexes: [
    { keys: { location: '2dsphere' } },
    { keys: { expireAt: 1 }, expireAfterSeconds: 0 },
  ],
});

const client = await MongoClient.connect(process.env.MONGO_URI!);
await initModels(client.db('app'), [User, Item]);

const alice = await User.insert({ email: '[email protected]', password: 'secret' });
await Item.insert({
  title: 'Bike',
  owner: alice._id as Ref<'users'>,
  location: { type: 'Point', coordinates: [-122.42, 37.77] },
});

const items = await Item.find({}).populate('owner');
const near  = await Item.aggregate([
  { $geoNear: { near: { type: 'Point', coordinates: [-122.42, 37.77] },
                distanceField: 'dist', maxDistance: 5000, spherical: true } },
]).toArray();

Design

  • Schemas are runtime values. A defineModel call takes a Zod object schema, an optional list of indexes, and an optional timestamps flag. There is no Schema constructor.
  • Documents are plain objects. No class wrapping, no .save(), no Mongoose-style document mutation. Model.insert(obj) takes a Zod-input shape, validates it, writes it, and returns the stored doc with _id.
  • Refs are branded ObjectIds. ref('users') is a Zod schema whose inferred type is ObjectId & { __ref: 'users' }. The brand is what populate('field') uses to look up the target collection at query-build time — no string typos pointing nowhere.
  • populate is a query-builder concern. Model.find(filter).populate('owner') lowers to a $lookup-augmented aggregation pipeline. Multiple .populate() chains are stacked.
  • Indexes are declared, applied at startup. initModels(db, [...]) binds each model to a MongoDB collection and runs createIndexes for every declared index — including 2dsphere, compound, unique, and TTL (expireAfterSeconds).
  • Hooks are functions, not middleware. before(Model, 'insert' | 'update' | 'delete', fn) registers a hook. pre-save for password hashing maps to before(User, 'insert', ...).
  • Aggregate is a passthrough. Model.aggregate<T>(pipeline) returns the driver's AggregationCursor<T>. $geoNear, $lookup, $facet, anything Mongo supports.

Migrating from Mongoose

See MIGRATING_FROM_MONGOOSE.md — written as a rule book for an LLM agent doing a mechanical port.

What's not in v0.1

  • No first-class transactions API (use client.startSession() and pass { session } through Model.collection.*).
  • No first-class change streams (use Model.collection.watch(...)).
  • No plugin system. Compose with plain functions.
  • No custom timestamp field names (createdAt / updatedAt only).
  • populate(...).select(...) is not supported. Use aggregate with a manual $project stage if you need field selection on joins.
  • Discriminated unions are supported via Zod but defineModel itself takes a ZodObject. Validate the discriminator in a before hook if you need a single Model handle.

License

MIT