shadimage
Data Table Filters
blockdata-table-schemasource: ok · refreshed

Data Table Schema System

Declarative table definitions with col.* factories, presets, and generators for columns, filters, and sheet fields.

npx shadcn add "https://logs.run/r/data-table-schema.json"
free

Where it goes

data-table

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@tanstack/react-table@^9.2.3@tanstack/react-query@^5.101.4

Registry dependencies

https://data-table.openstatus.dev/r/data-table.jsonhttps://data-table.openstatus.dev/r/data-table-cell.jsonhttps://data-table.openstatus.dev/r/data-table-sheet.jsonhttps://data-table.openstatus.dev/r/data-table-filter-command.json

Files20 · source cached 10/2/2026, 1:52:48 AM

src/lib/table-schema/index.ts
import { col as _col, resolveColumns } from "./col";
import { presets } from "./presets";
import {
  deserializeSchema,
  migrateSchemaJSON,
  serializeSchema,
} from "./serialize";
import { validateSchema } from "./validate";

/**
 * Column builder factories and presets for defining table schemas.
 *
 * **Primitive factories** — choose based on the data type of the column:
 * - `col.string()` — text data (`string`)
 * - `col.number()` — numeric data (`number`)
 * - `col.boolean()` — boolean data (`boolean`)
 * - `col.timestamp()` — date/time data (`Date`)
 * - `col.enum(values)` — string union (`T[number]`)
 * - `col.array(item)` — array of values (`U[]`)
 * - `col.record()` — key-value map (`Record<string, string>`)
 *
 * **Presets** — pre-configured builders for common log table patterns:
 * - `col.presets.logLevel(values)` — severity levels
 * - `col.presets.httpMethod(values)` — HTTP verbs
 * - `col.presets.httpStatus(codes?)` — HTTP status codes
 * - `col.presets.duration(unit?, slider?)` — timing / latency
 * - `col.presets.timestamp()` — sortable timestamp with timerange filter
 * - `col.presets.traceId()` — trace / request ID (code display, not filterable)
 * - `col.presets.pathname()` — URL path with text search
 *
 * @example
 * ```ts
 * import { col, createTableSchema } from "@/lib/table-schema";
 *
 * export const tableSchema = createTableSchema({
 *   level:   col.presets.logLevel(LEVELS).description("Log severity"),
 *   date:    col.presets.timestamp().label("Date").size(200).sheet(),
 *   latency: col.presets.duration("ms").label("Latency").sortable().size(110).sheet(),
 *   status:  col.presets.httpStatus().label("Status").size(60),
 *   method:  col.presets.httpMethod(METHODS).size(69),
 *   host:    col.string().label("Host").size(125).sheet(),
 *   headers: col.record().label("Headers").hidden().sheet(),
 * });
 * ```
 */
export const col = { ..._col, presets };
export type {
  ColBuilder,
  ColKind,
  ColRenderers,
  ColumnDescriptor,
  ColumnDescriptorCommon,
  DatePresetDescriptor,
  DisplayConfig,
  DisplayDescriptor,
  FilterDescriptor,
  FilterType,
  InferTableType,
  JsonValue,
  OptionDescriptor,
  Provenance,
  ResolvedColumn,
  ResolvedColumnEntry,
  SchemaJSON,
  SchemaJSONVersion,
  SerializableDisplayConfig,
  SheetConfig,
  SheetDescriptor,
  TableSchemaDefinition,
} from "./types";
export { resolveColumn, resolveColumns } from "./col";
export {
  NAMED_DISPLAY_TYPES,
  applyRenderers,
  isNamedDisplayType,
  type ApplyRenderersOptions,
  type NamedDisplayType,
  type RendererOverrides,
} from "./renderers";
export { generateColumns } from "./generators/columns";
export { generateFilterFields } from "./generators/filter-fields";
export { generateFilterSchema } from "./generators/filter-schema";
export { generateSheetFields } from "./generators/sheet-fields";
export {
  SCHEMA_JSON_VERSION,
  deserializeSchema,
  migrateSchemaJSON,
  serializeSchema,
} from "./serialize";
export {
  DEFAULT_CAPABILITIES,
  MANIFEST_LIMITS,
  TABLE_MANIFEST_VERSION,
  TableManifestError,
  createRowAccessors,
  createTableManifest,
  createTableManifestHandler,
  fetchTableManifest,
  isSafeColumnKey,
  manifestETag,
  parseTableManifest,
  type FetchManifestOptions,
  type ManifestHandlerOptions,
  type ParseManifestOptions,
  type RowAccessors,
} from "./manifest";
export {
  type TableCapabilities,
  type TableChartConfig,
  type TableManifest,
  type TableManifestDefaults,
} from "./manifest";
export {
  manifestToModule,
  pullManifestModule,
  type SnapshotOptions,
} from "./snapshot";

/**
 * Derive defaultColumnVisibility from the schema.
 * Returns { [key]: false } for every column marked with .hidden().
 */
export function getDefaultColumnVisibility(
  schema: import("./types").TableSchemaDefinition,
): Record<string, boolean> {
  const visibility: Record<string, boolean> = {};
  for (const { key, hidden } of resolveColumns(schema)) {
    if (hidden) visibility[key] = false;
  }
  return visibility;
}

/**
 * Create a table schema from a map of col.* builders.
 *
 * The returned object holds the definition and exposes:
 * - `toJSON()` — serializes the schema to a function-free JSON descriptor,
 *   suitable for AI agents, MCP tools, and `JSON.stringify`.
 *
 * Use `createTableSchema.fromJSON(json)` to reconstruct a schema from a
 * JSON descriptor (e.g. one generated by an AI agent). Custom renderers
 * (display.cell, filter.component, sheet.component) are not serialized and
 * must be applied manually on top of the reconstructed builders.
 *
 * @example
 * ```ts
 * export const tableSchema = createTableSchema({
 *   level: col.enum(LEVELS).label("Level").defaultOpen().sheet(),
 *   date: col.timestamp().label("Date").sortable().size(200).sheet(),
 * });
 *
 * export type ColumnSchema = InferTableType<typeof tableSchema.definition>;
 *
 * // Serialize for an AI agent or MCP tool
 * const json = tableSchema.toJSON();
 * // JSON.stringify(tableSchema) also works — toJSON() is called automatically
 *
 * // Reconstruct from AI-generated JSON
 * const schema = createTableSchema.fromJSON(json);
 * ```
 */
export function createTableSchema<
  T extends import("./types").TableSchemaDefinition,
>(definition: T): { definition: T; toJSON(): import("./types").SchemaJSON } {
  validateSchema(definition);
  return {
    definition,
    toJSON() {
      return serializeSchema(definition);
    },
  };
}

/**
 * Reconstruct a schema from a JSON descriptor.
 *
 * Takes `unknown` on purpose: the builder pastes user-typed text in here, and
 * the cache replays JSON written by an older build. `migrateSchemaJSON` brings
 * older versions forward and throws on anything unrecognisable, so the cast
 * that used to sit on untrusted input is gone.
 */
createTableSchema.fromJSON = (
  json: unknown,
): {
  definition: import("./types").TableSchemaDefinition;
  toJSON(): import("./types").SchemaJSON;
} => {
  return createTableSchema(deserializeSchema(migrateSchemaJSON(json)));
};
src/lib/table-schema/col.ts
import type {
  DatePreset,
  Option,
} from "@/components/data-table/types";
import type {
  ColBuilder,
  ColKind,
  ColRenderers,
  ColumnDescriptor,
  ColumnDescriptorCommon,
  DatePresetDescriptor,
  DisplayDescriptor,
  FilterDescriptor,
  FilterType,
  OptionDescriptor,
  Provenance,
  ResolvedColumn,
  ResolvedColumnEntry,
  SheetConfig,
  TableSchemaDefinition,
} from "./types";

// ── Per-kind defaults — the ONE definition ──────────────────────────────────

/**
 * The default cell display for a column kind.
 *
 * This is the single definition. `infer.ts`, `generators/sheet-fields.ts`, and
 * `serialize.ts` each used to carry their own copy, and the third disagreed
 * with the other two.
 */
export function defaultDisplayForKind(kind: ColKind): DisplayDescriptor {
  switch (kind) {
    case "number":
      return { type: "number" };
    case "boolean":
    case "select":
      return { type: "boolean" };
    case "timestamp":
      return { type: "timestamp" };
    case "enum":
    case "array":
      return { type: "badge" };
    case "string":
    case "record":
      return { type: "text" };
  }
}

const MANUAL: Provenance = { source: "manual" };

/**
 * Drop keys whose value is `undefined`.
 *
 * Descriptors have to be canonical: `JSON.stringify` erases an explicit
 * `undefined`, so a descriptor carrying one compares unequal to its own round
 * trip under `toStrictEqual` and under any key-based comparison, while looking
 * identical through `JSON`. `display("number", { unit: undefined })` — which
 * `col.presets.duration()` produces whenever no unit is given — is the case
 * that actually occurs.
 *
 * Exported for `infer.ts`, which builds descriptors outside the builder and so
 * has to hold the same invariant.
 */
export function compact<T extends Record<string, unknown>>(object: T): T {
  const result = {} as Record<string, unknown>;
  for (const [key, value] of Object.entries(object)) {
    if (value !== undefined) result[key] = value;
  }
  return result as T;
}

/**
 * Strip caller-supplied `Option`s down to their serializable fields.
 *
 * `Option.value` is `string | boolean | number | undefined`, but an option with
 * no value cannot be selected, cannot be encoded into filter state, and would
 * vanish from the descriptor on `JSON.stringify` — so it is dropped here,
 * at construction, rather than disappearing later at serialization time.
 */
function toOptionDescriptors(options: readonly Option[]): OptionDescriptor[] {
  const result: OptionDescriptor[] = [];
  for (const option of options) {
    if (option.value === undefined) continue;
    result.push({ label: option.label, value: option.value });
  }
  return result;
}

/** Store preset bounds as ISO instants so they survive JSON. */
function toPresetDescriptor(preset: DatePreset): DatePresetDescriptor {
  return {
    label: preset.label,
    shortcut: preset.shortcut,
    from: preset.from.toISOString(),
    to: preset.to.toISOString(),
  };
}

/** Revive a serialized preset back into the `DatePreset` the UI consumes. */
export function fromPresetDescriptor(
  descriptor: DatePresetDescriptor,
): DatePreset {
  return {
    label: descriptor.label,
    shortcut: descriptor.shortcut,
    from: new Date(descriptor.from),
    to: new Date(descriptor.to),
  };
}

// ── The builder ─────────────────────────────────────────────────────────────

/**
 * Build a `ColBuilder` over an explicit descriptor + renderers pair.
 *
 * Exported because `deserializeSchema` reconstructs builders by handing a
 * descriptor straight back — no per-field replay of the fluent chain.
 */
export function createColBuilder<T, F extends FilterType = FilterType>(
  descriptor: ColumnDescriptor,
  renderers: ColRenderers = {},
): ColBuilder<T, F> {
  // The implementation uses loose parameter types to satisfy all overload
  // signatures at once. TypeScript enforces the constraints at call sites
  // via the ColBuilder<T, F> interface overloads, not here.
  const next = <U, G extends FilterType>(
    patch: Partial<ColumnDescriptor>,
    rendererPatch?: ColRenderers,
  ): ColBuilder<U, G> =>
    createColBuilder<U, G>(
      { ...descriptor, ...patch } as ColumnDescriptor,
      rendererPatch ? compact({ ...renderers, ...rendererPatch }) : renderers,
    );

  /** Return `renderers` without the named closures. */
  const withoutRenderers = (...keys: (keyof ColRenderers)[]): ColRenderers => {
    const result = { ...renderers };
    for (const key of keys) delete result[key];
    return result;
  };

  const builder = {
    get _descriptor() {
      return descriptor;
    },

    get _renderers() {
      return renderers;
    },

    label(text: string): ColBuilder<T, F> {
      return next<T, F>({ label: text });
    },

    description(text: string): ColBuilder<T, F> {
      return next<T, F>({ description: text });
    },

    display(type: string, options?: Record<string, unknown>): ColBuilder<T, F> {
      // "custom" is not a descriptor state — the closure goes to the renderers
      // half and the descriptor keeps whatever serializable display it had, so
      // it is already the correct fallback for the sheet and for toJSON().
      if (type === "custom") {
        const { cell, colorMap } = options as {
          cell: ColRenderers["cell"];
          colorMap?: Record<string, string>;
        };
        return next<T, F>(
          colorMap
            ? {
                display: {
                  ...descriptor.display,
                  colorMap,
                } as DisplayDescriptor,
              }
            : {},
          { cell },
        );
      }
      const display = compact(
        options ? { type, ...options } : { type },
      ) as DisplayDescriptor;
      return next<T, F>({ display });
    },

    filterable(
      type?: string,
      options?: Record<string, unknown>,
    ): ColBuilder<T, F> {
      const filterType = (type ||
        descriptor.filter?.type ||
        "input") as FilterType;
      const existing = descriptor.filter;
      const sameType = filterType === existing?.type;
      const {
        component,
        options: rawOptions,
        presets: rawPresets,
        ...rest
      } = (options ?? {}) as {
        component?: ColRenderers["filterComponent"];
        options?: Option[];
        presets?: DatePreset[];
        min?: number;
        max?: number;
        unit?: string;
      };

      const filter: FilterDescriptor = {
        type: filterType,
        defaultOpen: existing?.defaultOpen ?? false,
        commandDisabled: existing?.commandDisabled ?? false,
        // Preserve auto-derived options when the filter type stays the same
        // and no explicit options are provided by the caller.
        ...(sameType && existing?.options ? { options: existing.options } : {}),
        ...compact(rest),
        ...(rawOptions ? { options: toOptionDescriptors(rawOptions) } : {}),
        ...(rawPresets
          ? { presets: rawPresets.map(toPresetDescriptor) }
          : sameType && existing?.presets
            ? { presets: existing.presets }
            : {}),
      };
      return next<T, F>(
        { filter },
        component ? { filterComponent: component } : undefined,
      );
    },

    notFilterable(): ColBuilder<T, never> {
      return createColBuilder<T, never>(
        { ...descriptor, filter: null },
        // The filter component is meaningless without a filter.
        withoutRenderers("filterComponent"),
      );
    },

    defaultOpen(): ColBuilder<T, F> {
      if (!descriptor.filter) return next<T, F>({});
      return next<T, F>({
        filter: { ...descriptor.filter, defaultOpen: true },
      });
    },

    commandDisabled(): ColBuilder<T, F> {
      if (!descriptor.filter) return next<T, F>({});
      return next<T, F>({
        filter: { ...descriptor.filter, commandDisabled: true },
      });
    },

    hidden(): ColBuilder<T, F> {
      return next<T, F>({ hidden: true });
    },

    hideHeader(): ColBuilder<T, F> {
      return next<T, F>({ hideHeader: true });
    },

    resizable(): ColBuilder<T, F> {
      return next<T, F>({ resizable: true });
    },

    size(px: number): ColBuilder<T, F> {
      return next<T, F>({ size: px });
    },

    minSize(px: number): ColBuilder<T, F> {
      return next<T, F>({ minSize: px });
    },

    /**
     * Click-to-sort on the column header. Pass `false` to switch sorting off
     * again on a preset that enables it (`timestamp`, `latency`, …).
     */
    sortable(enabled = true): ColBuilder<T, F> {
      return next<T, F>({ sortable: enabled });
    },

    optional(): ColBuilder<T | undefined, F> {
      return next<T | undefined, F>({ optional: true });
    },

    sheet(sheetConfig?: SheetConfig): ColBuilder<T, F> {
      const { component, condition, ...sheetDescriptor } = sheetConfig ?? {};
      return next<T, F>(
        { sheet: compact(sheetDescriptor) },
        compact({ sheetComponent: component, sheetCondition: condition }),
      );
    },

    sheetOnly(): ColBuilder<T, never> {
      return createColBuilder<T, never>(
        {
          ...descriptor,
          hidden: true,
          filter: null,
          enableHiding: false,
        },
        withoutRenderers("filterComponent"),
      );
    },
  } as ColBuilder<T, F>;

  return builder;
}

/**
 * Attach provenance to a builder.
 *
 * Internal to this package — `col.presets.*` records which preset produced the
 * column and with what arguments, and `infer.ts` records which heuristic fired.
 * `schemaToTypeScript` reads this instead of pattern-matching the descriptor.
 */
export function withProvenance<T, F extends FilterType>(
  builder: ColBuilder<T, F>,
  provenance: Provenance,
): ColBuilder<T, F> {
  return createColBuilder<T, F>(
    { ...builder._descriptor, provenance },
    builder._renderers,
  );
}

// ── The public read model ───────────────────────────────────────────────────

/**
 * Read a column's complete state — both halves — through one public accessor.
 *
 * Prefer this over reaching into `_descriptor` / `_renderers`, which are
 * `@internal` and may change shape.
 */
// `any` on the filter-type parameter: callers hold builders of every filter
// type at once, and the read model does not depend on which one it is.
export function resolveColumn(
  builder: ColBuilder<unknown, any>,
): ResolvedColumn {
  return { ...builder._descriptor, renderers: builder._renderers };
}

/** `resolveColumn` over a whole definition, preserving key insertion order. */
export function resolveColumns(
  definition: TableSchemaDefinition,
): ResolvedColumnEntry[] {
  return Object.entries(definition).map(([key, builder]) => ({
    key,
    ...resolveColumn(builder),
  }));
}

// ── Factories ───────────────────────────────────────────────────────────────

/** The descriptor every `col.*` factory starts from, before kind-specific bits. */
function base(kind: ColKind): ColumnDescriptorCommon {
  return {
    optional: false,
    label: "",
    display: defaultDisplayForKind(kind),
    hidden: false,
    enableHiding: true,
    hideHeader: false,
    resizable: false,
    sortable: false,
    filter: null,
    sheet: null,
    provenance: MANUAL,
  };
}

/**
 * A string column.
 *
 * - Data type: `string`
 * - Default display: `"text"` (plain text with overflow tooltip)
 * - Default filter: `"input"` (text search)
 * - Allowed filters: `"input"`
 *
 * @example
 * col.string().label("Host").size(125).sheet()
 * col.string().label("Message").notFilterable().optional().hidden()
 */
function string(): ColBuilder<string, "input"> {
  return createColBuilder<string, "input">({
    ...base("string"),
    kind: "string",
    filter: { type: "input", defaultOpen: false, commandDisabled: false },
  });
}

/**
 * A numeric column.
 *
 * - Data type: `number`
 * - Default display: `"number"` (formatted, with optional unit)
 * - Default filter: `"input"` (exact match)
 * - Allowed filters: `"input"` | `"slider"` | `"checkbox"`
 *   - Use `"slider"` for continuous values (latency, file size)
 *   - Use `"checkbox"` for discrete values (HTTP status codes, port numbers)
 *
 * @example
 * col.number().label("Latency").display("number", { unit: "ms" }).filterable("slider", { min: 0, max: 5000 }).sortable()
 * col.number().label("Status").filterable("checkbox", { options: [{ label: "200", value: 200 }] })
 */
