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

@alisdev/mongo-kit

v1.0.3

Published

Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, TTL, and custom indexes

Readme

@alisdev/mongo-kit

Decorator-based MongoDB schema & repository library with fluent query builder, soft delete, hard delete, TTL, custom indexes, and auto-populate relations.

Features

  • 🔌 MongoDB ConnectionMongoConnection utility with lifecycle callbacks
  • 🏗️ Decorator-based Schema@Schema, @VirtualField, @Repository
  • 🔗 Auto-Populate Relations@Relation decorator for automatic $lookup
  • 🔍 Fluent Query Builder — Incremental condition building with $lookup support
  • 📄 Pageable — Built-in pagination with Pageable and PageResult
  • 🗑️ Soft DeletesoftDelete(), restore(), auto-filtering
  • 💥 Hard Deletedelete() / hardDelete() for permanent removal
  • TTL (Time-To-Live)@TTL decorator + per-document expireAt
  • 📇 Custom Indexes@Index decorator for unique, compound, text, and geospatial indexes
  • Zod IntegrationBaseEntitySchema for runtime validation
  • 🔒 Audit FieldscreatedBy, updatedBy, deletedBy with actor tracking

Installation

npm install @alisdev/mongo-kit

Quick Start

import "reflect-metadata";
import {
  MongoConnection,
  Schema, VirtualField, Repository, Relation, Index, TTL,
  BaseRepository, BaseEntity, BaseEntitySchema,
  CustomBuilder, SearchCustom, MultipleSearch, CustomOperation,
  Pageable
} from "@alisdev/mongo-kit";
import { z } from "zod";

1. Connect to MongoDB

// Simple connection
await MongoConnection.connect({
  uri: "mongodb://localhost:27017/mydb",
});

// With full options
await MongoConnection.connect({
  uri: process.env.MONGODB_URI!,
  debug: process.env.NODE_ENV === "development",
  options: {
    maxPoolSize: 10,
    serverSelectionTimeoutMS: 5000,
  },
  onConnected: () => console.log("✅ MongoDB connected"),
  onError: (err) => console.error("❌ MongoDB error:", err),
  onDisconnected: () => console.log("⚠️ MongoDB disconnected"),
});

// Check connection state
console.log(MongoConnection.isConnected()); // true
console.log(MongoConnection.getState());    // 'connected'

// Disconnect gracefully
await MongoConnection.disconnect();

2. Define Entities

@Schema({ collection: "profiles", timestamps: true })
class Profile extends BaseEntity {
  nama: string;
  city: string;
}

@Index({ email: 1 }, { unique: true })
@Index({ firstName: "text", lastName: "text" })
@Schema({ collection: "users", timestamps: true })
class User extends BaseEntity {
  @VirtualField((doc) => `${doc.firstName} ${doc.lastName}`)
  fullName: string;

  firstName: string;
  lastName: string;
  email: string;
  age: number;

  // Auto-populate: profile will be automatically joined on every query
  @Relation({ collection: "profiles", localField: "profileId" })
  profile: IProfile | null;
}

// TTL example — sessions expire 30 days after creation
@TTL("createdAt", 2592000)
@Schema({ collection: "sessions", timestamps: true })
class Session extends BaseEntity {
  token: string;
  userId: string;
}

3. Define Zod Schemas & Types

const IProfileSchema = BaseEntitySchema.extend({
  nama: z.string(),
  city: z.string(),
});
type IProfile = z.infer<typeof IProfileSchema>;

const IUserSchema = BaseEntitySchema.extend({
  firstName: z.string(),
  lastName: z.string(),
  fullName: z.string(),
  email: z.string(),
  age: z.number(),
  profile: IProfileSchema.nullable(), // null if not joined
});
type IUser = z.infer<typeof IUserSchema>;

4. Define Repository

@Repository(User)
class UserRepository extends BaseRepository<IUser> {

  async findByName(name: string): Promise<IUser[]> {
    const builder = new CustomBuilder<IUser>()
      .with(SearchCustom.of("firstName", CustomOperation.LIKE, name));
    return this.find(builder.build());
  }

  async search(filter: {
    name?: string;
    city?: string;
    minAge?: number;
    maxAge?: number;
  }, page: number, size: number) {
    const builder = new CustomBuilder<IUser>();

    if (filter.name) {
      builder.with(SearchCustom.of("firstName", CustomOperation.LIKE, filter.name));
    }

    if (filter.city) {
      builder.with(
        SearchCustom.of("user.profile.city", CustomOperation.OPERATION_JOIN_EQUAL, filter.city)
      );
    }

    if (filter.minAge !== undefined && filter.maxAge !== undefined) {
      builder.with(
        MultipleSearch.of(
          SearchCustom.OPERATION_AND,
          SearchCustom.of("age", CustomOperation.GTE, filter.minAge),
          SearchCustom.of("age", CustomOperation.LTE, filter.maxAge)
        )
      );
    }

    return this.findAll(builder.build(), Pageable.of(page, size, "createdAt", "desc"));
  }
}

