Skip to content

Queries ​

Routier queries are fluent and can only be performed through a collection. Build your query by chaining operations and finish with a terminal method to execute.

Quick Reference ​

Terminal Methods (Query Execution) ​

MethodDescriptionExample
toArrayAsync()Get all results as an arrayawait ctx.products.toArrayAsync()
firstAsync()Get first item (throws if none)await ctx.products.firstAsync()
firstOrUndefinedAsync()Get first item or undefinedawait ctx.products.firstOrUndefinedAsync()
someAsync()Check if any items matchawait ctx.products.someAsync(p => p.active)
everyAsync()Check if all items matchawait ctx.products.everyAsync(p => p.active)
countAsync()Count total itemsawait ctx.products.countAsync()
sumAsync(field)Sum numeric fieldawait ctx.products.sumAsync(p => p.price)
minAsync(field)Get minimum valueawait ctx.products.minAsync(p => p.price)
maxAsync(field)Get maximum valueawait ctx.products.maxAsync(p => p.price)
distinctAsync()Get unique valuesawait ctx.products.distinctAsync()
toGroupAsync(selector)Group items by keyawait ctx.products.toGroupAsync(p => p.category)

Query Operations (Chaining) ​

MethodDescriptionExample
where(predicate, params?)Filter resultsctx.products.where(p => p.price > 100)
sort(field)Sort ascendingctx.products.sort(p => p.name)
sortDescending(field)Sort descendingctx.products.sortDescending(p => p.price)
map(selector)Transform/select fieldsctx.products.map(p => ({ name: p.name }))
skip(count) / take(count)Window resultsctx.products.skip(10).take(5)
join(inner, outerKey, innerKey)Return matching tuplesctx.teams.join(s => s.members, t => t.id, m => m.teamId)
leftJoin(...)Keep unmatched left rowsctx.teams.leftJoin(s => s.members, t => t.id, m => m.teamId)
groupJoin(...)Each left row with an array of its matchesctx.teams.groupJoin(s => s.members, t => t.id, m => m.teamId)
nearest(field, vector, count)Rank vector similarityctx.products.nearest(p => p.embedding, query, 10)
search(terms, options?)Ranked full-text searchctx.products.search("copper pipe")
subscribe()Enable live updatesctx.products.subscribe().toArray(callback)
explain()Return { data, explanation } from terminalsctx.products.where(p => p.active).explain()

Major query features ​

  • Joins — inner and left equi-joins, same-store and cross-store joins, tuple operations, scopes, and backend execution.
  • Full-Text Search — ranked search over .searchable() strings.
  • Vector Search — cosine-similarity ordering with s.vector() and .nearest().
  • Reusable Queries — define a typed query with Queryable.compose() and execute it through collection.apply().
  • Explain — see which options ran in the database, which ran in memory, and the statements the plugin executed.

Detailed Examples ​

Getting All Results ​

ts
// Get all products
const allProducts = await ctx.products.toArrayAsync();

Getting Single Items ​

ts
// Get first product (throws if none exist)
const firstProduct = await ctx.products.firstAsync();

// Get first product or undefined if none exist
const firstOrUndefined = await ctx.products.firstOrUndefinedAsync();

Checking Existence ​

ts
// Check if any products exist
const hasProducts = await ctx.products.someAsync();

// Check if all products are in stock
const allInStock = await ctx.products.everyAsync((p) => p.inStock);

Counting Items ​

ts
// Count total products
const totalCount = await ctx.products.countAsync();

// Count products in specific category
const electronicsCount = await ctx.products
  .where((p) => p.category === "electronics")
  .countAsync();

Filtering Data ​

ts
// Simple filtering
const expensiveProducts = await ctx.products
  .where((p) => p.price > 100)
  .toArrayAsync();

// Multiple filters
const activeElectronics = await ctx.products
  .where((p) => p.category === "electronics")
  .where((p) => p.inStock === true)
  .toArrayAsync();

// Parameterized filtering
const productsInRange = await ctx.products
  .where((p, params) => p.price >= params.min && p.price <= params.max, {
    min: 50,
    max: 200,
  })
  .toArrayAsync();

Sorting Results ​

ts
// Sort by price (ascending)
const productsByPrice = await ctx.products.sort((p) => p.price).toArrayAsync();

// Sort by price (descending)
const expensiveFirst = await ctx.products
  .orderByDescending((p) => p.price)
  .toArrayAsync();

// Multiple sort criteria
const sortedProducts = await ctx.products
  .sort((p) => p.category)
  .sort((p) => p.name)
  .toArrayAsync();

Field Selection and Transformation ​

ts
// Select specific fields to reduce data transfer
const productSummaries = await ctx.products
  .map((p) => ({
    id: p.id,
    name: p.name,
    price: p.price,
  }))
  .toArrayAsync();

// Create computed fields on-the-fly
const productsWithTax = await ctx.products
  .map((p) => ({
    id: p.id,
    name: p.name,
    price: p.price,
    priceWithTax: p.price * 1.1,
  }))
  .toArrayAsync();

Pagination ​

ts
// Get first 10 products
const firstPage = await ctx.products.take(10).toArrayAsync();

// Get second page (skip first 10, take next 10)
const secondPage = await ctx.products.skip(10).take(10).toArrayAsync();

// Pagination helper
const pageSize = 10;
const pageNumber = 2; // 0-based
const page = await ctx.products
  .skip(pageSize * pageNumber)
  .take(pageSize)
  .toArrayAsync();

Aggregation Operations ​

ts
// Sum prices of in-stock products
const totalValue = await ctx.products
  .where((p) => p.inStock === true)
  .sumAsync((p) => p.price);

// Get minimum and maximum prices
const minPrice = await ctx.products.minAsync((p) => p.price);
const maxPrice = await ctx.products.maxAsync((p) => p.price);

// Get distinct categories
const categories = await ctx.products.map((p) => p.category).distinctAsync();

Complex Queries ​

ts
// Complex query with multiple operations
const topExpensiveElectronics = await ctx.products
  .where((p) => p.category === "electronics")
  .where((p) => p.inStock === true)
  .orderByDescending((p) => p.price)
  .take(5)
  .map((p) => ({
    name: p.name,
    price: p.price,
    priceWithTax: p.price * 1.1,
  }))
  .toArrayAsync();

Key Concepts ​

Query Execution ​

  • Lazy evaluation: Queries don't execute until you call a terminal method
  • Chaining: You can chain multiple operations together
  • Collection-based: All queries must start with a collection

Performance Tips ​

  • Database filters first: Apply where clauses on database fields before computed fields
  • Limit results: Use take() to limit large result sets
  • Efficient pagination: Use skip() and take() for pagination

Computed Properties ​

When filtering on computed properties (not stored in database), the filter runs in memory:

ts
// Good: Database-backed filter first
const expensiveElectronics = await ctx.products
  .where((p) => p.category === "electronics") // Database filter
  .where((p) => p.isExpensive === true) // Computed filter
  .toArrayAsync();

// Less efficient: Computed filter first
const allExpensive = await ctx.products
  .where((p) => p.isExpensive === true) // Loads all records
  .where((p) => p.category === "electronics") // Then filters
  .toArrayAsync();

Released under the MIT License.