Querying Data
This guide covers how to effectively query data using Kysera Repository's MongoDB-style operators, sorting, and pagination features.
Basic Queries
Simple Equality
The simplest form of querying uses direct equality:
// Find all active users
const activeUsers = await userRepo.find({
where: { status: 'active' }
})
// Find user by email
const user = await userRepo.findOne({
where: { email: 'alice@example.com' }
})
Multiple Conditions
Multiple conditions are combined with AND:
// Find active premium users
const premiumUsers = await userRepo.find({
where: {
status: 'active',
tier: 'premium',
verified: true
}
})
Using Operators
Comparison Queries
// Users 18 and older
const adults = await userRepo.find({
where: { age: { $gte: 18 } }
})
// Products under $50
const affordable = await productRepo.find({
where: { price: { $lt: 50 } }
})
// Orders from the last 30 days
const recentOrders = await orderRepo.find({
where: {
createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
}
})
IN Queries
// Find users with specific roles
const staffMembers = await userRepo.find({
where: {
role: { $in: ['admin', 'moderator', 'support'] }
}
})
// Exclude certain statuses
const visiblePosts = await postRepo.find({
where: {
status: { $nin: ['draft', 'archived', 'deleted'] }
}
})
Text Search
// Email domain search
const companyUsers = await userRepo.find({
where: {
email: { $endsWith: '@company.com' }
}
})
// Title contains keyword
const searchResults = await postRepo.find({
where: {
title: { $contains: 'typescript' }
}
})
// Case-insensitive search (PostgreSQL)
const nameSearch = await userRepo.find({
where: {
name: { $ilike: '%john%' }
}
})
The $contains, $startsWith, and $endsWith operators automatically escape special LIKE characters (%, _, \) in user input and add an ESCAPE '\' clause to the generated SQL. This prevents unintended wildcard matching when user input contains these characters.
The $like and $ilike operators pass the pattern through as-is, so you control the wildcards yourself.
NULL Checks
// Users who haven't verified email
const unverified = await userRepo.find({
where: {
emailVerifiedAt: { $isNull: true }
}
})
// Posts with featured image
const featuredPosts = await postRepo.find({
where: {
featuredImageUrl: { $isNotNull: true }
}
})
Range Queries
// Products in price range
const midRange = await productRepo.find({
where: {
price: { $between: [25, 75] }
}
})
// Events this week
const thisWeekEvents = await eventRepo.find({
where: {
startDate: { $between: [startOfWeek, endOfWeek] }
}
})
Complex Queries
OR Conditions
// Find admins or verified users
const privilegedUsers = await userRepo.find({
where: {
$or: [
{ role: 'admin' },
{ verified: true }
]
}
})
Nested Logic
// Active users who are either admin OR (verified AND created recently)
const targetUsers = await userRepo.find({
where: {
status: 'active',
$or: [
{ role: 'admin' },
{
$and: [
{ verified: true },
{ createdAt: { $gte: lastMonth } }
]
}
]
}
})
Real-World Example: E-commerce Product Search
interface ProductSearchParams {
query?: string
categories?: string[]
minPrice?: number
maxPrice?: number
inStock?: boolean
sortBy?: 'price' | 'rating' | 'newest'
sortDir?: 'asc' | 'desc'
page?: number
pageSize?: number
}
async function searchProducts(params: ProductSearchParams) {
const {
query,
categories,
minPrice,
maxPrice,
inStock,
sortBy = 'newest',
sortDir = 'desc',
page = 1,
pageSize = 20
} = params
// Build where clause dynamically
const where: WhereClause<Product> = {
status: 'published',
deletedAt: { $isNull: true }
}
if (query) {
where.name = { $contains: query }
}
if (categories?.length) {
where.category = { $in: categories }
}
if (minPrice !== undefined || maxPrice !== undefined) {
where.price = {}
if (minPrice !== undefined) where.price.$gte = minPrice
if (maxPrice !== undefined) where.price.$lte = maxPrice
}
if (inStock) {
where.stockQuantity = { $gt: 0 }
}
// Map sort field
const orderByMap = {
price: 'price',
rating: 'averageRating',
newest: 'createdAt'
} as const
return productRepo.findAndCount({
where,
orderBy: orderByMap[sortBy],
orderDirection: sortDir,
limit: pageSize,
offset: (page - 1) * pageSize
})
}
Sorting
Single Column
// Newest first
const recentPosts = await postRepo.find({
where: { status: 'published' },
orderBy: 'createdAt',
orderDirection: 'desc'
})
// Alphabetical
const sortedUsers = await userRepo.find({
orderBy: 'name',
orderDirection: 'asc'
})
Multiple Columns
// Sort by last name, then first name
const sortedContacts = await contactRepo.find({
sort: [
{ column: 'lastName', direction: 'asc' },
{ column: 'firstName', direction: 'asc' }
]
})
// Sort by priority (desc), then date (asc)
const sortedTasks = await taskRepo.find({
where: { status: 'pending' },
sort: [
{ column: 'priority', direction: 'desc' },
{ column: 'dueDate', direction: 'asc' }
]
})
Pagination
Offset-Based Pagination
Best for: Admin panels, content management, smaller datasets.
async function getPaginatedUsers(page: number, pageSize: number = 20) {
const { items, total } = await userRepo.findAndCount({
where: { status: 'active' },
orderBy: 'createdAt',
orderDirection: 'desc',
limit: pageSize,
offset: (page - 1) * pageSize
})
return {
users: items,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
hasNext: page * pageSize < total,
hasPrev: page > 1
}
}
}
Cursor-Based Pagination
Best for: Infinite scroll, real-time feeds, large datasets.
async function getUserFeed(cursor?: { id: number; createdAt: Date }) {
const result = await userRepo.paginateCursor({
limit: 20,
orderBy: 'createdAt',
orderDirection: 'desc',
cursor: cursor ? { value: cursor.createdAt, id: cursor.id } : null
})
return {
users: result.items,
nextCursor: result.nextCursor,
hasMore: result.hasMore
}
}
// Usage
const page1 = await getUserFeed()
// User scrolls down...
const page2 = await getUserFeed(page1.nextCursor)
Column Selection
Select only the columns you need for better performance:
// Get only IDs
const userIds = await userRepo.find({
where: { status: 'active' },
select: ['id']
})
// Get display info only
const userList = await userRepo.find({
where: { status: 'active' },
select: ['id', 'name', 'avatarUrl'],
orderBy: 'name',
limit: 100
})
// TypeScript knows the return type
// userList: Pick<User, 'id' | 'name' | 'avatarUrl'>[]
Counting and Existence
Count Records
// Total active users
const totalActive = await userRepo.count({
where: { status: 'active' }
})
// Pending orders
const pendingCount = await orderRepo.count({
where: {
status: 'pending',
createdAt: { $gte: today }
}
})
Check Existence
// Check if email is taken
const emailExists = await userRepo.exists({
where: { email: 'test@example.com' }
})
if (emailExists) {
throw new Error('Email already registered')
}
// Check if user has any orders
const hasOrders = await orderRepo.exists({
where: { userId: user.id }
})
Working with Transactions
Operators work seamlessly within transactions:
await userRepo.transaction(async (trx) => {
const txUserRepo = userRepo.withTransaction(trx)
const txOrderRepo = orderRepo.withTransaction(trx)
// Complex query within transaction
const eligibleUsers = await txUserRepo.find({
where: {
status: 'active',
balance: { $gte: 100 },
lastLoginAt: { $gte: thirtyDaysAgo }
}
})
for (const user of eligibleUsers) {
await txOrderRepo.create({
userId: user.id,
type: 'bonus',
amount: 10
})
}
})
Performance Tips
1. Use Indexes
Ensure your database has indexes on columns used in where, orderBy, and sort:
-- Single column indexes
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_email ON users(email);
-- Composite index for common query patterns
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_products_category_price ON products(category, price);
2. Limit Results
Always use limit when you don't need all records:
// Good
const recent = await postRepo.find({
where: { status: 'published' },
orderBy: 'createdAt',
orderDirection: 'desc',
limit: 10
})
// Avoid (fetches all records)
const all = await postRepo.find({
where: { status: 'published' }
})
const recent = all.slice(0, 10)
3. Select Only Needed Columns
// Good - only fetch what you need
const list = await userRepo.find({
select: ['id', 'name'],
limit: 100
})
// Avoid - fetching all columns including large text fields
const list = await userRepo.find({ limit: 100 })
4. Use count() Instead of find().length
// Good
const count = await userRepo.count({
where: { status: 'active' }
})
// Avoid
const users = await userRepo.find({
where: { status: 'active' }
})
const count = users.length
See Also
- Query Operators Reference - Complete operator documentation
- Pagination Guide - Pagination patterns in depth
- Repository API - Full repository documentation