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

@wenu/mongo

v0.4.3

Published

Declarative, immutable, type-safe MongoDB repository layer with Zod validation. Zero throws, dual ESM/CJS, pluggable ID strategies.

Readme

@wenu/mongo

npm node

Declarative, immutable, type-safe MongoDB repository layer with Zod validation. Zero throws. Dual ESM/CJS. MongoDB 5/6/7 compatible. Zod 3 and 4 compatible.

Table of Contents


Features

  • Zero throws — every method returns Result<T, DbError>, never throws
  • Full type inference — document shape, _id type, and filter types flow from a single defineCollection() call
  • Pluggable ID strategiesobjectid (default), uuid, string, or any Zod schema for custom types
  • Zod validation on write — inserts and updates are validated before touching the database
  • Upsert with id strategyupsertById and upsertOne generate _id per strategy on insert-path; preserve existing _id on replace-path
  • Domain errorsNotFoundError, duplicate-key, validation — no raw driver errors leaked
  • Immutable collection definitionsdefineCollection() returns a frozen, reusable descriptor
  • Index management — declare indexes alongside the schema, sync or generate migrate-mongo scripts
  • Typed aggregationaggregate() accepts an output schema and returns Result<Infer<Out>[]>
  • Driver options passthroughfind, findOne, findById accept FindOptions; updateById, updateOne accept FindOneAndUpdateOptions; updateMany accepts UpdateOptions
  • Atomic operations escape hatchupdateRaw passes any UpdateFilter directly to the driver for $inc, $push, $pull, $unset, and other operators not expressible as a validated patch
  • Transaction supportrepo.session(clientSession) returns an immutable repository view that threads the ClientSession into every driver call, enabling multi-document transactions
  • Dual ESM/CJS — works in Node ESM projects and CommonJS consumers alike

Installation

# npm
npm install @wenu/mongo

# pnpm
pnpm add @wenu/mongo

# yarn
yarn add @wenu/mongo

Peer dependencies

# MongoDB driver (choose one range)
npm install mongodb@^5   # or ^6 or ^7

# Zod (v3 or v4)
npm install zod@^3

Requirements: Node >=22.0.0


Quick Start

import * as z from 'zod';
import { MongoClient } from 'mongodb';
import { defineCollection, createRepository } from '@wenu/mongo';

// 1. Define the schema and collection
const UserCollection = defineCollection({
  name: 'users',
  schema: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    createdAt: z.date(),
  }),
});

// 2. Connect and create the repository
const client = await MongoClient.connect('mongodb://localhost:27017');
const db = client.db('myapp');

const users = createRepository(UserCollection, db);

// 3. Use it — no throws, ever
const result = await users.insert({
  name: 'Alice',
  email: '[email protected]',
  createdAt: new Date(),
});

if (result.ok) {
  console.log(result.value._id); // ObjectId (inferred from idStrategy: 'objectid')
} else {
  console.error(result.error.kind, result.error.message);
}

Core Concepts

Defining a Collection

defineCollection() is the single source of truth for a collection's shape, ID strategy, and indexes. It returns a frozen CollectionDef object you pass to createRepository() and index utilities.

import * as z from 'zod';
import { defineCollection, index } from '@wenu/mongo';

const ProductCollection = defineCollection({
  name: 'products',
  schema: z.object({
    sku: z.string(),
    name: z.string(),
    price: z.number().positive(),
    tags: z.array(z.string()).default([]),
  }),
  idStrategy: 'uuid', // optional — defaults to 'objectid'
  indexes: [index({ sku: 1 }, { unique: true }), index({ tags: 1 })],
});

The Doc<Schema, Id> type resolves to the schema output merged with the inferred _id type:

import type { Doc } from '@wenu/mongo';

type Product = Doc<(typeof ProductCollection)['schema'], (typeof ProductCollection)['id']>;
// { _id: string; sku: string; name: string; price: number; tags: string[] }

Creating a Repository

Pass the collection definition and a Db instance. The repository is a plain object — no classes, no state beyond the driver handle.

import { createRepository } from '@wenu/mongo';

const products = createRepository(ProductCollection, db);

All 12 methods return Promise<Result<T, DbError>>.

Result Type

The Result type is a discriminated union — it never throws and forces you to handle both paths:

