Search documentation

Search all pages, headings and text, in the current package or topic or in all documentation.

Server API

The server-mode types, the column query contract and the resource-mode adapter.

@workspace/active-grid 11.0.0Status unknowncontent differs from the recorded release 11.0.0baseline ce081dc3

The package's own reference

The text below is the package's own API reference, included unchanged. The complete list of exports, generated from the source, is on All exports.

ServerFetchFunction, ServerFetchParams, ServerOptions, PagedResponse, useGridMutation, useGridImperative. These types are mode-agnostic: they describe the fetch contract and the mutation API regardless of where the rows come from. For server-mode data access, the row source is a resource view via resource-mode.md (createResourceServerFetchFn). For tutorial-style how-to see ../guides/server-mode.md.

ServerFetchFunction#

typescript
import {type ServerFetchFunction} from '@workspace/active-grid';
typescript
type ServerFetchFunction<TData> = (  params: ServerFetchParams,) => Promise<PagedResponse<TData>>;

ServerFetchParams#

PropertyTypeDescription
pageIndexnumberZero-based fetch-block index.
fetchSizenumberEngine's fetch-block size (rows requested in this server call). Independent of UI page size — pass through to your Prisma take.
sortingSortingStateArray of {id: string, desc: boolean}.
filtersColumnFiltersStateArray of {id: string, value: FilterValue}.
globalFilterstring | undefinedGlobal search term.
signalAbortSignalAbortSignal for cancelling in-flight requests on rapid filter/sort changes.

ServerOptions#

typescript
import {type ServerOptions} from '@workspace/active-grid';

Options for server-mode data fetching, passed via the serverOptions prop on ActiveGrid.

PropertyTypeDefaultDescription
cacheSizenumber10Maximum number of blocks to keep in memory.
prefetchbooleantruePrefetch the next block when approaching a block boundary.
blockSizenumber200Rows per block. Grid fetches one block and distributes it across multiple visible pages locally. Navigation within a block is instant; only block boundaries trigger a server fetch. The block size lands on ServerFetchParams.fetchSize.
TSX
<ActiveGrid  mode="server"  fetchFn={fetchOrders}  serverOptions={{cacheSize: 10, prefetch: true, blockSize: 200}}/>

Valid ranges#

blockSize and cacheSize are validated at render in server mode. Both must be a finite integer of at least 1; anything else (0, a negative number, NaN, Infinity, a non-integer, a non-number) throws a GridConfigurationError with code GRID_SERVER_OPTIONS_INVALID naming the offending field.

That is deliberate: before this validation existed, every wrong value failed silently and differently. 0 and negatives collapsed pagesPerBlock to 1 (the block optimisation quietly off, one server call per visible page), and NaN propagated into ServerFetchParams.fetchSize, so the server was asked for NaN rows. Neither produced a message.

prefetch is not runtime-validated. Its contract (prefetch !== false) is total, and no boolean prop is runtime-checked anywhere in this package; it is left to the type system rather than growing a new convention for one field.

blockSize versus the UI page size#

They are independent and must stay that way.

ConceptOwnerDefaultMeaning
UI page sizeconfig/defaults10Rows the user sees at once; options [10, 20, 50, 100].
blockSizeserverOptions200Rows fetched per server call.
pagesPerBlockderivedfloor(block/page)How many visible pages one block serves.
fetchSizederivedpagesPerBlock × pageSizeWhat ServerFetchParams.fetchSize carries.

A blockSize smaller than the page size is valid but inert: pagesPerBlock floors to 1, so a block holds exactly one visible page and every page change becomes a server call. Nothing in the UI reveals that, so the grid emits a dev-only warning naming the grid, the block size and the page size, once per combination.

Never derive one from the other in consumer code. A page-size change is a dataset change (it is part of the dataset key and invalidates the block cache); a block-size change is a transport tuning knob.

PagedResponse#