5. Usage

Auto-Populate Relations (tanpa builder!)

const userRepo = new UserRepository();

// findById — profile otomatis terbawa!
const user = await userRepo.findById("some-id");
// {
//   _id: "some-id",
//   firstName: "Dudi",
//   profile: { _id: "...", nama: "Dudi S", city: "Jakarta" },  ← auto-populated!
//   ...
// }

// find() — semua user beserta profile-nya
const users = await userRepo.find();
// [{ _id: "...", firstName: "Dudi", profile: { ... } }, ...]

// findAll() dengan pagination — profile juga terbawa
const paged = await userRepo.findAll(undefined, Pageable.of(1, 10));
// { content: [{ profile: { ... }, ... }], page: 1, total: 42, ... }

@Relation Options

// belongsTo (default) — hasil: single object atau null
@Relation({ collection: "profiles", localField: "profileId" })
profile: IProfile | null;

// hasMany — hasil: array
@Relation({
  collection: "orders",
  localField: "_id",
  foreignField: "userId",
  type: "hasMany"
})
orders: IOrder[];

Save

// Save with actor
const newUser = await userRepo.save(
  { firstName: "Dudi", lastName: "Setiawan", email: "[email protected]", age: 25, profileId: "profile_id" },
  { actorId: "admin_user_id" }
);

// Save from system — createdBy/updatedBy will be null
const systemUser = await userRepo.save(
  { firstName: "System", lastName: "Bot", email: "[email protected]", age: 0 }
);

// Save with TTL — document expires in 1 hour
const tempUser = await userRepo.save(
  { firstName: "Temp", lastName: "User", email: "[email protected]", age: 0 },
  { ttl: 3600 }
);

// Save with exact expiry date
const scheduledUser = await userRepo.save(
  { firstName: "Scheduled", lastName: "User", email: "[email protected]", age: 0 },
  { expireAt: new Date("2025-12-31T23:59:59Z") }
);

Update

await userRepo.update(newUser._id, { age: 26 }, { actorId: "admin_user_id" });

Soft Delete & Restore

// Soft delete — document hidden from standard queries
await userRepo.softDelete(newUser._id, { actorId: "admin_user_id" });

// Find only soft-deleted documents (with relations auto-populated)
const deleted = await userRepo.findOnlyDeleted();

// Include soft-deleted in queries (with relations auto-populated)
const all = await userRepo.findWithDeleted();

// Restore a soft-deleted document
await userRepo.restore(newUser._id, { actorId: "admin_user_id" });

Hard Delete

// Permanently remove from database (irreversible)
await userRepo.delete(newUser._id);
// or
await userRepo.hardDelete(newUser._id);

Paginated Search

const result = await userRepo.search(
  { city: "Jakarta", minAge: 18, maxAge: 35 },
  1,
  10
);
// {
//   content: [{ _id: "...", firstName: "Dudi", profile: { ... }, ... }],
//   page: 1, size: 10, total: 24, totalPages: 3,
//   hasPrev: false, hasNext: true
// }

6. Indexes

// Unique index
@Index({ email: 1 }, { unique: true })

// Compound index
@Index({ category: 1, price: -1 })

// Text search index
@Index({ firstName: "text", lastName: "text" })

// Geospatial index
@Index({ location: "2dsphere" })

// Sparse index
@Index({ optionalField: 1 }, { sparse: true })

@Schema({ collection: "products" })
class Product extends BaseEntity { ... }

7. TTL (Time-To-Live)

Entity-level TTL

// Documents expire 24 hours after creation
@TTL("createdAt", 86400)
@Schema({ collection: "otps" })
class OTP extends BaseEntity {
  code: string;
  userId: string;
}

Per-document TTL via expireAt

Every entity has an expireAt field. Set it during save():

// Expires in 5 minutes
await otpRepo.save({ code: "123456", userId: "user1" }, { ttl: 300 });

// Expires at a specific date
await otpRepo.save({ code: "789012", userId: "user2" }, {
  expireAt: new Date("2025-06-01T00:00:00Z")
});

MongoDB's background thread checks TTL indexes every ~60 seconds and removes expired documents automatically.


8. Query Operations

