@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 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 query's filter so the live API returns rows for that
19
- * slice only. Undefined preserves pre-0.17.1 behaviour (web by default,
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 { between, clicks, date, extractMetricFilters, extractSpecialOperatorFilters, gsc, normalizeFilter } from "gscdump/query";
3
- import { assertDimensionsSupported, getFilterDimensions, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, metricValue } from "@gscdump/engine/resolver";
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 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 = [];
@@ -171,33 +275,15 @@ function canProxyToGsc(state) {
171
275
  return true;
172
276
  }
173
277
  function withSearchType(state, searchType) {
174
- const normalized = normalizeFilter(state.filter);
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
- filter: merged
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.38.2",
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.38.2",
40
- "gscdump": "0.38.2"
39
+ "@gscdump/engine": "0.40.1",
40
+ "gscdump": "0.40.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^4.1.10"