Queries
Use the chainable query builder for readable, type‑safe AQL generation. arango-typed provides a fluent, Mongoose-like query API that compiles to efficient AQL queries.
What are Queries?
Queries in arango-typed allow you to find, filter, sort, and retrieve documents from your collections using a chainable, type-safe API. Instead of writing raw AQL, you use a familiar query builder that automatically generates optimized AQL queries.
Queries help you:
- Type Safety: TypeScript support for query results
- Readability: Chainable API is easier to read than raw AQL
- Performance: Automatic query caching and optimization
- Flexibility: Support for complex filters, sorting, pagination
- Multi-tenancy: Automatic tenant filtering when enabled
Basic Query Methods
Find - Get Multiple Documents
Use find() to query multiple documents:
import { model, Schema } from 'arango-typed';
const UserSchema = new Schema({
name: String,
email: String,
age: Number,
active: Boolean
});
const User = model('users', UserSchema);
// Find all active users
const activeUsers = await User.find({ active: true }).all();
// Find users by age
const adults = await User.find({ age: { $gte: 18 } }).all();
// Find users with multiple conditions
const users = await User.find({
active: true,
age: { $gte: 18, $lte: 65 }
}).all();
FindOne - Get Single Document
Use findOne() to get a single document:
// Find one user by email
const user = await User.findOne({ email: 'john@example.com' });
if (user) {
console.log(user.name);
} else {
console.log('User not found');
}
// Find one with conditions
const activeUser = await User.findOne({
email: 'john@example.com',
active: true
});
FindById - Get Document by ID
Use findById() to get a document by its ID:
// Find by ID
const user = await User.findById('users/123');
if (user) {
console.log(user.name);
} else {
console.log('User not found');
}
// IDs can be in different formats
const user1 = await User.findById('users/123'); // Full ID
const user2 = await User.findById('123'); // Key only (if collection name is known)
Query Operators
arango-typed supports various query operators for flexible filtering:
Comparison Operators
// Greater than or equal
const adults = await User.find({ age: { $gte: 18 } }).all();
// Less than or equal
const seniors = await User.find({ age: { $lte: 65 } }).all();
// Greater than
const youngAdults = await User.find({ age: { $gt: 18 } }).all();
// Less than
const minors = await User.find({ age: { $lt: 18 } }).all();
// Not equal
const inactiveUsers = await User.find({ active: { $ne: true } }).all();
// Equal (explicit)
const activeUsers = await User.find({ active: { $eq: true } }).all();
// Or simply:
const activeUsers2 = await User.find({ active: true }).all();
Array Operators
// In array
const users = await User.find({
role: { $in: ['admin', 'editor', 'moderator'] }
}).all();
// Not in array
const regularUsers = await User.find({
role: { $nin: ['admin', 'editor'] }
}).all();
// Array size
const usersWithManyPosts = await User.find({
posts: { $size: { $gte: 10 } }
}).all();
// All elements match
const usersWithAllTags = await User.find({
tags: { $all: ['javascript', 'typescript'] }
}).all();
String Operators
// Regex pattern matching
const johns = await User.find({
name: { $regex: /^John/i }
}).all();
// Or with string pattern
const johns2 = await User.find({
name: { $regex: '^John', $options: 'i' }
}).all();
// Text search (requires fulltext index)
const results = await User.find({
bio: { $text: 'developer' }
}).all();
Partial Text Search (Automatic)
arango-typed automatically performs case-insensitive partial text search for fields ending with Contains. This is a simple and intuitive way to search for partial matches without using regex operators.
How it works: When you use a field name ending with Contains (case-insensitive), the library automatically:
- Extracts the actual field name by removing the
Containssuffix - Performs a case-insensitive
LIKEsearch with the value - Matches any document where the field contains the search term
// Automatic partial text search
// Searches for documents where 'name' field contains 'john' (case-insensitive)
const users = await User.find({
nameContains: 'john'
}).all();
// Generates: LOWER(doc.name) LIKE '%john%'
// Multiple partial searches
const results = await User.find({
nameContains: 'john',
codeContains: 'ABC'
}).all();
// Searches for name containing 'john' AND code containing 'ABC'
// Works with nested fields
const users = await User.find({
'user.emailContains': 'gmail'
}).all();
// Searches for user.email containing 'gmail'
// Combine with other operators
const filtered = await User.find({
nameContains: 'john',
age: { $gte: 18 },
active: true
}).all();
Examples:
nameContains: 'john'→ searchesnamefield for 'john'codeContains: 'ABC'→ searchescodefield for 'ABC'emailContains: 'gmail'→ searchesemailfield for 'gmail'user.emailContains: 'example'→ searches nesteduser.emailfield
Note: This feature only works with string values. Non-string values or operator objects will use normal exact matching.
Existence Operators
// Field exists
const usersWithEmail = await User.find({
email: { $exists: true }
}).all();
// Field does not exist
const usersWithoutEmail = await User.find({
email: { $exists: false }
}).all();
Logical Operators
// AND (default - multiple conditions)
const users = await User.find({
active: true,
age: { $gte: 18 }
}).all();
// OR
const users = await User.find({
$or: [
{ role: 'admin' },
{ role: 'editor' }
]
}).all();
// AND (explicit)
const users = await User.find({
$and: [
{ active: true },
{ age: { $gte: 18 } }
]
}).all();
// NOT
const users = await User.find({
role: { $not: { $eq: 'admin' } }
}).all();
// NOR (neither condition)
const users = await User.find({
$nor: [
{ role: 'admin' },
{ role: 'editor' }
]
}).all();
Date Operators
// Date comparisons
const recentUsers = await User.find({
createdAt: { $gte: new Date('2024-01-01') }
}).all();
const oldUsers = await User.find({
createdAt: { $lt: new Date('2020-01-01') }
}).all();
// Date range
const usersInRange = await User.find({
createdAt: {
$gte: new Date('2024-01-01'),
$lte: new Date('2024-12-31')
}
}).all();
Chaining Query Methods
Queries are chainable, allowing you to build complex queries step by step:
Where - Filter Conditions
// Chain where conditions
const users = await User.find({ active: true })
.where({ age: { $gte: 18 } })
.where({ email: { $exists: true } })
.all();
// Or combine in single find
const users = await User.find({
active: true,
age: { $gte: 18 },
email: { $exists: true }
}).all();
Select - Projection (Choose Fields)
Select only the fields you need to reduce data transfer:
// Select specific fields
const users = await User.find({ active: true })
.select(['name', 'email'])
.all();
// Returns only name and email fields
// Result: [{ name: 'John', email: 'john@example.com' }, ...]
// Select nested fields
const users = await User.find({})
.select(['name', 'profile.bio', 'profile.avatar'])
.all();
Sort - Order Results
Sort results by one or more fields:
// Single field sort (descending)
const users = await User.find({})
.sort({ createdAt: -1 }) // -1 = descending, 1 = ascending
.all();
// Single field sort (ascending)
const users = await User.find({})
.sort({ name: 1 })
.all();
// Multiple field sort
const users = await User.find({})
.sort({ active: -1, createdAt: -1 })
.all();
// Sort by active first (descending), then by createdAt (descending)
// Array format for sort
const users = await User.find({})
.sort([
{ field: 'active', direction: -1 },
{ field: 'createdAt', direction: -1 }
])
.all();
Limit - Limit Results
// Limit to 10 results
const users = await User.find({})
.limit(10)
.all();
// Always use limit for large collections
const topUsers = await User.find({ active: true })
.sort({ score: -1 })
.limit(100)
.all();
Skip - Skip Results
// Skip first 10 results
const users = await User.find({})
.skip(10)
.limit(10)
.all();
Combining Methods
// Chain multiple methods
const users = await User.find({ active: true })
.select(['name', 'email', 'age'])
.sort({ createdAt: -1 })
.skip(20)
.limit(10)
.all();
// This query:
// 1. Finds active users
// 2. Selects only name, email, and age
// 3. Sorts by createdAt (newest first)
// 4. Skips first 20 results
// 5. Limits to 10 results
Pagination
Use skip() and limit() for pagination:
Basic Pagination
// Pagination helper function
function paginate(page: number, pageSize: number) {
const skip = (page - 1) * pageSize;
return { skip, limit: pageSize };
}
// Get page 1 (first 20 results)
const page1 = await User.find({})
.skip(0)
.limit(20)
.all();
// Get page 2 (results 21-40)
const page2 = await User.find({})
.skip(20)
.limit(20)
.all();
// Get page 3 (results 41-60)
const page3 = await User.find({})
.skip(40)
.limit(20)
.all();
Complete Pagination Example
async function getUsersPage(page: number, pageSize: number = 20) {
const skip = (page - 1) * pageSize;
const [users, total] = await Promise.all([
User.find({ active: true })
.sort({ createdAt: -1 })
.skip(skip)
.limit(pageSize)
.all(),
User.find({ active: true }).count()
]);
return {
users,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
hasNext: page * pageSize < total,
hasPrev: page > 1
}
};
}
// Usage
const result = await getUsersPage(1, 20);
console.log(`Page ${result.pagination.page} of ${result.pagination.totalPages}`);
console.log(`Showing ${result.users.length} of ${result.pagination.total} users`);
Nested Field Queries
Query nested fields using dot notation:
const UserSchema = new Schema({
name: String,
profile: {
bio: String,
avatar: String,
settings: {
theme: String,
notifications: Boolean
}
}
});
const User = model('users', UserSchema);
// Query nested fields
const users = await User.find({
'profile.bio': { $exists: true },
'profile.settings.theme': 'dark'
}).all();
// Update nested fields
await User.updateOne(
{ _id: 'users/123' },
{ 'profile.settings.theme': 'light' }
);
Complex Queries
Multiple Conditions
// Complex query with multiple conditions
const users = await User.find({
active: true,
age: { $gte: 18, $lte: 65 },
email: { $exists: true, $regex: /@example\.com$/ },
role: { $in: ['user', 'premium'] },
createdAt: { $gte: new Date('2024-01-01') }
})
.sort({ score: -1, createdAt: -1 })
.limit(50)
.all();
OR Conditions
// Users who are either admins or have high score
const users = await User.find({
$or: [
{ role: 'admin' },
{ score: { $gte: 1000 } }
]
}).all();
// Multiple OR conditions
const users = await User.find({
$or: [
{ role: 'admin' },
{ role: 'editor' },
{ verified: true, score: { $gte: 500 } }
]
}).all();
Combining AND and OR
// Complex query with AND and OR
const users = await User.find({
active: true, // AND condition
$or: [
{ role: 'admin' },
{ role: 'editor' },
{ verified: true, score: { $gte: 1000 } } // Nested AND
],
createdAt: { $gte: new Date('2024-01-01') } // AND condition
}).all();
Query Results
All Results
// Get all results as array
const users = await User.find({ active: true }).all();
// Returns: Document[]
// Always use with limit for large collections
const users = await User.find({ active: true })
.limit(100)
.all();
First Result
// Get first result
const user = await User.find({ active: true })
.sort({ createdAt: -1 })
.first();
// Returns: Document | null
// Equivalent to findOne but with chaining
const user = await User.find({ active: true })
.limit(1)
.first();
Count Results
// Count matching documents
const count = await User.find({ active: true }).count();
console.log(`Found ${count} active users`);
// Count with conditions
const adultCount = await User.find({ age: { $gte: 18 } }).count();
Lean Queries
Lean queries return plain JavaScript objects instead of Document instances, providing better performance and lower memory usage.
When to Use Lean Queries
- Read-only operations
- When you don't need Document methods (save, remove, etc.)
- High-frequency queries
- Large result sets
- API responses
Using Lean Queries
// Regular query (returns Document instances)
const users = await User.find({ active: true }).all();
users[0].save(); // Document methods available
// Lean query (returns plain objects)
const users = await User.findLean({ active: true }).all();
// users[0].save(); // Error: plain object, no Document methods
// Lean queries support all chaining methods
const users = await User.findLean({ active: true })
.select(['name', 'email'])
.sort({ createdAt: -1 })
.limit(100)
.all();
// Performance comparison
// Regular query: ~100ms, ~10MB memory
// Lean query: ~85ms, ~6MB memory (15-25% faster, 30-40% less memory)
Query Caching
arango-typed automatically caches compiled queries for better performance:
How It Works
// First call - compiles and caches query structure
await User.find({ name: 'John' }).all();
// Subsequent calls - uses cached query (faster!)
await User.find({ name: 'Jane' }).all(); // Same structure, different value
await User.find({ name: 'Bob' }).all(); // Same structure, different value
// Cache is based on query structure, not values
// Same cache key: same collection + same query structure
Cache Benefits
- ⚡ 20-30% faster for repeated queries
- 💾 Reduced CPU usage (no recompilation)
- 🚀 Significant speedup for high-frequency queries
Raw AQL Queries
For complex queries that can't be expressed with the query builder, you can use raw AQL:
Execute Raw AQL
// Execute raw AQL query
const results = await User.find({}).execute(`
FOR user IN users
FILTER user.active == true
SORT user.createdAt DESC
LIMIT 10
RETURN user
`);
// With bind variables
const results = await User.find({}).execute(`
FOR user IN users
FILTER user.email == @email
RETURN user
`, { email: 'john@example.com' });
Using getDatabase() for Raw Queries
import { getDatabase } from 'arango-typed';
const db = getDatabase();
// Execute raw AQL
const cursor = await db.query(`
FOR user IN users
FILTER user.active == true
FOR post IN posts
FILTER post.userId == user._id
COLLECT userId = user._id WITH COUNT INTO postCount
RETURN { userId, postCount }
`);
const results = await cursor.all();
Subqueries
Use subqueries for complex conditions:
EXISTS Subquery
import { Subquery } from 'arango-typed';
// Find users who have posts
const usersWithPosts = await User.find({}).execute(`
FOR user IN users
FILTER ${Subquery.exists({
collection: 'posts',
where: { userId: 'user._id' }
})}
RETURN user
`);
IN Subquery
// Find users whose role is in a subquery result
const users = await User.find({}).execute(`
FOR user IN users
FILTER user.role IN ${Subquery.create({
collection: 'roles',
where: { active: true },
select: ['name']
})}
RETURN user
`);
Join Queries
Join multiple collections in a single query:
Left Join
import { JoinQuery } from 'arango-typed';
const db = getDatabase();
// Join users with profiles
const joinQuery = new JoinQuery(db, 'users', {
joins: [
{
collection: 'profiles',
options: {
type: 'left',
on: { localField: '_id', foreignField: 'userId' },
as: 'profile'
}
}
],
select: ['name', 'email', 'profile.bio', 'profile.avatar']
});
const results = await joinQuery.execute();
Inner Join
// Inner join - only users with profiles
const joinQuery = new JoinQuery(db, 'users', {
joins: [
{
collection: 'profiles',
options: {
type: 'inner',
on: { localField: '_id', foreignField: 'userId' },
as: 'profile'
}
}
]
});
const results = await joinQuery.execute();
Query with Multi-Tenancy
When multi-tenancy is enabled, queries automatically filter by tenant:
// Model with multi-tenancy enabled
const User = model('users', UserSchema, { tenantEnabled: true });
// This query automatically filters by current tenant
const users = await User.find({ active: true }).all();
// Only returns users for the current tenant
// Tenant filtering is automatic - no need to add tenantId manually
Error Handling
Always handle errors when executing queries:
try {
const users = await User.find({ active: true }).all();
console.log(`Found ${users.length} users`);
} catch (error) {
console.error('Query failed:', error);
// Handle error appropriately
}
// Or with async/await
try {
const user = await User.findOne({ email: 'john@example.com' });
if (!user) {
console.log('User not found');
}
} catch (error) {
console.error('Query error:', error);
}
Query Best Practices
1. Always Use Indexes
// Create indexes on frequently queried fields
UserSchema.index('email');
UserSchema.index('active');
UserSchema.index(['tenantId', 'email']); // Compound index
// Queries will use indexes automatically
const users = await User.find({ email: 'user@example.com' }).all();
2. Use Projections
// ✅ Good: Select only needed fields
const users = await User.find({})
.select(['name', 'email'])
.all();
// ❌ Bad: Fetch all fields
const users = await User.find({}).all();
3. Always Limit Results
// ✅ Good: Always use limit
const users = await User.find({})
.limit(100)
.all();
// ❌ Bad: No limit (could fetch millions!)
const users = await User.find({}).all();
4. Use Lean Queries for Read-Only
// ✅ Good: Use lean for read-only
const users = await User.findLean({ active: true })
.select(['name', 'email'])
.limit(100)
.all();
// ❌ Bad: Use Document instances when not needed
const users = await User.find({ active: true }).all();
5. Filter Early
// ✅ Good: Filter before other operations
const users = await User.find({ active: true })
.sort({ createdAt: -1 })
.limit(10)
.all();
// ❌ Bad: Fetch all, then filter
const allUsers = await User.find({}).all();
const activeUsers = allUsers.filter(u => u.active);
6. Use Appropriate Operators
// ✅ Good: Use $in for multiple values
const users = await User.find({
role: { $in: ['admin', 'editor'] }
}).all();
// ❌ Bad: Multiple OR conditions
const users = await User.find({
$or: [
{ role: 'admin' },
{ role: 'editor' }
]
}).all();
Common Query Patterns
Pattern 1: Search with Filters
async function searchUsers(filters: {
query?: string;
role?: string;
active?: boolean;
minAge?: number;
maxAge?: number;
}) {
const conditions: any = {};
if (filters.query) {
conditions.name = { $regex: filters.query, $options: 'i' };
}
if (filters.role) {
conditions.role = filters.role;
}
if (filters.active !== undefined) {
conditions.active = filters.active;
}
if (filters.minAge || filters.maxAge) {
conditions.age = {};
if (filters.minAge) conditions.age.$gte = filters.minAge;
if (filters.maxAge) conditions.age.$lte = filters.maxAge;
}
return await User.find(conditions)
.sort({ createdAt: -1 })
.limit(50)
.all();
}
Pattern 2: Recent Activity
// Get recently active users
const recentUsers = await User.find({
lastActive: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
})
.sort({ lastActive: -1 })
.limit(20)
.all();
Pattern 3: Top Users
// Get top users by score
const topUsers = await User.find({ active: true })
.sort({ score: -1 })
.limit(10)
.select(['name', 'score', 'avatar'])
.all();
Pattern 4: Date Range Queries
// Users created in last 30 days
const recentUsers = await User.find({
createdAt: {
$gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
}
}).all();
// Users created in specific month
const januaryUsers = await User.find({
createdAt: {
$gte: new Date('2024-01-01'),
$lt: new Date('2024-02-01')
}
}).all();
Performance Tips
- Use Indexes: Create indexes on frequently queried fields
- Use Projections: Select only needed fields
- Always Limit: Never fetch all documents without limit
- Use Lean Queries: For read-only operations
- Filter Early: Apply filters before sorting/limiting
- Use Compound Indexes: For multi-field queries
- Avoid N+1 Queries: Batch fetch related data
- Cache Results: Cache frequently accessed queries
Query Examples
Example 1: User Search
async function searchUsers(searchTerm: string) {
return await User.find({
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ email: { $regex: searchTerm, $options: 'i' } }
],
active: true
})
.select(['name', 'email', 'avatar'])
.sort({ score: -1 })
.limit(20)
.all();
}
Example 2: Filtered List
async function getFilteredUsers(filters: {
role?: string;
minScore?: number;
verified?: boolean;
}) {
const conditions: any = { active: true };
if (filters.role) {
conditions.role = filters.role;
}
if (filters.minScore) {
conditions.score = { $gte: filters.minScore };
}
if (filters.verified !== undefined) {
conditions.verified = filters.verified;
}
return await User.find(conditions)
.sort({ createdAt: -1 })
.limit(50)
.all();
}
Example 3: Complex Query
// Find active premium users with high scores created in last year
const users = await User.find({
active: true,
role: 'premium',
score: { $gte: 1000 },
createdAt: { $gte: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000) },
email: { $exists: true, $regex: /@/ },
$or: [
{ verified: true },
{ posts: { $size: { $gte: 10 } } }
]
})
.select(['name', 'email', 'score', 'avatar'])
.sort({ score: -1, createdAt: -1 })
.limit(100)
.all();