Filtering
Every filter type from Spatie laravel-query-builder has a typed builder method.
Partial / exact filter
ts
await User.crispy()
.filter('status', 'active') // ?filter[status]=active
.filter('id', [1, 2, 3]) // ?filter[id]=1,2,3 (comma-joined array)
.get();php
// Server
QueryBuilder::for(User::class)
->allowedFilters(
AllowedFilter::partial('status'),
AllowedFilter::exact('id'),
);Dynamic operator filter
Pairs with AllowedFilter::operator($name, FilterOperator::DYNAMIC) on the server.
ts
import { FilterOperator } from '@bir-tan/crisp-oquent';
await User.crispy()
.where('salary', FilterOperator.GREATER_THAN, 3000) // ?filter[salary]=>3000
.where('id', FilterOperator.NOT_EQUAL, 7) // ?filter[id]=!=7
.where('created_at', FilterOperator.LESS_THAN_OR_EQUAL, '2026-01-01')
.get();Available operators: EQUAL, NOT_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL.
Trashed (SoftDeletes)
ts
await User.crispy().withTrashed().get(); // ?filter[trashed]=with
await User.crispy().onlyTrashed().get(); // ?filter[trashed]=onlyNullable filter (Spatie v7.0.1)
ts
await User.crispy().whereNull('deleted_at').get(); // ?filter[deleted_at]=null
await User.crispy().whereNotNull('email').get(); // ?filter[email]=not-nullFilter Groups (Spatie v7.3.0 / #1060)
Server-side AllowedFilter::groupOr / groupAnd. Client just sends the shorthand; OR/AND composition lives on the server.
php
QueryBuilder::for(User::class)
->allowedFilters(
AllowedFilter::groupOr('q', [
AllowedFilter::partial('name'),
AllowedFilter::partial('full_name'),
]),
);ts
await User.crispy().filterGroup('q', 'John').get(); // ?filter[q]=John
// → server: WHERE (name LIKE '%John%' OR full_name LIKE '%John%')Read the Filter Groups deep dive →
Custom array delimiter (Spatie v7.2.0)
Mirror your server-side query-builder.array_value_delimiter config:
ts
import { CrispOquentConfig } from '@bir-tan/crisp-oquent';
CrispOquentConfig.setFilterDelimiter('|');
await User.crispy().filter('id', [1, 2, 3]).get(); // ?filter[id]=1|2|3
// Per-builder override:
await User.crispy().delimiter(';').filter('id', [1, 2]).get();BelongsTo filter
Server-side AllowedFilter::belongsTo('post'). Client emits same filter[post]=...:
ts
await Comment.crispy().filter('post', 42).get(); // ?filter[post]=42Scope filter
Server-side AllowedFilter::scope('starts_before'). Client just sends the value:
ts
await Event.crispy().filter('starts_before', '2026-12-31').get();