type Ok<T> = { readonly ok: true; readonly value: T };
type Err<E> = { readonly ok: false; readonly error: E };
type Result<T, E = DbError> = Ok<T> | Err<E>;

Use the ok discriminant or the isOk / isErr type guards:

import { isOk, isErr } from '@wenu/mongo';

const result = await users.findById(id);

// Discriminant
if (result.ok) {
  // result.value is Doc<...> | null
}

// Type guards
if (isOk(result)) {
  console.log(result.value);
}
if (isErr(result)) {
  console.error(result.error);
}

CRUD Operations

All examples use the users repository from the Quick Start.

Find

// By ID
const byId = await users.findById(someObjectId);

// By filter — returns first match or null
const byEmail = await users.findOne({ email: '[email protected]' });

// All matching documents
const all = await users.find({ name: /alice/i });

// All documents (no filter)
const everyone = await users.find();

query()

query() returns a chainable, immutable QueryBuilder that accumulates filter and options before executing a single find call. It is the ergonomic alternative to find(filter, options) for complex read scenarios.

// Simple filter
const active = await users.query().filter({ status: 'active' }).exec();

// Sort descending
const recent = await users.query().sort({ createdAt: -1 }).exec();

// Limit and skip (pagination)
const page2 = await users.query().sort({ createdAt: -1 }).limit(10).skip(10).exec();

// Full chain — filter + sort + pagination
const result = await users
  .query()
  .filter({ status: 'active' })
  .sort({ createdAt: -1 })
  .limit(10)
  .skip(20)
  .exec();

if (result.ok) {
  for (const user of result.value) {
    console.log(user.name);
  }
}

Immutability — each chainable call returns a new builder; the original is never mutated:

const base = users.query().filter({ status: 'active' });
const page1 = base.limit(10).skip(0);
const page2 = base.limit(10).skip(10); // independent from page1

Parity with find(filter, options)repo.query().exec() is equivalent to repo.find({}). filter sets the filter, sort/limit/skip set the corresponding FindOptions fields.

select() (projection) is not available in v1. Use find(filter, { projection: ... }) for projection queries.

Count and Exists

// Count all documents
const total = await users.count();
if (total.ok) console.log(total.value); // number

// Count matching a filter
const admins = await users.count({ role: 'admin' });

// Check existence — cheaper than count (uses limit: 1 internally)
const hasAdmin = await users.exists({ role: 'admin' });
if (hasAdmin.ok) console.log(hasAdmin.value); // boolean

Insert

// Single document — validated before insert
const inserted = await users.insert({
  name: 'Bob',
  email: '[email protected]',
  createdAt: new Date(),
});

// Multiple documents — validates all first; any failure returns an error before any DB write
const many = await users.insertMany([
  { name: 'Carol', email: '[email protected]', createdAt: new Date() },
  { name: 'Dave', email: '[email protected]', createdAt: new Date() },
]);

Upsert

upsertById always uses the caller-supplied id — on insert-path it becomes the document's _id. upsertOne matches by filter — on insert-path it generates a new _id per the collection's id strategy; on replace-path it preserves the existing _id.

// Insert or replace by explicit ID (uuid strategy)
const result = await products.upsertById('sku-123', {
  sku: 'sku-123',
  name: 'Widget',
  price: 9.99,
  tags: [],
});
if (result.ok) {
  console.log(result.value._id); // 'sku-123'
}

// Insert or replace by filter — _id auto-generated on insert, preserved on replace
const bySlug = await products.upsertOne(
  { sku: 'sku-456' },
  { sku: 'sku-456', name: 'Gadget', price: 19.99, tags: ['new'] },
);
if (!bySlug.ok && bySlug.error.kind === 'not-found') {
  // driver returned null after write — extremely rare
}

Update

// By ID — returns updated document or null if not found
const updated = await users.updateById(someObjectId, { name: 'Alice Smith' });

// By filter — returns updated document or null
const updatedOne = await users.updateOne({ email: '[email protected]' }, { name: 'Alice Smith' });

// Bulk update — returns { modifiedCount }
const bulk = await users.updateMany(
  { createdAt: { $lt: new Date('2024-01-01') } },
  { name: 'archived' },
);
if (bulk.ok) {
  console.log(`Updated ${bulk.value.modifiedCount} documents`);
}

All update methods accept an optional options parameter forwarded directly to the MongoDB driver:

