@pythia-software/query-table-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.d.ts +422 -0
- package/dist/index.js +859 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 query-table contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @pythia-software/query-table-core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic query state, schema contracts, filtering, sorting,
|
|
4
|
+
aggregation, serialization, and transport/storage interfaces for query-table.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @pythia-software/query-table-core
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Example
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import {
|
|
16
|
+
EMPTY_QUERY,
|
|
17
|
+
loadSchema,
|
|
18
|
+
normalizeQueryState,
|
|
19
|
+
toServerQuery,
|
|
20
|
+
} from "@pythia-software/query-table-core";
|
|
21
|
+
|
|
22
|
+
const schema = loadSchema({
|
|
23
|
+
name: "orders",
|
|
24
|
+
idField: "id",
|
|
25
|
+
fields: [
|
|
26
|
+
{
|
|
27
|
+
name: "id",
|
|
28
|
+
label: "Order",
|
|
29
|
+
type: "number",
|
|
30
|
+
bindings: { postgres: { expr: "o.id" } },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const query = normalizeQueryState({ ...EMPTY_QUERY, limit: 50 });
|
|
36
|
+
const request = toServerQuery(query, schema);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Query state is bounded whenever it crosses the library's URL, storage, or
|
|
40
|
+
server-projection boundaries. SQL expressions remain trusted server-side schema
|
|
41
|
+
configuration; request values are never SQL fragments.
|
|
42
|
+
|
|
43
|
+
See the [repository README](https://github.com/Pythia-Software/query-table#readme)
|
|
44
|
+
for the complete schema and backend documentation.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/** All filter operators across both source projects, unioned. Every value can
|
|
2
|
+
* be NULL, so `is_null`/`is_not_null` are valid for every field type. */
|
|
3
|
+
type FilterOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "contains" | "starts_with" | "ends_with" | "includes" | "is_null" | "is_not_null";
|
|
4
|
+
/** A single AND-combined filter. `value` is always the raw string the UI
|
|
5
|
+
* captured; coercion to number/bool/date happens at apply/compile time based on
|
|
6
|
+
* the field's declared type (never on the value's runtime shape). Unused for
|
|
7
|
+
* the nullary ops (`is_null`/`is_not_null`). */
|
|
8
|
+
interface WhereClause {
|
|
9
|
+
field: string;
|
|
10
|
+
op: FilterOp;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
/** One ORDER BY term. Array order in `QueryState.orderBy` is the sort priority. */
|
|
14
|
+
interface OrderByClause {
|
|
15
|
+
field: string;
|
|
16
|
+
dir: "asc" | "desc";
|
|
17
|
+
/** NULL placement. Omitted = "last" (the historical default both projects used,
|
|
18
|
+
* preserved so old `?q=` links round-trip identically). */
|
|
19
|
+
nulls?: "first" | "last";
|
|
20
|
+
}
|
|
21
|
+
/** One column in the SELECT list, plus the view state that travels with it.
|
|
22
|
+
* Order in `QueryState.select` is the left-to-right display order. */
|
|
23
|
+
interface SelectColumn {
|
|
24
|
+
field: string;
|
|
25
|
+
/** Pixel width override. Omitted = FieldDef.select.width, else the type default. */
|
|
26
|
+
width?: number;
|
|
27
|
+
}
|
|
28
|
+
/** A single aggregate operation. `count` is the only op that needs no measure
|
|
29
|
+
* column (it counts rows); the rest reduce one column's values. The server-side
|
|
30
|
+
* matrix (backends/go aggOpAllowed + the TS AGG_OPS_BY_TYPE) decides which ops a
|
|
31
|
+
* field's type permits. */
|
|
32
|
+
type AggOp = "count" | "count_distinct" | "sum" | "avg" | "min" | "max";
|
|
33
|
+
/** One optional "metric" pinned above the table: a single aggregate over a single
|
|
34
|
+
* measure column, optionally broken down by one or more group columns.
|
|
35
|
+
*
|
|
36
|
+
* Scope is deliberately the *whole filtered set* — the same WHERE as the table,
|
|
37
|
+
* but NOT its ORDER BY / LIMIT / OFFSET. It is always evaluated on the server
|
|
38
|
+
* (a real GROUP BY), so it reflects every matching row, not just the visible
|
|
39
|
+
* page. Lives inside QueryState so it round-trips through `?q=`, saved queries,
|
|
40
|
+
* and undo/redo for free — a saved query is a saved dashboard. */
|
|
41
|
+
interface AggregationClause {
|
|
42
|
+
/** Stable id; keys the metric panel and survives a `?q=` round-trip. */
|
|
43
|
+
id: string;
|
|
44
|
+
op: AggOp;
|
|
45
|
+
/** Measure column (FieldDef.name). Omit only for `count` (⇒ COUNT(*)). */
|
|
46
|
+
field?: string;
|
|
47
|
+
/** Group-by columns, in axis order. `[]` = a single grand-total value.
|
|
48
|
+
* One ⇒ a list/bar breakdown; two ⇒ an x/y pivot; three+ ⇒ a flat table. */
|
|
49
|
+
groupBy: string[];
|
|
50
|
+
/** Panel header override; defaults to a derived label like "avg total". */
|
|
51
|
+
label?: string;
|
|
52
|
+
}
|
|
53
|
+
interface QueryState {
|
|
54
|
+
/** Ordered SELECT list + per-column widths. Empty = the schema's default columns. */
|
|
55
|
+
select: SelectColumn[];
|
|
56
|
+
/** AND-combined filters. */
|
|
57
|
+
where: WhereClause[];
|
|
58
|
+
/** Multi-sort terms in priority order. Empty = the schema's default sort. */
|
|
59
|
+
orderBy: OrderByClause[];
|
|
60
|
+
/** Page size. */
|
|
61
|
+
limit: number;
|
|
62
|
+
/** Page offset (rows). */
|
|
63
|
+
offset: number;
|
|
64
|
+
/** Optional aggregate metrics shown above the table. Omitted/empty = none.
|
|
65
|
+
* Each runs as its own server GROUP BY over the WHERE-filtered set (no paging). */
|
|
66
|
+
aggregations?: AggregationClause[];
|
|
67
|
+
}
|
|
68
|
+
/** A row's stable identity, used for selection and per-row refresh. */
|
|
69
|
+
type RowId = string | number;
|
|
70
|
+
/** The query a fresh table starts from before schema defaults are layered on.
|
|
71
|
+
* Empty `select`/`orderBy`/`where` mean "defer to the schema", which also keeps
|
|
72
|
+
* encoded `?q=` tokens short (defaults are omitted). */
|
|
73
|
+
declare const EMPTY_QUERY: QueryState;
|
|
74
|
+
/** Resource limits applied whenever query state crosses a trust boundary (URL,
|
|
75
|
+
* storage, an imperative setQuery call, or a server projection). They keep a
|
|
76
|
+
* malformed bookmark from becoming an unexpectedly expensive request. */
|
|
77
|
+
declare const MAX_QUERY_LIMIT = 1000;
|
|
78
|
+
declare const MAX_QUERY_OFFSET = 1000000;
|
|
79
|
+
declare const MAX_SELECT_COLUMNS = 200;
|
|
80
|
+
declare const MAX_WHERE_CLAUSES = 100;
|
|
81
|
+
declare const MAX_ORDER_BY_TERMS = 20;
|
|
82
|
+
declare const MAX_AGGREGATIONS = 20;
|
|
83
|
+
declare const MAX_GROUP_BY_FIELDS = 20;
|
|
84
|
+
declare const MAX_QUERY_TOKEN_LENGTH: number;
|
|
85
|
+
/** Convert unknown input into a bounded, structurally valid QueryState.
|
|
86
|
+
* Invalid members are dropped; invalid scalar paging values use the supplied
|
|
87
|
+
* fallback. This function is intentionally schema-agnostic—field allowlisting
|
|
88
|
+
* still happens in toServerQuery and in the backend compiler. */
|
|
89
|
+
declare function normalizeQueryState(input: unknown, fallback?: QueryState): QueryState;
|
|
90
|
+
/** Structural equality on two queries — used to decide whether a `?q=` write or a
|
|
91
|
+
* refetch is actually needed (avoids history spam / redundant fetches). */
|
|
92
|
+
declare function queriesEqual(a: QueryState, b: QueryState): boolean;
|
|
93
|
+
|
|
94
|
+
type FieldType = "text" | "number" | "enum" | "datetime" | "bool" | "textarray";
|
|
95
|
+
type Align = "left" | "right" | "center";
|
|
96
|
+
/** A cell renderer is resolved by name against a `RenderRegistry` (in
|
|
97
|
+
* @pythia-software/query-table-ui) or supplied inline. The type lives in core so FieldDef can
|
|
98
|
+
* reference it, but core never imports React — `unknown` stands in for the node
|
|
99
|
+
* and @pythia-software/query-table-ui narrows it to `React.ReactNode`. */
|
|
100
|
+
type CellRenderer<Row = any, V = unknown> = (ctx: {
|
|
101
|
+
value: V;
|
|
102
|
+
row: Row;
|
|
103
|
+
field: FieldDef<Row, V>;
|
|
104
|
+
}) => unknown;
|
|
105
|
+
/** A column the backend returns and can filter/sort. Its SQL expression lives in
|
|
106
|
+
* the JSON schema doc's `bindings` (and the Go Schema), never on the frontend. */
|
|
107
|
+
interface BackendField {
|
|
108
|
+
kind: "backend";
|
|
109
|
+
/** Dotted path into the API row when it differs from `name` (e.g. "enqueued_at.Time"). */
|
|
110
|
+
path?: string;
|
|
111
|
+
/** Computed SQL with no backing column (e.g. is_starred). Documentation hint
|
|
112
|
+
* surfaced to the picker; the actual expression is server-side. */
|
|
113
|
+
synthetic?: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** A value computed on the client, or a purely render-driven column. Never
|
|
116
|
+
* pushed to the server; client-side filtering/sorting (if any) reads `accessor`. */
|
|
117
|
+
interface DerivedField<Row = any, V = unknown> {
|
|
118
|
+
kind: "derived";
|
|
119
|
+
/** Compute the value from the row for client filter/sort. Omit for render-only
|
|
120
|
+
* columns (the renderer reads the whole row from CellContext instead). */
|
|
121
|
+
accessor?: (row: Row) => V;
|
|
122
|
+
}
|
|
123
|
+
type FieldSource<Row = any, V = unknown> = BackendField | DerivedField<Row, V>;
|
|
124
|
+
/** How filter-value candidates are offered. Autocomplete is the DEFAULT for
|
|
125
|
+
* every field (design feedback): the value input is a combobox backed by
|
|
126
|
+
* Transport.fetchDistinctValues, which takes a search string so large domains
|
|
127
|
+
* refine per keystroke on the backend. `static` is for closed domains you don't
|
|
128
|
+
* want a round-trip for; `freeform` opts out of suggestions entirely. */
|
|
129
|
+
type FilterValues = {
|
|
130
|
+
source: "autocomplete";
|
|
131
|
+
} | {
|
|
132
|
+
source: "static";
|
|
133
|
+
options: string[];
|
|
134
|
+
} | {
|
|
135
|
+
source: "freeform";
|
|
136
|
+
};
|
|
137
|
+
interface FilterConfig {
|
|
138
|
+
/** Whether this field is filterable. Default: backend ⇒ true, derived ⇒ false. */
|
|
139
|
+
enabled?: boolean;
|
|
140
|
+
/** Evaluate on the backend (predicate pushdown)? Default: backend ⇒ true.
|
|
141
|
+
* false ⇒ the clause is stripped from the server request and applied locally
|
|
142
|
+
* by applyQuery (the "serverFilter:false" derived/JSONB-less columns). */
|
|
143
|
+
pushdown?: boolean;
|
|
144
|
+
/** Override the default operator set for the field's type. */
|
|
145
|
+
ops?: FilterOp[];
|
|
146
|
+
/** Value-suggestion strategy. Default { source: "autocomplete" }. */
|
|
147
|
+
values?: FilterValues;
|
|
148
|
+
}
|
|
149
|
+
interface SortConfig {
|
|
150
|
+
/** Whether this field is sortable. Default: backend ⇒ true, derived ⇒ false. */
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
/** Server field to ORDER BY when it differs from the display field
|
|
153
|
+
* (e.g. a "★" column whose server sort key is `is_starred`). */
|
|
154
|
+
field?: string;
|
|
155
|
+
}
|
|
156
|
+
interface SelectConfig {
|
|
157
|
+
/** Can this appear as a visible column? Default true. false ⇒ filter-only field. */
|
|
158
|
+
enabled?: boolean;
|
|
159
|
+
/** Shown by default when QueryState.select is empty. */
|
|
160
|
+
default?: boolean;
|
|
161
|
+
/** Default column width (px); a per-column override rides in QueryState.select. */
|
|
162
|
+
width?: number;
|
|
163
|
+
align?: Align;
|
|
164
|
+
}
|
|
165
|
+
interface AggregateConfig {
|
|
166
|
+
/** Can this field be a metric's MEASURE (sum/avg/min/max/count_distinct)?
|
|
167
|
+
* Default: backend ⇒ true, derived ⇒ false. */
|
|
168
|
+
measure?: boolean;
|
|
169
|
+
/** Can this field be a GROUP BY key? Default: enum/text/bool ⇒ true. */
|
|
170
|
+
groupable?: boolean;
|
|
171
|
+
/** Override the default aggregate-op set for the field's type. */
|
|
172
|
+
ops?: AggOp[];
|
|
173
|
+
}
|
|
174
|
+
interface FieldDef<Row = any, V = unknown> {
|
|
175
|
+
name: string;
|
|
176
|
+
label: string;
|
|
177
|
+
type: FieldType;
|
|
178
|
+
source: FieldSource<Row, V>;
|
|
179
|
+
filter?: FilterConfig;
|
|
180
|
+
sort?: SortConfig;
|
|
181
|
+
select?: SelectConfig;
|
|
182
|
+
aggregate?: AggregateConfig;
|
|
183
|
+
group?: string;
|
|
184
|
+
alias?: string;
|
|
185
|
+
aliases?: string[];
|
|
186
|
+
render?: string | CellRenderer<Row, V>;
|
|
187
|
+
}
|
|
188
|
+
/** The frontend's loaded schema for one dataset. */
|
|
189
|
+
interface FieldSchema<Row = any> {
|
|
190
|
+
/** Dataset id, also the StorageAdapter namespace for saved queries. */
|
|
191
|
+
name: string;
|
|
192
|
+
/** Field that yields a row's stable id for selection / per-row refresh. */
|
|
193
|
+
idField: string;
|
|
194
|
+
fields: FieldDef<Row>[];
|
|
195
|
+
/** Sort applied when QueryState.orderBy is empty. */
|
|
196
|
+
defaultSort?: OrderByClause[];
|
|
197
|
+
/** Columns shown when QueryState.select is empty. */
|
|
198
|
+
defaultSelect?: SelectColumn[];
|
|
199
|
+
/** Page size for a fresh query. */
|
|
200
|
+
defaultLimit?: number;
|
|
201
|
+
}
|
|
202
|
+
declare function isFilterable(f: FieldDef): boolean;
|
|
203
|
+
declare function isPushdownFilter(f: FieldDef): boolean;
|
|
204
|
+
declare function isSortable(f: FieldDef): boolean;
|
|
205
|
+
declare function isSelectable(f: FieldDef): boolean;
|
|
206
|
+
declare function filterValues(f: FieldDef): FilterValues;
|
|
207
|
+
/** Index fields by name for O(1) lookup. */
|
|
208
|
+
declare function indexFields<Row>(schema: FieldSchema<Row>): Map<string, FieldDef<Row>>;
|
|
209
|
+
/** Resolve the ordered, selectable FieldDefs for a query, falling back to schema
|
|
210
|
+
* defaults, then to every `select.default` field. Unknown/duplicate/unselectable
|
|
211
|
+
* names in `select` are dropped. */
|
|
212
|
+
declare function selectedFields<Row>(schema: FieldSchema<Row>, q: {
|
|
213
|
+
select: SelectColumn[];
|
|
214
|
+
}): FieldDef<Row>[];
|
|
215
|
+
/** Read a field's value from a row: derived `accessor` if present, else the
|
|
216
|
+
* backend `path` (dotted) / `name`. Returns null when any path segment is missing. */
|
|
217
|
+
declare function readFieldValue<Row, V = unknown>(field: FieldDef<Row, V>, row: Row): V | null;
|
|
218
|
+
/** Parse + validate a raw JSON schema document into a FieldSchema. The doc is the
|
|
219
|
+
* same one the Go backend loads; here we drop backend `bindings` (the frontend
|
|
220
|
+
* never sees SQL) and leave `render` as a string key the consumer maps to a
|
|
221
|
+
* renderer at runtime. Throws with a readable message on a malformed document. */
|
|
222
|
+
declare function loadSchema<Row = any>(doc: unknown): FieldSchema<Row>;
|
|
223
|
+
|
|
224
|
+
/** Default operator set per type. A FieldDef.filter.ops overrides this. */
|
|
225
|
+
declare const OPS_BY_TYPE: Record<FieldType, FilterOp[]>;
|
|
226
|
+
/** Operators that take no value (the value input is hidden). */
|
|
227
|
+
declare const NULLARY_OPS: ReadonlySet<FilterOp>;
|
|
228
|
+
/** Effective operators for a field (its override, else the type default). */
|
|
229
|
+
declare function opsForField(field: Pick<FieldDef, "type" | "filter">): FilterOp[];
|
|
230
|
+
/** Whether an op is valid for a type per the default matrix (server mirror lives
|
|
231
|
+
* in Go opAllowed; keep them in lockstep). */
|
|
232
|
+
declare function opAllowedForType(type: FieldType, op: FilterOp): boolean;
|
|
233
|
+
/** Coerce a raw string value to the type the field expects, for client-side
|
|
234
|
+
* comparison. Throws on invalid input (e.g. a non-numeric value on a number
|
|
235
|
+
* field) so callers can surface it rather than silently mis-filtering. */
|
|
236
|
+
declare function coerceValue(type: FieldType, raw: string): string | number | boolean;
|
|
237
|
+
|
|
238
|
+
/** Default aggregate ops per field type. A FieldDef.aggregate.ops overrides this.
|
|
239
|
+
* `count`/`count_distinct` work on anything countable; sum/avg are numeric-only;
|
|
240
|
+
* min/max need an ordered domain (numbers, datetimes, and lexical text/enum). */
|
|
241
|
+
declare const AGG_OPS_BY_TYPE: Record<FieldType, AggOp[]>;
|
|
242
|
+
/** Ops that reduce a measure column. `count` is the only op that may omit a
|
|
243
|
+
* field (counting rows), so it's the only one absent here. */
|
|
244
|
+
declare const AGG_OPS_NEEDING_FIELD: ReadonlySet<AggOp>;
|
|
245
|
+
/** Whether an op requires a measure column. */
|
|
246
|
+
declare function aggOpNeedsField(op: AggOp): boolean;
|
|
247
|
+
/** Effective aggregate ops for a field (its override, else the type default). */
|
|
248
|
+
declare function aggOpsForField(field: Pick<FieldDef, "type" | "aggregate">): AggOp[];
|
|
249
|
+
/** Whether an op is valid for a type per the default matrix (Go aggOpAllowed is
|
|
250
|
+
* the server mirror; keep them in lockstep). */
|
|
251
|
+
declare function aggOpAllowedForType(type: FieldType, op: AggOp): boolean;
|
|
252
|
+
/** Can this field be the MEASURE of a metric? Only backend fields (they have SQL
|
|
253
|
+
* to aggregate); derived/render-only columns are never pushed to the server. An
|
|
254
|
+
* explicit `aggregate.measure` wins over the type default. */
|
|
255
|
+
declare function isMeasurable(field: FieldDef): boolean;
|
|
256
|
+
/** Can this field be a GROUP BY key? Backend fields only. Default: low-cardinality
|
|
257
|
+
* types (enum/text/bool); numbers/datetimes need bucketing (out of scope) so they
|
|
258
|
+
* default off, but `aggregate.groupable: true` opts any backend field in. */
|
|
259
|
+
declare function isGroupable(field: FieldDef): boolean;
|
|
260
|
+
|
|
261
|
+
/** QueryState → base64url token (omitting defaults). All-default query → "". */
|
|
262
|
+
declare function encodeQuery(q: QueryState): string;
|
|
263
|
+
/** base64url token → QueryState. Tolerant: bad input and legacy shapes both
|
|
264
|
+
* normalize to a valid QueryState (never throws). */
|
|
265
|
+
declare function decodeQuery(token: string): QueryState;
|
|
266
|
+
/** The backend request derived from a QueryState + its schema. Mirrors the Go
|
|
267
|
+
* `WireQuery`. View-only state (column widths) is intentionally absent. */
|
|
268
|
+
interface ServerQuery {
|
|
269
|
+
/** field names to return — visible columns ∪ fields referenced by where/orderBy. */
|
|
270
|
+
select: string[];
|
|
271
|
+
/** only clauses on pushdown-filterable fields. */
|
|
272
|
+
where: WhereClause[];
|
|
273
|
+
/** only terms on server-sortable fields, with `field` already remapped to the
|
|
274
|
+
* field's server sort key (FieldDef.sort.field) when set. */
|
|
275
|
+
orderBy: OrderByClause[];
|
|
276
|
+
limit: number;
|
|
277
|
+
offset: number;
|
|
278
|
+
}
|
|
279
|
+
/** Project a QueryState into the server request, dropping client-only filters,
|
|
280
|
+
* client-only sorts, and remapping sort fields through `sort.field`. */
|
|
281
|
+
declare function toServerQuery<Row>(q: QueryState, schema: FieldSchema<Row>): ServerQuery;
|
|
282
|
+
/** The backend request for the metric panel. Scope is the whole filtered set:
|
|
283
|
+
* the SAME pushdown WHERE as the rows query, but NO ORDER BY / LIMIT / OFFSET —
|
|
284
|
+
* metrics describe every matching row, not the visible page. Mirrors the Go
|
|
285
|
+
* AggSpec list. */
|
|
286
|
+
interface AggregationRequest {
|
|
287
|
+
where: WhereClause[];
|
|
288
|
+
aggregations: AggregationClause[];
|
|
289
|
+
}
|
|
290
|
+
/** One group's result. `keys` has one entry per AggregationClause.groupBy field,
|
|
291
|
+
* in axis order (`[]` for a grand total); a null key is the NULL/empty bucket. */
|
|
292
|
+
interface AggregationBucket {
|
|
293
|
+
keys: (string | null)[];
|
|
294
|
+
/** The metric: a number for count/sum/avg, or the column's value for min/max
|
|
295
|
+
* (which may be a string for text/datetime). null when undefined (e.g. avg of
|
|
296
|
+
* an all-null column). */
|
|
297
|
+
value: number | string | null;
|
|
298
|
+
/** COUNT(*) of rows in the group — always present, even when `value` isn't a
|
|
299
|
+
* count, so the panel can show group sizes / shares. */
|
|
300
|
+
count: number;
|
|
301
|
+
}
|
|
302
|
+
interface AggregationResultEntry {
|
|
303
|
+
/** Echoes AggregationClause.id. */
|
|
304
|
+
id: string;
|
|
305
|
+
buckets: AggregationBucket[];
|
|
306
|
+
}
|
|
307
|
+
interface AggregationResult {
|
|
308
|
+
/** One entry per requested aggregation, in request order. */
|
|
309
|
+
metrics: AggregationResultEntry[];
|
|
310
|
+
}
|
|
311
|
+
/** Project a QueryState into the metric request: the pushdown WHERE subset (same
|
|
312
|
+
* rule as toServerQuery) plus the aggregations whose measure + group fields are
|
|
313
|
+
* all server-capable backend columns. Aggregations referencing a derived /
|
|
314
|
+
* unknown field are dropped — the server has no SQL for them. */
|
|
315
|
+
declare function toAggregationQuery<Row>(q: QueryState, schema: FieldSchema<Row>): AggregationRequest;
|
|
316
|
+
|
|
317
|
+
interface ApplyResult<Row> {
|
|
318
|
+
/** The page slice (after filter + sort + offset/limit). */
|
|
319
|
+
rows: Row[];
|
|
320
|
+
/** Total matching rows *before* pagination — powers "showing N of M". */
|
|
321
|
+
total: number;
|
|
322
|
+
}
|
|
323
|
+
/** Filter + multi-sort + paginate `rows` per `q`, resolving field types/paths
|
|
324
|
+
* from `schema`. Pure; never mutates `rows`. Clauses/sorts on unknown fields are
|
|
325
|
+
* ignored (the backend already enforced its own allowlist). */
|
|
326
|
+
declare function applyQuery<Row>(rows: Row[], q: QueryState, schema: FieldSchema<Row>): ApplyResult<Row>;
|
|
327
|
+
/** Does one row satisfy one clause? Exposed for the CellMenu preview + tests. */
|
|
328
|
+
declare function matchesClause<Row>(row: Row, clause: WhereClause, schema: FieldSchema<Row>): boolean;
|
|
329
|
+
/** Client-side mirror of the backend GROUP BY (the executor for `clientRows`
|
|
330
|
+
* mode / the demo). Scope matches the server contract: the WHERE-filtered set
|
|
331
|
+
* only — ORDER BY / LIMIT / OFFSET are intentionally ignored, so a metric
|
|
332
|
+
* reflects every matching row, not the visible page. Must agree with
|
|
333
|
+
* backends/go's aggregate compile on op semantics. */
|
|
334
|
+
declare function applyAggregations<Row>(rows: Row[], q: QueryState, schema: FieldSchema<Row>): AggregationResult;
|
|
335
|
+
|
|
336
|
+
interface FetchRowsResult<Row> {
|
|
337
|
+
rows: Row[];
|
|
338
|
+
/** Total matching rows before pagination (for "N of M" + paging bounds). */
|
|
339
|
+
total: number;
|
|
340
|
+
}
|
|
341
|
+
/** A keystroke-driven distinct-value lookup. Autocomplete is the DEFAULT value
|
|
342
|
+
* source for every field (design feedback), so this is how the filter combobox
|
|
343
|
+
* finds options. `search` is the user's current input; the backend returns the
|
|
344
|
+
* best matches and whether it truncated (so the UI can prompt "keep typing"). */
|
|
345
|
+
interface DistinctValuesQuery {
|
|
346
|
+
field: string;
|
|
347
|
+
/** Current substring/prefix the user has typed (empty = top values). */
|
|
348
|
+
search: string;
|
|
349
|
+
/** Max suggestions to return (default chosen by the adapter, ~50). */
|
|
350
|
+
limit?: number;
|
|
351
|
+
}
|
|
352
|
+
interface DistinctValuesResult {
|
|
353
|
+
values: string[];
|
|
354
|
+
/** true when more matches exist than were returned — refine by typing. */
|
|
355
|
+
hasMore: boolean;
|
|
356
|
+
/** True if the selected field has at least one NULL in the source dataset.
|
|
357
|
+
* False if that field is guaranteed non-null. Omitted when the backend does
|
|
358
|
+
* not compute this metadata. */
|
|
359
|
+
hasNull?: boolean;
|
|
360
|
+
}
|
|
361
|
+
/** Per-field metadata for the field picker. */
|
|
362
|
+
interface FieldStats {
|
|
363
|
+
distinct?: number;
|
|
364
|
+
min?: string | number;
|
|
365
|
+
max?: string | number;
|
|
366
|
+
}
|
|
367
|
+
interface Transport<Row = any> {
|
|
368
|
+
/** Run a server query. The only required method — a purely client-side table
|
|
369
|
+
* can omit a Transport entirely and rely on applyQuery over local rows. */
|
|
370
|
+
fetchRows(query: ServerQuery, signal?: AbortSignal): Promise<FetchRowsResult<Row>>;
|
|
371
|
+
/** Distinct values for filter autocomplete. Strongly recommended: without it,
|
|
372
|
+
* fields fall back to freeform input. The backend should match `search`
|
|
373
|
+
* server-side (e.g. ILIKE prefix) and cap at `limit` so large domains stay
|
|
374
|
+
* fast and refine per keystroke. */
|
|
375
|
+
fetchDistinctValues?(q: DistinctValuesQuery, signal?: AbortSignal): Promise<DistinctValuesResult>;
|
|
376
|
+
/** Run the metric panel's GROUP BY queries (one per requested aggregation),
|
|
377
|
+
* scoped to the WHERE-filtered set with no paging. Optional: without it, the
|
|
378
|
+
* metric panel falls back to applyAggregations over local rows (client mode).
|
|
379
|
+
* Implementations compile each AggSpec with backends/go CompileAggregation,
|
|
380
|
+
* sharing the same WHERE as fetchRows. */
|
|
381
|
+
fetchAggregations?(q: AggregationRequest, signal?: AbortSignal): Promise<AggregationResult>;
|
|
382
|
+
/** Re-fetch a single row by id, for in-place updates without a full re-query
|
|
383
|
+
* after a mutation. Optional. */
|
|
384
|
+
fetchRow?(id: RowId, signal?: AbortSignal): Promise<Row | null>;
|
|
385
|
+
/** Stats for the field picker. Optional. */
|
|
386
|
+
fetchFieldStats?(fields: string[], signal?: AbortSignal): Promise<Record<string, FieldStats>>;
|
|
387
|
+
}
|
|
388
|
+
interface SavedQuery {
|
|
389
|
+
id: string;
|
|
390
|
+
name: string;
|
|
391
|
+
/** Epoch millis; injected by the caller so core stays deterministic/testable. */
|
|
392
|
+
savedAt: number;
|
|
393
|
+
query: QueryState;
|
|
394
|
+
}
|
|
395
|
+
/** Persistence for the "last" query (auto-restored) and named saved queries. All
|
|
396
|
+
* methods are namespaced by `key` (the schema/dataset name) so multiple tables
|
|
397
|
+
* on one origin don't collide. Async to allow backend implementations; the
|
|
398
|
+
* browser adapters can still resolve synchronously. */
|
|
399
|
+
interface StorageAdapter {
|
|
400
|
+
loadLast(key: string): Promise<QueryState | null>;
|
|
401
|
+
saveLast(key: string, query: QueryState): Promise<void>;
|
|
402
|
+
listSaved(key: string): Promise<SavedQuery[]>;
|
|
403
|
+
/** Load the saved query selected as the default view for this key. Optional so
|
|
404
|
+
* existing backend adapters keep working until they add first-class support. */
|
|
405
|
+
loadDefaultSaved?(key: string): Promise<SavedQuery | null>;
|
|
406
|
+
/** Set or clear the saved query used as the default view for this key. */
|
|
407
|
+
setDefaultSaved?(key: string, id: string | null): Promise<void>;
|
|
408
|
+
/** Persist a named query snapshot. Rejects when a saved query with the same
|
|
409
|
+
* name already exists in the key namespace. */
|
|
410
|
+
saveNamed(key: string, name: string, query: QueryState, savedAt: number): Promise<SavedQuery>;
|
|
411
|
+
deleteSaved(key: string, id: string): Promise<void>;
|
|
412
|
+
}
|
|
413
|
+
/** In-memory persistence used by default by the React package. It supports the
|
|
414
|
+
* full saved-query UI for the lifetime of a mounted table without writing
|
|
415
|
+
* filter values to durable browser storage. */
|
|
416
|
+
declare function memoryStorageAdapter(): StorageAdapter;
|
|
417
|
+
/** Opt-in StorageAdapter over window.localStorage. Query state includes raw
|
|
418
|
+
* filter values and is stored as cleartext JSON; do not use it for sensitive
|
|
419
|
+
* datasets. Falls back to no persistence when storage is unavailable. */
|
|
420
|
+
declare function localStorageAdapter(): StorageAdapter;
|
|
421
|
+
|
|
422
|
+
export { AGG_OPS_BY_TYPE, AGG_OPS_NEEDING_FIELD, type AggOp, type AggregateConfig, type AggregationBucket, type AggregationClause, type AggregationRequest, type AggregationResult, type AggregationResultEntry, type Align, type ApplyResult, type BackendField, type CellRenderer, type DerivedField, type DistinctValuesQuery, type DistinctValuesResult, EMPTY_QUERY, type FetchRowsResult, type FieldDef, type FieldSchema, type FieldSource, type FieldStats, type FieldType, type FilterConfig, type FilterOp, type FilterValues, MAX_AGGREGATIONS, MAX_GROUP_BY_FIELDS, MAX_ORDER_BY_TERMS, MAX_QUERY_LIMIT, MAX_QUERY_OFFSET, MAX_QUERY_TOKEN_LENGTH, MAX_SELECT_COLUMNS, MAX_WHERE_CLAUSES, NULLARY_OPS, OPS_BY_TYPE, type OrderByClause, type QueryState, type RowId, type SavedQuery, type SelectColumn, type SelectConfig, type ServerQuery, type SortConfig, type StorageAdapter, type Transport, type WhereClause, aggOpAllowedForType, aggOpNeedsField, aggOpsForField, applyAggregations, applyQuery, coerceValue, decodeQuery, encodeQuery, filterValues, indexFields, isFilterable, isGroupable, isMeasurable, isPushdownFilter, isSelectable, isSortable, loadSchema, localStorageAdapter, matchesClause, memoryStorageAdapter, normalizeQueryState, opAllowedForType, opsForField, queriesEqual, readFieldValue, selectedFields, toAggregationQuery, toServerQuery };
|