@gscdump/engine-gsc-api 0.38.2 → 0.40.1
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.mts +4 -3
- package/dist/index.mjs +127 -41
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -12,12 +12,13 @@ interface CreateLiveGscSourceOptions {
|
|
|
12
12
|
* cost is paid only when the source actually runs. Host owns refresh logic.
|
|
13
13
|
*/
|
|
14
14
|
getAccessToken: () => Promise<string>;
|
|
15
|
+
/** Optional host client factory (for custom timeouts, fetch, or telemetry). */
|
|
16
|
+
createClient?: (accessToken: string) => GoogleSearchConsoleClient | Promise<GoogleSearchConsoleClient>;
|
|
15
17
|
/**
|
|
16
18
|
* GSC `searchType` slice this source is scoped to (`web`, `discover`,
|
|
17
19
|
* `news`, `googleNews`, `image`, `video`). When set, the slice is injected
|
|
18
|
-
* into every outgoing
|
|
19
|
-
* slice only.
|
|
20
|
-
* unless the BuilderState's filter already names a `searchType`).
|
|
20
|
+
* into every outgoing builder state so the live API returns rows for that
|
|
21
|
+
* slice only. An explicit search type already present on the state wins.
|
|
21
22
|
*/
|
|
22
23
|
searchType?: SearchType$1;
|
|
23
24
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { googleSearchConsole } from "gscdump/api";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { assertDimensionsSupported, dimensionValue, getFilterDimensions, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
|
|
3
|
+
import { between, clicks, date, extractMetricFilters, extractSpecialOperatorFilters, gsc } from "gscdump/query";
|
|
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 = [];
|
|
@@ -171,33 +275,15 @@ function canProxyToGsc(state) {
|
|
|
171
275
|
return true;
|
|
172
276
|
}
|
|
173
277
|
function withSearchType(state, searchType) {
|
|
174
|
-
|
|
175
|
-
const existingFilters = normalized?._filters ?? [];
|
|
176
|
-
if (existingFilters.some((f) => f.dimension === "searchType")) return normalized === state.filter ? state : {
|
|
177
|
-
...state,
|
|
178
|
-
filter: normalized
|
|
179
|
-
};
|
|
180
|
-
const newEntry = {
|
|
181
|
-
dimension: "searchType",
|
|
182
|
-
operator: "eq",
|
|
183
|
-
expression: searchType
|
|
184
|
-
};
|
|
185
|
-
const merged = normalized ? {
|
|
186
|
-
...normalized,
|
|
187
|
-
_filters: [...existingFilters, newEntry]
|
|
188
|
-
} : {
|
|
189
|
-
_filters: [newEntry],
|
|
190
|
-
_groupType: "and"
|
|
191
|
-
};
|
|
192
|
-
return {
|
|
278
|
+
return state.searchType ? state : {
|
|
193
279
|
...state,
|
|
194
|
-
|
|
280
|
+
searchType
|
|
195
281
|
};
|
|
196
282
|
}
|
|
197
283
|
function createLiveGscSource(opts) {
|
|
198
284
|
let clientPromise = null;
|
|
199
285
|
function getClient() {
|
|
200
|
-
if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => googleSearchConsole({ accessToken }));
|
|
286
|
+
if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => opts.createClient?.(accessToken) ?? googleSearchConsole({ accessToken }));
|
|
201
287
|
return clientPromise;
|
|
202
288
|
}
|
|
203
289
|
return {
|
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.40.1",
|
|
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.40.1",
|
|
40
|
+
"gscdump": "0.40.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"vitest": "^4.1.10"
|