// FindOneAndUpdateOptions on updateById / updateOne
await users.updateById(id, { name: 'Alice' }, { comment: 'profile-update' });

// UpdateOptions on updateMany
await users.updateMany({ role: 'guest' }, { role: 'user' }, { writeConcern: { w: 'majority' } });

Atomic operations — updateRaw

When you need MongoDB atomic operators that cannot be expressed as a validated Partial<Schema>, use updateRaw. It passes an UpdateFilter directly to the driver without $set wrapping or schema validation on the update itself:

// Increment a counter
await users.updateRaw({ tag: 'active' }, { $inc: { loginCount: 1 } });

// Push to an array
await users.updateRaw({ _id: id }, { $push: { tags: 'premium' } });

// Pull from an array
await users.updateRaw({ _id: id }, { $pull: { tags: 'trial' } });

// Unset a field
await users.updateRaw({ _id: id }, { $unset: { legacyField: '' } });

// With options
const result = await users.updateRaw(
  { status: 'inactive' },
  { $set: { archivedAt: new Date() } },
  { writeConcern: { w: 'majority' } },
);
if (result.ok) {
  console.log(`Archived ${result.value.modifiedCount} users`);
}

updateRaw matches all documents — equivalent to updateMany in scope. Use it when atomic operators are required. For validated partial updates, prefer updateById, updateOne, or updateMany.

updateRaw does not validate the update argument — see Security before passing it anything derived from user input.

Delete

// By ID — returns deleted document or null
const deleted = await users.deleteById(someObjectId);

// By filter — returns deleted document or null
const deletedOne = await users.deleteOne({ email: '[email protected]' });

// Bulk delete — returns { deletedCount }
const bulk = await users.deleteMany({ createdAt: { $lt: cutoff } });
if (bulk.ok) {
  console.log(`Deleted ${bulk.value.deletedCount} documents`);
}

Transactions — session()

repo.session(clientSession) returns a new repository view that threads the given ClientSession into every MongoDB driver call. The base repository is unaffected.

import { MongoClient } from 'mongodb';

const client = new MongoClient(uri);
const db = client.db('mydb');

const users = createRepository(UserCollection, db);
const orders = createRepository(OrderCollection, db);

const session = client.startSession();
try {
  await session.withTransaction(async () => {
    const user = await users.session(session).findById(userId);
    if (!user.ok || !user.value) throw new Error('user not found');

    await orders.session(session).insert({ userId, total: 99 });
    await users.session(session).updateById(userId, { lastOrderAt: new Date() });
  });
} finally {
  await session.endSession();
}

session() is immutable — it always returns a new repository view, never mutates the original. Calling repo.session(s1).session(s2) binds s2, discarding s1.

Note: multi-document transactions require a MongoDB replica set (or Atlas). A standalone instance will reject commitTransaction.


Error Handling

DbError carries a discriminated kind field so you can branch on error type without inspecting message strings:

import type { DbError, DbErrorKind } from '@wenu/mongo';

type DbErrorKind =
  | 'validation' // Zod parse failed (insert/update input) — no DB call was made
  | 'duplicate-key' // MongoDB error code 11000 (unique index violation)
  | 'not-found' // upsert returned null after write (NotFoundError)
  | 'connection' // reserved for future use
  | 'unknown'; // any other driver error
const result = await products.insert({ sku: 'existing-sku', name: 'X', price: 10, tags: [] });

if (!result.ok) {
  switch (result.error.kind) {
    case 'duplicate-key':
      throw new ConflictError('SKU already exists');
    case 'validation':
      throw new BadRequestError(result.error.message);
    default:
      throw new InternalError(result.error.message);
  }
}

You can also convert any caught value into a DbError using toDbError, or catch a specific domain error class directly:

import { toDbError, NotFoundError } from '@wenu/mongo';

const dbError = toDbError(caughtError);

// Catch domain errors directly
try {
  // ...
} catch (error) {
  if (error instanceof NotFoundError) {
    // kind: 'not-found'
  }
}

ID Strategies

Set idStrategy in defineCollection(). The _id type on every document is inferred automatically. id is still accepted as a deprecated alias for idStrategy — do not pass both in the same call.

objectid (default)

Generated by the library using new ObjectId(). No _id in input data.

