Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/abejarano/ts-mongo-criteria/llms.txt

Use this file to discover all available pages before exploring further.

Switching to the Criteria pattern does not require a big-bang rewrite. Because MongoRepository sits on top of the native MongoDB driver, old query code and new Criteria-based code can coexist in the same repository class and run against the same collection simultaneously. This guide shows you how to migrate incrementally, validate correctness, and clean up legacy code without downtime.

Migration Strategies

From Raw MongoDB

Basic find

// ── Before ───────────────────────────────────────────────────────────────────
const users = await db.collection("users")
  .find({ status: "active" })
  .toArray();

// ── After ────────────────────────────────────────────────────────────────────
const criteria = new Criteria(
  Filters.fromValues([
    new Map([["field", "status"], ["operator", Operator.EQUAL], ["value", "active"]]),
  ]),
  Order.none()
);
const { results } = await userRepo.list<User>(criteria);

Date range

// ── Before ───────────────────────────────────────────────────────────────────
const start = new Date("2024-01-01");
const end   = new Date("2024-01-31");

const orders = await db.collection("orders")
  .find({ createdAt: { $gte: start, $lte: end } })
  .sort({ createdAt: -1 })
  .toArray();

// ── After ────────────────────────────────────────────────────────────────────
const criteria = new Criteria(
  Filters.fromValues([
    new Map([["field", "createdAt"], ["operator", Operator.GTE], ["value", start.toISOString()]]),
    new Map([["field", "createdAt"], ["operator", Operator.LTE], ["value", end.toISOString()]]),
  ]),
  Order.desc("createdAt")
);
const { results: orders } = await orderRepo.list<Order>(criteria);

$or query

// ── Before ───────────────────────────────────────────────────────────────────
const results = await db.collection("users").find({
  $or: [
    { name:  { $regex: "alice", $options: "i" } },
    { email: { $regex: "alice", $options: "i" } },
  ],
}).toArray();

// ── After ────────────────────────────────────────────────────────────────────
const searchConditions: OrCondition[] = [
  { field: "name",  operator: Operator.CONTAINS, value: "alice" },
  { field: "email", operator: Operator.CONTAINS, value: "alice" },
];

const criteria = new Criteria(
  Filters.fromValues([
    new Map([["field", "search"], ["operator", Operator.OR], ["value", searchConditions]]),
  ]),
  Order.none()
);
const { results } = await userRepo.list<User>(criteria);

From Mongoose

.find().sort().limit().exec()

// ── Before (Mongoose) ─────────────────────────────────────────────────────
const users = await UserModel
  .find({ status: "active" })
  .sort({ createdAt: -1 })
  .limit(20)
  .exec();

// After paginated with page number:
const skip = (page - 1) * 20;
const paginatedUsers = await UserModel
  .find({ status: "active" })
  .sort({ createdAt: -1 })
  .skip(skip)
  .limit(20)
  .exec();

// ── After (Criteria) ──────────────────────────────────────────────────────
const criteria = new Criteria(
  Filters.fromValues([
    new Map([["field", "status"], ["operator", Operator.EQUAL], ["value", "active"]]),
  ]),
  Order.desc("createdAt"),
  20,
  page // replaces the manual skip calculation
);
const { results, count } = await userRepo.list<User>(criteria);

.populate() → separate queries

MongoRepository has no population concept. Replace Mongoose .populate() with explicit queries and join the results in application code:
// ── Before (Mongoose) ─────────────────────────────────────────────────────
const orders = await OrderModel
  .find({ userId })
  .populate("user")
  .sort({ createdAt: -1 })
  .exec();

// ── After (explicit join) ─────────────────────────────────────────────────
const orderCriteria = new Criteria(
  Filters.fromValues([
    new Map([["field", "userId"], ["operator", Operator.EQUAL], ["value", userId]]),
  ]),
  Order.desc("createdAt")
);

