Scopes & default scopes

Read this page in the documentation

Scopes & default scopes Scopes let you name a reusable set of query options — a where filter, an order, an attributes projection, a limit, and so on — and attach them to a model. Instead of repeating the same conditions on every call, you apply a scope by name and let prorm merge it into the query for you. prorm supports two kinds of scope: A default scope, applied automatically to every findAll / findOne / count unless you explicitly opt out. Named scopes, applied on demand with Model.scope('name'). All of the examples below use the same model: A scope definition is a plain object shaped like FindOptions. The supported keys are where, order, attributes, include, limit, and offset. The default scope Whatever you pass as defaultScope is folded into every query on the model. No call site has to know about it. The default scope contributes each option only when your call does not already provide it: Its where keys are merged with the call's where. When the same key appears in both, the value you pass at the call site wins. Its order, limit, and attributes apply only if you did not set them on the call. Named scopes Named scopes are declared under scopes and pulled in with Model.scope(name), which returns a scoped copy of the model. You then run any finder on it. Important: a named scope replaces the default scope. Once you apply a named scope, the default scope's where is no longer added. That is why the inactive scope can return inactive rows even though the default scope filters to status = 'active': To read the default scope explicitly, request it by name: How scopes merge where clauses combine — every applied scope's conditions must hold. Singular options like limit and order do not combine: the last one applied wins, and the call's own options win over every scope. Combining scopes Apply several named scopes at once by passing an array. Their where clauses are merged together (later scopes override earlier ones on conflicting keys): scope() returns a model you can scope again, so the calls also chain fluently — this is equivalent to the array form above: For non-where options in a combined scope, the first value set wins: once an order, limit, or attributes has been contributed by a scope (or by your call), later scopes leave it alone. Merging a scope with a call-site where You can still pass query options to a finder on a scoped model. The scope and your options are merged: Non-overlapping where keys are combined (ANDed together), so the query above matches role = 'admin' AND status = 'active'. When the scope and your call-site where set the same key, the named scope's value takes precedence. addScope() — registering scopes after definition Scopes do not have to be declared up front. addScope(name, definition) registers a new named scope on an already-defined model, and it behaves exactly like a scope declared in define. This is handy when a scope depends on values you only know at runtime, or when a plugin or module wants to extend a model it did not define. You can register scopes before or after any query — each findAll resolves the scopes that exist at call time. Bypassing scopes with unscoped() unscoped() returns a model view with no scopes applied — not the default scope, and not any named scope. Use it when you genuinely need every row. Managing scopes A model exposes a small API for inspecting and editing its scopes at runtime: hasScope(name) reports whether a named scope is registered. getScope(name) returns a single scope definition. A getScope('defaultScope') call returns the default scope. getScopes() (and its alias allScopes()) returns every named scope as a plain object keyed by name. removeScope(name) deletes one named scope and returns true if it existed, false otherwise. clearScopes() removes all named scopes at once. The default scope is left intact. How merging works, in short When a query runs, prorm resolves the active scopes into your FindOptions: 1. If the model view is not unscoped and no named scope is applied, the default scope is merged in. Its where combines with your call-site where (your keys win on conflict); its order, limit, and attributes fill in only what you left unset. 2. If one or more named scopes are applied, the default scope is skipped entirely. Each named scope's where is merged into the query in order (later keys override earlier ones), while order, limit, and attributes keep the first value that gets set. 3. unscoped() skips both steps — nothing is merged. Keeping filters in scopes rather than scattered across call sites means the rule lives in one place, reads clearly, and is easy to change later. Related reading Querying — the options a scope is made of Associations — scopes applied through a relationship