8000
Skip to content
< 8000 !--&-->

Repository files navigation

πŸ” TypeScript MongoDB Criteria Pattern

npm version GitHub Package TypeScript MongoDB License: MIT Node.js Tests

A robust, type-safe implementation of the Criteria Pattern for MongoDB queries in TypeScript. Build complex database queries dynamically with a fluent, composable API designed following Domain-Driven Design (DDD) and Clean Architecture principles.

πŸ“š Table of Contents

🎯 Overview

The Criteria Pattern is a powerful design pattern that enables dynamic query construction without writing raw database queries. This library provides a type-safe, MongoDB-specific implementation that helps you:

  • Build queries dynamically based on runtime conditions
  • Maintain type safety throughout your query construction
  • Compose and reuse query components across your application
  • Separate concerns between query logic and data models
  • Test queries easily with a mockable interface

What is the Criteria Pattern?

The Criteria pattern encapsulates query logic in a structured, object-oriented way. Instead of building query strings or objects directly, you compose Criteria objects that represent your search intentions. This approach provides flexibility, reusability, testability, and type safety.

✨ Key Features

πŸ”’ Type Safety First

  • Full TypeScript support with strict typing
  • Compile-time validation of query structure
  • IntelliSense support for all operations

🧩 Flexible Query Building

  • Support for all common MongoDB operators (EQUAL, NOT_EQUAL, GT, GTE, LT, LTE, CONTAINS, NOT_CONTAINS)
  • NEW: OR operator for complex logical combinations
  • Composable filters that can be combined and reused
  • Dynamic query construction based on runtime conditions

πŸ“Š Advanced Querying

// Simple equality
{ status: { $eq: "active" } }

// Complex OR conditions
{ $or: [
  { name: { $regex: "john" } },
  { email: { $regex: "john" } }
]}

// Range queries
{ age: { $gte: 18 }, price: { $lte: 999.99 } }

🎯 MongoDB Optimized

  • Native MongoDB 6.0+ support
  • Efficient query generation
  • Automatic index-friendly query structure

πŸ“¦ Zero Dependencies

  • Only peer dependencies (MongoDB driver)
  • Lightweight bundle size

πŸ—οΈ Clean Architecture

  • Repository pattern implementation
  • Domain-driven design principles
  • Separation of concerns

πŸ“¦ Installation

# Using npm
npm install @abejarano/ts-mongodb-criteria

# Using yarn
yarn add @abejarano/ts-mongodb-criteria

# Using pnpm
pnpm add @abejarano/ts-mongodb-criteria

Package Manager

This repo is Bun-first and ships with a bun.lock. Use Bun for installs and scripts:

bun install

Peer Dependencies

# MongoDB driver (required)
npm install mongodb@^6.0.0

# TypeScript (for development)
npm install -D typescript@^5.0.0

System Requirements

  • Node.js: 20.0.0 or higher
  • TypeScript: 5.0.0 or higher (for development)
  • MongoDB: 6.0.0 or higher

πŸš€ Quick Start

import {
  Criteria,
  Filters,
  Order,
  Operator,
  MongoRepository,
} from "@abejarano/ts-mongodb-criteria"

// 1. Create filters using a simple Map-based syntax
const filters = [
  new Map([
    ["field", "status"],
    ["operator", Operator.EQUAL],
    ["value", "active"],
  ]),
  new Map([
    ["field", "age"],
    ["operator", Operator.GTE],
    ["value", "18"],
  ]),
]

// 2. Build criteria with filters, sorting, and pagination
const criteria = new Criteria(
  Filters.fromValues(filters),
  Order.desc("createdAt"),
  20, // limit
  1 // page
)

// 3. Use with your MongoDB repository
class UserRepository extends MongoRepository<User> {
  constructor() {
    super(User)
  }

  collectionName(): string {
    return "users"
  }

  // Create indexes the first time the collection is accessed
  protected async ensureIndexes(collection: Collection): Promise<void> {
    await collection.createIndex({ email: 1 }, { unique: true })
  }
}

const userRepo = new UserRepository()
const { results } = await userRepo.list(criteria)

Indexes are created lazily the first time the repository accesses the collection (via list, one, or upsert). Use the collection argument inside ensureIndexes to avoid recursion.

MongoRepository provides ready-to-use public methods for repositories that extend it:

  • list(criteria, transaction?) for paginated queries
  • many(filter, options?) to fetch multiple entities matching a filter, with optional { transaction?, sort? }
  • one(filter, transaction?) to fetch a single entity
  • upsert(entity, transaction?) to persist an aggregate Internal helpers are private, so repositories should call these public methods directly.

If you need a repository interface in your app, extend IRepository<T> so your custom interfaces stay aligned with the library return types:

import type { IRepository } from "@abejarano/ts-mongodb-criteria"

export interface IUserRepository extends IRepository<User> {
  // Add domain-specific methods here
}

The goal of IRepository is to prevent signature drift between your app's interfaces and the library's repository return types.

Atomic Transactions

MongoTransaction.run groups writes from one or more repositories into a single MongoDB transaction. Pass the tx context to every write that must be committed or rolled back together:

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