// Run both queries in parallel
const [orders, user] = await Promise.all([
  orderRepo.list<Order>(orderCriteria).then((r) => r.results),
  userRepo.one({ id: userId }),
]);

const ordersWithUser = orders.map((order) => ({ ...order.toPrimitives(), user }));

Step-by-step migration plan

1

Phase 1 — Install and wire up (Day 1)

Install the package and create a single repository for one of your existing collections. Verify the connection works by calling one({}) and checking that a document is returned.
npm install @abejarano/ts-mongodb-criteria
// Set MONGO_URI in your .env, then:
const repo = new UserRepository();
const sample = await repo.one({});
console.log("Connected:", sample?.getId());
2

Phase 2 — Migrate basic find queries (Week 1)

Replace find({ field: value }) calls with equality Criteria objects one method at a time. Add a @deprecated JSDoc on the old method and ship both in the same PR.
// New
async getActiveUsers(): Promise<User[]> {
  const criteria = new Criteria(
    Filters.fromValues([
      new Map([["field", "status"], ["operator", Operator.EQUAL], ["value", "active"]]),
    ]),
    Order.desc("createdAt"),
    100
  );
  return (await this.list<User>(criteria)).results;
}
3

Phase 3 — Migrate range and search queries (Week 2)

Add Operator.GTE, Operator.LTE, Operator.BETWEEN, and Operator.CONTAINS / OR to cover date-range and text-search patterns. Set up text indexes in ensureIndexes alongside the migration.
4

Phase 4 — Dynamic builders and pagination (Week 3)

Replace any hand-rolled skip / limit pagination with the page argument on Criteria. Centralise dynamic filter construction in dedicated builder classes or methods to avoid repetitive Map construction scattered across the codebase.
class UserCriteriaBuilder {
  static search(req: UserSearchRequest): Criteria {
    const maps: Array<Map<string, any>> = [];
    if (req.status)
      maps.push(new Map([["field","status"],["operator",Operator.EQUAL],["value",req.status]]));
    if (req.minAge)
      maps.push(new Map([["field","age"],["operator",Operator.GTE],["value",req.minAge.toString()]]));
    return new Criteria(Filters.fromValues(maps), Order.desc("createdAt"), req.limit ?? 20, req.page ?? 1);
  }
}
5

Phase 5 — Cleanup (Week 4)

Delete every @deprecated method, remove legacy MongoDB driver imports that are no longer used, and update your team documentation. Run the full integration test suite to confirm nothing regressed.
Use feature flags for high-risk query paths. Gate the Criteria code behind a flag like criteria-migration-users and enable it for 1 % of traffic, then ramp up. If error rates spike, flip the flag off — the legacy path is still in place and zero schema changes are needed.

Testing strategy

Unit-test the converter output

import { MongoCriteriaConverter } from "@abejarano/ts-mongodb-criteria";

describe("UserCriteriaBuilder", () => {
  const converter = new MongoCriteriaConverter();

  it("generates the correct filter for active adults", () => {
    const criteria = UserCriteriaBuilder.search({ status: "active", minAge: 18 });
    const { filter } = converter.convert(criteria);

    expect(filter).toEqual({ status: "active", age: { $gte: 18 } });
  });

  it("includes $or for search terms", () => {
    const criteria = UserCriteriaBuilder.search({ searchTerm: "alice" });
    const { filter } = converter.convert(criteria);

    expect(filter.$or).toHaveLength(2);
  });
});

Integration-test both paths in parallel

describe("Migration integration", () => {
  it("returns identical IDs from old and new implementations", async () => {
    const req = { status: "active", minAge: 25 };

    const [oldIds, newIds] = await Promise.all([
      legacyService.findUsers(req).then((r) => r.map((u) => u.id).sort()),
      newService.findUsers(req).then((r) => r.map((u) => u.getId()).sort()),
    ]);

    expect(newIds).toEqual(oldIds);
  });
});
Run both test layers in CI on every PR that touches repository or criteria code.

Build docs developers (and LLMs) love