@gscdump/cli 3.8.0 → 4.0.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/README.md +115 -30
- package/bin/gscdump.mjs +21 -1
- package/dist/analysis-local.mjs +172 -53
- package/dist/auth-state.mjs +21 -6
- package/dist/auth.mjs +27 -18
- package/dist/bing-data.mjs +6 -2
- package/dist/bing-hosted.mjs +1 -1
- package/dist/cli-args.mjs +8 -0
- package/dist/cli.d.mts +5 -1
- package/dist/cli.mjs +41 -10
- package/dist/cloud-google.mjs +8 -1
- package/dist/command-meta.mjs +4 -4
- package/dist/command-registry.mjs +86 -10
- package/dist/commands/analyze.mjs +84 -43
- package/dist/commands/auth.mjs +46 -37
- package/dist/commands/bing.mjs +3 -3
- package/dist/commands/compact.mjs +12 -8
- package/dist/commands/config.mjs +14 -14
- package/dist/commands/doctor.mjs +26 -26
- package/dist/commands/dump.mjs +203 -105
- package/dist/commands/entities.mjs +10 -113
- package/dist/commands/gc.mjs +4 -3
- package/dist/commands/indexing-urls.mjs +185 -0
- package/dist/commands/indexing.mjs +12 -8
- package/dist/commands/init.mjs +77 -18
- package/dist/commands/inspect.mjs +97 -110
- package/dist/commands/mcp.mjs +37 -57
- package/dist/commands/papercut.mjs +1 -1
- package/dist/commands/profile.mjs +9 -17
- package/dist/commands/query.mjs +219 -210
- package/dist/commands/report.mjs +68 -85
- package/dist/commands/rollups.mjs +9 -6
- package/dist/commands/sitemaps.mjs +51 -104
- package/dist/commands/sites.mjs +40 -24
- package/dist/commands/stats.mjs +14 -17
- package/dist/commands/store-purge.mjs +9 -5
- package/dist/commands/store.mjs +0 -12
- package/dist/commands/sync.mjs +774 -296
- package/dist/config.mjs +7 -4
- package/dist/context.mjs +51 -21
- package/dist/coverage.mjs +251 -0
- package/dist/dump-bing.mjs +71 -0
- package/dist/dump-writers.mjs +246 -0
- package/dist/error-handler.mjs +108 -38
- package/dist/filters.mjs +125 -0
- package/dist/hosted-site.mjs +91 -0
- package/dist/inspect-urls.mjs +76 -0
- package/dist/inspection-record.mjs +93 -0
- package/dist/local-entities.mjs +658 -0
- package/dist/mcp/errors.mjs +71 -18
- package/dist/mcp/handlers/diagnostics.mjs +15 -12
- package/dist/mcp/handlers/reports.mjs +33 -42
- package/dist/mcp/server/index.mjs +80 -38
- package/dist/mcp/types.mjs +7 -7
- package/dist/package.mjs +1 -1
- package/dist/quota-ledger.mjs +370 -0
- package/dist/render/analysis.mjs +8 -1
- package/dist/render/report.mjs +8 -1
- package/dist/request-pacer.mjs +29 -0
- package/dist/route.mjs +301 -0
- package/dist/sitemap.mjs +4 -0
- package/dist/sql-views.mjs +117 -0
- package/dist/store-sites.mjs +134 -0
- package/dist/sync-plan.mjs +129 -0
- package/dist/sync-run.mjs +96 -0
- package/dist/table-sources.mjs +57 -0
- package/dist/token-info.mjs +54 -0
- package/dist/utils.mjs +28 -19
- package/dist/window.mjs +159 -0
- package/package.json +17 -14
- package/skills/gscdump/SKILL.md +172 -37
- package/dist/commands/export.mjs +0 -76
- package/dist/native-duckdb.mjs +0 -46
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { blockUntil, googleErrorMessage, parseQuotaRefusal } from "./quota-ledger.mjs";
|
|
2
|
+
import { runWithConcurrency } from "./utils.mjs";
|
|
3
|
+
import { latestByUrl, planInspections, recentMonths, scheduleOf, toInspectionRecord } from "./inspection-record.mjs";
|
|
4
|
+
import { createIndexingMetadataStore, createInspectionStore, createSitemapListStore, createSitemapReadStore, createSitemapStore, hashUrl, parseSitemapFeedIdentity } from "@gscdump/engine/entities";
|
|
5
|
+
async function loadInspectionHistory(dataSource, ctx) {
|
|
6
|
+
const inspector = createInspectionStore({ dataSource });
|
|
7
|
+
const months = await inspector.listHistoryMonths(ctx);
|
|
8
|
+
const records = [];
|
|
9
|
+
for (const month of months) {
|
|
10
|
+
const shard = await inspector.loadHistory(ctx, month);
|
|
11
|
+
if (shard) records.push(...shard.records);
|
|
12
|
+
}
|
|
13
|
+
return records;
|
|
14
|
+
}
|
|
15
|
+
async function loadInspectionState(dataSource, ctx, now) {
|
|
16
|
+
const inspector = createInspectionStore({ dataSource });
|
|
17
|
+
const indexed = await inspector.loadMaterialized(ctx);
|
|
18
|
+
if (!indexed) {
|
|
19
|
+
const history = await loadInspectionHistory(dataSource, ctx);
|
|
20
|
+
return {
|
|
21
|
+
latest: latestByUrl(history),
|
|
22
|
+
recent: history
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const recent = [];
|
|
26
|
+
for (const month of recentMonths(now)) {
|
|
27
|
+
const shard = await inspector.loadHistory(ctx, month);
|
|
28
|
+
if (shard) recent.push(...shard.records);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
latest: latestByUrl([...indexed.map(fromParquetRow), ...recent]),
|
|
32
|
+
recent
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function fromParquetRow(row) {
|
|
36
|
+
const record = {
|
|
37
|
+
url: row.url,
|
|
38
|
+
inspectedAt: row.inspectedAt
|
|
39
|
+
};
|
|
40
|
+
for (const field of [
|
|
41
|
+
"indexStatus",
|
|
42
|
+
"lastCrawlTime",
|
|
43
|
+
"googleCanonical",
|
|
44
|
+
"userCanonical",
|
|
45
|
+
"coverageState",
|
|
46
|
+
"robotsTxtState",
|
|
47
|
+
"indexingState",
|
|
48
|
+
"pageFetchState",
|
|
49
|
+
"mobileUsabilityVerdict",
|
|
50
|
+
"richResultsVerdict"
|
|
51
|
+
]) {
|
|
52
|
+
const value = row[field];
|
|
53
|
+
if (value != null) record[field] = value;
|
|
54
|
+
}
|
|
55
|
+
if (row.scheduleNextAt != null && row.scheduleConsecutiveUnchanged != null && row.schedulePolicyVersion != null) record.raw = { schedule: {
|
|
56
|
+
nextAt: row.scheduleNextAt,
|
|
57
|
+
consecutiveUnchanged: row.scheduleConsecutiveUnchanged,
|
|
58
|
+
policyVersion: row.schedulePolicyVersion
|
|
59
|
+
} };
|
|
60
|
+
return record;
|
|
61
|
+
}
|
|
62
|
+
function toParquetRow(record) {
|
|
63
|
+
const schedule = scheduleOf(record);
|
|
64
|
+
return {
|
|
65
|
+
urlHash: hashUrl(record.url),
|
|
66
|
+
url: record.url,
|
|
67
|
+
inspectedAt: record.inspectedAt,
|
|
68
|
+
indexStatus: record.indexStatus ?? null,
|
|
69
|
+
lastCrawlTime: record.lastCrawlTime ?? null,
|
|
70
|
+
googleCanonical: record.googleCanonical ?? null,
|
|
71
|
+
userCanonical: record.userCanonical ?? null,
|
|
72
|
+
coverageState: record.coverageState ?? null,
|
|
73
|
+
robotsTxtState: record.robotsTxtState ?? null,
|
|
74
|
+
indexingState: record.indexingState ?? null,
|
|
75
|
+
pageFetchState: record.pageFetchState ?? null,
|
|
76
|
+
mobileUsabilityVerdict: record.mobileUsabilityVerdict ?? null,
|
|
77
|
+
richResultsVerdict: record.richResultsVerdict ?? null,
|
|
78
|
+
scheduleNextAt: schedule.nextAt,
|
|
79
|
+
scheduleConsecutiveUnchanged: schedule.consecutiveUnchanged,
|
|
80
|
+
schedulePolicyVersion: schedule.policyVersion
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function appendInspections(dataSource, ctx, records) {
|
|
84
|
+
if (records.length > 0) await createInspectionStore({ dataSource }).appendHistory(ctx, records);
|
|
85
|
+
}
|
|
86
|
+
async function materializeInspectionIndex(dataSource, ctx, latest, records) {
|
|
87
|
+
const merged = latestByUrl([...latest.values(), ...records]);
|
|
88
|
+
const result = await createInspectionStore({ dataSource }).materialize(ctx, [...merged.values()].map(toParquetRow));
|
|
89
|
+
return {
|
|
90
|
+
indexKey: result.key,
|
|
91
|
+
indexRows: result.rowCount
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function recordInspections(dataSource, ctx, latest, records) {
|
|
95
|
+
if (records.length === 0) return void 0;
|
|
96
|
+
await appendInspections(dataSource, ctx, records);
|
|
97
|
+
return materializeInspectionIndex(dataSource, ctx, latest, records);
|
|
98
|
+
}
|
|
99
|
+
function urlInProperty(siteUrl, url) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(url);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
107
|
+
if (siteUrl.startsWith("sc-domain:")) {
|
|
108
|
+
const domain = siteUrl.slice(10).toLowerCase();
|
|
109
|
+
const host = parsed.hostname.toLowerCase();
|
|
110
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
111
|
+
}
|
|
112
|
+
return url.startsWith(siteUrl);
|
|
113
|
+
}
|
|
114
|
+
function resolvePagePaths(paths, siteUrl, knownUrls) {
|
|
115
|
+
let origin;
|
|
116
|
+
if (siteUrl.startsWith("sc-domain:")) {
|
|
117
|
+
const counts = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const url of knownUrls) {
|
|
119
|
+
if (!urlInProperty(siteUrl, url)) continue;
|
|
120
|
+
const o = new URL(url).origin;
|
|
121
|
+
counts.set(o, (counts.get(o) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
origin = [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? `https://${siteUrl.slice(10)}`;
|
|
124
|
+
}
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const value of paths) try {
|
|
127
|
+
out.push(new URL(value, origin ?? siteUrl).toString());
|
|
128
|
+
} catch {}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
const INSPECT_PAGE_CANDIDATES = 5e3;
|
|
132
|
+
async function topPagePaths(store, siteUrl) {
|
|
133
|
+
if ((await store.engine.listLive({
|
|
134
|
+
userId: store.userId,
|
|
135
|
+
siteId: store.siteIdFor(siteUrl),
|
|
136
|
+
table: "pages"
|
|
137
|
+
})).length === 0) return [];
|
|
138
|
+
const { rows } = await store.runRawSql({
|
|
139
|
+
sql: `SELECT url, SUM(impressions) AS impressions FROM read_parquet({{FILES}}, union_by_name = true) GROUP BY url ORDER BY impressions DESC LIMIT ${INSPECT_PAGE_CANDIDATES}`,
|
|
140
|
+
siteUrl,
|
|
141
|
+
table: "pages"
|
|
142
|
+
});
|
|
143
|
+
return rows.map((row) => String(row.url));
|
|
144
|
+
}
|
|
145
|
+
async function inspectionCandidates(store, siteUrl) {
|
|
146
|
+
const ctx = {
|
|
147
|
+
userId: store.userId,
|
|
148
|
+
siteId: store.siteIdFor(siteUrl)
|
|
149
|
+
};
|
|
150
|
+
const sitemapUrls = await loadSitemapGenerationUrls(store.dataSource, ctx);
|
|
151
|
+
const trafficUrls = resolvePagePaths(await topPagePaths(store, siteUrl), siteUrl, sitemapUrls);
|
|
152
|
+
const rank = new Map(trafficUrls.map((url, i) => [url, i]));
|
|
153
|
+
const sitemapFirst = [...sitemapUrls].sort((a, b) => (rank.get(a) ?? Infinity) - (rank.get(b) ?? Infinity));
|
|
154
|
+
const listed = new Set(sitemapUrls);
|
|
155
|
+
return [.../* @__PURE__ */ new Set([...sitemapFirst, ...trafficUrls.filter((url) => !listed.has(url))])].filter((url) => urlInProperty(siteUrl, url));
|
|
156
|
+
}
|
|
157
|
+
const INSPECTION_CHUNK = 20;
|
|
158
|
+
async function syncInspections(deps) {
|
|
159
|
+
const state = await loadInspectionState(deps.dataSource, deps.ctx, deps.now());
|
|
160
|
+
let latest = state.latest;
|
|
161
|
+
const candidates = deps.candidates.filter((url) => urlInProperty(deps.siteUrl, url));
|
|
162
|
+
const plan = planInspections({
|
|
163
|
+
candidates,
|
|
164
|
+
latest,
|
|
165
|
+
recent: state.recent,
|
|
166
|
+
now: deps.now(),
|
|
167
|
+
limit: deps.limit
|
|
168
|
+
});
|
|
169
|
+
if (plan.urls.length === 0) return {
|
|
170
|
+
_tag: "nothing_due",
|
|
171
|
+
candidates: candidates.length,
|
|
172
|
+
quotaLeft: plan.quotaLeft
|
|
173
|
+
};
|
|
174
|
+
const failures = [];
|
|
175
|
+
let inspected = 0;
|
|
176
|
+
let done = 0;
|
|
177
|
+
let stopped;
|
|
178
|
+
const queue = [...plan.urls];
|
|
179
|
+
while (queue.length > 0 && !stopped) {
|
|
180
|
+
const decision = deps.ledger.reserve("urlInspection", deps.siteUrl, Math.min(INSPECTION_CHUNK, queue.length));
|
|
181
|
+
if (decision.kind === "exhausted") {
|
|
182
|
+
stopped = {
|
|
183
|
+
reason: decision.reason,
|
|
184
|
+
resetsAt: decision.resetsAt
|
|
185
|
+
};
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const chunk = queue.splice(0, decision.n);
|
|
189
|
+
const records = [];
|
|
190
|
+
let unused = 0;
|
|
191
|
+
await runWithConcurrency(chunk, deps.concurrency, async (url) => {
|
|
192
|
+
if (stopped) {
|
|
193
|
+
unused++;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const response = await deps.client.inspect(deps.siteUrl, url).catch((error) => ({ error }));
|
|
197
|
+
if ("error" in response) {
|
|
198
|
+
const refusal = parseQuotaRefusal(response.error);
|
|
199
|
+
if (refusal) {
|
|
200
|
+
deps.ledger.record("urlInspection", deps.siteUrl, {
|
|
201
|
+
kind: "refused",
|
|
202
|
+
reason: refusal
|
|
203
|
+
});
|
|
204
|
+
stopped ??= {
|
|
205
|
+
reason: refusal,
|
|
206
|
+
resetsAt: blockUntil("urlInspection", refusal, deps.now())
|
|
207
|
+
};
|
|
208
|
+
unused++;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
failures.push({
|
|
212
|
+
url,
|
|
213
|
+
error: googleErrorMessage(response.error)
|
|
214
|
+
});
|
|
215
|
+
} else records.push(toInspectionRecord({
|
|
216
|
+
url,
|
|
217
|
+
result: response.inspectionResult,
|
|
218
|
+
inspectedAt: deps.now(),
|
|
219
|
+
previous: latest.get(url)
|
|
220
|
+
}));
|
|
221
|
+
done++;
|
|
222
|
+
deps.onProgress?.(done, plan.urls.length);
|
|
223
|
+
});
|
|
224
|
+
if (unused > 0) deps.ledger.record("urlInspection", deps.siteUrl, {
|
|
225
|
+
kind: "unused",
|
|
226
|
+
n: unused
|
|
227
|
+
});
|
|
228
|
+
await recordInspections(deps.dataSource, deps.ctx, latest, records);
|
|
229
|
+
latest = latestByUrl([...latest.values(), ...records]);
|
|
230
|
+
inspected += records.length;
|
|
231
|
+
}
|
|
232
|
+
const notRun = plan.urls.length - done;
|
|
233
|
+
if (done === 0 && stopped) return {
|
|
234
|
+
_tag: "quota_exhausted",
|
|
235
|
+
reason: stopped.reason,
|
|
236
|
+
resetsAt: stopped.resetsAt,
|
|
237
|
+
deferred: plan.deferred + notRun
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
_tag: "inspected",
|
|
241
|
+
inspected,
|
|
242
|
+
failed: failures.length,
|
|
243
|
+
failures,
|
|
244
|
+
deferred: plan.deferred + notRun,
|
|
245
|
+
quotaLeft: Math.max(0, plan.quotaLeft - done),
|
|
246
|
+
...stopped ? { stopped } : {}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function toCount(value) {
|
|
250
|
+
const n = Number(value);
|
|
251
|
+
return Number.isFinite(n) ? n : 0;
|
|
252
|
+
}
|
|
253
|
+
function toSitemapListEntry(sitemap) {
|
|
254
|
+
if (!sitemap.path) return void 0;
|
|
255
|
+
return {
|
|
256
|
+
path: sitemap.path,
|
|
257
|
+
type: sitemap.type ?? null,
|
|
258
|
+
isPending: sitemap.isPending ?? false,
|
|
259
|
+
isSitemapsIndex: sitemap.isSitemapsIndex ?? false,
|
|
260
|
+
lastSubmitted: sitemap.lastSubmitted ?? null,
|
|
261
|
+
lastDownloaded: sitemap.lastDownloaded ?? null,
|
|
262
|
+
warnings: toCount(sitemap.warnings),
|
|
263
|
+
errors: toCount(sitemap.errors),
|
|
264
|
+
contents: (sitemap.contents ?? []).map((content) => ({
|
|
265
|
+
type: content.type ?? "unknown",
|
|
266
|
+
submitted: toCount(content.submitted),
|
|
267
|
+
indexed: content.indexed == null ? null : toCount(content.indexed)
|
|
268
|
+
}))
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function errorMessage(error) {
|
|
272
|
+
return error instanceof Error ? error.message : String(error);
|
|
273
|
+
}
|
|
274
|
+
async function syncSitemaps(deps) {
|
|
275
|
+
const listed = await deps.client.sitemaps.list(deps.siteUrl).catch((error) => new Error(errorMessage(error)));
|
|
276
|
+
if (listed instanceof Error) return {
|
|
277
|
+
_tag: "failed",
|
|
278
|
+
reason: `Search Console sitemap list failed: ${listed.message}`
|
|
279
|
+
};
|
|
280
|
+
const fetchedAt = deps.now();
|
|
281
|
+
const sitemaps = listed.flatMap((sitemap) => toSitemapListEntry(sitemap) ?? []);
|
|
282
|
+
const doc = {
|
|
283
|
+
version: 1,
|
|
284
|
+
fetchedAt: fetchedAt.toISOString(),
|
|
285
|
+
sitemaps
|
|
286
|
+
};
|
|
287
|
+
await createSitemapListStore({ dataSource: deps.dataSource }).save(deps.ctx, doc);
|
|
288
|
+
const store = createSitemapStore({
|
|
289
|
+
dataSource: deps.dataSource,
|
|
290
|
+
withMutation: (_ctx, effect) => effect(),
|
|
291
|
+
now: () => deps.now().getTime()
|
|
292
|
+
});
|
|
293
|
+
const current = await store.getSitemapGeneration(deps.ctx);
|
|
294
|
+
const published = new Set(current._tag === "available" ? Object.values(current.manifest.feeds).map((feed) => feed.feedpath) : []);
|
|
295
|
+
if (sitemaps.length === 0 && published.size === 0) return {
|
|
296
|
+
_tag: "saved",
|
|
297
|
+
sitemaps: 0,
|
|
298
|
+
urls: 0,
|
|
299
|
+
feeds: [],
|
|
300
|
+
generation: "unchanged"
|
|
301
|
+
};
|
|
302
|
+
const generation = {
|
|
303
|
+
_tag: "complete",
|
|
304
|
+
id: deps.generationId(),
|
|
305
|
+
observedAt: fetchedAt.getTime()
|
|
306
|
+
};
|
|
307
|
+
const feeds = [];
|
|
308
|
+
const staged = [];
|
|
309
|
+
let urls = 0;
|
|
310
|
+
let keepReason;
|
|
311
|
+
for (const sitemap of sitemaps) {
|
|
312
|
+
const loaded = await deps.loadFeed(sitemap.path);
|
|
313
|
+
if (loaded._tag === "error") {
|
|
314
|
+
feeds.push({
|
|
315
|
+
_tag: "unreachable",
|
|
316
|
+
path: sitemap.path,
|
|
317
|
+
message: loaded.message
|
|
318
|
+
});
|
|
319
|
+
const identity = parseSitemapFeedIdentity(sitemap.path);
|
|
320
|
+
if (identity._tag === "ok" && published.has(identity.url)) {
|
|
321
|
+
keepReason = `${sitemap.path} could not be read: ${loaded.message}`;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const result = await store.stageSitemapGenerationFeed(deps.ctx, generation, sitemap.path, loaded.entries);
|
|
327
|
+
if (result._tag !== "staged") {
|
|
328
|
+
feeds.push({
|
|
329
|
+
_tag: "unreachable",
|
|
330
|
+
path: sitemap.path,
|
|
331
|
+
message: `not stored (${result._tag}: ${result.reason})`
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
staged.push(sitemap.path);
|
|
336
|
+
urls += loaded.entries.length;
|
|
337
|
+
feeds.push({
|
|
338
|
+
_tag: "read",
|
|
339
|
+
path: sitemap.path,
|
|
340
|
+
urls: loaded.entries.length,
|
|
341
|
+
complete: loaded.complete
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (keepReason) {
|
|
345
|
+
await store.abortSitemapGeneration(deps.ctx, generation);
|
|
346
|
+
return {
|
|
347
|
+
_tag: "list_only",
|
|
348
|
+
sitemaps: sitemaps.length,
|
|
349
|
+
feeds,
|
|
350
|
+
reason: keepReason
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const finalized = await store.finalizeSitemapGeneration(deps.ctx, generation, {
|
|
354
|
+
_tag: "complete",
|
|
355
|
+
expectedFeedpaths: staged
|
|
356
|
+
});
|
|
357
|
+
if (finalized._tag === "published" || finalized._tag === "unchanged") return {
|
|
358
|
+
_tag: "saved",
|
|
359
|
+
sitemaps: sitemaps.length,
|
|
360
|
+
urls,
|
|
361
|
+
feeds,
|
|
362
|
+
generation: finalized._tag
|
|
363
|
+
};
|
|
364
|
+
await store.abortSitemapGeneration(deps.ctx, generation);
|
|
365
|
+
return {
|
|
366
|
+
_tag: "list_only",
|
|
367
|
+
sitemaps: sitemaps.length,
|
|
368
|
+
feeds,
|
|
369
|
+
reason: `sitemap URLs not stored: ${finalized._tag}`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function loadSitemapGenerationUrls(dataSource, ctx) {
|
|
373
|
+
const result = await createSitemapReadStore({ dataSource }).iterateSitemapGenerationUrls(ctx);
|
|
374
|
+
if (result._tag !== "iterator") return [];
|
|
375
|
+
const urls = [];
|
|
376
|
+
for await (const record of result.items) urls.push(record.loc);
|
|
377
|
+
return urls;
|
|
378
|
+
}
|
|
379
|
+
const ENTITY_DATASETS = [
|
|
380
|
+
"inspections",
|
|
381
|
+
"inspection_history",
|
|
382
|
+
"sitemaps",
|
|
383
|
+
"sitemap_urls",
|
|
384
|
+
"indexing_metadata"
|
|
385
|
+
];
|
|
386
|
+
const INSPECTION_COLUMNS = [
|
|
387
|
+
{
|
|
388
|
+
name: "url",
|
|
389
|
+
type: "VARCHAR",
|
|
390
|
+
nullable: false
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "inspected_at",
|
|
394
|
+
type: "VARCHAR",
|
|
395
|
+
nullable: false
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "index_status",
|
|
399
|
+
type: "VARCHAR",
|
|
400
|
+
nullable: true
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "coverage_state",
|
|
404
|
+
type: "VARCHAR",
|
|
405
|
+
nullable: true
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "indexing_state",
|
|
409
|
+
type: "VARCHAR",
|
|
410
|
+
nullable: true
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "page_fetch_state",
|
|
414
|
+
type: "VARCHAR",
|
|
415
|
+
nullable: true
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "robots_txt_state",
|
|
419
|
+
type: "VARCHAR",
|
|
420
|
+
nullable: true
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "last_crawl_time",
|
|
424
|
+
type: "VARCHAR",
|
|
425
|
+
nullable: true
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "google_canonical",
|
|
429
|
+
type: "VARCHAR",
|
|
430
|
+
nullable: true
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: "user_canonical",
|
|
434
|
+
type: "VARCHAR",
|
|
435
|
+
nullable: true
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "mobile_usability_verdict",
|
|
439
|
+
type: "VARCHAR",
|
|
440
|
+
nullable: true
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "rich_results_verdict",
|
|
444
|
+
type: "VARCHAR",
|
|
445
|
+
nullable: true
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "next_check_at",
|
|
449
|
+
type: "VARCHAR",
|
|
450
|
+
nullable: true
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
name: "raw",
|
|
454
|
+
type: "VARCHAR",
|
|
455
|
+
nullable: true
|
|
456
|
+
}
|
|
457
|
+
];
|
|
458
|
+
const SITEMAP_COLUMNS = [
|
|
459
|
+
{
|
|
460
|
+
name: "path",
|
|
461
|
+
type: "VARCHAR",
|
|
462
|
+
nullable: false
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "type",
|
|
466
|
+
type: "VARCHAR",
|
|
467
|
+
nullable: true
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "is_pending",
|
|
471
|
+
type: "INTEGER",
|
|
472
|
+
nullable: false
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "is_sitemaps_index",
|
|
476
|
+
type: "INTEGER",
|
|
477
|
+
nullable: false
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "last_submitted",
|
|
481
|
+
type: "VARCHAR",
|
|
482
|
+
nullable: true
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "last_downloaded",
|
|
486
|
+
type: "VARCHAR",
|
|
487
|
+
nullable: true
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "warnings",
|
|
491
|
+
type: "BIGINT",
|
|
492
|
+
nullable: false
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "errors",
|
|
496
|
+
type: "BIGINT",
|
|
497
|
+
nullable: false
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "submitted",
|
|
501
|
+
type: "BIGINT",
|
|
502
|
+
nullable: false
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: "contents",
|
|
506
|
+
type: "VARCHAR",
|
|
507
|
+
nullable: false
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "fetched_at",
|
|
511
|
+
type: "VARCHAR",
|
|
512
|
+
nullable: false
|
|
513
|
+
}
|
|
514
|
+
];
|
|
515
|
+
const SITEMAP_URL_COLUMNS = [
|
|
516
|
+
{
|
|
517
|
+
name: "sitemap_path",
|
|
518
|
+
type: "VARCHAR",
|
|
519
|
+
nullable: false
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: "url",
|
|
523
|
+
type: "VARCHAR",
|
|
524
|
+
nullable: false
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "lastmod",
|
|
528
|
+
type: "VARCHAR",
|
|
529
|
+
nullable: true
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: "first_seen_at",
|
|
533
|
+
type: "VARCHAR",
|
|
534
|
+
nullable: false
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "last_seen_at",
|
|
538
|
+
type: "VARCHAR",
|
|
539
|
+
nullable: false
|
|
540
|
+
}
|
|
541
|
+
];
|
|
542
|
+
const INDEXING_METADATA_COLUMNS = [
|
|
543
|
+
{
|
|
544
|
+
name: "url",
|
|
545
|
+
type: "VARCHAR",
|
|
546
|
+
nullable: false
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "captured_at",
|
|
550
|
+
type: "VARCHAR",
|
|
551
|
+
nullable: false
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: "latest_update_at",
|
|
555
|
+
type: "VARCHAR",
|
|
556
|
+
nullable: true
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: "latest_remove_at",
|
|
560
|
+
type: "VARCHAR",
|
|
561
|
+
nullable: true
|
|
562
|
+
}
|
|
563
|
+
];
|
|
564
|
+
function inspectionRow(record) {
|
|
565
|
+
const nextAt = scheduleOf(record).nextAt;
|
|
566
|
+
return {
|
|
567
|
+
url: record.url,
|
|
568
|
+
inspected_at: record.inspectedAt,
|
|
569
|
+
index_status: record.indexStatus ?? null,
|
|
570
|
+
coverage_state: record.coverageState ?? null,
|
|
571
|
+
indexing_state: record.indexingState ?? null,
|
|
572
|
+
page_fetch_state: record.pageFetchState ?? null,
|
|
573
|
+
robots_txt_state: record.robotsTxtState ?? null,
|
|
574
|
+
last_crawl_time: record.lastCrawlTime ?? null,
|
|
575
|
+
google_canonical: record.googleCanonical ?? null,
|
|
576
|
+
user_canonical: record.userCanonical ?? null,
|
|
577
|
+
mobile_usability_verdict: record.mobileUsabilityVerdict ?? null,
|
|
578
|
+
rich_results_verdict: record.richResultsVerdict ?? null,
|
|
579
|
+
next_check_at: Number.isFinite(nextAt) && nextAt > 0 ? new Date(nextAt).toISOString() : null,
|
|
580
|
+
raw: record.raw ? JSON.stringify(record.raw) : null
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function sitemapRow(entry, fetchedAt) {
|
|
584
|
+
return {
|
|
585
|
+
path: entry.path,
|
|
586
|
+
type: entry.type,
|
|
587
|
+
is_pending: entry.isPending,
|
|
588
|
+
is_sitemaps_index: entry.isSitemapsIndex,
|
|
589
|
+
last_submitted: entry.lastSubmitted,
|
|
590
|
+
last_downloaded: entry.lastDownloaded,
|
|
591
|
+
warnings: entry.warnings,
|
|
592
|
+
errors: entry.errors,
|
|
593
|
+
submitted: entry.contents.reduce((sum, content) => sum + content.submitted, 0),
|
|
594
|
+
contents: JSON.stringify(entry.contents),
|
|
595
|
+
fetched_at: fetchedAt
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
async function sitemapUrlRows(dataSource, ctx) {
|
|
599
|
+
const result = await createSitemapReadStore({ dataSource }).iterateSitemapGenerationUrls(ctx);
|
|
600
|
+
if (result._tag !== "iterator") return [];
|
|
601
|
+
const rows = [];
|
|
602
|
+
for await (const record of result.items) rows.push({
|
|
603
|
+
sitemap_path: record.feedpath,
|
|
604
|
+
url: record.loc,
|
|
605
|
+
lastmod: record.lastmod ?? null,
|
|
606
|
+
first_seen_at: new Date(record.firstSeenAt).toISOString(),
|
|
607
|
+
last_seen_at: new Date(record.lastSeenAt).toISOString()
|
|
608
|
+
});
|
|
609
|
+
return rows;
|
|
610
|
+
}
|
|
611
|
+
async function readEntityDatasets(dataSource, ctx, wanted) {
|
|
612
|
+
const want = new Set(wanted);
|
|
613
|
+
const out = [];
|
|
614
|
+
if (want.has("inspections") || want.has("inspection_history")) {
|
|
615
|
+
const history = (await loadInspectionHistory(dataSource, ctx)).sort((a, b) => a.url.localeCompare(b.url) || a.inspectedAt.localeCompare(b.inspectedAt));
|
|
616
|
+
if (want.has("inspections")) {
|
|
617
|
+
const latest = [...latestByUrl(history).values()].sort((a, b) => a.url.localeCompare(b.url));
|
|
618
|
+
out.push({
|
|
619
|
+
dataset: "inspections",
|
|
620
|
+
columns: INSPECTION_COLUMNS,
|
|
621
|
+
rows: latest.map(inspectionRow)
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (want.has("inspection_history")) out.push({
|
|
625
|
+
dataset: "inspection_history",
|
|
626
|
+
columns: INSPECTION_COLUMNS,
|
|
627
|
+
rows: history.map(inspectionRow)
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (want.has("sitemaps")) {
|
|
631
|
+
const doc = await createSitemapListStore({ dataSource }).load(ctx);
|
|
632
|
+
out.push({
|
|
633
|
+
dataset: "sitemaps",
|
|
634
|
+
columns: SITEMAP_COLUMNS,
|
|
635
|
+
rows: doc ? doc.sitemaps.map((entry) => sitemapRow(entry, doc.fetchedAt)) : []
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (want.has("sitemap_urls")) out.push({
|
|
639
|
+
dataset: "sitemap_urls",
|
|
640
|
+
columns: SITEMAP_URL_COLUMNS,
|
|
641
|
+
rows: await sitemapUrlRows(dataSource, ctx)
|
|
642
|
+
});
|
|
643
|
+
if (want.has("indexing_metadata")) {
|
|
644
|
+
const index = await createIndexingMetadataStore({ dataSource }).loadIndex(ctx);
|
|
645
|
+
out.push({
|
|
646
|
+
dataset: "indexing_metadata",
|
|
647
|
+
columns: INDEXING_METADATA_COLUMNS,
|
|
648
|
+
rows: Object.values(index.records).sort((a, b) => a.url.localeCompare(b.url)).map((record) => ({
|
|
649
|
+
url: record.url,
|
|
650
|
+
captured_at: record.capturedAt,
|
|
651
|
+
latest_update_at: record.latestUpdateAt ?? null,
|
|
652
|
+
latest_remove_at: record.latestRemoveAt ?? null
|
|
653
|
+
}))
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
export { ENTITY_DATASETS, appendInspections, inspectionCandidates, loadInspectionHistory, loadInspectionState, loadSitemapGenerationUrls, materializeInspectionIndex, readEntityDatasets, recordInspections, resolvePagePaths, syncInspections, syncSitemaps, toSitemapListEntry, urlInProperty };
|