@gscdump/engine-gsc-api 1.4.0 → 1.4.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 -204
- package/dist/index.mjs +4 -554
- package/dist/live.d.mts +25 -0
- package/dist/live.mjs +39 -0
- package/dist/post-process.mjs +146 -0
- package/dist/rollup-synth.d.mts +45 -0
- package/dist/rollup-synth.mjs +82 -0
- package/dist/source.d.mts +8 -0
- package/dist/source.mjs +50 -0
- package/dist/sync-slice.d.mts +136 -0
- package/dist/sync-slice.mjs +248 -0
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,555 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { normalizeUrl } from "gscdump/normalize";
|
|
6
|
-
const METRIC_NAMES = [
|
|
7
|
-
"clicks",
|
|
8
|
-
"impressions",
|
|
9
|
-
"ctr",
|
|
10
|
-
"position"
|
|
11
|
-
];
|
|
12
|
-
function isMetricDimension$1(dim) {
|
|
13
|
-
return METRIC_NAMES.includes(dim);
|
|
14
|
-
}
|
|
15
|
-
function isLocalDimensionFilter(filter) {
|
|
16
|
-
return filter.dimension !== "date" && !isMetricDimension$1(filter.dimension) && filter.operator !== "topLevel" && !filter.operator.startsWith("metric");
|
|
17
|
-
}
|
|
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
|
-
}
|
|
35
|
-
}
|
|
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;
|
|
67
|
-
return true;
|
|
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) => {
|
|
136
|
-
const left = column === "date" ? String(a.date ?? "") : metricValue(a, column);
|
|
137
|
-
const right = column === "date" ? String(b.date ?? "") : metricValue(b, column);
|
|
138
|
-
if (left === right) return 0;
|
|
139
|
-
if (dir === "asc") return left < right ? -1 : 1;
|
|
140
|
-
return left > right ? -1 : 1;
|
|
141
|
-
};
|
|
142
|
-
const offset = Math.max(0, Number(state.startRow ?? 0));
|
|
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);
|
|
147
|
-
}
|
|
148
|
-
async function collectRows(gen) {
|
|
149
|
-
const out = [];
|
|
150
|
-
for await (const batch of gen) out.push(...batch);
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
async function fetchGscTopN(opts) {
|
|
154
|
-
const { client, siteUrl, dimension, range, orderByClicksDesc, limit, sliceTop, searchType } = opts;
|
|
155
|
-
let builder = gsc.select(dimension).where(between(date, range.start, range.end));
|
|
156
|
-
if (searchType) builder = builder.type(searchType);
|
|
157
|
-
if (orderByClicksDesc) builder = builder.orderBy(clicks, "desc");
|
|
158
|
-
if (typeof limit === "number") builder = builder.limit(limit);
|
|
159
|
-
const rows = await collectRows(client.query(siteUrl, builder));
|
|
160
|
-
const mapRow = (raw) => {
|
|
161
|
-
const row = raw;
|
|
162
|
-
const key = row[dimension.dimension];
|
|
163
|
-
if (typeof key !== "string" || !key) return null;
|
|
164
|
-
const impressions = Number(row.impressions ?? 0);
|
|
165
|
-
const position = Math.max(1, Number(row.position ?? 0));
|
|
166
|
-
return {
|
|
167
|
-
key,
|
|
168
|
-
clicks: Number(row.clicks ?? 0),
|
|
169
|
-
impressions,
|
|
170
|
-
sum_position: (position - 1) * impressions
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
const topLimit = typeof sliceTop === "number" && Number.isInteger(sliceTop) && sliceTop >= 0 ? sliceTop : void 0;
|
|
174
|
-
const mapped = [];
|
|
175
|
-
if (topLimit === 0) return mapped;
|
|
176
|
-
if (orderByClicksDesc && topLimit !== void 0) {
|
|
177
|
-
for (const row of rows) {
|
|
178
|
-
const value = mapRow(row);
|
|
179
|
-
if (value) mapped.push(value);
|
|
180
|
-
if (mapped.length >= topLimit) break;
|
|
181
|
-
}
|
|
182
|
-
return mapped;
|
|
183
|
-
}
|
|
184
|
-
if (!orderByClicksDesc && topLimit !== void 0 && topLimit <= 128) {
|
|
185
|
-
let requiresFullSort = false;
|
|
186
|
-
for (const row of rows) {
|
|
187
|
-
const value = mapRow(row);
|
|
188
|
-
if (!value) continue;
|
|
189
|
-
if (!Number.isFinite(value.clicks)) {
|
|
190
|
-
requiresFullSort = true;
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
let insertAt = 0;
|
|
194
|
-
while (insertAt < mapped.length && mapped[insertAt].clicks >= value.clicks) insertAt++;
|
|
195
|
-
if (insertAt < topLimit) {
|
|
196
|
-
mapped.splice(insertAt, 0, value);
|
|
197
|
-
if (mapped.length > topLimit) mapped.pop();
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (!requiresFullSort) return mapped;
|
|
201
|
-
mapped.length = 0;
|
|
202
|
-
}
|
|
203
|
-
for (const row of rows) {
|
|
204
|
-
const value = mapRow(row);
|
|
205
|
-
if (value) mapped.push(value);
|
|
206
|
-
}
|
|
207
|
-
if (!orderByClicksDesc) mapped.sort((a, b) => b.clicks - a.clicks);
|
|
208
|
-
return typeof sliceTop === "number" ? mapped.slice(0, sliceTop) : mapped;
|
|
209
|
-
}
|
|
210
|
-
async function fetchGscDaily(opts) {
|
|
211
|
-
const { client, siteUrl, range, searchType } = opts;
|
|
212
|
-
let builder = gsc.select(date).where(between(date, range.start, range.end));
|
|
213
|
-
if (searchType) builder = builder.type(searchType);
|
|
214
|
-
const query = builder;
|
|
215
|
-
return (await collectRows(client.query(siteUrl, query))).map((r) => {
|
|
216
|
-
const row = r;
|
|
217
|
-
if (!row.date) return null;
|
|
218
|
-
const impressions = row.impressions ?? 0;
|
|
219
|
-
return {
|
|
220
|
-
date: Date.parse(`${row.date}T00:00:00Z`),
|
|
221
|
-
clicks: row.clicks ?? 0,
|
|
222
|
-
impressions,
|
|
223
|
-
sum_position: (Math.max(1, row.position ?? 0) - 1) * impressions,
|
|
224
|
-
anonymizedImpressionsPct: 0
|
|
225
|
-
};
|
|
226
|
-
}).filter((x) => x != null).sort((a, b) => a.date - b.date);
|
|
227
|
-
}
|
|
228
|
-
const GSC_API_CAPABILITIES = {
|
|
229
|
-
regex: true,
|
|
230
|
-
multiDataset: false,
|
|
231
|
-
comparisonJoin: false,
|
|
232
|
-
windowTotals: false
|
|
233
|
-
};
|
|
234
|
-
function isMetricDimension(dim) {
|
|
235
|
-
return [
|
|
236
|
-
"clicks",
|
|
237
|
-
"impressions",
|
|
238
|
-
"ctr",
|
|
239
|
-
"position"
|
|
240
|
-
].includes(dim);
|
|
241
|
-
}
|
|
242
|
-
function builderFromState(state) {
|
|
243
|
-
return { getState: () => state };
|
|
244
|
-
}
|
|
245
|
-
function canPushDownPagination(state) {
|
|
246
|
-
return !state.orderBy && extractMetricFilters(state.filter).length === 0 && extractSpecialOperatorFilters(state.filter).length === 0;
|
|
247
|
-
}
|
|
248
|
-
function createGscApiQuerySource(options) {
|
|
249
|
-
const { client, siteUrl } = options;
|
|
250
|
-
return {
|
|
251
|
-
name: "gsc-api",
|
|
252
|
-
kind: "live",
|
|
253
|
-
capabilities: GSC_API_CAPABILITIES,
|
|
254
|
-
async queryRows(state) {
|
|
255
|
-
buildLogicalPlan(state, GSC_API_CAPABILITIES);
|
|
256
|
-
const filterDims = getFilterDimensions(state.filter, isMetricDimension);
|
|
257
|
-
assertDimensionsSupported([...state.dimensions, ...filterDims], "api", "gsc-api query source");
|
|
258
|
-
const pushDownPagination = canPushDownPagination(state);
|
|
259
|
-
const apiState = pushDownPagination ? state : {
|
|
260
|
-
...state,
|
|
261
|
-
rowLimit: void 0,
|
|
262
|
-
startRow: void 0
|
|
263
|
-
};
|
|
264
|
-
return applyBuilderStatePostProcessing(await collectRows(client.query(siteUrl, builderFromState(apiState))), pushDownPagination ? {
|
|
265
|
-
...state,
|
|
266
|
-
rowLimit: void 0,
|
|
267
|
-
startRow: void 0
|
|
268
|
-
} : state);
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
const PRO_ONLY_DIMENSIONS = /* @__PURE__ */ new Set(["queryCanonical", "page_keywords"]);
|
|
273
|
-
function canProxyToGsc(state) {
|
|
274
|
-
if (state.dimensions.some((d) => PRO_ONLY_DIMENSIONS.has(d))) return false;
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
function withSearchType(state, searchType) {
|
|
278
|
-
return state.searchType ? state : {
|
|
279
|
-
...state,
|
|
280
|
-
searchType
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function createLiveGscSource(opts) {
|
|
284
|
-
let clientPromise = null;
|
|
285
|
-
function getClient() {
|
|
286
|
-
if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => opts.createClient?.(accessToken) ?? googleSearchConsole({ accessToken }));
|
|
287
|
-
return clientPromise;
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
name: "gsc-api",
|
|
291
|
-
kind: "live",
|
|
292
|
-
capabilities: {
|
|
293
|
-
regex: true,
|
|
294
|
-
multiDataset: false,
|
|
295
|
-
comparisonJoin: false,
|
|
296
|
-
windowTotals: false
|
|
297
|
-
},
|
|
298
|
-
async queryRows(state) {
|
|
299
|
-
const client = await getClient();
|
|
300
|
-
const scopedState = opts.searchType !== void 0 ? withSearchType(state, opts.searchType) : state;
|
|
301
|
-
return createGscApiQuerySource({
|
|
302
|
-
client,
|
|
303
|
-
siteUrl: opts.siteUrl
|
|
304
|
-
}).queryRows(scopedState);
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
const DIMENSIONS_BY_TABLE = {
|
|
309
|
-
pages: ["page", "date"],
|
|
310
|
-
queries: ["query", "date"],
|
|
311
|
-
countries: ["country", "date"],
|
|
312
|
-
dates: ["device", "date"],
|
|
313
|
-
page_queries: [
|
|
314
|
-
"page",
|
|
315
|
-
"query",
|
|
316
|
-
"date"
|
|
317
|
-
],
|
|
318
|
-
search_appearance: ["searchAppearance"],
|
|
319
|
-
search_appearance_pages: ["page", "date"],
|
|
320
|
-
search_appearance_queries: ["query", "date"],
|
|
321
|
-
search_appearance_page_queries: [
|
|
322
|
-
"page",
|
|
323
|
-
"query",
|
|
324
|
-
"date"
|
|
325
|
-
],
|
|
326
|
-
hourly_pages: ["hour", "page"]
|
|
327
|
-
};
|
|
328
|
-
function isTimeoutLike(err) {
|
|
329
|
-
if (!(err instanceof Error)) return false;
|
|
330
|
-
return err.name === "AbortError" || err.message?.includes("timeout") || err.message?.includes("aborted");
|
|
331
|
-
}
|
|
332
|
-
function buildDimensionFilterGroups(domainFilter, filters = []) {
|
|
333
|
-
const out = filters.map((f) => ({
|
|
334
|
-
dimension: f.dimension,
|
|
335
|
-
operator: f.operator ?? "equals",
|
|
336
|
-
expression: f.expression
|
|
337
|
-
}));
|
|
338
|
-
if (domainFilter?.domain) {
|
|
339
|
-
const pattern = `^https?://(www\\.)?${domainFilter.domain.replace(/^www\./, "").replace(/\./g, "\\.")}/`;
|
|
340
|
-
out.push({
|
|
341
|
-
dimension: "page",
|
|
342
|
-
operator: "includingRegex",
|
|
343
|
-
expression: pattern
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
return out.length > 0 ? [{ filters: out }] : void 0;
|
|
347
|
-
}
|
|
348
|
-
async function runGscSyncSlice(opts) {
|
|
349
|
-
const rowLimit = opts.rowLimit ?? 500;
|
|
350
|
-
const cpuBudgetMs = opts.cpuBudgetMs ?? 2e4;
|
|
351
|
-
const maxPages = opts.maxPages ?? Infinity;
|
|
352
|
-
const searchType = opts.searchType ?? "web";
|
|
353
|
-
const dimensions = opts.dimensions ? [...opts.dimensions] : [...DIMENSIONS_BY_TABLE[opts.table]];
|
|
354
|
-
const dataState = opts.dataState ?? (dimensions.includes("hour") ? "hourly_all" : "all");
|
|
355
|
-
const dimensionFilterGroups = buildDimensionFilterGroups(opts.domainFilter, opts.dimensionFilters);
|
|
356
|
-
const loopStart = Date.now();
|
|
357
|
-
let startRow = opts.initialStartRow ?? 0;
|
|
358
|
-
let totalRows = 0;
|
|
359
|
-
let pageCount = 0;
|
|
360
|
-
let metadata;
|
|
361
|
-
const fetchPage = async (row) => {
|
|
362
|
-
const query = {
|
|
363
|
-
startDate: opts.startDate,
|
|
364
|
-
endDate: opts.endDate,
|
|
365
|
-
dimensions,
|
|
366
|
-
rowLimit,
|
|
367
|
-
startRow: row,
|
|
368
|
-
dataState,
|
|
369
|
-
type: searchType,
|
|
370
|
-
...dimensionFilterGroups ? { dimensionFilterGroups } : {}
|
|
371
|
-
};
|
|
372
|
-
try {
|
|
373
|
-
const response = await opts.client.searchAnalytics.query(opts.siteUrl, query);
|
|
374
|
-
return {
|
|
375
|
-
kind: "ok",
|
|
376
|
-
startRow: row,
|
|
377
|
-
rows: response.rows ?? [],
|
|
378
|
-
metadata: response.metadata
|
|
379
|
-
};
|
|
380
|
-
} catch (err) {
|
|
381
|
-
if (isTimeoutLike(err)) return {
|
|
382
|
-
kind: "timeout",
|
|
383
|
-
startRow: row
|
|
384
|
-
};
|
|
385
|
-
return {
|
|
386
|
-
kind: "error",
|
|
387
|
-
error: err
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
const startFetchIfAllowed = (row) => {
|
|
392
|
-
if (pageCount >= maxPages) return null;
|
|
393
|
-
if (Date.now() - loopStart >= cpuBudgetMs) return null;
|
|
394
|
-
return fetchPage(row);
|
|
395
|
-
};
|
|
396
|
-
let pending = startFetchIfAllowed(startRow);
|
|
397
|
-
if (pending === null) return {
|
|
398
|
-
totalRows,
|
|
399
|
-
hasMore: true,
|
|
400
|
-
nextStartRow: startRow,
|
|
401
|
-
metadata
|
|
402
|
-
};
|
|
403
|
-
while (pending !== null) {
|
|
404
|
-
const page = await pending;
|
|
405
|
-
pending = null;
|
|
406
|
-
if (page.kind === "error") throw page.error;
|
|
407
|
-
if (page.kind === "timeout") return {
|
|
408
|
-
totalRows,
|
|
409
|
-
hasMore: true,
|
|
410
|
-
nextStartRow: page.startRow,
|
|
411
|
-
metadata
|
|
412
|
-
};
|
|
413
|
-
const rows = page.rows;
|
|
414
|
-
totalRows += rows.length;
|
|
415
|
-
pageCount++;
|
|
416
|
-
if (page.metadata) metadata = page.metadata;
|
|
417
|
-
opts.onPage?.({
|
|
418
|
-
searchType,
|
|
419
|
-
rowsThisPage: rows.length
|
|
420
|
-
});
|
|
421
|
-
const isLastPage = rows.length === 0;
|
|
422
|
-
const nextStartRow = page.startRow + rows.length;
|
|
423
|
-
const prefetch = isLastPage ? null : startFetchIfAllowed(nextStartRow);
|
|
424
|
-
if (rows.length > 0) {
|
|
425
|
-
if (await opts.onBatch(rows).then(() => false).catch((err) => {
|
|
426
|
-
if (isTimeoutLike(err)) return true;
|
|
427
|
-
throw err;
|
|
428
|
-
})) return {
|
|
429
|
-
totalRows: totalRows - rows.length,
|
|
430
|
-
hasMore: true,
|
|
431
|
-
nextStartRow: page.startRow,
|
|
432
|
-
metadata
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
if (isLastPage) return {
|
|
436
|
-
totalRows,
|
|
437
|
-
hasMore: false,
|
|
438
|
-
nextStartRow,
|
|
439
|
-
metadata
|
|
440
|
-
};
|
|
441
|
-
if (prefetch === null) return {
|
|
442
|
-
totalRows,
|
|
443
|
-
hasMore: true,
|
|
444
|
-
nextStartRow,
|
|
445
|
-
metadata
|
|
446
|
-
};
|
|
447
|
-
startRow = nextStartRow;
|
|
448
|
-
pending = prefetch;
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
totalRows,
|
|
452
|
-
hasMore: false,
|
|
453
|
-
nextStartRow: startRow,
|
|
454
|
-
metadata
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
function contextTableForGrain(grain) {
|
|
458
|
-
switch (grain) {
|
|
459
|
-
case "page": return "search_appearance_pages";
|
|
460
|
-
case "query": return "search_appearance_queries";
|
|
461
|
-
case "page_query": return "search_appearance_page_queries";
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
async function runGscSearchAppearanceContextSlice(opts) {
|
|
465
|
-
const table = opts.table ?? contextTableForGrain(opts.grain ?? "page_query");
|
|
466
|
-
const appearances = opts.continuation?.appearances?.slice() ?? opts.appearances?.slice() ?? [];
|
|
467
|
-
let totalRows = 0;
|
|
468
|
-
let hasMore = false;
|
|
469
|
-
if (!opts.appearances && opts.continuation?.phase !== "context") {
|
|
470
|
-
const discovered = new Set(appearances);
|
|
471
|
-
const discovery = await runGscSyncSlice({
|
|
472
|
-
client: opts.client,
|
|
473
|
-
siteUrl: opts.siteUrl,
|
|
474
|
-
table: "search_appearance",
|
|
475
|
-
startDate: opts.startDate,
|
|
476
|
-
endDate: opts.endDate,
|
|
477
|
-
domainFilter: opts.domainFilter,
|
|
478
|
-
dataState: opts.dataState,
|
|
479
|
-
rowLimit: opts.rowLimit,
|
|
480
|
-
maxPages: opts.maxPages,
|
|
481
|
-
cpuBudgetMs: opts.cpuBudgetMs,
|
|
482
|
-
searchType: opts.searchType,
|
|
483
|
-
initialStartRow: opts.continuation?.phase === "discovery" ? opts.continuation.nextStartRow : void 0,
|
|
484
|
-
onPage: opts.onPage,
|
|
485
|
-
onBatch: async (rows) => {
|
|
486
|
-
for (const row of rows) {
|
|
487
|
-
const value = String(row.keys?.[0] ?? "");
|
|
488
|
-
if (value) discovered.add(value);
|
|
489
|
-
}
|
|
490
|
-
await opts.onTotalBatch?.(rows);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
totalRows += discovery.totalRows;
|
|
494
|
-
if (discovery.hasMore) return {
|
|
495
|
-
appearances: [...discovered],
|
|
496
|
-
totalRows,
|
|
497
|
-
hasMore: true,
|
|
498
|
-
continuation: {
|
|
499
|
-
phase: "discovery",
|
|
500
|
-
appearances: [...discovered],
|
|
501
|
-
nextStartRow: discovery.nextStartRow
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
hasMore ||= discovery.hasMore;
|
|
505
|
-
appearances.splice(0, appearances.length, ...discovered);
|
|
506
|
-
}
|
|
507
|
-
const startIndex = opts.continuation?.phase === "context" ? opts.continuation.appearanceIndex : 0;
|
|
508
|
-
const startRow = opts.continuation?.phase === "context" ? opts.continuation.nextStartRow : 0;
|
|
509
|
-
for (let i = startIndex; i < appearances.length; i++) {
|
|
510
|
-
const searchAppearance = appearances[i];
|
|
511
|
-
const context = await runGscSyncSlice({
|
|
512
|
-
client: opts.client,
|
|
513
|
-
siteUrl: opts.siteUrl,
|
|
514
|
-
table,
|
|
515
|
-
startDate: opts.startDate,
|
|
516
|
-
endDate: opts.endDate,
|
|
517
|
-
domainFilter: opts.domainFilter,
|
|
518
|
-
dimensionFilters: [{
|
|
519
|
-
dimension: "searchAppearance",
|
|
520
|
-
expression: searchAppearance
|
|
521
|
-
}],
|
|
522
|
-
dataState: opts.dataState,
|
|
523
|
-
rowLimit: opts.rowLimit,
|
|
524
|
-
maxPages: opts.maxPages,
|
|
525
|
-
cpuBudgetMs: opts.cpuBudgetMs,
|
|
526
|
-
searchType: opts.searchType,
|
|
527
|
-
initialStartRow: i === startIndex ? startRow : void 0,
|
|
528
|
-
onPage: opts.onPage,
|
|
529
|
-
onBatch: (rows) => opts.onContextBatch({
|
|
530
|
-
searchAppearance,
|
|
531
|
-
table,
|
|
532
|
-
rows
|
|
533
|
-
})
|
|
534
|
-
});
|
|
535
|
-
totalRows += context.totalRows;
|
|
536
|
-
if (context.hasMore) return {
|
|
537
|
-
appearances,
|
|
538
|
-
totalRows,
|
|
539
|
-
hasMore: true,
|
|
540
|
-
continuation: {
|
|
541
|
-
phase: "context",
|
|
542
|
-
appearances,
|
|
543
|
-
appearanceIndex: i,
|
|
544
|
-
nextStartRow: context.nextStartRow
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
hasMore ||= context.hasMore;
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
appearances,
|
|
551
|
-
totalRows,
|
|
552
|
-
hasMore
|
|
553
|
-
};
|
|
554
|
-
}
|
|
1
|
+
import { fetchGscDaily, fetchGscTopN } from "./rollup-synth.mjs";
|
|
2
|
+
import { createGscApiQuerySource } from "./source.mjs";
|
|
3
|
+
import { canProxyToGsc, createLiveGscSource } from "./live.mjs";
|
|
4
|
+
import { runGscSearchAppearanceContextSlice, runGscSyncSlice } from "./sync-slice.mjs";
|
|
555
5
|
export { canProxyToGsc, createGscApiQuerySource, createLiveGscSource, fetchGscDaily, fetchGscTopN, runGscSearchAppearanceContextSlice, runGscSyncSlice };
|
package/dist/live.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GoogleSearchConsoleClient } from "gscdump";
|
|
2
|
+
import { BuilderState } from "gscdump/query";
|
|
3
|
+
import { SearchType as SearchType$1 } from "@gscdump/engine";
|
|
4
|
+
import { AnalysisQuerySource } from "@gscdump/engine/source";
|
|
5
|
+
declare function canProxyToGsc(state: BuilderState): boolean;
|
|
6
|
+
interface CreateLiveGscSourceOptions {
|
|
7
|
+
/** GSC property URL (e.g. `sc-domain:example.com` or `https://example.com/`). */
|
|
8
|
+
siteUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* Returns a valid GSC access token. Called lazily on first query so refresh
|
|
11
|
+
* cost is paid only when the source actually runs. Host owns refresh logic.
|
|
12
|
+
*/
|
|
13
|
+
getAccessToken: () => Promise<string>;
|
|
14
|
+
/** Optional host client factory (for custom timeouts, fetch, or telemetry). */
|
|
15
|
+
createClient?: (accessToken: string) => GoogleSearchConsoleClient | Promise<GoogleSearchConsoleClient>;
|
|
16
|
+
/**
|
|
17
|
+
* GSC `searchType` slice this source is scoped to (`web`, `discover`,
|
|
18
|
+
* `news`, `googleNews`, `image`, `video`). When set, the slice is injected
|
|
19
|
+
* into every outgoing builder state so the live API returns rows for that
|
|
20
|
+
* slice only. An explicit search type already present on the state wins.
|
|
21
|
+
*/
|
|
22
|
+
searchType?: SearchType$1;
|
|
23
|
+
}
|
|
24
|
+
declare function createLiveGscSource(opts: CreateLiveGscSourceOptions): AnalysisQuerySource;
|
|
25
|
+
export { CreateLiveGscSourceOptions, canProxyToGsc, createLiveGscSource };
|
package/dist/live.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createGscApiQuerySource } from "./source.mjs";
|
|
2
|
+
import { googleSearchConsole } from "gscdump";
|
|
3
|
+
const PRO_ONLY_DIMENSIONS = /* @__PURE__ */ new Set(["queryCanonical", "page_keywords"]);
|
|
4
|
+
function canProxyToGsc(state) {
|
|
5
|
+
if (state.dimensions.some((d) => PRO_ONLY_DIMENSIONS.has(d))) return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
function withSearchType(state, searchType) {
|
|
9
|
+
return state.searchType ? state : {
|
|
10
|
+
...state,
|
|
11
|
+
searchType
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function createLiveGscSource(opts) {
|
|
15
|
+
let clientPromise = null;
|
|
16
|
+
function getClient() {
|
|
17
|
+
if (!clientPromise) clientPromise = opts.getAccessToken().then((accessToken) => opts.createClient?.(accessToken) ?? googleSearchConsole({ accessToken }));
|
|
18
|
+
return clientPromise;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: "gsc-api",
|
|
22
|
+
kind: "live",
|
|
23
|
+
capabilities: {
|
|
24
|
+
regex: true,
|
|
25
|
+
multiDataset: false,
|
|
26
|
+
comparisonJoin: false,
|
|
27
|
+
windowTotals: false
|
|
28
|
+
},
|
|
29
|
+
async queryRows(state) {
|
|
30
|
+
const client = await getClient();
|
|
31
|
+
const scopedState = opts.searchType !== void 0 ? withSearchType(state, opts.searchType) : state;
|
|
32
|
+
return createGscApiQuerySource({
|
|
33
|
+
client,
|
|
34
|
+
siteUrl: opts.siteUrl
|
|
35
|
+
}).queryRows(scopedState);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export { canProxyToGsc, createLiveGscSource };
|