Skip to content
Drivers/Ransack (Rails)

Ransack (Rails)

driverransack3/8 capabilities

Import the driver directly so every other driver tree-shakes away.

ransack.ts
import { RANSACK_DRIVER, QubeeStore, QueryBuilder, Paginator } from '@qubeejs/core';
const store = new QubeeStore();
const qb = new QueryBuilder(store, RANSACK_DRIVER.createRequestStrategy('query'));
const paginator = new Paginator(
store,
RANSACK_DRIVER.createResponseStrategy(),
RANSACK_DRIVER.createResponseOptions({}),
);
qb.setResource('articles')
.addFilter('status', 'published')
.addSort('created_at', SortEnum.DESC)
.setLimit(25)
.generateUri();

Produces:

/articles?q[status_eq]=published&q[s][]=created_at+desc&page=1&limit=25
Capability Supported
Filters ✅
Operators ✅
Sort ✅
Select —
Fields —
Includes —
Search —
Embedded —

Ransack filters and sorts; a separate gem paginates, and its headers carry the totals. Both common ones are read without configuration:

Pagy Kaminari + api-pagination
Page-size param limit (the default) per_page
Headers read total-count, total-pages, current-page, page-limit, link total, per-page, link
Last page total-pages the rel="last" link, else total ÷ page size

Kaminari reads the page size from per_page, so rename the parameter once:

const { builder, paginator } = createQubee({
driver: RANSACK_DRIVER,
request: { limit: 'per_page' },
});

Pagy ignores a client-supplied limit unless the server sets its client_limit option. Kaminari on its own sends no headers at all; pair it with the api-pagination gem. A header renamed on the server is renamed here through response: { total: 'x-total' } reads x-total first.

Conditions and sorts name the model’s own attributes, in snake_case: created_at, not createdAt. Ransack 4 and later search only what the model allowlists in ransackable_attributes (and ransackable_associations, for a condition such as author_name_cont); a condition or sort on any other attribute is ignored.

Ransack has no global search parameter; a combinator attribute name searches several columns in one condition:

builder.addFilterOperator('title_or_body', FilterOperatorEnum.ILIKE, 'rails');
// q[title_or_body_i_cont]=rails

ILIKE maps to _i_cont. CONTAINS maps to _cont, which Ransack 6.0 made case-sensitive on PostgreSQL.

The totals travel in the response headers, so pass them as the second argument:

const response = await fetch(`https://example.com/api${uri}`);
const page = paginator.paginate(await response.json(), response.headers);
page.data; // rows
page.total; // total rows the backend reported
page.lastPage; // final page number