SlintORM
Docsnpm

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 ASC

Scopes 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 difference

Lazy 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 order

Validator — 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
PreviousRelation TraversalNextAdvanced Queries