gscdump 0.3.1 → 0.5.2
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 +7 -5
- package/dist/contracts.d.mts +136 -0
- package/dist/contracts.mjs +1 -0
- package/dist/driver.d.mts +78 -0
- package/dist/driver.mjs +1 -0
- package/dist/index.d.mts +158 -89
- package/dist/index.mjs +389 -257
- package/dist/normalize.d.mts +2 -0
- package/dist/normalize.mjs +16 -0
- package/dist/query/index.d.mts +73 -33
- package/dist/query/index.mjs +238 -190
- package/dist/query/plan.d.mts +130 -0
- package/dist/query/plan.mjs +296 -0
- package/dist/sitemap.d.mts +13 -0
- package/dist/sitemap.mjs +31 -0
- package/dist/tenant.d.mts +18 -0
- package/dist/tenant.mjs +18 -0
- package/dist/url.d.mts +9 -0
- package/dist/url.mjs +6 -0
- package/package.json +43 -9
- package/dist/analysis/index.d.mts +0 -513
- package/dist/analysis/index.mjs +0 -872
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
name: string;
|
|
3
|
+
'alpha-2': string;
|
|
4
|
+
'alpha-3': string;
|
|
5
|
+
'country-code': string;
|
|
6
|
+
}[];
|
|
7
|
+
declare const Devices: {
|
|
8
|
+
readonly MOBILE: "MOBILE";
|
|
9
|
+
readonly DESKTOP: "DESKTOP";
|
|
10
|
+
readonly TABLET: "TABLET";
|
|
11
|
+
};
|
|
12
|
+
type Device = typeof Devices[keyof typeof Devices];
|
|
13
|
+
declare const SearchTypes: {
|
|
14
|
+
readonly WEB: "web";
|
|
15
|
+
readonly IMAGE: "image";
|
|
16
|
+
readonly VIDEO: "video";
|
|
17
|
+
readonly NEWS: "news";
|
|
18
|
+
readonly DISCOVER: "discover";
|
|
19
|
+
readonly GOOGLE_NEWS: "googleNews";
|
|
20
|
+
};
|
|
21
|
+
type SearchType = typeof SearchTypes[keyof typeof SearchTypes];
|
|
22
|
+
declare const Countries: { [K in (typeof _default)[number]["alpha-3"]]: Lowercase<K> };
|
|
23
|
+
type Country = typeof Countries[keyof typeof Countries];
|
|
24
|
+
interface DimensionValueMap {
|
|
25
|
+
query: string;
|
|
26
|
+
queryCanonical: string;
|
|
27
|
+
page: string;
|
|
28
|
+
country: Country;
|
|
29
|
+
device: Device;
|
|
30
|
+
searchAppearance: string;
|
|
31
|
+
date: string;
|
|
32
|
+
}
|
|
33
|
+
type Dimension = keyof DimensionValueMap;
|
|
34
|
+
interface QueryParamValueMap {
|
|
35
|
+
searchType: SearchType;
|
|
36
|
+
}
|
|
37
|
+
type QueryParamName = keyof QueryParamValueMap;
|
|
38
|
+
type FilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'includingRegex' | 'excludingRegex';
|
|
39
|
+
type DateOperator = 'gte' | 'gt' | 'lte' | 'lt' | 'between';
|
|
40
|
+
type MetricOperator = 'metricGte' | 'metricGt' | 'metricLte' | 'metricLt' | 'metricBetween';
|
|
41
|
+
type SpecialOperator = 'topLevel';
|
|
42
|
+
interface InternalFilter {
|
|
43
|
+
dimension: Dimension | QueryParamName | Metric;
|
|
44
|
+
operator: FilterOperator | DateOperator | MetricOperator | SpecialOperator;
|
|
45
|
+
expression: string;
|
|
46
|
+
expression2?: string;
|
|
47
|
+
}
|
|
48
|
+
interface Filter<C = object> {
|
|
49
|
+
readonly __filterBrand: 'gscdump.Filter';
|
|
50
|
+
readonly _constraints: C;
|
|
51
|
+
readonly _filters: InternalFilter[];
|
|
52
|
+
readonly _nestedGroups?: Filter<any>[];
|
|
53
|
+
readonly _groupType?: 'and' | 'or';
|
|
54
|
+
}
|
|
55
|
+
type Metric = 'clicks' | 'impressions' | 'ctr' | 'position';
|
|
56
|
+
interface BuilderState {
|
|
57
|
+
dimensions: Dimension[];
|
|
58
|
+
metrics?: Metric[];
|
|
59
|
+
filter?: Filter<any>;
|
|
60
|
+
orderBy?: {
|
|
61
|
+
column: Metric | 'date';
|
|
62
|
+
dir: 'asc' | 'desc';
|
|
63
|
+
};
|
|
64
|
+
rowLimit?: number;
|
|
65
|
+
startRow?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Logical table / dataset identifier. Canonical across query builder + storage engine. */
|
|
68
|
+
type TableName = 'pages' | 'keywords' | 'countries' | 'devices' | 'page_keywords' | 'search_appearance';
|
|
69
|
+
type LogicalDataset = TableName;
|
|
70
|
+
type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
|
|
71
|
+
interface PlannerCapabilities {
|
|
72
|
+
regex?: boolean;
|
|
73
|
+
multiDataset?: boolean;
|
|
74
|
+
comparisonJoin?: boolean;
|
|
75
|
+
windowTotals?: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface LogicalDimensionFilter {
|
|
78
|
+
dimension: Dimension;
|
|
79
|
+
operator: FilterOperator;
|
|
80
|
+
expression: string;
|
|
81
|
+
expression2?: string;
|
|
82
|
+
}
|
|
83
|
+
interface LogicalMetricFilter {
|
|
84
|
+
metric: Metric;
|
|
85
|
+
operator: MetricOperator;
|
|
86
|
+
expression: number;
|
|
87
|
+
expression2?: number;
|
|
88
|
+
}
|
|
89
|
+
interface LogicalFilterLeaf {
|
|
90
|
+
kind: 'leaf';
|
|
91
|
+
filter: LogicalDimensionFilter;
|
|
92
|
+
}
|
|
93
|
+
interface LogicalFilterGroup {
|
|
94
|
+
kind: 'group';
|
|
95
|
+
groupType: 'and' | 'or';
|
|
96
|
+
children: LogicalFilterNode[];
|
|
97
|
+
}
|
|
98
|
+
type LogicalFilterNode = LogicalFilterLeaf | LogicalFilterGroup;
|
|
99
|
+
interface LogicalQueryPlan {
|
|
100
|
+
dataset: LogicalDataset;
|
|
101
|
+
dimensions: Dimension[];
|
|
102
|
+
groupByDimensions: Dimension[];
|
|
103
|
+
hasDate: boolean;
|
|
104
|
+
metrics: Metric[];
|
|
105
|
+
dateRange: {
|
|
106
|
+
startDate: string;
|
|
107
|
+
endDate: string;
|
|
108
|
+
};
|
|
109
|
+
dimensionFilters: LogicalDimensionFilter[];
|
|
110
|
+
dimensionFilterTree?: LogicalFilterNode;
|
|
111
|
+
metricFilters: LogicalMetricFilter[];
|
|
112
|
+
specialFilters: {
|
|
113
|
+
topLevel: boolean;
|
|
114
|
+
};
|
|
115
|
+
queryParams: Partial<Record<QueryParamName, string>>;
|
|
116
|
+
orderBy?: BuilderState['orderBy'];
|
|
117
|
+
rowLimit?: number;
|
|
118
|
+
startRow?: number;
|
|
119
|
+
}
|
|
120
|
+
interface LogicalComparisonPlan {
|
|
121
|
+
current: LogicalQueryPlan;
|
|
122
|
+
previous: LogicalQueryPlan;
|
|
123
|
+
comparisonFilter?: ComparisonFilter;
|
|
124
|
+
}
|
|
125
|
+
declare class UnsupportedLogicalCapabilityError extends Error {
|
|
126
|
+
constructor(capability: keyof PlannerCapabilities, context: string);
|
|
127
|
+
}
|
|
128
|
+
declare function buildLogicalPlan(state: BuilderState, capabilities?: PlannerCapabilities): LogicalQueryPlan;
|
|
129
|
+
declare function buildLogicalComparisonPlan(current: BuilderState, previous: BuilderState, capabilities?: PlannerCapabilities, comparisonFilter?: ComparisonFilter): LogicalComparisonPlan;
|
|
130
|
+
export { type ComparisonFilter, type LogicalComparisonPlan, type LogicalDataset, type LogicalDimensionFilter, LogicalFilterGroup, LogicalFilterLeaf, LogicalFilterNode, type LogicalMetricFilter, type LogicalQueryPlan, type PlannerCapabilities, type TableName, UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const DATE_OPERATORS = [
|
|
2
|
+
"gte",
|
|
3
|
+
"gt",
|
|
4
|
+
"lte",
|
|
5
|
+
"lt",
|
|
6
|
+
"between"
|
|
7
|
+
];
|
|
8
|
+
const METRIC_OPERATORS = [
|
|
9
|
+
"metricGte",
|
|
10
|
+
"metricGt",
|
|
11
|
+
"metricLte",
|
|
12
|
+
"metricLt",
|
|
13
|
+
"metricBetween"
|
|
14
|
+
];
|
|
15
|
+
const SPECIAL_OPERATORS = ["topLevel"];
|
|
16
|
+
const QUERY_PARAMS = ["searchType"];
|
|
17
|
+
const FILTER_METRICS = [
|
|
18
|
+
"clicks",
|
|
19
|
+
"impressions",
|
|
20
|
+
"ctr",
|
|
21
|
+
"position"
|
|
22
|
+
];
|
|
23
|
+
function isDateOperator(op) {
|
|
24
|
+
return DATE_OPERATORS.includes(op);
|
|
25
|
+
}
|
|
26
|
+
function isMetricOperator(op) {
|
|
27
|
+
return METRIC_OPERATORS.includes(op);
|
|
28
|
+
}
|
|
29
|
+
function isSpecialOperator(op) {
|
|
30
|
+
return SPECIAL_OPERATORS.includes(op);
|
|
31
|
+
}
|
|
32
|
+
function isQueryParam(value) {
|
|
33
|
+
return QUERY_PARAMS.includes(value);
|
|
34
|
+
}
|
|
35
|
+
function isMetric(value) {
|
|
36
|
+
return FILTER_METRICS.includes(value);
|
|
37
|
+
}
|
|
38
|
+
function isRegexOperator(op) {
|
|
39
|
+
return op === "includingRegex" || op === "excludingRegex";
|
|
40
|
+
}
|
|
41
|
+
const MS_PER_DAY = 864e5;
|
|
42
|
+
function toIsoDate(d) {
|
|
43
|
+
return d.toISOString().slice(0, 10);
|
|
44
|
+
}
|
|
45
|
+
function addDays(dateStr, n) {
|
|
46
|
+
return toIsoDate(new Date(Date.parse(`${dateStr}T00:00:00Z`) + n * MS_PER_DAY));
|
|
47
|
+
}
|
|
48
|
+
function isWireGroupType(type) {
|
|
49
|
+
return type === "and" || type === "or";
|
|
50
|
+
}
|
|
51
|
+
function convertWireLeaf(alt) {
|
|
52
|
+
if (!alt.column || !alt.type || isWireGroupType(alt.type)) return null;
|
|
53
|
+
const f = {
|
|
54
|
+
dimension: alt.column,
|
|
55
|
+
operator: alt.type,
|
|
56
|
+
expression: alt.type === "between" ? alt.from ?? "" : alt.value ?? ""
|
|
57
|
+
};
|
|
58
|
+
if (alt.type === "between" && alt.to) f.expression2 = alt.to;
|
|
59
|
+
return f;
|
|
60
|
+
}
|
|
61
|
+
function convertWireGroup(alt) {
|
|
62
|
+
if (!isWireGroupType(alt.type)) {
|
|
63
|
+
const leaf = convertWireLeaf(alt);
|
|
64
|
+
return leaf ? { _filters: [leaf] } : null;
|
|
65
|
+
}
|
|
66
|
+
const leaves = [];
|
|
67
|
+
const nested = [];
|
|
68
|
+
for (const child of alt.filters ?? []) if (isWireGroupType(child.type)) {
|
|
69
|
+
const sub = convertWireGroup(child);
|
|
70
|
+
if (sub) nested.push(sub);
|
|
71
|
+
} else {
|
|
72
|
+
const leaf = convertWireLeaf(child);
|
|
73
|
+
if (leaf) leaves.push(leaf);
|
|
74
|
+
}
|
|
75
|
+
if (leaves.length === 0 && nested.length === 0) return null;
|
|
76
|
+
return {
|
|
77
|
+
_filters: leaves,
|
|
78
|
+
_nestedGroups: nested.length > 0 ? nested : void 0,
|
|
79
|
+
_groupType: alt.type
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isWireFilter(input) {
|
|
83
|
+
if (!input || typeof input !== "object") return false;
|
|
84
|
+
const o = input;
|
|
85
|
+
if ("_filters" in o) return false;
|
|
86
|
+
return "type" in o && typeof o.type === "string" || "filters" in o && Array.isArray(o.filters);
|
|
87
|
+
}
|
|
88
|
+
function normalizeFilter(input) {
|
|
89
|
+
if (!input) return void 0;
|
|
90
|
+
if (isWireFilter(input)) return convertWireGroup(input) ?? void 0;
|
|
91
|
+
return input;
|
|
92
|
+
}
|
|
93
|
+
function extractSpecialFilters(filter) {
|
|
94
|
+
if (!filter) return {};
|
|
95
|
+
let startDate;
|
|
96
|
+
let endDate;
|
|
97
|
+
let searchType;
|
|
98
|
+
const otherFilters = [];
|
|
99
|
+
const cleanedNestedGroups = [];
|
|
100
|
+
for (const f of filter._filters) if (f.dimension === "date" && isDateOperator(f.operator)) switch (f.operator) {
|
|
101
|
+
case "gte":
|
|
102
|
+
startDate = f.expression;
|
|
103
|
+
break;
|
|
104
|
+
case "gt":
|
|
105
|
+
startDate = addDays(f.expression, 1);
|
|
106
|
+
break;
|
|
107
|
+
case "lte":
|
|
108
|
+
endDate = f.expression;
|
|
109
|
+
break;
|
|
110
|
+
case "lt":
|
|
111
|
+
endDate = addDays(f.expression, -1);
|
|
112
|
+
break;
|
|
113
|
+
case "between":
|
|
114
|
+
startDate = f.expression;
|
|
115
|
+
endDate = f.expression2;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
else if (isQueryParam(f.dimension)) {
|
|
119
|
+
if (f.dimension === "searchType") searchType = f.expression;
|
|
120
|
+
} else if (isMetricOperator(f.operator) || isSpecialOperator(f.operator)) otherFilters.push(f);
|
|
121
|
+
else otherFilters.push(f);
|
|
122
|
+
if (filter._nestedGroups) for (const nested of filter._nestedGroups) {
|
|
123
|
+
const extracted = extractSpecialFilters(nested);
|
|
124
|
+
if (extracted.startDate) startDate = extracted.startDate;
|
|
125
|
+
if (extracted.endDate) endDate = extracted.endDate;
|
|
126
|
+
if (extracted.searchType) searchType = extracted.searchType;
|
|
127
|
+
if (extracted.dimensionFilter) cleanedNestedGroups.push(extracted.dimensionFilter);
|
|
128
|
+
}
|
|
129
|
+
const dimensionFilter = otherFilters.length > 0 || cleanedNestedGroups.length > 0 ? {
|
|
130
|
+
...filter,
|
|
131
|
+
_filters: otherFilters,
|
|
132
|
+
_nestedGroups: cleanedNestedGroups.length > 0 ? cleanedNestedGroups : void 0
|
|
133
|
+
} : void 0;
|
|
134
|
+
return {
|
|
135
|
+
startDate,
|
|
136
|
+
endDate,
|
|
137
|
+
searchType,
|
|
138
|
+
dimensionFilter
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function extractDateRange(input) {
|
|
142
|
+
const { startDate, endDate } = extractSpecialFilters(normalizeFilter(input));
|
|
143
|
+
return {
|
|
144
|
+
startDate,
|
|
145
|
+
endDate
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function extractMetricFilters(input) {
|
|
149
|
+
const filter = normalizeFilter(input);
|
|
150
|
+
if (!filter) return [];
|
|
151
|
+
const metricFilters = filter._filters.filter((f) => isMetricOperator(f.operator));
|
|
152
|
+
const nested = filter._nestedGroups?.flatMap((g) => extractMetricFilters(g)) ?? [];
|
|
153
|
+
return [...metricFilters, ...nested];
|
|
154
|
+
}
|
|
155
|
+
function extractSpecialOperatorFilters(input) {
|
|
156
|
+
const filter = normalizeFilter(input);
|
|
157
|
+
if (!filter) return [];
|
|
158
|
+
const special = filter._filters.filter((f) => isSpecialOperator(f.operator));
|
|
159
|
+
const nested = filter._nestedGroups?.flatMap((g) => extractSpecialOperatorFilters(g)) ?? [];
|
|
160
|
+
return [...special, ...nested];
|
|
161
|
+
}
|
|
162
|
+
var UnsupportedLogicalCapabilityError = class extends Error {
|
|
163
|
+
constructor(capability, context) {
|
|
164
|
+
super(`${context} requires ${capability} capability`);
|
|
165
|
+
this.name = "UnsupportedLogicalCapabilityError";
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function collectInternalFilters(filter) {
|
|
169
|
+
if (!filter || !("_filters" in filter)) return [];
|
|
170
|
+
const flat = filter._filters;
|
|
171
|
+
const nested = filter._nestedGroups?.flatMap((group) => collectInternalFilters(group)) ?? [];
|
|
172
|
+
return [...flat, ...nested];
|
|
173
|
+
}
|
|
174
|
+
function inferDataset(dimensions, filterDims = []) {
|
|
175
|
+
const allDims = new Set([...dimensions, ...filterDims]);
|
|
176
|
+
const has = (dimension) => allDims.has(dimension);
|
|
177
|
+
if (has("searchAppearance")) return "search_appearance";
|
|
178
|
+
if (has("page") && (has("query") || has("queryCanonical"))) return "page_keywords";
|
|
179
|
+
if (has("query") || has("queryCanonical")) return "keywords";
|
|
180
|
+
if (has("page")) return "pages";
|
|
181
|
+
if (has("country")) return "countries";
|
|
182
|
+
if (has("device")) return "devices";
|
|
183
|
+
return "keywords";
|
|
184
|
+
}
|
|
185
|
+
function requireCapability(capabilities, capability, enabled, context) {
|
|
186
|
+
if (enabled && !capabilities?.[capability]) throw new UnsupportedLogicalCapabilityError(capability, context);
|
|
187
|
+
}
|
|
188
|
+
function isDimensionLeaf(filter) {
|
|
189
|
+
if (isMetric(filter.dimension)) return false;
|
|
190
|
+
if (filter.dimension === "date" && isDateOperator(filter.operator)) return false;
|
|
191
|
+
if (filter.operator === "topLevel" || filter.operator.startsWith("metric")) return false;
|
|
192
|
+
if (isQueryParam(filter.dimension)) return false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
function toLogicalDimensionFilter(filter) {
|
|
196
|
+
return {
|
|
197
|
+
dimension: filter.dimension,
|
|
198
|
+
operator: filter.operator,
|
|
199
|
+
expression: filter.expression,
|
|
200
|
+
expression2: filter.expression2
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function buildDimensionFilterTree(filter, capabilities) {
|
|
204
|
+
if (!filter || !("_filters" in filter)) return void 0;
|
|
205
|
+
const groupType = filter._groupType ?? "and";
|
|
206
|
+
const children = [];
|
|
207
|
+
for (const f of filter._filters) {
|
|
208
|
+
if (!isDimensionLeaf(f)) continue;
|
|
209
|
+
requireCapability(capabilities, "regex", isRegexOperator(f.operator), "logical plan");
|
|
210
|
+
children.push({
|
|
211
|
+
kind: "leaf",
|
|
212
|
+
filter: toLogicalDimensionFilter(f)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
for (const g of filter._nestedGroups ?? []) {
|
|
216
|
+
const sub = buildDimensionFilterTree(g, capabilities);
|
|
217
|
+
if (sub) children.push(sub);
|
|
218
|
+
}
|
|
219
|
+
if (children.length === 0) return void 0;
|
|
220
|
+
if (children.length === 1) return children[0];
|
|
221
|
+
return {
|
|
222
|
+
kind: "group",
|
|
223
|
+
groupType,
|
|
224
|
+
children
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function buildLogicalPlan(state, capabilities = {}) {
|
|
228
|
+
const normalizedFilter = normalizeFilter(state.filter);
|
|
229
|
+
const { startDate, endDate } = extractDateRange(normalizedFilter);
|
|
230
|
+
if (!startDate || !endDate) throw new Error("logical plan requires date range (use between(date, ...) or gte/lte)");
|
|
231
|
+
const allFilters = collectInternalFilters(normalizedFilter);
|
|
232
|
+
const metricFilters = extractMetricFilters(normalizedFilter);
|
|
233
|
+
const specialFilters = extractSpecialOperatorFilters(normalizedFilter);
|
|
234
|
+
const queryParams = {};
|
|
235
|
+
const dimensionFilters = [];
|
|
236
|
+
for (const filter of allFilters) {
|
|
237
|
+
if (isMetric(filter.dimension)) continue;
|
|
238
|
+
if (filter.dimension === "date" && isDateOperator(filter.operator)) continue;
|
|
239
|
+
if (filter.operator === "topLevel" || filter.operator.startsWith("metric")) continue;
|
|
240
|
+
if (isQueryParam(filter.dimension)) {
|
|
241
|
+
queryParams[filter.dimension] = filter.expression;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const operator = filter.operator;
|
|
245
|
+
requireCapability(capabilities, "regex", isRegexOperator(operator), "logical plan");
|
|
246
|
+
dimensionFilters.push({
|
|
247
|
+
dimension: filter.dimension,
|
|
248
|
+
operator,
|
|
249
|
+
expression: filter.expression,
|
|
250
|
+
expression2: filter.expression2
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const dimensionFilterTree = buildDimensionFilterTree(normalizedFilter, capabilities);
|
|
254
|
+
const filterDims = dimensionFilters.map((filter) => filter.dimension);
|
|
255
|
+
return {
|
|
256
|
+
dataset: inferDataset(state.dimensions, filterDims),
|
|
257
|
+
dimensions: [...state.dimensions],
|
|
258
|
+
groupByDimensions: state.dimensions.filter((d) => d !== "date"),
|
|
259
|
+
hasDate: state.dimensions.includes("date"),
|
|
260
|
+
metrics: state.metrics ? [...state.metrics] : [
|
|
261
|
+
"clicks",
|
|
262
|
+
"impressions",
|
|
263
|
+
"ctr",
|
|
264
|
+
"position"
|
|
265
|
+
],
|
|
266
|
+
dateRange: {
|
|
267
|
+
startDate,
|
|
268
|
+
endDate
|
|
269
|
+
},
|
|
270
|
+
dimensionFilters,
|
|
271
|
+
dimensionFilterTree,
|
|
272
|
+
metricFilters: metricFilters.map((filter) => ({
|
|
273
|
+
metric: filter.dimension,
|
|
274
|
+
operator: filter.operator,
|
|
275
|
+
expression: Number(filter.expression),
|
|
276
|
+
expression2: filter.expression2 == null ? void 0 : Number(filter.expression2)
|
|
277
|
+
})),
|
|
278
|
+
specialFilters: { topLevel: specialFilters.some((filter) => filter.operator === "topLevel") },
|
|
279
|
+
queryParams,
|
|
280
|
+
orderBy: state.orderBy,
|
|
281
|
+
rowLimit: state.rowLimit,
|
|
282
|
+
startRow: state.startRow
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function buildLogicalComparisonPlan(current, previous, capabilities = {}, comparisonFilter) {
|
|
286
|
+
requireCapability(capabilities, "comparisonJoin", true, "logical comparison plan");
|
|
287
|
+
const currentPlan = buildLogicalPlan(current, capabilities);
|
|
288
|
+
const previousPlan = buildLogicalPlan(previous, capabilities);
|
|
289
|
+
requireCapability(capabilities, "multiDataset", currentPlan.dataset !== previousPlan.dataset, "logical comparison plan");
|
|
290
|
+
return {
|
|
291
|
+
current: currentPlan,
|
|
292
|
+
previous: previousPlan,
|
|
293
|
+
comparisonFilter
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export { UnsupportedLogicalCapabilityError, buildLogicalComparisonPlan, buildLogicalPlan };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface DiscoverSitemapOptions {
|
|
2
|
+
/** User-Agent sent on the discovery requests. */
|
|
3
|
+
userAgent?: string;
|
|
4
|
+
/** AbortSignal threaded through fetches; defaults to a 10s timeout per call. */
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Try to discover a sitemap for `domain` by checking robots.txt for a
|
|
9
|
+
* `Sitemap:` directive, then a small set of common paths. Returns the first
|
|
10
|
+
* URL that responds with a 2xx, or `null`.
|
|
11
|
+
*/
|
|
12
|
+
declare function discoverSitemap(domain: string, options?: DiscoverSitemapOptions): Promise<string | null>;
|
|
13
|
+
export { DiscoverSitemapOptions, discoverSitemap };
|
package/dist/sitemap.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
2
|
+
const COMMON_PATHS = ["/sitemap.xml", "/sitemap_index.xml"];
|
|
3
|
+
const SITEMAP_DIRECTIVE_RE = /^Sitemap:\s*(\S+)/im;
|
|
4
|
+
async function discoverSitemap(domain, options = {}) {
|
|
5
|
+
const userAgent = options.userAgent ?? "gscdump sitemap fetcher";
|
|
6
|
+
const baseUrl = `https://${domain}`;
|
|
7
|
+
const signalFor = () => options.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
8
|
+
const robotsRes = await fetch(`${baseUrl}/robots.txt`, {
|
|
9
|
+
headers: { "User-Agent": userAgent },
|
|
10
|
+
signal: signalFor()
|
|
11
|
+
}).catch(() => null);
|
|
12
|
+
if (robotsRes?.ok) {
|
|
13
|
+
const match = (await robotsRes.text()).match(SITEMAP_DIRECTIVE_RE);
|
|
14
|
+
if (match?.[1]) {
|
|
15
|
+
if ((await fetch(match[1], {
|
|
16
|
+
method: "HEAD",
|
|
17
|
+
signal: signalFor()
|
|
18
|
+
}).catch(() => null))?.ok) return match[1];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const path of COMMON_PATHS) {
|
|
22
|
+
const url = `${baseUrl}${path}`;
|
|
23
|
+
if ((await fetch(url, {
|
|
24
|
+
method: "HEAD",
|
|
25
|
+
headers: { "User-Agent": userAgent },
|
|
26
|
+
signal: signalFor()
|
|
27
|
+
}).catch(() => null))?.ok) return url;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export { discoverSitemap };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare function encodeSiteId(siteUrl: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Best-effort inverse of `encodeSiteId` for the common prefixes. Lossy
|
|
4
|
+
* (`encodeSiteId` collapses non-word chars to `_` and strips trailing
|
|
5
|
+
* underscores), but round-trips domain properties + https origins cleanly —
|
|
6
|
+
* the only two shapes GSC hands out in practice.
|
|
7
|
+
*
|
|
8
|
+
* Returns the input unchanged when neither prefix is recognised, so callers
|
|
9
|
+
* can pass through canonical site URLs without branching.
|
|
10
|
+
*/
|
|
11
|
+
declare function decodeSiteId(encoded: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a siteUrl to the form Google APIs expect: domain properties get
|
|
14
|
+
* the `sc-domain:` prefix added if missing, URL properties pass through.
|
|
15
|
+
* Idempotent — safe to call on already-prefixed values.
|
|
16
|
+
*/
|
|
17
|
+
declare function normalizeSiteUrl(siteUrl: string): string;
|
|
18
|
+
export { decodeSiteId, encodeSiteId, normalizeSiteUrl };
|
package/dist/tenant.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const SC_DOMAIN_RE = /^sc-domain:/;
|
|
2
|
+
const PROTOCOL_RE = /^https?:\/\//;
|
|
3
|
+
const NON_ID_RE = /[^\w.-]/g;
|
|
4
|
+
const TRAILING_UNDERSCORES_RE = /_+$/;
|
|
5
|
+
function encodeSiteId(siteUrl) {
|
|
6
|
+
return siteUrl.replace(SC_DOMAIN_RE, "d_").replace(PROTOCOL_RE, "h_").replace(NON_ID_RE, "_").replace(TRAILING_UNDERSCORES_RE, "");
|
|
7
|
+
}
|
|
8
|
+
function decodeSiteId(encoded) {
|
|
9
|
+
if (encoded.startsWith("d_")) return `sc-domain:${encoded.slice(2)}`;
|
|
10
|
+
if (encoded.startsWith("h_")) return `https://${encoded.slice(2)}/`;
|
|
11
|
+
return encoded;
|
|
12
|
+
}
|
|
13
|
+
function normalizeSiteUrl(siteUrl) {
|
|
14
|
+
if (siteUrl.startsWith("sc-domain:")) return siteUrl;
|
|
15
|
+
if (siteUrl.startsWith("http://") || siteUrl.startsWith("https://")) return siteUrl;
|
|
16
|
+
return `sc-domain:${siteUrl}`;
|
|
17
|
+
}
|
|
18
|
+
export { decodeSiteId, encodeSiteId, normalizeSiteUrl };
|
package/dist/url.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equivalence key for matching two URLs that point at the same resource
|
|
3
|
+
* across protocol (http/https), www. prefix, trailing slash, and casing.
|
|
4
|
+
* Returns `null` for unparseable input.
|
|
5
|
+
*
|
|
6
|
+
* Example: `https://www.Example.com/Foo/` → `example.com/foo`
|
|
7
|
+
*/
|
|
8
|
+
declare function urlMatchKey(url: string): string | null;
|
|
9
|
+
export { urlMatchKey };
|
package/dist/url.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gscdump",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "Google Search Console API wrapper with typed query builder, streaming pagination, and SEO analysis functions",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -40,9 +40,33 @@
|
|
|
40
40
|
"types": "./dist/query/index.d.mts",
|
|
41
41
|
"import": "./dist/query/index.mjs"
|
|
42
42
|
},
|
|
43
|
-
"./
|
|
44
|
-
"types": "./dist/
|
|
45
|
-
"import": "./dist/
|
|
43
|
+
"./query/plan": {
|
|
44
|
+
"types": "./dist/query/plan.d.mts",
|
|
45
|
+
"import": "./dist/query/plan.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./contracts": {
|
|
48
|
+
"types": "./dist/contracts.d.mts",
|
|
49
|
+
"import": "./dist/contracts.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./driver": {
|
|
52
|
+
"types": "./dist/driver.d.mts",
|
|
53
|
+
"import": "./dist/driver.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./tenant": {
|
|
56
|
+
"types": "./dist/tenant.d.mts",
|
|
57
|
+
"import": "./dist/tenant.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./normalize": {
|
|
60
|
+
"types": "./dist/normalize.d.mts",
|
|
61
|
+
"import": "./dist/normalize.mjs"
|
|
62
|
+
},
|
|
63
|
+
"./url": {
|
|
64
|
+
"types": "./dist/url.d.mts",
|
|
65
|
+
"import": "./dist/url.mjs"
|
|
66
|
+
},
|
|
67
|
+
"./sitemap": {
|
|
68
|
+
"types": "./dist/sitemap.d.mts",
|
|
69
|
+
"import": "./dist/sitemap.mjs"
|
|
46
70
|
}
|
|
47
71
|
},
|
|
48
72
|
"main": "./dist/index.mjs",
|
|
@@ -53,9 +77,21 @@
|
|
|
53
77
|
"engines": {
|
|
54
78
|
"node": ">=18"
|
|
55
79
|
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@googleapis/indexing": "^6.0.1",
|
|
82
|
+
"@googleapis/searchconsole": "^6.0.1"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"@googleapis/indexing": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@googleapis/searchconsole": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
56
92
|
"dependencies": {
|
|
57
|
-
"dayjs": "^1.11.
|
|
58
|
-
"defu": "^6.1.
|
|
93
|
+
"dayjs": "^1.11.20",
|
|
94
|
+
"defu": "^6.1.7",
|
|
59
95
|
"ofetch": "^1.5.1",
|
|
60
96
|
"ufo": "^1.6.3"
|
|
61
97
|
},
|
|
@@ -68,8 +104,6 @@
|
|
|
68
104
|
"build": "obuild",
|
|
69
105
|
"typecheck": "tsc --noEmit",
|
|
70
106
|
"test": "vitest",
|
|
71
|
-
"test:run": "vitest run"
|
|
72
|
-
"test:e2e": "vitest run e2e/",
|
|
73
|
-
"test:real": "RUN_REAL_CREDENTIALS=true vitest run e2e/real-credentials.test.ts"
|
|
107
|
+
"test:run": "vitest run"
|
|
74
108
|
}
|
|
75
109
|
}
|