function number(): ColBuilder<number, "input" | "slider" | "checkbox"> {
  return createColBuilder<number, "input" | "slider" | "checkbox">({
    ...base("number"),
    kind: "number",
    filter: { type: "input", defaultOpen: false, commandDisabled: false },
  });
}

/**
 * A boolean column.
 *
 * - Data type: `boolean`
 * - Default display: `"boolean"` (checkmark / dash icon)
 * - Default filter: `"checkbox"` with `true` / `false` options pre-wired
 * - Allowed filters: `"checkbox"`
 *
 * @example
 * col.boolean().label("Cache Hit").defaultOpen()
 */
function boolean(): ColBuilder<boolean, "checkbox"> {
  return createColBuilder<boolean, "checkbox">({
    ...base("boolean"),
    kind: "boolean",
    filter: {
      type: "checkbox",
      defaultOpen: false,
      commandDisabled: false,
      options: [
        { label: "true", value: true },
        { label: "false", value: false },
      ],
    },
  });
}

/**
 * A timestamp column.
 *
 * - Data type: `Date`
 * - Default display: `"timestamp"` (relative time, absolute datetime on hover)
 * - Default filter: `"timerange"` (date range picker)
 * - Allowed filters: `"timerange"`
 *
 * @example
 * col.timestamp().label("Date").sortable().commandDisabled().size(200).sheet()
 */
function timestamp(): ColBuilder<Date, "timerange"> {
  return createColBuilder<Date, "timerange">({
    ...base("timestamp"),
    kind: "timestamp",
    filter: { type: "timerange", defaultOpen: false, commandDisabled: true },
  });
}

/**
 * An enum column from a `readonly string[]` union.
 *
 * - Data type: `T[number]` (union of the provided string literals)
 * - Default display: `"badge"` (colored chip)
 * - Default filter: `"checkbox"` with options auto-derived from `values`
 * - Allowed filters: `"checkbox"`
 *
 * Checkbox options are auto-derived from `values` by default. Override them via
 * `.filterable("checkbox", { options: [...] })` when you need custom labels,
 * icons, or a subset of values.
 *
 * @param values - `as const` array of allowed string values
 *
 * @example
 * col.enum(LEVELS).label("Level").defaultOpen()
 * col.enum(LEVELS).label("Level").filterable("checkbox", {
 *   options: LEVELS.map(v => ({ label: v, value: v })), // override labels
 * })
 */
function colEnum<T extends readonly string[]>(
  values: T,
): ColBuilder<T[number], "checkbox"> {
  return createColBuilder<T[number], "checkbox">({
    ...base("enum"),
    kind: "enum",
    enumValues: values,
    filter: {
      type: "checkbox",
      defaultOpen: false,
      commandDisabled: false,
      options: Array.from(values).map((v) => ({ label: v, value: v })),
    },
  });
}

/**
 * An array column, typically used for multi-value enum fields.
 *
 * - Data type: `U[]` where `U` is the item builder's type
 * - Default display: `"badge"` (colored chip per value)
 * - Default filter: `"checkbox"`
 * - Allowed filters: `"checkbox"`
 *
 * Most commonly used as `col.array(col.enum(values))` for tags / regions / labels.
 * `col.array(col.string())` is equally valid — the item type is recorded, so a
 * `string[]` column stays a `string[]` column across a JSON round trip.
 *
 * @param itemBuilder - A `ColBuilder` describing the array item type
 *
 * @example
 * col.array(col.enum(REGIONS)).label("Regions").filterable("checkbox", {
 *   options: REGIONS.map(r => ({ label: r, value: r })),
 * })
 */
function array<U>(
  itemBuilder: ColBuilder<U, any>,
): ColBuilder<U[], "checkbox"> {
  return createColBuilder<U[], "checkbox">({
    ...base("array"),
    kind: "array",
    arrayItem: itemBuilder._descriptor,
    filter: { type: "checkbox", defaultOpen: false, commandDisabled: false },
  });
}

/**
 * A key-value record column.
 *
 * - Data type: `Record<string, string>`
 * - Default display: `"text"`
 * - Not filterable (`F = never`)
 *
 * Use for metadata maps, HTTP headers, environment variables, etc.
 * Typically rendered with a custom sheet component (key-value table / tabs).
 *
 * @example
 * col.record().label("Headers").hidden().sheet({
 *   component: (row) => <KVTabs data={row.headers} />,
 *   className: "flex-col items-start w-full gap-1",
 * })
 */
function record(): ColBuilder<Record<string, string>, never> {
  return createColBuilder<Record<string, string>, never>({
    ...base("record"),
    kind: "record",
  });
}

/**
 * A row-selection checkbox column.
 *
 * - Data type: `boolean` (selected state)
 * - Not filterable (`F = never`)
 * - Not shown in sheet or filters
 * - Renders a checkbox in both the header (select all) and each row
 *
 * @example
 * col.select().label("Select")
 */
function select(): ColBuilder<boolean, never> {
  return createColBuilder<boolean, never>({
    ...base("select"),
    kind: "select",
    label: "Select",
    enableHiding: false,
    size: 40,
  });
}

export const col = {
  string,
  number,
  boolean,
  timestamp,
  enum: colEnum,
  array,
  record,
  select,
};
src/lib/table-schema/presets.ts
import { col, withProvenance } from "./col";
import type { ColBuilder, FilterType, JsonValue } from "./types";

/**
 * Tag a preset-built column so `schemaToTypeScript` can re-emit the exact call
 * instead of reverse-engineering it from the descriptor's shape.
 */
function tag<T, F extends FilterType>(
  preset: string,
  args: readonly (JsonValue | undefined)[],
  builder: ColBuilder<T, F>,
): ColBuilder<T, F> {
  // Trailing omitted args are dropped so the re-emitted call matches what the
  // author wrote: `duration("ms")`, not `duration("ms", undefined)`. An omitted
  // arg in the *middle* becomes `null`, which the emitter prints as `undefined`
  // — `undefined` inside a JSON array does not survive `JSON.stringify`.
  const trimmed = [...args];
  while (trimmed.length > 0 && trimmed[trimmed.length - 1] === undefined) {
    trimmed.pop();
  }
  return withProvenance(builder, {
    source: "preset",
    preset,
    args: trimmed.map((a) => (a === undefined ? null : a)),
  });
}

const DEFAULT_HTTP_STATUS_CODES = [
  200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504,
];

/**
 * Pre-configured column builders for patterns common in log and observability tables.
 *
 * Every preset returns a `ColBuilder` with sensible defaults already applied.
 * All builders remain fully customizable — chain additional methods to override
 * any default (label, size, display, sheet, etc.).
 *
 * @example
 * ```ts
 * const tableSchema = createTableSchema({
 *   level:   col.presets.logLevel(LEVELS).description("Log severity"),
 *   date:    col.presets.timestamp().label("Date").size(200).sheet(),
 *   latency: col.presets.duration("ms").label("Latency").sortable().size(110).sheet(),
 *   status:  col.presets.httpStatus().label("Status").size(60),
 *   method:  col.presets.httpMethod(METHODS).size(69),
 *   path:    col.presets.pathname().label("Path").size(130).sheet(),
 *   traceId: col.presets.traceId().label("Request ID").hidden().sheet(),
 * });
 * ```
 */
export const presets = {
  /**
   * A log severity level column.
   *
   * Defaults: enum + badge display + checkbox filter + `defaultOpen`.
   * Checkbox options are auto-derived from `values` — no need to map them manually.
   *
   * @param values - The allowed severity levels, e.g. `["error", "warn", "info", "debug"] as const`
   *
   * @example
   * ```ts
   * col.presets.logLevel(LEVELS)
   *   .label("Level")
   *   .description("Log severity: error > warn > info > debug")
   *   .size(27)
   * ```
   */
  logLevel<T extends readonly string[]>(
    values: T,
  ): ColBuilder<T[number], "checkbox"> {
    return tag(
      "logLevel",
      [values as readonly string[]],
      col
        .enum(values)
        .label("Level")
        .filterable("checkbox", {
          options: values.map((v) => ({ label: v, value: v })),
        })
        .defaultOpen(),
    );
  },

  /**
   * An HTTP method column.
   *
   * Defaults: enum + plain text display + checkbox filter.
   * Options are auto-derived from `values`.
   *
   * @param values - The allowed HTTP methods, e.g. `["GET", "POST", "PUT", "DELETE"] as const`
   *
   * @example
   * ```ts
   * col.presets.httpMethod(METHODS).size(69)
   * ```
   */
  httpMethod<T extends readonly string[]>(
    values: T,
  ): ColBuilder<T[number], "checkbox"> {
    return tag(
      "httpMethod",
      [values as readonly string[]],
      col
        .enum(values)
        .label("Method")
        .display("text")
        .filterable("checkbox", {
          options: values.map((v) => ({ label: v, value: v })),
        }),
    );
  },

  /**
   * An HTTP status code column.
   *
   * Defaults: number + checkbox filter with a standard set of common status codes.
   * Pass a custom `codes` array to override the defaults.
   *
   * Default codes: 200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504
   *
   * @param codes - Override the default status code options
   *
   * @example
   * ```ts
   * col.presets.httpStatus().label("Status").size(60)
   * col.presets.httpStatus([200, 400, 500]).label("Status") // custom codes
   * ```
   */
  httpStatus(
    codes?: number[],
  ): ColBuilder<number, "input" | "slider" | "checkbox"> {
    return tag(
      "httpStatus",
      [codes as readonly number[] | undefined],
      col
        .number()
        .label("Status")
        .filterable("checkbox", {
          options: (codes ?? DEFAULT_HTTP_STATUS_CODES).map((code) => ({
            label: String(code),
            value: code,
          })),
        }),
    );
  },

  /**
   * A duration / latency / timing column.
   *
   * Defaults: number + formatted number display with unit + slider filter
   * with bounds `{ min: 0, max: 5000 }`.
   *
   * @param unit   - Unit label shown after the value, e.g. `"ms"`, `"s"`, `"µs"`
   * @param slider - Override the slider bounds (default: `{ min: 0, max: 5000 }`)
   *
   * @example
   * ```ts
   * col.presets.duration("ms").label("Latency").sortable().size(110).sheet()
   * col.presets.duration("s", { min: 0, max: 60 }).label("Response time")
   * ```
   */
  duration(
    unit?: string,
    slider?: { min: number; max: number },
  ): ColBuilder<number, "input" | "slider" | "checkbox"> {
    return tag(
      "duration",
      [unit, slider],
      col
        .number()
        .label("Duration")
        .display("number", { unit })
        .filterable("slider", slider ?? { min: 0, max: 5000 }),
    );
  },

  /**
   * A timestamp column.
   *
   * Defaults: Date + relative timestamp display (absolute on hover) +
   * timerange filter + sortable.
   *
   * @example
   * ```ts
   * col.presets.timestamp().label("Date").commandDisabled().size(200).sheet()
   * ```
   */
  timestamp(): ColBuilder<Date, "timerange"> {
    return tag(
      "timestamp",
      [],
      col.timestamp().label("Timestamp").display("timestamp").sortable(),
    );
  },

  /**
   * A trace / span / request ID column.
   *
   * Defaults: string + monospace code display + not filterable.
   * Typically hidden in the table and shown only in the row detail drawer.
   *
   * @example
   * ```ts
   * col.presets.traceId().label("Request ID").hidden().sheet({ skeletonClassName: "w-64" })
   * ```
   */
  traceId(): ColBuilder<string, never> {
    return tag(
      "traceId",
      [],
      col.string().label("Trace ID").display("code").notFilterable(),
    );
  },

  /**
   * A URL pathname column.
   *
   * Defaults: string + plain text display + input (text search) filter.
   *
   * @example
   * ```ts
   * col.presets.pathname().size(130).sheet()
   * ```
   */
  pathname(): ColBuilder<string, "input"> {
    return tag(
      "pathname",
      [],
      col.string().label("Pathname").filterable("input"),
    );
  },

  /**
   * A latency column with heatmap visualization.
   *
   * Defaults: number + heatmap display with unit + slider filter
   * with bounds `{ min: 0, max: 5000 }` + sortable.
   *
   * @param unit   - Unit label shown after the value, e.g. `"ms"`, `"s"`, `"µs"`
   * @param slider - Override the slider bounds (default: `{ min: 0, max: 5000 }`)
   *
   * @example
   * ```ts
   * col.presets.latency("ms").label("Latency").size(110).sheet()
   * col.presets.latency("s", { min: 0, max: 60 }).label("Response time")
   * ```
   */
  latency(
    unit?: string,
    slider?: { min: number; max: number },
  ): ColBuilder<number, "input" | "slider" | "checkbox"> {
    const bounds = slider ?? { min: 0, max: 5000 };
    return tag(
      "latency",
      [unit, slider],
      col
        .number()
        .label("Latency")
        .display("heatmap", { unit, min: bounds.min, max: bounds.max })
        .filterable("slider", bounds)
        .sortable(),
    );
  },

  /**
   * A health/score column with gauge visualization.
   *
   * Defaults: number + gauge display (0–100) + sortable.
   *
   * @param range - Override the min/max range (default: `{ min: 0, max: 100 }`)
   *
   * @example
   * ```ts
   * col.presets.health().label("Health")
   * col.presets.health({ min: 0, max: 1000 }).label("Score")
   * ```
   */
  health(range?: {
    min: number;
    max: number;
  }): ColBuilder<number, "input" | "slider" | "checkbox"> {
    const { min = 0, max = 100 } = range ?? {};
    return tag(
      "health",
      [range],
      col.number().label("Health").display("gauge", { min, max }).sortable(),
    );
  },

  /**
   * A progress column with bar visualization.
   *
   * Defaults: number + bar display (0–100) + sortable.
   *
   * @param range - Override the min/max range (default: `{ min: 0, max: 100 }`)
   *
   * @example
   * ```ts
   * col.presets.progress().label("Progress")
   * col.presets.progress({ min: 0, max: 1000 }).label("Completion")
   * ```
   */
  progress(range?: {
    min: number;
    max: number;
  }): ColBuilder<number, "input" | "slider" | "checkbox"> {
    const { min = 0, max = 100 } = range ?? {};
    return tag(
      "progress",
      [range],
      col.number().label("Progress").display("bar", { min, max }).sortable(),
    );
  },
};
src/lib/table-schema/validate.ts
import { resolveColumns } from "./col";
import type { TableSchemaDefinition } from "./types";

/**
 * Validates a table schema definition and throws a descriptive error on the
 * first violation found.
 *
 * Called automatically by `createTableSchema()` — no need to call manually.
 *
 * Catches errors that the TypeScript type system cannot prevent:
 * - Missing label (`.label()` was never called)
 * - Slider `min` greater than `max`
 *
 * These checks run for both the TypeScript-authored path (`createTableSchema({...})`)
 * and the AI-generated path (`createTableSchema.fromJSON(json)`).
 */
export function validateSchema(definition: TableSchemaDefinition): void {
  for (const c of resolveColumns(definition)) {
    const { key } = c;

    // 1. Label is required — col.* factories default to label: ""
    if (!c.label) {
      throw new Error(
        `[createTableSchema] Column "${key}" is missing a label.\n` +
          `  Fix: .label("${key[0]!.toUpperCase()}${key.slice(1)}")`,
      );
    }

    // 2. Number checkbox filter requires explicit options — the number factory
    //    has no value list to auto-derive from, so an empty options list would
    //    render a filter with no checkboxes (a silent no-op in the UI).
    if (
      c.kind === "number" &&
      c.filter?.type === "checkbox" &&
      (!c.filter.options || c.filter.options.length === 0)
    ) {
      throw new Error(
        `[createTableSchema] Column "${key}": checkbox filter on a number column requires explicit options.\n` +
          `  Fix: .filterable("checkbox", { options: [{ label: "200", value: 200 }, ...] })`,
      );
    }

    // 3. Slider bounds must be valid — type system requires { min, max } to be
    //    passed but cannot enforce min < max
    if (c.filter?.type === "slider") {
      const { min, max } = c.filter;
      if (min === undefined || max === undefined) {
        throw new Error(
          `[createTableSchema] Column "${key}": slider filter is missing min/max bounds.\n` +
            `  Fix: .filterable("slider", { min: 0, max: 100 })`,
        );
      }
      if (min > max) {
        throw new Error(
          `[createTableSchema] Column "${key}": slider min (${min}) must be less than max (${max}).\n` +
            `  Fix: swap the values — .filterable("slider", { min: ${max}, max: ${min} })`,
        );
      }
    }

    // 4. A sheet-only column cannot carry a filter. `.sheetOnly()` is the only
    //    chain step that sets `enableHiding: false` on a hideable column, and
    //    it clears the filter, so no builder can produce this combination —
    //    only hand-written or AI-generated JSON can. It has no chain form,
    //    which means `schemaToTypeScript` would have to drop one half of it.
    if (c.enableHiding === false && c.hidden && c.filter !== null) {
      throw new Error(
        `[createTableSchema] Column "${key}": a sheet-only column cannot have a filter.\n` +
          `  Fix: drop the filter (.sheetOnly() implies it), or make the column filterable and hidden instead.`,
      );
    }
  }
}
src/lib/table-schema/serialize.ts
import { createColBuilder, defaultDisplayForKind } from "./col";
import type {
  ColKind,
  ColumnDescriptor,
  DisplayDescriptor,
  FilterDescriptor,
  Provenance,
  SchemaJSON,
  SchemaJSONVersion,
  SheetDescriptor,
  TableSchemaDefinition,
} from "./types";

/** The current `SchemaJSON` version. Bump alongside a `migrateSchemaJSON` step. */
export const SCHEMA_JSON_VERSION: SchemaJSONVersion = 1;

// ── Serialization ───────────────────────────────────────────────────────────
//
// The descriptor IS the serializable state, so serialization is a rename of the
// key and nothing else. There is no projection to keep in sync, which is why
// `unit`, `presets`, and `resizable` can no longer be silently dropped.

export function serializeSchema(definition: TableSchemaDefinition): SchemaJSON {
  return {
    version: SCHEMA_JSON_VERSION,
    columns: Object.entries(definition).map(([key, builder]) => ({
      key,
      ...builder._descriptor,
    })),
  };
}

// ── Deserialization ─────────────────────────────────────────────────────────
//
// Descriptors are stored verbatim, so reconstruction hands the descriptor
// straight back to the builder. The old ~40-branch replay of the fluent chain
// — including the `enableHiding === false && hidden ⇒ .sheetOnly()` inference
// archaeology — is gone.
//
// Limitation, unchanged: renderers (display cell, filter component, sheet
// component/condition) are closures and cannot be serialized. A deserialized
// column falls back to its descriptor's display, which is why the descriptor
// keeps a real display type even when a custom renderer was supplied.

