Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ From the esix package directory (`packages/esix/`):

## Query Methods

- **where**: Filter by field equality
- **where**: Filter by field equality or comparison
- Two-parameter syntax: `where('status', 'active')` for equality
- Three-parameter syntax: `where('age', '>', 18)` with comparison operators
- Supported operators: `=`, `!=`, `<>`, `>`, `>=`, `<`, `<=`
- Maps to MongoDB operators: `$gt`, `$gte`, `$lt`, `$lte`, `$ne`
- **whereIn**: Filter where a field's value is in an array
- **whereNotIn**: Filter where a field's value is not in an array
- **find**: Find a model by its ID
Expand All @@ -79,6 +83,26 @@ From the esix package directory (`packages/esix/`):
- **skip**: Skip a number of results (for pagination)
- **pluck**: Extract an array of values for a specific field

### Query Examples

```typescript
// Equality (two-parameter syntax)
const activeUsers = await User.where('status', 'active').get()

// Comparison operators (three-parameter syntax)
const adults = await User.where('age', '>', 18).get()
const seniors = await User.where('age', '>=', 65).get()
const youngUsers = await User.where('age', '<', 30).get()
const affordableProducts = await Product.where('price', '<=', 50).get()
const activeUsers = await User.where('status', '!=', 'banned').get()

// Chaining multiple conditions
const workingAge = await User
.where('age', '>=', 18)
.where('age', '<=', 65)
.get()
```

## Aggregation Methods

- **aggregate**: Direct access to MongoDB aggregation pipeline
Expand Down Expand Up @@ -123,6 +147,9 @@ types.

## Recent Implementations

- **Comparison Operators**: Added support for comparison operators in where
clauses (`>`, `>=`, `<`, `<=`, `=`, `!=`, `<>`), following Laravel Eloquent
and Rails Active Record patterns
- **Aggregation Functions**: Added static aggregation methods to BaseModel for
direct use on model classes (count, sum, average, max, min, percentile,
aggregate)
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,24 @@ await user.delete()
// Find by field
const activeUsers = await User.where('isActive', true).get()

// Comparison operators
const adults = await User.where('age', '>', 18).get()
const seniors = await User.where('age', '>=', 65).get()
const youngUsers = await User.where('age', '<', 30).get()
const affordableItems = await Product.where('price', '<=', 50).get()
const nonBannedUsers = await User.where('status', '!=', 'banned').get()

// Multiple conditions
const youngActiveUsers = await User.where('isActive', true)
.where('age', '<', 25)
.get()

// Range queries
const workingAge = await User
.where('age', '>=', 18)
.where('age', '<=', 65)
.get()

// Find one
const admin = await User.where('email', 'admin@example.com').first()

Expand Down
249 changes: 249 additions & 0 deletions packages/esix/src/base-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,255 @@ describe('BaseModel', () => {
})
})

describe('where with comparison operators', () => {
it('filters using greater than operator', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780141439518',
pages: 544,
title: 'Jane Eyre',
updatedAt: null
},
{
_id: '5f0aefba348289a81889a920',
authorId: 'author-2',
createdAt: 1594552346653,
isAvailable: true,
isbn: '9780140449266',
pages: 688,
title: 'Crime and Punishment',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '>', 300).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $gt: 300 }
})

expect(books).toHaveLength(2)
})

it('filters using greater than or equal operator', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486284736',
pages: 279,
title: 'Pride and Prejudice',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '>=', 279).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $gte: 279 }
})

expect(books).toHaveLength(1)
})

it('filters using less than operator', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486284736',
pages: 279,
title: 'Pride and Prejudice',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '<', 300).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $lt: 300 }
})

expect(books).toHaveLength(1)
})

it('filters using less than or equal operator', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486284736',
pages: 279,
title: 'Pride and Prejudice',
updatedAt: null
},
{
_id: '5f0aefba348289a81889a920',
authorId: 'author-1',
createdAt: 1594552346653,
isAvailable: true,
isbn: '9780486411095',
pages: 376,
title: 'Wuthering Heights',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '<=', 376).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $lte: 376 }
})

expect(books).toHaveLength(2)
})

it('filters using equals operator', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486284736',
pages: 279,
title: 'Pride and Prejudice',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '=', 279).get()

expect(collection.find).toHaveBeenCalledWith({
pages: 279
})

expect(books).toHaveLength(1)
})

it('filters using not equals operator (!=)', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780141439518',
pages: 544,
title: 'Jane Eyre',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '!=', 279).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $ne: 279 }
})

expect(books).toHaveLength(1)
})

it('filters using not equals operator (<>)', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780141439518',
pages: 544,
title: 'Jane Eyre',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '<>', 279).get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $ne: 279 }
})

expect(books).toHaveLength(1)
})

it('chains multiple comparison operators', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486411095',
pages: 376,
title: 'Wuthering Heights',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('pages', '>', 300)
.where('pages', '<', 500)
.get()

expect(collection.find).toHaveBeenCalledWith({
pages: { $lt: 500 }
})

expect(books).toHaveLength(1)
})

it('maintains backward compatibility with 2-parameter syntax', async () => {
const cursor = createCursor([
{
_id: '5f0aeaeacff57e3ec676b340',
authorId: 'author-1',
createdAt: 1594552340652,
isAvailable: true,
isbn: '9780486284736',
pages: 279,
title: 'Pride and Prejudice',
updatedAt: null
}
])

collection.find.mockReturnValue(cursor)

const books = await Book.where('authorId', 'author-1').get()

expect(collection.find).toHaveBeenCalledWith({
authorId: 'author-1'
})

expect(books).toHaveLength(1)
})
})

describe('whereIn', () => {
it('finds all documets that matches the ids', async () => {
const cursor = createCursor([
Expand Down
30 changes: 26 additions & 4 deletions packages/esix/src/base-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'reflect-metadata'

import QueryBuilder from './query-builder'
import type { ObjectType, Dictionary } from './types'
import type { ComparisonOperator, Dictionary, ObjectType } from './types'
import { camelCase } from 'change-case'

export default class BaseModel {
Expand Down Expand Up @@ -323,22 +323,44 @@ export default class BaseModel {
}

/**
* Returns a QueryBuilder where `key` matches `value`.
* Returns a QueryBuilder where `key` matches `value` or satisfies the comparison.
*
* Example
* ```
* const posts = await BlogPost.where('status', 'published').get();
* const adults = await User.where('age', '>=', 18).get();
* const youngUsers = await User.where('age', '<', 30).get();
* ```
*
* @param key
* @param value
* @param operatorOrValue - Comparison operator or value when using 2-param syntax
* @param value - The value when using 3-param syntax with operator
*/
static where<T extends BaseModel>(
this: ObjectType<T>,
key: string,
value: any
): QueryBuilder<T>
static where<T extends BaseModel>(
this: ObjectType<T>,
key: string,
operator: ComparisonOperator,
value: any
): QueryBuilder<T>
static where<T extends BaseModel>(
this: ObjectType<T>,
key: string,
operatorOrValue: ComparisonOperator | any,
value?: any
): QueryBuilder<T> {
return new QueryBuilder(this).where(key, value)
if (value !== undefined) {
return new QueryBuilder(this).where(
key,
operatorOrValue as ComparisonOperator,
value
)
}
return new QueryBuilder(this).where(key, operatorOrValue)
}

/**
Expand Down
Loading