Tulip Logo IconTulip
Query

Sorting

Define portable sort options and use them across client, server, and table flows.

Sorting definitions map public sort keys to portable sort fields.

Define Sorting

import {
  createSortingParser,
  defineSorting,
  resolveSortingSchema,
  sortOption,
} from "@tulip-systems/query";

export const projectSorting = defineSorting({
  title: sortOption((direction) => ({ field: "title", direction })),
  customerName: sortOption((direction) => ({ field: "customerName", direction })),
  createdAt: sortOption((direction) => ({ field: "createdAt", direction })),
});

export const projectSortsSchema = resolveSortingSchema(projectSorting);
export const projectSortingParser = createSortingParser(projectSorting, {
  fallback: [{ key: "createdAt", direction: "desc" }],
});

The URL/API input shape is an ordered array:

type SortInput = {
  key: string;
  direction: "asc" | "desc";
};

The parsed output uses logical backend fields:

type ParsedSort = {
  field: string;
  direction: "asc" | "desc";
};

Client Hook

Use useSorting(...) anywhere a client component owns sorting state.

import { useSorting } from "@tulip-systems/query/client";

const sorting = useSorting(projectSorting, {
  defaults: [{ key: "createdAt", direction: "desc" }],
});

The hook returns:

{
  input;
  query;
  setQuery;
  defaults;
  constraints;
}

Use sorting.input in query input. It includes defaults and constraints and is the shape expected by schemas from resolveSortingSchema(...).

Server Compilation

Compile parsed sorts at the backend boundary.

import { createDatabaseSortingCompiler } from "@tulip-systems/query/drizzle";

const parsedSorts = projectSortingParser.parse(input.sorts);
const orderBy = createDatabaseSortingCompiler(projectSorting)({
  title: projects.title,
  customerName: customers.name,
  createdAt: projects.createdAt,
}).parse(parsedSorts);

Like filters, sort fields are logical compiler keys. Provider-specific compilers can map them to an external API format instead of SQL.

Table Integration

Pass the sorting controller into table config. The table layer bridges TanStack sorting events back into sorting.setQuery(...).

const sorting = useSorting(projectSorting);

const config = createTableConfig({
  queryData: data,
  columns,
  strategy,
  sorting,
});

Avoid keeping a separate table-owned sorting query state. Dataset sorting is the source of truth.

On this page