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
| Export | Kind | Role on this page |
|---|---|---|
CountRestDataProvider | class | DefaultRestDataProvider subclass that overrides requestList |
DefaultRestDataProvider | class | The base provider; supplies getList, getOne, getMany and the network service |
RequestTypes | class | GET_LIST for the list call, SEND for the count call |
RequestCountData | class | Both calls use RequestCountData.DATA_ONLY |
CoreBindings | class | Holds DEFAULT_REST_DATA_PROVIDER, the key you bind this class under |
TRequestType | type | Parameter 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.
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():
- The list request, made by the base
requestList:typeas passed bygetList(RequestTypes.GET_LIST), path[resource], query{ ...queryKey, filter }, and thex-request-extraheader when extras were asked for. - 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.
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:
// 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.
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.
import { CountRestDataProvider } from '@venizia/ardor';
export class AppDataProvider extends CountRestDataProvider<'products' | 'categories'> {}Common pitfalls
- The API must expose
GET <resource>/countfor 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 -totalsilently becomes0, and react-admin shows an empty pagination. - The count request only carries
where. Filters that rely onincludeorfieldsto restrict rows are not applied to the count. - Keys from
metaand fromfilter.paramsare copied intoqueryKey, andqueryKeyis spread into both requests. Anything you put there also hits the count endpoint. - The count call uses
RequestTypes.SEND, notRequestTypes.GET_LIST. Any logic that branches on request type seesSENDfor 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_OPTIONSandAPPLICATION_INFO.
Related
- Data provider -
DefaultRestDataProvider,getListfilter building,IDataProvider - Binding keys -
CoreBindings.DEFAULT_REST_DATA_PROVIDER - Binding keys - best practices
- Network -
DefaultNetworkRequestService,RequestTypes,RequestCountData - Custom transport - replacing the network layer instead of the provider
- Application