TypeScript SDKDatasets

Filters

Use typed filter helpers in dataset queries and filtered measures.

Filters are structured query objects. They are used by dataset queries and filtered measures.

import {
  between,
  eq,
  gt,
  gte,
  inList,
  isNotNull,
  isNull,
  like,
  lt,
  lte,
  neq,
  notInList,
} from '@hypequery/datasets';

Filter helpers

eq('status', 'completed')
neq('status', 'cancelled')
gt('amount', 100)
gte('amount', 100)
lt('amount', 1000)
lte('amount', 1000)
inList('country', ['US', 'UK', 'CA'])
notInList('status', ['cancelled', 'refunded'])
between('createdAt', '2026-01-01', '2026-01-31')
like('email', '%@example.com')
isNull('churnedAt')
isNotNull('churnedAt')

Each helper returns a filter with a field, operator, and value. isNull and isNotNull take no value: they render IS NULL and IS NOT NULL, and a filter that sets value on them is rejected.

eq('status', 'completed')
// { field: 'status', operator: 'eq', value: 'completed' }

Timestamp values

Timestamp filters take ISO 8601 strings. A date ('2026-02-01') or an offset-free date-time ('2026-02-01T00:00:00', '2026-02-01 00:00:00') is read as local time in the query timezone. A value carrying Z or an offset, such as new Date().toISOString() produces, is an instant and selects the same rows in every query timezone:

gte('createdAt', '2026-02-01T00:00:00Z')      // instant
gte('createdAt', '2026-02-01T00:00:00.000Z')  // instant, fractional seconds kept
gte('createdAt', '2026-02-01')                // local midnight in the query timezone

A between or notIn may mix the two forms; each bound keeps its own meaning. An in list must use one form or the other.

Use filters in dataset queries

await analytics.execute(Orders, {
  dimensions: ['country', 'status'],
  measures: ['revenue', 'orderCount'],
  filters: [
    eq('status', 'completed'),
    between('createdAt', '2026-01-01', '2026-01-31'),
  ],
});

Filter aggregated values with having

filters narrow the rows before they are aggregated. To keep only the groups whose aggregated value passes a condition, such as countries with more than 10,000 in revenue, use having:

await analytics.execute(Orders, {
  dimensions: ['country'],
  measures: ['revenue', 'orderCount'],
  filters: [eq('status', 'completed')],
  having: [
    { measure: 'revenue', operator: 'gt', value: 10_000 },
    { measure: 'orderCount', operator: 'between', value: [2, 50] },
  ],
});
  • Each condition must name a measure the query selects, including derived measures. A condition on an unselected measure is rejected.
  • Values are finite numbers, bound as query parameters. Operators are eq, neq, gt, gte, lt, lte, between (a [min, max] pair), in and notIn (non-empty arrays).
  • Conditions apply after grouping and before ordering and limits. A group whose measure is NULL never matches.
  • A dataset's limits.maxFilters bounds filters and having together.
  • Queries that select window or shift measures do not accept having.

Use filters in measures

Filtered measures are useful when a business state should always be part of the aggregation.

import { dataset, dimension, eq, measure } from '@hypequery/datasets';

const Orders = dataset('orders', {
  source: 'orders',
  dimensions: {
    status: dimension.string(),
  },
  measures: {
    completedRevenue: measure.sum('amount', {
      filters: [eq('status', 'completed')],
    }),
    refundedRevenue: measure.sum('amount', {
      filters: [eq('status', 'refunded')],
    }),
  },
});

Restrict filter operators

By default, filterable dimensions become available as filters. Use the dataset filters map when you want a named filter contract or a restricted operator set.

import { dataset, dimension, measure } from '@hypequery/datasets';

const Orders = dataset('orders', {
  source: 'orders',
  dimensions: {
    status: dimension.string(),
    country: dimension.string(),
  },
  measures: {
    revenue: measure.sum('amount'),
  },
  filters: {
    status: {
      __type: 'filter_definition',
      field: 'status',
      operators: ['eq', 'neq', 'in', 'notIn'],
    },
  },
});

With this contract, status allows only the listed operators, and country is still filterable with the default operators. Declared filters add to the dimensions marked filterable; they do not replace them. TypeScript checks these names and operators in dataset and metric execution, Serve API inputs, and React hooks, including infinite-query hooks. The filter helpers preserve their operator literals, so eq('status', 'completed') works with an eq-only policy.

Use DatasetQueryFor<typeof Orders> or MetricQueryFor<typeof Orders, 'revenue'> when storing query inputs in variables. DatasetFilterNames<typeof Orders> describes the exposed names, and DatasetFilterFor<typeof Orders> describes the field/operator pairs. A declared filter's name is its key in filters, which may differ from its backing dimension. A dimension named directly takes the operators of the filters declared on it, so declaring planFilter on plan with operators: ['eq'] also limits plan to eq.

isNull and isNotNull are opt-in for query filters: a filter allows them only when its operators list names them, as in operators: ['gte', 'isNull', 'isNotNull']. Measure filters and segments can use them without a list.

Every dimension with filterable !== false is filterable, whether or not filters is declared; set filterable: false to remove one, or declare a filter to expose a filterable: false dimension under its own policy. One-hop belongsTo and hasOne filters reach the target's filterable dimensions and the filters it declares under a dimension's name, with the target's operator policy. SQL-backed target dimensions, hasMany, and multi-hop paths remain unavailable.

Keep dataset definitions inferred to retain their literal policies. Broad DatasetInstance or SemanticFiltersDefinition annotations lose that information. Explicit result-row generic calls retain the legacy dynamic-query contract. Filter values and runtime tenant restrictions continue to be validated when the query runs.

Order helpers

asc and desc create structured order clauses.

import { asc, desc } from '@hypequery/datasets';

await analytics.execute(Orders, {
  dimensions: ['country'],
  measures: ['revenue'],
  orderBy: [desc('revenue'), asc('country')],
});

Tenant filters

When runtime tenancy is active, explicit filters on the tenant field are rejected. Use runtime tenant context instead.

await analytics.execute(Orders, {
  measures: ['revenue'],
  filters: [eq('tenantId', 'tenant_123')],
}, {
  runtime: {
    tenant: 'tenant_123',
  },
});
// Error: Cannot filter on tenant field "tenantId" when runtime tenancy enforcement is active.

On this page

Get early access to hypequery cloud.

Cloud hosts the same model you run in your backend, with a managed API and MCP server, versions, and rollback. We're letting teams in a few at a time. Join the waitlist and we'll reach out when your project can go live.

How do you want to build your analytics layer? (Optional)
What features do you most need? (Optional)

Pick as many as you like.

Only your email is required. All other questions are optional.