@gscdump/engine-gsc-api 0.38.1 → 0.39.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.
Files changed (2) hide show
  1. package/dist/index.mjs +123 -19
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { googleSearchConsole } from "gscdump/api";
2
2
  import { between, clicks, date, extractMetricFilters, extractSpecialOperatorFilters, gsc, normalizeFilter } from "gscdump/query";
3
- import { assertDimensionsSupported, getFilterDimensions, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
3
+ import { assertDimensionsSupported, dimensionValue, getFilterDimensions, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
4
4
  import { buildLogicalPlan } from "gscdump/query/plan";
5
+ import { normalizeUrl } from "gscdump/normalize";
5
6
  const METRIC_NAMES = [
6
7
  "clicks",
7
8
  "impressions",
@@ -14,32 +15,135 @@ function isMetricDimension$1(dim) {
14
15
  function isLocalDimensionFilter(filter) {
15
16
  return filter.dimension !== "date" && !isMetricDimension$1(filter.dimension) && filter.operator !== "topLevel" && !filter.operator.startsWith("metric");
16
17
  }
17
- function matchesDimensionFilterTree(row, filter) {
18
- if (!filter || !("_filters" in filter)) return true;
19
- const checks = [...filter._filters.filter(isLocalDimensionFilter).map((f) => matchesDimensionFilter(row, f)), ...(filter._nestedGroups ?? []).map((group) => matchesDimensionFilterTree(row, group))];
20
- if (checks.length === 0) return true;
21
- return filter._groupType === "or" ? checks.some(Boolean) : checks.every(Boolean);
18
+ function compileDimensionFilter(filter) {
19
+ const { dimension, expression } = filter;
20
+ switch (filter.operator) {
21
+ case "equals": return dimension === "page" ? (row) => normalizeUrl(dimensionValue(row, dimension)) === expression : (row) => dimensionValue(row, dimension) === expression;
22
+ case "notEquals": return dimension === "page" ? (row) => normalizeUrl(dimensionValue(row, dimension)) !== expression : (row) => dimensionValue(row, dimension) !== expression;
23
+ case "contains": return (row) => dimensionValue(row, dimension).includes(expression);
24
+ case "notContains": return (row) => !dimensionValue(row, dimension).includes(expression);
25
+ case "includingRegex": {
26
+ let regex;
27
+ return (row) => (regex ??= new RegExp(expression)).test(dimensionValue(row, dimension));
28
+ }
29
+ case "excludingRegex": {
30
+ let regex;
31
+ return (row) => !(regex ??= new RegExp(expression)).test(dimensionValue(row, dimension));
32
+ }
33
+ default: return () => true;
34
+ }
22
35
  }
23
- function applyBuilderStatePostProcessing(rows, state) {
24
- const metricFilters = extractMetricFilters(state.filter);
25
- const specialFilters = extractSpecialOperatorFilters(state.filter);
26
- const ordered = [...rows.filter((row) => {
27
- if (!matchesDimensionFilterTree(row, state.filter)) return false;
28
- if (!metricFilters.every((filter) => matchesMetricFilter(row, filter))) return false;
29
- if (specialFilters.some((filter) => filter.operator === "topLevel") && !matchesTopLevelPage(row)) return false;
36
+ function compileMetricFilter(filter) {
37
+ const { dimension } = filter;
38
+ const target = Number(filter.expression);
39
+ switch (filter.operator) {
40
+ case "metricGte": return (row) => metricValue(row, dimension) >= target;
41
+ case "metricGt": return (row) => metricValue(row, dimension) > target;
42
+ case "metricLte": return (row) => metricValue(row, dimension) <= target;
43
+ case "metricLt": return (row) => metricValue(row, dimension) < target;
44
+ case "metricBetween": {
45
+ const target2 = Number(filter.expression2);
46
+ return (row) => {
47
+ const value = metricValue(row, dimension);
48
+ return value >= target && value <= target2;
49
+ };
50
+ }
51
+ default: return () => true;
52
+ }
53
+ }
54
+ function compileDimensionFilterTree(filter) {
55
+ if (!filter || !("_filters" in filter)) return () => true;
56
+ const localFilters = filter._filters;
57
+ const matchers = [];
58
+ for (const localFilter of localFilters) if (isLocalDimensionFilter(localFilter)) matchers.push(compileDimensionFilter(localFilter));
59
+ for (const group of filter._nestedGroups ?? []) matchers.push(compileDimensionFilterTree(group));
60
+ if (matchers.length === 0) return () => true;
61
+ if (filter._groupType === "or") return (row) => {
62
+ for (const matches of matchers) if (matches(row)) return true;
63
+ return false;
64
+ };
65
+ return (row) => {
66
+ for (const matches of matchers) if (!matches(row)) return false;
30
67
  return true;
31
- })].sort((a, b) => {
32
- const column = state.orderBy?.column ?? "clicks";
33
- const dir = state.orderBy?.dir ?? "desc";
68
+ };
69
+ }
70
+ function selectTopK(rows, k, compareRows) {
71
+ const heap = [];
72
+ const compare = (a, b) => compareRows(a.row, b.row) || a.index - b.index;
73
+ const siftUp = (start) => {
74
+ let child = start;
75
+ while (child > 0) {
76
+ const parent = child - 1 >>> 1;
77
+ if (compare(heap[parent], heap[child]) >= 0) break;
78
+ const swap = heap[parent];
79
+ heap[parent] = heap[child];
80
+ heap[child] = swap;
81
+ child = parent;
82
+ }
83
+ };
84
+ const siftDown = () => {
85
+ let parent = 0;
86
+ while (true) {
87
+ const left = parent * 2 + 1;
88
+ if (left >= heap.length) return;
89
+ const right = left + 1;
90
+ const worse = right < heap.length && compare(heap[right], heap[left]) > 0 ? right : left;
91
+ if (compare(heap[parent], heap[worse]) >= 0) return;
92
+ const swap = heap[parent];
93
+ heap[parent] = heap[worse];
94
+ heap[worse] = swap;
95
+ parent = worse;
96
+ }
97
+ };
98
+ for (let index = 0; index < rows.length; index++) {
99
+ const candidate = {
100
+ row: rows[index],
101
+ index
102
+ };
103
+ if (heap.length < k) {
104
+ heap.push(candidate);
105
+ siftUp(heap.length - 1);
106
+ } else if (compare(candidate, heap[0]) < 0) {
107
+ heap[0] = candidate;
108
+ siftDown();
109
+ }
110
+ }
111
+ heap.sort(compare);
112
+ return heap.map((entry) => entry.row);
113
+ }
114
+ function applyBuilderStatePostProcessing(rows, state) {
115
+ const matchesDimensions = compileDimensionFilterTree(state.filter);
116
+ const metricMatchers = extractMetricFilters(state.filter).map(compileMetricFilter);
117
+ const hasTopLevelFilter = extractSpecialOperatorFilters(state.filter).some((filter) => filter.operator === "topLevel");
118
+ const column = state.orderBy?.column ?? "clicks";
119
+ const dir = state.orderBy?.dir ?? "desc";
120
+ const checkFiniteOrderValues = column !== "date" && state.rowLimit !== void 0;
121
+ let hasNonFiniteOrderValue = false;
122
+ const filtered = [];
123
+ for (const row of rows) {
124
+ if (!matchesDimensions(row)) continue;
125
+ let matchesMetrics = true;
126
+ for (const matchesMetric of metricMatchers) if (!matchesMetric(row)) {
127
+ matchesMetrics = false;
128
+ break;
129
+ }
130
+ if (!matchesMetrics) continue;
131
+ if (hasTopLevelFilter && !matchesTopLevelPage(row)) continue;
132
+ filtered.push(row);
133
+ if (checkFiniteOrderValues && !Number.isFinite(metricValue(row, column))) hasNonFiniteOrderValue = true;
134
+ }
135
+ const compareRows = (a, b) => {
34
136
  const left = column === "date" ? String(a.date ?? "") : metricValue(a, column);
35
137
  const right = column === "date" ? String(b.date ?? "") : metricValue(b, column);
36
138
  if (left === right) return 0;
37
139
  if (dir === "asc") return left < right ? -1 : 1;
38
140
  return left > right ? -1 : 1;
39
- });
141
+ };
40
142
  const offset = Math.max(0, Number(state.startRow ?? 0));
41
- const limit = Math.max(0, Number((state.rowLimit ?? ordered.length) || 0));
42
- return ordered.slice(offset, offset + limit);
143
+ const limit = Math.max(0, Number((state.rowLimit ?? filtered.length) || 0));
144
+ if (limit === 0) return [];
145
+ const requested = offset + limit;
146
+ return (Number.isInteger(requested) && requested > 0 && requested < filtered.length / 2 && !hasNonFiniteOrderValue ? selectTopK(filtered, requested, compareRows) : filtered.sort(compareRows)).slice(offset, offset + limit);
43
147
  }
44
148
  async function collectRows(gen) {
45
149
  const out = [];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine-gsc-api",
3
3
  "type": "module",
4
- "version": "0.38.1",
4
+ "version": "0.39.0",
5
5
  "description": "GSC live-API engine adapter — wraps the Search Analytics REST API as an AnalysisQuerySource for typed analyzer dispatch.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -36,8 +36,8 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@gscdump/engine": "0.38.1",
40
- "gscdump": "0.38.1"
39
+ "@gscdump/engine": "0.39.0",
40
+ "gscdump": "0.39.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^4.1.10"