{"version":3,"file":"query.cjs","names":["FILTER_TREE_PARAM","FILTER_TREE_VERSION","VERSION_PREFIX","isActiveFilterTree","tree","conditions","length","isCombinator","raw","sanitizeCondition","undefined","node","key","op","value","sanitizeGroup","combinator","Array","isArray","child","group","push","condition","parseFilterTree","startsWith","JSON","parse","slice","serializeFilterTree","stringify","MAX_FORMULA_COLUMNS","decodeField","value","decodeURIComponent","serializeFormulaColumns","specs","parts","spec","key","trim","formula","head","encodeURIComponent","header","push","undefined","slice","join","deserializeFormulaColumns","raw","seen","Set","part","split","fields","length","has","add","PIVOT_ZONES","EMPTY_PIVOT_CONFIG","rows","columns","measures","availableFields","fields","config","used","Set","filter","field","has","key","placed","list","index","without","existing","at","Math","min","max","length","slice","assignField","zone","Number","MAX_SAFE_INTEGER","splice","agg","other","removeField","_","i","moveField","delta","target","moved","keys","undefined","setMeasureAgg","map","measure","isPivotReady","measureLabel","label","find","candidate","name","EMPTY_PIVOT_CONFIG","PATH_SEP","AGGREGATIONS","ROWS","COLUMNS","SUBTOTALS","GRAND_TOTALS","COLLAPSED","OFF","COLLAPSED_PATH_SEP","COLLAPSED_SEP","NOTHING_COLLAPSED","isAggregateName","value","includes","decodeField","decodeURIComponent","encodeCollapsed","key","split","map","encodeURIComponent","join","decodeCollapsed","serializePivotState","state","config","collapsed","parts","rows","length","push","columns","measure","measures","agg","subtotals","grandTotals","folded","filter","deserializePivotState","raw","part","at","indexOf","head","slice","body","undefined","serializePivot","deserializePivot","devWarn","isFilterGroup","node","REQUIRES","groupBy","expandedIds","aggregates","filterTree","facets","cursor","REMEDY","grouping","tree","applyQuerySupport","extensions","support","warn","allowed","field","value","Object","entries","undefined","capability","assign"],"sources":["../src/filters/filterTreeCodec.ts","../src/formula/formulaUrlCodec.ts","../src/pivot/pivotConfigModel.ts","../src/pivot/pivotUrlCodec.ts","../src/source/queryContract.ts"],"sourcesContent":["/**\n * Versioned AND/OR filter-tree encoding (`ft=1.{…}`).\n * Kept free of predicates so the URL layer does not pull the filter engine\n * into a table that never declares filters.\n */\nimport type { QueryCondition, QueryFilterGroup } from \"../source/queryContract\";\n\n/** URL param for the versioned tree (`ft=1.{…}`). */\nexport const FILTER_TREE_PARAM = \"ft\";\n\n/** Current encoding version. Unknown versions are dropped, never reinterpreted. */\nexport const FILTER_TREE_VERSION = 1;\n\nconst VERSION_PREFIX = `${FILTER_TREE_VERSION}.`;\n\n/** True when a tree has at least one condition (nested groups count). */\nexport function isActiveFilterTree(\n  tree: QueryFilterGroup | undefined\n): tree is QueryFilterGroup {\n  return tree != null && tree.conditions.length > 0;\n}\n\nfunction isCombinator(raw: unknown): raw is QueryFilterGroup[\"combinator\"] {\n  return raw === \"and\" || raw === \"or\";\n}\n\nfunction sanitizeCondition(raw: unknown): QueryCondition | undefined {\n  if (raw == null || typeof raw !== \"object\") return undefined;\n  const node = raw as Record<string, unknown>;\n  if (typeof node.key !== \"string\" || node.key === \"\") return undefined;\n  if (typeof node.op !== \"string\" || node.op === \"\") return undefined;\n  return { key: node.key, op: node.op, value: node.value };\n}\n\nfunction sanitizeGroup(raw: unknown): QueryFilterGroup | undefined {\n  if (raw == null || typeof raw !== \"object\") return undefined;\n  const node = raw as Record<string, unknown>;\n  if (!isCombinator(node.combinator)) return undefined;\n  if (!Array.isArray(node.conditions)) return undefined;\n  const conditions: (QueryCondition | QueryFilterGroup)[] = [];\n  for (const child of node.conditions) {\n    const group = sanitizeGroup(child);\n    if (group) {\n      conditions.push(group);\n      continue;\n    }\n    const condition = sanitizeCondition(child);\n    if (condition) conditions.push(condition);\n  }\n  return { combinator: node.combinator, conditions };\n}\n\n/**\n * Parse a stored `ft` value. Missing, malformed, or unknown-version\n * strings return `undefined` so an old or hand-edited link never\n * silently becomes a different query.\n */\nexport function parseFilterTree(\n  raw: string | null | undefined\n): QueryFilterGroup | undefined {\n  if (raw == null || raw === \"\") return undefined;\n  if (!raw.startsWith(VERSION_PREFIX)) return undefined;\n  try {\n    return sanitizeGroup(JSON.parse(raw.slice(VERSION_PREFIX.length)));\n  } catch {\n    return undefined;\n  }\n}\n\n/** Encode a tree for the URL. Empty / undefined trees omit the param. */\nexport function serializeFilterTree(\n  tree: QueryFilterGroup | undefined\n): string | undefined {\n  if (!isActiveFilterTree(tree)) return undefined;\n  return `${VERSION_PREFIX}${JSON.stringify(tree)}`;\n}\n","/**\n * Formula columns as a URL parameter — the encoding on its own, without the\n * hook that keeps it in sync.\n *\n * A typed formula is the most expensive table state there is to reproduce by\n * hand, because it is not a choice among things the table offers: it is text\n * somebody wrote. It sits in the URL beside sort, filters and the pivot for\n * exactly that reason, and a [saved view](../url/useSavedViews.ts) captures it\n * with the rest.\n *\n * The encoding is one entry per column, `key:formula` (plus `:header` when the\n * header is not the key), entries joined by `;`, every field percent-encoded so\n * a formula may itself contain the delimiters:\n * `formula=total:quantity%20*%20unitPrice:Total`.\n *\n * **Nothing here parses or evaluates a formula.** Reading a URL produces\n * `FormulaColumnSpec`s and stops — the text stays text until the engine is\n * asked for a value, which is the same rule the parser exists to keep. A codec\n * that \"checked\" a formula by running it would have handed the page to whoever\n * sent the link, in the one place that is easiest to do and hardest to notice.\n *\n * A `format` function cannot travel: a function has no URL form. The entry\n * still travels — the formula computes the same value either way, so the link\n * loses the presentation and keeps the column, unlike a pivot's custom\n * aggregator, where the function IS the computation.\n *\n * The codec lives apart from {@link ./useFormulaUrlState} because the two ends\n * of a shared link do not run in the same place: the table writes the parameter\n * in a browser, and a route handler reads it in Node. Keeping the reading half\n * free of React is what lets `@adapttable/core/query` decode the same string a\n * backend never renders.\n */\nimport type { FormulaColumnSpec } from \"./formulaColumn\";\n\n/**\n * How many formula columns one URL may describe.\n *\n * A URL is hostile input, and a formula column is work per row rather than a\n * flag: a hand-edited parameter naming two hundred of them would be a page\n * that renders once, slowly, for no reason anyone asked for. The limit is the\n * same kind of clamp the column-layout codec puts on a width.\n */\nconst MAX_FORMULA_COLUMNS = 24;\n\n/**\n * Decode one field, tolerating the malformed input a hand-edited URL brings.\n *\n * Local rather than shared with `url/serialize`: this module deliberately\n * imports nothing at runtime, because it is one of the two files a backend can\n * read a shared link with in a process where React is not installed.\n */\nfunction decodeField(value: string): string {\n  try {\n    return decodeURIComponent(value);\n  } catch {\n    return value;\n  }\n}\n\n/**\n * Write formula columns as a URL parameter value.\n *\n * @param specs - The columns to serialize, in the order to show them.\n * @returns The parameter value, or `\"\"` when there is nothing to say.\n */\nexport function serializeFormulaColumns(\n  specs: readonly FormulaColumnSpec[]\n): string {\n  const parts: string[] = [];\n  for (const spec of specs) {\n    const key = spec.key.trim();\n    const formula = spec.formula.trim();\n    // A column with no key or no formula is not a column. Writing it would\n    // produce a link that reads back as one entry fewer than it looks.\n    if (key === \"\" || formula === \"\") continue;\n    const head = `${encodeURIComponent(key)}:${encodeURIComponent(formula)}`;\n    const header = spec.header?.trim();\n    parts.push(\n      header === undefined || header === \"\" || header === key\n        ? head\n        : `${head}:${encodeURIComponent(header)}`\n    );\n  }\n  return parts.slice(0, MAX_FORMULA_COLUMNS).join(\";\");\n}\n\n/**\n * Read formula columns back from a URL parameter value.\n *\n * A malformed entry is dropped rather than thrown: a URL is user input, and a\n * hand-edited one should degrade to the columns it still describes instead of\n * an error page. The formula text is carried through untouched and unparsed.\n *\n * @param raw - The parameter value.\n * @returns The columns it describes, in order, each key appearing once.\n */\nexport function deserializeFormulaColumns(\n  raw: string | null\n): FormulaColumnSpec[] {\n  if (!raw) return [];\n  const specs: FormulaColumnSpec[] = [];\n  const seen = new Set<string>();\n  for (const part of raw.split(\";\")) {\n    const fields = part.split(\":\");\n    // Two fields, or three with a header. Anything else is an entry whose\n    // delimiters were not written by this codec, and guessing which field is\n    // the formula is how a link starts computing something else.\n    if (fields.length < 2 || fields.length > 3) continue;\n    const key = decodeField(fields[0] ?? \"\").trim();\n    const formula = decodeField(fields[1] ?? \"\").trim();\n    if (key === \"\" || formula === \"\") continue;\n    // First entry wins: two columns under one key is one column shadowing the\n    // other, and which one won would depend on render order.\n    if (seen.has(key)) continue;\n    seen.add(key);\n    const header = fields.length === 3 ? decodeField(fields[2]!).trim() : \"\";\n    specs.push({ key, formula, ...(header === \"\" ? {} : { header }) });\n    if (specs.length === MAX_FORMULA_COLUMNS) break;\n  }\n  return specs;\n}\n","/**\n * Editing a pivot configuration: which field sits on which axis, in what\n * order, and what each measure computes.\n *\n * This is the part of the configuration UI that is not a widget. Every kit\n * draws the panel differently, but \"move Team from Available to Rows, above\n * Region\" is one answer everywhere — so it is decided here, once, and the\n * adapters are left with buttons.\n *\n * The operations are deliberately total: moving a field that is already on\n * an axis takes it off the old one rather than duplicating it, moving to an\n * index past the end appends, and moving a field that does not exist changes\n * nothing. A configuration panel whose buttons can produce an invalid pivot\n * is a panel that will produce one.\n *\n * Measures are the exception to \"a field lives on one axis\": the same column\n * can be summed and counted in the same pivot, so measures are keyed by\n * their position rather than by their column.\n */\nimport type { AggregateName } from \"../aggregate/aggregate\";\nimport type { PivotConfig, PivotMeasure } from \"./pivotModel\";\n\n/** Where a field can sit. */\nexport type PivotZone = \"rows\" | \"columns\" | \"measures\";\n\n/** The zones a field can be moved between, in panel order. */\nexport const PIVOT_ZONES: readonly PivotZone[] = [\n  \"rows\",\n  \"columns\",\n  \"measures\",\n];\n\n/** A field the user can put on an axis. */\nexport interface PivotField {\n  /** The column key. */\n  key: string;\n  /** What to call it in the panel. */\n  label: string;\n}\n\n/** An empty configuration — nothing on any axis. */\nexport const EMPTY_PIVOT_CONFIG: PivotConfig = {\n  rows: [],\n  columns: [],\n  measures: [],\n};\n\n/** The fields not yet used on either axis. */\nexport function availableFields(\n  fields: readonly PivotField[],\n  config: PivotConfig\n): PivotField[] {\n  const used = new Set([...config.rows, ...config.columns]);\n  return fields.filter((field) => !used.has(field.key));\n}\n\n/** Move a key within a list, or insert it, returning a new list. */\nfunction placed(list: readonly string[], key: string, index: number): string[] {\n  const without = list.filter((existing) => existing !== key);\n  const at = Math.min(Math.max(index, 0), without.length);\n  return [...without.slice(0, at), key, ...without.slice(at)];\n}\n\n/**\n * Put a field on an axis.\n *\n * @param config - The configuration to change.\n * @param key - The column key to place.\n * @param zone - Where it should go.\n * @param index - Position within that zone. Past the end appends.\n * @returns A new configuration.\n */\nexport function assignField(\n  config: PivotConfig,\n  key: string,\n  zone: PivotZone,\n  index = Number.MAX_SAFE_INTEGER\n): PivotConfig {\n  if (zone === \"measures\") {\n    // A measure is not an axis slot: the same column can be summed and\n    // counted at once, so this adds rather than moves.\n    const measures = [...config.measures];\n    const at = Math.min(Math.max(index, 0), measures.length);\n    measures.splice(at, 0, { key, agg: \"sum\" });\n    return { ...config, measures };\n  }\n  // A dimension lives on one axis only — placing it on the other takes it\n  // off the first, rather than pivoting the same field twice.\n  const other = zone === \"rows\" ? \"columns\" : \"rows\";\n  return {\n    ...config,\n    [zone]: placed(config[zone], key, index),\n    [other]: config[other].filter((existing) => existing !== key),\n  };\n}\n\n/**\n * Take a field off an axis.\n *\n * @param config - The configuration to change.\n * @param zone - Which axis to remove from.\n * @param index - The position to remove.\n * @returns A new configuration.\n */\nexport function removeField(\n  config: PivotConfig,\n  zone: PivotZone,\n  index: number\n): PivotConfig {\n  if (zone === \"measures\") {\n    return {\n      ...config,\n      measures: config.measures.filter((_, i) => i !== index),\n    };\n  }\n  return { ...config, [zone]: config[zone].filter((_, i) => i !== index) };\n}\n\n/**\n * Move a field one step within its zone.\n *\n * The keyboard counterpart of dragging. A step past either end is a no-op\n * rather than a wrap: wrapping makes the last press of a held key undo the\n * whole journey.\n *\n * @param config - The configuration to change.\n * @param zone - Which axis the field is on.\n * @param index - Its current position.\n * @param delta - `-1` to move it out one level, `1` to move it in.\n * @returns A new configuration.\n */\nexport function moveField(\n  config: PivotConfig,\n  zone: PivotZone,\n  index: number,\n  delta: -1 | 1\n): PivotConfig {\n  const target = index + delta;\n  const list: readonly unknown[] =\n    zone === \"measures\" ? config.measures : config[zone];\n  if (index < 0 || index >= list.length) return config;\n  if (target < 0 || target >= list.length) return config;\n  if (zone === \"measures\") {\n    const measures = [...config.measures];\n    const [moved] = measures.splice(index, 1);\n    if (moved) measures.splice(target, 0, moved);\n    return { ...config, measures };\n  }\n  const keys = [...config[zone]];\n  const [moved] = keys.splice(index, 1);\n  if (moved !== undefined) keys.splice(target, 0, moved);\n  return { ...config, [zone]: keys };\n}\n\n/**\n * Change what a measure computes.\n *\n * @param config - The configuration to change.\n * @param index - Which measure.\n * @param agg - The new aggregation.\n * @returns A new configuration.\n */\nexport function setMeasureAgg(\n  config: PivotConfig,\n  index: number,\n  agg: AggregateName\n): PivotConfig {\n  return {\n    ...config,\n    measures: config.measures.map((measure, i) =>\n      i === index ? { ...measure, agg } : measure\n    ),\n  };\n}\n\n/**\n * Whether a configuration can actually be rendered.\n *\n * A pivot with no measure has nothing to put in its cells, which is a\n * half-built configuration rather than an error — the panel shows it and the\n * table waits.\n */\nexport function isPivotReady(config: PivotConfig): boolean {\n  return config.measures.length > 0;\n}\n\n/** The display label for one measure, for the panel and the column header. */\nexport function measureLabel(\n  measure: PivotMeasure,\n  fields: readonly PivotField[]\n): string {\n  if (measure.label !== undefined) return measure.label;\n  const field = fields.find((candidate) => candidate.key === measure.key);\n  const name = field?.label ?? measure.key;\n  return typeof measure.agg === \"string\" ? `${measure.agg} ${name}` : name;\n}\n","/**\n * The pivot state as a URL parameter — the encoding on its own, without the\n * hook that keeps it in sync.\n *\n * A pivot is the most expensive table state there is to rebuild by hand —\n * two axes, an order on each, and a measure list — which makes it the state\n * most worth putting in a link. It sits alongside sort, filters and column\n * layout for exactly the reason those do.\n *\n * The serialization is compact and readable rather than JSON-in-a-parameter:\n * `pivot=rows:region,team;cols:quarter;sum:amount`. A URL someone might read\n * or hand-edit should look like something, and the round trip is tested\n * rather than assumed.\n *\n * What travels is everything a reader can change: the two axes, the measures,\n * whether subtotals and grand totals are shown (`sub:0`, `grand:0`), and which\n * groups are folded (`hide:EU/Alpha`). A link that carried the axes and dropped\n * the rest would reopen showing numbers its sender had switched off, or lines\n * they had folded away, which is a different table from the one they sent.\n *\n * Only the departures are written. Subtotals and grand totals default to on, so\n * a parameter says so by staying silent about them — the same rule the density\n * and column-layout parameters follow. That is also what makes the encoding\n * backward compatible: a link or a saved view from before these fields existed\n * says nothing about them and reads back exactly as it always did.\n *\n * Custom aggregators cannot be serialized — a function has no URL form. A\n * configuration carrying one keeps working in memory and simply does not write\n * that measure to the URL, because a link that silently turned a custom\n * aggregation into `sum` would be worse than a link that omits it.\n *\n * The codec lives apart from {@link usePivotUrlState} because the two ends of\n * that link do not run in the same place: the table writes the parameter in a\n * browser, and a route handler reads it in Node. Keeping the reading half free\n * of React is what lets `@adapttable/core/query` — and `@adapttable/server`\n * through it — decode the same string a backend never renders.\n */\nimport type { AggregateName } from \"../aggregate/aggregate\";\nimport { EMPTY_PIVOT_CONFIG } from \"./pivotConfigModel\";\nimport { PATH_SEP } from \"./pivotKeys\";\nimport type { PivotConfig, PivotMeasure } from \"./pivotModel\";\n\nconst AGGREGATIONS: readonly AggregateName[] = [\n  \"sum\",\n  \"avg\",\n  \"count\",\n  \"min\",\n  \"max\",\n];\n\n/** The segment heads that are not an aggregation. */\nconst ROWS = \"rows\";\nconst COLUMNS = \"cols\";\nconst SUBTOTALS = \"sub\";\nconst GRAND_TOTALS = \"grand\";\nconst COLLAPSED = \"hide\";\n\n/** What a flag that is off looks like. On is written by saying nothing. */\nconst OFF = \"0\";\n\n/** Between the values of one folded path, once they are percent-encoded. */\nconst COLLAPSED_PATH_SEP = \"/\";\n/** Between one folded path and the next. */\nconst COLLAPSED_SEP = \",\";\n\n/** Nothing folded, with a stable identity so a read cannot churn a memo. */\nconst NOTHING_COLLAPSED: readonly string[] = [];\n\n/** Everything the pivot parameter carries. */\nexport interface PivotUrlState {\n  /** What to pivot, and how. */\n  config: PivotConfig;\n  /**\n   * The keys of the folded subtotal lines — a `PivotRow.key`, which is what\n   * `pivot`'s `collapsed` option matches against.\n   */\n  collapsed: readonly string[];\n}\n\n/** Whether a string names a built-in aggregation. */\nfunction isAggregateName(value: string): value is AggregateName {\n  return (AGGREGATIONS as readonly string[]).includes(value);\n}\n\n/**\n * Decode one field, tolerating the malformed input a hand-edited URL brings.\n *\n * Local rather than shared with `url/serialize`: this module is one of the few a\n * backend reads a shared link with in a process where React is not installed,\n * and it stays that light by importing almost nothing.\n */\nfunction decodeField(value: string): string {\n  try {\n    return decodeURIComponent(value);\n  } catch {\n    return value;\n  }\n}\n\n/**\n * A folded path in URL form: `EU/Alpha`.\n *\n * A collapse key is dimension values joined by a control character, and those\n * values are user data — a team can be called \"A/B\" or \"Q1,Q2\". Each value is\n * percent-encoded, which escapes both the separators this grammar reserves and\n * the control character itself, so nothing in a label can split a path in the\n * wrong place.\n */\nfunction encodeCollapsed(key: string): string {\n  return key.split(PATH_SEP).map(encodeURIComponent).join(COLLAPSED_PATH_SEP);\n}\n\n/** The collapse key a URL path names. Inverse of {@link encodeCollapsed}. */\nfunction decodeCollapsed(value: string): string {\n  return value.split(COLLAPSED_PATH_SEP).map(decodeField).join(PATH_SEP);\n}\n\n/**\n * Write the whole pivot state as a URL parameter value.\n *\n * @param state - The configuration, and which groups are folded.\n * @returns The parameter value, or `\"\"` when there is nothing to say.\n */\nexport function serializePivotState(state: PivotUrlState): string {\n  const { config, collapsed } = state;\n  const parts: string[] = [];\n  if (config.rows.length > 0) parts.push(`${ROWS}:${config.rows.join(\",\")}`);\n  if (config.columns.length > 0) {\n    parts.push(`${COLUMNS}:${config.columns.join(\",\")}`);\n  }\n  for (const measure of config.measures) {\n    // A function has no URL form. Omitting it beats writing `sum` and\n    // quietly changing what the link computes.\n    if (typeof measure.agg !== \"string\") continue;\n    parts.push(`${measure.agg}:${measure.key}`);\n  }\n  // Nothing on either axis and nothing to compute is not a pivot, so it writes\n  // no parameter at all — and it has no groups to fold, which is why the folded\n  // set goes with it rather than lingering as the only thing in the link.\n  if (parts.length === 0) return \"\";\n  if (config.subtotals === false) parts.push(`${SUBTOTALS}:${OFF}`);\n  if (config.grandTotals === false) parts.push(`${GRAND_TOTALS}:${OFF}`);\n  const folded = collapsed\n    .filter((key) => key !== \"\")\n    .map((key) => encodeCollapsed(key));\n  if (folded.length > 0) {\n    parts.push(`${COLLAPSED}:${folded.join(COLLAPSED_SEP)}`);\n  }\n  return parts.join(\";\");\n}\n\n/**\n * Read the whole pivot state back from a URL parameter value.\n *\n * Unknown segments are ignored rather than throwing: a URL is user input,\n * and a hand-edited one should degrade to a simpler pivot instead of an\n * error page.\n *\n * @param raw - The parameter value.\n * @returns The configuration it describes, and which groups are folded.\n */\nexport function deserializePivotState(raw: string | null): PivotUrlState {\n  if (!raw) return { config: EMPTY_PIVOT_CONFIG, collapsed: NOTHING_COLLAPSED };\n  let rows: readonly string[] = [];\n  let columns: readonly string[] = [];\n  let collapsed: readonly string[] = NOTHING_COLLAPSED;\n  let subtotals: boolean | undefined;\n  let grandTotals: boolean | undefined;\n  const measures: PivotMeasure[] = [];\n  for (const part of raw.split(\";\")) {\n    const at = part.indexOf(\":\");\n    if (at < 0) continue;\n    const head = part.slice(0, at);\n    const body = part.slice(at + 1);\n    if (body === \"\") continue;\n    if (head === ROWS) rows = body.split(\",\");\n    else if (head === COLUMNS) columns = body.split(\",\");\n    else if (head === SUBTOTALS) subtotals = body !== OFF;\n    else if (head === GRAND_TOTALS) grandTotals = body !== OFF;\n    else if (head === COLLAPSED) {\n      collapsed = body\n        .split(COLLAPSED_SEP)\n        // An empty entry — `hide:,EU` from a hand-edited URL — names no group.\n        .filter((key) => key !== \"\")\n        .map((key) => decodeCollapsed(key));\n    } else if (isAggregateName(head)) measures.push({ key: body, agg: head });\n  }\n  return {\n    config: {\n      rows,\n      columns,\n      measures,\n      // Absent rather than `true`: a parameter that said nothing about them is a\n      // parameter that leaves the engine's own defaults in charge.\n      ...(subtotals === undefined ? {} : { subtotals }),\n      ...(grandTotals === undefined ? {} : { grandTotals }),\n    },\n    collapsed,\n  };\n}\n\n/**\n * Write a configuration as a URL parameter value.\n *\n * @param config - The configuration to serialize.\n * @returns The parameter value, or `\"\"` when there is nothing to say.\n */\nexport function serializePivot(config: PivotConfig): string {\n  return serializePivotState({ config, collapsed: NOTHING_COLLAPSED });\n}\n\n/**\n * Read a configuration back from a URL parameter value.\n *\n * @param raw - The parameter value.\n * @returns The configuration it describes.\n */\nexport function deserializePivot(raw: string | null): PivotConfig {\n  return deserializePivotState(raw).config;\n}\n","/**\n * The query contract — the shape of everything the table can ask a server for,\n * defined once so the features that arrive later never have to change it.\n *\n * Every field here is optional, on both sides of the wire. A server that\n * ignores all of them behaves exactly as it does today; a table whose source\n * declares no support asks for nothing new. That is the whole design: adding\n * grouping, aggregates, filter trees, facets or cursors to this contract can\n * never break an integration that predates them.\n *\n * A source declares what it can answer through {@link QuerySupport}. The table\n * checks that before it asks, and says so in development when the UI wants\n * something the source did not sign up for — silence there would look like a\n * bug in the table rather than a gap in the integration.\n */\nimport { devWarn } from \"../utils/devWarn\";\n\n/**\n * What a server source can answer, beyond the baseline every source handles\n * (page, limit, search, sort, filters).\n *\n * Omit the whole object — or any field — and the table treats that capability\n * as unavailable: it keeps the work on the frontend where it can, and warns in\n * development where it cannot.\n */\nexport interface QuerySupport {\n  /** Grouping keys reach the server, which returns group rows. */\n  grouping?: boolean;\n  /**\n   * The open tree nodes reach the server, which returns the rows of the\n   * hierarchy that are visible — the roots, plus the children of every node\n   * the reader has opened. The only correct place for a large tree: a browser\n   * holding one page cannot know what is under a branch it has never seen.\n   */\n  tree?: boolean;\n  /** Aggregate requests reach the server, which computes the values. */\n  aggregates?: boolean;\n  /** The nested AND/OR condition tree reaches the server. */\n  filterTree?: boolean;\n  /** Distinct-value counts per column reach the server. */\n  facets?: boolean;\n  /** Pagination is by opaque cursor rather than page number. */\n  cursor?: boolean;\n}\n\n/** The aggregate functions every implementation is expected to understand. */\nexport type AggregateFn = \"sum\" | \"avg\" | \"count\" | \"min\" | \"max\";\n\n/**\n * One aggregate the table wants computed for a column.\n *\n * `fn` accepts any string so a backend can expose its own aggregations\n * (`\"median\"`, `\"p95\"`) without waiting for this type to name them, while the\n * five standard ones still autocomplete.\n */\nexport interface QueryAggregate {\n  /** Column key to aggregate. */\n  key: string;\n  /** Aggregate function name. */\n  fn: AggregateFn | (string & {});\n}\n\n/** A single condition in a filter tree: one column, one operator, one value. */\nexport interface QueryCondition {\n  /** Column or filter key. */\n  key: string;\n  /**\n   * Operator id — `\"eq\"`, `\"contains\"`, `\"between\"`, … The operator set is\n   * owned by the filtering work; this contract only carries it.\n   */\n  op: string;\n  /** Operand, if the operator takes one. `\"empty\"` and friends do not. */\n  value?: unknown;\n}\n\n/**\n * A node in the filter tree: conditions combined with one operator, nestable\n * so `(a AND b) OR c` is expressible.\n */\nexport interface QueryFilterGroup {\n  /** How this group's children combine. */\n  combinator: \"and\" | \"or\";\n  /** Conditions and nested groups, in the order the user built them. */\n  conditions: readonly (QueryCondition | QueryFilterGroup)[];\n}\n\n/** Narrows a filter-tree child to a nested group. */\nexport function isFilterGroup(\n  node: QueryCondition | QueryFilterGroup\n): node is QueryFilterGroup {\n  return \"combinator\" in node;\n}\n\n/**\n * Everything the table can additionally ask for, all optional. These ride on\n * {@link TableQuery} — see that type for the baseline fields.\n */\nexport interface QueryExtensions {\n  /**\n   * Grouping column keys, outermost first. A single-level grouping sends one\n   * key; the field is an array so multi-level grouping needs no new field.\n   */\n  groupBy?: readonly string[];\n  /** Aggregates to compute — per group when grouping, else over the result set. */\n  aggregates?: readonly QueryAggregate[];\n  /**\n   * The nested condition tree, when the UI has built one. The flat `filters`\n   * bag stays populated alongside it for servers that only read that.\n   */\n  filterTree?: QueryFilterGroup;\n  /** Column keys needing distinct-value counts for their filter UI. */\n  facets?: readonly string[];\n  /** Opaque cursor from the previous response, when paginating by cursor. */\n  cursor?: string;\n  /**\n   * The ids of the tree nodes the reader has open, so the server can return\n   * their children with the page. Empty means a folded tree — the roots and\n   * nothing else.\n   */\n  expandedIds?: readonly string[];\n}\n\n/** The capability each extension field needs before the table will send it. */\nconst REQUIRES: Record<keyof QueryExtensions, keyof QuerySupport> = {\n  groupBy: \"grouping\",\n  expandedIds: \"tree\",\n  aggregates: \"aggregates\",\n  filterTree: \"filterTree\",\n  facets: \"facets\",\n  cursor: \"cursor\",\n};\n\n/** Human-readable reason, so a warning tells the reader what to do next. */\nconst REMEDY: Record<keyof QuerySupport, string> = {\n  grouping: \"return group rows for `query.groupBy`\",\n  tree: \"return the children of every node in `query.expandedIds`\",\n  aggregates: \"compute `query.aggregates`\",\n  filterTree: \"evaluate `query.filterTree`\",\n  facets: \"return counts for `query.facets`\",\n  cursor: \"page by `query.cursor` instead of `query.page`\",\n};\n\n/**\n * Keep only the extensions the source declared support for, and say something\n * in development about the ones dropped.\n *\n * The table asks for what the UI is showing; the source decides what it can\n * answer. Where those disagree the field is omitted rather than sent and\n * ignored — a server should never receive a field it never agreed to read.\n */\nexport function applyQuerySupport(\n  extensions: QueryExtensions,\n  support: QuerySupport | undefined,\n  { warn = true }: { warn?: boolean } = {}\n): QueryExtensions {\n  const allowed: QueryExtensions = {};\n  for (const [field, value] of Object.entries(extensions) as [\n    keyof QueryExtensions,\n    QueryExtensions[keyof QueryExtensions],\n  ][]) {\n    if (value === undefined) continue;\n    const capability = REQUIRES[field];\n    if (support?.[capability]) {\n      Object.assign(allowed, { [field]: value });\n    } else if (warn) {\n      // devWarn dedupes by message, so this fires once per capability per\n      // session however often the query changes.\n      devWarn(\n        `The table wants to send \\`${field}\\`, but this source does not declare ` +\n          `\\`supports.${capability}\\`. The field is omitted. Set it once your ` +\n          `endpoint can ${REMEDY[capability]}.`\n      );\n    }\n  }\n  return allowed;\n}\n"],"mappings":"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"}