| Operation | Description | MongoDB Equivalent | |-----------|-------------|-------------------| | EQUAL | Exact match | { field: value } | | NOT_EQUAL | Not equal | { $ne: value } | | GT | Greater than | { $gt: value } | | GTE | Greater than or equal | { $gte: value } | | LT | Less than | { $lt: value } | | LTE | Less than or equal | { $lte: value } | | LIKE | Case-insensitive contains | { $regex: value, $options: 'i' } | | STARTS_WITH | Starts with | { $regex: '^value' } | | ENDS_WITH | Ends with | { $regex: 'value$' } | | IN | In array | { $in: [values] } | | NOT_IN | Not in array | { $nin: [values] } | | IS_NULL | Is null | { field: null } | | IS_NOT_NULL | Is not null | { $ne: null } | | EXISTS | Field exists | { $exists: true } | | NOT_EXISTS | Field doesn't exist | { $exists: false } |

All operations also have OPERATION_JOIN_* variants that trigger $lookup for cross-collection queries.


9. BaseEntity Fields

| Field | Type | Description | |-------|------|-------------| | _id | string | MongoDB document ID | | createdAt | Date | Auto-managed by Mongoose | | updatedAt | Date | Auto-managed by Mongoose | | createdBy | string \| null | Set via actorId on save | | updatedBy | string \| null | Set via actorId on save/update | | deletedAt | Date \| null | Set on soft delete, null = active | | deletedBy | string \| null | Set on soft delete | | expireAt | Date \| null | TTL expiry date |


10. Connection — Real-World Usage

Express / NestJS App Bootstrap

// src/database.ts
import { MongoConnection } from "@alisdev/mongo-kit";

export async function connectDatabase() {
  await MongoConnection.connect({
    uri: process.env.MONGODB_URI || "mongodb://localhost:27017/myapp",
    debug: process.env.NODE_ENV === "development",
    options: {
      maxPoolSize: 10,
      minPoolSize: 2,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000,
    },
    onConnected: () => console.log("✅ MongoDB connected"),
    onError: (err) => console.error("❌ MongoDB error:", err.message),
    onDisconnected: () => console.log("⚠️ MongoDB disconnected"),
  });
}

// src/app.ts
import express from "express";
import { connectDatabase } from "./database";
import { MongoConnection } from "@alisdev/mongo-kit";

const app = express();

// Connect before starting server
connectDatabase().then(() => {
  app.listen(3000, () => console.log("Server running on :3000"));
});

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({
    db: MongoConnection.isConnected(),         // true/false
    dbState: MongoConnection.getState(),       // 'connected' | 'disconnected' | ...
  });
});

// Graceful shutdown
process.on("SIGTERM", async () => {
  await MongoConnection.disconnect();
  process.exit(0);
});

Direct Mongoose Access

// If you need the raw mongoose instance
const mongooseInstance = MongoConnection.getMongoose();
// Use for advanced operations like transactions, etc.

11. Relation — Detailed Usage

belongsTo (Many-to-One)

// User has ONE profile → profileId stores the Profile._id
@Schema({ collection: "users" })
class User extends BaseEntity {
  firstName: string;

  @Relation({ collection: "profiles", localField: "profileId" })
  profile: IProfile | null;    // ← single object or null
}

// Usage — profile auto-populated on ALL queries:
const user = await userRepo.findById("user123");
// user.profile = { _id: "...", nama: "Dudi", city: "Jakarta" }  ← auto!

const users = await userRepo.find();
// users[0].profile = { ... }  ← auto!

const paged = await userRepo.findAll(undefined, Pageable.of(1, 10));
// paged.content[0].profile = { ... }  ← auto!

hasMany (One-to-Many)

// User has MANY orders → Order.userId references User._id
@Schema({ collection: "users" })
class User extends BaseEntity {
  firstName: string;

  @Relation({
    collection: "orders",
    localField: "_id",           // match User._id
    foreignField: "userId",      // against Order.userId
    type: "hasMany",
  })
  orders: IOrder[];              // ← array of orders
}

// Usage:
const user = await userRepo.findById("user123");
// user.orders = [{ _id: "...", total: 150000 }, { _id: "...", total: 80000 }]

Multiple Relations on One Entity

@Schema({ collection: "users" })
class User extends BaseEntity {
  firstName: string;
  lastName: string;

  // belongsTo profile
  @Relation({ collection: "profiles", localField: "profileId" })
  profile: IProfile | null;

  // belongsTo department
  @Relation({ collection: "departments", localField: "departmentId" })
  department: IDepartment | null;