import * as z from 'zod';
import { defineCollection, createRepository } from '@wenu/mongo';

const PostCollection = defineCollection({
  name: 'posts',
  schema: z.object({ title: z.string(), body: z.string() }),
  // idStrategy: 'objectid' is the default — can be omitted
});

const posts = createRepository(PostCollection, db);

const result = await posts.insert({ title: 'Hello', body: 'World' });
if (result.ok) {
  result.value._id; // ObjectId — auto-generated, never in input
}

uuid

Generated using crypto.randomUUID(). No _id in input data. _id is always a UUID v4 string.

const SessionCollection = defineCollection({
  name: 'sessions',
  schema: z.object({ userId: z.string(), expiresAt: z.date() }),
  idStrategy: 'uuid',
});

const sessions = createRepository(SessionCollection, db);

const result = await sessions.insert({ userId: 'u_123', expiresAt: new Date() });
if (result.ok) {
  result.value._id; // string — '3b4d5e6f-...' (UUID v4)
}

// upsertById uses the caller-supplied UUID as _id on insert-path
const upserted = await sessions.upsertById('my-session-id', {
  userId: 'u_123',
  expiresAt: new Date(),
});
if (upserted.ok) {
  upserted.value._id; // 'my-session-id'
}

string

Caller-supplied string _id. Include _id in both the schema and the input data — the library does not generate one.

idStrategy: 'string' means "declare _id directly in your schema" — it does NOT mean "declare a field named id". A schema may also have an unrelated id domain field; the library never reads or infers anything from it.

const CountryCollection = defineCollection({
  name: 'countries',
  schema: z.object({ _id: z.string(), name: z.string(), population: z.number() }),
  idStrategy: 'string',
});

const countries = createRepository(CountryCollection, db);

// _id must be in the data — no auto-generation
const result = await countries.insert({ _id: 'AR', name: 'Argentina', population: 46_000_000 });
if (result.ok) {
  result.value._id; // 'AR'
}

// upsertOne with string strategy — _id comes from data, not generated
await countries.upsertOne({ _id: 'AR' }, { _id: 'AR', name: 'Argentina', population: 46_500_000 });

Custom Zod schema — branded type

Pass any Zod schema as the idStrategy value. The _id type is inferred from the schema output. Include _id in the data — the library validates it through the schema.

import * as z from 'zod';

const OrderId = z.string().brand<'OrderId'>();

const OrderCollection = defineCollection({
  name: 'orders',
  schema: z.object({ _id: OrderId, total: z.number(), status: z.string() }),
  idStrategy: OrderId,
});

const orders = createRepository(OrderCollection, db);

const id = 'order_123' as z.infer<typeof OrderId>;
const result = await orders.insert({ _id: id, total: 99.9, status: 'pending' });
if (result.ok) {
  result.value._id; // string & Brand<'OrderId'>
}

Custom Zod schema — composite (multi-field) _id

Use a Zod object schema as idStrategy to get a composite _id. MongoDB treats the whole object as the document identifier — uniqueness is enforced across the combination of all fields.

import * as z from 'zod';

const TenantSlugId = z.object({ tenantId: z.string(), slug: z.string() });

const ArticleCollection = defineCollection({
  name: 'articles',
  schema: z.object({ _id: TenantSlugId, title: z.string() }),
  idStrategy: TenantSlugId,
});

const articles = createRepository(ArticleCollection, db);

// _id is validated and typed as { tenantId: string; slug: string }
const result = await articles.insert({
  _id: { tenantId: 'acme', slug: 'hello-world' },
  title: 'Hello World',
});
if (result.ok) {
  result.value._id; // { tenantId: string; slug: string }
}

// findById and upsertOne require the full _id object — dot notation ('_id.tenantId') does not match
const found = await articles.findById({ tenantId: 'acme', slug: 'hello-world' });

// annotate with z.infer so TypeScript resolves the filter type correctly
const compositeId: z.infer<typeof TenantSlugId> = { tenantId: 'acme', slug: 'hello-world' };
await articles.upsertOne({ _id: compositeId }, { _id: compositeId, title: 'Updated' });

// Invalid _id shape → kind: 'validation', no DB call made
const bad = await articles.insert({ _id: { tenantId: 'acme', slug: 123 as never }, title: 'Bad' });
if (!bad.ok) {
  bad.error.kind; // 'validation'
}