export function deserializeSchema(json: SchemaJSON): TableSchemaDefinition {
  const definition: TableSchemaDefinition = {};
  for (const { key, ...descriptor } of json.columns) {
    definition[key] = createColBuilder(descriptor as ColumnDescriptor, {});
  }
  return definition;
}

// ── Migration ───────────────────────────────────────────────────────────────

const COL_KINDS: readonly ColKind[] = [
  "string",
  "number",
  "boolean",
  "timestamp",
  "enum",
  "array",
  "record",
  "select",
];

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function asKind(value: unknown, key: unknown): ColKind {
  if (COL_KINDS.includes(value as ColKind)) return value as ColKind;
  // Falling back is better than throwing — one bad column should not make a
  // whole saved schema unloadable — but it is a real loss, so say so.
  if (value !== undefined) {
    console.warn(
      `[migrateSchemaJSON] Column ${JSON.stringify(key)} has unknown kind ` +
        `${JSON.stringify(value)}. Falling back to "string".`,
    );
  }
  return "string";
}

/**
 * Normalize an array column's item descriptor.
 *
 * v1 nests a full `ColumnDescriptor`; v0 carried only `{ dataType, enumValues }`
 * under `arrayItemType` — and, for non-enum items, nothing at all, which is
 * exactly how `string[]` columns used to collapse into `string` columns.
 */
function normalizeArrayItem(raw: unknown): ColumnDescriptor {
  const { key: _key, ...item } = normalizeColumn(raw);
  return item as ColumnDescriptor;
}

function isProvenance(value: unknown): value is Provenance {
  if (!isRecord(value)) return false;
  return (
    value.source === "manual" ||
    (value.source === "preset" &&
      typeof value.preset === "string" &&
      Array.isArray(value.args)) ||
    (value.source === "inferred" && typeof value.rule === "string")
  );
}

/**
 * Normalize one column from either version into a complete `ColumnDescriptor`.
 *
 * v1 payloads go through this too rather than being trusted: `fromJSON` accepts
 * user-typed text, so "it says version 1" is not evidence that its columns are
 * well-formed. Every required field is filled or defaulted here, which is what
 * makes the unchecked `as SchemaJSON` cast on untrusted input unnecessary.
 */
function normalizeColumn(raw: unknown): ColumnDescriptor & { key: string } {
  const c = isRecord(raw) ? raw : {};
  // v1 writes `kind`; v0 wrote `dataType`.
  const kind = asKind(c.kind ?? c.dataType, c.key);

  // v0 wrote `{ type: "text" }` for custom displays and omitted `unit`,
  // `presets`, and `resizable` entirely. Nothing can recover those — the
  // migration fills the type-required defaults and stops there.
  const display = isRecord(c.display)
    ? (c.display as DisplayDescriptor)
    : defaultDisplayForKind(kind);

  // Carried through wholesale, like `display` and `sheet`. Whitelisting the
  // optional fields here would reintroduce exactly the hand-maintained
  // projection this split removed from `serializeSchema` — a new
  // `FilterDescriptor` field would be dropped on migration and nothing would
  // say so. Only the three required fields are coerced, because they must exist.
  const filter: FilterDescriptor | null = isRecord(c.filter)
    ? {
        ...(c.filter as Partial<FilterDescriptor>),
        type: (c.filter.type ?? "input") as FilterDescriptor["type"],
        defaultOpen: c.filter.defaultOpen === true,
        commandDisabled: c.filter.commandDisabled === true,
      }
    : null;

  const sheet: SheetDescriptor | null = isRecord(c.sheet)
    ? (c.sheet as SheetDescriptor)
    : null;

  const common = {
    label: typeof c.label === "string" ? c.label : "",
    ...(typeof c.description === "string"
      ? { description: c.description }
      : {}),
    optional: c.optional === true,
    display,
    ...(typeof c.size === "number" ? { size: c.size } : {}),
    ...(typeof c.minSize === "number" ? { minSize: c.minSize } : {}),
    hidden: c.hidden === true,
    enableHiding: c.enableHiding !== false,
    hideHeader: c.hideHeader === true,
    // v0 had no `resizable` at all, so it defaults to false there; v1 writes it.
    resizable: c.resizable === true,
    sortable: c.sortable === true,
    filter,
    sheet,
    provenance: (isProvenance(c.provenance)
      ? c.provenance
      : { source: "manual" }) as Provenance,
  };

  const key = typeof c.key === "string" ? c.key : "";

  if (kind === "enum") {
    return {
      key,
      ...common,
      kind: "enum",
      enumValues: Array.isArray(c.enumValues) ? (c.enumValues as string[]) : [],
    };
  }
  if (kind === "array") {
    return {
      key,
      ...common,
      kind: "array",
      // v1 nests a full descriptor under `arrayItem`; v0 carried only
      // `{ dataType, enumValues }` under `arrayItemType`.
      arrayItem: normalizeArrayItem(c.arrayItem ?? c.arrayItemType),
    };
  }
  return { key, ...common, kind } as ColumnDescriptor & { key: string };
}

/**
 * Bring untrusted schema JSON up to the current version.
 *
 * `SchemaJSON` is persisted (the builder cache) and pasted by hand into the
 * schema editor, so every entry point that accepts it must run through here
 * first. This is the one hand-written conversion the descriptor split
 * deliberately keeps.
 *
 * v0 (no `version` field) → v1: `dataType` → `kind`, `arrayItemType` →
 * recursive `arrayItem`, and the newly-required `resizable`, `enableHiding`,
 * `hideHeader`, and `provenance` get their defaults.
 *
 * @throws if `json` is not an object with a `columns` array.
 */
export function migrateSchemaJSON(json: unknown): SchemaJSON {
  if (!isRecord(json) || !Array.isArray(json.columns)) {
    throw new Error(
      "[migrateSchemaJSON] Expected an object with a `columns` array.",
    );
  }

  if (json.version !== undefined && json.version !== SCHEMA_JSON_VERSION) {
    throw new Error(
      `[migrateSchemaJSON] Unknown schema version ${JSON.stringify(json.version)}. ` +
        `This build understands version ${SCHEMA_JSON_VERSION}.`,
    );
  }

  return {
    version: SCHEMA_JSON_VERSION,
    columns: json.columns.map(normalizeColumn),
  };
}
src/lib/table-schema/manifest.ts
import type { ActionDescriptor } from "@/lib/actions/types";
import { sanitizeActionDescriptors } from "@/lib/actions/validate";
import type { ActionValidationOptions } from "@/lib/actions/validate";
import { migrateSchemaJSON } from "./serialize";
import type { SchemaJSON } from "./types";

/**
 * The table manifest — what an endpoint says about itself.
 *
 * This is the missing half of the headless story. The schema was already
 * serializable, but every consumer built it locally from TypeScript, so
 * "point the table at a URL" was never actually possible. A manifest is that
 * URL's answer: the schema, which column identifies a row, what the server can
 * and cannot compute, and what may be done to a row.
 *
 * It is fetched over the network from a server the app may not own, so
 * `parseTableManifest` treats every field as untrusted and bounded.
 */

/** Bump alongside a migration step in `parseTableManifest`. */
export const TABLE_MANIFEST_VERSION = 1;

/**
 * What the endpoint can compute.
 *
 * The client used to assume all of it. An endpoint that cannot group facets or
 * count matching rows had no way to say so, and the UI had no way to degrade —
 * it simply rendered empty filters and a blank count. Each flag here is a thing
 * the table will stop asking for, or compute on the client instead.
 */
export type TableCapabilities = {
  /** `meta.facets` is populated. When false, facet counts come from loaded rows. */
  facets: boolean;
  /**
   * The columns the server can facet, when it can only do some. Omitted means
   * "every filterable column".
   */
  facetedColumns?: readonly string[];
  /** `meta.totalRowCount` is meaningful. */
  totalRowCount: boolean;
  /** `meta.filterRowCount` is meaningful. */
  filterRowCount: boolean;
  /** `meta.chartData` is populated. When false, the chart slot is not rendered. */
  chart: boolean;
  /** `prevCursor` is honoured — the precondition for live mode. */
  backwardPagination: boolean;
  /** `meta.actions` and the per-row `_actions` stamp are populated. */
  actions: boolean;
};

/**
 * The conservative reading of an endpoint that said nothing.
 *
 * Everything optional is off: a table that renders no chart against a server
 * that has one is a missing feature, while a table that renders a chart against
 * a server that has none is a broken screen.
 */
export const DEFAULT_CAPABILITIES: TableCapabilities = {
  facets: false,
  totalRowCount: false,
  filterRowCount: false,
  chart: false,
  backwardPagination: false,
  actions: false,
};

/**
 * How the timeline chart is drawn.
 *
 * `meta.chartData` is `{ timestamp, [series]: number }[]`, and until now which
 * column it bucketed, which series it carried, and what colour each series took
 * were all decided in app code. A table pointed at an endpoint has none of that,
 * so the endpoint states it.
 */
export type TableChartConfig = {
  /** The timestamp column the buckets are over. Must be a column in the schema. */
  columnKey: string;
  /** The numeric series in each point, in stacking order. */
  series: { key: string; label?: string; color?: string }[];
  /** Bucket width in milliseconds, when the endpoint has a fixed one. */
  intervalMs?: number;
};

export type TableManifestDefaults = {
  sort?: { id: string; desc: boolean };
  size?: number;
  columnVisibility?: Record<string, boolean>;
};

export type TableManifest = {
  version: number;
  schema: SchemaJSON;
  /**
   * The column that identifies a row on the wire.
   *
   * Actions key rows by it and the sheet resolves the open row through it. It
   * used to be a closure written by hand at each call site
   * (`getRowId={(row) => row.uuid}`), which is exactly the kind of thing a
   * pointed-at table cannot supply.
   */
  primaryKey: string;
  /**
   * A human name for a row, as a template over column keys:
   * `"{method} {pathname}"`. Used for accessible labels, never for identity.
   */
  rowLabel?: string;
  capabilities: TableCapabilities;
  /** Present when `capabilities.chart` is true. */
  chart?: TableChartConfig;
  actions?: ActionDescriptor[];
  defaults?: TableManifestDefaults;
};

/**
 * Build a manifest from a table schema.
 *
 * The server side of the contract. `createTableSchema(...)` already produces
 * the `SchemaJSON`; this names the row identity and states what the endpoint
 * can actually compute, which are the two things the schema alone never said.
 */
export function createTableManifest(config: {
  schema: { toJSON(): SchemaJSON } | SchemaJSON;
  primaryKey: string;
  rowLabel?: string;
  capabilities?: Partial<TableCapabilities>;
  chart?: TableChartConfig;
  actions?: ActionDescriptor[];
  defaults?: TableManifestDefaults;
}): TableManifest {
  const schema =
    "toJSON" in config.schema ? config.schema.toJSON() : config.schema;

  if (!schema.columns.some((column) => column.key === config.primaryKey)) {
    // Caught here rather than at the client, which would only be able to blank
    // the table and say the endpoint was wrong.
    throw new TableManifestError(
      `primaryKey ${JSON.stringify(config.primaryKey)} is not a column in the schema`,
    );
  }

  return {
    version: TABLE_MANIFEST_VERSION,
    schema,
    primaryKey: config.primaryKey,
    ...(config.rowLabel ? { rowLabel: config.rowLabel } : {}),
    capabilities: { ...DEFAULT_CAPABILITIES, ...config.capabilities },
    ...(config.chart ? { chart: config.chart } : {}),
    ...(config.actions?.length ? { actions: config.actions } : {}),
    ...(config.defaults ? { defaults: config.defaults } : {}),
  };
}

// ── Bounds ──────────────────────────────────────────────────────────────────

/**
 * Caps on an incoming manifest.
 *
 * A remote schema drives column generation, filter rendering and URL state, so
 * an unbounded one is a way to hang the tab. These are deliberately far above
 * any real table.
 */
export const MANIFEST_LIMITS = {
  maxColumns: 200,
  maxKeyLength: 200,
  maxLabelLength: 500,
  maxActions: 50,
} as const;

/**
 * Path segments that reach an object's prototype rather than its own data.
 *
 * Column keys are server-authored. `definition[key] = builder` with a
 * `"__proto__"` key does not add an own property — it reassigns the object's
 * prototype, so the column silently vanishes from every `Object.keys` consumer
 * with none of the warnings every other rejected input here produces. As a
 * `primaryKey` it is worse: `getRowId` returns `"[object Object]"` for every
 * row, collapsing selection and bulk actions onto a single id.
 */
const RESERVED_SEGMENTS: ReadonlySet<string> = new Set([
  "__proto__",
  "constructor",
  "prototype",
]);

/** True when every segment of a dotted key addresses own data. */
export function isSafeColumnKey(key: string): boolean {
  return key.split(".").every((segment) => !RESERVED_SEGMENTS.has(segment));
}

export class TableManifestError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "TableManifestError";
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function parseCapabilities(value: unknown): TableCapabilities {
  if (!isRecord(value)) return { ...DEFAULT_CAPABILITIES };
  const facetedColumns = Array.isArray(value.facetedColumns)
    ? value.facetedColumns.filter(
        (key): key is string =>
          typeof key === "string" && key.length <= MANIFEST_LIMITS.maxKeyLength,
      )
    : undefined;
  return {
    facets: value.facets === true,
    ...(facetedColumns ? { facetedColumns } : {}),
    totalRowCount: value.totalRowCount === true,
    filterRowCount: value.filterRowCount === true,
    chart: value.chart === true,
    backwardPagination: value.backwardPagination === true,
    actions: value.actions === true,
  };
}

function parseDefaults(
  value: unknown,
  keys: ReadonlySet<string>,
): TableManifestDefaults | undefined {
  if (!isRecord(value)) return undefined;
  const defaults: TableManifestDefaults = {};

  if (isRecord(value.sort) && typeof value.sort.id === "string") {
    // A default sort on a column that does not exist would be written straight
    // into the URL and then fail to resolve against the table.
    if (keys.has(value.sort.id)) {
      defaults.sort = { id: value.sort.id, desc: value.sort.desc === true };
    }
  }

  if (
    typeof value.size === "number" &&
    Number.isInteger(value.size) &&
    value.size > 0
  ) {
    defaults.size = value.size;
  }

  if (isRecord(value.columnVisibility)) {
    const visibility: Record<string, boolean> = {};
    for (const [key, visible] of Object.entries(value.columnVisibility)) {
      if (keys.has(key) && typeof visible === "boolean") {
        visibility[key] = visible;
      }
    }
    if (Object.keys(visibility).length > 0) {
      defaults.columnVisibility = visibility;
    }
  }

  return Object.keys(defaults).length > 0 ? defaults : undefined;
}

function parseChart(
  value: unknown,
  keys: ReadonlySet<string>,
  warn: (message: string) => void,
): TableChartConfig | undefined {
  if (!isRecord(value)) return undefined;

  if (typeof value.columnKey !== "string" || !keys.has(value.columnKey)) {
    // Without a column to bucket over there is nothing to draw, and a chart
    // pinned to a column that no longer exists is worse than no chart.
    warn("Dropped the chart config: `columnKey` names no column in the schema");
    return undefined;
  }

  const series = (Array.isArray(value.series) ? value.series : [])
    .filter(
      (item): item is Record<string, unknown> =>
        isRecord(item) &&
        typeof item.key === "string" &&
        item.key.length > 0 &&
        item.key.length <= MANIFEST_LIMITS.maxKeyLength,
    )
    .slice(0, MANIFEST_LIMITS.maxColumns)
    .map((item) => ({
      key: item.key as string,
      ...(typeof item.label === "string" ? { label: item.label } : {}),
      ...(typeof item.color === "string" ? { color: item.color } : {}),
    }));

  if (series.length === 0) {
    warn("Dropped the chart config: it declares no series");
    return undefined;
  }

  return {
    columnKey: value.columnKey,
    series,
    ...(typeof value.intervalMs === "number" && value.intervalMs > 0
      ? { intervalMs: value.intervalMs }
      : {}),
  };
}

export type ParseManifestOptions = {
  /** Passed to the action validator — see `sanitizeActionDescriptors`. */
  actionValidation?: ActionValidationOptions;
  /** Called with anything dropped, so a host can log it. */
  onWarning?: (message: string) => void;
};

/**
 * Validate and normalize an untrusted manifest.
 *
 * Throws only for the failures that leave nothing renderable — a missing
 * schema, no columns, no usable primary key. Everything else is repaired or
 * dropped with a warning, because one bad column should not blank the table.
 */
export function parseTableManifest(
  value: unknown,
  options?: ParseManifestOptions,
): TableManifest {
  const warn = options?.onWarning ?? (() => {});

  if (!isRecord(value)) {
    throw new TableManifestError("Manifest is not an object");
  }

  // `migrateSchemaJSON` already normalizes every descriptor field and brings
  // older versions forward, so this only has to add the bounds it does not. Its
  // own rejection is re-thrown as a `TableManifestError` so that a caller has
  // one error type to catch for "this endpoint did not answer with a manifest".
  let schema: SchemaJSON;
  try {
    schema = migrateSchemaJSON(value.schema);
  } catch (cause) {
    throw new TableManifestError(
      `Manifest schema is not usable: ${cause instanceof Error ? cause.message : String(cause)}`,
      { cause },
    );
  }

  if (schema.columns.length === 0) {
    throw new TableManifestError("Manifest schema has no columns");
  }
  if (schema.columns.length > MANIFEST_LIMITS.maxColumns) {
    throw new TableManifestError(
      `Manifest schema has ${schema.columns.length} columns, over the limit of ${MANIFEST_LIMITS.maxColumns}`,
    );
  }

  const seenKeys = new Set<string>();
  const columns = schema.columns.filter((column) => {
    if (column.key.length === 0) {
      warn("Dropped a column with an empty key");
      return false;
    }
    if (column.key.length > MANIFEST_LIMITS.maxKeyLength) {
      warn(`Dropped column ${JSON.stringify(column.key)}: key too long`);
      return false;
    }
    if (!isSafeColumnKey(column.key)) {
      warn(`Dropped column ${JSON.stringify(column.key)}: reserved key`);
      return false;
    }
    // An empty label is not merely ugly: `createTableSchema.fromJSON` rejects
    // it, which throws inside the component and blanks the whole table.
    if (column.label.length === 0) {
      warn(`Dropped column ${JSON.stringify(column.key)}: empty label`);
      return false;
    }
    if (column.label.length > MANIFEST_LIMITS.maxLabelLength) {
      warn(`Dropped column ${JSON.stringify(column.key)}: label too long`);
      return false;
    }
    // The definition is keyed by column key, so a duplicate would silently
    // overwrite the first with no diagnostic.
    if (seenKeys.has(column.key)) {
      warn(`Dropped column ${JSON.stringify(column.key)}: duplicate key`);
      return false;
    }
    seenKeys.add(column.key);
    return true;
  });

  if (columns.length === 0) {
    throw new TableManifestError("Manifest schema has no usable columns");
  }

  const keys = new Set(columns.map((column) => column.key));

  const primaryKey =
    typeof value.primaryKey === "string" && keys.has(value.primaryKey)
      ? value.primaryKey
      : null;
  if (!primaryKey) {
    throw new TableManifestError(
      "Manifest primaryKey is missing or names a column that does not exist",
    );
  }

  const capabilities = parseCapabilities(value.capabilities);

  const { actions, rejected } = sanitizeActionDescriptors(
    Array.isArray(value.actions)
      ? value.actions.slice(0, MANIFEST_LIMITS.maxActions)
      : undefined,
    options?.actionValidation,
  );
  for (const rejection of rejected) {
    warn(`Dropped action ${rejection.id}: ${rejection.reason}`);
  }

  return {
    version: TABLE_MANIFEST_VERSION,
    schema: { version: schema.version, columns },
    primaryKey,
    ...(typeof value.rowLabel === "string" &&
    value.rowLabel.length <= MANIFEST_LIMITS.maxLabelLength
      ? { rowLabel: value.rowLabel }
      : {}),
    capabilities,
    ...(() => {
      const chart = parseChart(value.chart, keys, warn);
      return chart ? { chart } : {};
    })(),
    ...(actions.length > 0 ? { actions } : {}),
    ...(() => {
      const defaults = parseDefaults(value.defaults, keys);
      return defaults ? { defaults } : {};
    })(),
  };
}

