Scopes & ExtendedQueryBuilder
model.extended() returns an ExtendedQueryBuilder<T> — a full QueryBuilder<T> with one additional method: .scope(fn). Every base QB method (where, preload, orderBy, etc.) is available.
ExtendedQueryBuilder — what it inherits
ExtendedQueryBuilder<T> is a superset of QueryBuilder<T>. Use it exactly like model.query() but with .scope() available.
// ExtendedQueryBuilder<T> — returned by model.extended()
// Extends QueryBuilder<T> with one additional method: scope()
//
// This means ALL base QueryBuilder methods are available:
// .where() .orWhere() .whereRaw()
// .whereIn() .whereNotIn() .whereNull() .whereNotNull()
// .whereBetween() .ILike()
// .select() .exclude() .orderBy()
// .limit() .offset() .paginate()
// .join() .leftJoin() .preload()
// .throughRelation() .whereRelated() .relatedTo()
// .get() .first() .getPaginated()
//
// Plus:
// .scope(fn) — add a reusable, lazily-applied query fragment
const users = await User.extended()
.where('active', '=', true) // base QB method
.preload('posts') // base QB method
.scope(q => q.whereNotNull('verifiedAt')) // ExtendedQueryBuilder only
.orderBy('name', 'ASC') // base QB method
.limit(50) // base QB method
.get();Basic scope
// .scope(fn) — reusable, composable query fragment
// Scopes are lazy — applied just before SQL is built
const activeUsers = await User.extended()
.scope(q => q.where('active', '=', true))
.get();Named scope functions
// Define named scope functions for reuse
// scopes.ts
import type { ExtendedQueryBuilder } from 'slintorm';
export const activeScope = (q: ExtendedQueryBuilder<any>) =>
q.where('active', '=', true);
export const adminScope = (q: ExtendedQueryBuilder<any>) =>
q.where('role', '=', 'admin');
export const recentScope = (q: ExtendedQueryBuilder<any>) =>
q.orderBy('createdAt', 'DESC').limit(10);
// Use in queries:
import { activeScope, adminScope } from './scopes';
const activeAdmins = await User.extended()
.scope(activeScope)
.scope(adminScope)
.get();Composing multiple scopes
// Composing multiple scopes
// Each .scope() call adds its conditions — all are AND-ed
const results = await User.extended()
.scope(q => q.where('active', '=', true))
.scope(q => q.where('role', '=', 'admin'))
.scope(q => q.whereNotNull('verifiedAt'))
.scope(q => q.orderBy('name', 'ASC'))
.get();
// WHERE active = 1 AND role = 'admin' AND verifiedAt IS NOT NULL
// ORDER BY name ASCScopes with arguments
// Scopes that accept parameters — return a scope function
const withRole = (role: string) =>
(q: ExtendedQueryBuilder<any>) => q.where('role', '=', role);
const createdAfter = (date: string) =>
(q: ExtendedQueryBuilder<any>) => q.where('createdAt', '>', date);
const users = await User.extended()
.scope(withRole('admin'))
.scope(createdAfter('2024-01-01'))
.orderBy('name', 'ASC')
.get();Scopes alongside preload, join, paginate
Because ExtendedQueryBuilder inherits the full base QB, scopes compose naturally with every other method.
// Scopes work alongside ALL other QB methods
// including preload, join, paginate
const publishedPostsForTeam = await Post.extended()
.scope(q => q.where('published', '=', true))
.scope(q => q.whereNotNull('publishedAt'))
.preload('user')
.preload('comments')
.exclude('user.password')
.orderBy('publishedAt', 'DESC')
.getPaginated(1, 20);
// Scopes are resolved last, but the resulting SQL is identical to
// writing .where() calls directly — no performance differenceLazy application
Scopes don't execute immediately. They're stored and applied just before.buildSql() is called, so you can mix scopes and regular clauses in any order.
// Scopes are lazy — they run just before .buildSql() is called
// This means you can add scopes and other clauses in any order
const q = User.extended();
// Interleave scopes and regular clauses — all resolved at build time
q.scope(q => q.where('active', '=', true));
q.orderBy('name', 'ASC');
q.scope(q => q.whereNotNull('email'));
q.limit(20);
const users = await q.get();
// All conditions applied correctly regardless of declaration orderValidator — standalone class
Validator<T> is the class behind model.validate() andmodel.check(). It's exported directly from slintorm for use outside of a model context — validating arbitrary objects, request bodies, or config.
// Validator<T> — standalone class exported from 'slintorm'
// The same engine used by model.validate() and model.check()
// Use it outside of a model context if needed
import { Validator } from 'slintorm';
const validator = new Validator<{ email: string; age: number }>();
// validate() — throws ValidationError
await validator.validate(
{ email: 'bad', age: 15 },
{
email: { required: true, email: true },
age: { required: true, min: 18 },
}
);
// check() — returns error map or null
const errors = await validator.check(
{ email: 'joe@example.com', age: 25 },
{
email: { required: true, email: true },
age: { required: true, min: 18 },
}
);
// null — all valid