@gscdump/engine-gsc-api 0.38.2 → 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.
- package/dist/index.mjs +123 -19
- 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,
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
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 ??
|
|
42
|
-
|
|
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.
|
|
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.
|
|
40
|
-
"gscdump": "0.
|
|
39
|
+
"@gscdump/engine": "0.39.0",
|
|
40
|
+
"gscdump": "0.39.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^4.1.10"
|