DataFn

Filters

Filter records with comparison, set, string, and logical operators.

Filters are the primary mechanism for narrowing query results. A filter is a JSON object where keys are field names and values describe the conditions records must satisfy.

Shorthand Syntax

For simple cases, DFQL supports shorthand filter values:

// Scalar value becomes $eq
{ completed: false }
// Equivalent to: { completed: { $eq: false } }

// Array value becomes $in
{ status: ["active", "pending"] }
// Equivalent to: { status: { $in: ["active", "pending"] } }

Operator Normalization

Operators can be written with or without the $ prefix. Non-prefixed operators are automatically remapped:

// These are equivalent:
{ priority: { gte: 3 } }
{ priority: { $gte: 3 } }

The full remap table:

InputNormalized
eq$eq
ne$ne
gt$gt
gte$gte
lt$lt
lte$lte
in$in
not_in$nin
like$like
ilike$ilike
not_like$not_like
not_ilike$not_ilike
is_null$is_null
is_not_null$is_not_null
is_empty$is_empty
is_not_empty$is_not_empty
before$before
after$after
between$between
not_between$not_between

You can normalize filters programmatically:

import { normalizeFilterOps } from "@datafn/core";

const normalized = normalizeFilterOps({ priority: { gte: 3 } });
// { priority: { $gte: 3 } }

Comparison Operators

OperatorDescriptionExample
$eqEqual to{ status: { $eq: "active" } }
$neNot equal to{ status: { $ne: "archived" } }
$gtGreater than{ priority: { $gt: 3 } }
$gteGreater than or equal{ priority: { $gte: 3 } }
$ltLess than{ priority: { $lt: 5 } }
$lteLess than or equal{ priority: { $lte: 5 } }
{
  resource: "todos",
  version: 1,
  filters: {
    priority: { $gte: 3, $lte: 8 },
  },
}

Multiple operators on the same field are ANDed together. The example above matches records where 3 <= priority <= 8.

Set Operators

OperatorDescriptionExample
$inValue is in array{ status: { $in: ["active", "pending"] } }
$ninValue is not in array{ status: { $nin: ["archived", "deleted"] } }
{
  resource: "users",
  version: 1,
  filters: {
    role: { $in: ["admin", "moderator"] },
  },
}

String Operators

OperatorDescriptionExample
$containsString contains substring{ title: { $contains: "urgent" } }
$startsWithString starts with prefix{ email: { $startsWith: "admin" } }
$endsWithString ends with suffix{ email: { $endsWith: ".com" } }
$likeSQL LIKE pattern (case-sensitive){ title: { $like: "%shop%" } }
$ilikeSQL LIKE pattern (case-insensitive){ title: { $ilike: "%SHOP%" } }
$not_likeNegated LIKE (case-sensitive){ title: { $not_like: "%draft%" } }
$not_ilikeNegated LIKE (case-insensitive){ title: { $not_ilike: "%DRAFT%" } }

The $like and $ilike operators use SQL LIKE syntax: % matches any sequence of characters, _ matches a single character. In the client-side evaluator, these patterns are converted to regular expressions via likeToRegExp().

{
  resource: "articles",
  version: 1,
  filters: {
    title: { $ilike: "%typescript%" },
    slug: { $startsWith: "2024-" },
  },
}

The $contains operator also works on array fields, checking whether the array includes the specified value:

// Match records where the tags array contains "important"
{ tags: { $contains: "important" } }

Null and Empty Operators

OperatorDescriptionExample
$is_nullValue is null or undefined{ deletedAt: { $is_null: true } }
$is_not_nullValue is not null/undefined{ assignedTo: { $is_not_null: true } }
$is_emptyValue is null, undefined, empty string, or empty array{ description: { $is_empty: true } }
$is_not_emptyOpposite of $is_empty{ description: { $is_not_empty: true } }

These operators accept a boolean value. Pass true to assert the condition, false to negate it:

// Records where deletedAt IS null
{ deletedAt: { $is_null: true } }

// Records where deletedAt IS NOT null (equivalent)
{ deletedAt: { $is_null: false } }

Range Operators

OperatorDescriptionExample
$betweenValue is within [low, high] (inclusive){ age: { $between: [18, 65] } }
$not_betweenValue is outside [low, high]{ age: { $not_between: [18, 65] } }
$beforeValue is strictly less than (alias for date/time semantics){ createdAt: { $before: 1700000000000 } }
$afterValue is strictly greater than{ createdAt: { $after: 1700000000000 } }
{
  resource: "orders",
  version: 1,
  filters: {
    total: { $between: [100, 500] },
    createdAt: { $after: 1700000000000 },
  },
}

$between and $not_between require a two-element array [low, high]. The check is inclusive on both ends.

Logical Operators

Combine multiple filter conditions using $and and $or:

// $and: all conditions must match (this is the default for top-level keys)
{
  filters: {
    $and: [
      { status: "active" },
      { priority: { $gte: 3 } },
    ],
  },
}

// $or: at least one condition must match
{
  filters: {
    $or: [
      { completed: true },
      { priority: { $gte: 5 } },
    ],
  },
}

Logical operators can be nested:

{
  filters: {
    $or: [
      {
        $and: [
          { status: "active" },
          { priority: { $gte: 5 } },
        ],
      },
      { assignedTo: { $is_null: true } },
    ],
  },
}

Filter nesting depth is capped at 10 levels to prevent abuse.

Relation Quantifier Filters (Server-Side)

On the server, filters can target related records using quantifier operators. These are evaluated against the related records resolved through schema-defined relations.

OperatorDescription
$anyAt least one related record matches the sub-filter
$allAll related records match the sub-filter (false if no related records exist)
$noneNo related records match the sub-filter (true if no related records exist)
// Find users who have at least one completed todo
{
  resource: "users",
  version: 1,
  filters: {
    todos: {
      $any: { completed: true },
    },
  },
}

// Find categories where ALL items are in stock
{
  resource: "categories",
  version: 1,
  filters: {
    items: {
      $all: { inStock: true },
    },
  },
}

// Find users with no overdue tasks
{
  resource: "users",
  version: 1,
  filters: {
    tasks: {
      $none: { overdue: true },
    },
  },
}

Dot-Path Filters

Filters can traverse nested object fields using dot notation:

// Filter on a nested object field
{ "address.city": "London" }

// Filter on a relation field (requires resolveRelation)
{ "author.name": "Alice" }

When the first segment of a dot-path does not resolve to a field on the record, the evaluator attempts to resolve it as a relation (if a relation resolver is provided).

Programmatic Filter Evaluation

The evaluateFilter function evaluates a filter against a single record in memory. This is used by the client's offline query engine and the server's in-memory execution path.

import { evaluateFilter } from "@datafn/core";

const record = { id: "1", title: "Test", priority: 3, completed: false };

evaluateFilter(record, { priority: { $gte: 2 }, completed: false });
// true

evaluateFilter(record, { priority: { $gt: 5 } });
// false

For server-side evaluation with relation support, pass a FilterEvalOptions object:

evaluateFilter(record, filters, {
  resource: "todos",
  resolveRelation: (resource, recordId, relationName) => {
    // Return array of related records
    return relatedRecords;
  },
});