  // hasMany orders
  @Relation({ collection: "orders", localField: "_id", foreignField: "userId", type: "hasMany" })
  orders: IOrder[];
}

// ALL three relations auto-populated on every query:
const user = await userRepo.findById("user123");
// user.profile     = { nama: "Dudi", city: "Jakarta" }
// user.department  = { name: "Engineering" }
// user.orders      = [{ total: 150000 }, { total: 80000 }]

Relation + Query Builder (Combined)

@Repository(User)
class UserRepository extends BaseRepository<IUser> {

  // @Relation auto-populates profile on the result
  // CustomBuilder adds filtering logic
  async searchByCity(city: string, page: number, size: number) {
    const builder = new CustomBuilder<IUser>()
      .with(SearchCustom.of(
        "user.profile.city",
        CustomOperation.OPERATION_JOIN_EQUAL,
        city
      ));

    return this.findAll(builder.build(), Pageable.of(page, size));
    // Results have profile auto-populated + filtered by city ✅
  }
}

Without @Relation (Manual Join via Builder)

// If you DON'T use @Relation, relations are NOT auto-populated.
// You must explicitly use OPERATION_JOIN_* in CustomBuilder:

@Schema({ collection: "users" })
class User extends BaseEntity {
  firstName: string;
  // No @Relation here — profile NOT auto-populated
}

const userRepo = new UserRepository();

// findById → NO profile data
const user = await userRepo.findById("user123");
// user = { _id: "...", firstName: "Dudi" }  ← no profile!

// To get profile, you must use builder with join:
const builder = new CustomBuilder<IUser>()
  .with(SearchCustom.of("user.profile.city", CustomOperation.OPERATION_JOIN_EQUAL, "Jakarta"));
const users = await userRepo.find(builder.build());
// Now profile data is in the pipeline via $lookup

12. Complete E-Commerce Example

import "reflect-metadata";
import {
  MongoConnection, Schema, VirtualField, Repository, Relation,
  Index, TTL, BaseRepository, BaseEntity, BaseEntitySchema,
  CustomBuilder, SearchCustom, CustomOperation, Pageable
} from "@alisdev/mongo-kit";
import { z } from "zod";

// ── Connect ───────────────────────────────────────────────────────
await MongoConnection.connect({ uri: "mongodb://localhost:27017/shop" });

// ── Entities ──────────────────────────────────────────────────────
@Schema({ collection: "categories" })
class Category extends BaseEntity { name: string; }

@Index({ sku: 1 }, { unique: true })
@Index({ name: "text", description: "text" })
@Schema({ collection: "products" })
class Product extends BaseEntity {
  name: string;
  sku: string;
  price: number;
  description: string;

  @Relation({ collection: "categories", localField: "categoryId" })
  category: any;
}

@TTL("createdAt", 900)  // OTP expires in 15 minutes
@Schema({ collection: "otps" })
class OTP extends BaseEntity { code: string; userId: string; }

// ── Types ─────────────────────────────────────────────────────────
const IProductSchema = BaseEntitySchema.extend({
  name: z.string(), sku: z.string(), price: z.number(),
  category: z.object({ name: z.string() }).nullable(),
});
type IProduct = z.infer<typeof IProductSchema>;

// ── Repository ────────────────────────────────────────────────────
@Repository(Product)
class ProductRepository extends BaseRepository<IProduct> {
  async searchProducts(keyword: string, minPrice?: number, maxPrice?: number) {
    const builder = new CustomBuilder<IProduct>();
    builder.with(SearchCustom.of("name", CustomOperation.LIKE, keyword));
    if (minPrice) builder.with(SearchCustom.of("price", CustomOperation.GTE, minPrice));
    if (maxPrice) builder.with(SearchCustom.of("price", CustomOperation.LTE, maxPrice));
    return this.findAll(builder.build(), Pageable.of(1, 20, "price", "asc"));
  }
}

// ── Usage ─────────────────────────────────────────────────────────
const productRepo = new ProductRepository();

// Save with TTL
const product = await productRepo.save(
  { name: "Laptop", sku: "LPT-001", price: 15000000, categoryId: "cat_id" },
  { actorId: "admin_id" }
);

// findById — category auto-populated!
const found = await productRepo.findById(product._id);
// found.category = { _id: "cat_id", name: "Electronics" }  ✅

// Soft delete
await productRepo.softDelete(product._id, { actorId: "admin_id" });

// Restore
await productRepo.restore(product._id, { actorId: "admin_id" });

// Hard delete (permanent)
await productRepo.hardDelete(product._id);

// Disconnect
await MongoConnection.disconnect();

License

MIT