// ── ETag ────────────────────────────────────────────────────────────────────

/**
 * FNV-1a over the serialized manifest.
 *
 * A schema changes on deploy, not per request, so the point of the ETag is to
 * turn the client's revalidation into a 304 rather than a re-download. A
 * non-cryptographic hash is the right tool: this guards cache freshness, not
 * integrity.
 */
export function manifestETag(manifest: TableManifest): string {
  const json = JSON.stringify(manifest);
  let hash = 0x811c9dc5;
  for (let i = 0; i < json.length; i++) {
    hash ^= json.charCodeAt(i);
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return `"${hash.toString(16)}-${json.length.toString(16)}"`;
}

// ── Server ──────────────────────────────────────────────────────────────────

export type ManifestHandlerOptions = {
  /**
   * `Cache-Control` for the response. The default revalidates every time and
   * relies on the ETag for the cheap path, which is the safe default for
   * something that changes on deploy without a version in its URL.
   */
  cacheControl?: string;
  /** Extra response headers — CORS, most likely. */
  headers?: Record<string, string>;
};

/**
 * A `Request → Response` handler serving the manifest, with ETag revalidation.
 *
 * Pass a function rather than a value when the manifest depends on the request
 * — per-tenant columns, or actions that depend on the caller's permissions.
 */
export function createTableManifestHandler(
  manifest: TableManifest | ((request: Request) => TableManifest),
  options?: ManifestHandlerOptions,
) {
  return async function handler(request: Request): Promise<Response> {
    const resolved =
      typeof manifest === "function" ? manifest(request) : manifest;
    const etag = manifestETag(resolved);

    const headers: Record<string, string> = {
      "content-type": "application/json",
      etag,
      "cache-control": options?.cacheControl ?? "no-cache",
      ...options?.headers,
    };

    // `If-None-Match` may carry a list, and a proxy may have weakened the tag.
    const ifNoneMatch = request.headers.get("if-none-match");
    if (ifNoneMatch) {
      const candidates = ifNoneMatch
        .split(",")
        .map((value) => value.trim().replace(/^W\//, ""));
      if (candidates.includes(etag) || candidates.includes("*")) {
        return new Response(null, { status: 304, headers });
      }
    }

    return new Response(JSON.stringify(resolved), { status: 200, headers });
  };
}

// ── Client ──────────────────────────────────────────────────────────────────

export type FetchManifestOptions = ParseManifestOptions & {
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
  credentials?: RequestCredentials;
  fetch?: typeof fetch;
  signal?: AbortSignal;
};

/**
 * Fetch and validate a manifest.
 *
 * Deliberately not wired to React Query here — the manifest is needed *before*
 * the query layer exists, because the URL-state adapter is built from it.
 */
export async function fetchTableManifest(
  url: string,
  options?: FetchManifestOptions,
): Promise<TableManifest> {
  const doFetch = options?.fetch ?? fetch;
  const headers =
    typeof options?.headers === "function"
      ? await options.headers()
      : options?.headers;

  const response = await doFetch(url, {
    headers,
    credentials: options?.credentials,
    signal: options?.signal,
  });

  if (!response.ok) {
    throw new TableManifestError(
      `Could not load the table manifest from ${url}: ${response.status} ${response.statusText}`.trim(),
    );
  }

  let payload: unknown;
  try {
    payload = await response.json();
  } catch (cause) {
    throw new TableManifestError(
      `The table manifest at ${url} is not valid JSON`,
      { cause },
    );
  }

  return parseTableManifest(payload, options);
}

// ── Row accessors ───────────────────────────────────────────────────────────

/** Reads a possibly-dotted key path off a row. */
function readPath(row: unknown, path: string): unknown {
  if (row === null || typeof row !== "object") return undefined;
  if (!isSafeColumnKey(path)) return undefined;
  const record = row as Record<string, unknown>;
  // Rows may carry the dotted key literally — `createDrizzleHandler` projects
  // `"timing.dns"` as a flat property — so the exact key wins before walking.
  if (Object.hasOwn(record, path)) return record[path];
  if (!path.includes(".")) return undefined;
  let current: unknown = row;
  for (const segment of path.split(".")) {
    if (current === null || typeof current !== "object") return undefined;
    // Own data only. `isSafeColumnKey` is a name list and cannot catch
    // `"meta.toString"`, which otherwise resolves an inherited method — the
    // same value on every row, collapsing every row id onto one.
    if (!Object.hasOwn(current, segment)) return undefined;
    current = (current as Record<string, unknown>)[segment];
  }
  return current;
}

export type RowAccessors<TRow> = {
  /** The row's wire identity, from `manifest.primaryKey`. */
  getRowId: (row: TRow) => string;
  /**
   * A human name for the row, from `manifest.rowLabel`. `undefined` when the
   * manifest declared none — callers then fall back to static copy rather than
   * reading an internal id out loud.
   */
  getRowLabel?: (row: TRow) => string;
};

/**
 * Derive `getRowId` / `getRowLabel` from a manifest.
 *
 * These were hand-written closures at every call site
 * (`getRowId={(row) => row.uuid}`), which a table pointed at an endpoint cannot
 * supply. `rowLabel` is a template over column keys — `"{method} {pathname}"` —
 * with unknown keys left as written, so a typo is visible rather than silent.
 */
export function createRowAccessors<TRow = unknown>(
  manifest: Pick<TableManifest, "primaryKey" | "rowLabel">,
): RowAccessors<TRow> {
  const getRowId = (row: TRow): string => {
    const value = readPath(row, manifest.primaryKey);
    // Coerced rather than asserted: the id addresses a row in the DOM and on
    // the wire, and a numeric primary key is perfectly ordinary.
    return value === null || value === undefined ? "" : String(value);
  };

  const template = manifest.rowLabel;
  if (!template) return { getRowId };

  return {
    getRowId,
    getRowLabel: (row: TRow): string =>
      template.replace(/\{([^{}]+)\}/g, (match, key: string) => {
        const value = readPath(row, key.trim());
        return value === null || value === undefined ? match : String(value);
      }),
  };
}
src/lib/table-schema/renderers.ts
import { createColBuilder } from "./col";
import { isSafeColumnKey } from "./manifest";
import type {
  ColRenderers,
  ColumnDescriptor,
  TableSchemaDefinition,
} from "./types";

/**
 * Attaching renderers to a schema that came over the wire.
 *
 * `serialize.ts` is explicit that the four renderers — the display cell, the
 * filter component, the sheet component and its condition — are closures and
 * cannot be serialized. A deserialized column therefore falls back to whatever
 * named display its descriptor carries, which is why the descriptor keeps a
 * real display type even when a custom renderer was originally supplied.
 *
 * The named displays (`badge`, `bar`, `status-code`, `level-indicator`, …)
 * already cover most of what a table needs, and they travel as data. This
 * module is the escape hatch for the rest: an app pointed at a remote schema
 * supplies the handful of columns it wants to draw itself, by key, and keeps
 * every other column's declarative rendering.
 *
 * @example
 * ```ts
 * const { definition } = createTableSchema.fromJSON(manifest.schema);
 * const withRenderers = applyRenderers(definition, {
 *   pathname: { cell: (value) => <PathnameCell value={String(value)} /> },
 *   timing: { sheetComponent: (row) => <TimingPhases row={row} /> },
 * });
 * ```
 */

/** Renderer closures to attach, keyed by column key. */
export type RendererOverrides = Record<string, ColRenderers>;

export type ApplyRenderersOptions = {
  /**
   * Called for an override whose key is not in the schema.
   *
   * Worth listening to: a remote schema can drop a column between deploys, and
   * an override left behind for it is silently dead. The default warns to the
   * console rather than throwing, because one stale override should not blank
   * a table.
   */
  onUnknownKey?: (key: string) => void;
  /**
   * Called for a schema column whose key cannot be used.
   *
   * A separate channel from {@link onUnknownKey} because the two say opposite
   * things: one means "your override names nothing", the other means "the
   * schema's own column is unusable". Reporting a dropped column through the
   * override warning sends the reader looking for a stale override that does
   * not exist.
   */
  onUnusableColumn?: (key: string) => void;
};

function defaultOnUnknownKey(key: string): void {
  console.warn(
    `[applyRenderers] no column ${JSON.stringify(key)} in the schema — ` +
      `the override will not be used. Did the endpoint's schema change?`,
  );
}

function defaultOnUnusableColumn(key: string): void {
  console.warn(
    `[applyRenderers] dropped schema column ${JSON.stringify(key)}: the key ` +
      `reaches an object prototype, so it cannot address a column.`,
  );
}

/**
 * Return a copy of `definition` with the given renderers attached.
 *
 * Non-mutating: builders are immutable, so each overridden column is rebuilt
 * over the same descriptor with the merged renderers. Columns with no override
 * are passed through by reference.
 *
 * Only the renderer keys present in an override are set; passing
 * `{ cell }` leaves an existing `sheetComponent` alone.
 */
export function applyRenderers(
  definition: TableSchemaDefinition,
  overrides: RendererOverrides,
  options?: ApplyRenderersOptions,
): TableSchemaDefinition {
  const onUnknownKey = options?.onUnknownKey ?? defaultOnUnknownKey;
  const onUnusableColumn = options?.onUnusableColumn ?? defaultOnUnusableColumn;
  const keys = Object.keys(overrides);
  if (keys.length === 0) return definition;

  for (const key of keys) {
    // `in` walks the prototype chain, so `toString` would read as a column.
    if (!Object.hasOwn(definition, key)) onUnknownKey(key);
  }

  const result: TableSchemaDefinition = {};
  for (const [key, builder] of Object.entries(definition)) {
    // Skipped before any assignment, not merely excluded from overrides:
    // `result["__proto__"] = builder` invokes the setter and reassigns the
    // result's prototype rather than adding a column, so the column vanishes
    // from every `Object.keys` consumer with no diagnostic. Guarding only the
    // override lookup still ran that assignment on the pass-through path.
    if (!isSafeColumnKey(key)) {
      onUnusableColumn(key);
      continue;
    }
    const override = Object.hasOwn(overrides, key) ? overrides[key] : undefined;
    if (!override) {
      result[key] = builder;
      continue;
    }
    result[key] = createColBuilder(
      builder._descriptor as ColumnDescriptor,
      // The descriptor is untouched: an override changes how a column draws,
      // never what it is. `toJSON()` on the result still round-trips.
      { ...builder._renderers, ...override },
    );
  }
  return result;
}

/**
 * The display types a schema can name without shipping code.
 *
 * Exported so a tool — a schema builder UI, an agent writing a schema, a
 * validator — can enumerate what is available declaratively before reaching
 * for {@link applyRenderers}.
 */
export const NAMED_DISPLAY_TYPES = [
  "text",
  "code",
  "boolean",
  "star",
  "badge",
  "timestamp",
  "number",
  "bar",
  "heatmap",
  "gauge",
  "status-code",
  "level-indicator",
] as const;

export type NamedDisplayType = (typeof NAMED_DISPLAY_TYPES)[number];

/** Is this display type one the renderer can draw from the descriptor alone? */
export function isNamedDisplayType(type: string): type is NamedDisplayType {
  return (NAMED_DISPLAY_TYPES as readonly string[]).includes(type);
}
src/lib/table-schema/snapshot.ts
import { parseTableManifest, type TableManifest } from "./manifest";

/**
 * Freezing a manifest into the app at build time.
 *
 * Fetching the manifest at runtime is one round trip before the table can draw
 * anything. A snapshot removes it: check the endpoint's answer into the repo,
 * pass it as `initialManifest`, and the first paint has a schema while the
 * query revalidates behind it.
 *
 * The trade is explicit rather than hidden. A snapshot is a copy, so it goes
 * stale when the endpoint changes — which is exactly why `useTableManifest`
 * still revalidates instead of trusting it forever, and why the generated file
 * records where and when it came from.
 */

export type SnapshotOptions = {
  /** The URL it came from, recorded in the header comment. */
  source?: string;
  /** Name of the exported const. Defaults to `manifest`. */
  exportName?: string;
  /**
   * Import specifier for the `TableManifest` type. Defaults to the registry
   * path; set it to wherever the schema block landed in the consuming app
   * (`@/lib/table-schema`, most likely).
   */
  typeImport?: string;
  /**
   * Timestamp for the header comment. Injectable so the output is
   * deterministic in tests, and so a build can stamp its own commit time.
   */
  generatedAt?: Date | string;
};

const DEFAULT_TYPE_IMPORT = "@/lib/table-schema";

/** A JS identifier, so the generated module actually parses. */
function assertIdentifier(name: string): void {
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
    throw new Error(
      `exportName ${JSON.stringify(name)} is not a valid identifier`,
    );
  }
}

/**
 * Render a manifest as a TypeScript module.
 *
 * The manifest is re-parsed on the way in: a snapshot is generated from a live
 * endpoint's response, so it goes through the same validation as one consumed
 * at runtime. Freezing an invalid manifest into the repo would only move the
 * failure to a place where nobody is looking for it.
 */
export function manifestToModule(
  manifest: unknown,
  options?: SnapshotOptions,
): string {
  const exportName = options?.exportName ?? "manifest";
  assertIdentifier(exportName);

  const parsed: TableManifest = parseTableManifest(manifest);
  const typeImport = options?.typeImport ?? DEFAULT_TYPE_IMPORT;
  const generatedAt =
    options?.generatedAt instanceof Date
      ? options.generatedAt.toISOString()
      : options?.generatedAt;

  const provenance = [
    " * Generated by `manifestToModule`. Do not edit by hand.",
    options?.source ? ` * Source: ${options.source}` : null,
    generatedAt ? ` * Generated: ${generatedAt}` : null,
    " *",
    " * This is a snapshot. The table still revalidates it at runtime, so an",
    " * endpoint whose schema has moved on will correct itself — but regenerate",
    " * this file when that happens, or every first paint starts from stale",
    " * columns.",
  ].filter((line): line is string => line !== null);

  return [
    "/**",
    ...provenance,
    " */",
    "",
    // Built with `JSON.stringify` rather than an interpolated string literal:
    // it escapes the specifier properly, and it keeps this line from reading as
    // a real import to any tool that scans this file's own imports.
    `import type { TableManifest } from ${JSON.stringify(typeImport)};`,
    "",
    `export const ${exportName}: TableManifest = ${JSON.stringify(parsed, null, 2)};`,
    "",
  ].join("\n");
}

/**
 * Fetch a manifest and render it as a module, ready to write to disk.
 *
 * The whole `dtf pull` step, minus the file write — which is left to the
 * caller so this stays runtime-agnostic and needs no filesystem access.
 *
 * @example
 * ```ts
 * // scripts/pull-manifest.ts
 * import { writeFileSync } from "node:fs";
 * import { pullManifestModule } from "@/lib/table-schema";
 *
 * const url = "https://api.example.com/logs/schema";
 * writeFileSync("src/app/logs/manifest.ts", await pullManifestModule(url));
 * ```
 */
export async function pullManifestModule(
  url: string,
  options?: SnapshotOptions & {
    headers?: HeadersInit;
    fetch?: typeof fetch;
  },
): Promise<string> {
  const doFetch = options?.fetch ?? fetch;
  const response = await doFetch(url, { headers: options?.headers });
  if (!response.ok) {
    throw new Error(
      `Could not pull the manifest from ${url}: ${response.status} ${response.statusText}`.trim(),
    );
  }
  return manifestToModule(await response.json(), {
    source: url,
    generatedAt: new Date(),
    ...options,
  });
}
src/lib/table-schema/infer.ts
import { compact, defaultDisplayForKind } from "./col";
import { SCHEMA_JSON_VERSION } from "./serialize";
import type {
  ColKind,
  ColumnDescriptor,
  DisplayDescriptor,
  FilterDescriptor,
  SchemaJSON,
} from "./types";

/** A descriptor plus the key it will be registered under. */
type InferredColumn = ColumnDescriptor & { key: string };

// Unix ms timestamps are 13-digit numbers (> Sep 2001, < Nov 2286)
const UNIX_MS_MIN = 1_000_000_000_000;
const UNIX_MS_MAX = 9_999_999_999_999;

function isIso8601(value: string): boolean {
  return (
    /^\d{4}-\d{2}-\d{2}(T[\d:.Z+\-]+)?$/.test(value) &&
    !isNaN(Date.parse(value))
  );
}

function isUnixMs(value: number): boolean {
  return (
    Number.isInteger(value) && value >= UNIX_MS_MIN && value <= UNIX_MS_MAX
  );
}

/** Convert camelCase, snake_case, or kebab-case key to a human-readable label. */
function keyToLabel(key: string): string {
  let label = key.replace(/[-_]/g, " ");
  label = label.replace(/([a-z])([A-Z])/g, "$1 $2");
  return label.replace(/\b\w/g, (c) => c.toUpperCase());
}

/**
 * The fields every inferred column shares. `display` comes from the one
 * `defaultDisplayForKind` in `col.ts` — this module used to carry its own copy.
 */
function inferredCommon(
  label: string,
  kind: ColKind,
  filter: FilterDescriptor | null,
  rule: string,
) {
  return {
    label,
    optional: false,
    display: defaultDisplayForKind(kind) as DisplayDescriptor,
    hidden: false,
    enableHiding: true,
    hideHeader: false,
    resizable: false,
    sortable: false,
    filter,
    sheet: {} as { label?: string },
    provenance: { source: "inferred" as const, rule },
  };
}

function makeDescriptor(
  key: string,
  label: string,
  kind: Exclude<ColKind, "array" | "enum">,
  filter: FilterDescriptor | null,
  rule: string,
): InferredColumn {
  return { key, ...inferredCommon(label, kind, filter, rule), kind };
}

/** Split a key into lowercase words, handling camelCase, snake_case, and kebab-case. */
function keyToWords(key: string): string[] {
  return key
    .replace(/([a-z])([A-Z])/g, "$1_$2")
    .split(/[-_]/)
    .map((w) => w.toLowerCase())
    .filter(Boolean);
}

const ID_WORDS = new Set(["id", "uuid", "hash", "token", "key"]);
const CODE_WORDS = new Set([
  "path",
  "url",
  "uri",
  "endpoint",
  "route",
  "host",
  "link",
  "href",
  "website",
]);
const LATENCY_WORDS = new Set([
  "latency",
  "duration",
  "elapsed",
  "delay",
  "wait",
  "ttfb",
  "rtt",
  "ping",
]);
const SIZE_WORDS = new Set(["size", "bytes", "length"]);
const LEVEL_WORDS = new Set(["level", "severity"]);
const TRACE_ID_WORDS = new Set(["trace", "span", "request"]);
const FAVORITE_WORDS = new Set(["favorite", "starred", "bookmarked", "pinned"]);
const EMAIL_WORDS = new Set(["email", "mail"]);
const STATUS_WORDS = new Set(["status", "state"]);
const HEALTH_WORDS = new Set([
  "health",
  "score",
  "rating",
  "accuracy",
  "uptime",
]);
const PROGRESS_WORDS = new Set(["progress", "completion", "percentage"]);
const HP_WORDS = new Set(["hp", "hitpoints"]);

/** Semantic color mapping for status-like enum values. */
const STATUS_COLORS: Record<string, string> = {
  active: "#22c55e",
  completed: "#22c55e",
  success: "#22c55e",
  published: "#22c55e",
  approved: "#22c55e",
  pending: "#f59e0b",
  draft: "#f59e0b",
  inactive: "#f59e0b",
  paused: "#f59e0b",
  error: "#ef4444",
  failed: "#ef4444",
  rejected: "#ef4444",
  cancelled: "#ef4444",
  archived: "#6b7280",
  deleted: "#6b7280",
  disabled: "#6b7280",
};

/** Neutral palette for enum values without semantic meaning. */
const NEUTRAL_PALETTE = [
  "#6366f1",
  "#8b5cf6",
  "#ec4899",
  "#14b8a6",
  "#f97316",
  "#06b6d4",
  "#84cc16",
  "#eab308",
  "#ef4444",
  "#64748b",
];

/** Generate a colorMap for enum values, using semantic colors where possible. */
function generateColorMap(values: readonly string[]): Record<string, string> {
  const colorMap: Record<string, string> = {};
  let neutralIdx = 0;
  for (const value of values) {
    const lower = value.toLowerCase();
    if (STATUS_COLORS[lower]) {
      colorMap[value] = STATUS_COLORS[lower];
    } else {
      colorMap[value] = NEUTRAL_PALETTE[neutralIdx % NEUTRAL_PALETTE.length]!;
      neutralIdx++;
    }
  }
  return colorMap;
}

/** Post-process an inferred descriptor with smart display/config heuristics. */
function enhanceDescriptor(descriptor: InferredColumn): InferredColumn {
  const words = keyToWords(descriptor.key);
  const joined = words.join("");
  const d = { ...descriptor };

  const hasIdWord = words.some((w) => ID_WORDS.has(w));
  const hasCodeWord = words.some((w) => CODE_WORDS.has(w));
  const hasLatencyWord =
    words.some((w) => LATENCY_WORDS.has(w)) || joined.includes("responsetime");
  const hasSizeWord = words.some((w) => SIZE_WORDS.has(w));
  const hasLevelWord = words.some((w) => LEVEL_WORDS.has(w));
  const isTraceId = hasIdWord && words.some((w) => TRACE_ID_WORDS.has(w));
  const hasFavoriteWord = words.some((w) => FAVORITE_WORDS.has(w));
  const hasEmailWord = words.some((w) => EMAIL_WORDS.has(w));
  const hasStatusWord = words.some((w) => STATUS_WORDS.has(w));
  const hasHealthWord = words.some((w) => HEALTH_WORDS.has(w));
  const hasProgressWord = words.some((w) => PROGRESS_WORDS.has(w));
  const hasHpWord = words.some((w) => HP_WORDS.has(w));

  // ID-like columns → code display, not sortable
  if (hasIdWord) {
    d.display = { type: "code" };
    d.sortable = false;
    // Trace/span/request IDs → hidden, not filterable (matches col.presets.traceId())
    if (isTraceId) {
      d.hidden = true;
      d.filter = null;
    }
  }
  // Favorite/starred booleans → star display, hide column header
  else if (hasFavoriteWord && d.kind === "boolean") {
    d.display = { type: "star" };
    d.hideHeader = true;
  }
  // Email columns → code display
  else if (hasEmailWord && d.kind === "string") {
    d.display = { type: "code" };
  }
  // Path/URL-like columns → code display
  else if (hasCodeWord) {
    d.display = { type: "code" };
  }
  // Latency-like number columns → heatmap with ms unit, sortable
  else if (hasLatencyWord && d.kind === "number") {
    // `compact` for the same reason the builder uses it: a zero-variance
    // sample gives the column an `input` filter with no bounds, and a display
    // carrying `min: undefined` is not the canonical form of one that omits it.
    d.display = compact({
      type: "heatmap",
      unit: "ms",
      min: d.filter?.min,
      max: d.filter?.max,
    }) as DisplayDescriptor;
    d.sortable = true;
  }
  // Size-like number columns → number with B unit, sortable
  else if (hasSizeWord && d.kind === "number") {
    d.display = { type: "number", unit: "B" };
    d.sortable = true;
  }
  // Health/score/rating → gauge display (min always 0 for visual baseline)
  else if (hasHealthWord && d.kind === "number") {
    d.display = compact({
      type: "gauge",
      min: 0,
      max: d.filter?.max,
    }) as DisplayDescriptor;
    d.sortable = true;
  }
  // HP/hitpoints → bar display (min always 0 for visual baseline)
  else if (hasHpWord && d.kind === "number") {
    d.display = compact({
      type: "bar",
      min: 0,
      max: d.filter?.max,
    }) as DisplayDescriptor;
    d.sortable = true;
  }
  // Progress/completion → bar display (min always 0 for visual baseline)
  else if (hasProgressWord && d.kind === "number") {
    d.display = compact({
      type: "bar",
      min: 0,
      max: d.filter?.max,
    }) as DisplayDescriptor;
    d.sortable = true;
  }

  // Sortable defaults by type (unless ID-like)
  if (!hasIdWord) {
    if (d.kind === "timestamp" || d.kind === "number") {
      d.sortable = true;
    }
  }

  // Log level / severity enums: expand filter by default (matches col.presets.logLevel())
  if (hasLevelWord && d.kind === "enum" && d.filter) {
    d.filter = { ...d.filter, defaultOpen: true };
  }

  // Status/state enums → semantic colorMap on badge display
  if (hasStatusWord && d.kind === "enum") {
    d.display = { type: "badge", colorMap: generateColorMap(d.enumValues) };
  }

  // Column sizing defaults
  const sizeDefaults: Record<string, number> = {
    boolean: 100,
    timestamp: 220,
    number: 120,
    enum: 130,
  };
  if (sizeDefaults[d.kind] !== undefined) {
    d.size = sizeDefaults[d.kind];
  }

  return d;
}

/** Infer the item descriptor of an array column from its flattened items. */
function inferArrayItem(items: unknown[]): ColumnDescriptor {
  const common = (kind: ColKind, rule: string) =>
    inferredCommon("", kind, null, rule);

  if (items.length === 0) {
    return { ...common("string", "array-item:empty"), kind: "string" };
  }
  if (items.every((v) => typeof v === "string")) {
    const distinct = new Set(items as string[]);
    if (distinct.size <= 10) {
      return {
        ...common("enum", "array-item:enum"),
        kind: "enum",
        enumValues: Array.from(distinct),
      };
    }
    return { ...common("string", "array-item:string"), kind: "string" };
  }
  if (items.every((v) => typeof v === "number")) {
    return { ...common("number", "array-item:number"), kind: "number" };
  }
  if (items.every((v) => typeof v === "boolean")) {
    return { ...common("boolean", "array-item:boolean"), kind: "boolean" };
  }
  return { ...common("string", "array-item:mixed"), kind: "string" };
}

function inferColDescriptor(key: string, values: unknown[]): InferredColumn {
  const label = keyToLabel(key);
  const nonNull = values.filter((v) => v !== null && v !== undefined);

  if (nonNull.length === 0) {
    return makeDescriptor(
      key,
      label,
      "string",
      { type: "input", defaultOpen: false, commandDisabled: false },
      "empty-column",
    );
  }

  // Timestamp: ISO 8601 strings
  if (nonNull.every((v) => typeof v === "string" && isIso8601(v as string))) {
    return makeDescriptor(
      key,
      label,
      "timestamp",
      { type: "timerange", defaultOpen: false, commandDisabled: true },
      "iso8601-string",
    );
  }

  // Timestamp: Unix ms numbers
  if (nonNull.every((v) => typeof v === "number" && isUnixMs(v as number))) {
    return makeDescriptor(
      key,
      label,
      "timestamp",
      { type: "timerange", defaultOpen: false, commandDisabled: true },
      "unix-ms-number",
    );
  }

  // Boolean — options match `col.boolean()` exactly. Inference used to omit
  // them and let `generateFilterFields` derive "Yes"/"No", while the factory
  // baked in "true"/"false": two construction paths producing different filter
  // options for the same column kind. The codegen round trip cannot hold while
  // they disagree, because no chain step can clear the factory's options.
  if (nonNull.every((v) => v === true || v === false)) {
    return makeDescriptor(
      key,
      label,
      "boolean",
      {
        type: "checkbox",
        defaultOpen: false,
        commandDisabled: false,
        options: [
          { label: "true", value: true },
          { label: "false", value: false },
        ],
      },
      "boolean",
    );
  }

  // Number
  if (nonNull.every((v) => typeof v === "number")) {
    const nums = nonNull as number[];
    const min = Math.min(...nums);
    const max = Math.max(...nums);
    const filter: FilterDescriptor =
      min !== max
        ? {
            type: "slider",
            defaultOpen: false,
            commandDisabled: false,
            min,
            max,
          }
        : { type: "input", defaultOpen: false, commandDisabled: false };
    return makeDescriptor(key, label, "number", filter, "number");
  }

  // Array — the item type is always described, so a `string[]` column with more
  // distinct values than the enum threshold stays a `string[]` column.
  if (nonNull.every((v) => Array.isArray(v))) {
    const allItems = (nonNull as unknown[][])
      .flat()
      .filter((v) => v !== null && v !== undefined);
    const arrayItem = inferArrayItem(allItems);
    const filter: FilterDescriptor | null =
      arrayItem.kind === "enum"
        ? {
            type: "checkbox",
            defaultOpen: false,
            commandDisabled: false,
            options: arrayItem.enumValues.map((v) => ({ label: v, value: v })),
          }
        : null;
    return {
      key,
      ...inferredCommon(label, "array", filter, "array"),
      kind: "array",
      arrayItem,
    };
  }

  // Record (plain object, non-array)
  if (nonNull.every((v) => typeof v === "object" && !Array.isArray(v))) {
    return makeDescriptor(key, label, "record", null, "record");
  }

  // String: check if enum (≤ 10 distinct values)
  if (nonNull.every((v) => typeof v === "string")) {
    const distinct = new Set(nonNull as string[]);
    if (distinct.size <= 10) {
      const enumValues = Array.from(distinct);
      return {
        key,
        ...inferredCommon(
          label,
          "enum",
          {
            type: "checkbox",
            defaultOpen: false,
            commandDisabled: false,
            options: enumValues.map((v) => ({ label: v, value: v })),
          },
          "string-enum",
        ),
        kind: "enum",
        enumValues,
      };
    }
    return makeDescriptor(
      key,
      label,
      "string",
      { type: "input", defaultOpen: false, commandDisabled: false },
      "string",
    );
  }

  // Fallback: mixed or unrecognised types — warn and treat as string
  const types = [
    ...new Set(nonNull.map((v) => (Array.isArray(v) ? "array" : typeof v))),
  ];
  console.warn(
    `[inferSchemaFromJSON] Column "${key}" has mixed or ambiguous types (${types.join(", ")}). ` +
      `Falling back to string input filter.`,
  );
  return makeDescriptor(
    key,
    label,
    "string",
    { type: "input", defaultOpen: false, commandDisabled: false },
    "mixed-fallback",
  );
}

/**
 * Infer a SchemaJSON from an array of plain data objects.
 *
 * Walks all rows, collects per-key values, and infers the best ColKind and
 * FilterType for each column using these heuristics:
 * - `timestamp`: ISO 8601 strings or Unix-ms numbers
 * - `boolean`: all values strictly true/false
 * - `number`: all non-null values are typeof "number"
 * - `enum`: strings with ≤ 10 distinct values across the sample
 * - `array`: values are arrays (item type inferred recursively)
 * - `record`: values are plain objects (non-array)
 * - `string`: fallback
 *
 * Number columns with min ≠ max get a "slider" filter; otherwise "input".
 */
export function inferSchemaFromJSON(data: unknown[]): SchemaJSON {
  if (!Array.isArray(data) || data.length === 0) {
    return { version: SCHEMA_JSON_VERSION, columns: [] };
  }

  // Collect all keys and their values across rows (preserving insertion order)
  const keyValues = new Map<string, unknown[]>();

  for (const row of data) {
    if (typeof row !== "object" || row === null || Array.isArray(row)) continue;
    for (const [key, value] of Object.entries(row as Record<string, unknown>)) {
      if (!keyValues.has(key)) keyValues.set(key, []);
      keyValues.get(key)!.push(value);
    }
  }

  const columns = Array.from(keyValues.entries()).map(([key, values]) =>
    enhanceDescriptor(inferColDescriptor(key, values)),
  );

  return { version: SCHEMA_JSON_VERSION, columns };
}
src/lib/table-schema/to-typescript.ts
import { col } from "./col";
import { presets } from "./presets";
import type {
  ColBuilder,
  ColumnDescriptor,
  ColumnDescriptorCommon,
  DatePresetDescriptor,
  JsonValue,
  SchemaJSON,
} from "./types";

/**
 * Emit the `col.*` / `col.presets.*` factory call a descriptor came from, and
 * produce the descriptor that call yields on its own.
 *
 * Provenance is recorded at construction, so this reads it instead of
 * pattern-matching the descriptor's shape back into a guess at the preset.
 */
function factoryFor(c: ColumnDescriptor): {
  source: string;
  baseline: ColumnDescriptor;
} {
  if (c.provenance.source === "preset") {
    const factory = presets[c.provenance.preset as keyof typeof presets];
    if (typeof factory === "function") {
      const args = c.provenance.args;
      // Re-running the preset with its recorded arguments gives the exact
      // baseline the author started from, so the chain below emits only what
      // they actually changed.
      const builder = (
        factory as (...a: unknown[]) => ColBuilder<unknown, any>
      )(...args.map((a) => (a === null ? undefined : a)));
      return {
        source: `col.presets.${c.provenance.preset}(${args.map(emitValue).join(", ")})`,
        baseline: builder._descriptor,
      };
    }
    // Unknown preset name (schema written by a newer build) — fall through to
    // the primitive factory rather than emitting a call that will not compile.
  }

  if (c.kind === "enum") {
    return {
      source: `col.enum([${c.enumValues.map(emitValue).join(", ")}])`,
      baseline: col.enum(c.enumValues)._descriptor,
    };
  }

  if (c.kind === "array") {
    const item = factoryFor(c.arrayItem);
    const itemChain = chainFor(c.arrayItem, item.baseline);
    // `col.array()` requires an item builder — emitting a bare `col.array()`
    // is what used to produce code that did not compile.
    const itemSource = item.source + itemChain.join("");
    return {
      source: `col.array(${itemSource})`,
      baseline: col.array(rebuild(c.arrayItem))._descriptor,
    };
  }

  const factory = col[c.kind] as () => ColBuilder<unknown, any>;
  return { source: `col.${c.kind}()`, baseline: factory()._descriptor };
}

/** Rebuild a builder from a descriptor — used to seed `col.array`'s baseline. */
function rebuild(c: ColumnDescriptor): ColBuilder<unknown, any> {
  if (c.kind === "enum") return col.enum(c.enumValues);
  if (c.kind === "array") return col.array(rebuild(c.arrayItem));
  return (col[c.kind] as () => ColBuilder<unknown, any>)();
}

function emitValue(value: JsonValue | readonly string[] | undefined): string {
  if (value === undefined || value === null) return "undefined";
  // Arrays are spaced by hand so preset args (`logLevel(["a", "b"])`) match the
  // per-value assembly used elsewhere (`col.enum(["a", "b"])`) — bare
  // `JSON.stringify` would emit one of them without spaces.
  if (Array.isArray(value)) {
    return `[${value.map((v) => emitValue(v as JsonValue)).join(", ")}]`;
  }
  return JSON.stringify(value);
}

/**
 * Emit a timerange preset.
 *
 * The descriptor stores `from`/`to` as ISO strings so they survive JSON, but
 * `.filterable("timerange", { presets })` takes `DatePreset`s holding real
 * `Date`s — printing the descriptor verbatim produced code that threw
 * `preset.from.toISOString is not a function` on the first run.
 */
function emitDatePreset(preset: DatePresetDescriptor): string {
  return (
    `{ label: ${JSON.stringify(preset.label)}, ` +
    `shortcut: ${JSON.stringify(preset.shortcut)}, ` +
    `from: new Date(${JSON.stringify(preset.from)}), ` +
    `to: new Date(${JSON.stringify(preset.to)}) }`
  );
}

/** Emit an object literal with unquoted keys, matching the docs' house style. */
function emitObject(entries: Record<string, unknown>): string {
  const parts = Object.entries(entries)
    .filter(([, v]) => v !== undefined)
    .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
  return `{ ${parts.join(", ")} }`;
}

function sameJSON(a: unknown, b: unknown): boolean {
  return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}

/**
 * One emitter per serializable descriptor field.
 *
 * The `-?` plus full key coverage means adding a field to
 * `ColumnDescriptorCommon` without adding an emitter here is a **compile
 * error**, not a silently dropped chain step.
 */
type Emitter<K extends keyof ColumnDescriptorCommon> = (
  value: ColumnDescriptorCommon[K],
  baseline: ColumnDescriptor,
  descriptor: ColumnDescriptor,
) => string[];

type EmitterMap = { [K in keyof ColumnDescriptorCommon]-?: Emitter<K> };

const EMITTERS: EmitterMap = {
  label: (value) => [`.label(${JSON.stringify(value)})`],

  description: (value) =>
    value === undefined ? [] : [`.description(${JSON.stringify(value)})`],

  display: (value) => {
    const { type, ...options } = value as { type: string } & Record<
      string,
      unknown
    >;
    const hasOptions = Object.values(options).some((v) => v !== undefined);
    return [
      hasOptions
        ? `.display(${JSON.stringify(type)}, ${emitObject(options)})`
        : `.display(${JSON.stringify(type)})`,
    ];
  },

  filter: (value) => {
    if (value === null) return [".notFilterable()"];
    const parts: string[] = [];
    const { type, defaultOpen, commandDisabled, options, presets, ...rest } =
      value;

    if (type === "checkbox") {
      parts.push(
        options
          ? `.filterable("checkbox", { options: [${options
              .map((o) => emitObject({ label: o.label, value: o.value }))
              .join(", ")}] })`
          : `.filterable("checkbox")`,
      );
    } else if (type === "slider") {
      parts.push(`.filterable("slider", ${emitObject(rest)})`);
    } else if (type === "timerange") {
      parts.push(
        presets
          ? `.filterable("timerange", { presets: [${presets
              .map(emitDatePreset)
              .join(", ")}] })`
          : `.filterable("timerange")`,
      );
    } else {
      parts.push(`.filterable("input")`);
    }

    if (defaultOpen) parts.push(".defaultOpen()");
    if (commandDisabled) parts.push(".commandDisabled()");
    return parts;
  },

  sheet: (value) => {
    if (value === null) return [];
    const args = emitObject(value as Record<string, unknown>);
    return [args === "{  }" ? ".sheet()" : `.sheet(${args})`];
  },

  size: (value) => (value === undefined ? [] : [`.size(${value})`]),
  minSize: (value) => (value === undefined ? [] : [`.minSize(${value})`]),
  hidden: (value) => (value ? [".hidden()"] : []),
  hideHeader: (value) => (value ? [".hideHeader()"] : []),
  resizable: (value) => (value ? [".resizable()"] : []),
  // Only reached when the value differs from the baseline, so `false` here
  // means a preset (`timestamp`, `latency`, …) switched sorting on and the
  // author switched it off again.
  sortable: (value) => [value ? ".sortable()" : ".sortable(false)"],
  optional: (value) => (value ? [".optional()"] : []),

  // `enableHiding: false` together with `hidden` is exactly `.sheetOnly()`;
  // on its own it is only reachable via `col.select()`, whose baseline already
  // has it, so nothing needs emitting.
  enableHiding: (value, _baseline, descriptor) =>
    value === false && descriptor.hidden ? [".sheetOnly()"] : [],

  // Not a chain step — provenance is how the factory call was chosen.
  provenance: () => [],
};

/**
 * The order chain steps are emitted in. Listed explicitly because `.sheetOnly()`
 * clears the filter and must come after `.filterable()` would have set it.
 */
export const EMIT_ORDER: (keyof ColumnDescriptorCommon)[] = [
  "label",
  "description",
  "display",
  "filter",
  "enableHiding",
  "sortable",
  "hidden",
  "hideHeader",
  "resizable",
  "optional",
  "size",
  "minSize",
  "sheet",
];

/** Emit only the steps where the descriptor differs from the factory baseline. */
function chainFor(c: ColumnDescriptor, baseline: ColumnDescriptor): string[] {
  const parts: string[] = [];
  for (const key of EMIT_ORDER) {
    const value = c[key];
    if (sameJSON(value, baseline[key])) continue;
    // `.sheetOnly()` already implies `.hidden()` and `.notFilterable()`.
    //
    // The `filter === null` check is the whole reason this branch is safe:
    // without it, any descriptor shaped like a sheet-only column had its
    // filter dropped from the emitted source, silently. `validateSchema`
    // rejects that combination, so it can only arrive here through a raw
    // `SchemaJSON` that never went through a schema — and then the filter is
    // emitted rather than swallowed.
    if (
      c.enableHiding === false &&
      c.hidden &&
      c.filter === null &&
      baseline.enableHiding !== false &&
      (key === "hidden" || key === "filter")
    ) {
      continue;
    }
    const emit = EMITTERS[key] as Emitter<typeof key>;
    parts.push(...emit(value as never, baseline, c));
  }
  return parts;
}

/**
 * Convert a `SchemaJSON` descriptor to a `createTableSchema(...)` TypeScript
 * source code string.
 *
 * The output is ready to copy-paste into a project that imports from
 * `@/lib/table-schema`. Custom cell renderers are not serialized, so the
 * emitted chain uses the descriptor's fallback display for those columns.
 *
 * @example
 * ```ts
 * const ts = schemaToTypeScript(tableSchema.toJSON());
 * // → 'import { createTableSchema, col } from "@/lib/table-schema"; ...'
 * ```
 */
export function schemaToTypeScript(json: SchemaJSON): string {
  const lines: string[] = [
    'import { createTableSchema, col } from "@/lib/table-schema";',
    "",
    "export const schema = createTableSchema({",
  ];

  for (const { key, ...descriptor } of json.columns) {
    const c = descriptor as ColumnDescriptor;
    const { source, baseline } = factoryFor(c);
    const chain = [source, ...chainFor(c, baseline)].join("\n    ");
    lines.push(`  ${key}: ${chain},`);
  }

  lines.push("});");
  return lines.join("\n");
}
src/lib/filters/types.ts
import type {
  ColBuilder,
  ColKind,
  FilterType,
  TableSchemaDefinition,
} from "../table-schema/types";

export type { ColKind, FilterType };

/** The member type of a checkbox filter: the item type for an array column. */
type FilterItem<T> =
  NonNullable<T> extends readonly (infer U)[] ? U : NonNullable<T>;

/**
 * The value one column's filter accepts, from its `ColBuilder<T, F>`.
 *
 * Mirrors `normalize`: dispatch is on the declared filter type, and the
 * member type comes from the column's data type — so a checkbox on
 * `col.enum(LEVELS)` only accepts members of `LEVELS`. Distributes over `F`,
 * so a column whose filter type was never narrowed (`col.number()` allows
 * three) accepts any of them.
 *
 * A checkbox on a non-scalar member type is `never`: `col.array()` takes any
 * item builder, so `col.array(col.record())` type-checks, but `normalize` has
 * nothing to compare a record against. Advertising the member type there
 * would promise a filter the engine cannot plan.
 */
export type FilterValueFor<T, F extends FilterType> = F extends "input"
  ? NonNullable<T> extends number
    ? number
    : string
  : F extends "checkbox"
    ? [FilterItem<T>] extends [Scalar]
      ? FilterItem<T> | readonly FilterItem<T>[]
      : never
    : F extends "slider"
      ? number | readonly [number, number]
      : F extends "timerange"
        ? Date | readonly [Date, Date]
        : never;

/**
 * The filter values a table schema accepts, keyed by filterable column.
 *
 * Columns that are `.notFilterable()` (or `col.record()` / `col.select()`,
 * which never are) are absent, so a key typo or a guard on an unfilterable
 * column is a compile error — the same rule `defineActions` enforces at
 * runtime for a `when` clause.
 */
export type FilterValues<TSchema extends TableSchemaDefinition> = {
  [K in keyof TSchema as TSchema[K] extends ColBuilder<unknown, infer F>
    ? [F] extends [never]
      ? never
      : K
    : never]: TSchema[K] extends ColBuilder<infer T, infer F>
    ? FilterValueFor<T, F>
    : never;
};

/** A value a filter can compare against. */
export type Scalar = string | number | boolean;

/**
 * A column's declared filter semantics, flattened to plain data.
 *
 * This is the whole input to the normalization table. Backends never see
 * anything else about a column, which is what stops them re-deriving semantics
 * from the runtime shape of a value.
 */
export type FilterSpec = {
  /** Dot-notation — the ONE identity space, matching the table schema key. */
  key: string;
  type: FilterType;
  kind: ColKind;
  /** Present when `kind === "array"`. */
  itemKind?: ColKind;
  options?: readonly Scalar[];
  min?: number;
  max?: number;
};

/**
 * The canonical, backend-neutral operation set.
 *
 * CLOSED union — exactly six members. Every backend implements all six with an
 * exhaustive switch and no default branch, so adding a seventh is a compile
 * error at every backend rather than a silent fallthrough. That is the point of
 * the design, not a defect: a new op is a breaking change to every engine.
 */
export type FilterOp =
  /** Case-insensitive substring match. */
  | { op: "substring"; key: string; value: string }
  | { op: "equals"; key: string; value: Scalar }
  /** Scalar column, value ∈ set. */
  | { op: "oneOf"; key: string; values: Scalar[] }
  /** Array column, column ∩ set ≠ ∅. */
  | { op: "overlaps"; key: string; values: Scalar[] }
  /** Inclusive on both ends. */
  | { op: "numberRange"; key: string; min: number; max: number }
  /** Inclusive on both ends. */
  | { op: "dateRange"; key: string; from: Date; to: Date };

/** Restrict which keys `plan` / `matches` consider. */
export type FilterSelection = {
  only?: readonly string[];
  exclude?: readonly string[];
};
src/lib/filters/normalize.ts
import type { ColKind, FilterOp, FilterSpec, Scalar } from "./types";

/** Unix ms timestamps are 13-digit numbers (> Sep 2001, < Nov 2286). */
const UNIX_MS_MIN = 1_000_000_000_000;

/**
 * Is this filter value active?
 *
 * Inactive means "the user has not filtered on this column": absent, cleared,
 * an empty multi-select, or a value that cannot be compared at all. This was
 * written four slightly different ways across four engines.
 */
export function isActive(value: unknown): boolean {
  if (value === null || value === undefined) return false;
  if (typeof value === "string") return value.length > 0;
  if (typeof value === "number") return !Number.isNaN(value);
  if (value instanceof Date) return !Number.isNaN(value.getTime());
  if (Array.isArray(value)) {
    // ANY usable member makes the filter active. `normalize` drops the members
    // it cannot use, so requiring all of them to be usable would make one blank
    // entry silently delete the whole filter — and inconsistently, since
    // `["abc", 500]` on a slider degenerates to 500 while `["", 500]` vanished.
    return value.some(isActive);
  }
  return true;
}

/** Coerce to a `Date`, or `null` if it cannot be one. */
function asDate(value: unknown): Date | null {
  if (value instanceof Date) {
    return Number.isNaN(value.getTime()) ? null : value;
  }
  // superjson revives Dates, but MCP args and LLM output arrive as ISO strings
  // or epoch millis.
  if (typeof value === "string" || typeof value === "number") {
    // Epoch millis that arrived as a string: `new Date("1700000000000")` is an
    // Invalid Date while `new Date(1700000000000)` is not. The command palette
    // serializes timeranges with `getTime()`, and JSON has no Date type, so
    // this shape is common. The magnitude test disambiguates it from a bare
    // year like "2024", which `Date` should keep parsing as a year.
    if (typeof value === "string" && /^\d+$/.test(value)) {
      const millis = Number(value);
      if (millis >= UNIX_MS_MIN) {
        const date = new Date(millis);
        return Number.isNaN(date.getTime()) ? null : date;
      }
    }
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? null : date;
  }
  return null;
}

function asNumber(value: unknown): number | null {
  if (typeof value === "number") return Number.isNaN(value) ? null : value;
  if (typeof value === "string" && value.trim() !== "") {
    const parsed = Number(value);
    return Number.isNaN(parsed) ? null : parsed;
  }
  return null;
}

function asScalar(value: unknown): Scalar | null {
  if (
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean"
  ) {
    return value;
  }
  return null;
}

function asArray(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [value];
}

function asBoolean(value: unknown): boolean | null {
  if (typeof value === "boolean") return value;
  if (value === "true") return true;
  if (value === "false") return false;
  return null;
}

/**
 * Coerce one checkbox member to the column's declared type.
 *
 * The URL layer, MCP args, and LLM output all deliver `["200", "500"]` for a
 * numeric column. Without this, the SQL backend received
 * `inArray(integerColumn, ["200", "500"])` and depended on Postgres casting the
 * literals, and `coerce()` — the entry point documented as producing validated
 * values — handed back strings for a number column.
 */
function asDeclaredScalar(kind: ColKind, value: unknown): Scalar | null {
  switch (kind) {
    case "number":
      return asNumber(value);
    case "boolean":
      return asBoolean(value);
    default: {
      const scalar = asScalar(value);
      return scalar === null ? null : String(scalar);
    }
  }
}

function startOfDay(date: Date): Date {
  const result = new Date(date);
  result.setHours(0, 0, 0, 0);
  return result;
}

function endOfDay(date: Date): Date {
  const result = new Date(date);
  result.setHours(23, 59, 59, 999);
  return result;
}

/**
 * Turn one declared filter and its value into a canonical op.
 *
 * This is the whole design. Dispatch is on the **declared** `(type, kind)`
 * pair — never on the runtime shape of `value`. A numeric checkbox is
 * `{ type: "checkbox", kind: "number" }`, so `[200, 500]` can only become
 * `oneOf`; `numberRange` is unreachable from a checkbox and
 * `BETWEEN 200 AND 500` is therefore unrepresentable. Backends never see array
 * length, so length-based dispatch has nowhere to live.
 *
 * Returns `null` when the filter is inactive or the value cannot be coerced
 * into the declared shape.
 */
export function normalize(spec: FilterSpec, value: unknown): FilterOp | null {
  if (!isActive(value)) return null;
  const { key } = spec;

  switch (spec.type) {
    case "input": {
      // A number column's text box is an exact match, not a substring search —
      // substring matching on a stringified number makes "5" match 1500.
      if (spec.kind === "number") {
        const parsed = asNumber(Array.isArray(value) ? value[0] : value);
        return parsed === null ? null : { op: "equals", key, value: parsed };
      }
      const raw = Array.isArray(value) ? value[0] : value;
      const text = asScalar(raw);
      return text === null
        ? null
        : { op: "substring", key, value: String(text) };
    }

    case "checkbox": {
      // For an array column the members are compared against the *item* type.
      const memberKind =
        spec.kind === "array" ? (spec.itemKind ?? "string") : spec.kind;
      const values = asArray(value)
        .map((member) => asDeclaredScalar(memberKind, member))
        .filter((v): v is Scalar => v !== null);
      if (values.length === 0) return null;
      // An array column is a set on both sides — overlap, not membership.
      return spec.kind === "array"
        ? { op: "overlaps", key, values }
        : { op: "oneOf", key, values };
    }

    case "slider": {
      const numbers = asArray(value)
        .map(asNumber)
        .filter((v): v is number => v !== null);
      if (numbers.length === 0) return null;
      // A single-handle slider is a degenerate range, not an equality — the
      // two disagree once the column holds non-integers.
      const [first, second] = numbers;
      const min = first!;
      const max = second ?? first!;
      return {
        op: "numberRange",
        key,
        min: Math.min(min, max),
        max: Math.max(min, max),
      };
    }

    case "timerange": {
      const dates = asArray(value)
        .map(asDate)
        .filter((v): v is Date => v !== null);
      if (dates.length === 0) return null;
      // One date means "that whole day".
      if (dates.length === 1) {
        return {
          op: "dateRange",
          key,
          from: startOfDay(dates[0]!),
          to: endOfDay(dates[0]!),
        };
      }
      const [from, to] = dates;
      return from!.getTime() <= to!.getTime()
        ? { op: "dateRange", key, from: from!, to: to! }
        : { op: "dateRange", key, from: to!, to: from! };
    }
  }
}
src/lib/filters/evaluate.ts
import type { FilterOp, Scalar } from "./types";

/**
 * Read a possibly dotted key off a row.
 *
 * Table schema keys are dot-notation (`"timing.dns"`). Rows may store that
 * either as a literal flat key or as a nested object, so both are tried —
 * a flat hit wins, since that is the wire shape.
 */
export function getValueAtKey(row: unknown, key: string): unknown {
  if (row === null || typeof row !== "object") return undefined;
  const flat = (row as Record<string, unknown>)[key];
  if (flat !== undefined) return flat;
  if (!key.includes(".")) return undefined;
  let current: unknown = row;
  for (const part of key.split(".")) {
    if (current === null || typeof current !== "object") return undefined;
    current = (current as Record<string, unknown>)[part];
  }
  return current;
}

function sameScalar(cell: unknown, value: Scalar): boolean {
  if (cell === value) return true;
  // The URL layer can only produce strings, so a number column filtered from a
  // link arrives as one. Compare by string as a last resort rather than by
  // coercing, which would make `0 == false` true.
  if (
    (typeof cell === "number" || typeof cell === "boolean") &&
    typeof value === "string"
  ) {
    return String(cell) === value;
  }
  if (typeof cell === "string" && typeof value !== "string") {
    return cell === String(value);
  }
  return false;
}

function asComparableNumber(cell: unknown): number | null {
  if (typeof cell === "number") return Number.isNaN(cell) ? null : cell;
  if (typeof cell === "string" && cell.trim() !== "") {
    const parsed = Number(cell);
    return Number.isNaN(parsed) ? null : parsed;
  }
  return null;
}

function asComparableTime(cell: unknown): number | null {
  if (cell instanceof Date) {
    return Number.isNaN(cell.getTime()) ? null : cell.getTime();
  }
  if (typeof cell === "string" || typeof cell === "number") {
    const time = new Date(cell).getTime();
    return Number.isNaN(time) ? null : time;
  }
  return null;
}

/**
 * Evaluate one canonical op against one cell value.
 *
 * Exhaustive over `FilterOp` with no default branch — a seventh op fails to
 * compile here rather than silently passing every row.
 */
export function evaluateOp(op: FilterOp, cell: unknown): boolean {
  switch (op.op) {
    case "substring": {
      if (cell === null || cell === undefined) return false;
      // Case-insensitive, matching the SQL side's `ilike`.
      return String(cell).toLowerCase().includes(op.value.toLowerCase());
    }

    case "equals":
      return sameScalar(cell, op.value);

    case "oneOf":
      return op.values.some((value) => sameScalar(cell, value));

    case "overlaps": {
      // The column is a set. A scalar cell is treated as a one-element set so
      // a single-value column still matches — this is where the infinite route
      // used to look only at `row[key][0]`.
      const cells = Array.isArray(cell) ? cell : [cell];
      return cells.some((item) =>
        op.values.some((value) => sameScalar(item, value)),
      );
    }

    case "numberRange": {
      const value = asComparableNumber(cell);
      return value !== null && value >= op.min && value <= op.max;
    }

    case "dateRange": {
      const time = asComparableTime(cell);
      return (
        time !== null && time >= op.from.getTime() && time <= op.to.getTime()
      );
    }
  }
}
src/lib/filters/index.ts
import { resolveColumns } from "../table-schema/col";
import type {
  ColumnDescriptor,
  SchemaJSON,
  TableSchemaDefinition,
} from "../table-schema/types";
import { evaluateOp, getValueAtKey } from "./evaluate";
import { isActive, normalize } from "./normalize";
import type {
  FilterOp,
  FilterSelection,
  FilterSpec,
  FilterValues,
  Scalar,
} from "./types";

export { evaluateOp, getValueAtKey } from "./evaluate";
export { isActive, normalize } from "./normalize";
export type {
  ColKind,
  FilterOp,
  FilterSelection,
  FilterSpec,
  FilterType,
  FilterValueFor,
  FilterValues,
  Scalar,
} from "./types";

/**
 * Where a `Filters` gets its declarations.
 *
 * `SchemaJSON` is here so the declaration can cross the `"use client"` boundary
 * as data: `table-schema.tsx` cannot be imported server-side, but its
 * `toJSON()` can.
 */
export type FilterSource =
  | TableSchemaDefinition
  | SchemaJSON
  | readonly FilterSpec[];

/**
 * `TValues` is the shape of the values these semantics accept, keyed by
 * filterable column. It is `FilterValues<typeof definition>` when built from
 * a table schema definition and the loose `Record<string, unknown>` from
 * `SchemaJSON` or specs, where nothing about the columns is known at compile
 * time. Search params stay untyped either way — `plan` / `matches` take what
 * the wire delivers — but declarations written by hand (an action's `when`
 * guard) and the output of `coerce` are checked against it.
 */
export interface Filters<TValues = Record<string, unknown>> {
  readonly specs: readonly FilterSpec[];

  /** Look up one column's declaration. */
  spec(key: string): FilterSpec | undefined;

  /**
   * Values → canonical ops. Never guesses from the shape of a value.
   * Inactive and unknown keys are dropped.
   */
  plan(
    values: Record<string, unknown>,
    selection?: FilterSelection,
  ): FilterOp[];

  /** In-memory predicate. Replaces `filterData` and `filterGenericData`. */
  matches(
    values: Record<string, unknown>,
    row: unknown,
    selection?: FilterSelection,
  ): boolean;

  /** Filter a whole collection. */
  apply<TRow>(
    rows: readonly TRow[],
    values: Record<string, unknown>,
    selection?: FilterSelection,
  ): TRow[];

  /**
   * A TanStack `ColumnDef.filterFn`. Returns a *function*, so nothing has to be
   * registered on the table via `filterFns: { inDateRange, arrSome }`.
   */
  filterFn(
    key: string,
  ):
    | ((
        row: { getValue: (id: string) => unknown },
        columnId: string,
        value: unknown,
      ) => boolean)
    | undefined;

  /** Validate and coerce untrusted input (AI structured output, MCP args). */
  coerce(raw: Record<string, unknown>): Partial<TValues>;
}

// ── Deriving specs ──────────────────────────────────────────────────────────

function specFromDescriptor(
  key: string,
  column: Pick<ColumnDescriptor, "kind" | "filter"> &
    Partial<Pick<Extract<ColumnDescriptor, { kind: "array" }>, "arrayItem">>,
): FilterSpec | null {
  const { filter } = column;
  if (!filter) return null;

  const spec: FilterSpec = {
    key,
    type: filter.type,
    kind: column.kind,
  };

  if (column.kind === "array" && column.arrayItem) {
    spec.itemKind = column.arrayItem.kind;
  }
  if (filter.options) {
    spec.options = filter.options.map((option) => option.value);
  }
  if (filter.min !== undefined) spec.min = filter.min;
  if (filter.max !== undefined) spec.max = filter.max;

  return spec;
}

function isSchemaJSON(source: FilterSource): source is SchemaJSON {
  return (
    !Array.isArray(source) &&
    typeof source === "object" &&
    source !== null &&
    Array.isArray((source as SchemaJSON).columns)
  );
}

function toSpecs(source: FilterSource): FilterSpec[] {
  if (Array.isArray(source)) {
    return [...(source as readonly FilterSpec[])];
  }

  if (isSchemaJSON(source)) {
    const specs: FilterSpec[] = [];
    for (const column of source.columns) {
      const spec = specFromDescriptor(column.key, column);
      if (spec) specs.push(spec);
    }
    return specs;
  }

  const specs: FilterSpec[] = [];
  for (const column of resolveColumns(source as TableSchemaDefinition)) {
    const spec = specFromDescriptor(column.key, column);
    if (spec) specs.push(spec);
  }
  return specs;
}

function isSelected(key: string, selection?: FilterSelection): boolean {
  if (selection?.exclude?.includes(key)) return false;
  if (selection?.only && !selection.only.includes(key)) return false;
  return true;
}

// ── Coercion ────────────────────────────────────────────────────────────────

function coerceValue(spec: FilterSpec, raw: unknown): unknown {
  // Round-tripping through `normalize` is the coercion: if the declared
  // semantics cannot make an op out of it, it is not a usable filter value.
  const op = normalize(spec, raw);
  if (!op) return undefined;

  switch (op.op) {
    case "substring":
      return op.value;
    case "equals":
      return op.value;
    case "oneOf":
    case "overlaps": {
      // Drop values outside the declared option set — an LLM will happily
      // invent an enum member that does not exist.
      if (!spec.options) return op.values;
      const allowed = op.values.filter((value) =>
        spec.options!.some((option) => String(option) === String(value)),
      );
      return allowed.length > 0 ? allowed : undefined;
    }
    case "numberRange": {
      // Clamp to the declared bounds. `coerce` is the untrusted-input entry
      // point, and an LLM asked for "slow requests" will happily produce
      // `[0, 99999]` against a slider declared `{ min: 0, max: 5000 }`.
      const lower = spec.min ?? -Infinity;
      const upper = spec.max ?? Infinity;
      const min = Math.min(Math.max(op.min, lower), upper);
      const max = Math.min(Math.max(op.max, lower), upper);
      return [min, max];
    }
    case "dateRange":
      return [op.from, op.to];
  }
}

// ── Entry point ─────────────────────────────────────────────────────────────

/**
 * Build the one interpretation of a table's filter semantics.
 *
 * Every engine — SQL, in-memory, TanStack — goes through this, so a column's
 * declared `(FilterType, ColKind)` pair is honoured identically everywhere
 * instead of being re-derived from whatever the value happened to look like at
 * runtime.
 *
 * Built from a table schema definition, the result knows which columns are
 * filterable and what each accepts (`FilterValues`); from `SchemaJSON` or
 * specs it is untyped.
 */
export function defineFilters<TSchema extends TableSchemaDefinition>(
  source: TSchema,
): Filters<FilterValues<TSchema>>;
export function defineFilters(source: FilterSource): Filters;
export function defineFilters(source: FilterSource): Filters {
  const specs = toSpecs(source);
  const byKey = new Map(specs.map((spec) => [spec.key, spec]));

  const plan = (
    values: Record<string, unknown>,
    selection?: FilterSelection,
  ): FilterOp[] => {
    const ops: FilterOp[] = [];
    for (const [key, value] of Object.entries(values)) {
      if (!isSelected(key, selection)) continue;
      const spec = byKey.get(key);
      if (!spec) continue;
      const op = normalize(spec, value);
      if (op) ops.push(op);
    }
    return ops;
  };

  const matches = (
    values: Record<string, unknown>,
    row: unknown,
    selection?: FilterSelection,
  ): boolean =>
    plan(values, selection).every((op) =>
      evaluateOp(op, getValueAtKey(row, op.key)),
    );

  return {
    specs,

    spec: (key) => byKey.get(key),

    plan,

    matches,

    apply: (rows, values, selection) => {
      const ops = plan(values, selection);
      if (ops.length === 0) return [...rows];
      return rows.filter((row) =>
        ops.every((op) => evaluateOp(op, getValueAtKey(row, op.key))),
      );
    },

    filterFn: (key) => {
      const spec = byKey.get(key);
      if (!spec) return undefined;
      return (row, columnId, value) => {
        const op = normalize(spec, value);
        // An inactive filter matches everything, which is what TanStack expects
        // when a column filter is present but empty.
        if (!op) return true;
        return evaluateOp(op, row.getValue(columnId));
      };
    },

    coerce: (raw) => {
      const result: Record<string, unknown> = {};
      for (const spec of specs) {
        const value = raw[spec.key];
        if (!isActive(value)) continue;
        const coerced = coerceValue(spec, value);
        if (coerced !== undefined) result[spec.key] = coerced;
      }
      return result;
    },
  };
}

export type { Scalar as FilterScalar };
src/lib/table-schema/generators/columns.tsx
"use client";

import {
  DataTableCellBadge,
  DataTableCellBar,
  DataTableCellBoolean,
  DataTableCellCode,
  DataTableCellGauge,
  DataTableCellHeatmap,
  DataTableCellLevelIndicator,
  DataTableCellNumber,
  DataTableCellStar,
  DataTableCellStatusCode,
  DataTableCellText,
  DataTableCellTimestamp,
} from "@/components/data-table/data-table-cell";
import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header";
import { Checkbox } from "@/components/ui/checkbox";
import { defineFilters } from "@/lib/filters";
import type { DataTableFeatures } from "@/lib/table/features";
import type { ColumnDef, RowData } from "@tanstack/react-table";
import type { ComponentProps, JSX } from "react";
import { resolveColumns } from "../col";
import type { DisplayDescriptor, TableSchemaDefinition } from "../types";

/**
 * Render the cell based on the display config.
 */
function renderCell(
  display: DisplayDescriptor,
  value: unknown,
  context?: { min: number; max: number },
): JSX.Element | null {
  const fallback = <DataTableCellText value={String(value ?? "")} />;
  const colorMap = "colorMap" in display ? display.colorMap : undefined;
  switch (display.type) {
    case "text": {
      const hex = colorMap?.[String(value)];
      return typeof value === "string" || typeof value === "number" ? (
        <DataTableCellText value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "code": {
      const hex = colorMap?.[String(value)];
      return typeof value === "string" || typeof value === "number" ? (
        <DataTableCellCode value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "number": {
      const hex = colorMap?.[String(value)];
      return typeof value === "number" ? (
        <DataTableCellNumber value={value} unit={display.unit} color={hex} />
      ) : (
        fallback
      );
    }
    case "timestamp": {
      const hex = colorMap?.[String(value)];
      return value instanceof Date ||
        typeof value === "string" ||
        typeof value === "number" ? (
        <DataTableCellTimestamp date={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "badge": {
      if (Array.isArray(value)) {
        return (
          <div className="flex-no-wrap flex gap-1">
            {value.map((item, i) => (
              <DataTableCellBadge
                key={i}
                value={item}
                color={colorMap?.[String(item)]}
              />
            ))}
          </div>
        );
      }
      const hex = colorMap?.[String(value)];
      return typeof value === "string" || typeof value === "number" ? (
        <DataTableCellBadge value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "boolean": {
      const hex = colorMap?.[String(value)];
      return typeof value === "boolean" ? (
        <DataTableCellBoolean value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "star": {
      return typeof value === "boolean" ? (
        <DataTableCellStar value={value} />
      ) : (
        fallback
      );
    }
    case "status-code": {
      const hex = colorMap?.[String(value)];
      return typeof value === "number" ? (
        <DataTableCellStatusCode value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "level-indicator": {
      const hex = colorMap?.[String(value)];
      return typeof value === "string" ? (
        <DataTableCellLevelIndicator value={value} color={hex} />
      ) : (
        fallback
      );
    }
    case "heatmap": {
      const { min = 0, max = 100 } = context ?? {};
      return typeof value === "number" ? (
        <DataTableCellHeatmap
          value={value}
          min={min}
          max={max}
          unit={display.unit}
          color={display.color}
        />
      ) : (
        fallback
      );
    }
    case "bar": {
      const { min = 0, max = 100 } = context ?? {};
      return typeof value === "number" ? (
        <DataTableCellBar
          value={value}
          min={min}
          max={max}
          unit={display.unit}
          color={display.color}
        />
      ) : (
        fallback
      );
    }
    case "gauge": {
      const { min = 0, max = 100 } = context ?? {};
      return typeof value === "number" ? (
        <DataTableCellGauge
          value={value}
          min={min}
          max={max}
          unit={display.unit}
          color={display.color}
        />
      ) : (
        fallback
      );
    }
  }
}

/**
 * The one interpretation of `.size()` / `.minSize()` / `.resizable()`:
 *
 * - `.minSize(px)` — a flexing column with a floor: it absorbs the table's
 *   leftover width but never compresses below `px`.
 * - `.size(px)` without `.resizable()` — locked: min/max pin the rendered
 *   width, so only an unsized column can flex.
 * - `.size(px)` with `.resizable()` — `px` is the initial width only.
 */
function sizingFor(config: {
  size?: number;
  minSize?: number;
  resizable: boolean;
}): { size?: number; minSize?: number; maxSize?: number } {
  if (config.minSize !== undefined) {
    return {
      minSize: config.minSize,
      ...(config.size !== undefined ? { size: config.size } : {}),
    };
  }
  if (config.size === undefined) return {};
  if (config.resizable) return { size: config.size };
  return { size: config.size, minSize: config.size, maxSize: config.size };
}

/**
 * The header checkbox's third state, spelled for both libraries.
 *
 * `ui/checkbox` resolves from whichever library the project was initialized
 * with, and they disagree: Radix takes `checked="indeterminate"`, Base UI
 * types `checked` as a boolean and takes a separate `indeterminate` flag.
 * Setting both is the only form that compiles and renders on either — hence
 * the cast, the one place this file steps around a props type. In the partial
 * state Radix does receive the extra `indeterminate` and forwards it to its
 * `<button>` as `indeterminate="true"`, which is inert; setting it only when
 * it applies is what avoids `indeterminate="false"`, which React warns about
 * on a non-boolean attribute.
 */
function selectionState(allSelected: boolean, someSelected: boolean) {
  const indeterminate = !allSelected && someSelected;
  return {
    checked: allSelected || (someSelected && "indeterminate"),
    ...(indeterminate ? { indeterminate: true } : {}),
  } as ComponentProps<typeof Checkbox>;
}

/**
 * Generate ColumnDef[] from a table schema definition.
 *
 * Rules:
 * - Dotted keys (e.g. "timing.dns") → id + accessorFn
 * - Non-dotted keys → accessorKey
 * - Sortable columns get DataTableColumnHeader; others get a plain string header
 * - filterFn comes from the shared filter-semantics module
 * - Cell renders via built-in display components or the "custom" cell function
 * - meta.label is always set; meta.hidden reflects .hidden() calls
 *
 * Composite/virtual columns that span multiple fields must be appended manually:
 * @example
 * ```ts
 * const columns = [
 *   ...generateColumns(tableSchema),
 *   { id: "timing", header: ..., cell: ..., size: 130 },
 * ];
 * ```
 */
export function generateColumns<TData extends RowData>(
  schema: TableSchemaDefinition,
): ColumnDef<DataTableFeatures, TData>[] {
  // One interpretation of filter semantics, shared with the SQL and in-memory
  // engines. `filterFn` returns a *function*, so the consuming table no longer
  // has to register `filterFns: { inDateRange, arrSome }` — a contract that was
  // undocumented outside a comment and silently broke filtering when missed.
  const filters = defineFilters(schema);

  return resolveColumns(schema).map((config) => {
    const { key } = config;

    // Select column — checkbox header + cell
    if (config.kind === "select") {
      return {
        id: key,
        header: ({ table }) => (
          <div className="flex items-center justify-center">
            <Checkbox
              {...selectionState(
                table.getIsAllPageRowsSelected(),
                table.getIsSomePageRowsSelected(),
              )}
              onCheckedChange={(value: unknown) =>
                table.toggleAllPageRowsSelected(!!value)
              }
              aria-label="Select all"
              className="shadow-none"
            />
          </div>
        ),
        cell: ({ row }) => (
          <div
            className="flex items-center justify-center"
            onClick={(e) => e.stopPropagation()}
          >
            <Checkbox
              checked={row.getIsSelected()}
              onCheckedChange={(value) => row.toggleSelected(!!value)}
              aria-label="Select row"
              className="shadow-none"
            />
          </div>
        ),
        enableSorting: false,
        enableHiding: false,
        enableResizing: false,
        ...sizingFor(config),
        meta: { label: config.label, kind: "select", hidden: config.hidden },
      } as ColumnDef<DataTableFeatures, TData>;
    }

    const isDotted = key.includes(".");
    const filterFn = filters.filterFn(key);

    const header = config.hideHeader
      ? () => <span className="sr-only">{config.label}</span>
      : config.sortable
        ? ({
            column,
          }: {
            column: Parameters<typeof DataTableColumnHeader>[0]["column"];
          }) => <DataTableColumnHeader column={column} title={config.label} />
        : config.label;

    const needsMinMax =
      config.display.type === "heatmap" ||
      config.display.type === "bar" ||
      config.display.type === "gauge";

    const customCell = config.renderers.cell;

    const cell = ({
      getValue,
      row,
      column,
    }: {
      getValue: () => unknown;
      row: { original: TData };
      column: { getFacetedMinMaxValues?: () => [number, number] | undefined };
    }) => {
      // A custom renderer overrides the descriptor's display. The descriptor
      // still carries a real display type, which is what the sheet and
      // `toJSON()` fall back to.
      if (customCell) return customCell(getValue(), row.original);
      if (needsMinMax) {
        const display = config.display as {
          min?: number;
          max?: number;
        };
        const faceted = column.getFacetedMinMaxValues?.();
        const min = faceted?.[0] ?? display.min ?? 0;
        const max = faceted?.[1] ?? display.max ?? 100;
        return renderCell(config.display, getValue(), { min, max });
      }
      return renderCell(config.display, getValue());
    };

    const meta = {
      label: config.label,
      hidden: config.hidden,
      kind: config.kind,
    };

    const base = {
      header,
      cell,
      enableResizing: config.resizable,
      // Not just the header: with the default (`true`) a column that never
      // opted in could still be sorted through state, e.g. a URL param.
      enableSorting: config.sortable,
      ...(config.enableHiding === false ? { enableHiding: false } : {}),
      ...(filterFn ? { filterFn } : {}),
      ...sizingFor(config),
      meta,
    };

    if (isDotted) {
      return {
        ...base,
        id: key,
        accessorFn: (row: TData) => (row as Record<string, unknown>)[key],
      } as ColumnDef<DataTableFeatures, TData>;
    }

    return {
      ...base,
      accessorKey: key,
    } as ColumnDef<DataTableFeatures, TData>;
  });
}
src/lib/table-schema/generators/filter-fields.tsx
// Imported from its file, not the cell barrel: this generator runs on the
// server too (the filter schema reads it), and the barrel drags in cells that
// use client-only hooks.
import { DataTableCellLevelIndicator } from "@/components/data-table/data-table-cell/data-table-cell-level-indicator";
import type {
  CheckboxOptionProps,
  DataTableFilterField,
  Option,
} from "@/components/data-table/types";
import type { JSX } from "react";
import { fromPresetDescriptor, resolveColumns } from "../col";
import type { ResolvedColumn, TableSchemaDefinition } from "../types";

/**
 * The checkbox option a display picks when the column supplied no
 * `component` of its own. A `level-indicator` column shows the same dot it
 * renders in its cells (after the label, clear of the checkbox), so the
 * filter sidebar reads like the table — which
 * is why it is limited to the kinds whose cells draw the dot (`renderCell`
 * falls back to plain text for anything but a string).
 */
function defaultFilterComponent(
  config: ResolvedColumn,
  options?: Option[],
): ((props: CheckboxOptionProps) => JSX.Element | null) | undefined {
  if (config.display.type !== "level-indicator") return undefined;
  if (config.kind !== "enum" && config.kind !== "string") return undefined;
  const colorMap = config.display.colorMap;
  // The dot follows the label, so every option is sized to the widest label
  // to keep the dots in one column. The filter passes the field's options,
  // which win over the schema's: a field filled from facets after this
  // component was created only has them there.
  return function LevelOption({
    label,
    value,
    options: rendered = options,
  }: CheckboxOptionProps) {
    return (
      <DataTableCellLevelIndicator
        value={String(value)}
        label={label}
        color={colorMap?.[String(value)]}
        showLabel
        dotPosition="end"
        alignLabels={widestLabelCandidates(rendered)}
      />
    );
  };
}

/** Each option renders a sizer per label, so a long list is cut to a few. */
const MAX_ALIGN_LABELS = 8;

// Every option of a filter asks for the same list on every render, so the
// shortlist is kept per options array — a large facet is sorted once, not once
// per row.
const candidatesCache = new WeakMap<Option[], string[]>();

/**
 * The labels that might be the widest. Character count only shortlists them —
 * the browser does the measuring — so a miss costs alignment, never text.
 */
export function widestLabelCandidates(options?: Option[]): string[] {
  if (!options) return [];
  const cached = candidatesCache.get(options);
  if (cached) return cached;
  const candidates = [...new Set(options.map((o) => o.label))]
    .sort((a, b) => b.length - a.length)
    .slice(0, MAX_ALIGN_LABELS);
  candidatesCache.set(options, candidates);
  return candidates;
}

/**
 * Generate DataTableFilterField[] from a table schema definition.
 *
 * Only includes fields where filter !== null.
 * Order follows schema definition order (JS object key insertion order).
 *
 * Options for checkbox fields are auto-derived from col.enum(values) or
 * col.boolean() if not explicitly provided via filterable("checkbox", { options }).
 */
export function generateFilterFields<TData>(
  schema: TableSchemaDefinition,
): DataTableFilterField<TData>[] {
  const result: DataTableFilterField<TData>[] = [];

  for (const config of resolveColumns(schema)) {
    const { key, filter, label, kind } = config;
    if (!filter) continue;

    const base = {
      label,
      value: key as keyof TData,
      defaultOpen: filter.defaultOpen || undefined,
      commandDisabled: filter.commandDisabled || undefined,
    };

    switch (filter.type) {
      case "input": {
        result.push({ ...base, type: "input" });
        break;
      }
      case "timerange": {
        result.push({
          ...base,
          type: "timerange",
          presets: filter.presets?.map(fromPresetDescriptor),
        });
        break;
      }
      case "checkbox": {
        // Derive options if not explicitly provided
        let options = filter.options;
        if (!options) {
          if (config.kind === "enum") {
            options = config.enumValues.map((v) => ({ label: v, value: v }));
          } else if (kind === "boolean") {
            options = [
              { label: "Yes", value: true },
              { label: "No", value: false },
            ];
          } else if (
            config.kind === "array" &&
            config.arrayItem.kind === "enum"
          ) {
            options = config.arrayItem.enumValues.map((v) => ({
              label: v,
              value: v,
            }));
          }
        }
        result.push({
          ...base,
          type: "checkbox",
          options,
          component:
            config.renderers.filterComponent ??
            defaultFilterComponent(config, options),
        });
        break;
      }
      case "slider": {
        const displayUnit =
          "unit" in config.display ? config.display.unit : undefined;
        result.push({
          ...base,
          type: "slider",
          min: filter.min ?? 0,
          max: filter.max ?? 100,
          unit: filter.unit ?? displayUnit,
        });
        break;
      }
    }
  }

  return result;
}
src/lib/table-schema/generators/filter-schema.ts
import {
  ARRAY_DELIMITER,
  RANGE_DELIMITER,
  SLIDER_DELIMITER,
} from "@/lib/delimiters";
import { createSchema, field } from "@/lib/store/schema";
import type {
  FieldBuilder,
  Schema,
  SchemaDefinition,
} from "@/lib/store/schema";
import { resolveColumns } from "../col";
import type { ColBuilder, TableSchemaDefinition } from "../types";

/**
 * Extract keys from a TableSchemaDefinition where the column is filterable.
 * A column is filterable when its filter type `F` is not `never`.
 * Uses `[F] extends [never]` to avoid distribution issues with `any`.
 */
type FilterableKeys<T extends TableSchemaDefinition> = {
  [K in keyof T]: T[K] extends ColBuilder<infer _T, infer F>
    ? [F] extends [never]
      ? never
      : K
    : never;
}[keyof T];

// Extract T and F separately — TS struggles to infer both at once from ColBuilder
type GetColValue<B> = B extends ColBuilder<infer T, any> ? T : never;
type GetColFilter<B> = B extends ColBuilder<any, infer F> ? F : never;

/**
 * Map a single ColBuilder to the correct FieldBuilder type based on its
 * value type `T` and filter type `F`.
 */
type InferFilterFieldType<B> = [GetColFilter<B>] extends [never]
  ? never
  : GetColFilter<B> extends "input"
    ? GetColValue<B> extends string
      ? FieldBuilder<string | null>
      : GetColValue<B> extends number
        ? FieldBuilder<number | null>
        : FieldBuilder<unknown>
    : GetColFilter<B> extends "slider"
      ? FieldBuilder<(number | null)[]>
      : GetColFilter<B> extends "timerange"
        ? FieldBuilder<(Date | null)[]>
        : GetColFilter<B> extends "checkbox"
          ? GetColValue<B> extends (infer U)[]
            ? FieldBuilder<(U | null)[]>
            : FieldBuilder<(GetColValue<B> | null)[]>
          : FieldBuilder<unknown>;

/** The generated filter definition — preserves the filterable keys from `T`. */
type GeneratedFilterDef<T extends TableSchemaDefinition> = {
  [K in FilterableKeys<T>]: InferFilterFieldType<T[K]>;
};

function buildFilterDefinition(
  schema: TableSchemaDefinition,
): SchemaDefinition {
  const definition: SchemaDefinition = {};

  for (const config of resolveColumns(schema)) {
    const { key, kind, filter } = config;
    if (!filter) continue;

    switch (filter.type) {
      case "input": {
        if (kind === "string") {
          definition[key] = field.string();
        } else if (kind === "number") {
          definition[key] = field.number();
        }
        break;
      }
      case "checkbox": {
        if (config.kind === "enum") {
          definition[key] = field.array(field.stringLiteral(config.enumValues));
        } else if (kind === "number") {
          definition[key] = field
            .array(field.number())
            .delimiter(ARRAY_DELIMITER);
        } else if (kind === "boolean") {
          definition[key] = field
            .array(field.boolean())
            .delimiter(ARRAY_DELIMITER);
        } else if (
          config.kind === "array" &&
          config.arrayItem.kind === "enum"
        ) {
          definition[key] = field.array(
            field.stringLiteral(config.arrayItem.enumValues),
          );
        } else if (config.kind === "array") {
          // Non-enum array item — filter on the item's own scalar type.
          definition[key] =
            config.arrayItem.kind === "number"
              ? field.array(field.number()).delimiter(ARRAY_DELIMITER)
              : field.array(field.string()).delimiter(ARRAY_DELIMITER);
        }
        break;
      }
      case "slider": {
        definition[key] = field
          .array(field.number())
          .delimiter(SLIDER_DELIMITER);
        break;
      }
      case "timerange": {
        definition[key] = field
          .array(field.timestamp())
          .delimiter(RANGE_DELIMITER);
        break;
      }
    }
  }

  return definition;
}

/**
 * Generate a BYOS filter schema from a table schema definition.
 *
 * Each filterable column maps to the appropriate field.* builder:
 * - col.string()  + input    → field.string()
 * - col.number()  + input    → field.number()
 * - col.number()  + slider   → field.array(field.number()).delimiter(SLIDER_DELIMITER)
 * - col.number()  + checkbox → field.array(field.number()).delimiter(ARRAY_DELIMITER)
 * - col.boolean() + checkbox → field.array(field.boolean()).delimiter(ARRAY_DELIMITER)
 * - col.timestamp()+ timerange→ field.array(field.timestamp()).delimiter(RANGE_DELIMITER)
 * - col.enum(v)   + checkbox → field.array(field.stringLiteral(v))
 * - col.array(col.enum(v)) + checkbox → field.array(field.stringLiteral(v))
 *
 * Non-filterable fields are excluded. Pass `extraFields` for pagination,
 * sorting, and other non-filter state.
 *
 * @example
 * ```ts
 * // Without extra fields (filter fields only)
 * const filterSchema = generateFilterSchema(tableSchema.definition);
 *
 * // With extra fields (recommended)
 * const filterSchema = generateFilterSchema(tableSchema.definition, {
 *   sort: field.sort(),
 *   uuid: field.string(),
 *   live: field.boolean().default(false),
 *   size: field.number().default(40),
 * });
 * ```
 */
export function generateFilterSchema<T extends TableSchemaDefinition>(
  schema: T,
): Schema<GeneratedFilterDef<T>>;
export function generateFilterSchema<
  T extends TableSchemaDefinition,
  E extends SchemaDefinition,
>(schema: T, extraFields: E): Schema<GeneratedFilterDef<T> & E>;
export function generateFilterSchema<
  T extends TableSchemaDefinition,
  E extends SchemaDefinition,
>(schema: T, extraFields?: E) {
  const generated = buildFilterDefinition(schema);
  const merged = extraFields ? { ...generated, ...extraFields } : generated;
  return createSchema(merged);
}
src/lib/table-schema/generators/sheet-fields.ts
import type { SheetField } from "@/components/data-table/types";
import { resolveColumns } from "../col";
import type { TableSchemaDefinition } from "../types";

/**
 * Generate SheetField[] from a table schema definition.
 *
 * Only includes fields where sheet !== null (.sheet() was called).
 * Sheet type is derived from the filter type, or "readonly" if not filterable.
 * Sheet label falls back to the column label if not overridden in .sheet({ label }).
 */
export function generateSheetFields<TData>(
  schema: TableSchemaDefinition,
): SheetField<TData>[] {
  const result: SheetField<TData>[] = [];

  for (const config of resolveColumns(schema)) {
    const sheetConfig = config.sheet;
    if (sheetConfig === null) continue;

    // Derive sheet type from filter type, or "readonly" if not filterable
    const sheetType: SheetField<TData>["type"] =
      config.filter?.type ?? "readonly";

    result.push({
      id: config.key as keyof TData,
      label: sheetConfig.label ?? config.label,
      type: sheetType,
      // The descriptor's display is always a real serializable display, even
      // when a custom cell renderer was supplied — no per-kind fallback needed.
      display: config.display,
      component: config.renderers
        .sheetComponent as SheetField<TData>["component"],
      condition: config.renderers
        .sheetCondition as SheetField<TData>["condition"],
      className: sheetConfig.className,
      skeletonClassName: sheetConfig.skeletonClassName,
    });
  }

  return result;
}
src/components/data-table/data-table-auto.tsx
"use client";

import { DataTableFilterCommand } from "@/components/data-table/data-table-filter-command";
import { DataTableInfinite } from "@/components/data-table/data-table-infinite";
import { useDataTable } from "@/components/data-table/data-table-provider";
import { MemoizedDataTableSheetContent } from "@/components/data-table/data-table-sheet/data-table-sheet-content";
import { DataTableSheetDetails } from "@/components/data-table/data-table-sheet/data-table-sheet-details";
import type { SheetField } from "@/components/data-table/types";
import { useMemoryAdapter } from "@/lib/store/adapters/memory";
import { DataTableStoreProvider } from "@/lib/store/provider/DataTableStoreProvider";
import { field } from "@/lib/store/schema";
import type { SchemaDefinition } from "@/lib/store/schema/types";
import {
  createTableSchema,
  generateColumns,
  generateFilterFields,
  generateFilterSchema,
  generateSheetFields,
  getDefaultColumnVisibility,
} from "@/lib/table-schema";
import { inferSchemaFromJSON } from "@/lib/table-schema/infer";
import * as React from "react";

type AutoRow = Record<string, unknown>;

export interface DataTableAutoProps {
  data: AutoRow[];
}

export function DataTableAuto({ data }: DataTableAutoProps) {
  const schemaJson = React.useMemo(() => inferSchemaFromJSON(data), [data]);

  const { definition } = React.useMemo(
    () => createTableSchema.fromJSON(schemaJson),
    [schemaJson],
  );

  const columns = React.useMemo(
    () => generateColumns<AutoRow>(definition),
    [definition],
  );

  const filterFields = React.useMemo(
    () => generateFilterFields<AutoRow>(definition),
    [definition],
  );

  const sheetFields = React.useMemo(
    () => generateSheetFields<AutoRow>(definition),
    [definition],
  );

  const defaultColumnVisibility = React.useMemo(
    () => getDefaultColumnVisibility(definition),
    [definition],
  );

  // Merge `sort` BEFORE createSchema so `defaults` cannot disagree with
  // `definition` — splicing it in afterwards left `definition.sort` present
  // while `defaults.sort` was missing.
  const filterSchema = React.useMemo(
    () => generateFilterSchema(definition, { sort: field.sort() }),
    [definition],
  );

  const adapter = useMemoryAdapter(filterSchema.definition, { id: "auto" });

  return (
    <DataTableStoreProvider adapter={adapter}>
      <DataTableAutoInner
        data={data}
        columns={columns}
        filterFields={filterFields}
        sheetFields={sheetFields}
        defaultColumnVisibility={defaultColumnVisibility}
        schema={filterSchema.definition}
      />
    </DataTableStoreProvider>
  );
}

const noop = () => Promise.resolve();

function DataTableAutoInner({
  data,
  columns,
  filterFields,
  sheetFields,
  defaultColumnVisibility,
  schema,
}: {
  data: AutoRow[];
  columns: React.ComponentProps<typeof DataTableInfinite<AutoRow>>["columns"];
  filterFields: React.ComponentProps<
    typeof DataTableInfinite<AutoRow>
  >["filterFields"];
  sheetFields: SheetField<AutoRow>[];
  defaultColumnVisibility: Record<string, boolean>;
  schema: SchemaDefinition;
}) {
  return (
    <DataTableInfinite
      columns={columns}
      data={data}
      filterFields={filterFields}
      defaultColumnVisibility={defaultColumnVisibility}
      totalRowsFetched={data.length}
      hasNextPage={false}
      fetchNextPage={noop}
      isFetching={false}
      isLoading={false}
      tableId="auto"
      commandSlot={<DataTableFilterCommand schema={schema} tableId="auto" />}
      sheetSlot={
        <AutoSheetSlot sheetFields={sheetFields} totalRows={data.length} />
      }
      footerSlot={
        <div className="text-muted-foreground text-sm">
          powered by{" "}
          <a
            href="https://openstatus.dev"
            target="_blank"
            rel="noreferrer"
            className="text-foreground hover:text-primary"
          >
            openstatus.dev
          </a>
        </div>
      }
    />
  );
}

function AutoSheetSlot({
  sheetFields: fields,
  totalRows,
}: {
  sheetFields: SheetField<AutoRow>[];
  totalRows: number;
}) {
  const { table, rowSelection, isLoading, filterFields } = useDataTable<
    AutoRow,
    unknown
  >();
  const selectedRowKey = Object.keys(rowSelection)?.[0];
  const selectedRow = React.useMemo(() => {
    if (isLoading && !selectedRowKey) return undefined;
    return table
      .getCoreRowModel()
      .flatRows.find((row) => row.id === selectedRowKey);
  }, [selectedRowKey, isLoading, table]);

  return (
    <DataTableSheetDetails
      title={
        selectedRow ? String(Object.values(selectedRow.original)[0] ?? "") : ""
      }
      titleClassName="font-mono"
    >
      <MemoizedDataTableSheetContent
        table={table}
        data={selectedRow?.original}
        filterFields={filterFields}
        fields={fields}
        metadata={{
          totalRows,
          filterRows: totalRows,
          totalRowsFetched: totalRows,
        }}
      />
    </DataTableSheetDetails>
  );
}
src/hooks/use-table-manifest.ts
"use client";

import {
  fetchTableManifest,
  type FetchManifestOptions,
  type TableManifest,
} from "@/lib/table-schema";
import { useQuery, type UseQueryResult } from "@tanstack/react-query";

/**
 * Load a table's manifest.
 *
 * ## Why this is separate from the data query
 *
 * The manifest cannot be fetched alongside the rows, because everything that
 * fetches rows is *built from* it: the columns, the filter fields, and — the
 * binding constraint — the URL-state adapter, whose parsers are derived from
 * the schema. `useNuqsAdapter(schema, …)` needs a schema at first render, and a
 * hook cannot be called conditionally, so the manifest has to resolve one level
 * up from anything that consumes it.
 *
 * That is why the headless table is two components: an outer one that resolves
 * the manifest, and an inner one that is only mounted once it has. This hook is
 * the outer half.
 *
 * ## Avoiding the waterfall
 *
 * A round trip for the manifest before the first row request is a real cost.
 * Two ways out, both supported by `initialManifest`:
 *
 * - **Snapshot at build time.** Check the manifest into the app and pass it.
 *   The query still revalidates in the background, so a schema change on the
 *   server is picked up without a redeploy, but nothing blocks on it.
 * - **Prefetch on the server.** `fetchTableManifest` on the server component and
 *   hand the result down, or seed the React Query cache under `tableManifestKey`.
 *
 * With `initialManifest` the hook never suspends and the table renders on the
 * first paint.
 */

/** The query key a manifest is cached under. Exported so a server can seed it. */
export function tableManifestKey(endpoint: string) {
  return ["data-table-manifest", endpoint] as const;
}

export type UseTableManifestOptions = FetchManifestOptions & {
  /**
   * A manifest that is already known — a build-time snapshot, or one prefetched
   * on the server. Used as the initial value; the query still revalidates.
   */
  initialManifest?: TableManifest;
  /**
   * How long a manifest stays fresh. Schemas change on deploy, not per request,
   * so this is long by default.
   */
  staleTime?: number;
  /** Set false to render only from `initialManifest` and never fetch. */
  enabled?: boolean;
  /**
   * Retries before the query gives up. Defaults to 1: a malformed manifest is
   * not transient — retrying cannot repair the endpoint's answer — but a single
   * retry still covers a dropped connection.
   */
  retry?: number | boolean;
};

const FIVE_MINUTES = 1000 * 60 * 5;

export function useTableManifest(
  endpoint: string,
  options?: UseTableManifestOptions,
): UseQueryResult<TableManifest, Error> {
  const {
    initialManifest,
    staleTime = FIVE_MINUTES,
    enabled = true,
    retry = 1,
    ...fetchOptions
  } = options ?? {};

  return useQuery<TableManifest, Error>({
    queryKey: tableManifestKey(endpoint),
    queryFn: ({ signal }) =>
      fetchTableManifest(endpoint, { ...fetchOptions, signal }),
    ...(initialManifest ? { initialData: initialManifest } : {}),
    enabled,
    staleTime,
    retry,
    refetchOnWindowFocus: false,
  });
}

More from Data Table Filters

Browse all components in this library →