listerine is a powerful, type-safe, in-memory query engine with Mongo-ish syntax for performing complex (or simple!) queries on arrays of objects.
- MongoDB-like Query Syntax: Filters and stuff
- Type Safety: Full TypeScript support with generic types
- Logical Operators: Support for
$and
and$or
operations - Nested Object Queries: Query deeply nested object properties
- Array Operations: Comprehensive array filtering and matching
- String Operations: Text searching, pattern matching, and length checks
- Numeric Comparisons: Range / equality checks, mathematical operations
- Existence Checks: Test for null, undefined, empty values
npm install listerine
import { listerine } from 'listerine'
type UserT = {
id: string,
name: string,
age: number,
tags: string[]
}
const hannah = { id: '1', name: 'hannah', age: 25, tags: ['admin', 'dev'] }
const lily = { id: '2', name: 'lily', age: 18, tags: ['guest'] }
const sailor = { id: '3', name: 'sailor', age: 32, tags: ['guest'] }
const userList = [hannah, lily, sailor]
const userCollection = listerine<UserT>(userList)
const query = { age$: { $isGreaterThan: 20 } }
const adults = userCollection.find(query)
expect(adults).toEqual([hannah, sailor])
const query = { tags$: { $contains: 'guest' } }
const guests = userCollection.find(query)
expect(guests).toEqual([lily, sailor])
const members = userCollection.findById(['1', '2'])
expect(members).toEqual([hannah, lily])
const member = userCollection.findById('1')
expect(member).toEqual(hannah)
const member = userCollection.findOne({ name$: { $contains: 'i' } })
expect(member).toEqual(lily)
Creates a new listerine collection with the provided array of objects.
const collection = listerine<UserT>(userList)
expect(collection).toHaveProperty('find')
expect(collection).toHaveProperty('findById')
expect(collection).toHaveProperty('findByIds')
NOTE: listerine will apply an id
property to any object in dataList
that does not already have one.
Finds and returns an array of all documents matching the query criteria.
const collection = listerine<UserT>(userList)
const results = collection.find({ isActive: true })
expect(Array.isArray(results)).toEqual(true)
Finds and returns the document that has the provided id. If no document has the provided id, returns null
.
const collection = listerine<UserT>(userList)
const result = collection.findById('123')
Finds and returns any documents with an id found in the provided ids. If no documents match the provided ids, an empty array.
const collection = listerine<UserT>(userList)
const result = collection.findById('123')
expect(Array.isArray(results)).toEqual(true)
// Direct equality (implicit $equals)
list.find({ name: 'hannah' })
// Explicit equality
list.find({ name$: { $equals: 'hannah' } })
// Not equal
list.find({ name$: { $doesNotEqual: 'hannah' } })
// Greater than
list.find({ age$: { $isGreaterThan: 25 } })
// Less than
list.find({ age$: { $isLessThan: 30 } })
// Greater than or equal to
list.find({ age$: { $isGreaterThanOrEqualTo: 25 } })
// Less than or equal to
list.find({ age$: { $isLessThanOrEqualTo: 30 } })
// Between (inclusive)
list.find({ age$: { $isBetween: } })
// Not between
list.find({ age$: { $isNotBetween: } })
// Contains a value
list.find({ tags$: { $contains: 'admin' } })
// Contains all values
list.find({ tags$: { $containsAll: ['admin', 'dev'] } })
// Contains some values
list.find({ tags$: { $containsSome: ['admin', 'user'] } })
// Does not contain
list.find({ tags$: { $doesNotContain: 'admin' } })
// Is one of (value in array)
list.find({ role$: { $isOneOf: ['admin', 'moderator'] } })
// Is not one of
list.find({ role$: { $isNotOneOf: ['banned', 'suspended'] } })
// Is in (array is subset)
list.find({ permissions$: { $isIn: ['read', 'write', 'delete'] } })
// Is subset of
list.find({ tags$: { $isSubsetOf: ['admin', 'user', 'guest'] } })
// Is superset of
list.find({ permissions$: { $isSupersetOf: ['read'] } })
// Contains substring
list.find({ name$: { $contains: 'Ali' } })
// Starts with
list.find({ name$: { $startsWith: 'A' } })
// Ends with
list.find({ email$: { $endsWith: '@example.com' } })
// Does not start with
list.find({ name$: { $doesNotStartWith: 'B' } })
// Does not end with
list.find({ email$: { $doesNotEndWith: '@spam.com' } })
// Length comparisons
list.find({ name$: { $isLongerThan: 5 } })
list.find({ name$: { $isShorterThan: 10 } })
list.find({ name$: { $isNotLongerThan: 15 } })
list.find({ name$: { $isNotShorterThan: 3 } })
// Field exists
list.find({ email$: { $exists: true } })
// Field does not exist
list.find({ email$: { $exists: false } })
list.find({ email$: { $doesNotExist: true } })
// Field is empty (null, undefined, '', [], {})
list.find({ description$: { $isEmpty: true } })
// Field is not empty
list.find({ description$: { $isNotEmpty: true } })
// AND operation (default behavior)
list.find({
age$: { $isGreaterThan: 25 },
name$: { $startsWith: 'A' },
})
// Explicit AND
list.find({
$and: [{ age$: { $isGreaterThan: 25 } }, { name$: { $startsWith: 'A' } }],
})
// OR operation
list.find({
$or: [{ age$: { $isLessThan: 25 } }, { tags$: { $contains: 'admin' } }],
})
// Complex nested logic
list.find({
$and: [
{
$or: [{ age$: { $isLessThan: 25 } }, { age$: { $isGreaterThan: 35 } }],
},
{ tags$: { $contains: 'active' } },
],
})
const users = [
{
id: '1',
profile: {
personal: {
name: 'hannah',
age: 25,
},
settings: {
theme: 'dark',
},
},
},
]
const list = listerine(users)
// Nested query
list.find({
profile: {
personal: {
name: 'hannah',
age$: { $isGreaterThan: 25 },
},
},
})
// Or...
list.find({
'profile.personal.name': 'hannah',
'profile.personal.age$': { $isGreaterThan: 25 },
})
type UserT = {
id: string
username: string
email: string
age: number
roles: string[]
isActive: boolean
lastLogin?: Date
profile: {
firstName: string
lastName: string
bio?: string
}
}
// π Imagine there are more users that just the one.
const users: UserT[] = [
{
id: '1',
username: 'alice_admin',
email: '[email protected]',
age: 25,
roles: ['admin', 'user'],
isActive: true,
profile: {
firstName: 'hannah',
lastName: 'Johnson',
bio: 'System administrator',
},
}
]
const usersCollection = listerine<UserT>(memberList)
// Find active admin users
const activeAdmins = usersCollection.find({
isActive: true,
roles$: { $contains: 'admin' },
})
// Find users with incomplete profiles
const incompleteProfiles = usersCollection.find({
'profile.bio$': { $doesNotExist: true }
})
// Find young active users or admins
const targetUsers = usersCollection.find({
$or: [
{ roles$: { $contains: 'admin' } },
{
isActive: true,
age$: { $isLessThan: 25 }
},
],
})
type ProductT = {
id: string
name: string
price: number
category: string
tags: string[]
inStock: boolean
rating: number
description: string
}
// π Imagine there are more products that just the one.
const products: ProductT[] = [
{
id: '1',
name: 'Wireless Headphones',
price: 99.99,
category: 'Electronics',
tags: ['wireless', 'audio', 'bluetooth'],
inStock: true,
rating: 4.5,
description: 'High-quality wireless headphones with noise cancellation',
},
]
const productsCollection = listerine<ProductT>(products)
// Find affordable electronics in stock
const affordableElectronics = productsCollection.find({
category: 'Electronics',
price$: { $isLessThanOrEqualTo: 100 },
inStock: true,
})
// Find highly rated products with specific features
const premiumProducts = productsCollection.find({
rating$: { $isGreaterThanOrEqualTo: 4.0 },
$or: [
{ tags$: { $contains: 'premium' } },
{ price$: { $isGreaterThan: 200 } }
],
})
// Search products by description keywords
const results = productsCollection.find({
$or: [
{ name$: { $contains: 'wireless' } },
{ description$: { $contains: 'wireless' } },
{ tags$: { $contains: 'wireless' } }
],
})
listerine is built with TypeScript and provides full type safety:
type DataT = {
id: string
name: string
count: number
}
const data: DataT[] = [...]
const list = listerine<DataT>(data) // Fully typed
// TypeScript will enforce correct property names and types
const results = list.find({
name$: { $contains: 'test' }, // β
Valid
count$: { $isGreaterThan: 5 }, // β
Valid
// invalid$: { $contains: 'test' } // β TypeScript error
})
Contributions are welcome!
No rules or templates -- just have fun!
MIT License