@pythia-software/query-table-react 0.7.1 → 0.8.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/dist/index.d.ts CHANGED
@@ -1,7 +1,19 @@
1
- import { QueryState, GroupedRow, GroupingModel, FieldSchema, GroupingView, SummaryOp, RepresentativeRow, SummaryColumn, SelectColumn, PropertyViewState, SavedPropertyView, PropertyViewStorage, QueryLink, QuerySnapshot, ItemPropertyMode, ItemPageOptions, ItemDetailMode, ItemDetailsState, RelationshipAdapter, RowId, FormulaNode, FormulaValue, FormulaResult, ComputedColumn, FieldDef, ComputedColumnDraft, FormulaPlan, ColumnPreview, SavedQuery, StorageAdapter, AggOp, AggregationClause, AggregationResult, WhereClause, DistinctValuesResult, FieldStats, OrderByClause, Transport, ComputedColumnStore, ComputedExecution, QueryLinkOptions, ObjectReference, ResolvedObjectReference } from '@pythia-software/query-table-core';
1
+ import { FieldSchema, WhereClause, QueryState, GroupedRow, GroupingModel, GroupingView, SummaryOp, RepresentativeRow, SummaryColumn, SelectColumn, PropertyViewState, SavedPropertyView, PropertyViewStorage, QueryLink, QuerySnapshot, ItemPropertyMode, ItemPageOptions, ItemDetailMode, ItemDetailsState, RelationshipAdapter, RowId, FormulaNode, FormulaValue, FormulaResult, ComputedColumn, FieldDef, ComputedColumnDraft, FormulaPlan, ColumnPreview, SavedQuery, StorageAdapter, AggOp, AggregationClause, AggregationResult, DistinctValuesResult, FieldStats, OrderByClause, Transport, ComputedColumnStore, ComputedExecution, QueryLinkOptions, ObjectReference, ResolvedObjectReference } from '@pythia-software/query-table-core';
2
2
 
3
3
  /** Filter controls for either the result query or grouping's source query. */
4
4
  type QueryFilterApi<Row> = Pick<QueryTableApi<Row>, "query" | "setQuery" | "loading" | "addFilter" | "updateFilter" | "removeFilter" | "clearFilters" | "updatePredicate" | "removePredicate" | "negatePredicate" | "reorderFilters" | "mergeFilters" | "filterValues" | "fieldStats">;
5
+ /** Bind filter intents to a query stage, including the source WHERE of grouping. */
6
+ declare function useQueryFilters<Row>(schema: FieldSchema<Row>, setQuery: QueryTableApi<Row>["setQuery"]): {
7
+ addFilter: (clause: WhereClause) => void;
8
+ updateFilter: (index: number, clause: WhereClause) => void;
9
+ removeFilter: (index: number) => void;
10
+ clearFilters: () => void;
11
+ updatePredicate: (termIndex: number, predIndex: number, clause: WhereClause) => void;
12
+ removePredicate: (termIndex: number, predIndex: number) => void;
13
+ negatePredicate: (termIndex: number, predIndex: number) => void;
14
+ reorderFilters: (fromIndex: number, toIndex: number) => void;
15
+ mergeFilters: (sourceIndex: number, targetIndex: number) => void;
16
+ };
5
17
 
6
18
  /** Source-stage controls. Rows/columns/metrics remain on the active result API. */