Index Management

Declare indexes inside defineCollection() using the index() helper, then either sync them at startup or generate a migrate-mongo migration file.

Declaring indexes

import { defineCollection, index } from '@wenu/mongo';

const ArticleCollection = defineCollection({
  name: 'articles',
  schema: z.object({
    slug: z.string(),
    authorId: z.string(),
    publishedAt: z.date().optional(),
    tags: z.array(z.string()).default([]),
  }),
  indexes: [
    index({ slug: 1 }, { unique: true }),
    index({ authorId: 1, publishedAt: -1 }),
    index({ tags: 1 }),
  ],
});

Syncing at startup

syncIndexes() calls createIndexes() on the underlying collection and returns Result<void>. Safe to call on every startup — MongoDB skips indexes that already exist.

import { syncIndexes } from '@wenu/mongo';

const result = await syncIndexes(ArticleCollection, db);
if (!result.ok) {
  console.error('Index sync failed:', result.error.message);
}

Generating a migrate-mongo script

import { generateIndexMigration } from '@wenu/mongo';
import fs from 'node:fs';

const migration = generateIndexMigration(ArticleCollection);
fs.writeFileSync('migrations/20240101-articles-indexes.js', migration);

The generated file exports up(db) / down(db) compatible with migrate-mongo.


Aggregation

Pass the pipeline and an output schema. Results are parsed with the provided schema and returned as Result<Infer<Out>[]>.

import * as z from 'zod';

const SummarySchema = z.object({
  authorId: z.string(),
  articleCount: z.number(),
  latestPublishedAt: z.date().nullable(),
});

const result = await articles.aggregate(
  [
    { $match: { publishedAt: { $exists: true } } },
    {
      $group: {
        _id: '$authorId',
        articleCount: { $sum: 1 },
        latestPublishedAt: { $max: '$publishedAt' },
      },
    },
    { $project: { authorId: '$_id', articleCount: 1, latestPublishedAt: 1, _id: 0 } },
  ],
  SummarySchema,
);

if (result.ok) {
  for (const summary of result.value) {
    console.log(summary.authorId, summary.articleCount);
  }
}

Security

@wenu/mongo validates documents with Zod on every insert and update. It does not validate the shape of filters or updateRaw operators — Filter<Doc> and UpdateFilter<Doc> arguments are forwarded to the MongoDB driver as-is. Building a filter or update from untrusted input is a classic NoSQL operator-injection vector: an attacker-controlled body can inject operators like $ne, $gt, or $where to bypass the query you intended.

Never spread request bodies or query params into a filter or update object — allow-list the keys you expect:

// ❌ attacker can send { email: { $ne: null } } and match every document
const user = await users.findOne(req.query);

// ❌ attacker can send { $set: { role: 'admin' } } via req.body.update
await users.updateRaw({ _id: userId }, req.body.update);

// ✅ allow-list keys, build the filter/update as code
const email = String(req.query.email ?? '');
const user = await users.findOne({ email });

const patch = { name: req.body.name };
await users.updateOne({ _id: userId }, patch);

As defense-in-depth for services that must accept less-trusted input, consider disabling $where-class JavaScript execution on the server:

# mongod.conf
security:
  javascriptEnabled: false

See SECURITY.md at the repo root for the vulnerability reporting policy.


Compatibility

| Feature | Supported | | ---------------- | ------------------------------------------- | | Zod 3 | Yes | | Zod 4 | Yes (same ZodCompat API surface) | | MongoDB driver 5 | Yes (shim normalizes ModifyResult.value) | | MongoDB driver 6 | Yes (direct return from findOneAndUpdate) | | MongoDB driver 7 | Yes | | Node.js | >=22.0.0 | | ESM | Yes | | CJS | Yes |


API Reference

defineCollection(config)

Creates an immutable CollectionDef descriptor.

| Parameter | Type | Default | Description | | ------------------- | ------------ | ------------ | ----------------------------------------------------------------- | | config.name | string | — | MongoDB collection name | | config.schema | ZodCompat | — | Zod schema for the document body | | config.idStrategy | IdStrategy | 'objectid' | ID generation or inference strategy | | config.id | IdStrategy | 'objectid' | Deprecated alias for idStrategy. Providing both is an error | | config.indexes | IndexDef[] | [] | Index definitions |