await MongoTransaction.run(async (tx) => {
  await userRepository.upsert(user, tx)
  await auditRepository.upsert(auditEntry, tx)
  await sessionRepository.delete({ userId: user.getId() }, undefined, tx)

  // Reads can observe writes made earlier in this transaction.
  const persistedUser = await userRepository.one({ id: user.getId() }, tx)
  const activeUsers = await userRepository.many(
    { status: "active" },
    { transaction: tx, sort: Order.asc("name") }
  )
})

If any operation in the callback fails, MongoDB rolls back all the writes and the error is propagated to the caller. The MongoDB driver handles retryable transaction errors through withTransaction.

Custom repositories can include their protected atomic updates in the same transaction by accepting and forwarding MongoTransaction:

async deactivateUser(id: string, tx: MongoTransaction): Promise<void> {
  await this.updateOne({ id }, { $set: { active: false } }, tx)
}

MongoDB transactions require a replica set or a sharded cluster; standalone MongoDB instances do not support them. This initial API applies the transaction context to upsert, delete, protected updateOne, one, list, and many. Pass tx as the second argument to list, and inside the options object ({ transaction: tx }) to many.

Your First Query in 30 Seconds:

// Find active users over 18, sorted by creation date
const activeAdultUsers = new Criteria(
  Filters.fromValues([
    new Map([
      ["field", "status"],
      ["operator", Operator.EQUAL],
      ["value", "active"],
    ]),
    new Map([
      ["field", "age"],
      ["operator", Operator.GTE],
      ["value", "18"],
    ]),
  ]),
  Order.desc("createdAt"),
  10, // Get 10 results
  1 // First page
)

const results = await repository.list(activeAdultUsers)

πŸ—„οΈ Migrations

This repo includes a CLI wrapper for migrate-mongo. To use it in your application:

  1. Initialize the configuration:
bun ts-mongo init
  1. Edit migrate-mongo-config.js with your database details.

  2. Run migrations using the ts-mongo command:

# Create a new migration
bun ts-mongo migrate:create add-users-index

# Run migrations up
bun ts-mongo migrate:up

# Undo last migration
bun ts-mongo migrate:down

# Check status
bun ts-mongo migrate:status

Note: You can also use npx, bunx or yarn run.

See the full CLI guide here: docs/mongo-migrations.md.

πŸ“– Documentation

πŸ“– Complete Documentation

🎯 Key Concepts

1. Criteria - The main query builder

const criteria = new Criteria(filters, order, limit?, page?)

2. Filters - Collection of filter conditions

const filters = Filters.fromValues([...filterMaps])

3. Order - Sorting specification

const order = Order.desc("createdAt") // or Order.asc("name")

4. Operators - Available filter operations

  • EQUAL, NOT_EQUAL - Exact matching
  • GT, GTE, LT, LTE - Range operations
  • BETWEEN - Inclusive range with lower and upper bounds
  • CONTAINS, NOT_CONTAINS - Text search
  • OR - Logical OR combinations

βœ… Runtime Compatibility

This library ships dual builds (CJS + ESM) and works in both Node.js and Bun.

πŸ†• OR Operator Example

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

// Search across multiple fields
const searchConditions: OrCondition[] = [
  { field: "name", operator: Operator.CONTAINS, value: "john" },
  { field: "email", operator: Operator.CONTAINS, value: "john" },
]

const filters = [
  new Map([
    ["field", "search"],
    ["operator", Operator.OR],
    ["value", searchConditions],
  ]),
]

// Generates: { $or: [
//   { name: { $regex: "john" } },
//   { email: { $regex: "john" } }
// ]}

⏱️ BETWEEN Operator Example

// Filter users created between two dates
const filters = [
  new Map([
    ["field", "createdAt"],
    ["operator", Operator.BETWEEN],
    ["value", { start: new Date("2024-01-01"), end: new Date("2024-01-31") }],
  ]),
]

const criteria = new Criteria(Filters.fromValues(filters), Order.none())

// Generates: { createdAt: { $gte: 2024-01-01, $lte: 2024-01-31 } }

πŸ§ͺ Testing

The library includes comprehensive test coverage (45/45 tests passing).

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

🀝 Contributing

We welcome contributions! Please follow these guidelines:

Development Setup

# Clone the repository
git clone https://github.com/abejarano/ts-mongo-criteria.git
cd ts-mongo-criteria

# Install dependencies
yarn install

# Run tests
yarn test

# Build the project
yarn build

Contribution Process

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (yarn test)
  5. Commit using conventional commits (git commit -m 'feat: add amazing feature')
  6. Push to your branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

CLI & Migrations

To use the migration features, you must first initialize the configuration in your project:

bun run ts-mongo init

This will create a migrate-mongo-config.js file in your project root. You must configure your MongoDB connection settings in this file (or via environment variables as defined in the config).

Commands

Command Description
init Initialize configuration file
migrate:create <name> Create a new migration file
migrate:up Run pending migrations
migrate:down Revert the last applied migration
migrate:status Check the status of migrations

Example:

bun run ts-mongo migrate:create add-users-collection

πŸ“„ License

This project is licensed under the MIT License. See the LICENSE file for details.


πŸ‘¨β€πŸ’» Author

Ángel Bejarano
πŸ“§ angel.bejarano@jaspesoft.com
πŸ™ GitHub
🏒 Jaspesoft


⭐️ If this project helps you, please give it a star on GitHub!

🀝 Questions or suggestions? Open an issue or start a discussion.

πŸ“’ Follow us for updates and new features!

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0