@gscdump/cli 0.37.3 → 0.37.4
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/bin/gscdump.mjs +3 -0
- package/dist/_chunks/analysis-local.mjs +88 -0
- package/dist/_chunks/analyze.mjs +332 -0
- package/dist/_chunks/auth.mjs +476 -0
- package/dist/_chunks/auth2.mjs +293 -0
- package/dist/_chunks/config.mjs +93 -0
- package/dist/_chunks/config2.mjs +232 -0
- package/dist/_chunks/context.mjs +69 -0
- package/dist/_chunks/doctor.mjs +400 -0
- package/dist/_chunks/dump.mjs +193 -0
- package/dist/_chunks/entities.mjs +414 -0
- package/dist/_chunks/env-file.mjs +53 -0
- package/dist/_chunks/environment.mjs +30 -0
- package/dist/_chunks/error-handler.mjs +86 -0
- package/dist/_chunks/indexing.mjs +314 -0
- package/dist/_chunks/init.mjs +190 -0
- package/dist/_chunks/inspect.mjs +203 -0
- package/dist/_chunks/mcp.mjs +867 -0
- package/dist/_chunks/native-duckdb.mjs +46 -0
- package/dist/_chunks/profile.mjs +290 -0
- package/dist/_chunks/query.mjs +537 -0
- package/dist/_chunks/report.mjs +243 -0
- package/dist/_chunks/sitemaps.mjs +274 -0
- package/dist/_chunks/sites.mjs +500 -0
- package/dist/_chunks/store.mjs +652 -0
- package/dist/_chunks/sync.mjs +489 -0
- package/dist/_chunks/utils.mjs +191 -0
- package/dist/cli.d.mts +28 -0
- package/dist/cli.mjs +104 -0
- package/package.json +15 -7
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -7330
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { loadConfig } from "./config.mjs";
|
|
2
|
+
import { ALL_SEARCH_TYPES, exportToCSV, logger, parseSearchType } from "./utils.mjs";
|
|
3
|
+
import { allTables, createCommandContext, inferTable } from "./context.mjs";
|
|
4
|
+
import { gscErrorHandler } from "./error-handler.mjs";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import { cancel, isCancel, multiselect, text } from "@clack/prompts";
|
|
9
|
+
import { and, between, contains, country, date, device, eq, gsc, hour, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
|
|
10
|
+
import { daysAgoUtc } from "gscdump/dates";
|
|
11
|
+
import { collectSpans } from "@gscdump/engine";
|
|
12
|
+
const DIMENSIONS = [
|
|
13
|
+
"page",
|
|
14
|
+
"query",
|
|
15
|
+
"date",
|
|
16
|
+
"hour",
|
|
17
|
+
"country",
|
|
18
|
+
"device",
|
|
19
|
+
"searchAppearance"
|
|
20
|
+
];
|
|
21
|
+
const DIM_COLUMNS = {
|
|
22
|
+
page,
|
|
23
|
+
query,
|
|
24
|
+
date,
|
|
25
|
+
hour,
|
|
26
|
+
country,
|
|
27
|
+
device,
|
|
28
|
+
searchAppearance
|
|
29
|
+
};
|
|
30
|
+
const FILTER_DIMS = [
|
|
31
|
+
"query",
|
|
32
|
+
"page",
|
|
33
|
+
"country",
|
|
34
|
+
"device",
|
|
35
|
+
"searchAppearance"
|
|
36
|
+
];
|
|
37
|
+
const FILTER_COL = {
|
|
38
|
+
query,
|
|
39
|
+
page,
|
|
40
|
+
country,
|
|
41
|
+
device,
|
|
42
|
+
searchAppearance
|
|
43
|
+
};
|
|
44
|
+
const DATA_STATES = [
|
|
45
|
+
"all",
|
|
46
|
+
"final",
|
|
47
|
+
"hourly_all"
|
|
48
|
+
];
|
|
49
|
+
const AGGREGATION_TYPES = [
|
|
50
|
+
"auto",
|
|
51
|
+
"byPage",
|
|
52
|
+
"byProperty"
|
|
53
|
+
];
|
|
54
|
+
function buildFilterFromArg(col, raw) {
|
|
55
|
+
if (raw.startsWith("!~")) return makeLeaf(col, "notContains", raw.slice(2));
|
|
56
|
+
if (raw.startsWith("!re:")) return notRegex(col, raw.slice(4));
|
|
57
|
+
if (raw.startsWith("!")) return makeLeaf(col, "notEquals", raw.slice(1));
|
|
58
|
+
if (raw.startsWith("~")) return contains(col, raw.slice(1));
|
|
59
|
+
if (raw.startsWith("re:")) return regex(col, raw.slice(3));
|
|
60
|
+
if (raw.startsWith("contains:")) return contains(col, raw.slice(9));
|
|
61
|
+
if (raw.startsWith("eq:")) return eq(col, raw.slice(3));
|
|
62
|
+
return eq(col, raw);
|
|
63
|
+
}
|
|
64
|
+
function makeLeaf(col, operator, value) {
|
|
65
|
+
return {
|
|
66
|
+
_constraints: {},
|
|
67
|
+
_filters: [{
|
|
68
|
+
dimension: col.dimension,
|
|
69
|
+
operator,
|
|
70
|
+
expression: value
|
|
71
|
+
}]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function runLiveQuery(client, siteUrl, opts) {
|
|
75
|
+
const allRows = [];
|
|
76
|
+
const totalLimit = Math.max(0, opts.rowLimit);
|
|
77
|
+
const pageSize = Math.min(totalLimit, 25e3);
|
|
78
|
+
let startRow = 0;
|
|
79
|
+
const baseBody = {
|
|
80
|
+
startDate: opts.startDate,
|
|
81
|
+
endDate: opts.endDate,
|
|
82
|
+
dimensions: opts.dimensions
|
|
83
|
+
};
|
|
84
|
+
if (opts.searchType) baseBody.type = opts.searchType;
|
|
85
|
+
if (opts.dataState) baseBody.dataState = opts.dataState;
|
|
86
|
+
if (opts.aggregationType) baseBody.aggregationType = opts.aggregationType;
|
|
87
|
+
if (opts.dimensionFilter) {
|
|
88
|
+
const groups = filterToGroups(opts.dimensionFilter);
|
|
89
|
+
if (groups.length > 0) baseBody.dimensionFilterGroups = groups;
|
|
90
|
+
}
|
|
91
|
+
while (allRows.length < totalLimit) {
|
|
92
|
+
const remaining = totalLimit - allRows.length;
|
|
93
|
+
const rowLimit = Math.min(pageSize, remaining);
|
|
94
|
+
if (rowLimit <= 0) break;
|
|
95
|
+
const rows = ((await client._rawQuery(siteUrl, {
|
|
96
|
+
...baseBody,
|
|
97
|
+
rowLimit,
|
|
98
|
+
startRow
|
|
99
|
+
})).rows || []).map((row) => {
|
|
100
|
+
const result = {
|
|
101
|
+
clicks: row.clicks ?? 0,
|
|
102
|
+
impressions: row.impressions ?? 0,
|
|
103
|
+
ctr: row.ctr ?? 0,
|
|
104
|
+
position: row.position ?? 0
|
|
105
|
+
};
|
|
106
|
+
opts.dimensions.forEach((dim, i) => {
|
|
107
|
+
result[dim] = row.keys?.[i];
|
|
108
|
+
});
|
|
109
|
+
return result;
|
|
110
|
+
});
|
|
111
|
+
if (rows.length === 0) break;
|
|
112
|
+
allRows.push(...rows);
|
|
113
|
+
startRow += rows.length;
|
|
114
|
+
}
|
|
115
|
+
return { rows: allRows };
|
|
116
|
+
}
|
|
117
|
+
function filterToGroups(f) {
|
|
118
|
+
if (f._filters.length === 0) return [];
|
|
119
|
+
return [{ filters: f._filters.map((leaf) => ({
|
|
120
|
+
dimension: leaf.dimension,
|
|
121
|
+
operator: leaf.operator,
|
|
122
|
+
expression: leaf.expression
|
|
123
|
+
})) }];
|
|
124
|
+
}
|
|
125
|
+
const queryCommand = defineCommand({
|
|
126
|
+
meta: {
|
|
127
|
+
name: "query",
|
|
128
|
+
description: "Run a search analytics query (local Parquet by default, --live hits GSC API)"
|
|
129
|
+
},
|
|
130
|
+
args: {
|
|
131
|
+
"site": {
|
|
132
|
+
type: "string",
|
|
133
|
+
alias: "s",
|
|
134
|
+
description: "Site URL (e.g., sc-domain:example.com)"
|
|
135
|
+
},
|
|
136
|
+
"dimensions": {
|
|
137
|
+
type: "string",
|
|
138
|
+
alias: "d",
|
|
139
|
+
description: `Dimensions: ${DIMENSIONS.join(",")}`
|
|
140
|
+
},
|
|
141
|
+
"start": {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "Start date (YYYY-MM-DD)"
|
|
144
|
+
},
|
|
145
|
+
"end": {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "End date (YYYY-MM-DD)"
|
|
148
|
+
},
|
|
149
|
+
"limit": {
|
|
150
|
+
type: "string",
|
|
151
|
+
alias: "l",
|
|
152
|
+
default: "1000",
|
|
153
|
+
description: "Max rows (default: 1000)"
|
|
154
|
+
},
|
|
155
|
+
"output": {
|
|
156
|
+
type: "string",
|
|
157
|
+
alias: "o",
|
|
158
|
+
description: "Output file path (default: stdout)"
|
|
159
|
+
},
|
|
160
|
+
"format": {
|
|
161
|
+
type: "string",
|
|
162
|
+
alias: "f",
|
|
163
|
+
default: "json",
|
|
164
|
+
description: "Output format: json or csv"
|
|
165
|
+
},
|
|
166
|
+
"sql": {
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Raw DuckDB SQL using {{FILES}} as the file list placeholder (bypasses builder)"
|
|
169
|
+
},
|
|
170
|
+
"table": {
|
|
171
|
+
type: "string",
|
|
172
|
+
description: "Analytics table for --sql (default: pages)"
|
|
173
|
+
},
|
|
174
|
+
"live": {
|
|
175
|
+
type: "boolean",
|
|
176
|
+
default: false,
|
|
177
|
+
description: "Bypass local store; hit the GSC API directly"
|
|
178
|
+
},
|
|
179
|
+
"quiet": {
|
|
180
|
+
type: "boolean",
|
|
181
|
+
alias: "q",
|
|
182
|
+
default: false,
|
|
183
|
+
description: "Suppress progress output"
|
|
184
|
+
},
|
|
185
|
+
"interactive": {
|
|
186
|
+
type: "boolean",
|
|
187
|
+
alias: "i",
|
|
188
|
+
default: false,
|
|
189
|
+
description: "Interactive mode"
|
|
190
|
+
},
|
|
191
|
+
"query": {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Filter by query (prefix: ~contains, !exclude, re:regex, !re:not-regex)"
|
|
194
|
+
},
|
|
195
|
+
"page": {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Filter by page (same prefix syntax as --query)"
|
|
198
|
+
},
|
|
199
|
+
"country": {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Filter by country (ISO-3 lowercase, e.g. usa). Same prefix syntax as --query"
|
|
202
|
+
},
|
|
203
|
+
"device": {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Filter by device (DESKTOP/MOBILE/TABLET). Same prefix syntax"
|
|
206
|
+
},
|
|
207
|
+
"search-appearance": {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "Filter by search appearance feature. Same prefix syntax"
|
|
210
|
+
},
|
|
211
|
+
"type": {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: `Search type (live mode only). One of: ${ALL_SEARCH_TYPES.join(",")}`
|
|
214
|
+
},
|
|
215
|
+
"data-state": {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: `Data state (live mode only). One of: ${DATA_STATES.join(",")} (default: final)`
|
|
218
|
+
},
|
|
219
|
+
"aggregation-type": {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: `Aggregation type (live mode only). One of: ${AGGREGATION_TYPES.join(",")}`
|
|
222
|
+
},
|
|
223
|
+
"explain": {
|
|
224
|
+
type: "boolean",
|
|
225
|
+
default: false,
|
|
226
|
+
description: "Print the request body / planned local SQL and exit without executing"
|
|
227
|
+
},
|
|
228
|
+
"profile": {
|
|
229
|
+
type: "boolean",
|
|
230
|
+
default: false,
|
|
231
|
+
description: "Print a query timing breakdown (manifest list / file fetch / SQL run) to stderr (local mode only)"
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
async run({ args }) {
|
|
235
|
+
if (args.sql) {
|
|
236
|
+
await runRawSqlMode({
|
|
237
|
+
sql: String(args.sql),
|
|
238
|
+
site: args.site ? String(args.site) : void 0,
|
|
239
|
+
table: args.table ? String(args.table) : "pages",
|
|
240
|
+
output: args.output ? String(args.output) : void 0,
|
|
241
|
+
quiet: Boolean(args.quiet),
|
|
242
|
+
searchType: parseSearchType(args.type, "--type")
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const ctxConfig = await loadConfig();
|
|
247
|
+
const dimNames = await resolveDimensions(args);
|
|
248
|
+
const { startDate, endDate } = await resolveRange(args);
|
|
249
|
+
await promptFilters(args);
|
|
250
|
+
const limitArg = args.limit != null ? String(args.limit) : null;
|
|
251
|
+
const rowLimit = limitArg != null && limitArg !== "1000" ? Number.parseInt(limitArg, 10) : ctxConfig.defaultLimit ?? 1e3;
|
|
252
|
+
const format = String(args.format);
|
|
253
|
+
const dimensionFilter = buildDimensionFilter(args);
|
|
254
|
+
const searchType = parseSearchType(args.type ?? ctxConfig.defaultSearchType, "--type");
|
|
255
|
+
const dataState = args["data-state"] ? String(args["data-state"]) : ctxConfig.defaultDataState;
|
|
256
|
+
const aggregationType = args["aggregation-type"] ? String(args["aggregation-type"]) : void 0;
|
|
257
|
+
if (dataState && !DATA_STATES.includes(dataState)) {
|
|
258
|
+
logger.error(`Invalid --data-state: ${dataState}. Allowed: ${DATA_STATES.join(", ")}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
if (aggregationType && !AGGREGATION_TYPES.includes(aggregationType)) {
|
|
262
|
+
logger.error(`Invalid --aggregation-type: ${aggregationType}. Allowed: ${AGGREGATION_TYPES.join(", ")}`);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
const ctx = await createCommandContext({
|
|
266
|
+
needsAuth: true,
|
|
267
|
+
needsStore: !args.live,
|
|
268
|
+
interactive: Boolean(args.interactive)
|
|
269
|
+
});
|
|
270
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
271
|
+
if (args.live) {
|
|
272
|
+
if (args.explain) {
|
|
273
|
+
const body = {
|
|
274
|
+
startDate,
|
|
275
|
+
endDate,
|
|
276
|
+
dimensions: dimNames,
|
|
277
|
+
rowLimit
|
|
278
|
+
};
|
|
279
|
+
if (searchType) body.type = searchType;
|
|
280
|
+
if (dataState) body.dataState = dataState;
|
|
281
|
+
if (aggregationType) body.aggregationType = aggregationType;
|
|
282
|
+
if (dimensionFilter) body.dimensionFilterGroups = filterToGroups(dimensionFilter);
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
siteUrl,
|
|
285
|
+
body
|
|
286
|
+
}, null, 2));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!args.quiet) logger.info(`Querying ${siteUrl} via live GSC API...`);
|
|
290
|
+
const result = await runLiveQuery(ctx.client, siteUrl, {
|
|
291
|
+
startDate,
|
|
292
|
+
endDate,
|
|
293
|
+
dimensions: dimNames,
|
|
294
|
+
rowLimit,
|
|
295
|
+
searchType,
|
|
296
|
+
dataState,
|
|
297
|
+
aggregationType,
|
|
298
|
+
dimensionFilter
|
|
299
|
+
}).catch(gscErrorHandler);
|
|
300
|
+
await writeOutput({
|
|
301
|
+
output: {
|
|
302
|
+
siteUrl,
|
|
303
|
+
dimensions: dimNames,
|
|
304
|
+
dateRange: {
|
|
305
|
+
start: startDate,
|
|
306
|
+
end: endDate
|
|
307
|
+
},
|
|
308
|
+
total: result.rows.length,
|
|
309
|
+
data: result.rows
|
|
310
|
+
},
|
|
311
|
+
format,
|
|
312
|
+
path: args.output ? String(args.output) : void 0,
|
|
313
|
+
quiet: Boolean(args.quiet)
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
|
|
318
|
+
if (!args.quiet) logger.info(`Querying ${siteUrl} from local Parquet store...`);
|
|
319
|
+
const state = buildLocalState(dimNames, startDate, endDate, rowLimit, dimensionFilter);
|
|
320
|
+
const store = ctx.store;
|
|
321
|
+
const table = inferTable(dimNames);
|
|
322
|
+
if (args.explain) {
|
|
323
|
+
console.log(JSON.stringify({
|
|
324
|
+
siteUrl,
|
|
325
|
+
table,
|
|
326
|
+
state
|
|
327
|
+
}, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
await assertRangeCovered(store, siteUrl, table, startDate, endDate, searchType);
|
|
331
|
+
const probe = Boolean(args.profile) ? collectSpans() : void 0;
|
|
332
|
+
const result = await store.engine.query({
|
|
333
|
+
userId: store.userId,
|
|
334
|
+
siteId: store.siteIdFor(siteUrl),
|
|
335
|
+
table,
|
|
336
|
+
...searchType !== void 0 ? { searchType } : {},
|
|
337
|
+
...probe ? { profiler: probe.profiler } : {}
|
|
338
|
+
}, state).catch((e) => {
|
|
339
|
+
logger.error(`Query failed: ${e.message}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
if (probe) logProfile(probe.spans);
|
|
343
|
+
await writeOutput({
|
|
344
|
+
output: {
|
|
345
|
+
siteUrl,
|
|
346
|
+
dimensions: dimNames,
|
|
347
|
+
dateRange: {
|
|
348
|
+
start: startDate,
|
|
349
|
+
end: endDate
|
|
350
|
+
},
|
|
351
|
+
total: result.rows.length,
|
|
352
|
+
data: result.rows
|
|
353
|
+
},
|
|
354
|
+
format,
|
|
355
|
+
path: args.output ? String(args.output) : void 0,
|
|
356
|
+
quiet: Boolean(args.quiet)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
async function resolveDimensions(args) {
|
|
361
|
+
if (args.dimensions) return String(args.dimensions).split(",").filter((d) => DIMENSIONS.includes(d));
|
|
362
|
+
if (args.interactive) {
|
|
363
|
+
const selected = await multiselect({
|
|
364
|
+
message: "Select dimensions",
|
|
365
|
+
options: DIMENSIONS.map((d) => ({
|
|
366
|
+
value: d,
|
|
367
|
+
label: d
|
|
368
|
+
})),
|
|
369
|
+
initialValues: ["page", "query"]
|
|
370
|
+
});
|
|
371
|
+
if (isCancel(selected)) {
|
|
372
|
+
cancel("Cancelled");
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
return selected;
|
|
376
|
+
}
|
|
377
|
+
return ["page", "query"];
|
|
378
|
+
}
|
|
379
|
+
async function resolveRange(args) {
|
|
380
|
+
if (args.start && args.end) return {
|
|
381
|
+
startDate: String(args.start),
|
|
382
|
+
endDate: String(args.end)
|
|
383
|
+
};
|
|
384
|
+
if (args.interactive) {
|
|
385
|
+
const startInput = await text({
|
|
386
|
+
message: "Start date (YYYY-MM-DD)",
|
|
387
|
+
placeholder: daysAgoUtc(28)
|
|
388
|
+
});
|
|
389
|
+
if (isCancel(startInput)) {
|
|
390
|
+
cancel("Cancelled");
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
const endInput = await text({
|
|
394
|
+
message: "End date (YYYY-MM-DD)",
|
|
395
|
+
placeholder: daysAgoUtc(3)
|
|
396
|
+
});
|
|
397
|
+
if (isCancel(endInput)) {
|
|
398
|
+
cancel("Cancelled");
|
|
399
|
+
process.exit(0);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
startDate: String(startInput) || daysAgoUtc(28),
|
|
403
|
+
endDate: String(endInput) || daysAgoUtc(3)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
startDate: daysAgoUtc(31),
|
|
408
|
+
endDate: daysAgoUtc(3)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function promptFilters(args) {
|
|
412
|
+
if (!args.interactive) return;
|
|
413
|
+
for (const dim of FILTER_DIMS) {
|
|
414
|
+
if (args[dim]) continue;
|
|
415
|
+
const v = await text({
|
|
416
|
+
message: `Filter by ${dim} (blank to skip; prefix ~ for contains, ! for not-equals, re: for regex)`,
|
|
417
|
+
placeholder: ""
|
|
418
|
+
});
|
|
419
|
+
if (isCancel(v)) {
|
|
420
|
+
cancel("Cancelled");
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
if (v && String(v).length > 0) args[dim] = String(v);
|
|
424
|
+
}
|
|
425
|
+
if (!args.type) {
|
|
426
|
+
const t = await text({
|
|
427
|
+
message: `Search type (blank for default web; allowed: ${ALL_SEARCH_TYPES.join(", ")})`,
|
|
428
|
+
placeholder: ""
|
|
429
|
+
});
|
|
430
|
+
if (isCancel(t)) {
|
|
431
|
+
cancel("Cancelled");
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
if (t && String(t).length > 0) args.type = String(t);
|
|
435
|
+
}
|
|
436
|
+
if (!args["data-state"]) {
|
|
437
|
+
const ds = await text({
|
|
438
|
+
message: `Data state (blank for default 'final'; allowed: ${DATA_STATES.join(", ")})`,
|
|
439
|
+
placeholder: ""
|
|
440
|
+
});
|
|
441
|
+
if (isCancel(ds)) {
|
|
442
|
+
cancel("Cancelled");
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
if (ds && String(ds).length > 0) args["data-state"] = String(ds);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function buildLocalState(dimNames, startDate, endDate, rowLimit, dimensionFilter) {
|
|
449
|
+
const dims = dimNames.map((d) => DIM_COLUMNS[d]).filter((c) => Boolean(c));
|
|
450
|
+
const dateFilter = between(date, startDate, endDate);
|
|
451
|
+
const filter = dimensionFilter ? and(dateFilter, dimensionFilter) : dateFilter;
|
|
452
|
+
return gsc.select(...dims).where(filter).limit(rowLimit).getState();
|
|
453
|
+
}
|
|
454
|
+
function buildDimensionFilter(args) {
|
|
455
|
+
const leaves = [];
|
|
456
|
+
for (const dim of FILTER_DIMS) {
|
|
457
|
+
const raw = args[dim];
|
|
458
|
+
if (raw == null || raw === "") continue;
|
|
459
|
+
leaves.push(buildFilterFromArg(FILTER_COL[dim], String(raw)));
|
|
460
|
+
}
|
|
461
|
+
if (leaves.length === 0) return void 0;
|
|
462
|
+
if (leaves.length === 1) return leaves[0];
|
|
463
|
+
return and(...leaves);
|
|
464
|
+
}
|
|
465
|
+
async function assertRangeCovered(store, siteUrl, table, startDate, endDate, searchType) {
|
|
466
|
+
const wm = (await store.engine.getWatermarks({
|
|
467
|
+
userId: store.userId,
|
|
468
|
+
siteId: store.siteIdFor(siteUrl),
|
|
469
|
+
table,
|
|
470
|
+
...searchType !== void 0 ? { searchType } : {}
|
|
471
|
+
}))[0];
|
|
472
|
+
if (!wm) {
|
|
473
|
+
logger.error(`No data synced for ${siteUrl} / ${table}. Run \`gscdump sync\` first, or pass --live.`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
if (endDate > wm.newestDateSynced) {
|
|
477
|
+
logger.error(`Requested end=${endDate} is newer than last sync (${wm.newestDateSynced}). Run \`gscdump sync\` first, or pass --live.`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
if (startDate < wm.oldestDateSynced) {
|
|
481
|
+
logger.error(`Requested start=${startDate} is older than first sync (${wm.oldestDateSynced}). Run \`gscdump sync --start=${startDate}\` first, or pass --live.`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function runRawSqlMode(opts) {
|
|
486
|
+
if (!isKnownTable(opts.table)) {
|
|
487
|
+
logger.error(`Unknown table "${opts.table}". Known: ${allTables().join(", ")}`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
const ctx = await createCommandContext({
|
|
491
|
+
needsAuth: true,
|
|
492
|
+
needsStore: true
|
|
493
|
+
});
|
|
494
|
+
const siteUrl = await ctx.resolveSite(opts.site);
|
|
495
|
+
const store = ctx.store;
|
|
496
|
+
if (!opts.quiet) logger.info(`Running raw SQL over table "${opts.table}" for ${siteUrl}`);
|
|
497
|
+
const { rows, sql } = await store.runRawSql({
|
|
498
|
+
sql: opts.sql,
|
|
499
|
+
siteUrl,
|
|
500
|
+
table: opts.table,
|
|
501
|
+
...opts.searchType !== void 0 ? { searchType: opts.searchType } : {}
|
|
502
|
+
}).catch((e) => {
|
|
503
|
+
logger.error(`SQL failed: ${e.message}`);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
});
|
|
506
|
+
const payload = JSON.stringify({
|
|
507
|
+
sql,
|
|
508
|
+
total: rows.length,
|
|
509
|
+
data: rows
|
|
510
|
+
}, null, 2);
|
|
511
|
+
if (opts.output && opts.output !== "-") {
|
|
512
|
+
await fs.writeFile(opts.output, payload);
|
|
513
|
+
if (!opts.quiet) logger.info(`Written to ${opts.output}`);
|
|
514
|
+
} else console.log(payload);
|
|
515
|
+
}
|
|
516
|
+
function logProfile(spans) {
|
|
517
|
+
if (spans.length === 0) {
|
|
518
|
+
logger.warn("No profiling spans recorded (the executor may not be instrumented).");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const fmtMeta = (m) => m ? Object.entries(m).map(([k, v]) => `${k}=${v}`).join(" ") : "";
|
|
522
|
+
const row = (name, ms, meta) => ` ${name.padEnd(18)} ${`${ms}ms`.padStart(8)} ${fmtMeta(meta)}`.trimEnd();
|
|
523
|
+
const lines = spans.map((s) => row(s.name, s.ms, s.meta));
|
|
524
|
+
const total = spans.filter((s) => s.name === "manifest.list" || s.name === "executor.execute").reduce((n, s) => n + s.ms, 0);
|
|
525
|
+
logger.info(`Query timing breakdown:\n${lines.join("\n")}\n${row("total", total)}`);
|
|
526
|
+
}
|
|
527
|
+
async function writeOutput(opts) {
|
|
528
|
+
const content = opts.format === "csv" ? exportToCSV(opts.output) : JSON.stringify(opts.output, null, 2);
|
|
529
|
+
if (opts.path && opts.path !== "-") {
|
|
530
|
+
await fs.writeFile(opts.path, content);
|
|
531
|
+
if (!opts.quiet) logger.info(`Written to ${opts.path}`);
|
|
532
|
+
} else console.log(content);
|
|
533
|
+
}
|
|
534
|
+
function isKnownTable(name) {
|
|
535
|
+
return allTables().includes(name);
|
|
536
|
+
}
|
|
537
|
+
export { queryCommand };
|