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.

product.tsTypeScript
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 operators

Basic Query Operations

Use field expressions with repository methods to build type-safe queries:

queries.tsTypeScript
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:

and-example.tsTypeScript
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:

or-example.tsTypeScript
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:

not-example.tsTypeScript
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:

ordering.tsTypeScript
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:

pagination.tsTypeScript
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:

type-safety.tsTypeScript
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)