@pythia-software/query-table-core 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 +221 -7
- package/dist/index.js +855 -98
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var options = {
|
|
|
16
16
|
number: numberOptions,
|
|
17
17
|
percent: numberOptions,
|
|
18
18
|
chip: { labels: "labels", tones: "tones" },
|
|
19
|
-
text: { truncate: "boolean" }
|
|
19
|
+
text: { truncate: "boolean", lines: [1, 20] }
|
|
20
20
|
};
|
|
21
21
|
function parseCellDisplay(raw, context = "cell display") {
|
|
22
22
|
const fail = (reason) => {
|
|
@@ -385,42 +385,63 @@ function normalizeAliases(rawAliases, rawAlias) {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
// src/itemPage.ts
|
|
388
|
-
|
|
388
|
+
var MAX_TEMPLATE_LENGTH = 8e3;
|
|
389
|
+
var BASE = "https://query-table.invalid/";
|
|
390
|
+
var ITEM_PAGE_TOKENS = ["id", "query"];
|
|
391
|
+
function placeholders(tokens) {
|
|
392
|
+
return new RegExp(`\\{(${[...tokens, "field:[^{}]+"].join("|")})\\}`, "g");
|
|
393
|
+
}
|
|
394
|
+
function httpOrRelative(href) {
|
|
395
|
+
try {
|
|
396
|
+
return ["http:", "https:"].includes(new URL(href, BASE).protocol);
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function templateFields(template) {
|
|
389
402
|
return [...new Set(Array.from(template.matchAll(/\{field:([^{}]+)\}/g), (match) => match[1]))];
|
|
390
403
|
}
|
|
391
|
-
function
|
|
392
|
-
if (template.length >
|
|
404
|
+
function urlTemplateProblem(template, tokens, hasField) {
|
|
405
|
+
if (template.length > MAX_TEMPLATE_LENGTH) return "Use a URL template of at most 8,000 characters.";
|
|
393
406
|
if (!template.trim()) return null;
|
|
394
|
-
const remainder = template.replace(
|
|
395
|
-
if (/[{}]/.test(remainder))
|
|
396
|
-
|
|
397
|
-
|
|
407
|
+
const remainder = template.replace(placeholders(tokens), "value");
|
|
408
|
+
if (/[{}]/.test(remainder)) {
|
|
409
|
+
const names = [...tokens.map((token) => `{${token}}`), "{field:field_name}"];
|
|
410
|
+
const list = names.length > 1 ? `${names.slice(0, -1).join(", ")}${names.length > 2 ? "," : ""} or ${names.at(-1)}` : names[0];
|
|
411
|
+
return `Use ${list} placeholders.`;
|
|
412
|
+
}
|
|
413
|
+
for (const name of templateFields(template)) {
|
|
414
|
+
if (!hasField(name)) return `Unknown URL field: ${name}.`;
|
|
398
415
|
}
|
|
399
416
|
try {
|
|
400
|
-
if (!["http:", "https:"].includes(new URL(remainder,
|
|
417
|
+
if (!["http:", "https:"].includes(new URL(remainder, BASE).protocol))
|
|
401
418
|
return "Use an HTTP, HTTPS, or relative page URL.";
|
|
402
419
|
} catch {
|
|
403
420
|
return "Enter a valid page URL.";
|
|
404
421
|
}
|
|
405
422
|
return null;
|
|
406
423
|
}
|
|
407
|
-
function
|
|
408
|
-
if (!template.trim() || validateItemPageTemplate(template, schema)) return null;
|
|
424
|
+
function fillUrlTemplate(template, tokens, read) {
|
|
409
425
|
let missing = false;
|
|
410
|
-
const href = template.replace(
|
|
411
|
-
const value =
|
|
426
|
+
const href = template.replace(placeholders(tokens), (_match, token) => {
|
|
427
|
+
const value = read(token);
|
|
412
428
|
if (value == null || !["string", "number", "boolean"].includes(typeof value)) {
|
|
413
429
|
missing = true;
|
|
414
430
|
return "";
|
|
415
431
|
}
|
|
416
432
|
return encodeURIComponent(String(value));
|
|
417
433
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
434
|
+
return missing || !httpOrRelative(href) ? null : href;
|
|
435
|
+
}
|
|
436
|
+
function itemPageFields(template) {
|
|
437
|
+
return templateFields(template);
|
|
438
|
+
}
|
|
439
|
+
function validateItemPageTemplate(template, schema) {
|
|
440
|
+
return urlTemplateProblem(template, ITEM_PAGE_TOKENS, (name) => schema.fields.some((field) => field.name === name));
|
|
441
|
+
}
|
|
442
|
+
function renderItemPageTemplate(template, schema, row, id, query) {
|
|
443
|
+
if (!template.trim() || validateItemPageTemplate(template, schema)) return null;
|
|
444
|
+
return fillUrlTemplate(template, ITEM_PAGE_TOKENS, (token) => token === "id" ? id : token === "query" ? query : readFieldValue(schema.fields.find((field) => field.name === token.slice(6)), row));
|
|
424
445
|
}
|
|
425
446
|
|
|
426
447
|
// src/metricNormalize.ts
|
|
@@ -444,7 +465,8 @@ function normalizeMetricOptions(raw) {
|
|
|
444
465
|
expression: "string",
|
|
445
466
|
expressionY: "string",
|
|
446
467
|
scope: ["allMatching", "shownRows"],
|
|
447
|
-
groupLimit: "integer"
|
|
468
|
+
groupLimit: "integer",
|
|
469
|
+
minCount: "positiveInteger"
|
|
448
470
|
})
|
|
449
471
|
);
|
|
450
472
|
if (raw.dateGrouping !== void 0) {
|
|
@@ -666,7 +688,50 @@ function normalizeSetFilter(item) {
|
|
|
666
688
|
if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
|
|
667
689
|
return { id, mode: item.mode, values: [...item.values] };
|
|
668
690
|
}
|
|
691
|
+
function normalizeWhereTerms(input) {
|
|
692
|
+
const where = [];
|
|
693
|
+
let literalBudget = MAX_WHERE_CLAUSES;
|
|
694
|
+
for (const item of input) {
|
|
695
|
+
if (literalBudget <= 0) break;
|
|
696
|
+
const start = where.length;
|
|
697
|
+
if (isRecord(item) && Array.isArray(item.any)) {
|
|
698
|
+
const members = [];
|
|
699
|
+
for (const inner of item.any) {
|
|
700
|
+
if (literalBudget <= 0) break;
|
|
701
|
+
const clause = normalizeWhereClause(inner);
|
|
702
|
+
if (clause) {
|
|
703
|
+
members.push(clause);
|
|
704
|
+
literalBudget--;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (members.length === 1) where.push(members[0]);
|
|
708
|
+
else if (members.length > 1) where.push({ any: members });
|
|
709
|
+
} else {
|
|
710
|
+
const clause = normalizeWhereClause(item);
|
|
711
|
+
if (clause) {
|
|
712
|
+
where.push(clause);
|
|
713
|
+
literalBudget--;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (where.length > start && isRecord(item)) {
|
|
717
|
+
const metadata = normalizeSetFilter(item.setFilter);
|
|
718
|
+
if (metadata) where[start].setFilter = metadata;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return where;
|
|
722
|
+
}
|
|
723
|
+
function normalizeMetricWhere(value) {
|
|
724
|
+
if (value === void 0) return { invalid: false };
|
|
725
|
+
if (!Array.isArray(value)) return { invalid: true };
|
|
726
|
+
const where = normalizeWhereTerms(value);
|
|
727
|
+
const supplied = value.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
|
|
728
|
+
const kept = where.reduce((count, term) => count + predicatesOf(term).length, 0);
|
|
729
|
+
return { ...where.length ? { where } : {}, invalid: supplied !== kept };
|
|
730
|
+
}
|
|
669
731
|
function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
732
|
+
return normalizeQuery(input, fallback, true);
|
|
733
|
+
}
|
|
734
|
+
function normalizeQuery(input, fallback, links) {
|
|
670
735
|
const raw = isRecord(input) ? input : {};
|
|
671
736
|
const select = [];
|
|
672
737
|
if (Array.isArray(raw.select)) {
|
|
@@ -678,44 +743,18 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
678
743
|
if (typeof item.width === "number" && Number.isFinite(item.width)) {
|
|
679
744
|
column.width = boundedInteger(item.width, MIN_COLUMN_WIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
|
|
680
745
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
const where = [];
|
|
687
|
-
if (Array.isArray(raw.where)) {
|
|
688
|
-
let literalBudget = MAX_WHERE_CLAUSES;
|
|
689
|
-
for (const item of raw.where) {
|
|
690
|
-
if (literalBudget <= 0) break;
|
|
691
|
-
const start = where.length;
|
|
692
|
-
if (isRecord(item) && Array.isArray(item.any)) {
|
|
693
|
-
const members = [];
|
|
694
|
-
for (const inner of item.any) {
|
|
695
|
-
if (literalBudget <= 0) break;
|
|
696
|
-
const clause = normalizeWhereClause(inner);
|
|
697
|
-
if (clause) {
|
|
698
|
-
members.push(clause);
|
|
699
|
-
literalBudget--;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (members.length === 1) where.push(members[0]);
|
|
703
|
-
else if (members.length > 1) where.push({ any: members });
|
|
704
|
-
} else {
|
|
705
|
-
const clause = normalizeWhereClause(item);
|
|
706
|
-
if (clause) {
|
|
707
|
-
where.push(clause);
|
|
708
|
-
literalBudget--;
|
|
746
|
+
if (item.display !== void 0) {
|
|
747
|
+
try {
|
|
748
|
+
column.display = parseCellDisplay(item.display, `column ${field} display`);
|
|
749
|
+
} catch {
|
|
709
750
|
}
|
|
710
751
|
}
|
|
711
|
-
|
|
712
|
-
const metadata = normalizeSetFilter(item.setFilter);
|
|
713
|
-
if (metadata) where[start].setFilter = metadata;
|
|
714
|
-
}
|
|
752
|
+
select.push(column);
|
|
715
753
|
}
|
|
716
754
|
} else {
|
|
717
|
-
|
|
755
|
+
select.push(...fallback.select.map((column) => ({ ...column, ...column.display ? { display: JSON.parse(JSON.stringify(column.display)) } : {} })));
|
|
718
756
|
}
|
|
757
|
+
const where = Array.isArray(raw.where) ? normalizeWhereTerms(raw.where) : fallback.where.map(cloneWhereTerm);
|
|
719
758
|
const orderBy = [];
|
|
720
759
|
if (Array.isArray(raw.orderBy)) {
|
|
721
760
|
for (const item of raw.orderBy.slice(0, MAX_ORDER_BY_TERMS)) {
|
|
@@ -742,7 +781,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
742
781
|
offset: boundedInteger(raw.offset, fallback.offset, 0, MAX_QUERY_OFFSET)
|
|
743
782
|
};
|
|
744
783
|
if (raw.fitToScreen === true) out.fitToScreen = true;
|
|
745
|
-
if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping);
|
|
784
|
+
if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping, links);
|
|
746
785
|
if (Array.isArray(raw.aggregations)) {
|
|
747
786
|
const aggregations = [];
|
|
748
787
|
for (const item of raw.aggregations.slice(0, MAX_AGGREGATIONS)) {
|
|
@@ -758,6 +797,9 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
758
797
|
if (field) aggregation.field = field;
|
|
759
798
|
if (label) aggregation.label = label;
|
|
760
799
|
Object.assign(aggregation, normalizeMetricOptions(item));
|
|
800
|
+
const metricWhere = normalizeMetricWhere(item.where);
|
|
801
|
+
if (metricWhere.where) aggregation.where = metricWhere.where;
|
|
802
|
+
if (metricWhere.invalid) aggregation.diagnostics = [...aggregation.diagnostics ?? [], "Invalid metric filter."];
|
|
761
803
|
if (!Array.isArray(item.groupBy) || rawGroups.length > MAX_GROUP_BY_FIELDS || groupBy.length !== rawGroups.length) aggregation.diagnostics = [...aggregation.diagnostics ?? [], "Invalid metric grouping fields or grouping budget exceeded."];
|
|
762
804
|
const unsupportedOp = boundedString(item.unsupportedOp ?? (!validOp ? item.op : void 0), MAX_FIELD_NAME_LENGTH);
|
|
763
805
|
if (unsupportedOp) aggregation.unsupportedOp = unsupportedOp;
|
|
@@ -804,60 +846,147 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
804
846
|
}
|
|
805
847
|
return out;
|
|
806
848
|
}
|
|
807
|
-
function normalizeGroupingState(value) {
|
|
849
|
+
function normalizeGroupingState(value, links) {
|
|
808
850
|
const raw = isRecord(value) ? value : {};
|
|
809
851
|
const diagnostics = [];
|
|
810
|
-
const fail = () => {
|
|
811
|
-
diagnostics.push(
|
|
852
|
+
const fail = (message) => {
|
|
853
|
+
diagnostics.push(`Invalid grouping: ${message}`);
|
|
812
854
|
};
|
|
813
855
|
const keys = Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS).filter((key) => !!boundedString(key, MAX_FIELD_NAME_LENGTH)) : [];
|
|
814
|
-
if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keys).size !== keys.length)
|
|
856
|
+
if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keys).size !== keys.length)
|
|
857
|
+
fail(`keys must list 1\u2013${MAX_GROUP_BY_FIELDS} distinct field names.`);
|
|
815
858
|
const summaries = [];
|
|
816
|
-
if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail();
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
859
|
+
if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail("summaries must be a list of at most 64 columns.");
|
|
860
|
+
const summaryIds = /* @__PURE__ */ new Set();
|
|
861
|
+
for (const [index, item] of (Array.isArray(raw.summaries) ? raw.summaries.slice(0, 64) : []).entries()) {
|
|
862
|
+
const name = isRecord(item) && boundedString(item.id, MAX_FIELD_NAME_LENGTH) ? `summary "${item.id}"` : `summary ${index + 1}`;
|
|
863
|
+
if (!isRecord(item) || !boundedString(item.id, MAX_FIELD_NAME_LENGTH)) {
|
|
864
|
+
fail(`${name} needs an id of 1\u2013${MAX_FIELD_NAME_LENGTH} characters.`);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (typeof item.op !== "string" || !AGG_OPS.has(item.op) && item.op !== "row" && item.op !== "formula" && (item.op !== "link" || !links)) {
|
|
868
|
+
fail(item.op === "link" ? `${name} is a link column; a link's patch cannot contain link columns.` : `${name} has unsupported op ${JSON.stringify(String(item.op).slice(0, 40))}; use count, count_distinct, sum, avg, min, max, row, formula, or link${String(item.op).toLowerCase() === "median" ? ' (for a median, use op "formula" with expression "MEDIAN([field])")' : ""}.`);
|
|
820
869
|
continue;
|
|
821
870
|
}
|
|
822
871
|
const column = { id: item.id, op: item.op };
|
|
872
|
+
if (summaryIds.has(column.id)) fail(`${name} uses the same id as another summary column.`);
|
|
873
|
+
else if (keys.includes(column.id)) fail(`${name} uses the same id as group key "${column.id}"; summary ids must differ from source field names.`);
|
|
874
|
+
summaryIds.add(column.id);
|
|
875
|
+
if (item.op === "link") {
|
|
876
|
+
const link = normalizeLinkSpec(item.link);
|
|
877
|
+
if (link) column.link = link;
|
|
878
|
+
if (item.label !== void 0) {
|
|
879
|
+
if (typeof item.label !== "string" || item.label.length > MAX_LABEL_LENGTH) fail(`${name} label must be text of at most ${MAX_LABEL_LENGTH.toLocaleString("en-US")} characters.`);
|
|
880
|
+
else column.label = item.label;
|
|
881
|
+
}
|
|
882
|
+
summaries.push(column);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
823
885
|
if (item.field !== void 0) {
|
|
824
|
-
if (!boundedString(item.field, MAX_FIELD_NAME_LENGTH)) fail();
|
|
886
|
+
if (!boundedString(item.field, MAX_FIELD_NAME_LENGTH)) fail(`${name} has an invalid field name.`);
|
|
825
887
|
else column.field = item.field;
|
|
826
888
|
}
|
|
827
889
|
if (item.op === "formula") {
|
|
828
|
-
if (typeof item.expression !== "string" || item.expression.length > 1e4) fail();
|
|
890
|
+
if (typeof item.expression !== "string" || item.expression.length > 1e4) fail(`${name} needs an expression of at most 10,000 characters.`);
|
|
829
891
|
else column.expression = item.expression;
|
|
830
892
|
}
|
|
831
893
|
if (item.label !== void 0) {
|
|
832
|
-
if (typeof item.label !== "string" || item.label.length > MAX_LABEL_LENGTH) fail();
|
|
894
|
+
if (typeof item.label !== "string" || item.label.length > MAX_LABEL_LENGTH) fail(`${name} label must be text of at most ${MAX_LABEL_LENGTH.toLocaleString("en-US")} characters.`);
|
|
833
895
|
else column.label = item.label;
|
|
834
896
|
}
|
|
897
|
+
if (item.display !== void 0) {
|
|
898
|
+
try {
|
|
899
|
+
column.display = parseCellDisplay(item.display, `${name} display`);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
fail(`${error instanceof Error ? error.message : String(error)}.`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
835
904
|
summaries.push(column);
|
|
836
905
|
}
|
|
837
906
|
let representative = null;
|
|
838
907
|
if (raw.representative != null) {
|
|
839
908
|
const ordering = isRecord(raw.representative) ? raw.representative.orderBy : void 0;
|
|
840
909
|
const orderBy = [];
|
|
841
|
-
if (!Array.isArray(ordering) || !ordering.length || ordering.length > 8) fail();
|
|
910
|
+
if (!Array.isArray(ordering) || !ordering.length || ordering.length > 8) fail("representative ordering must list 1\u20138 fields.");
|
|
842
911
|
for (const term of Array.isArray(ordering) ? ordering.slice(0, 8) : []) {
|
|
843
912
|
if (!isRecord(term) || !boundedString(term.field, MAX_FIELD_NAME_LENGTH) || !["asc", "desc"].includes(String(term.dir)) || term.nulls !== void 0 && !["first", "last"].includes(String(term.nulls))) {
|
|
844
|
-
fail();
|
|
913
|
+
fail('each representative ordering term needs a field, dir "asc" or "desc", and optional nulls "first" or "last".');
|
|
845
914
|
continue;
|
|
846
915
|
}
|
|
847
916
|
orderBy.push({ field: term.field, dir: term.dir, nulls: term.nulls === "first" ? "first" : "last" });
|
|
848
917
|
}
|
|
849
918
|
representative = { orderBy };
|
|
850
919
|
}
|
|
851
|
-
if (!isRecord(raw.source)
|
|
920
|
+
if (!isRecord(raw.source)) fail("source must be the record query that is grouped.");
|
|
921
|
+
else if (raw.source.grouping != null) fail("only one grouping stage is supported; the source query cannot itself be grouped.");
|
|
852
922
|
const { grouping: _nested, ...source } = isRecord(raw.source) ? raw.source : {};
|
|
853
|
-
if (Array.isArray(raw.diagnostics)
|
|
923
|
+
if (Array.isArray(raw.diagnostics))
|
|
924
|
+
for (const message of raw.diagnostics.slice(0, 20))
|
|
925
|
+
diagnostics.push(typeof message === "string" && message ? message.slice(0, MAX_LABEL_LENGTH) : "Invalid grouping definition.");
|
|
854
926
|
const normalizedSource = normalizeQueryState(source);
|
|
855
927
|
if (Array.isArray(source.where)) {
|
|
856
928
|
const supplied = source.where.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
|
|
857
|
-
if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail();
|
|
929
|
+
if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail("source filters contain malformed predicates or exceed the predicate budget.");
|
|
858
930
|
}
|
|
859
931
|
return { keys, summaries, representative, source: normalizedSource, ...diagnostics.length ? { diagnostics: [...new Set(diagnostics)] } : {} };
|
|
860
932
|
}
|
|
933
|
+
var QUERY_FACETS = /* @__PURE__ */ new Set(["filters", "columns", "order", "grouping", "metrics", "limit"]);
|
|
934
|
+
var PATCH_QUERY_KEYS = ["select", "where", "orderBy", "limit", "aggregations", "grouping", "fitToScreen"];
|
|
935
|
+
var MAX_LINK_PARAMS = 50;
|
|
936
|
+
function normalizeLinkSpec(value) {
|
|
937
|
+
if (!isRecord(value) || !isRecord(value.to)) return void 0;
|
|
938
|
+
let label;
|
|
939
|
+
if (typeof value.label === "string" && value.label.length <= MAX_LABEL_LENGTH) label = value.label;
|
|
940
|
+
else if (isRecord(value.label) && boundedString(value.label.column, MAX_FIELD_NAME_LENGTH)) label = { column: value.label.column };
|
|
941
|
+
else return void 0;
|
|
942
|
+
const to = value.to;
|
|
943
|
+
if (to.kind === "template") {
|
|
944
|
+
if (typeof to.template !== "string" || to.template.length > 8e3) return void 0;
|
|
945
|
+
return { label, to: { kind: "template", template: to.template } };
|
|
946
|
+
}
|
|
947
|
+
if (to.kind !== "query" || !boundedString(to.target, MAX_FIELD_NAME_LENGTH)) return void 0;
|
|
948
|
+
const destination = { kind: "query", target: to.target };
|
|
949
|
+
if (to.base !== void 0) {
|
|
950
|
+
if (to.base !== "source" && to.base !== "empty") return void 0;
|
|
951
|
+
destination.base = to.base;
|
|
952
|
+
}
|
|
953
|
+
if (to.keyFilters !== void 0) {
|
|
954
|
+
if (typeof to.keyFilters !== "boolean") return void 0;
|
|
955
|
+
destination.keyFilters = to.keyFilters;
|
|
956
|
+
}
|
|
957
|
+
if (to.patch !== void 0) {
|
|
958
|
+
const patch = normalizeLinkPatch(to.patch);
|
|
959
|
+
if (!patch) return void 0;
|
|
960
|
+
destination.patch = patch;
|
|
961
|
+
}
|
|
962
|
+
if (to.params !== void 0) {
|
|
963
|
+
if (!isRecord(to.params)) return void 0;
|
|
964
|
+
const entries = Object.entries(to.params);
|
|
965
|
+
if (entries.length > MAX_LINK_PARAMS || entries.some(([key, item]) => !boundedString(key, MAX_FIELD_NAME_LENGTH) || typeof item !== "string" || item.length > MAX_FILTER_VALUE_LENGTH))
|
|
966
|
+
return void 0;
|
|
967
|
+
destination.params = Object.fromEntries(entries);
|
|
968
|
+
}
|
|
969
|
+
return { label, to: destination };
|
|
970
|
+
}
|
|
971
|
+
function keptEveryTerm(supplied, kept) {
|
|
972
|
+
if (!Array.isArray(supplied)) return false;
|
|
973
|
+
const count = supplied.reduce((total, term) => total + (isRecord(term) && Array.isArray(term.any) ? Math.max(1, term.any.length) : 1), 0);
|
|
974
|
+
return count === kept.reduce((total, term) => total + predicatesOf(term).length, 0);
|
|
975
|
+
}
|
|
976
|
+
function normalizeLinkPatch(value) {
|
|
977
|
+
if (!isRecord(value) || !boundedString(value.id, MAX_FIELD_NAME_LENGTH) || !Array.isArray(value.owns) || !isRecord(value.query)) return void 0;
|
|
978
|
+
if (value.owns.length > QUERY_FACETS.size || value.owns.some((facet) => typeof facet !== "string" || !QUERY_FACETS.has(facet))) return void 0;
|
|
979
|
+
const raw = value.query;
|
|
980
|
+
const normalized = normalizeQuery(raw, EMPTY_QUERY, false);
|
|
981
|
+
if (normalized.grouping?.diagnostics) return void 0;
|
|
982
|
+
if (raw.where !== void 0 && !keptEveryTerm(raw.where, normalized.where)) return void 0;
|
|
983
|
+
if (raw.aggregations !== void 0 && (!Array.isArray(raw.aggregations) || raw.aggregations.length !== (normalized.aggregations?.length ?? 0) || normalized.aggregations?.some((clause) => clause.diagnostics?.length))) return void 0;
|
|
984
|
+
const query = {};
|
|
985
|
+
for (const key of PATCH_QUERY_KEYS) {
|
|
986
|
+
if (raw[key] !== void 0 && normalized[key] !== void 0) query[key] = normalized[key];
|
|
987
|
+
}
|
|
988
|
+
return { id: value.id, owns: [...new Set(value.owns)], query };
|
|
989
|
+
}
|
|
861
990
|
function queriesEqual(a, b) {
|
|
862
991
|
if (a === b) return true;
|
|
863
992
|
if (a.limit !== b.limit || a.offset !== b.offset) return false;
|
|
@@ -1051,15 +1180,26 @@ function isGroupable(field) {
|
|
|
1051
1180
|
import { deflateSync, Inflate } from "fflate";
|
|
1052
1181
|
|
|
1053
1182
|
// src/setFilter.ts
|
|
1054
|
-
function
|
|
1183
|
+
function setFilterTarget(field) {
|
|
1184
|
+
if (field.filter?.editor !== "set") return null;
|
|
1185
|
+
if (field.type === "textarray") return "array";
|
|
1186
|
+
if (field.type === "enum" || field.type === "text" && filterValues(field).source === "static") return "scalar";
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
function setFilterModes(target) {
|
|
1190
|
+
return target === "array" ? ["any", "all", "none", "empty"] : ["any", "none"];
|
|
1191
|
+
}
|
|
1192
|
+
function createSetFilter(field, mode, values, id, target = "array") {
|
|
1193
|
+
if (!setFilterModes(target).includes(mode)) throw new Error(`${mode.toUpperCase()} set filters require a textarray field.`);
|
|
1055
1194
|
const keys = [...new Set(values)];
|
|
1056
1195
|
if (mode !== "empty" && keys.length === 0) return [];
|
|
1057
|
-
const
|
|
1196
|
+
const members = keys.map((value) => ({ field, op: target === "array" ? "includes" : "=", value }));
|
|
1197
|
+
const isNull = { field, op: "is_null", value: "" };
|
|
1058
1198
|
let terms;
|
|
1059
|
-
if (mode === "empty") terms = [
|
|
1060
|
-
else if (mode === "any") terms =
|
|
1061
|
-
else if (mode === "all") terms =
|
|
1062
|
-
else terms =
|
|
1199
|
+
if (mode === "empty") terms = [isNull];
|
|
1200
|
+
else if (mode === "any") terms = members.length === 1 ? members : [{ any: members }];
|
|
1201
|
+
else if (mode === "all") terms = members;
|
|
1202
|
+
else terms = members.map((clause) => ({ any: [isNull, target === "array" ? { ...clause, negated: true } : { ...clause, op: "!=" }] }));
|
|
1063
1203
|
return terms.map((term) => ({ ...term, setFilter: { id, mode, values: mode === "empty" ? [] : [...keys] } }));
|
|
1064
1204
|
}
|
|
1065
1205
|
function stripSetFilter(term) {
|
|
@@ -1072,14 +1212,15 @@ function readSetFilter(where, index) {
|
|
|
1072
1212
|
if (!term || !metadata || where[index - 1]?.setFilter?.id === metadata.id) return null;
|
|
1073
1213
|
const field = predicatesOf(term)[0]?.field;
|
|
1074
1214
|
if (!field) return null;
|
|
1075
|
-
const
|
|
1215
|
+
const target = predicatesOf(term).some((clause) => clause.op === "includes") || !setFilterModes("scalar").includes(metadata.mode) ? "array" : "scalar";
|
|
1216
|
+
const expected = createSetFilter(field, metadata.mode, metadata.values, metadata.id, target);
|
|
1076
1217
|
if (expected.length === 0) return null;
|
|
1077
1218
|
for (let offset = 0; offset < expected.length; offset++) {
|
|
1078
1219
|
const actual = where[index + offset];
|
|
1079
1220
|
if (!actual || JSON.stringify(actual.setFilter) !== JSON.stringify(expected[offset].setFilter) || JSON.stringify(stripSetFilter(actual)) !== JSON.stringify(stripSetFilter(expected[offset]))) return null;
|
|
1080
1221
|
}
|
|
1081
1222
|
if (where[index + expected.length]?.setFilter?.id === metadata.id) return null;
|
|
1082
|
-
return { field, metadata, count: expected.length };
|
|
1223
|
+
return { field, metadata, count: expected.length, target };
|
|
1083
1224
|
}
|
|
1084
1225
|
function mapSetFilterValues(where, canonicalize) {
|
|
1085
1226
|
const result = [];
|
|
@@ -1089,7 +1230,7 @@ function mapSetFilterValues(where, canonicalize) {
|
|
|
1089
1230
|
result.push(where[index]);
|
|
1090
1231
|
continue;
|
|
1091
1232
|
}
|
|
1092
|
-
result.push(...createSetFilter(set.field, set.metadata.mode, set.metadata.values.map((value) => canonicalize(set.field, value)), set.metadata.id));
|
|
1233
|
+
result.push(...createSetFilter(set.field, set.metadata.mode, set.metadata.values.map((value) => canonicalize(set.field, value)), set.metadata.id, set.target));
|
|
1093
1234
|
index += set.count - 1;
|
|
1094
1235
|
}
|
|
1095
1236
|
return result;
|
|
@@ -1119,7 +1260,7 @@ function compactQuery(q) {
|
|
|
1119
1260
|
q = normalizeQueryState(q);
|
|
1120
1261
|
const c = {};
|
|
1121
1262
|
if (q.select.length)
|
|
1122
|
-
c.s = q.select.map((col) => col.width != null ? [col.field, col.width] : [col.field]);
|
|
1263
|
+
c.s = q.select.map((col) => col.display ? [col.field, col.width ?? null, col.display] : col.width != null ? [col.field, col.width] : [col.field]);
|
|
1123
1264
|
if (q.where.length) c.w = q.where;
|
|
1124
1265
|
if (q.orderBy.length) c.o = q.orderBy;
|
|
1125
1266
|
c.l = q.limit;
|
|
@@ -1145,7 +1286,7 @@ function packQuery(c) {
|
|
|
1145
1286
|
const terms = [];
|
|
1146
1287
|
for (let i = 0; i < c.w.length; i++) {
|
|
1147
1288
|
const set = readSetFilter(c.w, i);
|
|
1148
|
-
if (set) {
|
|
1289
|
+
if (set?.target === "array") {
|
|
1149
1290
|
terms.push(["set", set.field, set.metadata.mode, set.metadata.values, set.metadata.id]);
|
|
1150
1291
|
i += set.count - 1;
|
|
1151
1292
|
} else terms.push(c.w[i]);
|
|
@@ -1283,7 +1424,10 @@ function normalizeSelect(s) {
|
|
|
1283
1424
|
return s.map((item) => {
|
|
1284
1425
|
if (typeof item === "string") return { field: item };
|
|
1285
1426
|
if (Array.isArray(item) && typeof item[0] === "string") {
|
|
1286
|
-
|
|
1427
|
+
const column = { field: item[0] };
|
|
1428
|
+
if (typeof item[1] === "number") column.width = item[1];
|
|
1429
|
+
if (item[2] !== void 0) column.display = item[2];
|
|
1430
|
+
return column;
|
|
1287
1431
|
}
|
|
1288
1432
|
return null;
|
|
1289
1433
|
}).filter((x) => x != null);
|
|
@@ -1337,7 +1481,7 @@ function toAggregationQuery(q, schema) {
|
|
|
1337
1481
|
const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
|
|
1338
1482
|
q = normalizeQueryState(q);
|
|
1339
1483
|
if (q.aggregations?.some((a) => a.dateGrouping)) throw new Error("Date grouping currently requires local metric execution.");
|
|
1340
|
-
if (q.aggregations?.some((a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.distribution || a.scope === "shownRows" || a.sort?.length || a.groupLimit)) throw new Error("Metric definition requires the version 2 metric transport.");
|
|
1484
|
+
if (q.aggregations?.some((a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.distribution || a.scope === "shownRows" || a.sort?.length || a.groupLimit || a.where?.length || a.minCount !== void 0)) throw new Error("Metric definition requires the version 2 metric transport.");
|
|
1341
1485
|
const byName = indexFields(schema);
|
|
1342
1486
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
1343
1487
|
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2)).map(stripSetFilter);
|
|
@@ -1830,6 +1974,15 @@ var FORMULA_FUNCTIONS = [
|
|
|
1830
1974
|
f("LENGTH", "LENGTH(text)", "Count Unicode code points.", 1, 1, "number", [
|
|
1831
1975
|
"text"
|
|
1832
1976
|
]),
|
|
1977
|
+
f(
|
|
1978
|
+
"URL_ENCODE",
|
|
1979
|
+
"URL_ENCODE(text)",
|
|
1980
|
+
"Percent-encode UTF-8 text for a URL. Only letters, digits and - . _ ~ are kept.",
|
|
1981
|
+
1,
|
|
1982
|
+
1,
|
|
1983
|
+
"text",
|
|
1984
|
+
["text"]
|
|
1985
|
+
),
|
|
1833
1986
|
...["LOWER", "UPPER", "TRIM", "LTRIM", "RTRIM"].map(
|
|
1834
1987
|
(n) => f(
|
|
1835
1988
|
n,
|
|
@@ -2202,6 +2355,14 @@ function compileFormula(source, fields, resolve, extension) {
|
|
|
2202
2355
|
} catch {
|
|
2203
2356
|
throw new FormulaError("Invalid JSON string literal.", from, pos);
|
|
2204
2357
|
}
|
|
2358
|
+
} else if (source[pos] === "'") {
|
|
2359
|
+
const end = source.indexOf("'", pos + 1);
|
|
2360
|
+
const text = end < 0 ? "" : source.slice(pos + 1, end);
|
|
2361
|
+
throw new FormulaError(
|
|
2362
|
+
end < 0 || text.length > 40 || /["\\\n]/.test(text) ? 'Text literals use double quotes ("text"), not single quotes.' : `Text literals use double quotes: write "${text}" instead of '${text}'.`,
|
|
2363
|
+
from,
|
|
2364
|
+
end < 0 ? pos + 1 : end + 1
|
|
2365
|
+
);
|
|
2205
2366
|
} else if (match = source.slice(pos).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)) {
|
|
2206
2367
|
pos += match[0].length;
|
|
2207
2368
|
tokens.push({ kind: "number", text: match[0], from, to: pos });
|
|
@@ -2295,6 +2456,12 @@ function compileFormula(source, fields, resolve, extension) {
|
|
|
2295
2456
|
const op = take();
|
|
2296
2457
|
const p = priority[op.text];
|
|
2297
2458
|
if (op.text === "IN") {
|
|
2459
|
+
if (peek().text !== "(")
|
|
2460
|
+
throw new FormulaError(
|
|
2461
|
+
'IN takes a parenthesized list, e.g. [status] IN ("a", "b"). To test array membership, use ARRAY_CONTAINS([tags], "a").',
|
|
2462
|
+
op.from,
|
|
2463
|
+
peek().to
|
|
2464
|
+
);
|
|
2298
2465
|
expect("(");
|
|
2299
2466
|
const args = [node];
|
|
2300
2467
|
do {
|
|
@@ -2313,6 +2480,12 @@ function compileFormula(source, fields, resolve, extension) {
|
|
|
2313
2480
|
node = call(`op:${op.text}`, [node, right], node.from, right.to);
|
|
2314
2481
|
}
|
|
2315
2482
|
}
|
|
2483
|
+
if (peek().text === "NOT" && tokens[i + 1]?.text === "IN")
|
|
2484
|
+
throw new FormulaError(
|
|
2485
|
+
'NOT IN is not supported. Write NOT ([status] IN ("a", "b")), or combine != comparisons with AND.',
|
|
2486
|
+
peek().from,
|
|
2487
|
+
tokens[i + 1].to
|
|
2488
|
+
);
|
|
2316
2489
|
depth--;
|
|
2317
2490
|
return node;
|
|
2318
2491
|
}
|
|
@@ -2376,6 +2549,12 @@ function compileFormula(source, fields, resolve, extension) {
|
|
|
2376
2549
|
ts.forEach((_, j) => need(j, "number"));
|
|
2377
2550
|
return "number";
|
|
2378
2551
|
}
|
|
2552
|
+
if (op === "IN" && ts.some((t) => t === "textarray"))
|
|
2553
|
+
throw new FormulaError(
|
|
2554
|
+
'IN compares one value with a list of values. For arrays, use ARRAY_CONTAINS([tags], "a"), or OR several ARRAY_CONTAINS calls.',
|
|
2555
|
+
n.from,
|
|
2556
|
+
n.to
|
|
2557
|
+
);
|
|
2379
2558
|
same(ts, n);
|
|
2380
2559
|
return "bool";
|
|
2381
2560
|
}
|
|
@@ -2630,6 +2809,14 @@ function formulaRuntime(ast, inputs, extension) {
|
|
|
2630
2809
|
case "LENGTH":
|
|
2631
2810
|
out = Array.from(str(a)).length;
|
|
2632
2811
|
break;
|
|
2812
|
+
case "URL_ENCODE":
|
|
2813
|
+
if (!/^(?:[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff])*$/.test(str(a)))
|
|
2814
|
+
fail("Text is not valid Unicode.");
|
|
2815
|
+
out = encodeURIComponent(str(a)).replace(
|
|
2816
|
+
/[!'()*]/g,
|
|
2817
|
+
(ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`
|
|
2818
|
+
);
|
|
2819
|
+
break;
|
|
2633
2820
|
case "LOWER":
|
|
2634
2821
|
out = str(a).toLowerCase();
|
|
2635
2822
|
break;
|
|
@@ -2907,7 +3094,40 @@ function formulaRuntime(ast, inputs, extension) {
|
|
|
2907
3094
|
}
|
|
2908
3095
|
}
|
|
2909
3096
|
|
|
3097
|
+
// src/groupKeys.ts
|
|
3098
|
+
function datetimeValue(value) {
|
|
3099
|
+
const time = value instanceof Date ? value.getTime() : typeof value === "number" ? value : typeof value === "string" && value !== "" ? Date.parse(value) : NaN;
|
|
3100
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
3101
|
+
}
|
|
3102
|
+
function keyClause(field, value) {
|
|
3103
|
+
if (!field || field.type === "textarray" || !isFilterable(field)) return null;
|
|
3104
|
+
if (value == null) return { field: field.name, op: "is_null", value: "" };
|
|
3105
|
+
if (field.type === "datetime") {
|
|
3106
|
+
const iso = datetimeValue(value);
|
|
3107
|
+
return iso === null ? null : { field: field.name, op: "=", value: iso };
|
|
3108
|
+
}
|
|
3109
|
+
if (typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" && typeof value !== "boolean")
|
|
3110
|
+
return null;
|
|
3111
|
+
return { field: field.name, op: "=", value: String(value) };
|
|
3112
|
+
}
|
|
3113
|
+
function groupKeyTerms(keys, group, schema) {
|
|
3114
|
+
const terms = [];
|
|
3115
|
+
for (const key of keys) {
|
|
3116
|
+
const term = keyClause(resolveField(schema, key), group[key]);
|
|
3117
|
+
if (!term) return null;
|
|
3118
|
+
terms.push(term);
|
|
3119
|
+
}
|
|
3120
|
+
return terms;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
2910
3123
|
// src/grouping.ts
|
|
3124
|
+
var GROUP_LINK_RENDER = "qt_group_link";
|
|
3125
|
+
function isGroupLinkValue(value) {
|
|
3126
|
+
return value != null && typeof value === "object" && value.kind === "group-link";
|
|
3127
|
+
}
|
|
3128
|
+
function isGroupLinkField(field) {
|
|
3129
|
+
return field?.render === GROUP_LINK_RENDER;
|
|
3130
|
+
}
|
|
2911
3131
|
var GROUP_ID_FIELD = "__qt_group_id__";
|
|
2912
3132
|
var MAX_REPRESENTATIVE_SORTS = 8;
|
|
2913
3133
|
var MAX_SUMMARY_COLUMNS = 64;
|
|
@@ -2916,6 +3136,38 @@ function withoutGrouping(query) {
|
|
|
2916
3136
|
const { grouping: _grouping, ...result } = query;
|
|
2917
3137
|
return result;
|
|
2918
3138
|
}
|
|
3139
|
+
function exitGrouping(query) {
|
|
3140
|
+
return query.grouping ? query.grouping.source : query;
|
|
3141
|
+
}
|
|
3142
|
+
function withGroupingKeys(query, keys) {
|
|
3143
|
+
const before = query.grouping;
|
|
3144
|
+
if (!keys.length) return exitGrouping(query);
|
|
3145
|
+
if (!before)
|
|
3146
|
+
return {
|
|
3147
|
+
select: keys.map((field) => ({ field })),
|
|
3148
|
+
where: [],
|
|
3149
|
+
orderBy: [],
|
|
3150
|
+
limit: query.limit,
|
|
3151
|
+
offset: 0,
|
|
3152
|
+
grouping: { keys, summaries: [], representative: null, source: query }
|
|
3153
|
+
};
|
|
3154
|
+
const removed = new Set(before.keys.filter((key) => !keys.includes(key)));
|
|
3155
|
+
const columns = query.select.length ? query.select : before.keys.map((field) => ({ field }));
|
|
3156
|
+
return {
|
|
3157
|
+
...query,
|
|
3158
|
+
grouping: { ...before, keys },
|
|
3159
|
+
offset: 0,
|
|
3160
|
+
aggregations: [],
|
|
3161
|
+
select: [
|
|
3162
|
+
...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
|
|
3163
|
+
...columns.filter((c) => !removed.has(c.field))
|
|
3164
|
+
],
|
|
3165
|
+
where: query.where.filter(
|
|
3166
|
+
(term) => !predicatesOf(term).some((p) => removed.has(p.field))
|
|
3167
|
+
),
|
|
3168
|
+
orderBy: query.orderBy.filter((term) => !removed.has(term.field))
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
2919
3171
|
var OP_LABEL = {
|
|
2920
3172
|
count: "COUNT",
|
|
2921
3173
|
count_distinct: "DISTINCT",
|
|
@@ -2924,7 +3176,8 @@ var OP_LABEL = {
|
|
|
2924
3176
|
min: "MIN",
|
|
2925
3177
|
max: "MAX",
|
|
2926
3178
|
row: "ROW",
|
|
2927
|
-
formula: "\u0192x"
|
|
3179
|
+
formula: "\u0192x",
|
|
3180
|
+
link: "LINK"
|
|
2928
3181
|
};
|
|
2929
3182
|
function createGroupingModel(inputSchema) {
|
|
2930
3183
|
const sourceField = (name) => inputSchema.fields.find((f2) => f2.name === name);
|
|
@@ -2949,6 +3202,7 @@ function createGroupingModel(inputSchema) {
|
|
|
2949
3202
|
}
|
|
2950
3203
|
function summaryExpression(column) {
|
|
2951
3204
|
if (column.op === "formula") return column.expression ?? "";
|
|
3205
|
+
if (column.op === "link") return "";
|
|
2952
3206
|
const field = column.field ? `[${column.field.replace(/\]/g, "]]")}]` : "";
|
|
2953
3207
|
return `${column.op === "row" ? "COUNT" : column.op.toUpperCase()}(${field})`;
|
|
2954
3208
|
}
|
|
@@ -2970,6 +3224,8 @@ function createGroupingModel(inputSchema) {
|
|
|
2970
3224
|
function summaryLabel(column) {
|
|
2971
3225
|
if (column.op === "formula")
|
|
2972
3226
|
return column.label?.trim() || "Custom aggregate";
|
|
3227
|
+
if (column.op === "link")
|
|
3228
|
+
return column.label?.trim() || typeof column.link?.label === "string" && column.link.label.trim() || "Link";
|
|
2973
3229
|
if (!column.field) return "Row count";
|
|
2974
3230
|
const label = sourceField(column.field)?.label ?? column.field;
|
|
2975
3231
|
const prefix = {
|
|
@@ -2980,10 +3236,70 @@ function createGroupingModel(inputSchema) {
|
|
|
2980
3236
|
min: "Min",
|
|
2981
3237
|
max: "Max",
|
|
2982
3238
|
row: "Representative \xB7",
|
|
2983
|
-
formula: ""
|
|
3239
|
+
formula: "",
|
|
3240
|
+
link: ""
|
|
2984
3241
|
};
|
|
2985
3242
|
return `${prefix[column.op]} ${label}`;
|
|
2986
3243
|
}
|
|
3244
|
+
function linkReferences(column, keys) {
|
|
3245
|
+
const link = column.link;
|
|
3246
|
+
if (!link) return [];
|
|
3247
|
+
const names = /* @__PURE__ */ new Set();
|
|
3248
|
+
if (typeof link.label === "object") names.add(link.label.column);
|
|
3249
|
+
if (link.to.kind === "template")
|
|
3250
|
+
for (const name of templateFields(link.to.template)) names.add(name);
|
|
3251
|
+
else if (link.to.keyFilters !== false)
|
|
3252
|
+
for (const key of keys) names.add(key);
|
|
3253
|
+
return [...names];
|
|
3254
|
+
}
|
|
3255
|
+
function linkProblem(column, keys, summaries) {
|
|
3256
|
+
const link = column.link;
|
|
3257
|
+
if (column.op !== "link") return null;
|
|
3258
|
+
if (!link) return "Invalid link definition.";
|
|
3259
|
+
const readable = (name) => keys.includes(name) || summaries.some((other) => other.id === name && other.op !== "link");
|
|
3260
|
+
if (typeof link.label === "object" && !readable(link.label.column))
|
|
3261
|
+
return `Unknown link label column: ${link.label.column}.`;
|
|
3262
|
+
if (link.to.kind === "template") {
|
|
3263
|
+
if (!link.to.template.trim()) return "Enter a link URL template.";
|
|
3264
|
+
return urlTemplateProblem(link.to.template, [], readable);
|
|
3265
|
+
}
|
|
3266
|
+
if (link.to.keyFilters !== false)
|
|
3267
|
+
for (const key of keys) {
|
|
3268
|
+
const field = sourceField(key);
|
|
3269
|
+
if (field?.type === "textarray")
|
|
3270
|
+
return `Key filters cannot match the textarray key ${field.label}.`;
|
|
3271
|
+
if (!field || !isFilterable(field))
|
|
3272
|
+
return `Key filters cannot match the key ${field?.label ?? key}.`;
|
|
3273
|
+
}
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
function linkDiagnostics(definition) {
|
|
3277
|
+
return definition.summaries.flatMap((column) => {
|
|
3278
|
+
const problem = linkProblem(column, definition.keys, definition.summaries);
|
|
3279
|
+
return problem ? [`${summaryLabel(column)}: ${problem}`] : [];
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
function linkValue(column, problem, keys, schema, row) {
|
|
3283
|
+
const link = column.link;
|
|
3284
|
+
const read = (name) => {
|
|
3285
|
+
const field = schema.fields.find((f2) => f2.name === name);
|
|
3286
|
+
const value2 = field ? readFieldValue(field, row) : void 0;
|
|
3287
|
+
if (value2 == null || isComputedCellError(value2)) return null;
|
|
3288
|
+
if (field?.type === "datetime" || value2 instanceof Date)
|
|
3289
|
+
return datetimeValue(value2) ?? value2;
|
|
3290
|
+
return Array.isArray(value2) ? value2.join(", ") : value2;
|
|
3291
|
+
};
|
|
3292
|
+
const label = !link ? summaryLabel(column) : typeof link.label === "string" ? link.label : String(read(link.label.column) ?? "");
|
|
3293
|
+
const value = { kind: "group-link", label, to: link?.to ?? null };
|
|
3294
|
+
if (problem || !link) return { ...value, problem: problem ?? "Invalid link definition." };
|
|
3295
|
+
if (link.to.kind === "template") {
|
|
3296
|
+
const href = fillUrlTemplate(link.to.template, [], (token) => read(token.slice(6)));
|
|
3297
|
+
return href === null ? { ...value, problem: "A value in this link is empty." } : { ...value, href };
|
|
3298
|
+
}
|
|
3299
|
+
if (link.to.keyFilters === false) return value;
|
|
3300
|
+
const terms = groupKeyTerms(keys, row, inputSchema);
|
|
3301
|
+
return terms ? { ...value, terms } : { ...value, problem: "This group cannot be matched exactly." };
|
|
3302
|
+
}
|
|
2987
3303
|
function resultSchema(keys, summaries) {
|
|
2988
3304
|
const fields = [
|
|
2989
3305
|
{
|
|
@@ -3014,7 +3330,28 @@ function createGroupingModel(inputSchema) {
|
|
|
3014
3330
|
}];
|
|
3015
3331
|
})
|
|
3016
3332
|
];
|
|
3333
|
+
let schema;
|
|
3017
3334
|
for (const column of summaries) {
|
|
3335
|
+
if (column.op === "link") {
|
|
3336
|
+
const problem = linkProblem(column, keys, summaries);
|
|
3337
|
+
fields.push({
|
|
3338
|
+
name: column.id,
|
|
3339
|
+
label: summaryLabel(column),
|
|
3340
|
+
type: "text",
|
|
3341
|
+
source: {
|
|
3342
|
+
kind: "derived",
|
|
3343
|
+
dependencies: linkReferences(column, keys),
|
|
3344
|
+
accessor: (row) => linkValue(column, problem, keys, schema, row)
|
|
3345
|
+
},
|
|
3346
|
+
select: { align: "left" },
|
|
3347
|
+
filter: { enabled: false },
|
|
3348
|
+
sort: { enabled: false },
|
|
3349
|
+
aggregate: { measure: false, groupable: false },
|
|
3350
|
+
render: GROUP_LINK_RENDER,
|
|
3351
|
+
group: "Links"
|
|
3352
|
+
});
|
|
3353
|
+
continue;
|
|
3354
|
+
}
|
|
3018
3355
|
if (column.op === "formula") {
|
|
3019
3356
|
let type = "number";
|
|
3020
3357
|
try {
|
|
@@ -3031,7 +3368,8 @@ function createGroupingModel(inputSchema) {
|
|
|
3031
3368
|
type,
|
|
3032
3369
|
source: { kind: "backend" },
|
|
3033
3370
|
group: "Aggregates",
|
|
3034
|
-
select: { align: type === "number" ? "right" : "left" }
|
|
3371
|
+
select: { align: type === "number" ? "right" : "left" },
|
|
3372
|
+
...column.display ? { display: column.display } : {}
|
|
3035
3373
|
});
|
|
3036
3374
|
continue;
|
|
3037
3375
|
}
|
|
@@ -3049,17 +3387,21 @@ function createGroupingModel(inputSchema) {
|
|
|
3049
3387
|
},
|
|
3050
3388
|
group: example ? "Representative row" : "Summaries",
|
|
3051
3389
|
...!numeric && input?.aggregate?.groupable !== void 0 ? { aggregate: { groupable: input.aggregate.groupable } } : {},
|
|
3052
|
-
|
|
3053
|
-
...
|
|
3390
|
+
// An explicit summary format replaces the inherited source presentation.
|
|
3391
|
+
...column.display ? { display: column.display } : {
|
|
3392
|
+
...typeof input?.render === "string" && !counting ? { render: input.render } : {},
|
|
3393
|
+
...input?.display && !counting ? { display: input.display } : {}
|
|
3394
|
+
}
|
|
3054
3395
|
});
|
|
3055
3396
|
}
|
|
3056
|
-
|
|
3397
|
+
schema = {
|
|
3057
3398
|
name: `${inputSchema.name}:groups`,
|
|
3058
3399
|
idField: GROUP_ID_FIELD,
|
|
3059
3400
|
fields,
|
|
3060
3401
|
defaultSelect: keys.filter((key) => sourceField(key)).map((field) => ({ field })),
|
|
3061
3402
|
defaultLimit: 25
|
|
3062
3403
|
};
|
|
3404
|
+
return schema;
|
|
3063
3405
|
}
|
|
3064
3406
|
function keyValue(value) {
|
|
3065
3407
|
if (value == null) return null;
|
|
@@ -3160,7 +3502,7 @@ function createGroupingModel(inputSchema) {
|
|
|
3160
3502
|
}
|
|
3161
3503
|
}
|
|
3162
3504
|
const formulas = new Map(
|
|
3163
|
-
summaries.filter((c) => c.op !== "row").map((column) => {
|
|
3505
|
+
summaries.filter((c) => c.op !== "row" && c.op !== "link").map((column) => {
|
|
3164
3506
|
try {
|
|
3165
3507
|
const problem = summaryProblem(column);
|
|
3166
3508
|
if (problem) throw new Error(problem);
|
|
@@ -3194,6 +3536,7 @@ function createGroupingModel(inputSchema) {
|
|
|
3194
3536
|
);
|
|
3195
3537
|
const chosen = rule && summaries.some((column) => column.op === "row") ? representative(members, rule, schema) : null;
|
|
3196
3538
|
for (const column of summaries) {
|
|
3539
|
+
if (column.op === "link") continue;
|
|
3197
3540
|
if (column.op !== "row") {
|
|
3198
3541
|
const compiled = formulas.get(column.id);
|
|
3199
3542
|
if (!compiled.plan) {
|
|
@@ -3301,12 +3644,22 @@ function createGroupingModel(inputSchema) {
|
|
|
3301
3644
|
GROUP_ID_FIELD,
|
|
3302
3645
|
...inputSchema.fields.map((field) => field.name)
|
|
3303
3646
|
]);
|
|
3647
|
+
const sourceNames = new Set(inputSchema.fields.map((field) => field.name));
|
|
3304
3648
|
for (const column of definition.summaries) {
|
|
3649
|
+
if (sourceNames.has(column.id))
|
|
3650
|
+
throw new Error(
|
|
3651
|
+
`Aggregate column ID "${column.id}" is already a source field name. Choose a different id, such as "${column.op === "formula" || column.op === "row" ? "summary" : column.op}_${column.id}".`
|
|
3652
|
+
);
|
|
3653
|
+
if (ids.has(column.id) && column.id !== GROUP_ID_FIELD)
|
|
3654
|
+
throw new Error(
|
|
3655
|
+
`Aggregate column ID "${column.id}" is used by more than one column.`
|
|
3656
|
+
);
|
|
3305
3657
|
if (!column.id || column.id.length > 256 || ids.has(column.id) || isComputedField(column.id) || ["__proto__", "constructor", "prototype"].includes(column.id))
|
|
3306
3658
|
throw new Error(
|
|
3307
|
-
"Aggregate column IDs must be
|
|
3659
|
+
"Aggregate column IDs must be 1\u2013256 characters and cannot be reserved names or @computed/ references."
|
|
3308
3660
|
);
|
|
3309
3661
|
ids.add(column.id);
|
|
3662
|
+
if (column.op === "link") continue;
|
|
3310
3663
|
if (column.op === "formula") {
|
|
3311
3664
|
const error = summaryProblem(column);
|
|
3312
3665
|
if (error) throw new Error(error);
|
|
@@ -3330,6 +3683,8 @@ function createGroupingModel(inputSchema) {
|
|
|
3330
3683
|
summaryExpression,
|
|
3331
3684
|
summaryProblem,
|
|
3332
3685
|
summaryLabel,
|
|
3686
|
+
linkProblem,
|
|
3687
|
+
linkDiagnostics,
|
|
3333
3688
|
resultSchema,
|
|
3334
3689
|
groupRows,
|
|
3335
3690
|
filteredInput,
|
|
@@ -3707,10 +4062,15 @@ function metricDateRange(config, query, observed, now, canonical) {
|
|
|
3707
4062
|
if (term.op === "<" || term.op === "<=" || term.op === "=")
|
|
3708
4063
|
end = Math.min(end, time - (term.op === "<" ? 1 : 0));
|
|
3709
4064
|
}
|
|
3710
|
-
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
4065
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
4066
|
+
const missing = [
|
|
4067
|
+
...Number.isFinite(start) ? [] : ["a lower bound (>= or >)"],
|
|
4068
|
+
...Number.isFinite(end) ? [] : ["an upper bound (<= or <)"]
|
|
4069
|
+
].join(" and ");
|
|
3711
4070
|
throw Error(
|
|
3712
|
-
|
|
4071
|
+
`Include empty dates with range "query" needs a bounded date filter on ${config.field}: add ${missing} as top-level filters (bounds inside OR groups do not count). Otherwise use range "observed" to fill from the first to the last observed date, or range "custom" with start and end.`
|
|
3713
4072
|
);
|
|
4073
|
+
}
|
|
3714
4074
|
if (end < start) return void 0;
|
|
3715
4075
|
return [
|
|
3716
4076
|
metricDateFloor(start, config.interval, config.timeZone),
|
|
@@ -4039,6 +4399,15 @@ function validateMetric(c, schema, resolve) {
|
|
|
4039
4399
|
}
|
|
4040
4400
|
if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
|
|
4041
4401
|
throw new FormulaError("Group limit must be 1\u201310,000.");
|
|
4402
|
+
if (c.minCount !== void 0) {
|
|
4403
|
+
if (!Number.isInteger(c.minCount) || c.minCount < 1 || c.minCount > 1e6)
|
|
4404
|
+
throw new FormulaError("Minimum rows per group must be 1\u20131,000,000.");
|
|
4405
|
+
if (!c.groupBy.length)
|
|
4406
|
+
throw new FormulaError(
|
|
4407
|
+
"Minimum rows per group requires at least one grouping field."
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
validateMetricWhere(c.where, schema);
|
|
4042
4411
|
for (const s of c.sort ?? []) {
|
|
4043
4412
|
if (s.key === "y" && c.display?.kind !== "scatter")
|
|
4044
4413
|
throw new FormulaError("Y sorting requires a scatter metric.");
|
|
@@ -4063,6 +4432,35 @@ function validateMetric(c, schema, resolve) {
|
|
|
4063
4432
|
];
|
|
4064
4433
|
}
|
|
4065
4434
|
}
|
|
4435
|
+
function validateMetricWhere(where, schema) {
|
|
4436
|
+
if (where === void 0) return;
|
|
4437
|
+
if (!Array.isArray(where)) throw new FormulaError("Invalid metric filter.");
|
|
4438
|
+
const predicates = where.flatMap((term) => predicatesOf(term));
|
|
4439
|
+
if (predicates.length > MAX_WHERE_CLAUSES)
|
|
4440
|
+
throw new FormulaError(
|
|
4441
|
+
`A metric filter allows at most ${MAX_WHERE_CLAUSES} predicates.`
|
|
4442
|
+
);
|
|
4443
|
+
const fields = indexFields(schema);
|
|
4444
|
+
for (const p of predicates) {
|
|
4445
|
+
const name = resolveFieldName(schema, p.field) ?? p.field;
|
|
4446
|
+
const field = fields.get(name);
|
|
4447
|
+
if (!field || !isFilterable(field) || isComputedField(name) && !computedInputAllowed(schema, name, "filter"))
|
|
4448
|
+
throw new FormulaError(`Field ${p.field} cannot filter metrics.`);
|
|
4449
|
+
if (p.op === "matches_regex" || p.op === "not_matches_regex")
|
|
4450
|
+
throw new FormulaError("Regex is unavailable in metric filters.");
|
|
4451
|
+
if (!opsForField(field).includes(p.op))
|
|
4452
|
+
throw new FormulaError(
|
|
4453
|
+
`Field ${p.field} does not support the ${p.op} filter.`
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
function metricPopulationError(clause, support) {
|
|
4458
|
+
if (clause.where?.length && !support.filters)
|
|
4459
|
+
return "This server does not support metric filters.";
|
|
4460
|
+
if (clause.minCount !== void 0 && !support.minCount)
|
|
4461
|
+
return "This server does not support a minimum rows per group.";
|
|
4462
|
+
return void 0;
|
|
4463
|
+
}
|
|
4066
4464
|
function metricDependencies(c, schema, resolve) {
|
|
4067
4465
|
return [
|
|
4068
4466
|
.../* @__PURE__ */ new Set([
|
|
@@ -4084,6 +4482,8 @@ function metricComputationKey(clauses) {
|
|
|
4084
4482
|
dateGrouping: c.dateGrouping,
|
|
4085
4483
|
sort: c.sort,
|
|
4086
4484
|
groupLimit: c.groupLimit,
|
|
4485
|
+
where: c.where,
|
|
4486
|
+
minCount: c.minCount,
|
|
4087
4487
|
distribution: distributionActive(c) ? c.distribution : void 0
|
|
4088
4488
|
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
4089
4489
|
);
|
|
@@ -4119,6 +4519,12 @@ function toMetricQuery(q, schema, execution) {
|
|
|
4119
4519
|
const raw = q;
|
|
4120
4520
|
q = normalizeQueryState(q);
|
|
4121
4521
|
const server = toServerQuery(q, schema);
|
|
4522
|
+
const metricWhere = /* @__PURE__ */ new Map();
|
|
4523
|
+
for (const c of q.aggregations ?? [])
|
|
4524
|
+
if (c.where?.length) {
|
|
4525
|
+
const pushed = toServerQuery({ ...EMPTY_QUERY, where: c.where }, schema).where;
|
|
4526
|
+
metricWhere.set(c.id, pushed);
|
|
4527
|
+
}
|
|
4122
4528
|
const metrics = (q.aggregations ?? []).map((c) => {
|
|
4123
4529
|
const {
|
|
4124
4530
|
label,
|
|
@@ -4128,10 +4534,12 @@ function toMetricQuery(q, schema, execution) {
|
|
|
4128
4534
|
expression,
|
|
4129
4535
|
expressionY,
|
|
4130
4536
|
dateGrouping,
|
|
4537
|
+
where,
|
|
4131
4538
|
...rest
|
|
4132
4539
|
} = c;
|
|
4133
4540
|
return {
|
|
4134
4541
|
...rest,
|
|
4542
|
+
...where?.length ? { where: metricWhere.get(c.id) ?? [] } : {},
|
|
4135
4543
|
...!distributionActive(c) && expression !== void 0 ? { expression } : {},
|
|
4136
4544
|
...display?.kind === "scatter" ? {
|
|
4137
4545
|
display: { kind: "scatter" },
|
|
@@ -4147,6 +4555,15 @@ function toMetricQuery(q, schema, execution) {
|
|
|
4147
4555
|
code: "invalid_metric"
|
|
4148
4556
|
}))
|
|
4149
4557
|
);
|
|
4558
|
+
for (const c of q.aggregations ?? [])
|
|
4559
|
+
if (c.where?.length && metricWhere.get(c.id)?.length !== c.where.length)
|
|
4560
|
+
diagnostics.push({
|
|
4561
|
+
metricId: c.id,
|
|
4562
|
+
code: "residual_metric_filter",
|
|
4563
|
+
from: 0,
|
|
4564
|
+
to: 1,
|
|
4565
|
+
message: "Metric filters must use server-filterable fields."
|
|
4566
|
+
});
|
|
4150
4567
|
for (const c of q.aggregations ?? [])
|
|
4151
4568
|
if (c.dateGrouping)
|
|
4152
4569
|
diagnostics.push({
|
|
@@ -4401,6 +4818,18 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4401
4818
|
}
|
|
4402
4819
|
if (inputRows.length > (options2.maxRows ?? 1e6))
|
|
4403
4820
|
throw Error("Metric row budget exceeded.");
|
|
4821
|
+
const population = inputRows.length;
|
|
4822
|
+
if (c.where?.length)
|
|
4823
|
+
inputRows = applyQuery(
|
|
4824
|
+
inputRows,
|
|
4825
|
+
{
|
|
4826
|
+
...EMPTY_QUERY,
|
|
4827
|
+
where: c.where,
|
|
4828
|
+
limit: Number.MAX_SAFE_INTEGER
|
|
4829
|
+
},
|
|
4830
|
+
schema,
|
|
4831
|
+
{ now }
|
|
4832
|
+
).rows;
|
|
4404
4833
|
const groups = /* @__PURE__ */ new Map();
|
|
4405
4834
|
const keyLimit = options2.maxGroupKeyLength ?? 65536, byteLimit = options2.maxGroupBytes ?? 16e6;
|
|
4406
4835
|
if (![keyLimit, byteLimit].every((v) => Number.isSafeInteger(v) && v >= 0))
|
|
@@ -4663,6 +5092,14 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4663
5092
|
}
|
|
4664
5093
|
}
|
|
4665
5094
|
}
|
|
5095
|
+
let hiddenGroups;
|
|
5096
|
+
if (c.minCount !== void 0) {
|
|
5097
|
+
const kept = buckets.filter(
|
|
5098
|
+
(b) => b.count === 0 || b.count >= c.minCount
|
|
5099
|
+
);
|
|
5100
|
+
hiddenGroups = buckets.length - kept.length;
|
|
5101
|
+
buckets = kept;
|
|
5102
|
+
}
|
|
4666
5103
|
const ordering = c.sort?.length ? c.sort : c.dateGrouping ? [{ key: "group0", dir: "asc", nulls: "last" }] : [{ key: "value", dir: "desc", nulls: "last" }];
|
|
4667
5104
|
const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
|
|
4668
5105
|
buckets.sort((a, b) => {
|
|
@@ -4701,9 +5138,10 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4701
5138
|
id: c.id,
|
|
4702
5139
|
buckets,
|
|
4703
5140
|
scope: c.scope ?? "allMatching",
|
|
4704
|
-
processedRows:
|
|
5141
|
+
processedRows: population,
|
|
4705
5142
|
groupCount,
|
|
4706
5143
|
coverage: "exact",
|
|
5144
|
+
...hiddenGroups !== void 0 ? { hiddenGroups } : {},
|
|
4707
5145
|
...other ? { other } : {}
|
|
4708
5146
|
};
|
|
4709
5147
|
} catch (e) {
|
|
@@ -4785,7 +5223,7 @@ function applyAggregations(rows, q, schema, options2 = {}) {
|
|
|
4785
5223
|
))
|
|
4786
5224
|
return evaluateMetrics(rows, q, schema, options2);
|
|
4787
5225
|
const advanced = (q.aggregations ?? []).filter(
|
|
4788
|
-
(a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
|
|
5226
|
+
(a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0 || a.where !== void 0 || a.minCount !== void 0
|
|
4789
5227
|
);
|
|
4790
5228
|
if (advanced.length) {
|
|
4791
5229
|
if ((q.aggregations?.length ?? 0) > 20 || normalizeQueryState(q).aggregations?.length !== q.aggregations?.length)
|
|
@@ -5390,6 +5828,7 @@ function sourceDependencies(query, schema, resolveComputed) {
|
|
|
5390
5828
|
model.validate(query.grouping);
|
|
5391
5829
|
for (const key of query.grouping.keys) fields.add(key);
|
|
5392
5830
|
for (const summary of query.grouping.summaries) {
|
|
5831
|
+
if (summary.op === "link") continue;
|
|
5393
5832
|
if (summary.op === "row") fields.add(summary.field);
|
|
5394
5833
|
else for (const dependency of compileMetric(model.summaryExpression(summary), schema, resolveComputed).dependencies) fields.add(dependency);
|
|
5395
5834
|
}
|
|
@@ -5424,6 +5863,242 @@ function sourceDependencies(query, schema, resolveComputed) {
|
|
|
5424
5863
|
return { fields: [...fields].sort(), baseFields: [...base].sort(), identity: schema.idField, paths };
|
|
5425
5864
|
}
|
|
5426
5865
|
|
|
5866
|
+
// src/queryPatch.ts
|
|
5867
|
+
function clauseKey(clause) {
|
|
5868
|
+
return JSON.stringify([
|
|
5869
|
+
clause.field,
|
|
5870
|
+
clause.op,
|
|
5871
|
+
NULLARY_OPS.has(clause.op) ? "" : clause.value,
|
|
5872
|
+
clause.negated === true
|
|
5873
|
+
]);
|
|
5874
|
+
}
|
|
5875
|
+
function whereTermKey(term) {
|
|
5876
|
+
if (!isOrGroup(term)) return clauseKey(term);
|
|
5877
|
+
return `any:${JSON.stringify(term.any.map(clauseKey).sort())}`;
|
|
5878
|
+
}
|
|
5879
|
+
function appendWhereTerms(where, terms) {
|
|
5880
|
+
const present = new Set(where.map(whereTermKey));
|
|
5881
|
+
const out = [...where];
|
|
5882
|
+
for (const term of terms) {
|
|
5883
|
+
const key = whereTermKey(term);
|
|
5884
|
+
if (present.has(key)) continue;
|
|
5885
|
+
present.add(key);
|
|
5886
|
+
out.push(term);
|
|
5887
|
+
}
|
|
5888
|
+
return out;
|
|
5889
|
+
}
|
|
5890
|
+
function filterUnits(where) {
|
|
5891
|
+
const units = [];
|
|
5892
|
+
for (const term of where) {
|
|
5893
|
+
const last = units[units.length - 1];
|
|
5894
|
+
const previous = last?.[last.length - 1];
|
|
5895
|
+
if (last && previous && !isOrGroup(term) && !isOrGroup(previous) && term.op === "!=" && previous.op === "!=" && term.field === previous.field)
|
|
5896
|
+
last.push(term);
|
|
5897
|
+
else units.push([term]);
|
|
5898
|
+
}
|
|
5899
|
+
return units;
|
|
5900
|
+
}
|
|
5901
|
+
function unitPresent(unit, present) {
|
|
5902
|
+
return unit.every((term) => present.has(whereTermKey(term)));
|
|
5903
|
+
}
|
|
5904
|
+
function removeUnits(where, units) {
|
|
5905
|
+
const present = new Set(where.map(whereTermKey));
|
|
5906
|
+
const remove = new Set(
|
|
5907
|
+
units.filter((unit) => unitPresent(unit, present)).flatMap((unit) => unit.map(whereTermKey))
|
|
5908
|
+
);
|
|
5909
|
+
return where.filter((term) => !remove.has(whereTermKey(term)));
|
|
5910
|
+
}
|
|
5911
|
+
function sameTermSet(a, b) {
|
|
5912
|
+
const keys = (where) => JSON.stringify(where.map(whereTermKey).sort());
|
|
5913
|
+
return keys(a) === keys(b);
|
|
5914
|
+
}
|
|
5915
|
+
function patchParts(patch) {
|
|
5916
|
+
const raw = patch.query ?? {};
|
|
5917
|
+
const normalized = normalizeQueryState(raw);
|
|
5918
|
+
const stage = normalized.grouping ? "groups" : "records";
|
|
5919
|
+
const filters = normalized.grouping ? normalized.grouping.source.where : normalized.where;
|
|
5920
|
+
const owns = new Set(patch.owns);
|
|
5921
|
+
if (typeof raw.limit !== "number") owns.delete("limit");
|
|
5922
|
+
return {
|
|
5923
|
+
owns,
|
|
5924
|
+
stage,
|
|
5925
|
+
filters,
|
|
5926
|
+
units: filterUnits(filters),
|
|
5927
|
+
grouping: normalized.grouping ? {
|
|
5928
|
+
keys: normalized.grouping.keys,
|
|
5929
|
+
summaries: normalized.grouping.summaries,
|
|
5930
|
+
representative: normalized.grouping.representative,
|
|
5931
|
+
having: normalized.where
|
|
5932
|
+
} : null,
|
|
5933
|
+
select: normalized.select,
|
|
5934
|
+
orderBy: normalized.orderBy,
|
|
5935
|
+
limit: typeof raw.limit === "number" ? normalized.limit : void 0,
|
|
5936
|
+
aggregations: normalized.aggregations ?? []
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
var recordStage = (query) => query.grouping?.source ?? query;
|
|
5940
|
+
function withRecordStage(query, next) {
|
|
5941
|
+
return query.grouping ? { ...query, grouping: { ...query.grouping, source: next } } : next;
|
|
5942
|
+
}
|
|
5943
|
+
function stageOf(query, stage) {
|
|
5944
|
+
if (stage === "records") return recordStage(query);
|
|
5945
|
+
return query.grouping ? query : void 0;
|
|
5946
|
+
}
|
|
5947
|
+
function withStage(query, stage, update) {
|
|
5948
|
+
if (stage === "records") return withRecordStage(query, update(recordStage(query)));
|
|
5949
|
+
return query.grouping ? update(query) : query;
|
|
5950
|
+
}
|
|
5951
|
+
function withAggregations(query, aggregations) {
|
|
5952
|
+
const { aggregations: _previous, ...rest } = query;
|
|
5953
|
+
return aggregations.length ? { ...rest, aggregations } : rest;
|
|
5954
|
+
}
|
|
5955
|
+
var clone2 = (value) => structuredClone(value);
|
|
5956
|
+
var summaryMeaning = ({ id, op, field, expression, link }) => JSON.stringify(op === "link" ? [id, op, link ?? null] : [id, op, field, expression]);
|
|
5957
|
+
function summarySignature(summaries) {
|
|
5958
|
+
return JSON.stringify(summaries.map(summaryMeaning));
|
|
5959
|
+
}
|
|
5960
|
+
function canonicalJson(value) {
|
|
5961
|
+
return JSON.stringify(
|
|
5962
|
+
value,
|
|
5963
|
+
(_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item
|
|
5964
|
+
);
|
|
5965
|
+
}
|
|
5966
|
+
function metricSignature(metric) {
|
|
5967
|
+
const { id: _id, label: _label, display: _display, layout: _layout, ...rest } = metric;
|
|
5968
|
+
return canonicalJson(rest);
|
|
5969
|
+
}
|
|
5970
|
+
function facetMatches(query, parts, facet) {
|
|
5971
|
+
switch (facet) {
|
|
5972
|
+
case "filters": {
|
|
5973
|
+
const present = new Set(recordStage(query).where.map(whereTermKey));
|
|
5974
|
+
return parts.units.every((unit) => unitPresent(unit, present));
|
|
5975
|
+
}
|
|
5976
|
+
case "grouping": {
|
|
5977
|
+
const grouping = query.grouping;
|
|
5978
|
+
if (!parts.grouping || !grouping) return !parts.grouping && !grouping;
|
|
5979
|
+
return JSON.stringify(grouping.keys) === JSON.stringify(parts.grouping.keys) && summarySignature(grouping.summaries) === summarySignature(parts.grouping.summaries) && JSON.stringify(grouping.representative) === JSON.stringify(parts.grouping.representative) && sameTermSet(query.where, parts.grouping.having);
|
|
5980
|
+
}
|
|
5981
|
+
case "columns": {
|
|
5982
|
+
const stage = stageOf(query, parts.stage);
|
|
5983
|
+
return !!stage && JSON.stringify(stage.select.map((c) => c.field)) === JSON.stringify(parts.select.map((c) => c.field));
|
|
5984
|
+
}
|
|
5985
|
+
case "order": {
|
|
5986
|
+
const stage = stageOf(query, parts.stage);
|
|
5987
|
+
return !!stage && JSON.stringify(stage.orderBy) === JSON.stringify(parts.orderBy);
|
|
5988
|
+
}
|
|
5989
|
+
case "limit": {
|
|
5990
|
+
const stage = stageOf(query, parts.stage);
|
|
5991
|
+
return !!stage && stage.limit === parts.limit;
|
|
5992
|
+
}
|
|
5993
|
+
case "metrics": {
|
|
5994
|
+
const stage = stageOf(query, parts.stage);
|
|
5995
|
+
const signatures = (metrics) => JSON.stringify(metrics.map(metricSignature).sort());
|
|
5996
|
+
return !!stage && signatures(stage.aggregations ?? []) === signatures(parts.aggregations);
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
function matchPatch(query, patch) {
|
|
6001
|
+
const normalized = normalizeQueryState(query);
|
|
6002
|
+
const parts = patchParts(patch);
|
|
6003
|
+
const owned = [...parts.owns];
|
|
6004
|
+
const modified = owned.filter((facet) => !facetMatches(normalized, parts, facet));
|
|
6005
|
+
if (!modified.length) return { status: "active", modified: [] };
|
|
6006
|
+
const present = new Set(recordStage(normalized).where.map(whereTermKey));
|
|
6007
|
+
const recognizable = owned.some((facet) => !modified.includes(facet) && (facet !== "filters" || parts.units.length > 0)) || parts.owns.has("filters") && parts.units.some((unit) => unitPresent(unit, present));
|
|
6008
|
+
return recognizable ? { status: "modified", modified } : { status: "inactive", modified: [] };
|
|
6009
|
+
}
|
|
6010
|
+
function resetOffsets(query) {
|
|
6011
|
+
return query.grouping ? { ...query, offset: 0, grouping: { ...query.grouping, source: { ...query.grouping.source, offset: 0 } } } : { ...query, offset: 0 };
|
|
6012
|
+
}
|
|
6013
|
+
function applyGrouping(query, grouping) {
|
|
6014
|
+
if (!grouping) return exitGrouping(query);
|
|
6015
|
+
const before = query.grouping;
|
|
6016
|
+
const keyed = before && JSON.stringify(before.keys) === JSON.stringify(grouping.keys) ? query : withGroupingKeys(query, grouping.keys);
|
|
6017
|
+
const previous = keyed.grouping;
|
|
6018
|
+
const summaries = clone2(grouping.summaries);
|
|
6019
|
+
const kept = new Set(summaries.map(summaryMeaning));
|
|
6020
|
+
const changed = new Set(previous.summaries.filter((column) => !kept.has(summaryMeaning(column))).map((column) => column.id));
|
|
6021
|
+
const next = {
|
|
6022
|
+
...keyed,
|
|
6023
|
+
grouping: { ...previous, summaries, representative: clone2(grouping.representative) },
|
|
6024
|
+
where: clone2(grouping.having),
|
|
6025
|
+
select: keyed.select.filter((c) => !changed.has(c.field)),
|
|
6026
|
+
orderBy: keyed.orderBy.filter((term) => !changed.has(term.field))
|
|
6027
|
+
};
|
|
6028
|
+
return changed.size ? withAggregations(next, []) : next;
|
|
6029
|
+
}
|
|
6030
|
+
function removePatch(query, patch) {
|
|
6031
|
+
const parts = patchParts(patch);
|
|
6032
|
+
const matches = (facet) => parts.owns.has(facet) && facetMatches(normalizeQueryState(query), parts, facet);
|
|
6033
|
+
const reset = {
|
|
6034
|
+
columns: matches("columns"),
|
|
6035
|
+
order: matches("order"),
|
|
6036
|
+
metrics: matches("metrics"),
|
|
6037
|
+
grouping: matches("grouping")
|
|
6038
|
+
};
|
|
6039
|
+
let next = query;
|
|
6040
|
+
if (parts.owns.has("filters")) {
|
|
6041
|
+
const records = recordStage(next);
|
|
6042
|
+
next = withRecordStage(next, { ...records, where: removeUnits(records.where, parts.units) });
|
|
6043
|
+
}
|
|
6044
|
+
next = withStage(next, parts.stage, (stage) => {
|
|
6045
|
+
let out = stage;
|
|
6046
|
+
if (reset.columns) out = { ...out, select: [] };
|
|
6047
|
+
if (reset.order) out = { ...out, orderBy: [] };
|
|
6048
|
+
if (reset.metrics) out = withAggregations(out, []);
|
|
6049
|
+
return out;
|
|
6050
|
+
});
|
|
6051
|
+
if (reset.grouping) next = exitGrouping(next);
|
|
6052
|
+
return resetOffsets(next);
|
|
6053
|
+
}
|
|
6054
|
+
function applyPatch(query, patch, options2 = {}) {
|
|
6055
|
+
let next = options2.replacing ? removePatch(query, options2.replacing) : query;
|
|
6056
|
+
const parts = patchParts(patch);
|
|
6057
|
+
if (parts.owns.has("grouping")) next = applyGrouping(next, parts.grouping);
|
|
6058
|
+
if (parts.owns.has("filters")) {
|
|
6059
|
+
const records = recordStage(next);
|
|
6060
|
+
next = withRecordStage(next, { ...records, where: appendWhereTerms(records.where, clone2(parts.filters)) });
|
|
6061
|
+
}
|
|
6062
|
+
next = withStage(next, parts.stage, (stage) => {
|
|
6063
|
+
let out = stage;
|
|
6064
|
+
if (parts.owns.has("columns")) out = { ...out, select: clone2(parts.select) };
|
|
6065
|
+
if (parts.owns.has("order")) out = { ...out, orderBy: clone2(parts.orderBy) };
|
|
6066
|
+
if (parts.owns.has("limit")) out = { ...out, limit: parts.limit ?? out.limit };
|
|
6067
|
+
if (parts.owns.has("metrics")) out = withAggregations(out, clone2(parts.aggregations));
|
|
6068
|
+
return out;
|
|
6069
|
+
});
|
|
6070
|
+
return resetOffsets(next);
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
// src/groupLinks.ts
|
|
6074
|
+
function withoutLinkSummaries(summaries) {
|
|
6075
|
+
return summaries.filter((column) => column.op !== "link");
|
|
6076
|
+
}
|
|
6077
|
+
function groupLinkQuery(query, value) {
|
|
6078
|
+
const to = value.to;
|
|
6079
|
+
if (!query.grouping || to?.kind !== "query" || value.problem) return null;
|
|
6080
|
+
const base = to.base === "empty" ? structuredClone(EMPTY_QUERY) : exitGrouping(query);
|
|
6081
|
+
if (to.keyFilters !== false && !value.terms) return null;
|
|
6082
|
+
const filtered = { ...base, where: appendWhereTerms(base.where, value.terms ?? []), offset: 0 };
|
|
6083
|
+
return to.patch ? applyPatch(filtered, to.patch) : filtered;
|
|
6084
|
+
}
|
|
6085
|
+
function resolveGroupLink(query, value, targets = {}) {
|
|
6086
|
+
const unresolved = (problem) => ({ label: value.label, href: null, problem });
|
|
6087
|
+
if (value.problem) return unresolved(value.problem);
|
|
6088
|
+
if (value.to?.kind === "template") return value.href ? { label: value.label, href: value.href } : unresolved("Invalid link.");
|
|
6089
|
+
if (value.to?.kind !== "query") return unresolved("Invalid link definition.");
|
|
6090
|
+
const route = Object.prototype.hasOwnProperty.call(targets, value.to.target) ? targets[value.to.target] : void 0;
|
|
6091
|
+
if (typeof route !== "function") return unresolved(`Link target "${value.to.target}" is not registered.`);
|
|
6092
|
+
const destination = groupLinkQuery(query, value);
|
|
6093
|
+
if (!destination) return unresolved("This group cannot be matched exactly.");
|
|
6094
|
+
try {
|
|
6095
|
+
const href = route(destination, { ...value.to.params });
|
|
6096
|
+
return typeof href === "string" && href.trim() ? { label: value.label, href } : unresolved(`Link target "${value.to.target}" returned no URL.`);
|
|
6097
|
+
} catch (error) {
|
|
6098
|
+
return unresolved(`Link target "${value.to.target}" failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
|
|
5427
6102
|
// src/queryV3.ts
|
|
5428
6103
|
function sourceExecutionSchema(schema, execution, catalogue = []) {
|
|
5429
6104
|
const fields = new Map(schema.fields.map((field) => [field.name, field]));
|
|
@@ -5473,7 +6148,9 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
|
|
|
5473
6148
|
for (const term of q.grouping.source.where) for (const predicate of predicatesOf(term))
|
|
5474
6149
|
if (predicate.field.startsWith("@computed/") && !capabilities.computedSourceFilters) diagnostics.push("This server does not support computed source predicates.");
|
|
5475
6150
|
}
|
|
5476
|
-
const
|
|
6151
|
+
const metricQuery = toMetricQuery(withoutGrouping(q), resultSchema, execution);
|
|
6152
|
+
diagnostics.push(...metricQuery.diagnostics.filter((issue) => issue.code === "residual_metric_filter").map((issue) => issue.message));
|
|
6153
|
+
const metrics = metricQuery.metrics.map((metric) => {
|
|
5477
6154
|
const dateGrouping = q.aggregations?.find((clause) => clause.id === metric.id)?.dateGrouping;
|
|
5478
6155
|
return dateGrouping ? { ...metric, dateGrouping } : metric;
|
|
5479
6156
|
});
|
|
@@ -5497,7 +6174,9 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
|
|
|
5497
6174
|
result,
|
|
5498
6175
|
...q.grouping ? { grouping: {
|
|
5499
6176
|
keys: q.grouping.keys,
|
|
5500
|
-
|
|
6177
|
+
// Summary display is presentation only and link columns are computed on
|
|
6178
|
+
// the client; the wire contract is unchanged.
|
|
6179
|
+
summaries: withoutLinkSummaries(q.grouping.summaries).map(({ display: _display, ...summary }) => summary),
|
|
5501
6180
|
representative: q.grouping.representative,
|
|
5502
6181
|
...q.grouping.diagnostics ? { diagnostics: q.grouping.diagnostics } : {}
|
|
5503
6182
|
} } : {},
|
|
@@ -5520,6 +6199,68 @@ function validateQueryResultV3(result, request, schema) {
|
|
|
5520
6199
|
seen.add(id);
|
|
5521
6200
|
}
|
|
5522
6201
|
}
|
|
6202
|
+
|
|
6203
|
+
// src/drilldown.ts
|
|
6204
|
+
function metricSchema(query, schema) {
|
|
6205
|
+
if (!query.grouping || schema.idField === GROUP_ID_FIELD) return schema;
|
|
6206
|
+
return createGroupingModel(schema).resultSchema(query.grouping.keys, query.grouping.summaries);
|
|
6207
|
+
}
|
|
6208
|
+
function distributionActive2(clause) {
|
|
6209
|
+
return !!clause.distribution && (!clause.display || clause.display.kind === "box" || clause.display.kind === "histogram");
|
|
6210
|
+
}
|
|
6211
|
+
function inspectBucketQuery(query, bucket, clause, schema) {
|
|
6212
|
+
if (clause.scope === "shownRows" || bucket.keys.length !== clause.groupBy.length) return null;
|
|
6213
|
+
const fields = metricSchema(query, schema);
|
|
6214
|
+
const terms = [...clause.where ?? []];
|
|
6215
|
+
for (const [index, name] of clause.groupBy.entries()) {
|
|
6216
|
+
const field = resolveField(fields, name);
|
|
6217
|
+
const key = bucket.keys[index] ?? null;
|
|
6218
|
+
if (clause.dateGrouping && index === 0 && key !== null) {
|
|
6219
|
+
if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
|
|
6220
|
+
let start;
|
|
6221
|
+
try {
|
|
6222
|
+
start = metricDateTime(key);
|
|
6223
|
+
} catch {
|
|
6224
|
+
return null;
|
|
6225
|
+
}
|
|
6226
|
+
const { interval, timeZone } = clause.dateGrouping;
|
|
6227
|
+
const next = metricDateNext(start, interval, timeZone);
|
|
6228
|
+
terms.push(
|
|
6229
|
+
{ field: field.name, op: ">=", value: new Date(start).toISOString() },
|
|
6230
|
+
{ field: field.name, op: "<", value: new Date(next).toISOString() }
|
|
6231
|
+
);
|
|
6232
|
+
continue;
|
|
6233
|
+
}
|
|
6234
|
+
const term = keyClause(field, key);
|
|
6235
|
+
if (!term) return null;
|
|
6236
|
+
terms.push(term);
|
|
6237
|
+
}
|
|
6238
|
+
if (bucket.bin) {
|
|
6239
|
+
const { min, max, inclusiveMax } = bucket.bin;
|
|
6240
|
+
if (!distributionActive2(clause) || clause.distribution.kind !== "histogram" || !Number.isFinite(min) || !Number.isFinite(max) || max < min)
|
|
6241
|
+
return null;
|
|
6242
|
+
let input;
|
|
6243
|
+
try {
|
|
6244
|
+
input = compileFormula(clause.distribution.input, fields.fields).ast;
|
|
6245
|
+
} catch {
|
|
6246
|
+
return null;
|
|
6247
|
+
}
|
|
6248
|
+
const field = input.kind === "field" ? resolveField(fields, input.name) : void 0;
|
|
6249
|
+
if (!field || field.type !== "number" || !isFilterable(field)) return null;
|
|
6250
|
+
terms.push(
|
|
6251
|
+
{ field: field.name, op: ">=", value: String(min) },
|
|
6252
|
+
{ field: field.name, op: inclusiveMax ? "<=" : "<", value: String(max) }
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
return { ...query, where: appendWhereTerms(query.where, terms), offset: 0 };
|
|
6256
|
+
}
|
|
6257
|
+
function groupSourceQuery(query, group, schema) {
|
|
6258
|
+
if (!query.grouping) return null;
|
|
6259
|
+
const terms = groupKeyTerms(query.grouping.keys, group, schema);
|
|
6260
|
+
if (!terms) return null;
|
|
6261
|
+
const source = exitGrouping(query);
|
|
6262
|
+
return { ...source, where: appendWhereTerms(source.where, terms), offset: 0 };
|
|
6263
|
+
}
|
|
5523
6264
|
export {
|
|
5524
6265
|
AGG_OPS_BY_TYPE,
|
|
5525
6266
|
AGG_OPS_NEEDING_FIELD,
|
|
@@ -5528,6 +6269,7 @@ export {
|
|
|
5528
6269
|
FORMULA_FUNCTIONS,
|
|
5529
6270
|
FormulaError,
|
|
5530
6271
|
GROUP_ID_FIELD,
|
|
6272
|
+
GROUP_LINK_RENDER,
|
|
5531
6273
|
ITEM_DETAILS_COLUMN,
|
|
5532
6274
|
MAX_AGGREGATIONS,
|
|
5533
6275
|
MAX_GROUP_BY_FIELDS,
|
|
@@ -5554,6 +6296,7 @@ export {
|
|
|
5554
6296
|
aggOpNeedsField,
|
|
5555
6297
|
aggOpsForField,
|
|
5556
6298
|
applyAggregations,
|
|
6299
|
+
applyPatch,
|
|
5557
6300
|
applyQuery,
|
|
5558
6301
|
boxSummary,
|
|
5559
6302
|
coerceValue,
|
|
@@ -5571,18 +6314,24 @@ export {
|
|
|
5571
6314
|
encodeQuery,
|
|
5572
6315
|
evaluateMetricReductions,
|
|
5573
6316
|
evaluateMetrics,
|
|
6317
|
+
exitGrouping,
|
|
5574
6318
|
filterValues,
|
|
5575
6319
|
formulaRuntime,
|
|
6320
|
+
groupLinkQuery,
|
|
5576
6321
|
groupPreview,
|
|
6322
|
+
groupSourceQuery,
|
|
5577
6323
|
hasRegex,
|
|
5578
6324
|
histogramCounts,
|
|
5579
6325
|
histogramEdges,
|
|
5580
6326
|
httpComputedColumnStore,
|
|
5581
6327
|
httpQuerySnapshotStore,
|
|
5582
6328
|
indexFields,
|
|
6329
|
+
inspectBucketQuery,
|
|
5583
6330
|
isComputedCellError,
|
|
5584
6331
|
isComputedField,
|
|
5585
6332
|
isFilterable,
|
|
6333
|
+
isGroupLinkField,
|
|
6334
|
+
isGroupLinkValue,
|
|
5586
6335
|
isGroupable,
|
|
5587
6336
|
isMeasurable,
|
|
5588
6337
|
isNegativePredicate,
|
|
@@ -5597,6 +6346,7 @@ export {
|
|
|
5597
6346
|
localQuerySnapshotCache,
|
|
5598
6347
|
localStorageAdapter,
|
|
5599
6348
|
mapSetFilterValues,
|
|
6349
|
+
matchPatch,
|
|
5600
6350
|
matchesClause,
|
|
5601
6351
|
memoryComputedColumnStore,
|
|
5602
6352
|
memoryPropertyViewStorage,
|
|
@@ -5611,6 +6361,7 @@ export {
|
|
|
5611
6361
|
metricDependencies,
|
|
5612
6362
|
metricExpression,
|
|
5613
6363
|
metricPlans,
|
|
6364
|
+
metricPopulationError,
|
|
5614
6365
|
negateClause,
|
|
5615
6366
|
normalizePropertyFormat,
|
|
5616
6367
|
normalizePropertyView,
|
|
@@ -5628,10 +6379,14 @@ export {
|
|
|
5628
6379
|
readObjectReference,
|
|
5629
6380
|
readSetFilter,
|
|
5630
6381
|
reduceMetricValues,
|
|
6382
|
+
removePatch,
|
|
5631
6383
|
renderItemPageTemplate,
|
|
5632
6384
|
resolveFieldName,
|
|
6385
|
+
resolveGroupLink,
|
|
5633
6386
|
selectMetricShownRows,
|
|
5634
6387
|
selectedFields,
|
|
6388
|
+
setFilterModes,
|
|
6389
|
+
setFilterTarget,
|
|
5635
6390
|
sourceDependencies,
|
|
5636
6391
|
sourceExecutionSchema,
|
|
5637
6392
|
stripSetFilter,
|
|
@@ -5646,6 +6401,8 @@ export {
|
|
|
5646
6401
|
validateMetric,
|
|
5647
6402
|
validateQueryResultV3,
|
|
5648
6403
|
validateQuerySnapshot,
|
|
5649
|
-
|
|
6404
|
+
withGroupingKeys,
|
|
6405
|
+
withoutGrouping,
|
|
6406
|
+
withoutLinkSummaries
|
|
5650
6407
|
};
|
|
5651
6408
|
//# sourceMappingURL=index.js.map
|