Skip to content

Documentation · ARDOR

CountRestDataProvider ​

CountRestDataProvider is a drop-in replacement for DefaultRestDataProvider. It changes one thing: how getList obtains total. Instead of reading the total from the list response, it issues a second request to resource/count and takes total from that response's count field.

Use it when the API does not send a total with the list payload (for example, no content-range header) but does expose a count endpoint that accepts a where filter.

Prerequisites ​

You have an ARDOR application with REST_DATA_PROVIDER_OPTIONS and APPLICATION_INFO bound - see Application and Binding keys.

Quick Reference ​

ExportKindRole on this page
CountRestDataProviderclassDefaultRestDataProvider subclass that overrides requestList
DefaultRestDataProviderclassThe base provider; supplies getList, getOne, getMany and the network service
RequestTypesclassGET_LIST for the list call, SEND for the count call
RequestCountDataclassBoth calls use RequestCountData.DATA_ONLY
CoreBindingsclassHolds DEFAULT_REST_DATA_PROVIDER, the key you bind this class under
TRequestTypetypeParameter type of requestList

What it overrides ​

CountRestDataProvider extends DefaultRestDataProvider and overrides only the protected requestList. Everything else - the constructor, getOne, getMany, filter building in getList, getNetworkService() - is inherited unchanged.

ts
class CountRestDataProvider<TResource extends string = string>
  extends DefaultRestDataProvider<TResource> {

  protected override requestList<RecordType extends RaRecord = AnyType>(opts: {
    resource: TResource;
    type: TRequestType;
    queryKey: Record<string, AnyType>;
    filter: Record<string, AnyType>;
    extra?: TExtraRequest;
  }): Promise<{ data: RecordType[]; total: number; meta?: { extra: ... } }>;
  // ...
}

getList in the base class builds filter (with where, order, limit, skip, include, fields) and queryKey (from filter.params and meta), then delegates to requestList. The override receives exactly those arguments, and the list extras the caller asked for in extra.

The two requests ​

The override fires two requests in parallel through the inherited protected request():

  1. The list request, made by the base requestList: type as passed by getList (RequestTypes.GET_LIST), path [resource], query { ...queryKey, filter }, and the x-request-extra header when extras were asked for.
  2. The count request. type: RequestTypes.SEND, path [resource, 'count'], query { ...queryKey, where: filter?.where }.

Each is a GET that builds its own headers. The count request carries no list extras, and it has its own tracing id.

Only filter.where reaches the count endpoint. order, limit, skip, include, and fields are not sent, so the count reflects every record matching the filter, not just the current page.

ts
import { CountRestDataProvider } from '@venizia/ardor';

declare const provider: CountRestDataProvider<'products'>;

export async function loadProducts() {
  const page = await provider.getList({
    resource: 'products',
    params: {
      pagination: { page: 1, perPage: 25 },
      sort: { field: 'id', order: 'ASC' },
      filter: { status: 'active' },
    },
  });

  // GET /products?filter=...            -> page.data
  // GET /products/count?where=...       -> page.total
  return page;
}

Return shape ​

The result is Promise.all over the two responses, mapped to the react-admin getList contract:

ts
// excerpt of the value resolved by requestList
const result = {
  data: listResponse.data,
  total: countResponse.data?.count ?? 0,
};
// ...

total comes from data.count on the count response. If the count endpoint returns nothing, or returns an object without count, total is 0.

Binding it ​

Provide the class under CoreBindings.DEFAULT_REST_DATA_PROVIDER in a @configuration() class, in place of DefaultRestDataProvider. The constructor is inherited, so it still injects CoreBindings.REST_DATA_PROVIDER_OPTIONS and CoreBindings.APPLICATION_INFO from the container - provide the class, not a hand-built instance.

ts
import {
  type BaseArdorApplication as TArdorApplication,
  configuration,
  CoreBindings,
  CountRestDataProvider,
  inject,
  provide,
} from '@venizia/ardor';

@configuration()
export class ShopConfiguration {
  constructor(
    @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private readonly application: TArdorApplication,
  ) {}

  @provide({ key: CoreBindings.DEFAULT_REST_DATA_PROVIDER })
  restDataProvider() {
    return this.application.instantiate(CountRestDataProvider).value(this.application);
  }
}

See Binding keys best practices for the exact @provide shape used by your application setup. The IDataProvider handed to react-admin by the container keeps its positional (resource, params) methods; nothing on the react-admin side changes.

Extending it ​

Because requestList is protected, you can subclass CountRestDataProvider and narrow the resource union or adjust the hook further.

ts
import { CountRestDataProvider } from '@venizia/ardor';

export class AppDataProvider extends CountRestDataProvider<'products' | 'categories'> {}

Common pitfalls ​

  • The API must expose GET <resource>/count for every resource that has a list view, and it must answer with { count: number }. A missing endpoint or a different field name does not throw at this layer - total silently becomes 0, and react-admin shows an empty pagination.
  • The count request only carries where. Filters that rely on include or fields to restrict rows are not applied to the count.
  • Keys from meta and from filter.params are copied into queryKey, and queryKey is spread into both requests. Anything you put there also hits the count endpoint.
  • The count call uses RequestTypes.SEND, not RequestTypes.GET_LIST. Any logic that branches on request type sees SEND for the second call.
  • The count request is made without the list extras, so a route that answers extras by default answers them only on the list.
  • The two requests run in parallel and the result waits for both. If the count endpoint is slow, the list is slow.
  • Do not instantiate the class yourself - it depends on constructor injection of REST_DATA_PROVIDER_OPTIONS and APPLICATION_INFO.