7
19
  type GroupingSourceApi<Row> = QueryFilterApi<Row> & Pick<QueryTableApi<Row>, "itemDetails" | "rowId"> & {
@@ -21,6 +33,10 @@ interface GroupingApi<Row> {
21
33
  grouped: QueryTableApi<Row>;
22
34
  schema: FieldSchema<Row>;
23
35
  view: GroupingView;
36
+ /** Problems with link columns. They never block the grouped table: an
37
+ * invalid link renders as plain text. Unregistered host targets are
38
+ * reported when rendering. */
39
+ diagnostics: string[];
24
40
  inputs: Row[];
25
41
  notice: string;
26
42
  setNotice: (notice: string) => void;
@@ -463,4 +479,4 @@ declare function useObjectReferences(references: ObjectReference[], adapter?: Re
463
479
  error: Error | null;
464
480
  };
465
481
 
466
- export { type AggregationPatch, type AggregationsApi, type AggregationsApiState, type AutoRefreshApi, type AutoRefreshConfig, type AutoRefreshStatus, type ColumnDragApi, type ComputedColumnsApi, type FormulaWorkerFactory, type GroupingApi, type GroupingSourceApi, type ItemDetailRecord, type ItemDetailsApi, type ItemDetailsOptions, type PropertyViewApi, type QueryFilterApi, type QueryLinksApi, type QueryTableApi, type RequestActivityApi, type RequestActivityEntry, type SavedQueriesApi, type SelectApi, type SelectionApi, type UsePropertyViewOptions, type UseQueryTableOptions, createFormulaWorker, evaluateFormulaRows, useAggregations, useColumnDrag, useObjectReferences, usePropertyView, useQueryTable, useSavedQueries, useSelect, useSelection };
482
+ export { type AggregationPatch, type AggregationsApi, type AggregationsApiState, type AutoRefreshApi, type AutoRefreshConfig, type AutoRefreshStatus, type ColumnDragApi, type ComputedColumnsApi, type FormulaWorkerFactory, type GroupingApi, type GroupingSourceApi, type ItemDetailRecord, type ItemDetailsApi, type ItemDetailsOptions, type PropertyViewApi, type QueryFilterApi, type QueryLinksApi, type QueryTableApi, type RequestActivityApi, type RequestActivityEntry, type SavedQueriesApi, type SelectApi, type SelectionApi, type UsePropertyViewOptions, type UseQueryTableOptions, createFormulaWorker, evaluateFormulaRows, useAggregations, useColumnDrag, useObjectReferences, usePropertyView, useQueryFilters, useQueryTable, useSavedQueries, useSelect, useSelection };
package/dist/index.js CHANGED
@@ -138,6 +138,7 @@ import {
138
138
  MAX_SUMMARY_COLUMNS,
139
139
  normalizeQueryState,
140
140
  predicatesOf as predicatesOf2,
141
+ withGroupingKeys,
141
142
  withoutGrouping,
142
143
  queriesEqual
143
144
  } from "@pythia-software/query-table-core";
@@ -171,46 +172,12 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
171
172
  if (!keys.length && stage)
172
173
  previous.current = { query: api.query, revision: viewRevision };
173
174
  api.setQuery((current) => {
174
- const before = current.grouping;
175
- if (!keys.length) return before ? before.source : current;
176
- if (!before) {
177
- if (cached?.grouping && canRestore(current))
178
- return {
179
- ...cached,
180
- grouping: { ...cached.grouping, source: current }
181
- };
182
- const grouping2 = {
183
- keys,
184
- summaries: [],
185
- representative: null,
186
- source: current
187
- };
175
+ if (keys.length && !current.grouping && cached?.grouping && canRestore(current))
188
176
  return {
189
- select: keys.map((field) => ({ field })),
190
- where: [],
191
- orderBy: [],
192
- limit: current.limit,
193
- offset: 0,
194
- grouping: grouping2
177
+ ...cached,
178
+ grouping: { ...cached.grouping, source: current }
195
179
  };
196
- }
197
- const grouping = { ...before, keys };
198
- const removed = new Set(before.keys.filter((key) => !keys.includes(key)));
199
- const columns = current.select.length ? current.select : before.keys.map((field) => ({ field }));
200
- return {
201
- ...current,
202
- grouping,
203
- offset: 0,
204
- aggregations: [],
205
- select: [
206
- ...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
207
- ...columns.filter((c) => !removed.has(c.field))
208
- ],
209
- where: current.where.filter(
210
- (term) => !predicatesOf2(term).some((p) => removed.has(p.field))
211
- ),
212
- orderBy: current.orderBy.filter((term) => !removed.has(term.field))
213
- };
180
+ return withGroupingKeys(current, keys);
214
181
  });
215
182
  setNotice(
216
183
  stage && api.query.aggregations?.length && keys.length ? "Grouping changed. Group metrics cleared; Undo restores them." : ""
@@ -274,9 +241,11 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
274
241
  function changeSummary(id, op) {
275
242
  if (!stage) return;
276
243
  applyColumns(
277
- stage.summaries.map(
278
- (column) => column.id === id ? { ...column, op } : column
279
- ),
244
+ stage.summaries.map((column) => {
245
+ if (column.id !== id) return column;
246
+ const { display: _display, ...rest } = column;
247
+ return op === "row" ? { ...rest, op } : { ...column, op };
248
+ }),
280
249
  api.select.visible
281
250
  );
282
251
  }
@@ -325,6 +294,7 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
325
294
  grouped: api,
326
295
  schema: api.schema,
327
296
  view,
297
+ diagnostics: stage ? model.linkDiagnostics(stage) : [],
328
298
  inputs,
329
299
  notice,
330
300
  setNotice,
@@ -344,7 +314,8 @@ import {
344
314
  withoutGrouping as withoutGrouping2,
345
315
  sourceExecutionSchema,
346
316
  toQueryRequestV3,
347
- validateQueryResultV3
317
+ validateQueryResultV3,
318
+ metricPopulationError as metricPopulationError2
348
319
  } from "@pythia-software/query-table-core";
349
320
  import { ITEM_DETAILS_COLUMN, itemPageFields } from "@pythia-software/query-table-core";
350
321
 
@@ -753,6 +724,7 @@ function validateComputedResponse(request, result, schema) {
753
724
  // src/useComputedColumns.ts
754
725
  import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
755
726
  import {
727
+ isGroupLinkField,
756
728
  applyQuery,
757
729
  compileFormula,
758
730
  computedFieldName,
@@ -908,7 +880,7 @@ function useComputedColumns(options) {
908
880
  }, [reload]);
909
881
  const inputFields = useMemo3(
910
882
  () => schema.fields.filter(isSelectable).filter(
911
- (f) => f.source.kind === "backend" || f.source.accessor && f.source.dependencies
883
+ (f) => f.source.kind === "backend" || f.source.accessor && f.source.dependencies && !isGroupLinkField(f)
912
884
  ),
913
885
  [schema]
914
886
  );
@@ -1334,6 +1306,8 @@ import {
1334
1306
  evaluateMetrics,
1335
1307
  metricPlans,
1336
1308
  hasRegex as hasRegex2,
1309
+ metricPopulationError,
1310
+ predicatesOf as predicatesOf3,
1337
1311
  toAggregationQuery,
1338
1312
  toMetricQuery,
1339
1313
  validateMetric,
@@ -1348,7 +1322,7 @@ var abort = (signal) => {
1348
1322
  async function localMetrics(rows, query, context, signal) {
1349
1323
  const originalClauses = query.aggregations ?? [];
1350
1324
  const advanced = (query.aggregations ?? []).filter(
1351
- (c) => context.strictLocalMetrics || c.dateGrouping || isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0
1325
+ (c) => context.strictLocalMetrics || c.dateGrouping || isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0 || c.where !== void 0 || c.minCount !== void 0
1352
1326
  );
1353
1327
  if (advanced.length && (originalClauses.length > 20 || normalizeQueryState2(query).aggregations?.length !== originalClauses.length))
1354
1328
  return evaluateMetrics(rows, query, context.schema, {
@@ -1466,7 +1440,7 @@ async function executeMetrics(query, context, signal) {
1466
1440
  const shown = clauses.filter(
1467
1441
  (c) => c.scope === "shownRows" && (!supportsV2 || needsWorker(c) || !!c.dateGrouping)
1468
1442
  );
1469
- const matching = clauses.filter(
1443
+ let matching = clauses.filter(
1470
1444
  (c) => c.scope !== "shownRows" || supportsV2 && !needsWorker(c) && !c.dateGrouping
1471
1445
  );
1472
1446
  const results = [];
@@ -1478,6 +1452,9 @@ async function executeMetrics(query, context, signal) {
1478
1452
  const names = new Set(
1479
1453
  shown.flatMap((clause) => [
1480
1454
  ...clause.groupBy,
1455
+ ...(clause.where ?? []).flatMap(
1456
+ (term) => predicatesOf3(term).map((predicate) => predicate.field)
1457
+ ),
1481
1458
  ...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
1482
1459
  (p) => p.dependencies
1483
1460
  )
@@ -1509,20 +1486,37 @@ async function executeMetrics(query, context, signal) {
1509
1486
  ...(await localMetrics(context.shownRows, pageQuery, context, signal)).metrics
1510
1487
  );
1511
1488
  }
1489
+ const unsupported = (clause, error) => results.push({ id: clause.id, buckets: [], error });
1490
+ const calendar = matching.filter((c) => c.dateGrouping);
1491
+ for (const clause of calendar)
1492
+ unsupported(
1493
+ clause,
1494
+ "This server does not support calendar grouping. Enable the v3 query executor."
1495
+ );
1496
+ matching = matching.filter((c) => !c.dateGrouping);
1497
+ {
1498
+ const transport = context.transport;
1499
+ if (transport?.fetchMetrics || transport?.fetchAggregations) {
1500
+ const support = transport.fetchMetrics && transport.metricCapabilities?.version === 2 ? transport.metricCapabilities : {};
1501
+ matching = matching.filter((c) => {
1502
+ const error = metricPopulationError(c, support);
1503
+ if (error) unsupported(c, error);
1504
+ return !error;
1505
+ });
1506
+ }
1507
+ }
1512
1508
  if (matching.length) {
1513
- if (matching.some((c) => c.dateGrouping))
1514
- throw Error(
1515
- "This server does not support calendar grouping. Enable the v3 query executor."
1516
- );
1517
1509
  const transport = context.transport;
1518
- const scoped = { ...query, aggregations: matching };
1519
1510
  if (transport?.fetchMetrics && transport.metricCapabilities?.version === 2) {
1520
- const request = toMetricQuery(scoped, context.schema, context.execution);
1511
+ const capabilities = transport.metricCapabilities;
1512
+ const request = toMetricQuery(
1513
+ { ...query, aggregations: matching },
1514
+ context.schema,
1515
+ context.execution
1516
+ );
1521
1517
  if (request.diagnostics.length)
1522
1518
  throw Error(request.diagnostics.map((d) => d.message).join(" "));
1523
- if (matching.some((c) => c.scope === "shownRows") && !transport.metricCapabilities.shownRows)
1524
- throw Error("This server does not support shown-row metrics.");
1525
- if (request.profile && transport.metricCapabilities.profile && request.profile !== transport.metricCapabilities.profile)
1519
+ if (request.profile && capabilities.profile && request.profile !== capabilities.profile)
1526
1520
  throw Error(
1527
1521
  "Metric transport and computed execution profiles do not match. Refresh the handshake."
1528
1522
  );
@@ -1543,28 +1537,42 @@ async function executeMetrics(query, context, signal) {
1543
1537
  ...context.schema,
1544
1538
  fields: [...remoteFields.values()]
1545
1539
  };
1546
- if (matching.some(
1547
- (clause) => [
1540
+ const wire = new Map(request.metrics.map((m) => [m.id, m]));
1541
+ const limitation = (clause) => {
1542
+ if (clause.scope === "shownRows" && !capabilities.shownRows)
1543
+ return "This server does not support shown-row metrics.";
1544
+ if (!capabilities.computedFields && [
1548
1545
  ...clause.groupBy,
1549
1546
  ...metricPlans(clause, remoteSchema).flatMap(
1550
1547
  (plan) => plan.dependencies
1551
1548
  )
1552
- ].some(isComputedField3)
1553
- ) && !transport.metricCapabilities.computedFields)
1554
- throw Error("This metric server does not support computed fields.");
1555
- if (request.metrics.some(
1556
- (c) => !c.distribution && (c.expression !== void 0 || c.expressionY !== void 0)
1557
- ) && !transport.metricCapabilities.expressions)
1558
- throw Error(
1559
- "This server does not support composed metric expressions."
1549
+ ].some(isComputedField3))
1550
+ return "This metric server does not support computed fields.";
1551
+ const sent = wire.get(clause.id);
1552
+ if (!capabilities.expressions && sent && !sent.distribution && (sent.expression !== void 0 || sent.expressionY !== void 0))
1553
+ return "This server does not support composed metric expressions.";
1554
+ if (!capabilities.distributions && clause.distribution && (!clause.display || ["box", "histogram"].includes(clause.display.kind)))
1555
+ return "This server does not support distributions.";
1556
+ return void 0;
1557
+ };
1558
+ const rejected = /* @__PURE__ */ new Set();
1559
+ for (const clause of matching) {
1560
+ const error = limitation(clause);
1561
+ if (error) {
1562
+ rejected.add(clause.id);
1563
+ unsupported(clause, error);
1564
+ }
1565
+ }
1566
+ const metrics = request.metrics.filter((m) => !rejected.has(m.id));
1567
+ if (metrics.length)
1568
+ results.push(
1569
+ ...(await transport.fetchMetrics({ ...request, metrics }, signal)).metrics
1560
1570
  );
1561
- if (matching.some(
1562
- (c) => c.distribution && (!c.display || ["box", "histogram"].includes(c.display.kind))
1563
- ) && !transport.metricCapabilities.distributions)
1564
- throw Error("This server does not support distributions.");
1565
- results.push(...(await transport.fetchMetrics(request, signal)).metrics);
1566
1571
  } else if (transport?.fetchAggregations) {
1567
- const request = toAggregationQuery(scoped, context.schema);
1572
+ const request = toAggregationQuery(
1573
+ { ...query, aggregations: matching },
1574
+ context.schema
1575
+ );
1568
1576
  if (request.diagnostics?.length)
1569
1577
  throw Error(request.diagnostics.map((d) => d.message).join(" "));
1570
1578
  results.push(
@@ -2755,17 +2763,36 @@ function useQueryTable(opts) {
2755
2763
  );
2756
2764
  const executeV3Metrics = useCallback12(async (metricQuery, signal) => {
2757
2765
  if (!transport?.fetchQueryV3 || transport.queryCapabilities?.version !== 3) throw Error("The v3 query executor is unavailable.");
2758
- const request = toQueryRequestV3(
2759
- query.grouping ? { ...metricQuery, grouping: query.grouping } : metricQuery,
2760
- sourceSchema,
2761
- transport.queryCapabilities,
2762
- opts.computedExecution,
2763
- { now: groupingNow ?? (opts.evaluationNow ?? Date.now)(), rows: false }
2764
- );
2765
- if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2766
- const result = await transport.fetchQueryV3(request, signal);
2767
- validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
2768
- return { metrics: result.metrics };
2766
+ const capabilities = transport.queryCapabilities;
2767
+ const refused = /* @__PURE__ */ new Map();
2768
+ for (const clause of metricQuery.aggregations ?? []) {
2769
+ const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount });
2770
+ if (issue) refused.set(clause.id, issue);
2771
+ }
2772
+ const runnable = (metricQuery.aggregations ?? []).filter((clause) => !refused.has(clause.id));
2773
+ let metrics = [];
2774
+ if (runnable.length) {
2775
+ const scoped = { ...metricQuery, aggregations: runnable };
2776
+ const request = toQueryRequestV3(
2777
+ query.grouping ? { ...scoped, grouping: query.grouping } : scoped,
2778
+ sourceSchema,
2779
+ capabilities,
2780
+ opts.computedExecution,
2781
+ { now: groupingNow ?? (opts.evaluationNow ?? Date.now)(), rows: false }
2782
+ );
2783
+ if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2784
+ const result = await transport.fetchQueryV3(request, signal);
2785
+ validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
2786
+ metrics = result.metrics;
2787
+ }
2788
+ if (!refused.size) return { metrics };
2789
+ const byId = new Map(metrics.map((metric) => [metric.id, metric]));
2790
+ return { metrics: (metricQuery.aggregations ?? []).flatMap((clause) => {
2791
+ const issue = refused.get(clause.id);
2792
+ if (issue) return [{ id: clause.id, buckets: [], error: issue }];
2793
+ const metric = byId.get(clause.id);
2794
+ return metric ? [metric] : [];
2795
+ }) };
2769
2796
  }, [transport, sourceSchema, sourceCatalogue, schema, opts.computedExecution, groupKey, groupingNow, opts.evaluationNow]);
2770
2797
  const previewGrouping = useCallback12(async (draft, signal) => {
2771
2798
  if (!active || !storageReady || restoration.loading || restoration.error) throw Error("Wait for query restoration before previewing groups.");
@@ -3287,6 +3314,7 @@ export {
3287
3314
  useColumnDrag,
3288
3315
  useObjectReferences,
3289
3316
  usePropertyView,
3317
+ useQueryFilters,
3290
3318
  useQueryTable,
3291
3319
  useSavedQueries,
3292
3320
  useSelect,