typescript
import {type PagedResponse} from '@workspace/active-grid';
PropertyTypeDescription
dataTData[]Row data for the current page.
totalCountnumberTotal number of rows across all pages.
facetsRecord<string, FacetOption[]> | undefinedServer-calculated filter options per column.
aggregationsRecord<string, number | string> | undefinedServer-calculated aggregation values per column.

FacetOption#

PropertyTypeDescription
labelstringDisplay label.
valuestringFilter value.
countnumberNumber of rows with this value.

Where server-mode rows come from#

active-grid resolves no server-side query and has no server entry point. Server-mode rows come from a resource view in @workspace/active-resources, connected with createResourceServerFetchFn (resource-mode.md). The ownership split between the grid, the adapter and the server, and the conditions for a non-resource exception, are in ../ARCHITECTURE.md.

Which props reach the datasource#

Two groups, and the boundary between them is a contract: a presentation prop must never be able to stop, start or alter a fetch. R-32 was diagnosed for weeks against three props on the wrong side of this line, so it is written down rather than left to be inferred.

Datasource props — changing one can start, stop or change a fetch:

PropEffect
modeServer mode is what makes a datasource exist at all.
fetchFnIts identity matters — see below.
serverOptions.blockSize / .cacheSizeBlock geometry and therefore fetchSize.
serverOptions.retry*Retry behaviour of a failed fetch.
pagination.pageSizeOptions / initialPageSizePage size is part of the dataset key.
serverCapabilitiesDecides which sort/filter/group state may exist, and prunes stale hydrated state before the first fetch.
enablePersistence / enableUrlSync / statePriorityDecide the state the first fetch is made WITH.
pagination.mode: 'infinite'Switches to accumulate-blocks.

Presentation-only props — none of these can reach the datasource:

settings (including settings.showFilterRow), toolbar, headerMenu, contextMenu, rowMenu, loadingDisplay, height, autoSizeOnMount, columnTypes, columnDefaults, and every column.meta field except _serverFilterCapability (which the grid itself stamps from serverCapabilities).

In particular: hiding the filter row — with settings.showFilterRow: false, by omitting meta.filterType, or because no column declares a filter capability — changes visibility only. It does not start or stop a fetch, and it does not change ServerFetchParams.filters.

The initial fetch#

An initial fetch for the block containing the hydrated page, once the grid's initial state has resolved from every external source (URL, localStorage). It carries the hydrated sorting, filters and global search, so a deep-linked filter produces exactly one already-filtered query rather than an unfiltered one followed by a filtered one.

Two consequences worth knowing:

  • A superseded request is cancelled by the request that replaces it, not by an effect cleanup. Paginating to another block while the first request is still open therefore yields the block you navigated to. (ADR-025)
  • A server-mode grid that never fetches looks exactly like one that is still fetching, because isLoading starts true and the default first-load indicator is 'none' (ADR-021). In development the grid therefore warns when its datasource has been idle for a few seconds without a request, naming the grid and the blocking condition. It is a diagnostic: it changes no UI and never retries.

The external query#

The grid owns its own page, sort, column filters and search, and already treats a change in any of those as a new result set. A page can carry a SECOND set of controls outside the grid that narrows the same query server-side: a search box in a page toolbar, a folder picker, a kind filter carried in the URL. The grid cannot see those.

Tell it with serverQueryKey — an opaque string built from the values themselves:

