Skip to content

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]=only

Nullable 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-null

Filter 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]=42

Scope filter

Server-side AllowedFilter::scope('starts_before'). Client just sends the value:

ts
await Event.crispy().filter('starts_before', '2026-12-31').get();