Queries
Type-safe query system for building database queries
JustScale provides a type-safe query system through field expressions. Instead of writing raw SQL or using string-based query builders, you access fields through Model.fields.fieldName and chain type-safe operators that match your schema.
Field Expressions
Every model exposes a .fields property that provides type-safe query expressions for each field. These expressions know what operations are valid based on the field type.
import { defineModel, field } from '@justscale/models';
const Product = defineModel('Product', {
name: field.string().max(255),
price: field.decimal(10, 2),
status: field.enum('ProductStatus', ['draft', 'active', 'archived'] as const),
stock: field.int(),
});
// Access field expressions via Model.fields
const { name, price, status, stock } = Product.fields;
// Each field has operators matching its type
const activeProducts = price.gte(10); // Numeric: gte, lte, gt, lt, between
const draftStatus = status.eq('draft'); // Enum: knows valid values!
const searchName = name.ilike('%phone%'); // String: like, ilike, startsWith
const inStock = stock.gt(0); // Int: numeric operatorsBasic Query Operations
Use field expressions with repository methods to build type-safe queries:
import { q } from '@justscale/models';
import { Product } from './product';
import { productRepo } from './repositories';
// Simple equality
const activeProducts = await productRepo.find({
where: Product.fields.status.eq('active'),
});
// Comparison operators
const affordableProducts = await productRepo.find({
where: Product.fields.price.lte(50),
});
// String operations
const searchResults = await productRepo.find({
where: Product.fields.name.ilike('%phone%'),
orderBy: { price: 'asc' },
limit: 20,
});
// Multiple conditions with q.and()
const premiumInStock = await productRepo.find({
where: q.and(
Product.fields.status.eq('active'),
Product.fields.price.gte(100),
Product.fields.stock.gt(0),
),
});Logical Operators
The q namespace provides logical operators to combine conditions:
AND
Use q.and() to require all conditions to match:
import { q } from '@justscale/models';
import { Product } from './product';
// All conditions must be true
const results = await productRepo.find({
where: q.and(
Product.fields.status.eq('active'),
Product.fields.price.between(10, 100),
Product.fields.stock.gt(0),
),
});OR
Use q.or() to match any condition:
import { q } from '@justscale/models';
import { Product } from './product';
// Any condition can be true
const results = await productRepo.find({
where: q.or(
Product.fields.status.eq('draft'),
Product.fields.stock.eq(0),
),
});NOT
Use q.not() to negate a condition:
import { q } from '@justscale/models';
import { Product } from './product';
// Negate a condition
const results = await productRepo.find({
where: q.not(
Product.fields.status.eq('archived'),
),
});Ordering Results
Order results using the orderBy option with field names and direction:
import { Product } from './product';
// Simple ordering
const byPrice = await productRepo.find({
orderBy: { price: 'asc' },
});
// Multiple fields
const sorted = await productRepo.find({
orderBy: [
{ status: 'desc' },
{ price: 'asc' },
],
});
// Fluent syntax
const fluent = await productRepo.find({
orderBy: Product.fields.price.desc(),
});Pagination
Use limit and offset for pagination:
import { Product } from './product';
const page = 2;
const pageSize = 20;
const results = await productRepo.find({
where: Product.fields.status.eq('active'),
orderBy: { createdAt: 'desc' },
limit: pageSize,
offset: (page - 1) * pageSize,
});
// Get total count for pagination
const total = await productRepo.count({
where: Product.fields.status.eq('active'),
});Type Safety
The query system is fully type-safe. TypeScript catches invalid operations at compile time:
import { Product } from './product';
// Type-safe: enum values are checked
Product.fields.status.eq('active'); // ✓ Valid
Product.fields.status.eq('invalid'); // ✗ TypeScript error!
// Type-safe: operators match field types
Product.fields.price.gte(10); // ✓ Valid (numeric field)
Product.fields.status.gte('active'); // ✗ TypeScript error! (enum has no gte)
Product.fields.name.between(1, 10); // ✗ TypeScript error! (string has no between)