TSX
const serverQueryKey = `${q}|${kind}|${folder ?? ''}`;
const fetchFn = useMemo(  () =>    createResourceServerFetchFn<Row>({      serverAction: getCoreResourceData,      viewKey: 'core.things',      scope: {q, kind, folder},    }),  [serverQueryKey], // ← follow the key, not the object identity);
<ActiveGrid  mode="server"  fetchFn={fetchFn}  serverQueryKey={serverQueryKey}  pagination={{enabled: true}}/>;

A change gives the result set the same treatment the grid's own filters get: the block cache is invalidated, a fetch goes out, pageIndex returns to 0 and the row selection is cleared. There is no second fetch path — the key simply joins the dataset key.

Why the page reset matters on its own. Without it, narrowing from twelve pages to one leaves the reader on page 5 of a result set that no longer has one: an empty body with nothing on screen explaining why.

Why the selection is cleared. A selection is a set of row IDs belonging to the PREVIOUS question. Carried across a scope change, a bulk action would act on rows the reader can no longer see.

Build it from the values, never from a counter. Returning to a previous filter state should be the previous result set, not a third one.

Leave the prop undefined when the grid owns the whole question. The dataset key is then byte-identical to what it was before the prop existed, so no existing grid changes behaviour.

fetchFn identity is the older, weaker signal#

Before serverQueryKey the only signal was the identity of fetchFn: a new function invalidates the cache and refetches. That still works and is still what a memoised fetchFn should follow, but it does two things serverQueryKey does not — it does not reset the page and it does not clear the selection, and it is easy to get wrong in both directions. A fetchFn memoised on [] while its scope changes leaves the grid showing the previous answer, with no error and no warning; a fetchFn re-created on every render invalidates the cache on every render. Prefer the explicit key, and memoise fetchFn on that same key.

Route query keys can collide with enableUrlSync#

enableUrlSync claims exactly five query keys: page, pageSize, sort, filters, search (ADR-023). It merges rather than replaces, so other parameters survive — but a page that owns a key of the same NAME shares it with the grid and both will write to it.

page and sort are the realistic collisions. If your route already owns either (its own pager, its own sort control, its own grammar such as sort=name-asc), do not enable enableUrlSync on that grid: the grid would read page=2 as its own page index and re-serialise sort in id:asc form. Either the route owns the URL or the grid does.

useGridMutation#

typescript
import {  useGridMutation,  type GridMutationOptions,  type GridMutationResult,  type GridMutationStrategy,  type UseGridMutationResult,} from '@workspace/active-grid';

The required consumer API for mutation-flows on server-mode grids (ADR-009). Runs an async action and applies a named grid-strategy on success, tracking isPending — all without remounting the grid.

TSX
const gridRef = useRef<GridImperativeHandle>(null);const mutation = useGridMutation(gridRef);
const onDelete = async (id: string) => {  const result = await mutation.mutate(id, {    action: (rowId) => softDeleteAction(rowId),    onSuccess: {type: 'removeRow', rowId: id},  });  if (result.success) toast.success('Deleted');  else toast.error(result.error.message);};

Strategies#

StrategyBehavior
{type: "removeRow"}Patches the block cache, decrements totalCount. Preserves session-state.
{type: "addRow"}Inserts a row, increments totalCount.
{type: "refetch"}Invalidates the cache and re-fetches the current page.
{type: "resetSelection"}Clears all row selections (typical after a bulk action).
{type: "none"}No grid-side action; consumer handles reconciliation elsewhere.

Compose strategies#

Pass an array to compose strategies — canonical after a bulk delete:

TSX
const result = await mutation.mutate(formData, {  action: (fd) => deleteRowsAction(fd),  onSuccess: [{type: 'refetch'}, {type: 'resetSelection'}],});

mutation.isPending#

Toggles while the action is in flight — wire it to your dialog's isLoading prop. The return type of mutate is inferred from the action's result-shape, so typed errors survive result.success === false narrowing without casts.

Anti-pattern: <ActiveGrid key={...}>#

Never use <ActiveGrid key={mutationCounter}> to force a refresh — that remounts the whole grid and discards all session state. A dev-time detector emits a warn-once log when the same gridId mounts twice outside the React-Strict-Mode noise window (100 ms).

useGridImperative#

typescript
import {useGridImperative} from '@workspace/active-grid';

Typed wrapper that returns stable {refetch, removeRow, addRow, resetSelection} callbacks. The package considers this the package-internal primitive that useGridMutation is built on. Production consumer-code must go through useGridMutation — three exception classes may call the primitive directly:

  1. Library-internal tests inside @workspace/active-grid itself.
  2. Demo / showcase pages that exercise the primitive directly without a preceding async action.
  3. Pure UI commands tied to a button (e.g. a "Deselect" outside a mutation flow).

Client-mode mutations#

In mode="client", gridRef.removeRow/addRow are no-ops with a dev-time warning; the host mutates its own data. Pattern: ../guides/server-mode.md § Client-mode mutations.

Decimal serialization#

Prisma's Decimal type (used for @db.Decimal(p, s) columns) is a runtime Decimal.js-instance, not a primitive. It does not cross the Next.js Server-Action boundary cleanly — the React serializer rejects it with "Only plain objects can be passed".

@workspace/active-grid exports serializeDecimals as a re-export from @workspace/active-utils/serialization:

TypeScript
import { serializeDecimals } from "@workspace/active-grid";
// In a server-action:const rows = await dbCore.invoice.findMany({ where: { ... } });return { success: true, data: { rows: serializeDecimals(rows), total } };

What it does#

serializeDecimals(value) walks the entire value graph (objects, arrays, nested types) and replaces every Prisma Decimal-instance with its number equivalent via .toNumber(). Primitives, Date, RegExp, and plain objects pass through unchanged.

Input field typeOutput type
Decimal (Prisma)number
Decimal | nullnumber | null
DateDate (unchanged)
string | number | boolunchanged
Nested object / arrayrecursively walked

When to call it#

Always at the server-action output boundary when the response payload contains Prisma Decimal-fields. Resource views in @workspace/active-resources apply the serializer as part of the data-action pipeline, so resource-backed grids need no extra call. For custom server-actions that return rows with Decimal fields, call it explicitly:

TypeScript
"use server";export async function getInvoices() {  const invoices = await dbCore.invoice.findMany({ ... });  return { success: true, data: serializeDecimals(invoices) };}

Consumer-side type#

After serializeDecimals runs, the type from Prisma (Decimal) is in practice number on the client. TypeScript does not narrow this — the Prisma-typed value still has Decimal in its type. Two options at the client-side:

TypeScript
// Option A: cast at the consumer-boundary (single place)const rows = result.data as InvoiceWithNumberDecimal[];
// Option B: Zod-parse if you have an explicit response-schemaconst rows = invoiceResponseSchema.parse(result.data);

Use option A when the server action already owns this validation.

Note on decimal.js#

decimal.js is a transitive dependency of @prisma/client, not a direct dependency of @workspace/active-utils. Do not import from decimal.js in consumer code — runtime resolution is fragile across worker boundaries. The serializeDecimals helper duck-types via .toNumber() to avoid the instanceof brittleness.

Per-column declarative markers describing which runtime capabilities (sort / filter / globalFilter / groupBy / aggregate) a column supports. Lives on GridColumnDef.server, typed ServerColumnContract.

What the contract is for now#

The contract is client-side only. active-grid no longer resolves server-side queries: there is no /server subpath and no in-package query resolver. Server-mode data access goes through @workspace/active-resources (see resource-mode.md), where the model, permission, select and every where fragment live server-side and never cross the wire.

column.server supplies client-side capability metadata and the grid's hydration path (extractWireContract, called inside use-grid-setup) reads it to prune a stale persisted / URL sort or filter that the column does not declare (TD-3 contract-aware hydration pruning). If a saved view or URL carries a sort on a column whose contract has no sort mapping, that instruction is dropped before the first fetch rather than sent to a server that would reject it. No server-side resolver consumes these mappings. Column enhancement also uses the declared sort/filter mappings to resolve UI capabilities; grouping and toolbar affordances read their corresponding markers. These declarations do not execute a server query or independently authorize a server operation.

For the historical architecture of the sort/filter foundation see ADR-013, the global target R-22, and groupBy + aggregate ADR-017.

Serializable by construction#

column.server values are plain JSON — a tagged union of serializable mapping shapes. Every helper below returns one of these tagged objects. Because the contract is a declaration (not a closure), it survives persistence, URL-sync and any future wire transit unchanged.

ServerColumnContract#

typescript
import {type ServerColumnContract} from '@workspace/active-grid';

The contract-shape attached to column.server. Five sub-keys; all values are serializable.

Sub-keyShapeDeclares
sortfalse | SerializableSortMappingThe column supports server-side sort (survives hydration).
filterfalse | SerializableFilterMappingThe column supports server-side filter (survives hydration).
globalfalse | SerializableGlobalMappingThe column participates in the toolbar single-search input.
groupByfalse | SerializableGroupByMappingThe column supports server-side grouping.
aggregatefalse | SerializableAggregateMappingThe aggregations available for this column in a server groupBy.

Each sub-key is independent — declaring filter does not imply sort. Setting a sub-key to false explicitly marks the capability as absent, so a stale persisted/URL instruction for it is pruned at hydration.

typescript
import {  scalarField,  someByField,  globalField,  aggregateScalar,  type ServerColumnContract,} from '@workspace/active-grid';
const userColumns: GridColumnDef<User>[] = [  {    accessorKey: 'email',    header: 'Email',    server: {      ...scalarField(), // sort + filter on column-id "email"      global: globalField(), // joins the toolbar search    } satisfies ServerColumnContract,  },  {    id: 'roles',    accessorFn: (r) => r.roles.map((x) => x.name).join(', '),    header: 'Roles',    server: {      filter: someByField({        relation: 'userRoles',        path: 'role.name',        filterType: 'select',      }),    },  },  {    accessorKey: 'salary',    header: 'Salary',    server: {      ...scalarField(),      aggregate: aggregateScalar({ops: ['sum', 'avg']}),    },  },];

scalarField — combined sort + filter#

typescript
import {scalarField} from '@workspace/active-grid';

The 80%-case helper: fills both sort and filter for a plain scalar column. Returns Pick<ServerColumnContract, "sort" | "filter"> — spread it onto column.server and add other sub-keys as needed.

SignatureBehavior
scalarField()Uses the column-id as the field path. Deferred resolution when the column-id is dotted (user.email).
scalarField("user.email")Explicit nested path; both sort and filter use this path.
typescript
ch.accessor('email', {  header: t('user.email'),  server: scalarField(),});
ch.accessor('ownerName', {  header: t('entity.owner'),  server: scalarField('owner.displayName'), // nested 1:1 relation});

Sort helpers#

typescript
import {byField, byCount} from '@workspace/active-grid';
HelperReturnsDeclares
byField(path?)SortFieldMappingSort on <path ?? columnId>.
byCount(relation)SortCountMappingSort on the count of a relation (<relation> _count).
typescript
// Sort users by number of assigned roles.ch.accessor('roleCount', {  header: t('user.roleCount'),  server: {sort: byCount('userRoles')},});

Filter helpers#

typescript
import {  fieldFilter,  multiField,  someByField,  everyByField,  noneByField,} from '@workspace/active-grid';
HelperReturnsDescription
fieldFilter(path?, {filterType?})FilterFieldMappingScalar field; if path is omitted, column-id is used. filterType disambiguates when operator is ambiguous (equals text/number/date).
multiField(paths, {filterType?})FilterMultiFieldMappingOR across multiple scalar paths.
someByField({relation, path, filterType?})FilterRelationMappingPasses if at least one related record matches (relation: {some: ...}).
everyByField({relation, path, filterType?})FilterRelationMappingPasses only if every related record matches (relation: {every: ...}).
noneByField({relation, path, filterType?})FilterRelationMappingPasses only if no related record matches (relation: {none: ...}).
typescript
// Multi-field OR — search "name" across firstName + lastName + nickname.ch.accessor('name', {  header: t('user.name'),  server: {    filter: multiField(['firstName', 'lastName', 'nickname']),  },});
// "Users that have any role with name X"ch.accessor('roles', {  header: t('user.roles'),  server: {    filter: someByField({      relation: 'userRoles',      path: 'role.name',      filterType: 'select',    }),  },});

Global-search helpers#

typescript
import {  globalField,  globalSomeByField,  globalEveryByField,  globalNoneByField,} from '@workspace/active-grid';

Mark a column as a target of the toolbar single-search input (<ActiveGrid toolbar={{search: {enabled: true}}}>). Columns without a global-mapping silently sit out the search — fail-closed by design.

HelperReturns
globalField(path?, {operator?})GlobalFieldMapping
globalSomeByField({relation, path, operator?})GlobalRelationMapping
globalEveryByField({relation, path, operator?})GlobalRelationMapping
globalNoneByField({relation, path, operator?})GlobalRelationMapping

Default operator is "contains" (case-insensitive). Override with "startsWith" for prefix-match or "equals" for exact-match. See ../guides/server-mode.md § "Global search" for the UX trade-off discussion around relation-search.

GroupBy helpers#

typescript
import {groupByField, groupByRelation} from '@workspace/active-grid';

Declarative server-side grouping markers. Runtime activation requires enableGrouping, enableServerGrouping and ordered serverGroupBy declarations. Aggregate capability comes from the server; the former serverGroupAggregations prop was removed. See Server grouping.

HelperReturnsBehavior
groupByField(path?)GroupByFieldMappingScalar group-by marker on path ?? column-id. Works for any scalar including 1:1 FK columns.
groupByRelation({relation, path})GroupByRelationMappingReserved — type-shape exists for forward compatibility, but the runtime does not activate it. Use groupByField("<fk>") for 1:1 FK grouping.

Scalar group-by only accepts scalar field-names. One-to-many relation grouping is defined server-side in the resource view rather than declared here: for a 1:1 relation, group on the foreign-key scalar with groupByField('<fkScalar>'); for a one-to-many relation, the resource view defines the grouping.

typescript
ch.accessor('category', {  header: t('order.category'),  server: {    ...scalarField(),    groupBy: groupByField(),  },});

Aggregate helpers#

typescript
import {  aggregateScalar,  aggregateCount,  type AggregateOp,} from '@workspace/active-grid';

Declarative per-column aggregations available in a server group-by.

HelperReturnsBehavior
aggregateScalar({path?, ops})AggregateScalarMappingNumeric aggregation block. ops is a non-empty subset of AggregateOp ("sum" | "avg" | "min" | "max" | "count").
aggregateCount({relation?})AggregateCountMapping_count block. relation: undefined / null → count all rows; relation: "userRoles" → count of that relation.

Note on count: inside aggregateScalar it is the non-null-count on the scalar field. For row-counts or relation-counts use aggregateCount.

Aggregations are always declared explicitly per column: auto-aggregate is semantically dangerous (SUM on a versionNumber column is valid but nonsensical; AVG on a userId: Int is meaningless).

typescript
ch.accessor('salary', {  header: t('employee.salary'),  server: {    ...scalarField(),    aggregate: aggregateScalar({ops: ['sum', 'avg']}),  },});
// Row-count per group, no specific field.ch.accessor('rowCount', {  header: t('group.rowCount'),  server: {aggregate: aggregateCount()},});

Serializable mapping types#

The wire-types behind the helpers. Consumers rarely need to construct these directly — prefer the helpers — but the types are exported for typed metadata-tables and library composition.

typescript
import {  type SerializableSortMapping,  type SortFieldMapping,  type SortCountMapping,  type SerializableFilterMapping,  type FilterFieldMapping,  type FilterMultiFieldMapping,  type FilterRelationMapping,  type SerializableGlobalMapping,  type GlobalFieldMapping,  type GlobalRelationMapping,  type SerializableGroupByMapping,  type GroupByFieldMapping,  type GroupByRelationMapping,  type SerializableAggregateMapping,  type AggregateScalarMapping,  type AggregateCountMapping,  type AggregateOp,} from '@workspace/active-grid';

Each mapping carries a _kind-discriminator (e.g. "sort:field", "filter:relation", "global:field", "groupBy:relation", "aggregate:scalar"). The discriminator is how the grid's hydration pruning identifies each mapping — do not omit or rename it.

The resource adapter is the thin client half of the server-defined resource-view pipeline (the active-resources package). It is the only server-mode data path in active-grid: the browser transmits nothing but the server-defined view key plus the interaction state (filters, sorting, global search, paging). Model, permission, select and every where fragment live server-side in the resource declaration and never cross the wire.

TypeScript
import {  ActiveGrid,  createResourceServerFetchFn,  type CreateResourceServerFetchFnConfig,  type GridServerCapabilities,  type ResourceServerAction,} from '@workspace/active-grid';

createResourceServerFetchFn<TRow>(config)#

Builds a ServerFetchFunction<TRow> on top of a resource data-action (ResourceServerAction, produced by the active-resources data-action factory). It maps the grid's fetch params to the neutral wire-request {viewKey, pageIndex, fetchSize, filters, sorting, globalFilter, scope?, preset?, groupBy?, groupKeys?} and unwraps the response.

CreateResourceServerFetchFnConfig<TRow> fields:

  • serverAction — the resource data-action for the target database.
  • viewKey — the server-defined view key (the only view-selection value sent).
  • scope? — optional client-supplied, server-verified scope (value or (params) => value).
  • preset? — optional server-fixed preset key.
  • transform? — optional row mapper (defaults to identity).
  • onError? — optional error callback with the failed read's message. It fires once per logical fetch, and only when that fetch fails for good: after the grid's retries (serverOptions.retryCount) are spent, where the grid shows its error state. A read fails when the action answers {success: false} (the message is its error.message), when the action rejects or throws (the message is the error's message), and when transform throws on a row. With an onError, a rejection that is not an Error is thrown on as an Error with the original value as its cause and the message Failed to load data. A failed attempt that a retry redeems, or that a newer request supersedes (an aborted request), never calls it. Each failed attempt still throws, so the grid can retry. A callback that itself throws is not contained by the grid.
  • capabilities? — optional dev-only drift check against the grid's serverCapabilities prop; the prop always wins.

The external query: serverQueryKey#

A resource scope is computed per fetch and never re-fetches by itself. When the page owns controls OUTSIDE the grid that feed that scope — a toolbar search box, a folder picker, a URL-carried kind filter — give the grid serverQueryKey, an opaque string built from those values. A change invalidates the cache, refetches, resets pageIndex to 0 and clears the row selection. Full rules and the reasoning: server-mode.md.

GridServerCapabilities#

Server-declared UI capabilities for a resource-backed grid, passed to <ActiveGrid serverCapabilities={...}>. A FULL structural mirror of active-resources' ResourceCapabilities type ({filter, sort, group, aggregate, search, presets}) — pass the resource's deriveCapabilities map straight through, unnarrowed. It is the single source of the grid's interaction surface down to operator granularity: the setup synthesizes the internal per-column contract from it (no per-column server marker), stamps the per-field filter capability on column.meta, drives the filter components' operator lists, filter semantics and nullability, prunes stale persisted/URL sort/filter state (including a filter whose operator is no longer allowed) before the first fetch, and gates the toolbar search-box (search: false suppresses it). A contract-parity test in the consumer workspace keeps this type identical to the server's — a server-side map extension fails typecheck until the mirror follows.

GridServerFilterCapability#

The per-field entry of GridServerCapabilities.filter: {filterType, nullable?, operators}. When stamped on a column (meta._serverFilterCapability, done by the setup — never by hand) it is the operator source for that column's filter. allowedOperators/operatorPreset remain as presentation narrowing and intersect with it: they can only shrink the offered list, never widen it (an explicit entry outside the capability is ignored with a dev-warning). The column's type: stays presentation; if it diverges from the capability filterType the filter is disabled fail-closed with a dev-warning (exception: a 'select' presentation works over any type — the set/array wire-form is validated server-side against the declared type).

GridServerFilterType#

The server-side filter type vocabulary ('text' | 'number' | 'date' | 'boolean' | 'select') — mirror of active-resources' FieldFilterType union, deliberately narrower than the grid's own ColumnFilterType (no 'interval').

GridServerAggregateOp#

The server-side aggregate vocabulary ('sum' | 'avg' | 'min' | 'max' | 'count') — mirror of active-resources' AggregateOp.

This page as Markdown