createRepository(collection, db)

Returns a Repository<Schema, Id> bound to the collection definition and database. The Repository contract is defined in repository.ts; the MongoDB implementation lives in mongo-repository.ts.

Repository<Schema, Id> methods

| Method | Returns | | ------------------------------------- | -------------------------------------------- | | findById(id, options?) | Promise<Result<Doc \| null>> | | findOne(filter, options?) | Promise<Result<Doc \| null>> | | find(filter?, options?) | Promise<Result<Doc[]>> | | query() | QueryBuilder<Schema, Id> | | count(filter?) | Promise<Result<number>> | | exists(filter) | Promise<Result<boolean>> | | insert(data) | Promise<Result<Doc>> | | insertMany(data) | Promise<Result<Doc[]>> | | upsertById(id, data) | Promise<Result<Doc>> | | upsertOne(filter, data) | Promise<Result<Doc>> | | updateById(id, patch, options?) | Promise<Result<Doc \| null>> | | updateOne(filter, patch, options?) | Promise<Result<Doc \| null>> | | updateMany(filter, patch, options?) | Promise<Result<{ modifiedCount: number }>> | | updateRaw(filter, update, options?) | Promise<Result<{ modifiedCount: number }>> | | deleteById(id) | Promise<Result<Doc \| null>> | | deleteOne(filter) | Promise<Result<Doc \| null>> | | deleteMany(filter?) | Promise<Result<{ deletedCount: number }>> | | aggregate(pipeline, outputSchema) | Promise<Result<Infer<Out>[]>> | | session(clientSession) | Repository<Schema, Id> |

QueryBuilder<Schema, Id>

Returned by repo.query(). Each method returns a new, independent builder — never mutates the receiver.

| Method | Parameter | Returns | | ---------------- | ------------------------- | -------------------------- | | filter(filter) | Filter<Doc<Schema, Id>> | QueryBuilder<Schema, Id> | | sort(sort) | Sort (from mongodb) | QueryBuilder<Schema, Id> | | limit(n) | number | QueryBuilder<Schema, Id> | | skip(n) | number | QueryBuilder<Schema, Id> | | exec() | — | Promise<Result<Doc[]>> |

QueryBuilder is exported as a type-only export from @wenu/mongo. createQueryBuilder (the internal factory) is not exported.

options types by method:

| Method group | options type | | ----------------------------- | ------------------------- | | findById, findOne, find | FindOptions<Doc> | | updateById, updateOne | FindOneAndUpdateOptions | | updateMany, updateRaw | UpdateOptions |

All types are imported from mongodb — no custom wrappers.

index(spec, options?)

Creates an IndexDef. spec follows the MongoDB index key format ({ field: 1 }, { field: -1 }, etc.).

syncIndexes(collection, db)

Syncs all declared indexes to MongoDB. Returns Promise<Result<void>>. Idempotent.

generateIndexMigration(collection)

Returns a migrate-mongo compatible JS migration string (up / down) for the collection's indexes.

Result helpers

| Export | Signature | Description | | --------------- | ---------------------------------------- | ---------------------------------- | | ok | <T>(value: T) => Ok<T> | Construct a success result | | err | <E>(error: E) => Err<E> | Construct an error result | | isOk | <T, E>(r: Result<T, E>) => r is Ok<T> | Type guard for success | | isErr | <T, E>(r: Result<T, E>) => r is Err<E> | Type guard for error | | toDbError | (e: unknown) => DbError | Map any thrown value to DbError | | NotFoundError | class extends Error | Domain error → kind: 'not-found' |

ID Strategies at a glance

| Strategy | _id type | Auto-generated | | ---------------------- | --------------- | --------------------------- | | 'objectid' (default) | ObjectId | Yes — new ObjectId() | | 'uuid' | string | Yes — crypto.randomUUID() | | 'string' | string | No — embed _id in data | | Any Zod schema | Infer<Schema> | No — embed _id in data |


Acknowledgements

@wenu/mongo is a clean-room reimagination of ts-valid-mongodb. The original library proved the concept — a lightweight, Zod-validated MongoDB wrapper without Mongoose. @wenu/mongo takes that foundation further: a no-throw Result<T> contract, pluggable ID strategies, Zod 3/4 compatibility, and a MongoDB driver shim that works across v5, v6, and v7.